Skip to content

Commit 74e5e8f

Browse files
committed
dns,hosttun: route MagicDNS through a synthetic address
Persist an IPv4 resolver address from the synthetic pool so host DNS configuration no longer depends on installing the quad-100 route. Serve DNS on both addresses, treat the compatibility route as optional, and reconcile Darwin and Linux TUN state from the kernel before and after changes so silent route command failures are detected immediately.
1 parent e48c25d commit 74e5e8f

16 files changed

Lines changed: 1266 additions & 310 deletions

‎cmd/tailmixd/main.go‎

Lines changed: 91 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
152152
return err
153153
}
154154
st.Profiles = stateProfiles(runtimeProfiles)
155+
if err := ensureHostIPs(&st); err != nil {
156+
return err
157+
}
155158
if err := store.Save(st); err != nil {
156159
return err
157160
}
@@ -251,6 +254,9 @@ func configureSyntheticPools(st *state.State, ipv4Override, ipv6Override string)
251254
if strings.TrimSpace(family.override) != "" && oldNormalized != normalized {
252255
st.Leases = discardSyntheticLeases(st.Leases, family.ipv6)
253256
*family.nat = netip.Addr{}
257+
if !family.ipv6 {
258+
st.DNSIP = netip.Addr{}
259+
}
254260
}
255261
*family.current = normalized
256262
}
@@ -286,8 +292,8 @@ func discardSyntheticLeases(leases []state.EffectiveLease, ipv6 bool) []state.Ef
286292
return out
287293
}
288294

