hardening(proxy): don't proxy private IP addresses

...by default.

This is useful when `RelayDomainNamePattern` is lax (e.g. just "$")
(which is not the case by default, so this is simply
a hardening measure).
This commit is contained in:
WofWca 2024-08-31 22:05:34 +04:00 committed by Shelikhoo
parent 399bda5257
commit 94c6089cdd
No known key found for this signature in database
GPG key ID: 4C9764E9FE80A3DC
4 changed files with 45 additions and 9 deletions

View file

@ -31,6 +31,9 @@ The Snowflake proxy can be run with the following options:
Usage of ./proxy: Usage of ./proxy:
-allow-non-tls-relay -allow-non-tls-relay
allow relay without tls encryption allow relay without tls encryption
-allow-proxying-to-private-addresses
allow forwarding client connections to private IP addresses.
Useful when a Snowflake server (relay) is hosted on the same private network as this proxy.
-allowed-relay-hostname-pattern string -allowed-relay-hostname-pattern string
a pattern to specify allowed hostname pattern for relay URL. (default "snowflake.torproject.net$") a pattern to specify allowed hostname pattern for relay URL. (default "snowflake.torproject.net$")
-broker string -broker string

View file

@ -496,6 +496,7 @@ func TestUtilityFuncs(t *testing.T) {
Convey("isRelayURLAcceptable", t, func() { Convey("isRelayURLAcceptable", t, func() {
testingVector := []struct { testingVector := []struct {
pattern string pattern string
allowPrivateAddresses bool
allowNonTLS bool allowNonTLS bool
targetURL string targetURL string
expects error expects error
@ -525,6 +526,20 @@ func TestUtilityFuncs(t *testing.T) {
{pattern: "^1.1.1.1$", allowNonTLS: true, targetURL: "ws://1.1.1.1/test?test=test#test", expects: nil}, {pattern: "^1.1.1.1$", allowNonTLS: true, targetURL: "ws://1.1.1.1/test?test=test#test", expects: nil},
{pattern: "^1.1.1.1$", allowNonTLS: true, targetURL: "ws://231.1.1.1/test?test=test#test", expects: fmt.Errorf("")}, {pattern: "^1.1.1.1$", allowNonTLS: true, targetURL: "ws://231.1.1.1/test?test=test#test", expects: fmt.Errorf("")},
{pattern: "1.1.1.1$", allowNonTLS: true, targetURL: "ws://231.1.1.1/test?test=test#test", expects: nil}, {pattern: "1.1.1.1$", allowNonTLS: true, targetURL: "ws://231.1.1.1/test?test=test#test", expects: nil},
// Private IP address
{pattern: "$", allowNonTLS: true, targetURL: "ws://192.168.1.1", expects: fmt.Errorf("")},
{pattern: "$", allowNonTLS: true, targetURL: "ws://127.0.0.1", expects: fmt.Errorf("")},
{pattern: "$", allowNonTLS: true, targetURL: "ws://[fc00::]/", expects: fmt.Errorf("")},
{pattern: "$", allowNonTLS: true, targetURL: "ws://[::1]/", expects: fmt.Errorf("")},
{pattern: "$", allowNonTLS: true, targetURL: "ws://0.0.0.0/", expects: fmt.Errorf("")},
{pattern: "$", allowNonTLS: true, targetURL: "ws://169.254.1.1/", expects: fmt.Errorf("")},
{pattern: "$", allowNonTLS: true, targetURL: "ws://100.111.1.1/", expects: fmt.Errorf("")},
{pattern: "192.168.1.100$", allowPrivateAddresses: true, allowNonTLS: true, targetURL: "ws://192.168.1.100/test?test=test", expects: nil},
{pattern: "localhost$", allowPrivateAddresses: true, allowNonTLS: true, targetURL: "ws://localhost/test?test=test", expects: nil},
{pattern: "::1$", allowPrivateAddresses: true, allowNonTLS: true, targetURL: "ws://[::1]/test?test=test", expects: nil},
// Multicast IP address. `checkIsRelayURLAcceptable` allows it,
// but it's not valid in the context of WebSocket
{pattern: "255.255.255.255$", allowPrivateAddresses: true, allowNonTLS: true, targetURL: "ws://255.255.255.255/test?test=test", expects: nil},
// Port // Port
{pattern: "^snowflake.torproject.net$", allowNonTLS: false, targetURL: "wss://snowflake.torproject.net:8080/test?test=test#test", expects: nil}, {pattern: "^snowflake.torproject.net$", allowNonTLS: false, targetURL: "wss://snowflake.torproject.net:8080/test?test=test#test", expects: nil},
@ -551,7 +566,7 @@ func TestUtilityFuncs(t *testing.T) {
{pattern: "snowflake.torproject.net$", allowNonTLS: true, targetURL: "ftp://snowflake.torproject.net", expects: fmt.Errorf("")}, {pattern: "snowflake.torproject.net$", allowNonTLS: true, targetURL: "ftp://snowflake.torproject.net", expects: fmt.Errorf("")},
} }
for _, v := range testingVector { for _, v := range testingVector {
err := checkIsRelayURLAcceptable(v.pattern, v.allowNonTLS, v.targetURL) err := checkIsRelayURLAcceptable(v.pattern, v.allowPrivateAddresses, v.allowNonTLS, v.targetURL)
if v.expects != nil { if v.expects != nil {
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
} else { } else {

View file

@ -138,6 +138,11 @@ type SnowflakeProxy struct {
// There is no look ahead assertion when matching domain name suffix, // There is no look ahead assertion when matching domain name suffix,
// thus the string prepend the suffix does not need to be empty or ends with a dot. // thus the string prepend the suffix does not need to be empty or ends with a dot.
RelayDomainNamePattern string RelayDomainNamePattern string
// AllowProxyingToPrivateAddresses determines whether to allow forwarding
// client connections to private IP addresses.
// Useful when a Snowflake server (relay) is hosted on the same private network
// as this proxy.
AllowProxyingToPrivateAddresses bool
AllowNonTLSRelay bool AllowNonTLSRelay bool
// NATProbeURL is the URL of the probe service we use for NAT checks // NATProbeURL is the URL of the probe service we use for NAT checks
NATProbeURL string NATProbeURL string
@ -601,7 +606,7 @@ func (sf *SnowflakeProxy) runSession(sid string) {
log.Printf("Received Offer From Broker: \n\t%s", strings.ReplaceAll(offer.SDP, "\n", "\n\t")) log.Printf("Received Offer From Broker: \n\t%s", strings.ReplaceAll(offer.SDP, "\n", "\n\t"))
if relayURL != "" { if relayURL != "" {
if err := checkIsRelayURLAcceptable(sf.RelayDomainNamePattern, sf.AllowNonTLSRelay, relayURL); err != nil { if err := checkIsRelayURLAcceptable(sf.RelayDomainNamePattern, sf.AllowProxyingToPrivateAddresses, sf.AllowNonTLSRelay, relayURL); err != nil {
log.Printf("bad offer from broker: %v", err) log.Printf("bad offer from broker: %v", err)
tokens.ret() tokens.ret()
return return
@ -644,6 +649,7 @@ func (sf *SnowflakeProxy) runSession(sid string) {
// Returns nil if the relayURL is acceptable // Returns nil if the relayURL is acceptable
func checkIsRelayURLAcceptable( func checkIsRelayURLAcceptable(
allowedHostNamePattern string, allowedHostNamePattern string,
allowPrivateIPs bool,
allowNonTLSRelay bool, allowNonTLSRelay bool,
relayURL string, relayURL string,
) error { ) error {
@ -651,6 +657,16 @@ func checkIsRelayURLAcceptable(
if err != nil { if err != nil {
return fmt.Errorf("bad Relay URL %w", err) return fmt.Errorf("bad Relay URL %w", err)
} }
if !allowPrivateIPs {
ip := net.ParseIP(parsedRelayURL.Hostname())
// Otherwise it's a domain name, or an invalid IP.
if ip != nil {
// We should probably use a ready library for this.
if !isRemoteAddress(ip) {
return fmt.Errorf("rejected Relay URL: private IPs are not allowed")
}
}
}
if !allowNonTLSRelay && parsedRelayURL.Scheme != "wss" { if !allowNonTLSRelay && parsedRelayURL.Scheme != "wss" {
return fmt.Errorf("rejected Relay URL protocol: non-TLS not allowed") return fmt.Errorf("rejected Relay URL protocol: non-TLS not allowed")
} }

View file

@ -32,6 +32,7 @@ func main() {
probeURL := flag.String("nat-probe-server", sf.DefaultNATProbeURL, "NAT check probe server URL") probeURL := flag.String("nat-probe-server", sf.DefaultNATProbeURL, "NAT check probe server URL")
outboundAddress := flag.String("outbound-address", "", "prefer the given address as outbound address") outboundAddress := flag.String("outbound-address", "", "prefer the given address as outbound address")
allowedRelayHostNamePattern := flag.String("allowed-relay-hostname-pattern", "snowflake.torproject.net$", "a pattern to specify allowed hostname pattern for relay URL.") allowedRelayHostNamePattern := flag.String("allowed-relay-hostname-pattern", "snowflake.torproject.net$", "a pattern to specify allowed hostname pattern for relay URL.")
allowProxyingToPrivateAddresses := flag.Bool("allow-proxying-to-private-addresses", false, "allow forwarding client connections to private IP addresses.\nUseful when a Snowflake server (relay) is hosted on the same private network as this proxy.")
allowNonTLSRelay := flag.Bool("allow-non-tls-relay", false, "allow relay without tls encryption") allowNonTLSRelay := flag.Bool("allow-non-tls-relay", false, "allow relay without tls encryption")
NATTypeMeasurementInterval := flag.Duration("nat-retest-interval", time.Hour*24, NATTypeMeasurementInterval := flag.Duration("nat-retest-interval", time.Hour*24,
"the time interval in second before NAT type is retested, 0s disables retest. Valid time units are \"s\", \"m\", \"h\". ") "the time interval in second before NAT type is retested, 0s disables retest. Valid time units are \"s\", \"m\", \"h\". ")
@ -106,6 +107,7 @@ func main() {
EventDispatcher: eventLogger, EventDispatcher: eventLogger,
RelayDomainNamePattern: *allowedRelayHostNamePattern, RelayDomainNamePattern: *allowedRelayHostNamePattern,
AllowProxyingToPrivateAddresses: *allowProxyingToPrivateAddresses,
AllowNonTLSRelay: *allowNonTLSRelay, AllowNonTLSRelay: *allowNonTLSRelay,
SummaryInterval: *summaryInterval, SummaryInterval: *summaryInterval,