mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-13 20:11:19 -04:00
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:
parent
9d72b30603
commit
0780f2e809
6 changed files with 260 additions and 11 deletions
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module git.torproject.org/pluggable-transports/snowflake.git/v2
|
||||||
go 1.15
|
go 1.15
|
||||||
|
|
||||||
require (
|
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/clarkduvall/hyperloglog v0.0.0-20171127014514-a0107a5d8004
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/pion/ice/v2 v2.2.6
|
github.com/pion/ice/v2 v2.2.6
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -1,7 +1,7 @@
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
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=
|
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.3.0 h1:G+iuRUblCCC2xnO+0ag1/4+aaM98D5mjWP1M0v9s8a0=
|
||||||
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/go.mod h1:4PBMl1dg7/3vMWSoWb46eGWlrxkUyn/CAJmxhDLAlDs=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
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/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||||
|
|
|
@ -68,3 +68,22 @@ without having to run as root:
|
||||||
```
|
```
|
||||||
setcap 'cap_net_bind_service=+ep' /usr/local/bin/snowflake-server
|
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
41
server/randaddr.go
Normal 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
159
server/randaddr_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,21 +67,36 @@ func proxy(local *net.TCPConn, conn net.Conn) {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleConn bidirectionally connects a client snowflake connection with an ORPort.
|
// handleConn bidirectionally connects a client snowflake connection with the
|
||||||
func handleConn(conn net.Conn) error {
|
// 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()
|
addr := conn.RemoteAddr().String()
|
||||||
statsChannel <- addr != ""
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to ORPort: %s", err)
|
return fmt.Errorf("failed to connect to ORPort: %s", err)
|
||||||
}
|
}
|
||||||
defer or.Close()
|
defer or.Close()
|
||||||
proxy(or, conn)
|
|
||||||
|
proxy(or.(*net.TCPConn), conn)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// acceptLoop accepts incoming client snowflake connection and passes them to a handler function.
|
// acceptLoop accepts incoming client snowflake connections and passes them to
|
||||||
func acceptLoop(ln net.Listener) {
|
// 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 {
|
for {
|
||||||
conn, err := ln.Accept()
|
conn, err := ln.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -93,7 +108,7 @@ func acceptLoop(ln net.Listener) {
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
err := handleConn(conn)
|
err := handleConn(conn, orPortSrcAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("handleConn: %v", err)
|
log.Printf("handleConn: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -240,6 +255,21 @@ func main() {
|
||||||
}
|
}
|
||||||
transport = sf.NewSnowflakeServer(certManager.GetCertificate)
|
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)
|
ln, err := transport.Listen(bindaddr.Addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error opening listener: %s", err)
|
log.Printf("error opening listener: %s", err)
|
||||||
|
@ -247,7 +277,7 @@ func main() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
defer ln.Close()
|
defer ln.Close()
|
||||||
go acceptLoop(ln)
|
go acceptLoop(ln, orPortSrcAddr)
|
||||||
pt.SmethodArgs(bindaddr.MethodName, bindaddr.Addr, args)
|
pt.SmethodArgs(bindaddr.MethodName, bindaddr.Addr, args)
|
||||||
listeners = append(listeners, ln)
|
listeners = append(listeners, ln)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue