diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/config/config.go | 9 | ||||
| -rw-r--r-- | internal/config/defaults.go | 6 | ||||
| -rw-r--r-- | internal/config/flags.go | 5 | ||||
| -rw-r--r-- | internal/config/gen/gen.go | 14 | ||||
| -rw-r--r-- | internal/config/helpers.gen.go | 75 | ||||
| -rw-r--r-- | internal/config/util.go | 39 | ||||
| -rw-r--r-- | internal/httpclient/client.go | 6 | ||||
| -rw-r--r-- | internal/httpclient/sanitizer.go | 108 | ||||
| -rw-r--r-- | internal/httpclient/sanitizer_test.go | 154 | ||||
| -rw-r--r-- | internal/netutil/validate.go | 102 | ||||
| -rw-r--r-- | internal/netutil/validate_test.go | 54 | 
11 files changed, 394 insertions, 178 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index c809bbc1b..53514e20b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -150,6 +150,9 @@ type Configuration struct {  	AdvancedThrottlingRetryAfter time.Duration `name:"advanced-throttling-retry-after" usage:"Retry-After duration response to send for throttled requests."`  	AdvancedSenderMultiplier     int           `name:"advanced-sender-multiplier" usage:"Multiplier to use per cpu for batching outgoing fedi messages. 0 or less turns batching off (not recommended)."` +	// HTTPClient configuration vars. +	HTTPClient HTTPClientConfiguration `name:"http-client"` +  	// Cache configuration vars.  	Cache CacheConfiguration `name:"cache"` @@ -163,6 +166,12 @@ type Configuration struct {  	RequestIDHeader string `name:"request-id-header" usage:"Header to extract the Request ID from. Eg.,'X-Request-Id'."`  } +type HTTPClientConfiguration struct { +	AllowIPs []string      `name:"allow-ips"` +	BlockIPs []string      `name:"block-ips"` +	Timeout  time.Duration `name:"timeout"` +} +  type CacheConfiguration struct {  	GTS GTSCacheConfiguration `name:"gts"` diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 1cb53c8e2..34e46b342 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -208,6 +208,12 @@ var Defaults = Configuration{  		VisibilitySweepFreq: time.Minute,  	}, +	HTTPClient: HTTPClientConfiguration{ +		AllowIPs: make([]string, 0), +		BlockIPs: make([]string, 0), +		Timeout:  10 * time.Second, +	}, +  	AdminMediaPruneDryRun: true,  	RequestIDHeader: "X-Request-Id", diff --git a/internal/config/flags.go b/internal/config/flags.go index c9899b67e..c42b5c7b2 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -55,6 +55,11 @@ func (s *ConfigState) AddGlobalFlags(cmd *cobra.Command) {  		cmd.PersistentFlags().String(DbSqliteSynchronousFlag(), cfg.DbSqliteSynchronous, fieldtag("DbSqliteSynchronous", "usage"))  		cmd.PersistentFlags().Uint64(DbSqliteCacheSizeFlag(), uint64(cfg.DbSqliteCacheSize), fieldtag("DbSqliteCacheSize", "usage"))  		cmd.PersistentFlags().Duration(DbSqliteBusyTimeoutFlag(), cfg.DbSqliteBusyTimeout, fieldtag("DbSqliteBusyTimeout", "usage")) + +		// HTTPClient +		cmd.PersistentFlags().StringSlice(HTTPClientAllowIPsFlag(), cfg.HTTPClient.AllowIPs, "no usage string") +		cmd.PersistentFlags().StringSlice(HTTPClientBlockIPsFlag(), cfg.HTTPClient.BlockIPs, "no usage string") +		cmd.PersistentFlags().Duration(HTTPClientTimeoutFlag(), cfg.HTTPClient.Timeout, "no usage string")  	})  } diff --git a/internal/config/gen/gen.go b/internal/config/gen/gen.go index 30994c652..1c2c11747 100644 --- a/internal/config/gen/gen.go +++ b/internal/config/gen/gen.go @@ -96,16 +96,22 @@ func generateFields(output io.Writer, prefixes []string, t reflect.Type) {  		flagPath := strings.Join(append(prefixes, field.Tag.Get("name")), "-")  		flagPath = strings.ToLower(flagPath) +		// Get type without "config." prefix. +		fieldType := strings.ReplaceAll( +			field.Type.String(), +			"config.", "", +		) +  		// ConfigState structure helper methods  		fmt.Fprintf(output, "// Get%s safely fetches the Configuration value for state's '%s' field\n", name, fieldPath) -		fmt.Fprintf(output, "func (st *ConfigState) Get%s() (v %s) {\n", name, field.Type.String()) +		fmt.Fprintf(output, "func (st *ConfigState) Get%s() (v %s) {\n", name, fieldType)  		fmt.Fprintf(output, "\tst.mutex.Lock()\n")  		fmt.Fprintf(output, "\tv = st.config.%s\n", fieldPath)  		fmt.Fprintf(output, "\tst.mutex.Unlock()\n")  		fmt.Fprintf(output, "\treturn\n")  		fmt.Fprintf(output, "}\n\n")  		fmt.Fprintf(output, "// Set%s safely sets the Configuration value for state's '%s' field\n", name, fieldPath) -		fmt.Fprintf(output, "func (st *ConfigState) Set%s(v %s) {\n", name, field.Type.String()) +		fmt.Fprintf(output, "func (st *ConfigState) Set%s(v %s) {\n", name, fieldType)  		fmt.Fprintf(output, "\tst.mutex.Lock()\n")  		fmt.Fprintf(output, "\tdefer st.mutex.Unlock()\n")  		fmt.Fprintf(output, "\tst.config.%s = v\n", fieldPath) @@ -117,8 +123,8 @@ func generateFields(output io.Writer, prefixes []string, t reflect.Type) {  		fmt.Fprintf(output, "// %sFlag returns the flag name for the '%s' field\n", name, fieldPath)  		fmt.Fprintf(output, "func %sFlag() string { return \"%s\" }\n\n", name, flagPath)  		fmt.Fprintf(output, "// Get%s safely fetches the value for global configuration '%s' field\n", name, fieldPath) -		fmt.Fprintf(output, "func Get%[1]s() %[2]s { return global.Get%[1]s() }\n\n", name, field.Type.String()) +		fmt.Fprintf(output, "func Get%[1]s() %[2]s { return global.Get%[1]s() }\n\n", name, fieldType)  		fmt.Fprintf(output, "// Set%s safely sets the value for global configuration '%s' field\n", name, fieldPath) -		fmt.Fprintf(output, "func Set%[1]s(v %[2]s) { global.Set%[1]s(v) }\n\n", name, field.Type.String()) +		fmt.Fprintf(output, "func Set%[1]s(v %[2]s) { global.Set%[1]s(v) }\n\n", name, fieldType)  	}  } diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index c82eba3b3..56360a6c2 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -2299,6 +2299,81 @@ func GetAdvancedSenderMultiplier() int { return global.GetAdvancedSenderMultipli  // SetAdvancedSenderMultiplier safely sets the value for global configuration 'AdvancedSenderMultiplier' field  func SetAdvancedSenderMultiplier(v int) { global.SetAdvancedSenderMultiplier(v) } +// GetHTTPClientAllowIPs safely fetches the Configuration value for state's 'HTTPClient.AllowIPs' field +func (st *ConfigState) GetHTTPClientAllowIPs() (v []string) { +	st.mutex.Lock() +	v = st.config.HTTPClient.AllowIPs +	st.mutex.Unlock() +	return +} + +// SetHTTPClientAllowIPs safely sets the Configuration value for state's 'HTTPClient.AllowIPs' field +func (st *ConfigState) SetHTTPClientAllowIPs(v []string) { +	st.mutex.Lock() +	defer st.mutex.Unlock() +	st.config.HTTPClient.AllowIPs = v +	st.reloadToViper() +} + +// HTTPClientAllowIPsFlag returns the flag name for the 'HTTPClient.AllowIPs' field +func HTTPClientAllowIPsFlag() string { return "httpclient-allow-ips" } + +// GetHTTPClientAllowIPs safely fetches the value for global configuration 'HTTPClient.AllowIPs' field +func GetHTTPClientAllowIPs() []string { return global.GetHTTPClientAllowIPs() } + +// SetHTTPClientAllowIPs safely sets the value for global configuration 'HTTPClient.AllowIPs' field +func SetHTTPClientAllowIPs(v []string) { global.SetHTTPClientAllowIPs(v) } + +// GetHTTPClientBlockIPs safely fetches the Configuration value for state's 'HTTPClient.BlockIPs' field +func (st *ConfigState) GetHTTPClientBlockIPs() (v []string) { +	st.mutex.Lock() +	v = st.config.HTTPClient.BlockIPs +	st.mutex.Unlock() +	return +} + +// SetHTTPClientBlockIPs safely sets the Configuration value for state's 'HTTPClient.BlockIPs' field +func (st *ConfigState) SetHTTPClientBlockIPs(v []string) { +	st.mutex.Lock() +	defer st.mutex.Unlock() +	st.config.HTTPClient.BlockIPs = v +	st.reloadToViper() +} + +// HTTPClientBlockIPsFlag returns the flag name for the 'HTTPClient.BlockIPs' field +func HTTPClientBlockIPsFlag() string { return "httpclient-block-ips" } + +// GetHTTPClientBlockIPs safely fetches the value for global configuration 'HTTPClient.BlockIPs' field +func GetHTTPClientBlockIPs() []string { return global.GetHTTPClientBlockIPs() } + +// SetHTTPClientBlockIPs safely sets the value for global configuration 'HTTPClient.BlockIPs' field +func SetHTTPClientBlockIPs(v []string) { global.SetHTTPClientBlockIPs(v) } + +// GetHTTPClientTimeout safely fetches the Configuration value for state's 'HTTPClient.Timeout' field +func (st *ConfigState) GetHTTPClientTimeout() (v time.Duration) { +	st.mutex.Lock() +	v = st.config.HTTPClient.Timeout +	st.mutex.Unlock() +	return +} + +// SetHTTPClientTimeout safely sets the Configuration value for state's 'HTTPClient.Timeout' field +func (st *ConfigState) SetHTTPClientTimeout(v time.Duration) { +	st.mutex.Lock() +	defer st.mutex.Unlock() +	st.config.HTTPClient.Timeout = v +	st.reloadToViper() +} + +// HTTPClientTimeoutFlag returns the flag name for the 'HTTPClient.Timeout' field +func HTTPClientTimeoutFlag() string { return "httpclient-timeout" } + +// GetHTTPClientTimeout safely fetches the value for global configuration 'HTTPClient.Timeout' field +func GetHTTPClientTimeout() time.Duration { return global.GetHTTPClientTimeout() } + +// SetHTTPClientTimeout safely sets the value for global configuration 'HTTPClient.Timeout' field +func SetHTTPClientTimeout(v time.Duration) { global.SetHTTPClientTimeout(v) } +  // GetCacheGTSAccountMaxSize safely fetches the Configuration value for state's 'Cache.GTS.AccountMaxSize' field  func (st *ConfigState) GetCacheGTSAccountMaxSize() (v int) {  	st.mutex.Lock() diff --git a/internal/config/util.go b/internal/config/util.go new file mode 100644 index 000000000..a9df08b3c --- /dev/null +++ b/internal/config/util.go @@ -0,0 +1,39 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +package config + +import ( +	"net/netip" + +	"github.com/superseriousbusiness/gotosocial/internal/log" +) + +func MustParseIPPrefixes(in []string) []netip.Prefix { +	prefs := make([]netip.Prefix, 0, len(in)) + +	for _, i := range in { +		pref, err := netip.ParsePrefix(i) +		if err != nil { +			log.Panicf(nil, "error parsing ip prefix from %q: %v", i, err) +		} + +		prefs = append(prefs, pref) +	} + +	return prefs +} diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go index 2a2485561..65c521113 100644 --- a/internal/httpclient/client.go +++ b/internal/httpclient/client.go @@ -130,9 +130,9 @@ func New(cfg Config) *Client {  	}  	// Protect dialer with IP range sanitizer. -	d.Control = (&sanitizer{ -		allow: cfg.AllowRanges, -		block: cfg.BlockRanges, +	d.Control = (&Sanitizer{ +		Allow: cfg.AllowRanges, +		Block: cfg.BlockRanges,  	}).Sanitize  	// Prepare client fields. diff --git a/internal/httpclient/sanitizer.go b/internal/httpclient/sanitizer.go index 46540fd86..542698ba7 100644 --- a/internal/httpclient/sanitizer.go +++ b/internal/httpclient/sanitizer.go @@ -20,48 +20,126 @@ package httpclient  import (  	"net/netip"  	"syscall" +) + +var ( +	// ipv6GlobalUnicast is the prefix set aside by IANA for global unicast assignments, i.e "the internet". +	// https://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.xhtml +	ipv6GlobalUnicast = netip.MustParsePrefix("2000::/3") + +	// ipv6Reserved contains IPv6 reserved IP prefixes that fall within ipv6GlobalUnicast. +	// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml +	ipv6Reserved = [...]netip.Prefix{ +		netip.MustParsePrefix("2001::/23"),         // IETF Protocol Assignments (RFC 2928) +		netip.MustParsePrefix("2001:db8::/32"),     // Documentation (RFC 3849) +		netip.MustParsePrefix("2002::/16"),         // 6to4 (RFC 3056) +		netip.MustParsePrefix("2620:4f:8000::/48"), // Direct Delegation AS112 Service (RFC 7534) +	} -	"github.com/superseriousbusiness/gotosocial/internal/netutil" +	// ipv4Reserved contains IPv4 reserved IP prefixes. +	// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml +	ipv4Reserved = [...]netip.Prefix{ +		netip.MustParsePrefix("0.0.0.0/8"),       // Current network +		netip.MustParsePrefix("10.0.0.0/8"),      // Private +		netip.MustParsePrefix("100.64.0.0/10"),   // RFC6598 +		netip.MustParsePrefix("127.0.0.0/8"),     // Loopback +		netip.MustParsePrefix("169.254.0.0/16"),  // Link-local +		netip.MustParsePrefix("172.16.0.0/12"),   // Private +		netip.MustParsePrefix("192.0.0.0/24"),    // RFC6890 +		netip.MustParsePrefix("192.0.2.0/24"),    // Test, doc, examples +		netip.MustParsePrefix("192.31.196.0/24"), // AS112-v4, RFC 7535 +		netip.MustParsePrefix("192.52.193.0/24"), // AMT, RFC 7450 +		netip.MustParsePrefix("192.88.99.0/24"),  // IPv6 to IPv4 relay +		netip.MustParsePrefix("192.168.0.0/16"),  // Private +		netip.MustParsePrefix("192.175.48.0/24"), // Direct Delegation AS112 Service, RFC 7534 +		netip.MustParsePrefix("198.18.0.0/15"),   // Benchmarking tests +		netip.MustParsePrefix("198.51.100.0/24"), // Test, doc, examples +		netip.MustParsePrefix("203.0.113.0/24"),  // Test, doc, examples +		netip.MustParsePrefix("224.0.0.0/4"),     // Multicast +		netip.MustParsePrefix("240.0.0.0/4"),     // Reserved (includes broadcast / 255.255.255.255) +	}  ) -type sanitizer struct { -	allow []netip.Prefix -	block []netip.Prefix +type Sanitizer struct { +	Allow []netip.Prefix +	Block []netip.Prefix  }  // Sanitize implements the required net.Dialer.Control function signature. -func (s *sanitizer) Sanitize(ntwrk, addr string, _ syscall.RawConn) error { +func (s *Sanitizer) Sanitize(ntwrk, addr string, _ syscall.RawConn) error {  	// Parse IP+port from addr  	ipport, err := netip.ParseAddrPort(addr)  	if err != nil {  		return err  	} -	if !(ntwrk == "tcp4" || ntwrk == "tcp6") { +	// Ensure valid network. +	const ( +		tcp4 = "tcp4" +		tcp6 = "tcp6" +	) + +	if !(ntwrk == tcp4 || ntwrk == tcp6) {  		return ErrInvalidNetwork  	} -	// Seperate the IP +	// Separate the IP.  	ip := ipport.Addr() -	// Check if this is explicitly allowed -	for i := 0; i < len(s.allow); i++ { -		if s.allow[i].Contains(ip) { +	// Check if this IP is explicitly allowed. +	for i := 0; i < len(s.Allow); i++ { +		if s.Allow[i].Contains(ip) {  			return nil  		}  	} -	// Now check if explicity blocked -	for i := 0; i < len(s.block); i++ { -		if s.block[i].Contains(ip) { +	// Check if this IP is explicitly blocked. +	for i := 0; i < len(s.Block); i++ { +		if s.Block[i].Contains(ip) {  			return ErrReservedAddr  		}  	} -	// Validate this is a safe IP -	if !netutil.ValidateIP(ip) { +	// Validate this is a safe IP. +	if !SafeIP(ip) {  		return ErrReservedAddr  	}  	return nil  } + +// SafeIP returns whether ip is an IPv4/6 +// address in a non-reserved, public range. +func SafeIP(ip netip.Addr) bool { +	switch { +	// IPv4: check if IPv4 in reserved nets +	case ip.Is4(): +		for _, reserved := range ipv4Reserved { +			if reserved.Contains(ip) { +				return false +			} +		} +		return true + +	// IPv6: check if IP in IPv6 reserved nets +	case ip.Is6(): +		if !ipv6GlobalUnicast.Contains(ip) { +			// Address is not globally routeable, +			// ie., not "on the internet". +			return false +		} + +		for _, reserved := range ipv6Reserved { +			if reserved.Contains(ip) { +				// Address is globally routeable +				// but falls in a reserved range. +				return false +			} +		} +		return true + +	// Assume malicious by default +	default: +		return false +	} +} diff --git a/internal/httpclient/sanitizer_test.go b/internal/httpclient/sanitizer_test.go new file mode 100644 index 000000000..1cb8a7d2e --- /dev/null +++ b/internal/httpclient/sanitizer_test.go @@ -0,0 +1,154 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +package httpclient_test + +import ( +	"errors" +	"net/netip" +	"testing" + +	"github.com/superseriousbusiness/gotosocial/internal/httpclient" +) + +func TestSafeIP(t *testing.T) { +	tests := []struct { +		name string +		ip   netip.Addr +	}{ +		// IPv4 tests +		{ +			name: "IPv4 this host on this network", +			ip:   netip.MustParseAddr("0.0.0.0"), +		}, +		{ +			name: "IPv4 dummy address", +			ip:   netip.MustParseAddr("192.0.0.8"), +		}, +		{ +			name: "IPv4 Port Control Protocol Anycast", +			ip:   netip.MustParseAddr("192.0.0.9"), +		}, +		{ +			name: "IPv4 Traversal Using Relays around NAT Anycast", +			ip:   netip.MustParseAddr("192.0.0.10"), +		}, +		{ +			name: "IPv4 NAT64/DNS64 Discovery 1", +			ip:   netip.MustParseAddr("192.0.0.17"), +		}, +		{ +			name: "IPv4 NAT64/DNS64 Discovery 2", +			ip:   netip.MustParseAddr("192.0.0.171"), +		}, +		// IPv6 tests +		{ +			name: "IPv4-mapped address", +			ip:   netip.MustParseAddr("::ffff:169.254.169.254"), +		}, +	} + +	for _, tc := range tests { +		tc := tc +		t.Run(tc.name, func(t *testing.T) { +			t.Parallel() +			if safe := httpclient.SafeIP(tc.ip); safe { +				t.Fatalf("Expected IP %s to not safe (%t), got: %t", tc.ip, false, safe) +			} +		}) +	} +} + +func TestSanitizer(t *testing.T) { +	s := httpclient.Sanitizer{ +		Allow: []netip.Prefix{ +			netip.MustParsePrefix("192.0.0.8/32"), +			netip.MustParsePrefix("::ffff:169.254.169.254/128"), +		}, +		Block: []netip.Prefix{ +			netip.MustParsePrefix("93.184.216.34/32"), // example.org +		}, +	} + +	tests := []struct { +		name     string +		ntwrk    string +		addr     string +		expected error +	}{ +		// IPv4 tests +		{ +			name:     "IPv4 this host on this network", +			ntwrk:    "tcp4", +			addr:     "0.0.0.0:80", +			expected: httpclient.ErrReservedAddr, +		}, +		{ +			name:     "IPv4 dummy address", +			ntwrk:    "tcp4", +			addr:     "192.0.0.8:80", +			expected: nil, // We allowed this explicitly. +		}, +		{ +			name:     "IPv4 Port Control Protocol Anycast", +			ntwrk:    "tcp4", +			addr:     "192.0.0.9:80", +			expected: httpclient.ErrReservedAddr, +		}, +		{ +			name:     "IPv4 Traversal Using Relays around NAT Anycast", +			ntwrk:    "tcp4", +			addr:     "192.0.0.10:80", +			expected: httpclient.ErrReservedAddr, +		}, +		{ +			name:     "IPv4 NAT64/DNS64 Discovery 1", +			ntwrk:    "tcp4", +			addr:     "192.0.0.17:80", +			expected: httpclient.ErrReservedAddr, +		}, +		{ +			name:     "IPv4 NAT64/DNS64 Discovery 2", +			ntwrk:    "tcp4", +			addr:     "192.0.0.171:80", +			expected: httpclient.ErrReservedAddr, +		}, +		{ +			name:     "example.org", +			ntwrk:    "tcp4", +			addr:     "93.184.216.34:80", +			expected: httpclient.ErrReservedAddr, // We blocked this explicitly. +		}, +		// IPv6 tests +		{ +			name:     "IPv4-mapped address", +			ntwrk:    "tcp6", +			addr:     "[::ffff:169.254.169.254]:80", +			expected: nil, // We allowed this explicitly. +		}, +	} + +	for _, tc := range tests { +		tc := tc +		t.Run(tc.name, func(t *testing.T) { +			t.Parallel() +			if err := s.Sanitize(tc.ntwrk, tc.addr, nil); !errors.Is(err, tc.expected) { +				t.Fatalf("Expected error %q for addr %s, got: %q", tc.expected, tc.addr, err) +			} +		}) +	} +} diff --git a/internal/netutil/validate.go b/internal/netutil/validate.go deleted file mode 100644 index 798e1fc64..000000000 --- a/internal/netutil/validate.go +++ /dev/null @@ -1,102 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program.  If not, see <http://www.gnu.org/licenses/>. - -package netutil - -import ( -	"net/netip" -) - -var ( -	// IPv6Reserved contains IPv6 reserved IP prefixes. -	// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml -	IPv6Reserved = [...]netip.Prefix{ -		netip.MustParsePrefix("::1/128"),           // Loopback -		netip.MustParsePrefix("::/128"),            // Unspecified address -		netip.MustParsePrefix("::ffff:0:0/96"),     // IPv4-mapped address -		netip.MustParsePrefix("64:ff9b::/96"),      // IPv4/IPv6 translation, RFC 6052 -		netip.MustParsePrefix("64:ff9b:1::/48"),    // IPv4/IPv6 translation, RFC 8215 -		netip.MustParsePrefix("100::/64"),          // Discard prefix, RFC 6666 -		netip.MustParsePrefix("2001::/23"),         // IETF Protocol Assignments, RFC 2928 -		netip.MustParsePrefix("2001:db8::/32"),     // Test, doc, examples -		netip.MustParsePrefix("2002::/16"),         // 6to4 -		netip.MustParsePrefix("2620:4f:8000::/48"), // Direct Delegation AS112 Service, RFC 7534 -		netip.MustParsePrefix("fc00::/7"),          // Unique Local -		netip.MustParsePrefix("fe80::/10"),         // Link-local -		netip.MustParsePrefix("fec0::/10"),         // Site-local, deprecated -		netip.MustParsePrefix("ff00::/8"),          // Multicast -	} - -	// IPv4Reserved contains IPv4 reserved IP prefixes. -	// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml -	IPv4Reserved = [...]netip.Prefix{ -		netip.MustParsePrefix("0.0.0.0/8"),       // Current network -		netip.MustParsePrefix("10.0.0.0/8"),      // Private -		netip.MustParsePrefix("100.64.0.0/10"),   // RFC6598 -		netip.MustParsePrefix("127.0.0.0/8"),     // Loopback -		netip.MustParsePrefix("169.254.0.0/16"),  // Link-local -		netip.MustParsePrefix("172.16.0.0/12"),   // Private -		netip.MustParsePrefix("192.0.0.0/24"),    // RFC6890 -		netip.MustParsePrefix("192.0.2.0/24"),    // Test, doc, examples -		netip.MustParsePrefix("192.31.196.0/24"), // AS112-v4, RFC 7535 -		netip.MustParsePrefix("192.52.193.0/24"), // AMT, RFC 7450 -		netip.MustParsePrefix("192.88.99.0/24"),  // IPv6 to IPv4 relay -		netip.MustParsePrefix("192.168.0.0/16"),  // Private -		netip.MustParsePrefix("192.175.48.0/24"), // Direct Delegation AS112 Service, RFC 7534 -		netip.MustParsePrefix("198.18.0.0/15"),   // Benchmarking tests -		netip.MustParsePrefix("198.51.100.0/24"), // Test, doc, examples -		netip.MustParsePrefix("203.0.113.0/24"),  // Test, doc, examples -		netip.MustParsePrefix("224.0.0.0/4"),     // Multicast -		netip.MustParsePrefix("240.0.0.0/4"),     // Reserved (includes broadcast / 255.255.255.255) -	} -) - -// ValidateAddr will parse a netip.AddrPort from string, and return the result of ValidateIP() on addr. -func ValidateAddr(s string) bool { -	ipport, err := netip.ParseAddrPort(s) -	if err != nil { -		return false -	} -	return ValidateIP(ipport.Addr()) -} - -// ValidateIP returns whether IP is an IPv4/6 address in non-reserved, public ranges. -func ValidateIP(ip netip.Addr) bool { -	switch { -	// IPv4: check if IPv4 in reserved nets -	case ip.Is4(): -		for _, reserved := range IPv4Reserved { -			if reserved.Contains(ip) { -				return false -			} -		} -		return true - -	// IPv6: check if IP in IPv6 reserved nets -	case ip.Is6(): -		for _, reserved := range IPv6Reserved { -			if reserved.Contains(ip) { -				return false -			} -		} -		return true - -	// Assume malicious by default -	default: -		return false -	} -} diff --git a/internal/netutil/validate_test.go b/internal/netutil/validate_test.go deleted file mode 100644 index 37def4ce6..000000000 --- a/internal/netutil/validate_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package netutil - -import ( -	"net/netip" -	"testing" -) - -func TestValidateIP(t *testing.T) { -	tests := []struct { -		name string -		ip   netip.Addr -	}{ -		// IPv4 tests -		{ -			name: "IPv4 this host on this network", -			ip:   netip.MustParseAddr("0.0.0.0"), -		}, -		{ -			name: "IPv4 dummy address", -			ip:   netip.MustParseAddr("192.0.0.8"), -		}, -		{ -			name: "IPv4 Port Control Protocol Anycast", -			ip:   netip.MustParseAddr("192.0.0.9"), -		}, -		{ -			name: "IPv4 Traversal Using Relays around NAT Anycast", -			ip:   netip.MustParseAddr("192.0.0.10"), -		}, -		{ -			name: "IPv4 NAT64/DNS64 Discovery 1", -			ip:   netip.MustParseAddr("192.0.0.17"), -		}, -		{ -			name: "IPv4 NAT64/DNS64 Discovery 2", -			ip:   netip.MustParseAddr("192.0.0.171"), -		}, -		// IPv6 tests -		{ -			name: "IPv4-mapped address", -			ip:   netip.MustParseAddr("::ffff:169.254.169.254"), -		}, -	} - -	for _, tc := range tests { -		tc := tc -		t.Run(tc.name, func(t *testing.T) { -			t.Parallel() -			if valid := ValidateIP(tc.ip); valid != false { -				t.Fatalf("Expected IP %s to be: %t, got: %t", tc.ip, false, valid) -			} -		}) -	} -}  | 
