Add a orport-srcaddr server transport option.

The option controls what source address to use when dialing the
(Ext)ORPort. Using a source address other than 127.0.0.1, or a range of
addresses, can help with localhost ephemeral port exhaustion.

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40198
This commit is contained in:
David Fifield 2022-11-15 23:42:21 -07:00 committed by meskio
parent 9d72b30603
commit 0780f2e809
No known key found for this signature in database
GPG key ID: 52B8F5AC97A2DA86
6 changed files with 260 additions and 11 deletions

2
go.mod
View file

@ -3,7 +3,7 @@ module git.torproject.org/pluggable-transports/snowflake.git/v2
go 1.15
require (
git.torproject.org/pluggable-transports/goptlib.git v1.1.0
git.torproject.org/pluggable-transports/goptlib.git v1.3.0
github.com/clarkduvall/hyperloglog v0.0.0-20171127014514-a0107a5d8004
github.com/gorilla/websocket v1.5.0
github.com/pion/ice/v2 v2.2.6

4
go.sum
View file

@ -1,7 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.torproject.org/pluggable-transports/goptlib.git v1.1.0 h1:LMQAA8pAho+QtYrrVNimJQiINNEwcwuuD99vezD/PAo=
git.torproject.org/pluggable-transports/goptlib.git v1.1.0/go.mod h1:YT4XMSkuEXbtqlydr9+OxqFAyspUv0Gr9qhM3B++o/Q=
git.torproject.org/pluggable-transports/goptlib.git v1.3.0 h1:G+iuRUblCCC2xnO+0ag1/4+aaM98D5mjWP1M0v9s8a0=
git.torproject.org/pluggable-transports/goptlib.git v1.3.0/go.mod h1:4PBMl1dg7/3vMWSoWb46eGWlrxkUyn/CAJmxhDLAlDs=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=

View file

@ -68,3 +68,22 @@ without having to run as root:
```
setcap 'cap_net_bind_service=+ep' /usr/local/bin/snowflake-server
```
# Controlling source addresses
Use the `orport-srcaddr` pluggable transport option to control what source addresses
are used when connecting to the upstream Tor ExtORPort or ORPort.
The value of the option may be a single IP address (e.g. "127.0.0.2")
or a CIDR range (e.g. "127.0.2.0/24"). If a range is given,
an IP address from the range is randomly chosen for each new connection.
Use `ServerTransportOptions` in torrc to set the option:
```
ServerTransportOptions snowflake orport-srcaddr=127.0.2.0/24
```
Specifying a source address range other than the default 127.0.0.1
can help with conserving localhost ephemeral ports on servers
that receive a lot of connections:
https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/40198

41
server/randaddr.go Normal file
View file

@ -0,0 +1,41 @@
package main
import (
"crypto/rand"
"fmt"
"net"
)
// randIPAddr generates a random IP address within the network represented by
// ipnet.
func randIPAddr(ipnet *net.IPNet) (net.IP, error) {
if len(ipnet.IP) != len(ipnet.Mask) {
return nil, fmt.Errorf("IP and mask have unequal lengths (%v and %v)", len(ipnet.IP), len(ipnet.Mask))
}
ip := make(net.IP, len(ipnet.IP))
_, err := rand.Read(ip)
if err != nil {
return nil, err
}
for i := 0; i < len(ipnet.IP); i++ {
ip[i] = (ipnet.IP[i] & ipnet.Mask[i]) | (ip[i] & ^ipnet.Mask[i])
}
return ip, nil
}
// parseIPCIDR parses a CIDR-notation IP address and prefix length; or if that
// fails, as a plain IP address (with the prefix length equal to the address
// length).
func parseIPCIDR(s string) (*net.IPNet, error) {
_, ipnet, err := net.ParseCIDR(s)
if err == nil {
return ipnet, nil
}
// IP/mask failed; try just IP now, but remember err, to return it in
// case that fails too.
ip := net.ParseIP(s)
if ip != nil {
return &net.IPNet{IP: ip, Mask: net.CIDRMask(len(ip)*8, len(ip)*8)}, nil
}
return nil, err
}

159
server/randaddr_test.go Normal file
View file

@ -0,0 +1,159 @@
package main
import (
"bytes"
"net"
"testing"
)
func mustParseCIDR(s string) *net.IPNet {
_, ipnet, err := net.ParseCIDR(s)
if err != nil {
panic(err)
}
return ipnet
}
func TestRandAddr(t *testing.T) {
outer:
for _, ipnet := range []*net.IPNet{
mustParseCIDR("127.0.0.1/0"),
mustParseCIDR("127.0.0.1/24"),
mustParseCIDR("127.0.0.55/32"),
mustParseCIDR("2001:db8::1234/0"),
mustParseCIDR("2001:db8::1234/32"),
mustParseCIDR("2001:db8::1234/128"),
// Non-canonical masks (that don't consist of 1s followed by 0s)
// work too, why not.
&net.IPNet{
IP: net.IP{1, 2, 3, 4},
Mask: net.IPMask{0x00, 0x07, 0xff, 0xff},
},
} {
for i := 0; i < 100; i++ {
ip, err := randIPAddr(ipnet)
if err != nil {
t.Errorf("%v returned error %v", ipnet, err)
continue outer
}
if !ipnet.Contains(ip) {
t.Errorf("%v does not contain %v", ipnet, ip)
continue outer
}
}
}
}
func TestRandAddrUnequalLengths(t *testing.T) {
for _, ipnet := range []*net.IPNet{
&net.IPNet{
IP: net.IP{1, 2, 3, 4},
Mask: net.CIDRMask(32, 128),
},
&net.IPNet{
IP: net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
Mask: net.CIDRMask(24, 32),
},
&net.IPNet{
IP: net.IP{1, 2, 3, 4},
Mask: net.IPMask{},
},
&net.IPNet{
IP: net.IP{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
Mask: net.IPMask{},
},
} {
_, err := randIPAddr(ipnet)
if err == nil {
t.Errorf("%v did not result in error, but should have", ipnet)
}
}
}
func BenchmarkRandAddr(b *testing.B) {
for _, test := range []struct {
label string
ipnet net.IPNet
}{
{"IPv4/32", net.IPNet{IP: net.IP{127, 0, 0, 1}, Mask: net.CIDRMask(32, 32)}},
{"IPv4/24", net.IPNet{IP: net.IP{127, 0, 0, 1}, Mask: net.CIDRMask(32, 32)}},
{"IPv6/64", net.IPNet{
IP: net.IP{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34},
Mask: net.CIDRMask(64, 128),
}},
{"IPv6/128", net.IPNet{
IP: net.IP{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34},
Mask: net.CIDRMask(128, 128),
}},
} {
b.Run(test.label, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := randIPAddr(&test.ipnet)
if err != nil {
b.Fatal(err)
}
}
})
}
}
func ipNetEqual(a, b *net.IPNet) bool {
if !a.IP.Equal(b.IP) {
return false
}
// Comparing masks for equality is a little tricky because they may be
// different lengths. For masks in canonical form (those for which
// Size() returns other than (0, 0)), we consider two masks equal if the
// numbers of bits *not* covered by the prefix are equal; e.g.
// (120, 128) is equal to (24, 32), because they both have 8 bits not in
// the prefix. If either mask is not in canonical form, we require them
// to be equal as byte arrays (which includes length).
aOnes, aBits := a.Mask.Size()
bOnes, bBits := b.Mask.Size()
if aBits == 0 || bBits == 0 {
return bytes.Equal(a.Mask, b.Mask)
} else {
return aBits-aOnes == bBits-bOnes
}
}
func TestParseIPCIDR(t *testing.T) {
// Well-formed inputs.
for _, test := range []struct {
input string
expected *net.IPNet
}{
{"127.0.0.123", mustParseCIDR("127.0.0.123/32")},
{"127.0.0.123/0", mustParseCIDR("127.0.0.123/0")},
{"127.0.0.123/24", mustParseCIDR("127.0.0.123/24")},
{"127.0.0.123/32", mustParseCIDR("127.0.0.123/32")},
{"2001:db8::1234", mustParseCIDR("2001:db8::1234/128")},
{"2001:db8::1234/0", mustParseCIDR("2001:db8::1234/0")},
{"2001:db8::1234/32", mustParseCIDR("2001:db8::1234/32")},
{"2001:db8::1234/128", mustParseCIDR("2001:db8::1234/128")},
} {
ipnet, err := parseIPCIDR(test.input)
if err != nil {
t.Errorf("%q returned error %v", test.input, err)
continue
}
if !ipNetEqual(ipnet, test.expected) {
t.Errorf("%q → %v, expected %v", test.input, ipnet, test.expected)
}
}
// Bad inputs.
for _, input := range []string{
"",
"1.2.3",
"1.2.3/16",
"2001:db8:1234",
"2001:db8:1234/64",
"localhost",
} {
_, err := parseIPCIDR(input)
if err == nil {
t.Errorf("%q did not result in error, but should have", input)
}
}
}

