[feature] enable + document explicit IP dialer allowing/denying (#1950)

* [feature] enable + document explicit IP dialer allowing/denying

* lord have mercy

* allee jonge

* shortcut check ipv6 prefixes

* comment

* separate httpclient_test, export Sanitizer
This commit is contained in:
tobi 2023-07-07 16:17:39 +02:00 committed by GitHub
parent ac564c1862
commit 2a99df0588
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 554 additions and 200 deletions

View file

@ -105,8 +105,12 @@ var Start action.GTSAction = func(ctx context.Context) error {
// Set the state storage driver // Set the state storage driver
state.Storage = storage state.Storage = storage
// Build HTTP client (TODO: add configurables here) // Build HTTP client
client := httpclient.New(httpclient.Config{}) client := httpclient.New(httpclient.Config{
AllowRanges: config.MustParseIPPrefixes(config.GetHTTPClientAllowIPs()),
BlockRanges: config.MustParseIPPrefixes(config.GetHTTPClientBlockIPs()),
Timeout: config.GetHTTPClientTimeout(),
})
// Initialize workers. // Initialize workers.
state.Workers.Start() state.Workers.Start()

View file

@ -0,0 +1,56 @@
# HTTP Client
## Settings
```yaml
################################
##### HTTP CLIENT SETTINGS #####
################################
# Settings for OUTGOING http client connections used by GoToSocial to make
# requests to remote resources (status GETs, media GETs, inbox POSTs, etc).
http-client:
# Duration. Timeout to use for outgoing HTTP requests. If the timeout
# is exceeded, the connection to the remote server will be dropped.
# A value of 0s indicates no timeout: this is not advised!
# Examples: ["5s", "10s", "0s"]
# Default: "10s"
timeout: "10s"
########################################
#### RESERVED IP RANGE EXCEPTIONS ######
########################################
#
# Explicitly allow or block outgoing dialing within the provided IPv4/v6 CIDR ranges.
#
# By default, as a basic security precaution, GoToSocial blocks outgoing dialing within most "special-purpose"
# IP ranges. However, it may be desirable for admins with more exotic setups (proxies, funky NAT, etc) to
# explicitly override one or more of these otherwise blocked ranges.
#
# Each of the below allow/block config options accepts an array of IPv4 and/or IPv6 CIDR strings.
# For example, to override the hardcoded block of IPv4 and IPv6 dialing to localhost, set:
#
# allow-ips: ["127.0.0.1/32", "::1/128"].
#
# You can also use YAML multi-line arrays to define these, but be diligent with indentation.
#
# When dialing, GoToSocial will first check if the destination falls within explicitly allowed IP ranges,
# then explicitly blocked IP ranges, then the default (hardcoded) blocked IP ranges, returning OK on the
# first allowed match, not OK on the first blocked match, or just defaulting to OK if nothing is matched.
#
# As with all security settings, it is better to start too restrictive and then ease off depending on
# your use case, than to start too permissive and try to close the stable door after the horse has
# already bolted. With this in mind:
# - Don't touch these settings unless you have a good reason to, and only if you know what you're doing.
# - When adding explicitly allowed exceptions, use the narrowest possible CIDR for your use case.
#
# For reserved / special ranges, see:
# - https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
# - https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
#
# Both allow-ips and block-ips default to an empty array.
allow-ips: []
block-ips: []
```

View file

@ -1,29 +1,50 @@
# Frequently Asked Questions # Frequently Asked Questions
- **Where's the user interface?** GoToSocial is just a bare server for the most part and is designed to be used thru external applications. [Semaphore](https://semaphore.social/) and [Tusky](https://tusky.app/) are the best-supported, but anything that supports the Mastodon API should work, other than the features GoToSocial doesn't yet have. Permalinks and profile pages are served directly thru GoToSocial as well as the settings panel, but most interaction goes thru the apps. ## Where's the user interface?
- **Why aren't my posts showing up on my profile page?** Unlike Mastodon, the default post visibility is Unlisted. If you want something to be visible on your profile page, the post must have Public visibility. GoToSocial is just a bare server for the most part and is designed to be used thru external applications. [Semaphore](https://semaphore.social/) and [Tusky](https://tusky.app/) are the best-supported, but anything that supports the Mastodon API should work, other than the features GoToSocial doesn't yet have. Permalinks and profile pages are served directly through GoToSocial as well as the settings panel, but most interaction goes through the apps.
- **Why aren't my posts showing up on other servers?** First check the visibility as noted above. TODO: explain how to debug common federation issues ## Why aren't my posts showing up on my profile page?
- **Why am I getting frequent http 429 error responses?** GoToSocial is configured to use per-IP [rate limiting](./api/ratelimiting.md) by default, but in certain situations it can't accurately identify the remote IP and will treat all connections as coming from the same place. In those cases, the rate limiting needs to be disabled or reconfigured. Unlike Mastodon, the default post visibility is Unlisted. If you want something to be visible on your profile page, the post must have Public visibility.
- **Why am I getting frequent http 503 error responses?** Code 503 is returned to callers when your instance is under heavy load and requests are being throttled. This behavior can be tuned as desired, or turned off entirely, see [here](./api/throttling.md). ## Why aren't my posts showing up on other servers?
- **I keep getting a 400 Bad Request error, and I have done everything suggested by the error message. What should I do?** Verify that the `host` configuration matches the domain that GoToSocial is served from (the domain that users use to acces the server). First check the visibility as noted above. TODO: explain how to debug common federation issues
- **My instance is deployed and I'm logged in to a client but my timelines are empty, what's up there?** To see posts, you have to start following people! Once you've followed a few people and they've posted or boosted things, you'll start seeing them in your timelines. Right now GoToSocial doesn't have a way of 'backfilling' posts -- that is, fetching previous posts from other instances -- so you'll only see new posts of people you follow. If you want to interact with an older post of theirs, you can copy the link to the post from their web profile, and paste it in to your client's search bar. ## Why am I getting frequent http 429 error responses?
- **How can I sign up for a server?** Right now the only way to create an account is by the server's admin to run a command directly on the server. A web-based signup flow is in the roadmap but not implemented yet. GoToSocial is configured to use per-IP [rate limiting](./api/ratelimiting.md) by default, but in certain situations it can't accurately identify the remote IP and will treat all connections as coming from the same place. In those cases, the rate limiting needs to be disabled or reconfigured.
- **Why's it still in alpha?** Take a look at the [list of open bugs](https://github.com/superseriousbusiness/gotosocial/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and the [roadmap](https://github.com/superseriousbusiness/gotosocial/blob/main/ROADMAP.md) for a more detailed rundown, but the main missing features at the time of this writing are: ## Why am I getting frequent HTTP 503 error responses?
* muting conversations
* backfill of posts Code 503 is returned to callers when your instance is under heavy load and requests are being throttled. This behavior can be tuned as desired, or turned off entirely, see [here](./api/throttling.md).
* web-based signup
* profile metadata fields ## I keep getting a 400 Bad Request error, and I have done everything suggested by the error message. What should I do?
* lists of users
* polls Verify that the `host` configuration matches the domain that GoToSocial is served from (the domain that users use to acces the server).
* scheduling posts
* account migration ## I keep seeing 'dial within blocked / reserved IP range' in my server logs, and I can't connect to some instances from my instance, what do I do?
* federated hashtag search
* shared block lists across servers The IP address of the remote instance may be in one of the blocked "special use" IP ranges hardcoded into GoToSocial for security reasons. If you need to, you can override this in your configuration file. Have a look at the [http client docs](./configuration/httpclient.md) for this, and please read the warnings there carefully! If you add an explicit allow, you will have to restart your GoToSocial instance to make the config take effect.
## My instance is deployed and I'm logged in to a client but my timelines are empty, what's up there?
To see posts, you have to start following people! Once you've followed a few people and they've posted or boosted things, you'll start seeing them in your timelines. Right now GoToSocial doesn't have a way of 'backfilling' posts -- that is, fetching previous posts from other instances -- so you'll only see new posts of people you follow. If you want to interact with an older post of theirs, you can copy the link to the post from their web profile, and paste it in to your client's search bar.
## How can I sign up for a server?
Right now the only way to create an account is by the server's admin to run a command directly on the server. A web-based signup flow is in the roadmap but not implemented yet.
## Why's it still in alpha?
Take a look at the [list of open bugs](https://github.com/superseriousbusiness/gotosocial/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and the [roadmap](https://github.com/superseriousbusiness/gotosocial/blob/main/ROADMAP.md) for a more detailed rundown, but the main missing features at the time of this writing are:
- muting conversations
- backfill of posts
- web-based signup
- polls
- scheduling posts
- account migration
- federated hashtag search
- shared block lists across servers

View file

@ -813,6 +813,57 @@ tracing-endpoint: ""
# Default: false # Default: false
tracing-insecure-transport: false tracing-insecure-transport: false
################################
##### HTTP CLIENT SETTINGS #####
################################
# Settings for OUTGOING http client connections used by GoToSocial to make
# requests to remote resources (status GETs, media GETs, inbox POSTs, etc).
http-client:
# Duration. Timeout to use for outgoing HTTP requests. If the timeout
# is exceeded, the connection to the remote server will be dropped.
# A value of 0s indicates no timeout: this is not advised!
# Examples: ["5s", "10s", "0s"]
# Default: "10s"
timeout: "10s"
########################################
#### RESERVED IP RANGE EXCEPTIONS ######
########################################
#
# Explicitly allow or block outgoing dialing within the provided IPv4/v6 CIDR ranges.
#
# By default, as a basic security precaution, GoToSocial blocks outgoing dialing within most "special-purpose"
# IP ranges. However, it may be desirable for admins with more exotic setups (proxies, funky NAT, etc) to
# explicitly override one or more of these otherwise blocked ranges.
#
# Each of the below allow/block config options accepts an array of IPv4 and/or IPv6 CIDR strings.
# For example, to override the hardcoded block of IPv4 and IPv6 dialing to localhost, set:
#
# allow-ips: ["127.0.0.1/32", "::1/128"].
#
# You can also use YAML multi-line arrays to define these, but be diligent with indentation.
#
# When dialing, GoToSocial will first check if the destination falls within explicitly allowed IP ranges,
# then explicitly blocked IP ranges, then the default (hardcoded) blocked IP ranges, returning OK on the
# first allowed match, not OK on the first blocked match, or just defaulting to OK if nothing is matched.
#
# As with all security settings, it is better to start too restrictive and then ease off depending on
# your use case, than to start too permissive and try to close the stable door after the horse has
# already bolted. With this in mind:
# - Don't touch these settings unless you have a good reason to, and only if you know what you're doing.
# - When adding explicitly allowed exceptions, use the narrowest possible CIDR for your use case.
#
# For reserved / special ranges, see:
# - https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
# - https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
#
# Both allow-ips and block-ips default to an empty array.
allow-ips: []
block-ips: []
############################# #############################
##### ADVANCED SETTINGS ##### ##### ADVANCED SETTINGS #####
############################# #############################

View file

@ -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."` 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)."` 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 configuration vars.
Cache CacheConfiguration `name:"cache"` 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'."` 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 { type CacheConfiguration struct {
GTS GTSCacheConfiguration `name:"gts"` GTS GTSCacheConfiguration `name:"gts"`

View file

@ -208,6 +208,12 @@ var Defaults = Configuration{
VisibilitySweepFreq: time.Minute, VisibilitySweepFreq: time.Minute,
}, },
HTTPClient: HTTPClientConfiguration{
AllowIPs: make([]string, 0),
BlockIPs: make([]string, 0),
Timeout: 10 * time.Second,
},
AdminMediaPruneDryRun: true, AdminMediaPruneDryRun: true,
RequestIDHeader: "X-Request-Id", RequestIDHeader: "X-Request-Id",

View file

@ -55,6 +55,11 @@ func (s *ConfigState) AddGlobalFlags(cmd *cobra.Command) {
cmd.PersistentFlags().String(DbSqliteSynchronousFlag(), cfg.DbSqliteSynchronous, fieldtag("DbSqliteSynchronous", "usage")) cmd.PersistentFlags().String(DbSqliteSynchronousFlag(), cfg.DbSqliteSynchronous, fieldtag("DbSqliteSynchronous", "usage"))
cmd.PersistentFlags().Uint64(DbSqliteCacheSizeFlag(), uint64(cfg.DbSqliteCacheSize), fieldtag("DbSqliteCacheSize", "usage")) cmd.PersistentFlags().Uint64(DbSqliteCacheSizeFlag(), uint64(cfg.DbSqliteCacheSize), fieldtag("DbSqliteCacheSize", "usage"))
cmd.PersistentFlags().Duration(DbSqliteBusyTimeoutFlag(), cfg.DbSqliteBusyTimeout, fieldtag("DbSqliteBusyTimeout", "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")
}) })
} }

