Skip to content

Commit 29d0169

Browse files
committed
fix: add soft isolation macos impl
1 parent 4941977 commit 29d0169

File tree

10 files changed

+263
-36
lines changed

10 files changed

+263
-36
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ require (
3939
github.com/google/nftables v0.2.0
4040
github.com/google/uuid v1.3.0
4141
github.com/goreleaser/nfpm v1.10.3
42+
github.com/hashicorp/golang-lru/v2 v2.0.7
4243
github.com/hdevalence/ed25519consensus v0.1.0
4344
github.com/iancoleman/strcase v0.2.0
4445
github.com/illarion/gonotify v1.0.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,8 @@ github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
572572
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
573573
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
574574
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
575+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
576+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
575577
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
576578
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
577579
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=

net/netmon/netmon_darwin.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ func (m *darwinRouteMon) skipRouteMessage(msg *route.RouteMessage) bool {
137137
// dst = fe80::b476:66ff:fe30:c8f6%15
138138
return true
139139
}
140+
if msg.Type == unix.RTM_GET {
141+
// Skip RTM_GET messages, which are used to query the routing table.
142+
// See netns_darwin.go
143+
}
140144
return false
141145
}
142146

net/netns/netns_android.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ func control(logger.Logf, *netmon.Monitor) func(network, address string, c sysca
5454
return controlC
5555
}
5656

57+
func ClearRouteCache() {
58+
// There's no route cache to clear on Android.
59+
}
60+
5761
// controlC marks c as necessary to dial in a separate network namespace.
5862
//
5963
// It's intentionally the same signature as net.Dialer.Control

net/netns/netns_darwin.go

