p0: IANA special-purpose deny table, extra deny prefixes
Block 6to4/Teredo/NAT64-local/site-local/benchmarking/reserved ranges from the IANA registries (families, not decoded embeds). Guard.DenyPrefixes / -deny-prefix covers hairpin WAN /32s; home addresses stay on-box, not in git. Control is tcp/tcp4/tcp6 only.
This commit is contained in:
+35
-32
@@ -30,13 +30,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
cgnat = netip.MustParsePrefix("100.64.0.0/10") // tailnet IPv4 (CGNAT)
|
||||
tailnetULA = netip.MustParsePrefix("fd7a:115c:a1e0::/48") // tailnet IPv6
|
||||
nat64 = netip.MustParsePrefix("64:ff9b::/96")
|
||||
ipv4zero = netip.MustParsePrefix("0.0.0.0/8")
|
||||
)
|
||||
|
||||
const maxRedirects = 5
|
||||
|
||||
// LookupIP resolves a hostname to addresses. Tests inject this.
|
||||
@@ -45,6 +38,10 @@ type LookupIP func(ctx context.Context, host string) ([]netip.Addr, error)
|
||||
// Guard holds the SSRF policy. A zero Guard is ready to use.
|
||||
type Guard struct {
|
||||
LookupIP LookupIP
|
||||
// DenyPrefixes are extra CIDRs blocked at pre-check and at Control
|
||||
// (home WAN /32s that hairpin through the router into LAN services).
|
||||
// Set from config deny_prefixes / -deny-prefix; never from the URL.
|
||||
DenyPrefixes []netip.Prefix
|
||||
}
|
||||
|
||||
func (g *Guard) lookup(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||
@@ -232,14 +229,28 @@ func parseIPv4Part(s string) (uint64, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// CheckAddr is the address predicate applied to every resolved record and to
|
||||
// the literal IP in Dialer.Control.
|
||||
// CheckAddr is the default (no extra prefixes) address predicate.
|
||||
func CheckAddr(addr netip.Addr) error {
|
||||
var g Guard
|
||||
return g.CheckAddr(addr)
|
||||
}
|
||||
|
||||
// CheckAddr is the address predicate applied to every resolved record and to
|
||||
// the literal IP in Dialer.Control. Extra DenyPrefixes are checked first.
|
||||
func (g *Guard) CheckAddr(addr netip.Addr) error {
|
||||
if !addr.IsValid() {
|
||||
return blocked("invalid address")
|
||||
}
|
||||
addr = addr.Unmap()
|
||||
|
||||
if g != nil {
|
||||
for _, p := range g.DenyPrefixes {
|
||||
if p.Contains(addr) {
|
||||
return blockedAddr("denied prefix", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case addr.IsUnspecified():
|
||||
return blockedAddr("unspecified address", addr)
|
||||
@@ -253,33 +264,17 @@ func CheckAddr(addr netip.Addr) error {
|
||||
return blockedAddr("link-local address", addr)
|
||||
}
|
||||
|
||||
if nat64.Contains(addr) {
|
||||
var b [4]byte
|
||||
raw := addr.As16()
|
||||
copy(b[:], raw[12:])
|
||||
embedded := netip.AddrFrom4(b)
|
||||
if err := CheckAddr(embedded); err != nil {
|
||||
return blockedAddr("NAT64-embedded", embedded)
|
||||
}
|
||||
return blockedAddr("NAT64 prefix", addr)
|
||||
if err := checkIANA(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v4, ok := ipv4Compatible(addr); ok {
|
||||
return CheckAddr(v4)
|
||||
return g.CheckAddr(v4)
|
||||
}
|
||||
|
||||
if !addr.IsGlobalUnicast() {
|
||||
return blockedAddr("non-global address", addr)
|
||||
}
|
||||
if addr.Is4() && ipv4zero.Contains(addr) {
|
||||
return blockedAddr("unspecified address", addr)
|
||||
}
|
||||
if addr.Is4() && cgnat.Contains(addr) {
|
||||
return blockedAddr("tailnet address", addr)
|
||||
}
|
||||
if addr.Is6() && tailnetULA.Contains(addr) {
|
||||
return blockedAddr("tailnet address", addr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -311,7 +306,7 @@ func (g *Guard) CheckURL(ctx context.Context, raw string) (*url.URL, []netip.Add
|
||||
return nil, nil, err
|
||||
}
|
||||
if literal.IsValid() {
|
||||
if err := CheckAddr(literal); err != nil {
|
||||
if err := g.CheckAddr(literal); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return u, []netip.Addr{literal}, nil
|
||||
@@ -324,7 +319,7 @@ func (g *Guard) CheckURL(ctx context.Context, raw string) (*url.URL, []netip.Add
|
||||
return nil, nil, blocked("no addresses for " + u.Hostname())
|
||||
}
|
||||
for _, a := range addrs {
|
||||
if err := CheckAddr(a); err != nil {
|
||||
if err := g.CheckAddr(a); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
@@ -334,6 +329,11 @@ func (g *Guard) CheckURL(ctx context.Context, raw string) (*url.URL, []netip.Add
|
||||
// Control is a net.Dialer Control / ControlContext hook. address is the
|
||||
// remote IP:port about to be connect()ed.
|
||||
func (g *Guard) Control(network, address string, _ syscall.RawConn) error {
|
||||
switch network {
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
default:
|
||||
return blocked("network " + network)
|
||||
}
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return blocked("dial address " + address)
|
||||
@@ -345,7 +345,7 @@ func (g *Guard) Control(network, address string, _ syscall.RawConn) error {
|
||||
if err != nil {
|
||||
return blocked("dial address not an IP " + host)
|
||||
}
|
||||
return CheckAddr(addr)
|
||||
return g.CheckAddr(addr)
|
||||
}
|
||||
|
||||
// ControlContext is the context-aware form; policy does not use ctx today.
|
||||
@@ -389,7 +389,10 @@ func (g *Guard) Transport() *http.Transport {
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
DisableCompression: false,
|
||||
// Go gunzips transparently when this is false. P1 size caps MUST
|
||||
// apply to the decompressed resp.Body (io.LimitReader on the body
|
||||
// the client returns); that also covers zip-bombs.
|
||||
DisableCompression: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user