Skip to content

Commit 6f37b9b

Browse files
committed
chore: add configMaps component to tailnet
1 parent 21093c0 commit 6f37b9b

File tree

2 files changed

+426
-0
lines changed

2 files changed

+426
-0
lines changed

tailnet/configmaps.go

Lines changed: 274 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy