Skip to content

Commit da58080

Browse files
committed
chore: add a tailscale router that uses the CoderVPN protocol
1 parent 8b5a18c commit da58080

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

tailnet/conn.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ type Options struct {
110110
// DNSConfigurator is optional, and is passed to the underlying wireguard
111111
// engine.
112112
DNSConfigurator dns.OSConfigurator
113+
// Router is optional, and is passed to the underlying wireguard engine.
114+
Router router.Router
113115
}
114116

115117
// TelemetrySink allows tailnet.Conn to send network telemetry to the Coder
@@ -183,6 +185,7 @@ func NewConn(options *Options) (conn *Conn, err error) {
183185
ListenPort: options.ListenPort,
184186
SetSubsystem: sys.Set,
185187
DNS: options.DNSConfigurator,
188+
Router: options.Router,
186189
})
187190
if err != nil {
188191
return nil, xerrors.Errorf("create wgengine: %w", err)

vpn/router.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package vpn
2+
3+
import (
4+
"net"
5+
"net/netip"
6+
7+
"tailscale.com/wgengine/router"
8+
)
9+
10+
func NewRouter(t *Tunnel) router.Router {
11+
return &vpnRouter{tunnel: t}
12+
}
13+
14+
type vpnRouter struct {
15+
tunnel *Tunnel
16+
}
17+
18+
func (*vpnRouter) Up() error {
19+
// On macOS, this needs to bring the interface up: `ifconfig <interface> up`.
20+
// On Windows, this is a no-op.
21+
// How can we/Do we need to initiate this from within the dylib?
22+
return nil
23+
}
24+
25+
func (v *vpnRouter) Set(cfg *router.Config) error {
26+
req := convertRouterConfig(cfg)
27+
return v.tunnel.ApplyNetworkSettings(v.tunnel.ctx, req)
28+
}
29+
30+
func (*vpnRouter) Close() error {
31+
// There's no cleanup that we need to initiate from within the dylib.
32+
return nil
33+
}
34+
35+
func convertRouterConfig(cfg *router.Config) *NetworkSettingsRequest {
36+
v4LocalAddrs := make([]string, 0)
37+
v6LocalAddrs := make([]string, 0)
38+
for _, addrs := range cfg.LocalAddrs {
39+
if addrs.Addr().Is4() {
40+
v4LocalAddrs = append(v4LocalAddrs, addrs.String())
41+
} else if addrs.Addr().Is6() {
42+
v6LocalAddrs = append(v6LocalAddrs, addrs.String())
43+
} else {
44+
continue
45+
}
46+
}
47+
v4Routes := make([]*NetworkSettingsRequest_IPv4Settings_IPv4Route, 0)
48+
v6Routes := make([]*NetworkSettingsRequest_IPv6Settings_IPv6Route, 0)
49+
for _, route := range cfg.Routes {
50+
if route.Addr().Is4() {
51+
//nolint:gosec // outdated rule
52+
v4Routes = append(v4Routes, convertToIPV4Route(&route))
53+
} else if route.Addr().Is6() {
54+
//nolint:gosec // outdated rule
55+
v6Routes = append(v6Routes, convertToIPV6Route(&route))
56+
} else {
57+
continue
58+
}
59+
}
60+
v4ExcludedRoutes := make([]*NetworkSettingsRequest_IPv4Settings_IPv4Route, 0)
61+
v6ExcludedRoutes := make([]*NetworkSettingsRequest_IPv6Settings_IPv6Route, 0)
62+
for _, route := range cfg.LocalRoutes {
63+
if route.Addr().Is4() {
64+
//nolint:gosec // outdated rule
65+
v4ExcludedRoutes = append(v4ExcludedRoutes, convertToIPV4Route(&route))
66+
} else if route.Addr().Is6() {
67+
//nolint:gosec // outdated rule
68+
v6ExcludedRoutes = append(v6ExcludedRoutes, convertToIPV6Route(&route))
69+
} else {
70+
continue
71+
}
72+
}
73+
74+
return &NetworkSettingsRequest{
75+
Mtu: uint32(cfg.NewMTU),
76+
Ipv4Settings: &NetworkSettingsRequest_IPv4Settings{
77+
Addrs: v4LocalAddrs,
78+
IncludedRoutes: v4Routes,
79+
ExcludedRoutes: v4ExcludedRoutes,
80+
},
81+
Ipv6Settings: &NetworkSettingsRequest_IPv6Settings{
82+
Addrs: v6LocalAddrs,
83+
IncludedRoutes: v6Routes,
84+
ExcludedRoutes: v6ExcludedRoutes,
85+
},
86+
TunnelOverheadBytes: 0, // N/A
87+
TunnelRemoteAddress: "", // N/A (?)
88+
}
89+
}
90+
91+
func convertToIPV4Route(route *netip.Prefix) *NetworkSettingsRequest_IPv4Settings_IPv4Route {
92+
return &NetworkSettingsRequest_IPv4Settings_IPv4Route{
93+
Destination: route.Addr().String(),
94+
Mask: prefixToSubnetMask(route),
95+
Router: "", // N/A
96+
}
97+
}
98+
99+
func convertToIPV6Route(route *netip.Prefix) *NetworkSettingsRequest_IPv6Settings_IPv6Route {
100+
return &NetworkSettingsRequest_IPv6Settings_IPv6Route{
101+
Destination: route.Addr().String(),
102+
PrefixLength: uint32(route.Bits()),
103+
Router: "", // N/A
104+
}
105+
}
106+
107+
func prefixToSubnetMask(prefix *netip.Prefix) string {
108+
maskBytes := net.CIDRMask(prefix.Masked().Bits(), net.IPv4len*8)
109+
return net.IP(maskBytes).String()
110+
}

vpn/router_internal_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package vpn
2+
3+
import (
4+
"net/netip"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
"tailscale.com/wgengine/router"
9+
)
10+
11+
func TestConvertRouterConfig(t *testing.T) {
12+
t.Parallel()
13+
14+
tests := []struct {
15+
name string
16+
cfg *router.Config
17+
expected *NetworkSettingsRequest
18+
}{
19+
{
20+
name: "IPv4 and IPv6 configuration",
21+
cfg: &router.Config{
22+
LocalAddrs: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32"), netip.MustParsePrefix("fd7a:115c:a1e0::1/128")},
23+
Routes: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24"), netip.MustParsePrefix("fd00::/64")},
24+
LocalRoutes: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("2001:db8::/32")},
25+
NewMTU: 1500,
26+
},
27+
expected: &NetworkSettingsRequest{
28+
Mtu: 1500,
29+
Ipv4Settings: &NetworkSettingsRequest_IPv4Settings{
30+
Addrs: []string{"100.64.0.1/32"},
31+
IncludedRoutes: []*NetworkSettingsRequest_IPv4Settings_IPv4Route{
32+
{Destination: "192.168.0.0", Mask: "255.255.255.0", Router: ""},
33+
},
34+
ExcludedRoutes: []*NetworkSettingsRequest_IPv4Settings_IPv4Route{
35+
{Destination: "10.0.0.0", Mask: "255.0.0.0", Router: ""},
36+
},
37+
},
38+
Ipv6Settings: &NetworkSettingsRequest_IPv6Settings{
39+
Addrs: []string{"fd7a:115c:a1e0::1/128"},
40+
IncludedRoutes: []*NetworkSettingsRequest_IPv6Settings_IPv6Route{
41+
{Destination: "fd00::", PrefixLength: 64, Router: ""},
42+
},
43+
ExcludedRoutes: []*NetworkSettingsRequest_IPv6Settings_IPv6Route{
44+
{Destination: "2001:db8::", PrefixLength: 32, Router: ""},
45+
},
46+
},
47+
},
48+
},
49+
{
50+
name: "Empty",
51+
cfg: &router.Config{},
52+
expected: &NetworkSettingsRequest{
53+
Ipv4Settings: &NetworkSettingsRequest_IPv4Settings{
54+
Addrs: []string{},
55+
IncludedRoutes: []*NetworkSettingsRequest_IPv4Settings_IPv4Route{},
56+
ExcludedRoutes: []*NetworkSettingsRequest_IPv4Settings_IPv4Route{},
57+
},
58+
Ipv6Settings: &NetworkSettingsRequest_IPv6Settings{
59+
Addrs: []string{},
60+
IncludedRoutes: []*NetworkSettingsRequest_IPv6Settings_IPv6Route{},
61+
ExcludedRoutes: []*NetworkSettingsRequest_IPv6Settings_IPv6Route{},
62+
},
63+
},
64+
},
65+
}
66+
//nolint:paralleltest // outdated rule
67+
for _, tt := range tests {
68+
t.Run(tt.name, func(t *testing.T) {
69+
t.Parallel()
70+
result := convertRouterConfig(tt.cfg)
71+
require.Equal(t, tt.expected, result)
72+
})
73+
}
74+
}

0 commit comments

Comments
 (0)