diff --git a/client/cmd/root.go b/client/cmd/root.go index eddf4e543..f5d417547 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -144,7 +144,7 @@ func init() { defaultDaemonAddr := "unix:///var/run/netbird.sock" if runtime.GOOS == "windows" { - defaultDaemonAddr = "npipe://netbird" + defaultDaemonAddr = windowsPipeDaemonAddr } rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp|npipe]://[path|host:port|name]") diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index a74ffaada..e9a82c36b 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -62,6 +62,15 @@ func (p *program) Start(svc service.Service) error { // Collect static system and platform information system.UpdateStaticInfoAsync() + // A daemon installed before named-pipe support uses the old loopback-TCP + // address as the daemon address. We migrate to a named pipe so an + // upgraded daemon enforces per-caller authorization instead of silently + // running on identity-less TCP. + if migrated, ok := migrateLegacyDaemonAddr(daemonAddr); ok { + log.Infof("legacy daemon address %q predates named-pipe support. listening on %q so per-caller authorization is enforced", daemonAddr, migrated) + daemonAddr = migrated + } + network, _, err := parseListenAddress(daemonAddr) if err != nil { return fmt.Errorf("parse daemon address: %w", err) diff --git a/client/cmd/service_params.go b/client/cmd/service_params.go index f25087a69..1cb5bcc75 100644 --- a/client/cmd/service_params.go +++ b/client/cmd/service_params.go @@ -125,6 +125,10 @@ func applyServiceParams(cmd *cobra.Command, params *serviceParams) { if !rootCmd.PersistentFlags().Changed("daemon-addr") && params.DaemonAddr != "" { daemonAddr = params.DaemonAddr + if migrated, ok := migrateLegacyDaemonAddr(daemonAddr); ok { + cmd.Printf("Migrating saved daemon address %q to %q so per-caller authorization can be enforced\n", daemonAddr, migrated) + daemonAddr = migrated + } } if !serviceCmd.PersistentFlags().Changed("json-socket") && params.JSONSocket != "" { diff --git a/client/cmd/service_socket.go b/client/cmd/service_socket.go index 99ed96a03..219cdee2c 100644 --- a/client/cmd/service_socket.go +++ b/client/cmd/service_socket.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "os" + "runtime" "strings" "syscall" "time" @@ -14,6 +15,31 @@ import ( log "github.com/sirupsen/logrus" ) +const ( + windowsPipeDaemonAddr = "npipe://netbird" + + // legacyWindowsDaemonAddr is the loopback-TCP address the Windows daemon used + // before named-pipe support. TCP exposes no peer-identity primitive, so the + // authorization interceptor cannot run over it. + legacyWindowsDaemonAddr = "tcp://127.0.0.1:41731" +) + +// migrateLegacyDaemonAddr upgrades the pre-named-pipe Windows daemon address to +// the pipe. Existing installs persist daemon addr, so on upgrade the daemon +// would otherwise keep listening on TCP and silently run without IPC +// authorization. Only the exact legacy default is rewritten, while a +// deliberately-chosen custom TCP address is left alone. +func migrateLegacyDaemonAddr(addr string) (string, bool) { + return migrateLegacyDaemonAddrForOS(runtime.GOOS, addr) +} + +func migrateLegacyDaemonAddrForOS(goos, addr string) (string, bool) { + if goos == "windows" && addr == legacyWindowsDaemonAddr { + return windowsPipeDaemonAddr, true + } + return addr, false +} + type socketListener struct { net.Listener network string diff --git a/client/cmd/service_socket_test.go b/client/cmd/service_socket_test.go new file mode 100644 index 000000000..c1cb109e9 --- /dev/null +++ b/client/cmd/service_socket_test.go @@ -0,0 +1,63 @@ +//go:build !ios && !android + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMigrateLegacyDaemonAddrForOS(t *testing.T) { + cases := []struct { + name string + goos string + addr string + want string + migrate bool + }{ + { + name: "windows legacy tcp migrates to pipe", + goos: "windows", + addr: legacyWindowsDaemonAddr, + want: windowsPipeDaemonAddr, + migrate: true, + }, + { + name: "windows pipe already migrated stays", + goos: "windows", + addr: windowsPipeDaemonAddr, + want: windowsPipeDaemonAddr, + migrate: false, + }, + { + name: "windows custom tcp left alone", + goos: "windows", + addr: "tcp://127.0.0.1:9999", + want: "tcp://127.0.0.1:9999", + migrate: false, + }, + { + name: "linux legacy-looking tcp not migrated", + goos: "linux", + addr: legacyWindowsDaemonAddr, + want: legacyWindowsDaemonAddr, + migrate: false, + }, + { + name: "linux unix socket untouched", + goos: "linux", + addr: "unix:///var/run/netbird.sock", + want: "unix:///var/run/netbird.sock", + migrate: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, ok := migrateLegacyDaemonAddrForOS(tc.goos, tc.addr) + assert.Equal(t, tc.want, got) + assert.Equal(t, tc.migrate, ok) + }) + } +}