diff --git a/cli/vpndaemon_other.go b/cli/vpndaemon_other.go deleted file mode 100644 index 1526efb011..0000000000 --- a/cli/vpndaemon_other.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build !windows && !darwin - -package cli - -import ( - "golang.org/x/xerrors" - - "github.com/coder/serpent" -) - -func (*RootCmd) vpnDaemonRun() *serpent.Command { - cmd := &serpent.Command{ - Use: "run", - Short: "Run the VPN daemon on Windows.", - Middleware: serpent.Chain( - serpent.RequireNArgs(0), - ), - Handler: func(_ *serpent.Invocation) error { - return xerrors.New("vpn-daemon subcommand is not supported on this platform") - }, - } - - return cmd -} diff --git a/cli/vpndaemon_windows.go b/cli/vpndaemon_windows_linux_shared.go similarity index 87% rename from cli/vpndaemon_windows.go rename to cli/vpndaemon_windows_linux_shared.go index 2fa540e156..76e42cf865 100644 --- a/cli/vpndaemon_windows.go +++ b/cli/vpndaemon_windows_linux_shared.go @@ -1,4 +1,4 @@ -//go:build windows +//go:build windows || linux package cli @@ -11,7 +11,7 @@ import ( "github.com/coder/serpent" ) -func (r *RootCmd) vpnDaemonRun() *serpent.Command { +func (*RootCmd) vpnDaemonRun() *serpent.Command { var ( rpcReadHandleInt int64 rpcWriteHandleInt int64 @@ -19,7 +19,7 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command { cmd := &serpent.Command{ Use: "run", - Short: "Run the VPN daemon on Windows.", + Short: "Run the VPN daemon on Windows and Linux.", Middleware: serpent.Chain( serpent.RequireNArgs(0), ), @@ -53,8 +53,8 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command { return xerrors.Errorf("rpc-read-handle (%v) and rpc-write-handle (%v) must be different", rpcReadHandleInt, rpcWriteHandleInt) } - // We don't need to worry about duplicating the handles on Windows, - // which is different from Unix. + // The manager passes the read and write descriptors directly to the + // daemon, so we can open the RPC pipe from the raw values. logger.Info(ctx, "opening bidirectional RPC pipe", slog.F("rpc_read_handle", rpcReadHandleInt), slog.F("rpc_write_handle", rpcWriteHandleInt)) pipe, err := vpn.NewBidirectionalPipe(uintptr(rpcReadHandleInt), uintptr(rpcWriteHandleInt)) if err != nil { @@ -62,7 +62,7 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command { } defer pipe.Close() - logger.Info(ctx, "starting tunnel") + logger.Info(ctx, "starting VPN tunnel") tunnel, err := vpn.NewTunnel(ctx, logger, pipe, vpn.NewClient(), vpn.UseOSNetworkingStack()) if err != nil { return xerrors.Errorf("create new tunnel for client: %w", err) diff --git a/cli/vpndaemon_windows_test.go b/cli/vpndaemon_windows_linux_shared_test.go similarity index 74% rename from cli/vpndaemon_windows_test.go rename to cli/vpndaemon_windows_linux_shared_test.go index b03f74ee79..cfaf57f62f 100644 --- a/cli/vpndaemon_windows_test.go +++ b/cli/vpndaemon_windows_linux_shared_test.go @@ -1,4 +1,4 @@ -//go:build windows +//go:build windows || linux package cli_test @@ -67,22 +67,35 @@ func TestVPNDaemonRun(t *testing.T) { r1, w1, err := os.Pipe() require.NoError(t, err) - defer r1.Close() defer w1.Close() + r2, w2, err := os.Pipe() require.NoError(t, err) defer r2.Close() - defer w2.Close() + + // The daemon closes the handles passed via NewBidirectionalPipe. Since our + // CLI tests run in-process, pass duplicated handles so we can close the + // originals without risking a double-close on FD reuse. + rpcReadHandle := dupHandle(t, r1) + rpcWriteHandle := dupHandle(t, w2) + require.NoError(t, r1.Close()) + require.NoError(t, w2.Close()) ctx := testutil.Context(t, testutil.WaitLong) - inv, _ := clitest.New(t, "vpn-daemon", "run", "--rpc-read-handle", fmt.Sprint(r1.Fd()), "--rpc-write-handle", fmt.Sprint(w2.Fd())) + inv, _ := clitest.New(t, + "vpn-daemon", + "run", + "--rpc-read-handle", + fmt.Sprint(rpcReadHandle), + "--rpc-write-handle", + fmt.Sprint(rpcWriteHandle), + ) waiter := clitest.StartWithWaiter(t, inv.WithContext(ctx)) - // Send garbage which should cause the handshake to fail and the daemon - // to exit. - _, err = w1.Write([]byte("garbage")) + // Send an invalid header, including a newline delimiter, so the handshake + // fails without requiring context cancellation. + _, err = w1.Write([]byte("garbage\n")) require.NoError(t, err) - waiter.Cancel() err = waiter.Wait() require.ErrorContains(t, err, "handshake failed") }) diff --git a/cli/vpndaemon_windows_linux_shared_test_helpers_linux_test.go b/cli/vpndaemon_windows_linux_shared_test_helpers_linux_test.go new file mode 100644 index 0000000000..92ac21fdee --- /dev/null +++ b/cli/vpndaemon_windows_linux_shared_test_helpers_linux_test.go @@ -0,0 +1,19 @@ +//go:build linux + +package cli_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +func dupHandle(t *testing.T, f *os.File) uintptr { + t.Helper() + + dupFD, err := unix.Dup(int(f.Fd())) + require.NoError(t, err) + return uintptr(dupFD) +} diff --git a/cli/vpndaemon_windows_linux_shared_test_helpers_windows_test.go b/cli/vpndaemon_windows_linux_shared_test_helpers_windows_test.go new file mode 100644 index 0000000000..ee6d115be8 --- /dev/null +++ b/cli/vpndaemon_windows_linux_shared_test_helpers_windows_test.go @@ -0,0 +1,33 @@ +//go:build windows + +package cli_test + +import ( + "os" + "syscall" + "testing" + + "github.com/stretchr/testify/require" +) + +func dupHandle(t *testing.T, f *os.File) uintptr { + t.Helper() + + src := syscall.Handle(f.Fd()) + var dup syscall.Handle + + proc, err := syscall.GetCurrentProcess() + require.NoError(t, err) + + err = syscall.DuplicateHandle( + proc, + src, + proc, + &dup, + 0, + false, + syscall.DUPLICATE_SAME_ACCESS, + ) + require.NoError(t, err) + return uintptr(dup) +} diff --git a/vpn/tun.go b/vpn/tun.go deleted file mode 100644 index 28a99bb211..0000000000 --- a/vpn/tun.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !darwin && !windows - -package vpn - -import "cdr.dev/slog/v3" - -// This is a no-op on every platform except Darwin and Windows. -func GetNetworkingStack(_ *Tunnel, _ *StartRequest, _ slog.Logger) (NetworkStack, error) { - return NetworkStack{}, nil -} diff --git a/vpn/tun_linux.go b/vpn/tun_linux.go new file mode 100644 index 0000000000..5e3ed12a35 --- /dev/null +++ b/vpn/tun_linux.go @@ -0,0 +1,45 @@ +//go:build linux + +package vpn + +import ( + "golang.org/x/xerrors" + "tailscale.com/net/dns" + "tailscale.com/net/netmon" + "tailscale.com/net/tstun" + "tailscale.com/wgengine/router" + + "cdr.dev/slog/v3" + "github.com/coder/coder/v2/tailnet" +) + +const defaultTunName = "coder0" + +func GetNetworkingStack(_ *Tunnel, _ *StartRequest, logger slog.Logger) (NetworkStack, error) { + tunDev, tunName, err := tstun.New(tailnet.Logger(logger.Named("net.tun.device")), defaultTunName) + if err != nil { + return NetworkStack{}, xerrors.Errorf("create tun device: %w", err) + } + + wireguardMonitor, err := netmon.New(tailnet.Logger(logger.Named("net.wgmonitor"))) + if err != nil { + return NetworkStack{}, xerrors.Errorf("create wireguard monitor: %w", err) + } + + coderRouter, err := router.New(tailnet.Logger(logger.Named("net.router")), tunDev, wireguardMonitor) + if err != nil { + return NetworkStack{}, xerrors.Errorf("create router: %w", err) + } + + dnsConfigurator, err := dns.NewOSConfigurator(tailnet.Logger(logger.Named("net.dns")), tunName) + if err != nil { + return NetworkStack{}, xerrors.Errorf("create dns configurator: %w", err) + } + + return NetworkStack{ + WireguardMonitor: wireguardMonitor, + TUNDevice: tunDev, + Router: coderRouter, + DNSConfigurator: dnsConfigurator, + }, nil +}