Skip to content

Commit d63bd21

Browse files
authored
chore: add vpn-daemon run subcommand for windows (#15526)
`coder vpn-daemon run` will instantiate a RPC connection with the specified pipe handles and communicate with the (yet to be implemented) parent process. The tests don't ensure that the tunnel is actually usable yet as the tunnel functionality isn't implemented, but it does make sure that the tunnel tries to read from the RPC pipe. Closes #14735
1 parent 8ca8e01 commit d63bd21

File tree

6 files changed

+283
-0
lines changed

6 files changed

+283
-0
lines changed

cli/root.go

+1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
125125
r.expCmd(),
126126
r.gitssh(),
127127
r.support(),
128+
r.vpnDaemon(),
128129
r.vscodeSSH(),
129130
r.workspaceAgent(),
130131
}

cli/vpndaemon.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package cli
2+
3+
import (
4+
"github.com/coder/serpent"
5+
)
6+
7+
func (r *RootCmd) vpnDaemon() *serpent.Command {
8+
cmd := &serpent.Command{
9+
Use: "vpn-daemon [subcommand]",
10+
Short: "VPN daemon commands used by Coder Desktop.",
11+
Hidden: true,
12+
Handler: func(inv *serpent.Invocation) error {
13+
return inv.Command.HelpHandler(inv)
14+
},
15+
Children: []*serpent.Command{
16+
r.vpnDaemonRun(),
17+
},
18+
}
19+
20+
return cmd
21+
}

cli/vpndaemon_other.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//go:build !windows
2+
3+
package cli
4+
5+
import (
6+
"golang.org/x/xerrors"
7+
8+
"github.com/coder/serpent"
9+
)
10+
11+
func (*RootCmd) vpnDaemonRun() *serpent.Command {
12+
cmd := &serpent.Command{
13+
Use: "run",
14+
Short: "Run the VPN daemon on Windows.",
15+
Middleware: serpent.Chain(
16+
serpent.RequireNArgs(0),
17+
),
18+
Handler: func(_ *serpent.Invocation) error {
19+
return xerrors.New("vpn-daemon subcommand is not supported on this platform")
20+
},
21+
}
22+
23+
return cmd
24+
}

cli/vpndaemon_windows.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//go:build windows
2+
3+
package cli
4+
5+
import (
6+
"golang.org/x/xerrors"
7+
8+
"cdr.dev/slog"
9+
"cdr.dev/slog/sloggers/sloghuman"
10+
"github.com/coder/coder/v2/vpn"
11+
"github.com/coder/serpent"
12+
)
13+
14+
func (r *RootCmd) vpnDaemonRun() *serpent.Command {
15+
var (
16+
rpcReadHandleInt int64
17+
rpcWriteHandleInt int64
18+
)
19+
20+
cmd := &serpent.Command{
21+
Use: "run",
22+
Short: "Run the VPN daemon on Windows.",
23+
Middleware: serpent.Chain(
24+
serpent.RequireNArgs(0),
25+
),
26+
Options: serpent.OptionSet{
27+
{
28+
Flag: "rpc-read-handle",
29+
Env: "CODER_VPN_DAEMON_RPC_READ_HANDLE",
30+
Description: "The handle for the pipe to read from the RPC connection.",
31+
Value: serpent.Int64Of(&rpcReadHandleInt),
32+
Required: true,
33+
},
34+
{
35+
Flag: "rpc-write-handle",
36+
Env: "CODER_VPN_DAEMON_RPC_WRITE_HANDLE",
37+
Description: "The handle for the pipe to write to the RPC connection.",
38+
Value: serpent.Int64Of(&rpcWriteHandleInt),
39+
Required: true,
40+
},
41+
},
42+
Handler: func(inv *serpent.Invocation) error {
43+
ctx := inv.Context()
44+
logger := inv.Logger.AppendSinks(sloghuman.Sink(inv.Stderr)).Leveled(slog.LevelDebug)
45+
46+
if rpcReadHandleInt < 0 || rpcWriteHandleInt < 0 {
47+
return xerrors.Errorf("rpc-read-handle (%v) and rpc-write-handle (%v) must be positive", rpcReadHandleInt, rpcWriteHandleInt)
48+
}
49+
if rpcReadHandleInt == rpcWriteHandleInt {
50+
return xerrors.Errorf("rpc-read-handle (%v) and rpc-write-handle (%v) must be different", rpcReadHandleInt, rpcWriteHandleInt)
51+
}
52+
53+
// We don't need to worry about duplicating the handles on Windows,
54+
// which is different from Unix.
55+
logger.Info(ctx, "opening bidirectional RPC pipe", slog.F("rpc_read_handle", rpcReadHandleInt), slog.F("rpc_write_handle", rpcWriteHandleInt))
56+
pipe, err := vpn.NewBidirectionalPipe(uintptr(rpcReadHandleInt), uintptr(rpcWriteHandleInt))
57+
if err != nil {
58+
return xerrors.Errorf("create bidirectional RPC pipe: %w", err)
59+
}
60+
defer pipe.Close()
61+
62+
logger.Info(ctx, "starting tunnel")
63+
tunnel, err := vpn.NewTunnel(ctx, logger, pipe)
64+
if err != nil {
65+
return xerrors.Errorf("create new tunnel for client: %w", err)
66+
}
67+
defer tunnel.Close()
68+
69+
<-ctx.Done()
70+
return nil
71+
},
72+
}
73+
74+
return cmd
75+
}