Lines changed: 157 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import (
1313
"net/netip"
1414
"os"
1515
"strings"
16+
"sync"
1617
"syscall"
1718

19+
lru "github.com/hashicorp/golang-lru/v2"
1820
"golang.org/x/net/route"
1921
"golang.org/x/sys/unix"
2022
"tailscale.com/envknob"
@@ -33,60 +35,154 @@ var bindToInterfaceByRouteEnv = envknob.RegisterBool("TS_BIND_TO_INTERFACE_BY_RO
3335

3436
var errInterfaceStateInvalid = errors.New("interface state invalid")
3537

36-
// controlLogf marks c as necessary to dial in a separate network namespace.
37-
//
38-
// It's intentionally the same signature as net.Dialer.Control
39-
// and net.ListenConfig.Control.
38+
// routeCache caches the results of interfaceIndexFor calls to avoid
39+
// spamming the AF_ROUTE socket. This is used for soft
40+
// isolation mode where we do many route lookups.
41+
type routeCacheEntry struct {
42+
ifIndex int
43+
err error
44+
}
45+
46+
var (
47+
routeCache *lru.Cache[string, routeCacheEntry]
48+
routeCacheOnce sync.Once
49+
)
50+
51+
func getRouteCache() *lru.Cache[string, routeCacheEntry] {
52+
routeCacheOnce.Do(func() {
53+
routeCache, _ = lru.New[string, routeCacheEntry](256)
54+
})
55+
return routeCache
56+
}
57+
58+
// ClearRouteCache clears the route cache. This should be called by the
59+
// network monitor when a link changes occur.
60+
func ClearRouteCache() {
61+
getRouteCache().Purge()
62+
}
63+
64+
// isInterfaceCoderInterface can be swapped out in tests.
65+
var isInterfaceCoderInterface func(int) bool = isInterfaceCoderInterfaceDefault
66+
67+
func isInterfaceCoderInterfaceDefault(idx int) bool {
68+
_, tsif, err := interfaces.Coder()
69+
return err == nil && tsif != nil && tsif.Index == idx
70+
}
71+
72+
// controlLogf binds c to the default interface if it would otherwise
73+
// be bound to the Coder interface.
4074
func controlLogf(logf logger.Logf, netMon *netmon.Monitor, network, address string, c syscall.RawConn) error {
41-
if isLocalhost(address) {
42-
// Don't bind to an interface for localhost connections.
75+
if !shouldBindToDefaultInterface(logf, netMon, address) {
4376
return nil
4477
}
4578

46-
if disableBindConnToInterface.Load() {
47-
logf("netns_darwin: binding connection to interfaces disabled")
79+
idx, err := getDefaultInterfaceIndex(logf, netMon)
80+
if err != nil {
4881
return nil
4982
}
5083

51-
idx, err := getInterfaceIndex(logf, netMon, address)
84+
return bindConnToInterface(c, network, address, idx, logf)
85+
}
86+
87+
// parseAddrForRouting returns the IP address for the given address, or an invalid
88+
// address if the address is not specified.
89+
func parseAddrForRouting(address string) (netip.Addr, error) {
90+
host, _, err := net.SplitHostPort(address)
5291
if err != nil {
53-
// callee logged
54-
return nil
92+
return netip.Addr{}, fmt.Errorf("invalid address %q: %w", address, err)
93+
}
94+
if host == "" {
95+
// netip.ParseAddr("") will fail
96+
return netip.Addr{}, nil
5597
}
5698

57-
return bindConnToInterface(c, network, address, idx, logf)
99+
addr, err := netip.ParseAddr(host)
100+
if err != nil {
101+
return netip.Addr{}, fmt.Errorf("invalid address %q: %w", address, err)
102+
}
103+
if addr.Zone() != "" {
104+
// Addresses with zones *can* be represented as a route lookup with extra
105+
// effort, but we don't use or support them currently.
106+
return netip.Addr{}, fmt.Errorf("invalid address %q, has zone: %w", address, err)
107+
}
108+
if addr.IsUnspecified() {
109+
// This covers the cases of 0.0.0.0 and [::].
110+
return netip.Addr{}, nil
111+
}
112+
113+
return addr, nil
58114
}
59115

60-
func getInterfaceIndex(logf logger.Logf, netMon *netmon.Monitor, address string) (int, error) {
61-
// Helper so we can log errors.
62-
defaultIdx := func() (int, error) {
63-
if netMon == nil {
64-
idx, err := interfaces.DefaultRouteInterfaceIndex()
65-
if err != nil {
66-
// It's somewhat common for there to be no default gateway route
67-
// (e.g. on a phone with no connectivity), don't log those errors
68-
// since they are expected.
69-
if !errors.Is(err, interfaces.ErrNoGatewayIndexFound) {
70-
logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err)
71-
}
72-
return -1, err
73-
}
74-
return idx, nil
116+
func shouldBindToDefaultInterface(logf logger.Logf, _ *netmon.Monitor, address string) bool {
117+
if isLocalhost(address) {
118+
// Don't bind to an interface for localhost connections.
119+
return false
120+
}
121+
122+
if coderSoftIsolation.Load() {
123+
addr, err := parseAddrForRouting(address)
124+
if err != nil {
125+
logf("[unexpected] netns: Coder soft isolation: error parsing address %q, binding to default: %v", address, err)
126+
return true
75127
}
76-
state := netMon.InterfaceState()
77-
if state == nil {
78-
return -1, errInterfaceStateInvalid
128+
if !addr.IsValid() {
129+
// Unspecified addresses should not be bound to any interface.
130+
return false
79131
}
80132

81-
if iface, ok := state.Interface[state.DefaultRouteInterface]; ok {
82-
return iface.Index, nil
133+
// Ask Darwin routing table to find the best interface for this address
134+
// by using cached route lookups to avoid spamming the AF_ROUTE socket.
135+
idx, err := getBestInterfaceCached(addr)
136+
if err != nil {
137+
logf("[unexpected] netns: Coder soft isolation: error getting best interface, binding to default: %v", err)
138+
return true
83139
}
140+
141+
if isInterfaceCoderInterface(idx) {
142+
logf("[unexpected] netns: Coder soft isolation: detected socket destined for Coder interface, binding to default")
143+
return true
144+
}
145+
146+
// It doesn't look like our own interface, so we don't need to bind the
147+
// socket to the default interface.
148+
return false
149+
}
150+
151+
// The default isolation behavior is to always bind to the default
152+
// interface.
153+
return true
154+
}
155+
156+
func getDefaultInterfaceIndex(logf logger.Logf, netMon *netmon.Monitor) (int, error) {
157+
if netMon == nil {
158+
idx, err := interfaces.DefaultRouteInterfaceIndex()
159+
if err != nil {
160+
// It's somewhat common for there to be no default gateway route
161+
// (e.g. on a phone with no connectivity), don't log those errors
162+
// since they are expected.
163+
if !errors.Is(err, interfaces.ErrNoGatewayIndexFound) {
164+
logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err)
165+
}
166+
return -1, err
167+
}
168+
return idx, nil
169+
}
170+
171+
state := netMon.InterfaceState()
172+
if state == nil {
84173
return -1, errInterfaceStateInvalid
85174
}
86175

176+
if iface, ok := state.Interface[state.DefaultRouteInterface]; ok {
177+
return iface.Index, nil
178+
}
179+
return -1, errInterfaceStateInvalid
180+
}
181+
182+
func getInterfaceIndex(logf logger.Logf, netMon *netmon.Monitor, address string) (int, error) {
87183
useRoute := bindToInterfaceByRoute.Load() || bindToInterfaceByRouteEnv()
88184
if !useRoute {
89-
return defaultIdx()
185+
return getDefaultInterfaceIndex(logf, netMon)
90186
}
91187

92188
host, _, err := net.SplitHostPort(address)
@@ -99,21 +195,21 @@ func getInterfaceIndex(logf logger.Logf, netMon *netmon.Monitor, address string)
99195
addr, err := netip.ParseAddr(host)
100196
if err != nil {
101197
logf("[unexpected] netns: error parsing address %q: %v", host, err)
102-
return defaultIdx()
198+
return getDefaultInterfaceIndex(logf, netMon)
103199
}
104200

105201
idx, err := interfaceIndexFor(addr, true /* canRecurse */)
106202
if err != nil {
107203
logf("netns: error in interfaceIndexFor: %v", err)
108-
return defaultIdx()
204+
return getDefaultInterfaceIndex(logf, netMon)
109205
}
110206

111207
// Verify that we didn't just choose the Coder interface;
112208
// if so, we fall back to binding from the default.
113209
_, tsif, err2 := interfaces.Coder()
114210
if err2 == nil && tsif != nil && tsif.Index == idx {
115211
logf("[unexpected] netns: interfaceIndexFor returned Coder interface")
116-
return defaultIdx()
212+
return getDefaultInterfaceIndex(logf, netMon)
117213
}
118214

119215
return idx, err
@@ -225,6 +321,31 @@ func interfaceIndexFor(addr netip.Addr, canRecurse bool) (int, error) {
225321
return 0, fmt.Errorf("no valid address found")
226322
}
227323

324+
// getBestInterfaceCached returns the interface index that we should bind to in
325+
// order to send traffic to the provided address, using a cache to avoid
326+
// spamming the AF_ROUTE socket.
327+
func getBestInterfaceCached(addr netip.Addr) (int, error) {
328+
cache := getRouteCache()
329+
key := addr.String()
330+
331+
// Check cache first
332+
if entry, ok := cache.Get(key); ok {
333+
return entry.ifIndex, entry.err
334+
}
335+
336+
// Cache miss, do the actual lookup
337+
idx, err := interfaceIndexFor(addr, true /* canRecurse */)
338+
339+
// Cache the result
340+
entry := routeCacheEntry{
341+
ifIndex: idx,
342+
err: err,
343+
}
344+
cache.Add(key, entry)
345+
346+
return idx, err
347+
}
348+
228349
// SetListenConfigInterfaceIndex sets lc.Control such that sockets are bound
229350
// to the provided interface index.
230351
func SetListenConfigInterfaceIndex(lc *net.ListenConfig, ifIndex int) error {

net/netns/netns_darwin_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,94 @@
44
package netns
55

66
import (
7+
"strconv"
78
"testing"
89

910
"tailscale.com/net/interfaces"
1011
)
1112

13+
func TestShouldBindToDefaultInterface(t *testing.T) {
14+
t.Run("Normal", func(t *testing.T) {
15+
tests := []struct {
16+
address string
17+
want bool
18+
}{
19+
{"127.0.0.1:0", false},
20+
{"127.0.0.1:1234", false},
21+
{"1.2.3.4:0", true},
22+
{"1.2.3.4:1234", true},
23+
}
24+
25+
for _, test := range tests {
26+
t.Run(test.address, func(t *testing.T) {
27+
got := shouldBindToDefaultInterface(t.Logf, nil, test.address)
28+
if got != test.want {
29+
t.Errorf("want %v, got %v", test.want, got)
30+
}
31+
})
32+
}
33+
})
34+
35+
t.Run("CoderSoftIsolation", func(t *testing.T) {
36+
SetCoderSoftIsolation(true)
37+
t.Cleanup(func() {
38+
SetCoderSoftIsolation(false)
39+
})
40+
41+
tests := []struct {
42+
address string
43+
isCoderInterface bool
44+
want bool
45+
}{
46+
// isCoderInterface shouldn't even matter for localhost since it has
47+
// a special exemption.
48+
{"127.0.0.1:0", false, false},
49+
{"127.0.0.1:0", true, false},
50+
{"127.0.0.1:1234", false, false},
51+
{"127.0.0.1:1234", true, false},
52+
53+
{"1.2.3.4:0", false, false},
54+
{"1.2.3.4:0", true, true},
55+
{"1.2.3.4:1234", false, false},
56+
{"1.2.3.4:1234", true, true},
57+
58+
// Unspecified addresses should not be bound to any interface.
59+
{":0", false, false},
60+
{":0", true, false},
61+
{":1234", false, false},
62+
{":1234", true, false},
63+
{"0.0.0.0:1234", false, false},
64+
{"0.0.0.0:1234", true, false},
65+
{"[::]:1234", false, false},
66+
{"[::]:1234", true, false},
67+
68+
// Special cases should always bind to default:
69+
{"[::%eth0]:1234", false, true}, // zones are not supported
70+
{"1.2.3.4:", false, true}, // port is empty
71+
{"1.2.3.4:a", false, true}, // port is not a number
72+
{"1.2.3.4:-1", false, true}, // port is negative
73+
{"1.2.3.4:65536", false, true}, // port is too large
74+
}
75+
76+
for _, test := range tests {
77+
name := test.address + " (isCoderInterface=" + strconv.FormatBool(test.isCoderInterface) + ")"
78+
t.Run(name, func(t *testing.T) {
79+
isInterfaceCoderInterface = func(_ int) bool {
80+
return test.isCoderInterface
81+
}
82+
defer func() {
83+
isInterfaceCoderInterface = isInterfaceCoderInterfaceDefault
84+
}()
85+
86+
got := shouldBindToDefaultInterface(t.Logf, nil, test.address)
87+
if got != test.want {
88+
t.Errorf("want %v, got %v", test.want, got)
89+
}
90+
})
91+
}
92+
})
93+
}
94+
1295
func TestGetInterfaceIndex(t *testing.T) {
1396
oldVal := bindToInterfaceByRoute.Load()
1497
t.Cleanup(func() { bindToInterfaceByRoute.Store(oldVal) })

net/netns/netns_default.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ func control(logger.Logf, *netmon.Monitor) func(network, address string, c sysca
2020
func controlC(network, address string, c syscall.RawConn) error {
2121
return nil
2222
}
23+
24+
func ClearRouteCache() {}

0 commit comments

Comments
 (0)