mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-13 20:11:19 -04:00
TL;DR: The current implementation uses a 32K buffer size for a total of 64K of buffers/connection, but each read/write is less than 2K according to my measurements. # Background The Snwoflake proxy uses as particularly hot function `copyLoop` (proxy/lib/snowflake.go) to proxy data from a Tor relay to a connected client. This is currently done using the `io.Copy` function to write all incoming data both ways. Looking at the `io.Copy` implementation, it internally uses `io.CopyBuffer`, which in turn defaults to a buffer of size 32K for copying data (I checked and the current implementation uses 32K every time). Since `snowflake-proxy` is intended to be run in a very distributed manner, on as many machines as possible, minimizing the CPU and memory footprint of each proxied connection would be ideal, as well as maximising throughput for clients. # Hypothesis There might exist a buffer size `X` that is more suitable for usage in `copyLoop` than 32K. # Testing ## Using tcpdump Assuming you use `-ephemeral-ports-range 50000:51000` for `snowflake-proxy`, you can capture the UDP packets being proxied using ```sh sudo tcpdump -i <interface> udp portrange 50000-51000 ``` which will provide a `length` value for each packet captured. One good start value for `X` could then be slighly larger than the largest captured packet, assuming one packet is copied at a time. Experimentally I found this value to be 1265 bytes, which would make `X = 2K` a possible starting point. ## Printing actual read The following snippe was added in `proxy/lib/snowflake.go`: ```go // Taken straight from standardlib io.copyBuffer func copyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) { // If the reader has a WriteTo method, use it to do the copy. // Avoids an allocation and a copy. if wt, ok := src.(io.WriterTo); ok { return wt.WriteTo(dst) } // Similarly, if the writer has a ReadFrom method, use it to do the copy. if rt, ok := dst.(io.ReaderFrom); ok { return rt.ReadFrom(src) } if buf == nil { size := 32 * 1024 if l, ok := src.(*io.LimitedReader); ok && int64(size) > l.N { if l.N < 1 { size = 1 } else { size = int(l.N) } } buf = make([]byte, size) } for { nr, er := src.Read(buf) if nr > 0 { log.Printf("Read %d", nr) // THIS IS THE ONLY DIFFERENCE FROM io.CopyBuffer nw, ew := dst.Write(buf[0:nr]) if nw < 0 || nr < nw { nw = 0 if ew == nil { ew = errors.New("invalid write result") } } written += int64(nw) if ew != nil { err = ew break } if nr != nw { err = io.ErrShortWrite break } } if er != nil { if er != io.EOF { err = er } break } } return written, err } ``` and `copyLoop` was amended to use this instead of `io.Copy`. The `Read: BYTES` was saved to a file using this command ```sh ./proxy -verbose -ephemeral-ports-range 50000:50010 2>&1 >/dev/null | awk '/Read: / { print $4 }' | tee read_sizes.txt ``` I got the result: min: 8 max: 1402 median: 1402 average: 910.305 Suggested buffer size: 2K Current buffer size: 32768 (32K, experimentally verified) ## Using a Snowflake Proxy in Tor browser and use Wireshark I also used Wireshark, and concluded that all packets sent was < 2K. # Conclusion As per the commit I suggest changing the buffer size to 2K. Some things I have not been able to answer: 1. Does this make a big impact on performance? 1. Are there any unforseen consequences? What happens if a packet is > 2K (I think the Go standard libary just splits the packet, but someone please confirm). |
||
---|---|---|
.. | ||
lib | ||
main.go | ||
README.md |
Table of Contents
This is a standalone (not browser-based) version of the Snowflake proxy. For browser-based versions of the Snowflake proxy, see https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake-webext.
Dependencies
- Go 1.15+
- We use the pion/webrtc library for WebRTC communication with Snowflake proxies. Note: running
go get
will fetch this dependency automatically during the build process.
Building the standalone Snowflake proxy
To build the Snowflake proxy, make sure you are in the proxy/
directory, and then run:
go get
go build
Running a standalone Snowflake proxy
The Snowflake proxy can be run with the following options:
Usage of ./proxy:
-allow-non-tls-relay
allow relay without tls encryption
-allowed-relay-hostname-pattern string
a pattern to specify allowed hostname pattern for relay URL. (default "snowflake.torproject.net$")
-broker string
broker URL (default "https://snowflake-broker.torproject.net/")
-capacity uint
maximum concurrent clients (default is to accept an unlimited number of clients)
-disableStatsLogger
disable the exposing mechanism for stats using logs
-ephemeral-ports-range string
ICE UDP ephemeral ports range (format:"<min>:<max>")
-enableMetrics
enable the exposing mechanism for stats using metrics at "/internal/metrics"
-keep-local-addresses
keep local LAN address ICE candidates
-log string
log filename
-metricsAddress string
set listening address for metrics service by either hostname or ip-address (default localhost)
-metricsPort
set port for the metrics service (default 9999)
-nat-retest-interval duration
the time interval in second before NAT type is retested, 0s disables retest. Valid time units are "s", "m", "h". (default 24h0m0s)
-relay string
websocket relay URL (default "wss://snowflake.torproject.net/")
-outbound-address string
bind a specific outbound address. Replace all host candidates with this address without validation.
-probeURL string
NAT check probe server URL (default "https://snowflake-broker.torproject.net:8443/probe")
-stun string
stun URL (default "stun:stun.l.google.com:19302")
-summary-interval duration
the time interval to output summary, 0s disables summaries. Valid time units are "s", "m", "h". (default 1h0m0s)
-unsafe-logging
prevent logs from being scrubbed
-verbose
increase log verbosity
-version
display version info to stderr and quit
For more information on how to run a Snowflake proxy in deployment, see our community documentation.