cli/vpndaemon_windows_test.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//go:build windows
2+
3+
package cli_test
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/coder/coder/v2/cli/clitest"
13+
"github.com/coder/coder/v2/testutil"
14+
)
15+
16+
func TestVPNDaemonRun(t *testing.T) {
17+
t.Parallel()
18+
19+
t.Run("InvalidFlags", func(t *testing.T) {
20+
t.Parallel()
21+
22+
cases := []struct {
23+
Name string
24+
Args []string
25+
ErrorContains string
26+
}{
27+
{
28+
Name: "NoReadHandle",
29+
Args: []string{"--rpc-write-handle", "10"},
30+
ErrorContains: "rpc-read-handle",
31+
},
32+
{
33+
Name: "NoWriteHandle",
34+
Args: []string{"--rpc-read-handle", "10"},
35+
ErrorContains: "rpc-write-handle",
36+
},
37+
{
38+
Name: "NegativeReadHandle",
39+
Args: []string{"--rpc-read-handle", "-1", "--rpc-write-handle", "10"},
40+
ErrorContains: "rpc-read-handle",
41+
},
42+
{
43+
Name: "NegativeWriteHandle",
44+
Args: []string{"--rpc-read-handle", "10", "--rpc-write-handle", "-1"},
45+
ErrorContains: "rpc-write-handle",
46+
},
47+
{
48+
Name: "SameHandles",
49+
Args: []string{"--rpc-read-handle", "10", "--rpc-write-handle", "10"},
50+
ErrorContains: "rpc-read-handle",
51+
},
52+
}
53+
54+
for _, c := range cases {
55+
c := c
56+
t.Run(c.Name, func(t *testing.T) {
57+
t.Parallel()
58+
ctx := testutil.Context(t, testutil.WaitLong)
59+
inv, _ := clitest.New(t, append([]string{"vpn-daemon", "run"}, c.Args...)...)
60+
err := inv.WithContext(ctx).Run()
61+
require.ErrorContains(t, err, c.ErrorContains)
62+
})
63+
}
64+
})
65+
66+
t.Run("StartsTunnel", func(t *testing.T) {
67+
t.Parallel()
68+
69+
r1, w1, err := os.Pipe()
70+
require.NoError(t, err)
71+
defer r1.Close()
72+
defer w1.Close()
73+
r2, w2, err := os.Pipe()
74+
require.NoError(t, err)
75+
defer r2.Close()
76+
defer w2.Close()
77+
78+
ctx := testutil.Context(t, testutil.WaitLong)
79+
inv, _ := clitest.New(t, "vpn-daemon", "run", "--rpc-read-handle", fmt.Sprint(r1.Fd()), "--rpc-write-handle", fmt.Sprint(w2.Fd()))
80+
waiter := clitest.StartWithWaiter(t, inv.WithContext(ctx))
81+
82+
// Send garbage which should cause the handshake to fail and the daemon
83+
// to exit.
84+
_, err = w1.Write([]byte("garbage"))
85+
require.NoError(t, err)
86+
waiter.Cancel()
87+
err = waiter.Wait()
88+
require.ErrorContains(t, err, "handshake failed")
89+
})
90+
91+
// TODO: once the VPN tunnel functionality is implemented, add tests that
92+
// actually try to instantiate a tunnel to a workspace
93+
}

vpn/pipe.go

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package vpn
2+
3+
import (
4+
"io"
5+
"os"
6+
7+
"github.com/hashicorp/go-multierror"
8+
"golang.org/x/xerrors"
9+
)
10+
11+
// BidirectionalPipe combines a pair of files that can be used for bidirectional
12+
// communication.
13+
type BidirectionalPipe struct {
14+
read *os.File
15+
write *os.File
16+
}
17+
18+
var _ io.ReadWriteCloser = BidirectionalPipe{}
19+
20+
// NewBidirectionalPipe creates a new BidirectionalPipe from the given file
21+
// descriptors.
22+
func NewBidirectionalPipe(readFd, writeFd uintptr) (BidirectionalPipe, error) {
23+
read := os.NewFile(readFd, "pipe_read")
24+
_, err := read.Stat()
25+
if err != nil {
26+
return BidirectionalPipe{}, xerrors.Errorf("stat pipe_read (fd=%v): %w", readFd, err)
27+
}
28+
write := os.NewFile(writeFd, "pipe_write")
29+
_, err = write.Stat()
30+
if err != nil {
31+
return BidirectionalPipe{}, xerrors.Errorf("stat pipe_write (fd=%v): %w", writeFd, err)
32+
}
33+
return BidirectionalPipe{
34+
read: read,
35+
write: write,
36+
}, nil
37+
}
38+
39+
// Read implements io.Reader. Data is read from the read pipe.
40+
func (b BidirectionalPipe) Read(p []byte) (int, error) {
41+
n, err := b.read.Read(p)
42+
if err != nil {
43+
return n, xerrors.Errorf("read from pipe_read (fd=%v): %w", b.read.Fd(), err)
44+
}
45+
return n, nil
46+
}
47+
48+
// Write implements io.Writer. Data is written to the write pipe.
49+
func (b BidirectionalPipe) Write(p []byte) (n int, err error) {
50+
n, err = b.write.Write(p)
51+
if err != nil {
52+
return n, xerrors.Errorf("write to pipe_write (fd=%v): %w", b.write.Fd(), err)
53+
}
54+
return n, nil
55+
}
56+
57+
// Close implements io.Closer. Both the read and write pipes are closed.
58+
func (b BidirectionalPipe) Close() error {
59+
var err error
60+
rErr := b.read.Close()
61+
if rErr != nil {
62+
err = multierror.Append(err, xerrors.Errorf("close pipe_read (fd=%v): %w", b.read.Fd(), rErr))
63+
}
64+
wErr := b.write.Close()
65+
if err != nil {
66+
err = multierror.Append(err, xerrors.Errorf("close pipe_write (fd=%v): %w", b.write.Fd(), wErr))
67+
}
68+
return err
69+
}

0 commit comments

Comments
 (0)