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

Lines changed: 273 additions & 0 deletions
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)
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