Skip to content

Commit 79f824c

Browse files
committed
tests
1 parent 651ac20 commit 79f824c

File tree

6 files changed

+261
-13
lines changed

6 files changed

+261
-13
lines changed

cli/vpndaemon_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command {
6060
defer pipe.Close()
6161

6262
logger.Info(ctx, "starting tunnel")
63-
tunnel, err := vpn.NewTunnel(ctx, logger, pipe)
63+
tunnel, err := vpn.NewTunnel(ctx, logger, pipe, vpn.NewClient())
6464
if err != nil {
6565
return xerrors.Errorf("create new tunnel for client: %w", err)
6666
}

vpn/client.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"tailscale.com/net/dns"
1212
"tailscale.com/wgengine/router"
1313

14+
"github.com/tailscale/wireguard-go/tun"
15+
1416
"cdr.dev/slog"
1517
"github.com/coder/coder/v2/codersdk"
1618
"github.com/coder/coder/v2/codersdk/workspacesdk"
@@ -57,7 +59,7 @@ type Options struct {
5759
Logger slog.Logger
5860
DNSConfigurator dns.OSConfigurator
5961
Router router.Router
60-
TUNFileDescriptor int
62+
TUNFileDescriptor *int
6163
UpdateHandler tailnet.UpdatesHandler
6264
}
6365

@@ -66,10 +68,17 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string
6668
options = &Options{}
6769
}
6870

