Skip to content

Commit 067f1e5

Browse files
chore: soft net isolation for mac (#92)
Copy-pasted the code out of windows and into a common file. Tested locally.
1 parent 5273d4b commit 067f1e5

File tree

5 files changed

+140
-151
lines changed

5 files changed

+140
-151
lines changed

net/netns/netns.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ package netns
1515

1616
import (
1717
"context"
18+
"fmt"
1819
"net"
1920
"net/netip"
2021
"sync/atomic"
2122

2223
"tailscale.com/net/netknob"
2324
"tailscale.com/net/netmon"
25+
"tailscale.com/net/tsaddr"
2426
"tailscale.com/types/logger"
2527
)
2628

@@ -160,3 +162,66 @@ func isLocalhost(addr string) bool {
160162
ip, _ := netip.ParseAddr(host)
161163
return ip.IsLoopback()
162164
}
165+
166+
// shouldBindToDefaultInterface determines whether a socket should be bound to
167+
// the default interface based on the destination address and soft isolation settings.
168+
func shouldBindToDefaultInterface(logf logger.Logf, address string) bool {
169+
if isLocalhost(address) {
170+
// Don't bind to an interface for localhost connections.
171+
return false
172+
}
173+
174+
if coderSoftIsolation.Load() {
175+
addr, err := getAddr(address)
176+
if err != nil {
177+
logf("[unexpected] netns: Coder soft isolation: error getting addr for %q, binding to default: %v", address, err)
178+
return true
179+
}
180+
if !addr.IsValid() || addr.IsUnspecified() {
181+
// Invalid or unspecified addresses should not be bound to any
182+
// interface.
183+
return false
184+
}
185+
if tsaddr.IsCoderIP(addr) {
186+
logf("[unexpected] netns: Coder soft isolation: detected socket destined for Coder interface, binding to default")
187+
return true
188+
}
189+
190+
// It doesn't look like our own interface, so we don't need to bind the
191+
// socket to the default interface.
192+
return false
193+
}
194+
195+
// The default isolation behavior is to always bind to the default
196+
// interface.
197+
return true
198+
}
199+
200+
// getAddr returns the netip.Addr for the given address, or an invalid address
201+
// if the address is not specified. Use addr.IsValid() to check for this.
202+
func getAddr(address string) (netip.Addr, error) {
203+
host, _, err := net.SplitHostPort(address)
204+
if err != nil {
205+
return netip.Addr{}, fmt.Errorf("invalid address %q: %w", address, err)
206+
}
207+
if host == "" {
208+
// netip.ParseAddr("") will fail
209+
return netip.Addr{}, nil
210+
}
211+
212+
addr, err := netip.ParseAddr(host)
213+
if err != nil {
214+
return netip.Addr{}, fmt.Errorf("invalid address %q: %w", address, err)
215+
}
216+
if addr.Zone() != "" {
217+
// Addresses with zones *can* be represented as a Sockaddr with extra
218+
// effort, but we don't use or support them currently.
219+
return netip.Addr{}, fmt.Errorf("invalid address %q, has zone: %w", address, err)
220+
}
221+
if addr.IsUnspecified() {
222+
// This covers the cases of 0.0.0.0 and [::].
223+
return netip.Addr{}, nil
224+
}
225+
226+
return addr, nil
227+
}

net/netns/netns_darwin.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ var errInterfaceStateInvalid = errors.New("interface state invalid")
3838
// It's intentionally the same signature as net.Dialer.Control
3939
// and net.ListenConfig.Control.
4040
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.
41+
if !shouldBindToDefaultInterface(logf, address) {
4342
return nil
4443
}
4544

net/netns/netns_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ package netns
1515

1616
import (
1717
"flag"
18+
"fmt"
1819
"testing"
20+
21+
"tailscale.com/net/tsaddr"
1922
)
2023

2124
var extNetwork = flag.Bool("use-external-network", false, "use the external network in tests")
@@ -76,3 +79,74 @@ func TestIsLocalhost(t *testing.T) {
7679
}
7780
}
7881
}
82+
83+
func TestShouldBindToDefaultInterface(t *testing.T) {
84+
t.Run("Normal", func(t *testing.T) {
85+
tests := []struct {
86+
address string
87+
want bool
88+
}{
89+
{"127.0.0.1:0", false},
90+
{"127.0.0.1:1234", false},
91+
{"1.2.3.4:0", true},
92+
{"1.2.3.4:1234", true},
93+
}
94+
95+
for _, test := range tests {
96+
t.Run(test.address, func(t *testing.T) {
97+
got := shouldBindToDefaultInterface(t.Logf, test.address)
98+
if got != test.want {
99+
t.Errorf("want %v, got %v", test.want, got)
100+
}
101+
})
102+
}
103+
})
104+
105+
t.Run("CoderSoftIsolation", func(t *testing.T) {
106+
SetCoderSoftIsolation(true)
107+
t.Cleanup(func() {
108+
SetCoderSoftIsolation(false)
109+
})
110+
111+
tests := []struct {
112+
address string
113+
want bool
114+
}{
115+
// localhost should still not bind to any interface.
116+
{"127.0.0.1:0", false},
117+
{"127.0.0.1:0", false},
118+
{"127.0.0.1:1234", false},
119+
{"127.0.0.1:1234", false},
120+
121+
// Unspecified addresses should not be bound to any interface.
122+
{":1234", false},
123+
{":1234", false},
124+
{"0.0.0.0:1234", false},
125+
{"0.0.0.0:1234", false},
126+
{"[::]:1234", false},
127+
{"[::]:1234", false},
128+
129+
// Special cases should always bind to default:
130+
{"[::%eth0]:1234", true}, // zones are not supported
131+
{"a:1234", true}, // not an IP
132+
133+
// Coder IPs should bind to default.
134+
{fmt.Sprintf("[%s]:8080", tsaddr.CoderServiceIPv6()), true},
135+
{fmt.Sprintf("[%s]:8080", tsaddr.CoderV6Range().Addr().Next()), true},
136+
137+
// Non-Coder IPs should not bind to default.
138+
{fmt.Sprintf("[%s]:8080", tsaddr.TailscaleServiceIPv6()), false},
139+
{fmt.Sprintf("%s:8080", tsaddr.TailscaleServiceIP()), false},
140+
{"1.2.3.4:8080", false},
141+
}
142+
143+
for _, test := range tests {
144+
t.Run(test.address, func(t *testing.T) {
145+
got := shouldBindToDefaultInterface(t.Logf, test.address)
146+
if got != test.want {
147+
t.Errorf("want %v, got %v", test.want, got)
148+
}
149+
})
150+
}
151+
})
152+
}

