mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-13 17:09:07 +02:00
[client] Add nblink, an unprivileged forwarder into the network (#7955)
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.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
// Command nblink forwards local listeners into a NetBird network from an
|
||||
// unprivileged process.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/netbirdio/netbird/client/link"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
"github.com/netbirdio/netbird/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := rootCmd().Execute(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "nblink: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func rootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "nblink",
|
||||
Short: "Forward local ports into a NetBird network",
|
||||
Long: "nblink runs a NetBird peer in userspace and forwards local listeners\n" +
|
||||
"into the network. It needs no TUN device and no elevated privileges,\n" +
|
||||
"so it runs on managed machines and in rootless containers.\n\n" +
|
||||
"Every flag can also be set through an NB_-prefixed environment variable\n" +
|
||||
"of the same name, which is how the container image is configured.",
|
||||
Example: " nblink --forward 'http://8080=https://grafana.internal'\n" +
|
||||
" nblink --forward 'http://127.0.0.1:8080=https://grafana.internal' --setup-key file:/run/secrets/key",
|
||||
Version: version.NetbirdVersion(),
|
||||
// A stray positional argument is a mistake, most often a forward spec
|
||||
// that lost its --forward, and must not start the forwarder anyway.
|
||||
Args: cobra.NoArgs,
|
||||
// main prints the error, so cobra must not print it a second time.
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
}
|
||||
|
||||
raw := link.BindFlags(cmd)
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, _ []string) error {
|
||||
if err := link.SetFlagsFromEnvVars(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := raw.Resolve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := util.InitLog(cfg.LogLevel, util.LogConsole); err != nil {
|
||||
return fmt.Errorf("init log: %w", err)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
return link.Run(ctx, cfg)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/version"
|
||||
)
|
||||
|
||||
// TestMain clears the ambient NB_ environment before any test runs.
|
||||
//
|
||||
// Every flag reads an NB_-prefixed variable, so a developer who has nblink
|
||||
// configured in their own shell would otherwise hand these commands a
|
||||
// configuration the test never asked for: an inherited NB_FORWARD alone makes
|
||||
// the missing-forward case resolve successfully and the test fail. Clearing
|
||||
// here rather than in execute keeps t.Setenv working for the tests that do
|
||||
// want a variable set.
|
||||
func TestMain(m *testing.M) {
|
||||
for _, entry := range os.Environ() {
|
||||
name, _, _ := strings.Cut(entry, "=")
|
||||
if strings.HasPrefix(name, "NB_") {
|
||||
if err := os.Unsetenv(name); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unset %s: %v\n", name, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func execute(t *testing.T, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
cmd := rootCmd()
|
||||
var out strings.Builder
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&out)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
return out.String(), err
|
||||
}
|
||||
|
||||
// A forward spec that lost its --forward is the usual stray argument, and it
|
||||
// must not start a forwarder with whatever else was configured.
|
||||
func TestRejectsPositionalArguments(t *testing.T) {
|
||||
_, err := execute(t, "--check", "--forward", "http://8080=https://grafana.internal", "http://9090=https://prometheus.internal")
|
||||
require.Error(t, err, "a positional argument must be refused")
|
||||
assert.Contains(t, err.Error(), "unknown command", "the error must point at the stray argument")
|
||||
}
|
||||
|
||||
func TestRejectsInvalidEnvironmentValue(t *testing.T) {
|
||||
t.Setenv("NB_ALLOW_PUBLIC_BIND", "maybe")
|
||||
_, err := execute(t, "--check", "--forward", "http://8080=https://grafana.internal")
|
||||
require.Error(t, err, "an unparseable environment value must stop the start")
|
||||
assert.Contains(t, err.Error(), "NB_ALLOW_PUBLIC_BIND", "the error must name the variable")
|
||||
}
|
||||
|
||||
func TestEnvironmentConfiguresAFullCheck(t *testing.T) {
|
||||
t.Setenv("NB_FORWARD", "http://8080=https://grafana.internal,http://0.0.0.0:9090=https://prometheus.internal")
|
||||
t.Setenv("NB_ALLOW_PUBLIC_BIND", "true")
|
||||
t.Setenv("NB_CHECK", "true")
|
||||
_, err := execute(t)
|
||||
assert.NoError(t, err, "a complete environment configuration must pass the check without flags")
|
||||
}
|
||||
|
||||
func TestRequiresAForward(t *testing.T) {
|
||||
_, err := execute(t, "--check")
|
||||
require.Error(t, err, "a run with no forward must be refused")
|
||||
assert.Contains(t, err.Error(), "NB_FORWARD", "the error must name the environment variable too")
|
||||
}
|
||||
|
||||
func TestPrintsVersion(t *testing.T) {
|
||||
out, err := execute(t, "--version")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, out, version.NetbirdVersion(), "--version must print the build version")
|
||||
}
|
||||
Reference in New Issue
Block a user