Files
coder/vpn/client.go
T
Spike Curtis bddb808b25 chore: arrange imports in a standard way (#21452)
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example:

```
import (
	"context"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"golang.org/x/xerrors"
	"gopkg.in/natefinch/lumberjack.v2"

	"cdr.dev/slog/v3"
	"github.com/coder/coder/v2/codersdk/agentsdk"
	"github.com/coder/serpent"
)
```

3 groups: standard library, 3rd partly libs, Coder libs.

This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
2026-01-08 15:24:11 +04:00

214 lines
6.1 KiB
Go

package vpn
import (
"context"
"net/http"
"net/netip"
"net/url"
"time"
"github.com/google/uuid"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/xerrors"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/tailcfg"
"tailscale.com/wgengine/router"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/quartz"
"github.com/coder/websocket"
)
type Conn interface {
CurrentWorkspaceState() (tailnet.WorkspaceUpdate, error)
GetPeerDiagnostics(peerID uuid.UUID) tailnet.PeerDiagnostics
Ping(ctx context.Context, agentID uuid.UUID) (time.Duration, bool, *ipnstate.PingResult, error)
Node() *tailnet.Node
DERPMap() *tailcfg.DERPMap
Close() error
}
type vpnConn struct {
*tailnet.Conn
cancelFn func()
controller *tailnet.Controller
updatesCtrl *tailnet.TunnelAllWorkspaceUpdatesController
}
func (c *vpnConn) Ping(ctx context.Context, agentID uuid.UUID) (time.Duration, bool, *ipnstate.PingResult, error) {
return c.Conn.Ping(ctx, tailnet.TailscaleServicePrefix.AddrFromUUID(agentID))
}
func (c *vpnConn) CurrentWorkspaceState() (tailnet.WorkspaceUpdate, error) {
return c.updatesCtrl.CurrentState()
}
func (c *vpnConn) Close() error {
c.cancelFn()
<-c.controller.Closed()
return c.Conn.Close()
}
type client struct{}
type Client interface {
NewConn(ctx context.Context, serverURL *url.URL, token string, options *Options) (Conn, error)
}
func NewClient() Client {
return &client{}
}
type Options struct {
Headers http.Header
Logger slog.Logger
DNSConfigurator dns.OSConfigurator
Router router.Router
TUNDevice tun.Device
WireguardMonitor *netmon.Monitor
UpdateHandler tailnet.UpdatesHandler
}
type derpMapRewriter struct {
logger slog.Logger
serverURL *url.URL
}
var _ tailnet.DERPMapRewriter = &derpMapRewriter{}
// RewriteDERPMap implements tailnet.DERPMapRewriter. See
// tailnet.RewriteDERPMapDefaultRelay for more details on why this is necessary.
func (d *derpMapRewriter) RewriteDERPMap(derpMap *tailcfg.DERPMap) {
tailnet.RewriteDERPMapDefaultRelay(context.Background(), d.logger, derpMap, d.serverURL)
}
func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string, options *Options) (vpnC Conn, err error) {
if options == nil {
options = &Options{}
}
if options.Headers == nil {
options.Headers = http.Header{}
}
headers := options.Headers
sdk := codersdk.New(serverURL)
sdk.SetSessionToken(token)
sdk.HTTPClient.Transport = &codersdk.HeaderTransport{
Transport: http.DefaultTransport,
Header: headers.Clone(),
}
// New context, separate from initCtx. We don't want to cancel the
// connection if initCtx is canceled.
ctx, cancel := context.WithCancel(context.Background())
defer func() {
if err != nil {
cancel()
}
}()
rpcURL, err := sdk.URL.Parse("/api/v2/tailnet")
if err != nil {
return nil, xerrors.Errorf("parse rpc url: %w", err)
}
me, err := sdk.User(initCtx, codersdk.Me)
if err != nil {
return nil, xerrors.Errorf("get user: %w", err)
}
connInfo, err := workspacesdk.New(sdk).AgentConnectionInfoGeneric(initCtx)
if err != nil {
return nil, xerrors.Errorf("get connection info: %w", err)
}
// default to DNS suffix of "coder" if the server hasn't set it (might be too old).
dnsNameOptions := tailnet.DNSNameOptions{Suffix: tailnet.CoderDNSSuffix}
dnsMatch := tailnet.CoderDNSSuffix
if connInfo.HostnameSuffix != "" {
dnsNameOptions.Suffix = connInfo.HostnameSuffix
dnsMatch = connInfo.HostnameSuffix
}
headers.Set(codersdk.SessionTokenHeader, token)
dialer := workspacesdk.NewWebsocketDialer(options.Logger, rpcURL, &websocket.DialOptions{
HTTPClient: sdk.HTTPClient,
HTTPHeader: headers.Clone(),
CompressionMode: websocket.CompressionDisabled,
}, workspacesdk.WithWorkspaceUpdates(&proto.WorkspaceUpdatesRequest{
WorkspaceOwnerId: tailnet.UUIDToByteSlice(me.ID),
}))
derpMapRewriter := &derpMapRewriter{
logger: options.Logger,
serverURL: serverURL,
}
derpMapRewriter.RewriteDERPMap(connInfo.DERPMap)
clonedHeaders := headers.Clone()
ip := tailnet.CoderServicePrefix.RandomAddr()
conn, err := tailnet.NewConn(&tailnet.Options{
Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)},
DERPMap: connInfo.DERPMap,
DERPHeader: &clonedHeaders,
DERPForceWebSockets: connInfo.DERPForceWebSockets,
Logger: options.Logger,
BlockEndpoints: connInfo.DisableDirectConnections,
DNSConfigurator: options.DNSConfigurator,
Router: options.Router,
TUNDev: options.TUNDevice,
WireguardMonitor: options.WireguardMonitor,
DNSMatchDomain: dnsMatch,
})
if err != nil {
return nil, xerrors.Errorf("create tailnet: %w", err)
}
defer func() {
if err != nil {
_ = conn.Close()
}
}()
clk := quartz.NewReal()
controller := tailnet.NewController(options.Logger, dialer)
coordCtrl := tailnet.NewTunnelSrcCoordController(options.Logger, conn)
controller.ResumeTokenCtrl = tailnet.NewBasicResumeTokenController(options.Logger, clk)
controller.CoordCtrl = coordCtrl
controller.DERPCtrl = tailnet.NewBasicDERPController(options.Logger, derpMapRewriter, conn)
updatesCtrl := tailnet.NewTunnelAllWorkspaceUpdatesController(
options.Logger,
coordCtrl,
tailnet.WithDNS(conn, me.Username, dnsNameOptions),
tailnet.WithHandler(options.UpdateHandler),
)
controller.WorkspaceUpdatesCtrl = updatesCtrl
controller.Run(ctx)
options.Logger.Debug(ctx, "running tailnet API v2+ connector")
select {
case <-initCtx.Done():
return nil, xerrors.Errorf("timed out waiting for coordinator and derp map: %w", initCtx.Err())
case err = <-dialer.Connected():
if err != nil {
options.Logger.Error(ctx, "failed to connect to tailnet v2+ API", slog.Error(err))
return nil, xerrors.Errorf("start connector: %w", err)
}
options.Logger.Debug(ctx, "connected to tailnet v2+ API")
}
return &vpnConn{
Conn: conn,
cancelFn: cancel,
controller: controller,
updatesCtrl: updatesCtrl,
}, nil
}