69-
// No-op on non-Darwin platforms.
70-
dev, err := makeTUN(options.TUNFileDescriptor)
71-
if err != nil {
72-
return nil, xerrors.Errorf("make TUN: %w", err)
71+
if options.Headers == nil {
72+
options.Headers = http.Header{}
73+
}
74+
75+
var dev tun.Device
76+
if options.TUNFileDescriptor != nil {
77+
// No-op on non-Darwin platforms.
78+
dev, err = makeTUN(*options.TUNFileDescriptor)
79+
if err != nil {
80+
return nil, xerrors.Errorf("make TUN: %w", err)
81+
}
7382
}
7483

7584
headers := options.Headers

vpn/client_internal_test.go

Lines changed: 0 additions & 1 deletion
This file was deleted.

vpn/client_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package vpn_test
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"net/url"
7+
"sync/atomic"
8+
"testing"
9+
"time"
10+
11+
"github.com/google/uuid"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
"go.uber.org/mock/gomock"
15+
"nhooyr.io/websocket"
16+
"tailscale.com/net/dns"
17+
"tailscale.com/tailcfg"
18+
19+
"github.com/coder/coder/v2/coderd/httpapi"
20+
"github.com/coder/coder/v2/codersdk"
21+
"github.com/coder/coder/v2/codersdk/workspacesdk"
22+
"github.com/coder/coder/v2/tailnet"
23+
"github.com/coder/coder/v2/tailnet/proto"
24+
"github.com/coder/coder/v2/tailnet/tailnettest"
25+
"github.com/coder/coder/v2/testutil"
26+
"github.com/coder/coder/v2/vpn"
27+
)
28+
29+
func TestClient_WorkspaceUpdates(t *testing.T) {
30+
t.Parallel()
31+
32+
ctx := testutil.Context(t, testutil.WaitShort)
33+
logger := testutil.Logger(t)
34+
35+
userID := uuid.UUID{1}
36+
wsID := uuid.UUID{2}
37+
peerID := uuid.UUID{3}
38+
39+
fCoord := tailnettest.NewFakeCoordinator()
40+
var coord tailnet.Coordinator = fCoord
41+
coordPtr := atomic.Pointer[tailnet.Coordinator]{}
42+
coordPtr.Store(&coord)
43+
ctrl := gomock.NewController(t)
44+
mProvider := tailnettest.NewMockWorkspaceUpdatesProvider(ctrl)
45+
46+
mSub := tailnettest.NewMockSubscription(ctrl)
47+
outUpdateCh := make(chan *proto.WorkspaceUpdate, 1)
48+
inUpdateCh := make(chan *proto.WorkspaceUpdate, 1)
49+
mProvider.EXPECT().Subscribe(gomock.Any(), userID).Times(1).Return(mSub, nil)
50+
mSub.EXPECT().Updates().MinTimes(1).Return(outUpdateCh)
51+
mSub.EXPECT().Close().Times(1).Return(nil)
52+
53+
svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{
54+
Logger: logger,
55+
CoordPtr: &coordPtr,
56+
DERPMapUpdateFrequency: time.Hour,
57+
DERPMapFn: func() *tailcfg.DERPMap { return &tailcfg.DERPMap{} },
58+
WorkspaceUpdatesProvider: mProvider,
59+
ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(),
60+
})
61+
require.NoError(t, err)
62+
63+
user := make(chan struct{})
64+
connInfo := make(chan struct{})
65+
serveErrCh := make(chan error)
66+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
67+
switch r.URL.Path {
68+
case "/api/v2/users/me":
69+
httpapi.Write(ctx, w, http.StatusOK, codersdk.User{
70+
ReducedUser: codersdk.ReducedUser{
71+
MinimalUser: codersdk.MinimalUser{
72+
ID: userID,
73+
},
74+
},
75+
})
76+
user <- struct{}{}
77+
78+
case "/api/v2/workspaceagents/connection":
79+
httpapi.Write(ctx, w, http.StatusOK, workspacesdk.AgentConnectionInfo{
80+
DisableDirectConnections: false,
81+
})
82+
connInfo <- struct{}{}
83+
84+
case "/api/v2/tailnet":
85+
// need 2.3 for WorkspaceUpdates RPC
86+
cVer := r.URL.Query().Get("version")
87+
assert.Equal(t, "2.3", cVer)
88+
89+
sws, err := websocket.Accept(w, r, nil)
90+
if !assert.NoError(t, err) {
91+
return
92+
}
93+
wsCtx, nc := codersdk.WebsocketNetConn(ctx, sws, websocket.MessageBinary)
94+
serveErrCh <- svc.ServeConnV2(wsCtx, nc, tailnet.StreamID{
95+
Name: "client",
96+
ID: peerID,
97+
// Auth can be nil as we use a mock update provider
98+
Auth: tailnet.ClientUserCoordinateeAuth{
99+
Auth: nil,
100+
},
101+
})
102+
default:
103+
http.NotFound(w, r)
104+
}
105+
}))
106+
t.Cleanup(server.Close)
107+
108+
svrURL, err := url.Parse(server.URL)
109+
require.NoError(t, err)
110+
connErrCh := make(chan error)
111+
connCh := make(chan vpn.Conn)
112+
go func() {
113+
conn, err := vpn.NewClient().NewConn(ctx, svrURL, "fakeToken", &vpn.Options{
114+
UpdateHandler: updateHandler(func(wu *proto.WorkspaceUpdate) error {
115+
inUpdateCh <- wu
116+
return nil
117+
}),
118+
DNSConfigurator: &noopConfigurator{},
119+
})
120+
connErrCh <- err
121+
connCh <- conn
122+
}()
123+
testutil.RequireRecvCtx(ctx, t, user)
124+
testutil.RequireRecvCtx(ctx, t, connInfo)
125+
err = testutil.RequireRecvCtx(ctx, t, connErrCh)
126+
require.NoError(t, err)
127+
conn := testutil.RequireRecvCtx(ctx, t, connCh)
128+
129+
// Send a workspace update
130+
update := &proto.WorkspaceUpdate{
131+
UpsertedWorkspaces: []*proto.Workspace{
132+
{
133+
Id: wsID[:],
134+
},
135+
},
136+
}
137+
testutil.RequireSendCtx(ctx, t, outUpdateCh, update)
138+
139+
// It'll be received by the update handler
140+
recvUpdate := testutil.RequireRecvCtx(ctx, t, inUpdateCh)
141+
require.Len(t, recvUpdate.UpsertedWorkspaces, 1)
142+
require.Equal(t, update.UpsertedWorkspaces[0].Id, recvUpdate.UpsertedWorkspaces[0].Id)
143+
144+
// And be reflected on the Conn's state
145+
require.Equal(t, &proto.WorkspaceUpdate{
146+
UpsertedWorkspaces: []*proto.Workspace{
147+
{
148+
Id: wsID[:],
149+
},
150+
},
151+
UpsertedAgents: []*proto.Agent{},
152+
}, conn.CurrentWorkspaceState())
153+
154+
// Close the conn
155+
conn.Close()
156+
err = testutil.RequireRecvCtx(ctx, t, serveErrCh)
157+
require.NoError(t, err)
158+
}
159+
160+
type updateHandler func(*proto.WorkspaceUpdate) error
161+
162+
func (h updateHandler) Update(u *proto.WorkspaceUpdate) error {
163+
return h(u)
164+
}
165+
166+
type noopConfigurator struct{}
167+
168+
func (*noopConfigurator) Close() error {
169+
return nil
170+
}
171+
172+
func (*noopConfigurator) GetBaseConfig() (dns.OSConfig, error) {
173+
return dns.OSConfig{}, nil
174+
}
175+
176+
func (*noopConfigurator) SetDNS(dns.OSConfig) error {
177+
return nil
178+
}
179+
180+
func (*noopConfigurator) SupportsSplitDNS() bool {
181+
return true
182+
}
183+
184+
var _ dns.OSConfigurator = (*noopConfigurator)(nil)

vpn/tunnel.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"golang.org/x/xerrors"
1717

18+
"github.com/coder/coder/v2/coderd/util/ptr"
1819
"github.com/coder/coder/v2/tailnet/proto"
1920

2021
"cdr.dev/slog"
@@ -187,7 +188,7 @@ func (t *Tunnel) start(req *StartRequest) error {
187188
Logger: t.logger,
188189
DNSConfigurator: NewDNSConfigurator(t),
189190
Router: NewRouter(t),
190-
TUNFileDescriptor: int(req.GetTunnelFileDescriptor()),
191+
TUNFileDescriptor: ptr.Ref(int(req.GetTunnelFileDescriptor())),
191192
UpdateHandler: t,
192193
},
193194
)