View file

@ -67,21 +67,36 @@ func proxy(local *net.TCPConn, conn net.Conn) {
wg.Wait()
}
// handleConn bidirectionally connects a client snowflake connection with an ORPort.
func handleConn(conn net.Conn) error {
// handleConn bidirectionally connects a client snowflake connection with the
// ORPort. If orPortSrcAddr is not nil, addresses from the given range are used
// when dialing the ORPOrt.
func handleConn(conn net.Conn, orPortSrcAddr *net.IPNet) error {
addr := conn.RemoteAddr().String()
statsChannel <- addr != ""
or, err := pt.DialOr(&ptInfo, addr, ptMethodName)
dialer := net.Dialer{}
if orPortSrcAddr != nil {
// Use a random source IP address in the given range.
ip, err := randIPAddr(orPortSrcAddr)
if err != nil {
return err
}
dialer.LocalAddr = &net.TCPAddr{IP: ip}
}
or, err := pt.DialOrWithDialer(&dialer, &ptInfo, addr, ptMethodName)
if err != nil {
return fmt.Errorf("failed to connect to ORPort: %s", err)
}
defer or.Close()
proxy(or, conn)
proxy(or.(*net.TCPConn), conn)
return nil
}
// acceptLoop accepts incoming client snowflake connection and passes them to a handler function.
func acceptLoop(ln net.Listener) {
// acceptLoop accepts incoming client snowflake connections and passes them to
// handleConn. If orPortSrcAddr is not nil, addresses from the given range are
// used when dialing the ORPOrt.
func acceptLoop(ln net.Listener, orPortSrcAddr *net.IPNet) {
for {
conn, err := ln.Accept()
if err != nil {
@ -93,7 +108,7 @@ func acceptLoop(ln net.Listener) {
}
go func() {
defer conn.Close()
err := handleConn(conn)
err := handleConn(conn, orPortSrcAddr)
if err != nil {
log.Printf("handleConn: %v", err)
}
@ -240,6 +255,21 @@ func main() {
}
transport = sf.NewSnowflakeServer(certManager.GetCertificate)
}
// Are we requested to use source addresses from a particular
// range when dialing the ORPort for this transport?
var orPortSrcAddr *net.IPNet
if orPortSrcAddrCIDR, ok := bindaddr.Options.Get("orport-srcaddr"); ok {
ipnet, err := parseIPCIDR(orPortSrcAddrCIDR)
if err != nil {
err = fmt.Errorf("parsing srcaddr: %w", err)
log.Println(err)
pt.SmethodError(bindaddr.MethodName, err.Error())
continue
}
orPortSrcAddr = ipnet
}
ln, err := transport.Listen(bindaddr.Addr)
if err != nil {
log.Printf("error opening listener: %s", err)
@ -247,7 +277,7 @@ func main() {
continue
}
defer ln.Close()
go acceptLoop(ln)
go acceptLoop(ln, orPortSrcAddr)
pt.SmethodArgs(bindaddr.MethodName, bindaddr.Addr, args)
listeners = append(listeners, ln)
}