Skip to content

Commit da115a4

Browse files
committed
chore: add configMaps component to tailnet
1 parent 9704400 commit da115a4

File tree

2 files changed

+428
-0
lines changed

2 files changed

+428
-0
lines changed

tailnet/configmaps.go

+276
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
package tailnet
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/netip"
7+
"sync"
8+
"time"
9+
10+
"github.com/google/uuid"
11+
"go4.org/netipx"
12+
"tailscale.com/net/dns"
13+
"tailscale.com/tailcfg"
14+
"tailscale.com/types/ipproto"
15+
"tailscale.com/types/key"
16+
"tailscale.com/types/netmap"
17+
"tailscale.com/wgengine"
18+
"tailscale.com/wgengine/filter"
19+
"tailscale.com/wgengine/router"
20+
"tailscale.com/wgengine/wgcfg"
21+
"tailscale.com/wgengine/wgcfg/nmcfg"
22+
23+
"cdr.dev/slog"
24+
"github.com/coder/coder/v2/tailnet/proto"
25+
)
26+
27+
// engineConfigurable is the subset of wgengine.Engine that we use for configuration.
28+
//
29+
// This allows us to test configuration code without faking the whole interface.
30+
type engineConfigurable interface {
31+
SetNetworkMap(*netmap.NetworkMap)
32+
Reconfig(*wgcfg.Config, *router.Config, *dns.Config, *tailcfg.Debug) error
33+
SetDERPMap(*tailcfg.DERPMap)
34+
SetFilter(*filter.Filter)
35+
}
36+
37+
type phase int
38+
39+
const (
40+
idle phase = iota
41+
configuring
42+
closed
43+
)
44+
45+
type configMaps struct {
46+
sync.Cond
47+
netmapDirty bool
48+
derpMapDirty bool
49+
filterDirty bool
50+
closing bool
51+
phase phase
52+
53+
engine engineConfigurable
54+
static netmap.NetworkMap
55+
peers map[uuid.UUID]*peerLifecycle
56+
addresses []netip.Prefix
57+
endpoints []tailcfg.Endpoint
58+
derpMap *proto.DERPMap
59+
logger slog.Logger
60+
}
61+
62+
func newConfigMaps(logger slog.Logger, engine engineConfigurable, nodeID tailcfg.NodeID, nodeKey key.NodePrivate, discoKey key.DiscoPublic, addresses []netip.Prefix) *configMaps {
63+
pubKey := nodeKey.Public()
64+
c := &configMaps{
65+
Cond: *(sync.NewCond(&sync.Mutex{})),
66+
logger: logger,
67+
engine: engine,
68+
static: netmap.NetworkMap{
69+
SelfNode: &tailcfg.Node{
70+
ID: nodeID,
71+
Key: pubKey,
72+
DiscoKey: discoKey,
73+
},
74+
NodeKey: pubKey,
75+
PrivateKey: nodeKey,
76+
PacketFilter: []filter.Match{{
77+
// Allow any protocol!
78+
IPProto: []ipproto.Proto{ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6, ipproto.SCTP},
79+
// Allow traffic sourced from anywhere.
80+
Srcs: []netip.Prefix{
81+
netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0),
82+
netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
83+
},
84+
// Allow traffic to route anywhere.
85+
Dsts: []filter.NetPortRange{
86+
{
87+
Net: netip.PrefixFrom(netip.AddrFrom4([4]byte{}), 0),
88+
Ports: filter.PortRange{
89+
First: 0,
90+
Last: 65535,
91+
},
92+
},
93+
{
94+
Net: netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0),
95+
Ports: filter.PortRange{
96+
First: 0,
97+
Last: 65535,
98+
},
99+
},
100+
},
101+
Caps: []filter.CapMatch{},
102+
}},
103+
},
104+
peers: make(map[uuid.UUID]*peerLifecycle),
105+
addresses: addresses,
106+
}
107+
go c.configLoop()
108+
return c
109+
}
110+
111+
func (c *configMaps) configLoop() {
112+
c.L.Lock()
113+
defer c.L.Unlock()
114+
defer func() {
115+
c.phase = closed
116+
c.Broadcast()
117+
}()
118+
for {
119+
for !(c.closing || c.netmapDirty || c.filterDirty || c.derpMapDirty) {
120+
c.phase = idle
121+
c.Wait()
122+
}
123+
if c.closing {
124+
return
125+
}
126+
// queue up the reconfiguration actions we will take while we have
127+
// the configMaps locked. We will execute them while unlocked to avoid
128+
// blocking during reconfig.
129+
actions := make([]func(), 0, 3)
130+
if c.derpMapDirty {
131+
derpMap := c.derpMapLocked()
132+
actions = append(actions, func() {
133+
c.engine.SetDERPMap(derpMap)
134+
})
135+
}
136+
if c.netmapDirty {
137+
nm := c.netMapLocked()
138+
actions = append(actions, func() {
139+
c.engine.SetNetworkMap(nm)
140+
c.reconfig(nm)
141+
})
142+
}
143+
if c.filterDirty {
144+
f := c.filterLocked()
145+
actions = append(actions, func() {
146+
c.engine.SetFilter(f)
147+
})
148+
}
149+
150+
c.netmapDirty = false
151+
c.filterDirty = false
152+
c.derpMapDirty = false
153+
c.phase = configuring
154+
c.Broadcast()
155+
func() {
156+
// this may look a little odd, but here we want this code to run
157+
// without the lock, and then relock when done
158+
c.L.Unlock()
159+
defer c.L.Lock()
160+
for _, a := range actions {
161+
a()
162+
}
163+
}()
164+
}
165+
}
166+
167+
func (c *configMaps) close() {
168+
c.L.Lock()
169+
defer c.L.Unlock()
170+
c.closing = true
171+
c.Broadcast()
172+
for c.phase != closed {
173+
c.Wait()
174+
}
175+
}
176+
177+
func (c *configMaps) netMapLocked() *netmap.NetworkMap {
178+
nm := new(netmap.NetworkMap)
179+
*nm = c.static
180+
181+
nm.Addresses = make([]netip.Prefix, len(c.addresses))
182+
copy(nm.Addresses, c.addresses)
183+
184+
nm.DERPMap = DERPMapFromProto(c.derpMap)
185+
nm.Peers = c.peerConfigLocked()
186+
nm.SelfNode.Addresses = nm.Addresses
187+
nm.SelfNode.AllowedIPs = nm.Addresses
188+
return nm
189+
}
190+
191+
func (c *configMaps) peerConfigLocked() []*tailcfg.Node {
192+
out := make([]*tailcfg.Node, 0, len(c.peers))
193+
for _, p := range c.peers {
194+
out = append(out, p.node.Clone())
195+
}
196+
return out
197+
}
198+
199+
func (c *configMaps) setAddresses(ips []netip.Prefix) {
200+
c.L.Lock()
201+
defer c.L.Unlock()
202+
if d := prefixesDifferent(c.addresses, ips); !d {
203+
return
204+
}
205+
c.addresses = make([]netip.Prefix, len(ips))
206+
copy(c.addresses, ips)
207+
c.netmapDirty = true
208+
c.filterDirty = true
209+
c.Broadcast()
210+
return
211+
}
212+
213+
func (c *configMaps) derpMapLocked() *tailcfg.DERPMap {
214+
m := DERPMapFromProto(c.derpMap)
215+
return m
216+
}
217+
218+
func (c *configMaps) reconfig(nm *netmap.NetworkMap) {
219+
cfg, err := nmcfg.WGCfg(nm, Logger(c.logger.Named("net.wgconfig")), netmap.AllowSingleHosts, "")
220+
if err != nil {
221+
// WGCfg never returns an error at the time this code was written. If it starts, returning
222+
// errors if/when we upgrade tailscale, we'll need to deal.
223+
c.logger.Critical(context.Background(), "update wireguard config failed", slog.Error(err))
224+
return
225+
}
226+
227+
rc := &router.Config{LocalAddrs: nm.Addresses}
228+
err = c.engine.Reconfig(cfg, rc, &dns.Config{}, &tailcfg.Debug{})
229+
if err != nil {
230+
if errors.Is(err, wgengine.ErrNoChanges) {
231+
return
232+
}
233+
c.logger.Error(context.Background(), "failed to reconfigure wireguard engine", slog.Error(err))
234+
}
235+
}
236+
237+
func (c *configMaps) filterLocked() *filter.Filter {
238+
localIPSet := netipx.IPSetBuilder{}
239+
for _, addr := range c.addresses {
240+
localIPSet.AddPrefix(addr)
241+
}
242+
localIPs, _ := localIPSet.IPSet()
243+
logIPSet := netipx.IPSetBuilder{}
244+
logIPs, _ := logIPSet.IPSet()
245+
return filter.New(
246+
c.static.PacketFilter,
247+
localIPs,
248+
logIPs,
249+
nil,
250+
Logger(c.logger.Named("net.packet-filter")),
251+
)
252+
}
253+
254+
type peerLifecycle struct {
255+
node *tailcfg.Node
256+
lastHandshake time.Time
257+
timer time.Timer
258+
}
259+
260+
// prefixesDifferent returns true if the two slices contain different prefixes
261+
// where order doesn't matter.
262+
func prefixesDifferent(a, b []netip.Prefix) bool {
263+
if len(a) != len(b) {
264+
return true
265+
}
266+
as := make(map[string]bool)
267+
for _, p := range a {
268+
as[p.String()] = true
269+
}
270+
for _, p := range b {
271+
if !as[p.String()] {
272+
return true
273+
}
274+
}
275+
return false
276+
}

0 commit comments

Comments
 (0)