289-
func ensureNATIPs(st *state.State) error {
290-
used := make(map[netip.Addr]bool, len(st.Leases)+2)
295+
func ensureHostIPs(st *state.State) error {
296+
used := make(map[netip.Addr]bool, len(st.Leases)+3)
291297
for _, lease := range st.Leases {
292298
if lease.EffectiveIP.IsValid() {
293299
used[lease.EffectiveIP] = true
@@ -307,33 +313,68 @@ func ensureNATIPs(st *state.State) error {
307313
return fmt.Errorf("parse effective %s pool %q: %w", family.name, family.poolRaw, err)
308314
}
309315
pool = pool.Masked()
310-
// Keep the prefix base unassigned. In particular, using the IPv4
311-
// network address as the aggregate TUN's point-to-point identity can
312-
// make Darwin route packets DNATed to that address back out the TUN
313-
// instead of delivering them locally.
314-
if current := *family.current; current.IsValid() && current != pool.Addr() && current.Is6() == family.ipv6 && pool.Contains(current) && !used[current] {
315-
used[current] = true
316+
if validReservedIP(*family.current, pool, family.ipv6, used) {
317+
used[*family.current] = true
316318
continue
317319
}
318-
*family.current = netip.Addr{}
319-
for ip := pool.Addr().Next(); pool.Contains(ip); ip = ip.Next() {
320-
if !ip.IsValid() {
321-
break
322-
}
323-
if used[ip] || ip == tailmixdns.ServiceIP() {
324-
continue
325-
}
326-
*family.current = ip
327-
used[ip] = true
328-
break
329-
}
320+
*family.current = firstFreePoolIP(pool, used)
330321
if !family.current.IsValid() {
331322
return fmt.Errorf("effective %s pool %v has no free address for the host NAT", family.name, pool)
332323
}
324+
used[*family.current] = true
325+
}
326+
327+
pool, err := netip.ParsePrefix(st.SyntheticPool)
328+
if err != nil {
329+
return fmt.Errorf("parse effective IPv4 pool %q: %w", st.SyntheticPool, err)
330+
}
331+
pool = pool.Masked()
332+
if validReservedIP(st.DNSIP, pool, false, used) {
333+
return nil
334+
}
335+
st.DNSIP = netip.Addr{}
336+
preferred := poolIPAtOffset(pool, 53)
337+
if preferred.IsValid() && !used[preferred] {
338+
st.DNSIP = preferred
339+
} else {
340+
st.DNSIP = firstFreePoolIP(pool, used)
341+
}
342+
if !st.DNSIP.IsValid() {
343+
return fmt.Errorf("effective IPv4 pool %v has no free address for MagicDNS", pool)
333344
}
334345
return nil
335346
}
336347

348+
func validReservedIP(ip netip.Addr, pool netip.Prefix, ipv6 bool, used map[netip.Addr]bool) bool {
349+
return ip.IsValid() && ip != pool.Addr() && ip.Is6() == ipv6 && pool.Contains(ip) && !used[ip]
350+
}
351+
352+
func firstFreePoolIP(pool netip.Prefix, used map[netip.Addr]bool) netip.Addr {
353+
for ip := pool.Addr().Next(); pool.Contains(ip); ip = ip.Next() {
354+
if !ip.IsValid() {
355+
break
356+
}
357+
if !used[ip] && ip != tailmixdns.ServiceIP() {
358+
return ip
359+
}
360+
}
361+
return netip.Addr{}
362+
}
363+
364+
func poolIPAtOffset(pool netip.Prefix, offset int) netip.Addr {
365+
ip := pool.Addr()
366+
for range offset {
367+
ip = ip.Next()
368+
if !ip.IsValid() {
369+
return netip.Addr{}
370+
}
371+
}
372+
if !pool.Contains(ip) {
373+
return netip.Addr{}
374+
}
375+
return ip
376+
}
377+
337378
func parseProfileSpec(raw string) (profileSpec, error) {
338379
var spec profileSpec
339380
if strings.TrimSpace(raw) == "" {
@@ -605,11 +646,13 @@ func assignEffectiveIPs(st state.State, statuses []tailmixprofile.Status) (activ
605646
familyExisting = append(familyExisting, lease)
606647
}
607648
}
608-
reserved := st.NATIP
649+
reserved := []netip.Addr{st.NATIP}
609650
if family.ipv6 {
610-
reserved = st.NATIPv6
651+
reserved = []netip.Addr{st.NATIPv6}
652+
} else {
653+
reserved = append(reserved, st.DNSIP)
611654
}
612-
plan, assignErr := effectiveip.NewAllocator(pool, familyExisting, reserved).Assign(familyNodes)
655+
plan, assignErr := effectiveip.NewAllocator(pool, familyExisting, reserved...).Assign(familyNodes)
613656
if assignErr != nil {
614657
return nil, nil, fmt.Errorf("assign effective %s addresses: %w", family.name, assignErr)
615658
}
@@ -661,7 +704,7 @@ func buildTUNPlan(st state.State, statuses []tailmixprofile.Status) (tunPlan, er
661704
st.Profiles = slices.Clone(st.Profiles)
662705
st.Leases = slices.Clone(st.Leases)
663706
updateProfileMetadata(&st, statuses)
664-
if err := ensureNATIPs(&st); err != nil {
707+
if err := ensureHostIPs(&st); err != nil {
665708
return tunPlan{}, err
666709
}
667710
activeLeases, allLeases, err := assignEffectiveIPs(st, statuses)
@@ -984,14 +1027,19 @@ func tunConfigWithPolicy(st state.State, statuses []tailmixprofile.Status, lease
9841027
}
9851028
}
9861029
}
987-
serviceIP := tailmixdns.ServiceIP()
988-
for key, source := range table.Sources {
989-
if source.HostIP == serviceIP {
990-
return packetmap.Table{}, hosttun.Config{}, fmt.Errorf("profile %q host NAT IP conflicts with MagicDNS service IP %v", key.ProfileID, serviceIP)
1030+
serviceIPs := []netip.Addr{st.DNSIP, tailmixdns.ServiceIP()}
1031+
for _, serviceIP := range serviceIPs {
1032+
if !serviceIP.IsValid() {
1033+
return packetmap.Table{}, hosttun.Config{}, errors.New("MagicDNS service address is unavailable")
1034+
}
1035+
for key, source := range table.Sources {
1036+
if source.HostIP == serviceIP {
1037+
return packetmap.Table{}, hosttun.Config{}, fmt.Errorf("profile %q host NAT IP conflicts with MagicDNS service IP %v", key.ProfileID, serviceIP)
1038+
}
1039+
}
1040+
if destination, ok := table.Destinations.Lookup(serviceIP); ok {
1041+
return packetmap.Table{}, hosttun.Config{}, fmt.Errorf("profile %q effective target IP conflicts with MagicDNS service IP %v", destination.ProfileID, serviceIP)
9911042
}
992-
}
993-
if destination, ok := table.Destinations.Lookup(serviceIP); ok {
994-
return packetmap.Table{}, hosttun.Config{}, fmt.Errorf("profile %q effective target IP conflicts with MagicDNS service IP %v", destination.ProfileID, serviceIP)
9951043
}
9961044
if !st.NATIP.IsValid() {
9971045
return packetmap.Table{}, hosttun.Config{}, errors.New("host IPv4 NAT address is unavailable")
@@ -1003,10 +1051,17 @@ func tunConfigWithPolicy(st state.State, statuses []tailmixprofile.Status, lease
10031051
break
10041052
}
10051053
}
1006-
hostCfg.Routes = append(hostCfg.Routes, hosttun.Route{
1007-
Destination: netip.PrefixFrom(serviceIP, serviceIP.BitLen()),
1008-
Source: st.NATIP,
1009-
})
1054+
hostCfg.Routes = append(hostCfg.Routes,
1055+
hosttun.Route{
1056+
Destination: netip.PrefixFrom(st.DNSIP, st.DNSIP.BitLen()),
1057+
Source: st.NATIP,
1058+
},
1059+
hosttun.Route{
1060+
Destination: netip.PrefixFrom(tailmixdns.ServiceIP(), tailmixdns.ServiceIP().BitLen()),
1061+
Source: st.NATIP,
1062+
Optional: true,
1063+
},
1064+
)
10101065
return table, hostCfg, nil
10111066
}
10121067

@@ -1054,7 +1109,7 @@ func routeOverlapsReserved(st state.State, prefix netip.Prefix) bool {
10541109
return true
10551110
}
10561111
}
1057-
for _, addr := range []netip.Addr{st.NATIP, st.NATIPv6, tailmixdns.ServiceIP()} {
1112+
for _, addr := range []netip.Addr{st.NATIP, st.NATIPv6, st.DNSIP, tailmixdns.ServiceIP()} {
10581113
if addr.IsValid() && prefix.Addr().Is6() == addr.Is6() && prefix.Contains(addr) {
10591114
return true
10601115
}

‎cmd/tailmixd/main_test.go‎

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ func TestConfigureSyntheticPoolsOverrideDiscardsChangedFamilyLeasesAndNAT(t *tes
144144
SyntheticPoolV6: defaultSyntheticPoolV6,
145145
NATIP: netip.MustParseAddr("100.127.0.2"),
146146
NATIPv6: netip.MustParseAddr("fd6d:6e65:7400::2"),
147+
DNSIP: netip.MustParseAddr("100.127.0.53"),
147148
Leases: []state.EffectiveLease{canonicalV4, syntheticV4, syntheticV6},
148149
}
149150
if err := configureSyntheticPools(&st, "10.250.1.9/16", ""); err != nil {
@@ -155,8 +156,8 @@ func TestConfigureSyntheticPoolsOverrideDiscardsChangedFamilyLeasesAndNAT(t *tes
155156
if len(st.Leases) != 1 || st.Leases[0] != syntheticV6 {
156157
t.Fatalf("leases after IPv4 pool change = %+v, want only IPv6 lease", st.Leases)
157158
}
158-
if st.NATIP.IsValid() || st.NATIPv6 != netip.MustParseAddr("fd6d:6e65:7400::2") {
159-
t.Fatalf("NAT addresses after IPv4 pool change = %v, %v", st.NATIP, st.NATIPv6)
159+
if st.NATIP.IsValid() || st.DNSIP.IsValid() || st.NATIPv6 != netip.MustParseAddr("fd6d:6e65:7400::2") {
160+
t.Fatalf("reserved addresses after IPv4 pool change = NAT %v DNS %v IPv6 NAT %v", st.NATIP, st.DNSIP, st.NATIPv6)
160161
}
161162
}
162163

@@ -200,7 +201,7 @@ func TestEnsureNATIPsSkipsAndMigratesPrefixBase(t *testing.T) {
200201
}},
201202
}
202203

203-
if err := ensureNATIPs(&st); err != nil {
204+
if err := ensureHostIPs(&st); err != nil {
204205
t.Fatal(err)
205206
}
206207
if want := netip.MustParseAddr("10.250.0.2"); st.NATIP != want {
@@ -209,6 +210,54 @@ func TestEnsureNATIPsSkipsAndMigratesPrefixBase(t *testing.T) {
209210
if want := netip.MustParseAddr("fd6d:6e65:7400::2"); st.NATIPv6 != want {
210211
t.Fatalf("migrated IPv6 NAT address = %v, want %v", st.NATIPv6, want)
211212
}
213+
if want := netip.MustParseAddr("10.250.0.3"); st.DNSIP != want {
214+
t.Fatalf("MagicDNS address = %v, want first free %v", st.DNSIP, want)
215+
}
216+
}
217+
218+
func TestEnsureHostIPsPrefersAndPersistsPoolOffset53(t *testing.T) {
219+
st := state.State{
220+
SyntheticPool: "10.250.0.0/24",
221+
SyntheticPoolV6: "fd6d:6e65:7400::/120",
222+
}
223+
if err := ensureHostIPs(&st); err != nil {
224+
t.Fatal(err)
225+
}
226+
if want := netip.MustParseAddr("10.250.0.53"); st.DNSIP != want {
227+
t.Fatalf("MagicDNS address = %v, want preferred %v", st.DNSIP, want)
228+
}
229+
persisted := st.DNSIP
230+
st.Leases = append(st.Leases, state.EffectiveLease{
231+
ProfileID: "work",
232+
NodeID: "peer",
233+
CanonicalIP: netip.MustParseAddr("100.64.0.2"),
234+
EffectiveIP: netip.MustParseAddr("10.250.0.2"),
235+
})
236+
if err := ensureHostIPs(&st); err != nil {
237+
t.Fatal(err)
238+
}
239+
if st.DNSIP != persisted {
240+
t.Fatalf("persisted MagicDNS address changed from %v to %v", persisted, st.DNSIP)
241+
}
242+
}
243+
244+
func TestEnsureHostIPsFallsBackWhenPoolOffset53IsLeased(t *testing.T) {
245+
st := state.State{
246+
SyntheticPool: "10.250.0.0/24",
247+
SyntheticPoolV6: "fd6d:6e65:7400::/120",
248+
Leases: []state.EffectiveLease{{
249+
ProfileID: "work",
250+
NodeID: "peer",
251+
CanonicalIP: netip.MustParseAddr("100.64.0.2"),
252+
EffectiveIP: netip.MustParseAddr("10.250.0.53"),
253+
}},
254+
}
255+
if err := ensureHostIPs(&st); err != nil {
256+
t.Fatal(err)
257+
}
258+
if want := netip.MustParseAddr("10.250.0.2"); st.DNSIP != want {
259+
t.Fatalf("MagicDNS address = %v, want first free %v", st.DNSIP, want)
260+
}
212261
}
213262

214263
func TestLeaseTargetsIncludesPeersAndServicesInStableOrder(t *testing.T) {
@@ -348,7 +397,7 @@ func TestTunConfigUsesSharedNATRouteSource(t *testing.T) {
348397
SyntheticPool: "100.127.0.0/24",
349398
SyntheticPoolV6: "fd6d:6e65:7400::/120",
350399
}
351-
if err := ensureNATIPs(&st); err != nil {
400+
if err := ensureHostIPs(&st); err != nil {
352401
t.Fatal(err)
353402
}
354403
leases, _, err := assignEffectiveIPs(st, statuses)
@@ -359,20 +408,24 @@ func TestTunConfigUsesSharedNATRouteSource(t *testing.T) {
359408
if err != nil {
360409
t.Fatal(err)
361410
}
362-
if len(table.Sources) != 2 || table.Destinations.Size() != 2 || len(hostCfg.LocalAddrs) != 1 || len(hostCfg.Routes) != 3 {
411+
if len(table.Sources) != 2 || table.Destinations.Size() != 2 || len(hostCfg.LocalAddrs) != 1 || len(hostCfg.Routes) != 4 {
363412
t.Fatalf("TUN config sizes: sources=%d destinations=%d local=%d routes=%d", len(table.Sources), table.Destinations.Size(), len(hostCfg.LocalAddrs), len(hostCfg.Routes))
364413
}
365414
if hostCfg.LocalAddrs[0].Addr() != st.NATIP {
366415
t.Fatalf("local TUN addresses = %v, want only NAT IP %v", hostCfg.LocalAddrs, st.NATIP)
367416
}
368-
foundDNSRoute := false
417+
foundSyntheticDNSRoute := false
418+
foundQuad100Route := false
369419
for _, route := range hostCfg.Routes {
370-
if route.Destination.Addr() == tailmixdns.ServiceIP() {
371-
foundDNSRoute = true
420+
switch route.Destination.Addr() {
421+
case st.DNSIP:
422+
foundSyntheticDNSRoute = !route.Optional
423+
case tailmixdns.ServiceIP():
424+
foundQuad100Route = route.Optional
372425
}
373426
}
374-
if !foundDNSRoute {
375-
t.Fatalf("host routes %v do not route MagicDNS through the TUN", hostCfg.Routes)
427+
if !foundSyntheticDNSRoute || !foundQuad100Route {
428+
t.Fatalf("host routes %v do not include required synthetic DNS and optional quad-100 routes", hostCfg.Routes)
376429
}
377430
for _, route := range hostCfg.Routes {
378431
if route.Source != st.NATIP {
@@ -416,7 +469,7 @@ func TestTunDNSConfigUsesEffectiveAddresses(t *testing.T) {
416469
SyntheticPool: "100.127.0.0/24",
417470
SyntheticPoolV6: "fd6d:6e65:7400::/120",
418471
}
419-
if err := ensureNATIPs(&st); err != nil {
472+
if err := ensureHostIPs(&st); err != nil {
420473
t.Fatal(err)
421474
}
422475
leases, _, err := assignEffectiveIPs(st, statuses)
@@ -637,8 +690,8 @@ func TestTUNPlanTracksPeerAddAndRemoveAcrossNetmapUpdates(t *testing.T) {
637690
if err != nil {
638691
t.Fatal(err)
639692
}
640-
if initial.Table.Destinations.Size() != 0 || len(initial.HostConfig.Routes) != 1 {
641-
t.Fatalf("initial config = destinations %v routes %v, want only MagicDNS route", initial.Table.Destinations, initial.HostConfig.Routes)
693+
if initial.Table.Destinations.Size() != 0 || len(initial.HostConfig.Routes) != 2 {
694+
t.Fatalf("initial config = destinations %v routes %v, want only MagicDNS routes", initial.Table.Destinations, initial.HostConfig.Routes)
642695
}
643696

644697
withPeer := baseStatus
@@ -654,8 +707,8 @@ func TestTUNPlanTracksPeerAddAndRemoveAcrossNetmapUpdates(t *testing.T) {
654707
if err != nil {
655708
t.Fatal(err)
656709
}
657-
if added.Table.Destinations.Size() != 2 || len(added.HostConfig.Routes) != 3 {
658-
t.Fatalf("config after add = destinations %v routes %v, want peer plus MagicDNS", added.Table.Destinations, added.HostConfig.Routes)
710+
if added.Table.Destinations.Size() != 2 || len(added.HostConfig.Routes) != 4 {
711+
t.Fatalf("config after add = destinations %v routes %v, want peer plus MagicDNS routes", added.Table.Destinations, added.HostConfig.Routes)
659712
}
660713
effectiveByCanonical := map[netip.Addr]netip.Addr{}
661714
for prefix, destination := range added.Table.Destinations.All() {
@@ -681,7 +734,7 @@ func TestTUNPlanTracksPeerAddAndRemoveAcrossNetmapUpdates(t *testing.T) {
681734
if err != nil {
682735
t.Fatal(err)
683736
}
684-
if removed.Table.Destinations.Size() != 0 || len(removed.HostConfig.Routes) != 1 || hasDNSRecord(removed.Records, "peer.home.example", peerV4) || hasDNSRecord(removed.Records, "peer.home.example", peerV6) {
737+
if removed.Table.Destinations.Size() != 0 || len(removed.HostConfig.Routes) != 2 || hasDNSRecord(removed.Records, "peer.home.example", peerV4) || hasDNSRecord(removed.Records, "peer.home.example", peerV6) {
685738
t.Fatalf("peer remained active after removal: routes=%v records=%v", removed.HostConfig.Routes, removed.Records)
686739
}
687740
foundDormantLeases := 0

‎cmd/tailmixd/supervisor.go‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,9 @@ func (s *supervisor) startAggregateLocked() error {
271271
}
272272
s.host = host
273273
dnsService, err := tailmixdns.StartService(tailmixdns.ServiceConfig{
274-
TunName: host.Name(),
275-
Logf: prefixedLogf(s.cfg.Stderr, "dns"),
274+
TunName: host.Name(),
275+
ResolverIP: s.st.DNSIP,
276+
Logf: prefixedLogf(s.cfg.Stderr, "dns"),
276277
})
277278
if err != nil {
278279
_ = host.Close()
@@ -472,7 +473,7 @@ func (s *supervisor) reconcileLocked() (err error) {
472473
} else {
473474
next := cloneState(s.st)
474475
updateProfileMetadata(&next, statuses)
475-
if err := ensureNATIPs(&next); err != nil {
476+
if err := ensureHostIPs(&next); err != nil {
476477
return err
477478
}
478479
active, all, err := assignEffectiveIPs(next, statuses)

0 commit comments

Comments
 (0)