net/netns/netns_windows.go

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,14 @@
44
package netns
55

66
import (
7-
"fmt"
87
"math/bits"
9-
"net"
10-
"net/netip"
11-
"strings"
128
"syscall"
139

1410
"golang.org/x/sys/cpu"
1511
"golang.org/x/sys/windows"
1612
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
1713
"tailscale.com/net/interfaces"
1814
"tailscale.com/net/netmon"
19-
"tailscale.com/net/tsaddr"
2015
"tailscale.com/types/logger"
2116
)
2217

@@ -77,40 +72,6 @@ func controlLogf(logf logger.Logf, _ *netmon.Monitor, network, address string, c
7772
return nil
7873
}
7974

80-
func shouldBindToDefaultInterface(logf logger.Logf, address string) bool {
81-
if strings.HasPrefix(address, "127.") {
82-
// Don't bind to an interface for localhost connections,
83-
// otherwise we get:
84-
// connectex: The requested address is not valid in its context
85-
// (The derphttp tests were failing)
86-
return false
87-
}
88-
89-
if coderSoftIsolation.Load() {
90-
addr, err := getAddr(address)
91-
if err != nil {
92-
logf("[unexpected] netns: Coder soft isolation: error getting addr for %q, binding to default: %v", address, err)
93-
return true
94-
}
95-
if !addr.IsValid() || addr.IsUnspecified() {
96-
// Invalid or unspecified addresses should not be bound to any
97-
// interface.
98-
return false
99-
}
100-
if tsaddr.IsCoderIP(addr) {
101-
logf("[unexpected] netns: Coder soft isolation: detected socket destined for Coder interface, binding to default")
102-
return true
103-
}
104-
105-
// It doesn't look like our own interface, so we don't need to bind the
106-
// socket to the default interface.
107-
return false
108-
}
109-
110-
// The default isolation behavior is to always bind to the default
111-
// interface.
112-
return true
113-
}
11475

11576
// sockoptBoundInterface is the value of IP_UNICAST_IF and IPV6_UNICAST_IF.
11677
//
@@ -163,31 +124,3 @@ func nativeToBigEndian(i uint32) uint32 {
163124
return bits.ReverseBytes32(i)
164125
}
165126

166-
// getAddr returns the netip.Addr for the given address, or an invalid address
167-
// if the address is not specified. Use addr.IsValid() to check for this.
168-
func getAddr(address string) (netip.Addr, error) {
169-
host, _, err := net.SplitHostPort(address)
170-
if err != nil {
171-
return netip.Addr{}, fmt.Errorf("invalid address %q: %w", address, err)
172-
}
173-
if host == "" {
174-
// netip.ParseAddr("") will fail
175-
return netip.Addr{}, nil
176-
}
177-
178-
addr, err := netip.ParseAddr(host)
179-
if err != nil {
180-
return netip.Addr{}, fmt.Errorf("invalid address %q: %w", address, err)
181-
}
182-
if addr.Zone() != "" {
183-
// Addresses with zones *can* be represented as a Sockaddr with extra
184-
// effort, but we don't use or support them currently.
185-
return netip.Addr{}, fmt.Errorf("invalid address %q, has zone: %w", address, err)
186-
}
187-
if addr.IsUnspecified() {
188-
// This covers the cases of 0.0.0.0 and [::].
189-
return netip.Addr{}, nil
190-
}
191-
192-
return addr, nil
193-
}

net/netns/netns_windows_test.go

Lines changed: 0 additions & 82 deletions
This file was deleted.

0 commit comments

Comments
 (0)