mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(vpn): add Linux support for vpn-daemon and OS networking stack (#22051)
This change adds Linux support for Desktop VPN by aligning Linux behavior with the existing Windows daemon implementation and adding a Linux networking stack implementation. ### What changed - Consolidated the daemon command implementation into a shared file: - `cli/vpndaemon_windows_linux.go` (`//go:build windows || linux`) - Consolidated daemon tests into a shared file: - `cli/vpndaemon_windows_linux_test.go` (`//go:build windows || linux`) - Removed Linux-only duplicate daemon files: - `cli/vpndaemon_linux.go` - `cli/vpndaemon_linux_test.go` - Removed unsupported-platform stubs per current supported OS targets: - `cli/vpndaemon_other.go` - `vpn/tun.go` - Kept Linux networking stack implementation in: - `vpn/tun_linux.go` ### Notes - Linux now uses the same `rpc-read-handle` / `rpc-write-handle` flags and env vars as Windows. - The daemon logs to stderr (via CLI logger sinks), and does not forward logs over the RPC pipe.
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
-10
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user