wgengine/netstack: add local tailscale service IPs to route and terminate locally (#18461)

* wgengine/netstack: add local tailscale service IPs to route and terminate locally

This commit adds the tailscales service IPs served locally to OS routes, and
make interception to packets so that the traffic terminates locally without
making affects to the HA traffics.

Fixes tailscale/corp#34048

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>

* fix test

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>

* add ready field to avoid accessing lb before netstack starts

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>

* wgengine/netstack: store values from lb to avoid acquiring a lock

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>

* add active services to netstack on starts with stored prefs.

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>

* fix comments

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>

* update comments

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>

---------

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
This commit is contained in:
KevinLiang10
2026-01-30 16:46:03 -05:00
committed by GitHub
parent 3b6d542923
commit 03461ea7fb
5 changed files with 205 additions and 10 deletions
+45 -3
View File
@@ -922,6 +922,22 @@ func (b *LocalBackend) setStateLocked(state ipn.State) {
}
}
func (b *LocalBackend) IPServiceMappings() netmap.IPServiceMappings {
b.mu.Lock()
defer b.mu.Unlock()
return b.ipVIPServiceMap
}
func (b *LocalBackend) SetIPServiceMappingsForTest(m netmap.IPServiceMappings) {
b.mu.Lock()
defer b.mu.Unlock()
testenv.AssertInTest()
b.ipVIPServiceMap = m
if ns, ok := b.sys.Netstack.GetOK(); ok {
ns.UpdateIPServiceMappings(m)
}
}
// setConfigLocked uses the provided config to update the backend's prefs
// and other state.
func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error {
@@ -4502,6 +4518,12 @@ func (b *LocalBackend) onEditPrefsLocked(_ ipnauth.Actor, mp *ipn.MaskedPrefs, o
}
}
if mp.AdvertiseServicesSet {
if ns, ok := b.sys.Netstack.GetOK(); ok {
ns.UpdateActiveVIPServices(newPrefs.AdvertiseServices())
}
}
// This is recorded here in the EditPrefs path, not the setPrefs path on purpose.
// recordForEdit records metrics related to edits and changes, not the final state.
// If, in the future, we want to record gauge-metrics related to the state of prefs,
@@ -5125,7 +5147,7 @@ func (b *LocalBackend) authReconfigLocked() {
}
oneCGNATRoute := shouldUseOneCGNATRoute(b.logf, b.sys.NetMon.Get(), b.sys.ControlKnobs(), version.OS())
rcfg := b.routerConfigLocked(cfg, prefs, oneCGNATRoute)
rcfg := b.routerConfigLocked(cfg, prefs, nm, oneCGNATRoute)
err = b.e.Reconfig(cfg, rcfg, dcfg)
if err == wgengine.ErrNoChanges {
@@ -5500,7 +5522,7 @@ func peerRoutes(logf logger.Logf, peers []wgcfg.Peer, cgnatThreshold int, routeA
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
//
// b.mu must be held.
func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView, oneCGNATRoute bool) *router.Config {
func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView, nm *netmap.NetworkMap, oneCGNATRoute bool) *router.Config {
singleRouteThreshold := 10_000
if oneCGNATRoute {
singleRouteThreshold = 1
@@ -5585,11 +5607,23 @@ func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView
}
}
// Get the VIPs for VIP services this node hosts. We will add all locally served VIPs to routes then
// we terminate these connection locally in netstack instead of routing to peer.
vipServiceIPs := nm.GetIPVIPServiceMap()
v4, v6 := false, false
if slices.ContainsFunc(rs.LocalAddrs, tsaddr.PrefixIs4) {
rs.Routes = append(rs.Routes, netip.PrefixFrom(tsaddr.TailscaleServiceIP(), 32))
v4 = true
}
if slices.ContainsFunc(rs.LocalAddrs, tsaddr.PrefixIs6) {
rs.Routes = append(rs.Routes, netip.PrefixFrom(tsaddr.TailscaleServiceIPv6(), 128))
v6 = true
}
for vip := range vipServiceIPs {
if (vip.Is4() && v4) || (vip.Is6() && v6) {
rs.Routes = append(rs.Routes, netip.PrefixFrom(vip, vip.BitLen()))
}
}
return rs
@@ -6267,7 +6301,15 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
if buildfeatures.HasServe {
b.ipVIPServiceMap = nm.GetIPVIPServiceMap()
m := nm.GetIPVIPServiceMap()
b.ipVIPServiceMap = m
if ns, ok := b.sys.Netstack.GetOK(); ok {
ns.UpdateIPServiceMappings(m)
// In case the prefs reloaded from Profile Manager but didn't change,
// we still need to load the active VIP services into netstack.
ns.UpdateActiveVIPServices(b.pm.CurrentPrefs().AdvertiseServices())
}
}
if !oldSelf.Equal(nm.SelfNodeOrZero()) {
+24 -1
View File
@@ -7430,8 +7430,31 @@ func TestRouteAllDisabled(t *testing.T) {
cfg := &wgcfg.Config{
Peers: tt.peers,
}
ServiceIPMappings := tailcfg.ServiceIPMappings{
"svc:test-service": []netip.Addr{
netip.MustParseAddr("100.64.1.2"),
netip.MustParseAddr("fd7a:abcd:1234::1"),
},
}
svcIPMapJSON, err := json.Marshal(ServiceIPMappings)
if err != nil {
t.Fatalf("failed to marshal ServiceIPMappings: %v", err)
}
nm := &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Name: "test-node",
Addresses: []netip.Prefix{
pp("100.64.1.1/32"),
},
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{
tailcfg.RawMessage(svcIPMapJSON),
},
},
}).View(),
}
rcfg := lb.routerConfigLocked(cfg, prefs.View(), false)
rcfg := lb.routerConfigLocked(cfg, prefs.View(), nm, false)
for _, p := range rcfg.Routes {
found := false
for _, r := range tt.wantEndpoints {