Skip to content

Commit 8d938f7

Browse files
committed
chore: add configMaps component to tailnet
1 parent 9af1797 commit 8d938f7

File tree

2 files changed

+426
-0
lines changed

2 files changed

+426
-0
lines changed

tailnet/configmaps.go

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

0 commit comments

Comments
 (0)