View file

@ -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.Join(append(prefixes, field.Tag.Get("name")), "-")
flagPath = strings.ToLower(flagPath) flagPath = strings.ToLower(flagPath)
// Get type without "config." prefix.
fieldType := strings.ReplaceAll(
field.Type.String(),
"config.", "",
)
// ConfigState structure helper methods // 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, "// 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, "\tst.mutex.Lock()\n")
fmt.Fprintf(output, "\tv = st.config.%s\n", fieldPath) fmt.Fprintf(output, "\tv = st.config.%s\n", fieldPath)
fmt.Fprintf(output, "\tst.mutex.Unlock()\n") fmt.Fprintf(output, "\tst.mutex.Unlock()\n")
fmt.Fprintf(output, "\treturn\n") fmt.Fprintf(output, "\treturn\n")
fmt.Fprintf(output, "}\n\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, "// 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, "\tst.mutex.Lock()\n")
fmt.Fprintf(output, "\tdefer st.mutex.Unlock()\n") fmt.Fprintf(output, "\tdefer st.mutex.Unlock()\n")
fmt.Fprintf(output, "\tst.config.%s = v\n", fieldPath) 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, "// %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, "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, "// 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, "// 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)
} }
} }

View file

@ -2299,6 +2299,81 @@ func GetAdvancedSenderMultiplier() int { return global.GetAdvancedSenderMultipli
// SetAdvancedSenderMultiplier safely sets the value for global configuration 'AdvancedSenderMultiplier' field // SetAdvancedSenderMultiplier safely sets the value for global configuration 'AdvancedSenderMultiplier' field
func SetAdvancedSenderMultiplier(v int) { global.SetAdvancedSenderMultiplier(v) } 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 // GetCacheGTSAccountMaxSize safely fetches the Configuration value for state's 'Cache.GTS.AccountMaxSize' field
func (st *ConfigState) GetCacheGTSAccountMaxSize() (v int) { func (st *ConfigState) GetCacheGTSAccountMaxSize() (v int) {
st.mutex.Lock() st.mutex.Lock()

39
internal/config/util.go Normal file
View file

@ -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
}

View file

@ -130,9 +130,9 @@ func New(cfg Config) *Client {
} }
// Protect dialer with IP range sanitizer. // Protect dialer with IP range sanitizer.
d.Control = (&sanitizer{ d.Control = (&Sanitizer{
allow: cfg.AllowRanges, Allow: cfg.AllowRanges,
block: cfg.BlockRanges, Block: cfg.BlockRanges,
}).Sanitize }).Sanitize
// Prepare client fields. // Prepare client fields.

View file

@ -20,48 +20,126 @@ package httpclient
import ( import (
"net/netip" "net/netip"
"syscall" "syscall"
"github.com/superseriousbusiness/gotosocial/internal/netutil"
) )
type sanitizer struct { var (
allow []netip.Prefix // ipv6GlobalUnicast is the prefix set aside by IANA for global unicast assignments, i.e "the internet".
block []netip.Prefix // 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)
}
// 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
} }
// Sanitize implements the required net.Dialer.Control function signature. // 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 // Parse IP+port from addr
ipport, err := netip.ParseAddrPort(addr) ipport, err := netip.ParseAddrPort(addr)
if err != nil { if err != nil {
return err return err
} }
if !(ntwrk == "tcp4" || ntwrk == "tcp6") { // Ensure valid network.
const (
tcp4 = "tcp4"
tcp6 = "tcp6"
)
if !(ntwrk == tcp4 || ntwrk == tcp6) {
return ErrInvalidNetwork return ErrInvalidNetwork
} }
// Seperate the IP // Separate the IP.
ip := ipport.Addr() ip := ipport.Addr()
// Check if this is explicitly allowed // Check if this IP is explicitly allowed.
for i := 0; i < len(s.allow); i++ { for i := 0; i < len(s.Allow); i++ {
if s.allow[i].Contains(ip) { if s.Allow[i].Contains(ip) {
return nil return nil
} }
} }
// Now check if explicity blocked // Check if this IP is explicitly blocked.
for i := 0; i < len(s.block); i++ { for i := 0; i < len(s.Block); i++ {
if s.block[i].Contains(ip) { if s.Block[i].Contains(ip) {
return ErrReservedAddr return ErrReservedAddr
} }
} }
// Validate this is a safe IP // Validate this is a safe IP.
if !netutil.ValidateIP(ip) { if !SafeIP(ip) {
return ErrReservedAddr return ErrReservedAddr
} }
return nil 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
}
}

View file

@ -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)
}
})
}
}

View file

@ -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
}
}

View file

@ -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)
}
})
}
}

View file

@ -81,6 +81,7 @@ nav:
- "configuration/oidc.md" - "configuration/oidc.md"
- "configuration/smtp.md" - "configuration/smtp.md"
- "configuration/syslog.md" - "configuration/syslog.md"
- "configuration/httpclient.md"
- "configuration/advanced.md" - "configuration/advanced.md"
- "configuration/observability.md" - "configuration/observability.md"
- "Advanced": - "Advanced":

View file

@ -98,6 +98,11 @@ EXPECT=$(cat <<"EOF"
"dry-run": true, "dry-run": true,
"email": "", "email": "",
"host": "example.com", "host": "example.com",
"http-client": {
"allow-ips": [],
"block-ips": [],
"timeout": 10000000000
},
"instance-deliver-to-shared-inboxes": false, "instance-deliver-to-shared-inboxes": false,
"instance-expose-peers": true, "instance-expose-peers": true,
"instance-expose-public-timeline": true, "instance-expose-public-timeline": true,