Skip to content

Commit 0ec3722

Browse files
committed
chore: add configMaps component to tailnet
1 parent 21093c0 commit 0ec3722

File tree

2 files changed

+425
-0
lines changed

2 files changed

+425
-0
lines changed

tailnet/configmaps.go

+273
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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+
// configLoop waits for the config to be dirty, then reconfigures the engine.
110+
// It is internal to configMaps
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+
156+
c.L.Unlock()
157+
for _, a := range actions {
158+
a()
159+
}
160+
c.L.Lock()
161+
}
162+
}
163+
164+
func (c *configMaps) close() {
165+
c.L.Lock()
166+
defer c.L.Unlock()
167+
c.closing = true
168+
c.Broadcast()
169+
for c.phase != closed {
170+
c.Wait()
171+
}
172+
}
173+
174+
func (c *configMaps) netMapLocked() *netmap.NetworkMap {
175+
nm := new(netmap.NetworkMap)
176+
*nm = c.static
177+
178+
nm.Addresses = make([]netip.Prefix, len(c.addresses))
179+
copy(nm.Addresses, c.addresses)
180+
181+
nm.DERPMap = DERPMapFromProto(c.derpMap)
182+
nm.Peers = c.peerConfigLocked()
183+
nm.SelfNode.Addresses = nm.Addresses
184+
nm.SelfNode.AllowedIPs = nm.Addresses
185+
return nm
186+
}
187+
188+
func (c *configMaps) peerConfigLocked() []*tailcfg.Node {
189+
out := make([]*tailcfg.Node, 0, len(c.peers))
190+
for _, p := range c.peers {
191+
out = append(out, p.node.Clone())
192+
}
193+
return out
194+
}
195+
196+
func (c *configMaps) setAddresses(ips []netip.Prefix) {
197+
c.L.Lock()
198+
defer c.L.Unlock()
199+
if d := prefixesDifferent(c.addresses, ips); !d {
200+
return
201+
}
202+
c.addresses = make([]netip.Prefix, len(ips))
203+
copy(c.addresses, ips)
204+
c.netmapDirty = true
205+
c.filterDirty = true
206+
c.Broadcast()
207+
}
208+
209+
func (c *configMaps) derpMapLocked() *tailcfg.DERPMap {
210+
m := DERPMapFromProto(c.derpMap)
211+
return m
212+
}
213+
214+
func (c *configMaps) reconfig(nm *netmap.NetworkMap) {
215+
cfg, err := nmcfg.WGCfg(nm, Logger(c.logger.Named("net.wgconfig")), netmap.AllowSingleHosts, "")
216+
if err != nil {
217+
// WGCfg never returns an error at the time this code was written. If it starts, returning
218+
// errors if/when we upgrade tailscale, we'll need to deal.
219+
c.logger.Critical(context.Background(), "update wireguard config failed", slog.Error(err))
220+
return
221+
}
222+
223+
rc := &router.Config{LocalAddrs: nm.Addresses}
224+
err = c.engine.Reconfig(cfg, rc, &dns.Config{}, &tailcfg.Debug{})
225+
if err != nil {
226+
if errors.Is(err, wgengine.ErrNoChanges) {
227+
return
228+
}
229+
c.logger.Error(context.Background(), "failed to reconfigure wireguard engine", slog.Error(err))
230+
}
231+
}
232+
233+
func (c *configMaps) filterLocked() *filter.Filter {
234+
localIPSet := netipx.IPSetBuilder{}
235+
for _, addr := range c.addresses {
236+
localIPSet.AddPrefix(addr)
237+
}
238+
localIPs, _ := localIPSet.IPSet()
239+
logIPSet := netipx.IPSetBuilder{}
240+
logIPs, _ := logIPSet.IPSet()
241+
return filter.New(
242+
c.static.PacketFilter,
243+
localIPs,
244+
logIPs,
245+
nil,
246+
Logger(c.logger.Named("net.packet-filter")),
247+
)
248+
}
249+
250+
type peerLifecycle struct {
251+
node *tailcfg.Node
252+
// TODO: implement timers to track lost peers
253+
// lastHandshake time.Time
254+
// timer time.Timer
255+
}
256+
257+
// prefixesDifferent returns true if the two slices contain different prefixes
258+
// where order doesn't matter.
259+
func prefixesDifferent(a, b []netip.Prefix) bool {
260+
if len(a) != len(b) {
261+
return true
262+
}
263+
as := make(map[string]bool)
264+
for _, p := range a {
265+
as[p.String()] = true
266+
}
267+
for _, p := range b {
268+
if !as[p.String()] {
269+
return true
270+
}
271+
}
272+
return false
273+
}

0 commit comments

Comments
 (0)