Signed-off-by: dawn dawn@tangled.org
+114
-75
Diff
Round #0
+6
-44
spindle/engines/microvm/networking.go
+6
-44
spindle/engines/microvm/networking.go
···
6
6
_ "embed"
7
7
"fmt"
8
8
"log/slog"
9
-
"net"
10
9
"os"
11
10
"os/exec"
12
11
"text/template"
13
-
)
14
12
15
-
// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
16
-
// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
17
-
// https://datatracker.ietf.org/doc/rfc6890/
18
-
var blockedNamespaceRoutes = []string{
19
-
"0.0.0.0/8", // unspecified / "this network" addresses
20
-
"10.0.0.0/8", // private network
21
-
"100.64.0.0/10", // shared carrier-grade nat space
22
-
"127.0.0.0/8", // loopback
23
-
"169.254.0.0/16", // link-local / autoconfiguration
24
-
"172.16.0.0/12", // private network
25
-
"192.0.0.0/24", // ietf protocol assignments
26
-
"192.0.2.0/24", // documentation / examples
27
-
"192.88.99.0/24", // deprecated 6to4 relay anycast
28
-
"192.168.0.0/16", // private network
29
-
"198.18.0.0/15", // benchmarking / testing
30
-
"198.51.100.0/24", // documentation / examples
31
-
"203.0.113.0/24", // documentation / examples
32
-
"224.0.0.0/4", // multicast
33
-
"240.0.0.0/4", // reserved / future use, includes limited broadcast
34
-
"::/128", // unspecified address
35
-
"::1/128", // loopback
36
-
"::ffff:0:0/96", // ipv4-mapped addresses
37
-
"64:ff9b::/96", // ipv4/ipv6 translation prefix
38
-
"100::/64", // discard-only prefix
39
-
"2001::/23", // ietf protocol assignments
40
-
"2001:db8::/32", // documentation / examples
41
-
"2002::/16", // deprecated 6to4 addressing
42
-
"fc00::/7", // unique local addresses
43
-
"fe80::/10", // link-local unicast
44
-
"ff00::/8", // multicast
45
-
}
13
+
"tangled.org/core/spindle/netguard"
14
+
)
46
15
47
-
var blockedNamespaceNets = func() []*net.IPNet {
48
-
nets := make([]*net.IPNet, 0, len(blockedNamespaceRoutes))
49
-
for _, route := range blockedNamespaceRoutes {
50
-
_, ipnet, err := net.ParseCIDR(route)
51
-
if err != nil {
52
-
panic(fmt.Sprintf("parse blocked route %q: %v", route, err))
53
-
}
54
-
nets = append(nets, ipnet)
55
-
}
56
-
return nets
57
-
}()
16
+
var (
17
+
blockedNamespaceRoutes = netguard.BlockedRoutes
18
+
blockedNamespaceNets = netguard.BlockedNets
19
+
)
58
20
59
21
//go:embed netns_wrapper.sh.tmpl
60
22
var netnsWrapperTemplate string
+3
-29
spindle/engines/microvm/read_cache_proxy.go
+3
-29
spindle/engines/microvm/read_cache_proxy.go
···
14
14
"net/url"
15
15
"strings"
16
16
"sync"
17
-
"syscall"
18
17
"time"
19
18
20
19
"github.com/mdlayher/vsock"
20
+
21
+
"tangled.org/core/spindle/netguard"
21
22
)
22
23
23
24
const (
···
233
234
DialContext: (&net.Dialer{
234
235
Timeout: 30 * time.Second,
235
236
KeepAlive: 30 * time.Second,
236
-
Control: refuseSpecialPurposeAddrs,
237
+
Control: netguard.RefuseSpecialPurposeAddrs,
237
238
}).DialContext,
238
239
ForceAttemptHTTP2: true,
239
240
MaxIdleConns: 100,
···
242
243
ExpectContinueTimeout: 1 * time.Second,
243
244
}
244
245
245
-
// this should run after dns resolution, so it should cover any rebinding tricks
246
-
func refuseSpecialPurposeAddrs(network, address string, _ syscall.RawConn) error {
247
-
host, _, err := net.SplitHostPort(address)
248
-
if err != nil {
249
-
return fmt.Errorf("split dial address %q: %w", address, err)
250
-
}
251
-
ip := net.ParseIP(host)
252
-
if ip == nil {
253
-
return fmt.Errorf("refusing to dial non-IP address %q", host)
254
-
}
255
-
bits := 128
256
-
if ip4 := ip.To4(); ip4 != nil {
257
-
ip = ip4
258
-
bits = 32
259
-
}
260
-
for _, ipnet := range blockedNamespaceNets {
261
-
_, blockedBits := ipnet.Mask.Size()
262
-
if blockedBits != bits {
263
-
continue
264
-
}
265
-
if ipnet.Contains(ip) {
266
-
return fmt.Errorf("refusing to dial %s: %s is blocked for workflow caches", ip, ipnet)
267
-
}
268
-
}
269
-
return nil
270
-
}
271
-
272
246
// the proxy is the cache as far as the guest is concerned, so we answer
273
247
// /nix-cache-info ourselves instead of racing the upstreams for it. merging
274
248
// those also doesn't make any sense (none of the options make sense for
+3
-1
spindle/engines/microvm/read_cache_proxy_test.go
+3
-1
spindle/engines/microvm/read_cache_proxy_test.go
···
8
8
"strings"
9
9
"testing"
10
10
"time"
11
+
12
+
"tangled.org/core/spindle/netguard"
11
13
)
12
14
13
15
func TestCacheProxyFallsBackOnNotFound(t *testing.T) {
···
123
125
}
124
126
125
127
func TestCacheProxyGuardAllowsPublicIPv4(t *testing.T) {
126
-
if err := refuseSpecialPurposeAddrs("tcp", "104.26.13.82:443", nil); err != nil {
128
+
if err := netguard.RefuseSpecialPurposeAddrs("tcp", "104.26.13.82:443", nil); err != nil {
127
129
t.Fatalf("public IPv4 address was blocked: %v", err)
128
130
}
129
131
}
+82
spindle/netguard/netguard.go
+82
spindle/netguard/netguard.go
···
1
+
// refuses outbound dials to special-purpose addresses, for anywhere
2
+
// spindle fetches user-influenced urls (workflow caches, PDS blob
3
+
// fetches)
4
+
package netguard
5
+
6
+
import (
7
+
"fmt"
8
+
"net"
9
+
"syscall"
10
+
)
11
+
12
+
// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
13
+
// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
14
+
// https://datatracker.ietf.org/doc/rfc6890/
15
+
var BlockedRoutes = []string{
16
+
"0.0.0.0/8", // unspecified / "this network" addresses
17
+
"10.0.0.0/8", // private network
18
+
"100.64.0.0/10", // shared carrier-grade nat space
19
+
"127.0.0.0/8", // loopback
20
+
"169.254.0.0/16", // link-local / autoconfiguration
21
+
"172.16.0.0/12", // private network
22
+
"192.0.0.0/24", // ietf protocol assignments
23
+
"192.0.2.0/24", // documentation / examples
24
+
"192.88.99.0/24", // deprecated 6to4 relay anycast
25
+
"192.168.0.0/16", // private network
26
+
"198.18.0.0/15", // benchmarking / testing
27
+
"198.51.100.0/24", // documentation / examples
28
+
"203.0.113.0/24", // documentation / examples
29
+
"224.0.0.0/4", // multicast
30
+
"240.0.0.0/4", // reserved / future use, includes limited broadcast
31
+
"::/128", // unspecified address
32
+
"::1/128", // loopback
33
+
"::ffff:0:0/96", // ipv4-mapped addresses
34
+
"64:ff9b::/96", // ipv4/ipv6 translation prefix
35
+
"100::/64", // discard-only prefix
36
+
"2001::/23", // ietf protocol assignments
37
+
"2001:db8::/32", // documentation / examples
38
+
"2002::/16", // deprecated 6to4 addressing
39
+
"fc00::/7", // unique local addresses
40
+
"fe80::/10", // link-local unicast
41
+
"ff00::/8", // multicast
42
+
}
43
+
44
+
var BlockedNets = func() []*net.IPNet {
45
+
nets := make([]*net.IPNet, 0, len(BlockedRoutes))
46
+
for _, route := range BlockedRoutes {
47
+
_, ipnet, err := net.ParseCIDR(route)
48
+
if err != nil {
49
+
panic(fmt.Sprintf("parse blocked route %q: %v", route, err))
50
+
}
51
+
nets = append(nets, ipnet)
52
+
}
53
+
return nets
54
+
}()
55
+
56
+
// net.Dialer Control func rejecting blocked special-purpose addresses.
57
+
// this should run after dns resolution, so it should cover any rebinding tricks
58
+
func RefuseSpecialPurposeAddrs(network, address string, _ syscall.RawConn) error {
59
+
host, _, err := net.SplitHostPort(address)
60
+
if err != nil {
61
+
return fmt.Errorf("split dial address %q: %w", address, err)
62
+
}
63
+
ip := net.ParseIP(host)
64
+
if ip == nil {
65
+
return fmt.Errorf("refusing to dial non-IP address %q", host)
66
+
}
67
+
bits := 128
68
+
if ip4 := ip.To4(); ip4 != nil {
69
+
ip = ip4
70
+
bits = 32
71
+
}
72
+
for _, ipnet := range BlockedNets {
73
+
_, blockedBits := ipnet.Mask.Size()
74
+
if blockedBits != bits {
75
+
continue
76
+
}
77
+
if ipnet.Contains(ip) {
78
+
return fmt.Errorf("refusing to dial %s: %s is blocked", ip, ipnet)
79
+
}
80
+
}
81
+
return nil
82
+
}
+20
-1
spindle/tapclient.go
+20
-1
spindle/tapclient.go
···
7
7
"errors"
8
8
"fmt"
9
9
"log/slog"
10
+
"net"
10
11
"net/http"
11
12
"net/url"
12
13
"sync"
···
22
23
"tangled.org/core/spindle/db"
23
24
"tangled.org/core/spindle/git"
24
25
"tangled.org/core/spindle/models"
26
+
"tangled.org/core/spindle/netguard"
25
27
"tangled.org/core/tapc"
26
28
"tangled.org/core/tid"
27
29
"tangled.org/core/workflow"
···
32
34
pendingCollabTTL = 10 * time.Minute
33
35
)
34
36
37
+
// blobs are fetched from user controlled PDSes so protect our transport
38
+
// from dialing internal addresses
39
+
var guardedBlobClient = &http.Client{
40
+
Transport: &http.Transport{
41
+
DialContext: (&net.Dialer{
42
+
Timeout: 30 * time.Second,
43
+
KeepAlive: 30 * time.Second,
44
+
Control: netguard.RefuseSpecialPurposeAddrs,
45
+
}).DialContext,
46
+
ForceAttemptHTTP2: true,
47
+
MaxIdleConns: 100,
48
+
IdleConnTimeout: 90 * time.Second,
49
+
TLSHandshakeTimeout: 10 * time.Second,
50
+
ExpectContinueTimeout: 1 * time.Second,
51
+
},
52
+
}
53
+
35
54
type pendingCollabEvent struct {
36
55
evt *tapc.RecordEventData
37
56
at time.Time
···
576
595
}
577
596
req.Header.Set("Content-Type", "application/json")
578
597
579
-
blobResp, err := http.DefaultClient.Do(req)
598
+
blobResp, err := guardedBlobClient.Do(req)
580
599
if err != nil {
581
600
return nil, fmt.Errorf("failed to fetch blob: %w", err)
582
601
}
History
1 round
0 comments
ptr.pet
submitted
#0
1 commit
Expand
Collapse
spindle/netguard: refuse to dial internal addresses on user-controlled fetches
Signed-off-by: dawn <dawn@tangled.org>