Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions safehttp/safehttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
//
// 1. Dial-time IP gate. DNS is resolved once per dial; each resolved
// address is checked against the block list (loopback, RFC1918,
// CGNAT 100.64.0.0/10, link-local, multicast, unspecified) before
// CGNAT 100.64.0.0/10, link-local, multicast, unspecified, and IPv6
// addresses that carry such an IPv4 address: NAT64, 6to4 and
// IPv4-compatible) before
// any TCP connect. The connection then dials the resolved IP
// directly, so a rebind between check and connect cannot escape
// the gate.
Expand Down Expand Up @@ -176,7 +178,44 @@ func (g *ipGate) dial(ctx context.Context, network, addr string, dial func(conte
return nil, fmt.Errorf("safehttp: no addresses resolved for %s", host)
}

var cgnat = mustCIDR("100.64.0.0/10")
var (
cgnat = mustCIDR("100.64.0.0/10")
// nat64 is the NAT64 well-known prefix (RFC 6052): a gateway forwards
// 64:ff9b::a.b.c.d to the IPv4 address a.b.c.d.
nat64 = mustCIDR("64:ff9b::/96")
// nat64Local is the local-use NAT64 prefix (RFC 8215). Where the IPv4
// address sits in it depends on the network, so it is treated as
// private.
nat64Local = mustCIDR("64:ff9b:1::/48")
// sixToFour is the 6to4 prefix (RFC 3056): 2002:AABB:CCDD::/48 is
// relayed to the IPv4 address AA.BB.CC.DD.
sixToFour = mustCIDR("2002::/16")
// ipv4Compatible holds the deprecated IPv4-compatible addresses
// ::a.b.c.d (RFC 4291).
ipv4Compatible = mustCIDR("::/96")
)

// embeddedIPv4 returns the IPv4 address that ip reaches through a NAT64
// gateway or a 6to4 relay, or as an IPv4-compatible address, and nil for
// any other address. IPv4-mapped addresses are already IPv4 to net.IP.
func embeddedIPv4(ip net.IP) net.IP {
if ip.To4() != nil {
return nil
}
ip16 := ip.To16()
switch {
case ip16 == nil:
return nil
case nat64.Contains(ip16):
return net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15])
case sixToFour.Contains(ip16):
return net.IPv4(ip16[2], ip16[3], ip16[4], ip16[5])
case ipv4Compatible.Contains(ip16) && ip16[12] != 0:
// ::/104, which holds :: and ::1, embeds no routable address.
return net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15])
}
return nil
}

func mustCIDR(s string) *net.IPNet {
_, n, err := net.ParseCIDR(s)
Expand Down Expand Up @@ -213,6 +252,14 @@ func (g *ipGate) checkHost(host string, ip net.IP) error {
if cgnat.Contains(ip) {
return blockedErr(ip, "CGNAT")
}
if nat64Local.Contains(ip) {
return blockedErr(ip, "private")
}
}
if embedded := embeddedIPv4(ip); embedded != nil {
if err := g.checkHost(host, embedded); err != nil {
return fmt.Errorf("%w, embedded in %s", err, ip)
}
}
return nil
}
Expand Down
35 changes: 35 additions & 0 deletions safehttp/safehttp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,41 @@ func TestCheckIP_Blocks(t *testing.T) {
}
}

// IPv6 addresses that carry an IPv4 address reach that address through a
// NAT64 gateway or a 6to4 relay, so they are checked as the IPv4 address.
func TestCheckIP_BlocksEmbeddedIPv4(t *testing.T) {
g := newGate(Options{})
cases := map[string]string{
"64:ff9b::a9fe:a9fe": "link-local", // NAT64 well-known prefix, 169.254.169.254
"64:ff9b::a00:1": "private", // NAT64, 10.0.0.1
"64:ff9b::7f00:1": "loopback", // NAT64, 127.0.0.1
"64:ff9b::6440:1": "CGNAT", // NAT64, 100.64.0.1
"64:ff9b:1::1": "private", // local-use NAT64 prefix (RFC 8215)
"2002:a00:1::1": "private", // 6to4, 10.0.0.1
"2002:a9fe:a9fe::": "link-local", // 6to4, 169.254.169.254
"::a00:1": "private", // IPv4-compatible, 10.0.0.1
"::7f00:1": "loopback", // IPv4-compatible, 127.0.0.1
}
for in, wantKind := range cases {
err := g.check(net.ParseIP(in))
if err == nil {
t.Errorf("checkIP(%s) = nil; want %s", in, wantKind)
continue
}
if !strings.Contains(err.Error(), wantKind) {
t.Errorf("checkIP(%s) = %v; want kind %q", in, err, wantKind)
}
}
for _, in := range []string{"64:ff9b::808:808", "2002:808:808::1"} {
if err := g.check(net.ParseIP(in)); err != nil {
t.Errorf("checkIP(%s) = %v; want nil for a public embedded address", in, err)
}
}
if err := newGate(Options{AllowPrivate: true}).check(net.ParseIP("64:ff9b::a00:1")); err != nil {
t.Errorf("checkIP(64:ff9b::a00:1) with AllowPrivate = %v; want nil", err)
}
}

func TestCheckIP_Allows(t *testing.T) {
g := newGate(Options{})
for _, in := range []string{"8.8.8.8", "1.1.1.1", "2606:4700:4700::1111"} {
Expand Down
Loading