mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-13 17:09:07 +02:00
Reaching an internal service meant installing the agent, which needs root for a TUN device. nblink runs the embedded client in netstack mode and forwards local HTTP listeners to upstreams inside the tunnel, so it works on managed machines and in rootless containers. Upstream names resolve in-process. Forwards bind loopback unless --allow-public-bind is given. The listener refuses rebound and cross-site browser requests and bounds stalled request bodies. It ships as release archives and a distroless image that runs under an arbitrary UID. An embedded client given no StatePath now keeps its state in memory rather than falling back to the installed agent's state file, which every embedded client in a proxy process also shared. The state manager gets an explicit in-memory store for it.
220 lines
7.0 KiB
Go
220 lines
7.0 KiB
Go
package link
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// newTestCommand wires the flags the same way the binary does, so the tests
|
|
// exercise the real flag and environment plumbing.
|
|
func newTestCommand() (*cobra.Command, *rawConfig) {
|
|
cmd := &cobra.Command{Use: "nblink"}
|
|
return cmd, BindFlags(cmd)
|
|
}
|
|
|
|
func TestFlagNameToEnvVar(t *testing.T) {
|
|
assert.Equal(t, "NB_FORWARD", FlagNameToEnvVar("forward"))
|
|
assert.Equal(t, "NB_ALLOW_PUBLIC_BIND", FlagNameToEnvVar("allow-public-bind"))
|
|
assert.Equal(t, "NB_MANAGEMENT_URL", FlagNameToEnvVar("management-url"))
|
|
}
|
|
|
|
func TestSetFlagsFromEnvVars(t *testing.T) {
|
|
cmd, raw := newTestCommand()
|
|
t.Setenv("NB_FORWARD", "http://8080=https://a.internal,http://8081=https://b.internal")
|
|
t.Setenv("NB_MANAGEMENT_URL", "https://mgmt.example:443")
|
|
|
|
require.NoError(t, SetFlagsFromEnvVars(cmd))
|
|
|
|
assert.Equal(t, []string{"http://8080=https://a.internal", "http://8081=https://b.internal"},
|
|
raw.forwards, "NB_FORWARD should split on commas into repeated forwards")
|
|
assert.Equal(t, "https://mgmt.example:443", raw.managementURL)
|
|
}
|
|
|
|
// A value given on the command line must win over the environment, otherwise a
|
|
// container's baked-in defaults could not be overridden at run time.
|
|
func TestFlagBeatsEnvVar(t *testing.T) {
|
|
cmd, raw := newTestCommand()
|
|
require.NoError(t, cmd.PersistentFlags().Set("log-level", "debug"))
|
|
t.Setenv("NB_LOG_LEVEL", "error")
|
|
|
|
require.NoError(t, SetFlagsFromEnvVars(cmd))
|
|
|
|
assert.Equal(t, "debug", raw.logLevel, "an explicit flag should not be overwritten by the environment")
|
|
}
|
|
|
|
// A container must not start with a configuration other than the one it was
|
|
// given, so an unparseable value stops startup instead of leaving the default.
|
|
func TestSetFlagsFromEnvVarsRejectsInvalidValue(t *testing.T) {
|
|
cmd, raw := newTestCommand()
|
|
t.Setenv("NB_ALLOW_PUBLIC_BIND", "yes-please")
|
|
|
|
err := SetFlagsFromEnvVars(cmd)
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "NB_ALLOW_PUBLIC_BIND", "the error should name the variable at fault")
|
|
assert.False(t, raw.allowPublicBind, "the default must not be silently kept in effect")
|
|
}
|
|
|
|
func TestResolveRequiresForward(t *testing.T) {
|
|
_, raw := newTestCommand()
|
|
|
|
_, err := raw.Resolve()
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "at least one --forward")
|
|
}
|
|
|
|
func TestResolveGatesPublicBind(t *testing.T) {
|
|
_, raw := newTestCommand()
|
|
raw.forwards = []string{"http://0.0.0.0:8080=https://grafana.internal"}
|
|
|
|
_, err := raw.Resolve()
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "allow-public-bind",
|
|
"binding a public address should require the explicit opt-in")
|
|
}
|
|
|
|
func TestResolveAllowsPublicBindWhenOptedIn(t *testing.T) {
|
|
_, raw := newTestCommand()
|
|
raw.forwards = []string{"http://0.0.0.0:8080=https://grafana.internal"}
|
|
raw.allowPublicBind = true
|
|
|
|
cfg, err := raw.Resolve()
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, cfg.Forwards, 1)
|
|
assert.Equal(t, "0.0.0.0:8080", cfg.Forwards[0].Listen)
|
|
}
|
|
|
|
func TestResolveRejectsDuplicateListener(t *testing.T) {
|
|
_, raw := newTestCommand()
|
|
raw.forwards = []string{
|
|
"http://8080=https://a.internal",
|
|
"http://127.0.0.1:8080=https://b.internal",
|
|
}
|
|
|
|
_, err := raw.Resolve()
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "already bound",
|
|
"two forwards on one address should be caught before binding")
|
|
}
|
|
|
|
// Every bad spec should be reported together, so fixing a container
|
|
// environment does not take one deploy per mistake.
|
|
func TestResolveReportsEveryProblem(t *testing.T) {
|
|
_, raw := newTestCommand()
|
|
raw.forwards = []string{
|
|
"http://70000=https://a.internal",
|
|
"gopher://8081=https://b.internal",
|
|
}
|
|
|
|
_, err := raw.Resolve()
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "out of range")
|
|
assert.Contains(t, err.Error(), "unknown scheme")
|
|
}
|
|
|
|
func TestResolveAllowedHosts(t *testing.T) {
|
|
_, raw := newTestCommand()
|
|
raw.forwards = []string{"http://0.0.0.0:8080=https://a.internal", "http://8081=https://b.internal"}
|
|
raw.allowPublicBind = true
|
|
raw.allowedHosts = []string{"NBLink.LAN.", "svc.ns.svc.cluster.local"}
|
|
|
|
cfg, err := raw.Resolve()
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"nblink.lan", "svc.ns.svc.cluster.local"}, cfg.AllowedHosts,
|
|
"allowed hosts must be stored in the form they are compared in")
|
|
for _, f := range cfg.Forwards {
|
|
assert.Equal(t, cfg.AllowedHosts, f.AllowedHosts, "every forward must carry the allowed hosts")
|
|
}
|
|
}
|
|
|
|
func TestResolveRejectsMalformedAllowedHosts(t *testing.T) {
|
|
_, raw := newTestCommand()
|
|
raw.forwards = []string{"http://8080=https://a.internal"}
|
|
raw.allowedHosts = []string{"http://nblink.lan", "nblink.lan:8080", "*.lan", "10.0.0.5", ""}
|
|
|
|
_, err := raw.Resolve()
|
|
|
|
require.Error(t, err)
|
|
for _, entry := range []string{`"http://nblink.lan"`, `"nblink.lan:8080"`, `"*.lan"`, `""`} {
|
|
assert.Contains(t, err.Error(), entry, "every malformed entry must be reported")
|
|
}
|
|
assert.Contains(t, err.Error(), "addresses are already accepted", "an address must be explained, not just refused")
|
|
}
|
|
|
|
func TestResolveSetupKeyFromFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "key")
|
|
// The trailing newline mirrors what a mounted secret usually contains.
|
|
require.NoError(t, os.WriteFile(path, []byte("A1B2C3D4-SETUP-KEY\n"), 0o600))
|
|
|
|
_, raw := newTestCommand()
|
|
raw.forwards = []string{"http://8080=https://a.internal"}
|
|
raw.setupKey = "file:" + path
|
|
|
|
cfg, err := raw.Resolve()
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "A1B2C3D4-SETUP-KEY", cfg.SetupKey, "a mounted secret should be trimmed")
|
|
}
|
|
|
|
// Each port-0 forward gets its own OS-assigned port, so sharing the spec is
|
|
// not a collision.
|
|
func TestResolveAllowsSeveralEphemeralPorts(t *testing.T) {
|
|
_, raw := newTestCommand()
|
|
raw.forwards = []string{
|
|
"http://0=https://a.internal",
|
|
"http://0=https://b.internal",
|
|
}
|
|
|
|
cfg, err := raw.Resolve()
|
|
|
|
require.NoError(t, err)
|
|
assert.Len(t, cfg.Forwards, 2, "two ephemeral forwards should both be kept")
|
|
}
|
|
|
|
func TestResolveSetupKeyFileEmpty(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "key")
|
|
require.NoError(t, os.WriteFile(path, []byte(" \n"), 0o600))
|
|
|
|
_, raw := newTestCommand()
|
|
raw.forwards = []string{"http://8080=https://a.internal"}
|
|
raw.setupKey = "file:" + path
|
|
|
|
_, err := raw.Resolve()
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "is empty",
|
|
"an empty secret should fail rather than fall back to interactive login")
|
|
}
|
|
|
|
func TestResolveSetupKeyFileMissing(t *testing.T) {
|
|
_, raw := newTestCommand()
|
|
raw.forwards = []string{"http://8080=https://a.internal"}
|
|
raw.setupKey = "file:" + filepath.Join(t.TempDir(), "absent")
|
|
|
|
_, err := raw.Resolve()
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "setup key")
|
|
}
|
|
|
|
func TestStatePathsFollowStateDir(t *testing.T) {
|
|
cfg := &Config{}
|
|
assert.Empty(t, cfg.ConfigPath(), "an unset state dir should keep identity in memory")
|
|
assert.Empty(t, cfg.StatePath())
|
|
|
|
cfg.StateDir = "/var/lib/nblink"
|
|
assert.Equal(t, filepath.Join("/var/lib/nblink", "config.json"), cfg.ConfigPath())
|
|
assert.Equal(t, filepath.Join("/var/lib/nblink", "state.json"), cfg.StatePath())
|
|
}
|