vpn/tunnel_internal_test.go

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ func TestTunnel_PeerUpdate(t *testing.T) {
129129

130130
errCh := make(chan error, 1)
131131
var resp *TunnelMessage
132-
// When: we start the tunnel
133132
go func() {
134133
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
135134
Msg: &ManagerMessage_Start{
@@ -143,9 +142,7 @@ func TestTunnel_PeerUpdate(t *testing.T) {
143142
resp = r
144143
errCh <- err
145144
}()
146-
// Then: `NewConn` is called,
147145
testutil.RequireSendCtx(ctx, t, client.ch, conn)
148-
// And: a response is received
149146
err := testutil.RequireRecvCtx(ctx, t, errCh)
150147
require.NoError(t, err)
151148
_, ok := resp.Msg.(*TunnelMessage_Start)
@@ -185,22 +182,80 @@ func TestTunnel_PeerUpdate(t *testing.T) {
185182
require.Equal(t, []byte("2"), resp.GetPeerUpdate().UpsertedWorkspaces[1].Id)
186183
}
187184

185+
func TestTunnel_NetworkSettings(t *testing.T) {
186+
t.Parallel()
187+
188+
ctx := testutil.Context(t, testutil.WaitShort)
189+
190+
client := newFakeClient(ctx, t)
191+
conn := newFakeConn(nil)
192+
193+
tun, mgr := setupTunnel(t, ctx, client)
194+
195+
errCh := make(chan error, 1)
196+
var resp *TunnelMessage
197+
go func() {
198+
r, err := mgr.unaryRPC(ctx, &ManagerMessage{
199+
Msg: &ManagerMessage_Start{
200+
Start: &StartRequest{
201+
TunnelFileDescriptor: 2,
202+
CoderUrl: "https://coder.example.com",
203+
ApiToken: "fakeToken",
204+
},
205+
},
206+
})
207+
resp = r
208+
errCh <- err
209+
}()
210+
testutil.RequireSendCtx(ctx, t, client.ch, conn)
211+
err := testutil.RequireRecvCtx(ctx, t, errCh)
212+
require.NoError(t, err)
213+
_, ok := resp.Msg.(*TunnelMessage_Start)
214+
require.True(t, ok)
215+
216+
// When: we inform the tunnel of network settings
217+
go func() {
218+
err := tun.ApplyNetworkSettings(ctx, &NetworkSettingsRequest{
219+
Mtu: 1200,
220+
})
221+
errCh <- err
222+
}()
223+
// Then: the tunnel sends a NetworkSettings message
224+
req := testutil.RequireRecvCtx(ctx, t, mgr.requests)
225+
require.NotNil(t, req.msg.Rpc)
226+
require.Equal(t, uint32(1200), req.msg.GetNetworkSettings().Mtu)
227+
go func() {
228+
testutil.RequireSendCtx(ctx, t, mgr.sendCh, &ManagerMessage{
229+
Rpc: &RPC{ResponseTo: req.msg.Rpc.MsgId},
230+
Msg: &ManagerMessage_NetworkSettings{
231+
NetworkSettings: &NetworkSettingsResponse{
232+
Success: true,
233+
},
234+
},
235+
})
236+
}()
237+
// And: `ApplyNetworkSettings` returns without error once the manager responds
238+
err = testutil.RequireRecvCtx(ctx, t, errCh)
239+
require.NoError(t, err)
240+
}
241+
188242
//nolint:revive // t takes precedence
189243
func setupTunnel(t *testing.T, ctx context.Context, client *fakeClient) (*Tunnel, *speaker[*ManagerMessage, *TunnelMessage, TunnelMessage]) {
190244
mp, tp := net.Pipe()
191245
t.Cleanup(func() { _ = mp.Close() })
192246
t.Cleanup(func() { _ = tp.Close() })
247+
logger := testutil.Logger(t)
193248

194249
var tun *Tunnel
195250
var mgr *speaker[*ManagerMessage, *TunnelMessage, TunnelMessage]
196251
errCh := make(chan error, 2)
197252
go func() {
198-
tunnel, err := NewTunnel(ctx, testutil.Logger(t).Named("tunnel"), tp, client)
253+
tunnel, err := NewTunnel(ctx, logger.Named("tunnel"), tp, client)
199254
tun = tunnel
200255
errCh <- err
201256
}()
202257
go func() {
203-
manager, err := newSpeaker[*ManagerMessage, *TunnelMessage](ctx, testutil.Logger(t).Named("manager"), mp, SpeakerRoleManager, SpeakerRoleTunnel)
258+
manager, err := newSpeaker[*ManagerMessage, *TunnelMessage](ctx, logger.Named("manager"), mp, SpeakerRoleManager, SpeakerRoleTunnel)
204259
mgr = manager
205260
errCh <- err
206261
}()

0 commit comments

Comments
 (0)