From 9b470fbe4b8429070559d2e895b9be45509cdf78 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 18 Oct 2019 15:10:03 -0400 Subject: [PATCH 001/385] Removed "janky" snowflake state machine The only place it was used was in window.onpageunload, and we have a better way of determining if the proxy is active there (through the ui). I also removed that code from the webextension since the proxy won't stop running unless you close the browser and after testing it looks like that code doesn't notify the user anyway. --- proxy/init-badge.js | 2 +- proxy/init-testing.js | 4 ++-- proxy/init-webext.js | 12 ------------ proxy/proxypair.js | 5 +---- proxy/snowflake.js | 9 --------- proxy/spec/init.spec.js | 15 +++++++-------- 6 files changed, 11 insertions(+), 36 deletions(-) diff --git a/proxy/init-badge.js b/proxy/init-badge.js index fe8cc91..2cc5b07 100644 --- a/proxy/init-badge.js +++ b/proxy/init-badge.js @@ -193,7 +193,7 @@ var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotific if ( !silenceNotifications && snowflake !== null && - Snowflake.MODE.WEBRTC_READY === snowflake.state + ui.active ) { return Snowflake.MESSAGE.CONFIRMATION; } diff --git a/proxy/init-testing.js b/proxy/init-testing.js index 5b63099..90026a9 100644 --- a/proxy/init-testing.js +++ b/proxy/init-testing.js @@ -46,7 +46,7 @@ DebugUI.prototype.$status = null; Entry point. */ -var snowflake, query, debug, silenceNotifications, log, dbg, init; +var snowflake, query, debug, ui, silenceNotifications, log, dbg, init; (function() { @@ -108,7 +108,7 @@ var snowflake, query, debug, silenceNotifications, log, dbg, init; if ( !silenceNotifications && snowflake !== null && - Snowflake.MODE.WEBRTC_READY === snowflake.state + ui.active ) { return Snowflake.MESSAGE.CONFIRMATION; } diff --git a/proxy/init-webext.js b/proxy/init-webext.js index 6789ffd..ad345fa 100644 --- a/proxy/init-webext.js +++ b/proxy/init-webext.js @@ -193,18 +193,6 @@ var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotific return snowflake.beginWebRTC(); }; - // Notification of closing tab with active proxy. - window.onbeforeunload = function() { - if ( - !silenceNotifications && - snowflake !== null && - Snowflake.MODE.WEBRTC_READY === snowflake.state - ) { - return Snowflake.MESSAGE.CONFIRMATION; - } - return null; - }; - window.onunload = function() { if (snowflake !== null) { snowflake.disable(); } return null; diff --git a/proxy/proxypair.js b/proxy/proxypair.js index 58efa1a..9f6a7b2 100644 --- a/proxy/proxypair.js +++ b/proxy/proxypair.js @@ -1,4 +1,4 @@ -/* global snowflake, log, dbg, Util, PeerConnection, Snowflake, Parse, WS */ +/* global snowflake, log, dbg, Util, PeerConnection, Parse, WS */ /* Represents a single: @@ -89,7 +89,6 @@ class ProxyPair { return; } this.running = true; - snowflake.state = Snowflake.MODE.WEBRTC_READY; snowflake.ui.setActive(true); // This is the point when the WebRTC datachannel is done, so the next step // is to establish websocket to the server. @@ -99,7 +98,6 @@ class ProxyPair { log('WebRTC DataChannel closed.'); snowflake.ui.setStatus('disconnected by webrtc.'); snowflake.ui.setActive(false); - snowflake.state = Snowflake.MODE.INIT; this.flush(); return this.close(); }; @@ -139,7 +137,6 @@ class ProxyPair { log(relay.label + ' closed.'); snowflake.ui.setStatus('disconnected.'); snowflake.ui.setActive(false); - snowflake.state = Snowflake.MODE.INIT; this.flush(); return this.close(); }; diff --git a/proxy/snowflake.js b/proxy/snowflake.js index cdc59fb..78b1d0e 100644 --- a/proxy/snowflake.js +++ b/proxy/snowflake.js @@ -22,7 +22,6 @@ class Snowflake { this.config = config; this.ui = ui; this.broker = broker; - this.state = Snowflake.MODE.INIT; this.proxyPairs = []; if (void 0 === this.config.rateLimitBytes) { this.rateLimit = new DummyRateLimit(); @@ -44,7 +43,6 @@ class Snowflake { // Initialize WebRTC PeerConnection, which requires beginning the signalling // process. |pollBroker| automatically arranges signalling. beginWebRTC() { - this.state = Snowflake.MODE.WEBRTC_CONNECTING; log('ProxyPair Slots: ' + this.proxyPairs.length); log('Snowflake IDs: ' + (this.proxyPairs.map(function(p) { return p.id; @@ -173,13 +171,6 @@ Snowflake.prototype.pollInterval = null; Snowflake.prototype.retries = 0; -// Janky state machine -Snowflake.MODE = { - INIT: 0, - WEBRTC_CONNECTING: 1, - WEBRTC_READY: 2 -}; - Snowflake.MESSAGE = { CONFIRMATION: 'You\'re currently serving a Tor user via Snowflake.' }; diff --git a/proxy/spec/init.spec.js b/proxy/spec/init.spec.js index 748bc86..593add9 100644 --- a/proxy/spec/init.spec.js +++ b/proxy/spec/init.spec.js @@ -6,29 +6,28 @@ var snowflake = { ui: new UI, broker: { sendAnswer: function() {} - }, - state: Snowflake.MODE.INIT + } }; describe('Init', function() { it('gives a dialog when closing, only while active', function() { silenceNotifications = false; - snowflake.state = Snowflake.MODE.WEBRTC_READY; + ui.setActive(true); var msg = window.onbeforeunload(); - expect(snowflake.state).toBe(Snowflake.MODE.WEBRTC_READY); + expect(ui.active).toBe(true); expect(msg).toBe(Snowflake.MESSAGE.CONFIRMATION); - snowflake.state = Snowflake.MODE.INIT; + ui.setActive(false); msg = window.onbeforeunload(); - expect(snowflake.state).toBe(Snowflake.MODE.INIT); + expect(ui.active).toBe(false); expect(msg).toBe(null); }); it('does not give a dialog when silent flag is on', function() { silenceNotifications = true; - snowflake.state = Snowflake.MODE.WEBRTC_READY; + ui.setActive(true); var msg = window.onbeforeunload(); - expect(snowflake.state).toBe(Snowflake.MODE.WEBRTC_READY); + expect(ui.active).toBe(true); expect(msg).toBe(null); }); From d186fcd40103913bf06358e5dd47c0c776e07534 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 18 Oct 2019 16:58:54 -0400 Subject: [PATCH 002/385] Remove property "running" from proxy-pair We don't need it, and already have a function webrtcIsReady that tells us what we need to know (whether a datachannel was opened before the timeout period). --- proxy/proxypair.js | 4 ---- proxy/snowflake.js | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/proxy/proxypair.js b/proxy/proxypair.js index 9f6a7b2..64eafdc 100644 --- a/proxy/proxypair.js +++ b/proxy/proxypair.js @@ -88,7 +88,6 @@ class ProxyPair { if (!this.active) { return; } - this.running = true; snowflake.ui.setActive(true); // This is the point when the WebRTC datachannel is done, so the next step // is to establish websocket to the server. @@ -191,7 +190,6 @@ class ProxyPair { this.relay = null; this.onCleanup(); this.active = false; - this.running = false; } flush() { @@ -255,8 +253,6 @@ ProxyPair.prototype.relay = null; // websocket ProxyPair.prototype.timer = 0; -ProxyPair.prototype.running = false; // Whether a datachannel is opened - ProxyPair.prototype.active = false; // Whether serving a client. ProxyPair.prototype.flush_timeout_id = null; diff --git a/proxy/snowflake.js b/proxy/snowflake.js index 78b1d0e..c647b4e 100644 --- a/proxy/snowflake.js +++ b/proxy/snowflake.js @@ -78,7 +78,7 @@ class Snowflake { } //set a timeout for channel creation return setTimeout((() => { - if (!pair.running) { + if (!pair.webrtcIsReady()) { log('proxypair datachannel timed out waiting for open'); pair.close(); return pair.active = false; From 789285e0dfe69474b2876a79f82784ad2892d8c4 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 18 Oct 2019 17:28:53 -0400 Subject: [PATCH 003/385] Remove "active" property of proxyPairs Use their existence in the proxy pair list to indicate they are active. --- proxy/proxypair.js | 9 ------- proxy/snowflake.js | 58 ++++++++++++++++++++-------------------------- 2 files changed, 25 insertions(+), 42 deletions(-) diff --git a/proxy/proxypair.js b/proxy/proxypair.js index 64eafdc..52594e9 100644 --- a/proxy/proxypair.js +++ b/proxy/proxypair.js @@ -85,9 +85,6 @@ class ProxyPair { prepareDataChannel(channel) { channel.onopen = () => { log('WebRTC DataChannel opened!'); - if (!this.active) { - return; - } snowflake.ui.setActive(true); // This is the point when the WebRTC datachannel is done, so the next step // is to establish websocket to the server. @@ -179,17 +176,13 @@ class ProxyPair { if (this.webrtcIsReady()) { this.client.close(); } - this.client = null; if (this.peerConnOpen()) { this.pc.close(); } - this.pc = null; if (this.relayIsReady()) { this.relay.close(); } - this.relay = null; this.onCleanup(); - this.active = false; } flush() { @@ -253,8 +246,6 @@ ProxyPair.prototype.relay = null; // websocket ProxyPair.prototype.timer = 0; -ProxyPair.prototype.active = false; // Whether serving a client. - ProxyPair.prototype.flush_timeout_id = null; ProxyPair.prototype.onCleanup = null; diff --git a/proxy/snowflake.js b/proxy/snowflake.js index c647b4e..c914520 100644 --- a/proxy/snowflake.js +++ b/proxy/snowflake.js @@ -43,10 +43,6 @@ class Snowflake { // Initialize WebRTC PeerConnection, which requires beginning the signalling // process. |pollBroker| automatically arranges signalling. beginWebRTC() { - log('ProxyPair Slots: ' + this.proxyPairs.length); - log('Snowflake IDs: ' + (this.proxyPairs.map(function(p) { - return p.id; - })).join(' | ')); this.pollBroker(); return this.pollInterval = setInterval((() => { return this.pollBroker(); @@ -58,13 +54,13 @@ class Snowflake { pollBroker() { var msg, pair, recv; // Poll broker for clients. - pair = this.nextAvailableProxyPair(); + pair = this.makeProxyPair(); if (!pair) { log('At client capacity.'); return; } + log('Polling broker..'); // Do nothing until a new proxyPair is available. - pair.active = true; msg = 'Polling for client ... '; if (this.retries > 0) { msg += '[retries: ' + this.retries + ']'; @@ -72,35 +68,23 @@ class Snowflake { this.ui.setStatus(msg); recv = this.broker.getClientOffer(pair.id); recv.then((desc) => { - if (pair.active) { - if (!this.receiveOffer(pair, desc)) { - return pair.active = false; - } - //set a timeout for channel creation - return setTimeout((() => { - if (!pair.webrtcIsReady()) { - log('proxypair datachannel timed out waiting for open'); - pair.close(); - return pair.active = false; - } - }), 20000); // 20 second timeout + if (!this.receiveOffer(pair, desc)) { + return pair.close(); } + //set a timeout for channel creation + return setTimeout((() => { + if (!pair.webrtcIsReady()) { + log('proxypair datachannel timed out waiting for open'); + return pair.close(); + } + }), 20000); // 20 second timeout }, function() { - return pair.active = false; + //on error, close proxy pair + return pair.close(); }); return this.retries++; } - // Returns the first ProxyPair that's available to connect. - nextAvailableProxyPair() { - if (this.proxyPairs.length < this.config.connectionsPerClient) { - return this.makeProxyPair(this.relayAddr); - } - return this.proxyPairs.find(function(pp) { - return !pp.active; - }); - } - receiveOffer(pair, desc) { var e, offer, sdp; try { @@ -127,19 +111,27 @@ class Snowflake { return pair.pc.setLocalDescription(sdp).catch(fail); }; fail = function() { - pair.active = false + pair.close(); return dbg('webrtc: Failed to create or set Answer'); }; return pair.pc.createAnswer().then(next).catch(fail); } - makeProxyPair(relay) { + makeProxyPair() { + if (this.proxyPairs.length >= this.config.connectionsPerClient) { + return null; + } var pair; - pair = new ProxyPair(relay, this.rateLimit, this.config.pcConfig); + pair = new ProxyPair(this.relayAddr, this.rateLimit, this.config.pcConfig); this.proxyPairs.push(pair); + + log('Snowflake IDs: ' + (this.proxyPairs.map(function(p) { + return p.id; + })).join(' | ')); + pair.onCleanup = () => { var ind; - // Delete from the list of active proxy pairs. + // Delete from the list of proxy pairs. ind = this.proxyPairs.indexOf(pair); if (ind > -1) { return this.proxyPairs.splice(ind, 1); From 64b66c855fe35d2e56fca7510e913482cdb85793 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 18 Oct 2019 17:48:45 -0400 Subject: [PATCH 004/385] Moved function comments to their definitions Increase readability of code a bit, the function descriptions were automatically placed in the constructor when we moved from coffeescript. --- proxy/broker.js | 15 ++++++++------- proxy/proxypair.js | 16 ++++++---------- proxy/snowflake.js | 9 +++------ 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/proxy/broker.js b/proxy/broker.js index 9806e76..7b5b7e4 100644 --- a/proxy/broker.js +++ b/proxy/broker.js @@ -15,15 +15,9 @@ class Broker { // On construction, this Broker object does not do anything until // |getClientOffer| is called. constructor(url) { - // Promises some client SDP Offer. - // Registers this Snowflake with the broker using an HTTP POST request, and - // waits for a response containing some client offer that the Broker chooses - // for this proxy.. - // TODO: Actually support multiple clients. this.getClientOffer = this.getClientOffer.bind(this); - // urlSuffix for the broker is different depending on what action - // is desired. this._postRequest = this._postRequest.bind(this); + this.url = url; this.clients = 0; if (0 === this.url.indexOf('localhost', 0)) { @@ -38,6 +32,11 @@ class Broker { } } + // Promises some client SDP Offer. + // Registers this Snowflake with the broker using an HTTP POST request, and + // waits for a response containing some client offer that the Broker chooses + // for this proxy.. + // TODO: Actually support multiple clients. getClientOffer(id) { return new Promise((fulfill, reject) => { var xhr; @@ -87,6 +86,8 @@ class Broker { return this._postRequest(id, xhr, 'answer', JSON.stringify(answer)); } + // urlSuffix for the broker is different depending on what action + // is desired. _postRequest(id, xhr, urlSuffix, payload) { var err; try { diff --git a/proxy/proxypair.js b/proxy/proxypair.js index 52594e9..25eaa9d 100644 --- a/proxy/proxypair.js +++ b/proxy/proxypair.js @@ -17,17 +17,13 @@ class ProxyPair { - @rateLimit specifies a rate limit on traffic */ constructor(relayAddr, rateLimit, pcConfig) { - // Given a WebRTC DataChannel, prepare callbacks. this.prepareDataChannel = this.prepareDataChannel.bind(this); - // Assumes WebRTC datachannel is connected. this.connectRelay = this.connectRelay.bind(this); - // WebRTC --> websocket this.onClientToRelayMessage = this.onClientToRelayMessage.bind(this); - // websocket --> WebRTC this.onRelayToClientMessage = this.onRelayToClientMessage.bind(this); this.onError = this.onError.bind(this); - // Send as much data in both directions as the rate limit currently allows. this.flush = this.flush.bind(this); + this.relayAddr = relayAddr; this.rateLimit = rateLimit; this.pcConfig = pcConfig; @@ -82,6 +78,7 @@ class ProxyPair { return true; } + // Given a WebRTC DataChannel, prepare callbacks. prepareDataChannel(channel) { channel.onopen = () => { log('WebRTC DataChannel opened!'); @@ -104,6 +101,7 @@ class ProxyPair { return channel.onmessage = this.onClientToRelayMessage; } + // Assumes WebRTC datachannel is connected. connectRelay() { var params, peer_ip, ref; dbg('Connecting to relay...'); @@ -148,12 +146,14 @@ class ProxyPair { }), 5000); } + // WebRTC --> websocket onClientToRelayMessage(msg) { dbg('WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes'); this.c2rSchedule.push(msg.data); return this.flush(); } + // websocket --> WebRTC onRelayToClientMessage(event) { dbg('websocket --> WebRTC data: ' + event.data.byteLength + ' bytes'); this.r2cSchedule.push(event.data); @@ -185,6 +185,7 @@ class ProxyPair { this.onCleanup(); } + // Send as much data in both directions as the rate limit currently allows. flush() { var busy, checkChunks; if (this.flush_timeout_id) { @@ -239,15 +240,10 @@ class ProxyPair { ProxyPair.prototype.MAX_BUFFER = 10 * 1024 * 1024; ProxyPair.prototype.pc = null; - ProxyPair.prototype.client = null; // WebRTC Data channel - ProxyPair.prototype.relay = null; // websocket ProxyPair.prototype.timer = 0; - ProxyPair.prototype.flush_timeout_id = null; ProxyPair.prototype.onCleanup = null; - -ProxyPair.prototype.id = null; diff --git a/proxy/snowflake.js b/proxy/snowflake.js index c914520..ba1ef03 100644 --- a/proxy/snowflake.js +++ b/proxy/snowflake.js @@ -16,9 +16,8 @@ class Snowflake { // Prepare the Snowflake with a Broker (to find clients) and optional UI. constructor(config, ui, broker) { - // Receive an SDP offer from some client assigned by the Broker, - // |pair| - an available ProxyPair. this.receiveOffer = this.receiveOffer.bind(this); + this.config = config; this.ui = ui; this.broker = broker; @@ -85,6 +84,8 @@ class Snowflake { return this.retries++; } + // Receive an SDP offer from some client assigned by the Broker, + // |pair| - an available ProxyPair. receiveOffer(pair, desc) { var e, offer, sdp; try { @@ -156,13 +157,9 @@ class Snowflake { } Snowflake.prototype.relayAddr = null; - Snowflake.prototype.rateLimit = null; - Snowflake.prototype.pollInterval = null; -Snowflake.prototype.retries = 0; - Snowflake.MESSAGE = { CONFIRMATION: 'You\'re currently serving a Tor user via Snowflake.' }; From 300a23c6a0071a910ee3b56f7bf14e4f9529ee3f Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 31 Oct 2019 12:08:43 -0400 Subject: [PATCH 005/385] Changed variable name for multiplexed clients The variable maxNumClients was unused, while connectionsPerClient was used for spawning multiple proxyPairs. The former is a more appropriate name for the multiplexing behaviour we use it for. Multiplexing now just works thanks to implementing ticket #31310. --- proxy/config.js | 2 -- proxy/snowflake.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/proxy/config.js b/proxy/config.js index 95ae92e..9564f82 100644 --- a/proxy/config.js +++ b/proxy/config.js @@ -24,8 +24,6 @@ Config.prototype.defaultBrokerPollInterval = 300.0 * 1000; Config.prototype.maxNumClients = 1; -Config.prototype.connectionsPerClient = 1; - // TODO: Different ICE servers. Config.prototype.pcConfig = { iceServers: [ diff --git a/proxy/snowflake.js b/proxy/snowflake.js index ba1ef03..0e9730e 100644 --- a/proxy/snowflake.js +++ b/proxy/snowflake.js @@ -119,7 +119,7 @@ class Snowflake { } makeProxyPair() { - if (this.proxyPairs.length >= this.config.connectionsPerClient) { + if (this.proxyPairs.length >= this.config.maxNumClients) { return null; } var pair; From c417fd5599c5d39951c606856c69a9f05941afd3 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Wed, 16 Oct 2019 21:00:13 -0400 Subject: [PATCH 006/385] Stop using custom websocket library in server Trac: 31028 --- .travis.yml | 2 +- server/server.go | 88 ++++++++++++++++++++++++++++-------------------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9ed48bb..2f02f74 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,8 +26,8 @@ install: - go get -u github.com/keroserene/go-webrtc - go get -u github.com/pion/webrtc - go get -u github.com/dchest/uniuri + - go get -u github.com/gorilla/websocket - go get -u git.torproject.org/pluggable-transports/goptlib.git - - go get -u git.torproject.org/pluggable-transports/websocket.git/websocket - go get -u google.golang.org/appengine - go get -u golang.org/x/crypto/acme/autocert - go get -u golang.org/x/net/http2 diff --git a/server/server.go b/server/server.go index b1b566a..d111fce 100644 --- a/server/server.go +++ b/server/server.go @@ -21,7 +21,7 @@ import ( pt "git.torproject.org/pluggable-transports/goptlib.git" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" - "git.torproject.org/pluggable-transports/websocket.git/websocket" + "github.com/gorilla/websocket" "golang.org/x/crypto/acme/autocert" "golang.org/x/net/http2" ) @@ -53,50 +53,60 @@ additional HTTP listener on port 80 to work with ACME. // An abstraction that makes an underlying WebSocket connection look like an // io.ReadWriteCloser. type webSocketConn struct { - Ws *websocket.WebSocket - messageBuf []byte + Ws *websocket.Conn + r io.Reader } // Implements io.Reader. func (conn *webSocketConn) Read(b []byte) (n int, err error) { - for len(conn.messageBuf) == 0 { - var m websocket.Message - m, err = conn.Ws.ReadMessage() - if err != nil { - return + var opCode int + if conn.r == nil { + // New message + var r io.Reader + for { + if opCode, r, err = conn.Ws.NextReader(); err != nil { + return + } + if opCode != websocket.BinaryMessage && opCode != websocket.TextMessage { + continue + } + + conn.r = r + break } - if m.Opcode == 8 { - err = io.EOF - return - } - if m.Opcode != 2 { - err = fmt.Errorf("got non-binary opcode %d", m.Opcode) - return - } - conn.messageBuf = m.Payload } - n = copy(b, conn.messageBuf) - conn.messageBuf = conn.messageBuf[n:] - + n, err = conn.r.Read(b) + if err != nil { + if err == io.EOF { + // Message finished + conn.r = nil + err = nil + } + } return } // Implements io.Writer. -func (conn *webSocketConn) Write(b []byte) (int, error) { - err := conn.Ws.WriteMessage(2, b) - return len(b), err +func (conn *webSocketConn) Write(b []byte) (n int, err error) { + var w io.WriteCloser + if w, err = conn.Ws.NextWriter(websocket.BinaryMessage); err != nil { + return + } + if n, err = w.Write(b); err != nil { + return + } + err = w.Close() + return } // Implements io.Closer. func (conn *webSocketConn) Close() error { - // Ignore any error in trying to write a Close frame. - _ = conn.Ws.WriteFrame(8, nil) - return conn.Ws.Conn.Close() + return conn.Ws.Close() } // Create a new webSocketConn. -func newWebSocketConn(ws *websocket.WebSocket) webSocketConn { +func newWebSocketConn(ws *websocket.Conn) webSocketConn { var conn webSocketConn conn.Ws = ws return conn @@ -145,16 +155,22 @@ func clientAddr(clientIPParam string) string { return (&net.TCPAddr{IP: clientIP, Port: 1, Zone: ""}).String() } -func webSocketHandler(ws *websocket.WebSocket) { - // Undo timeouts on HTTP request handling. - if err := ws.Conn.SetDeadline(time.Time{}); err != nil { - log.Printf("unable to set deadlines with error: %v", err) +var upgrader = websocket.Upgrader{} + +type HTTPHandler struct{} + +func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return } + conn := newWebSocketConn(ws) defer conn.Close() // Pass the address of client as the remote address of incoming connection - clientIPParam := ws.Request().URL.Query().Get("client_ip") + clientIPParam := r.URL.Query().Get("client_ip") addr := clientAddr(clientIPParam) if addr == "" { statsChannel <- false @@ -162,7 +178,6 @@ func webSocketHandler(ws *websocket.WebSocket) { statsChannel <- true } or, err := pt.DialOr(&ptInfo, addr, ptMethodName) - if err != nil { log.Printf("failed to connect to ORPort: %s", err) return @@ -185,11 +200,12 @@ func initServer(addr *net.TCPAddr, return nil, fmt.Errorf("cannot listen on port %d; configure a port using ServerTransportListenAddr", addr.Port) } - var config websocket.Config - config.MaxMessageSize = maxMessageSize + upgrader.CheckOrigin = func(r *http.Request) bool { return true } + + var handler HTTPHandler server := &http.Server{ Addr: addr.String(), - Handler: config.Handler(webSocketHandler), + Handler: &handler, ReadTimeout: requestTimeout, } // We need to override server.TLSConfig.GetCertificate--but first From abefae158716b9f56692ea16336c1f8185eda27e Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Mon, 11 Nov 2019 16:59:33 -0500 Subject: [PATCH 007/385] Restore sending close message before closing And simplify EOF check. --- server/server.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/server.go b/server/server.go index d111fce..ce804fc 100644 --- a/server/server.go +++ b/server/server.go @@ -77,12 +77,10 @@ func (conn *webSocketConn) Read(b []byte) (n int, err error) { } n, err = conn.r.Read(b) - if err != nil { - if err == io.EOF { - // Message finished - conn.r = nil - err = nil - } + if err == io.EOF { + // Message finished + conn.r = nil + err = nil } return } @@ -102,6 +100,8 @@ func (conn *webSocketConn) Write(b []byte) (n int, err error) { // Implements io.Closer. func (conn *webSocketConn) Close() error { + // Ignore any error in trying to write a Close frame. + _ = conn.Ws.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second)) return conn.Ws.Close() } From c4ae64905b69512e50587c1ee749cddfc0937a4c Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 7 Oct 2019 14:02:01 -0400 Subject: [PATCH 008/385] Redo protocol for proxy--broker messages Switch to containing all communication between the proxy and the broker in the HTTP response body. This will make things easier if we ever use something other than HTTP communicate between different actors in the snowflake system. Other changes to the protocol are as follows: - requests are accompanied by a version number so the broker can be backwards compatable if desired in the future - all responses are 200 OK unless the request was badly formatted --- broker/broker.go | 66 +++++++--- broker/snowflake-broker_test.go | 58 +++++---- common/messages/proxy.go | 214 ++++++++++++++++++++++++++++++++ common/messages/proxy_test.go | 161 ++++++++++++++++++++++++ proxy-go/snowflake.go | 41 +++++- proxy/translation | 2 +- 6 files changed, 489 insertions(+), 53 deletions(-) create mode 100644 common/messages/proxy.go create mode 100644 common/messages/proxy_test.go diff --git a/broker/broker.go b/broker/broker.go index 2a253b0..4343de8 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -21,6 +21,7 @@ import ( "syscall" "time" + "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" "golang.org/x/crypto/acme/autocert" ) @@ -151,15 +152,16 @@ func (ctx *BrokerContext) AddSnowflake(id string) *Snowflake { For snowflake proxies to request a client from the Broker. */ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { - id := r.Header.Get("X-Session-ID") body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) - if nil != err { + if err != nil { log.Println("Invalid data.") w.WriteHeader(http.StatusBadRequest) return } - if string(body) != id { - log.Println("Mismatched IDs!") + + sid, err := messages.DecodePollRequest(body) + if err != nil { + log.Println("Invalid data.") w.WriteHeader(http.StatusBadRequest) return } @@ -173,14 +175,26 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { } // Wait for a client to avail an offer to the snowflake, or timeout if nil. - offer := ctx.RequestOffer(id) + offer := ctx.RequestOffer(sid) + var b []byte if nil == offer { ctx.metrics.proxyIdleCount++ - w.WriteHeader(http.StatusGatewayTimeout) + + b, err = messages.EncodePollResponse("", false) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(b) return } - log.Println("Passing client offer to snowflake.") - if _, err := w.Write(offer); err != nil { + b, err = messages.EncodePollResponse(string(offer), true) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if _, err := w.Write(b); err != nil { log.Printf("proxyPolls unable to write offer with error: %v", err) } } @@ -235,14 +249,7 @@ an offer from proxyHandler to respond with an answer in an HTTP POST, which the broker will pass back to the original client. */ func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { - id := r.Header.Get("X-Session-ID") - snowflake, ok := ctx.idToSnowflake[id] - if !ok || nil == snowflake { - // The snowflake took too long to respond with an answer, so its client - // disappeared / the snowflake is no longer recognized by the Broker. - w.WriteHeader(http.StatusGone) - return - } + body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) if nil != err || nil == body || len(body) <= 0 { log.Println("Invalid data.") @@ -250,7 +257,32 @@ func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { return } - snowflake.answerChannel <- body + answer, id, err := messages.DecodeAnswerRequest(body) + if err != nil || answer == "" { + log.Println("Invalid data.") + w.WriteHeader(http.StatusBadRequest) + return + } + + var success = true + snowflake, ok := ctx.idToSnowflake[id] + if !ok || nil == snowflake { + // The snowflake took too long to respond with an answer, so its client + // disappeared / the snowflake is no longer recognized by the Broker. + success = false + } + b, err := messages.EncodeAnswerResponse(success) + if err != nil { + log.Printf("Error encoding answer: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Write(b) + + if success { + snowflake.answerChannel <- []byte(answer) + } + } func debugHandler(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 4c78ecd..c35c1d6 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -113,9 +113,8 @@ func TestBroker(t *testing.T) { Convey("Responds to proxy polls...", func() { done := make(chan bool) w := httptest.NewRecorder() - data := bytes.NewReader([]byte("test")) + data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) - r.Header.Set("X-Session-ID", "test") So(err, ShouldBeNil) Convey("with a client offer if available.", func() { @@ -125,57 +124,59 @@ func TestBroker(t *testing.T) { }(ctx) // Pass a fake client offer to this proxy p := <-ctx.proxyPolls - So(p.id, ShouldEqual, "test") + So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") p.offerChannel <- []byte("fake offer") <-done So(w.Code, ShouldEqual, http.StatusOK) - So(w.Body.String(), ShouldEqual, "fake offer") + So(w.Body.String(), ShouldEqual, `{"Status":"client match","Offer":"fake offer"}`) }) - Convey("times out when no client offer is available.", func() { + Convey("return empty 200 OK when no client offer is available.", func() { go func(ctx *BrokerContext) { proxyPolls(ctx, w, r) done <- true }(ctx) p := <-ctx.proxyPolls - So(p.id, ShouldEqual, "test") + So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") // nil means timeout p.offerChannel <- nil <-done - So(w.Body.String(), ShouldEqual, "") - So(w.Code, ShouldEqual, http.StatusGatewayTimeout) + So(w.Body.String(), ShouldEqual, `{"Status":"no match","Offer":""}`) + So(w.Code, ShouldEqual, http.StatusOK) }) }) Convey("Responds to proxy answers...", func() { s := ctx.AddSnowflake("test") w := httptest.NewRecorder() - data := bytes.NewReader([]byte("fake answer")) + data := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"test","Answer":"test"}`)) Convey("by passing to the client if valid.", func() { r, err := http.NewRequest("POST", "snowflake.broker/answer", data) So(err, ShouldBeNil) - r.Header.Set("X-Session-ID", "test") go func(ctx *BrokerContext) { proxyAnswers(ctx, w, r) }(ctx) answer := <-s.answerChannel So(w.Code, ShouldEqual, http.StatusOK) - So(answer, ShouldResemble, []byte("fake answer")) + So(answer, ShouldResemble, []byte("test")) }) - Convey("with error if the proxy is not recognized", func() { - r, err := http.NewRequest("POST", "snowflake.broker/answer", nil) + Convey("with client gone status if the proxy is not recognized", func() { + data = bytes.NewReader([]byte(`{"Version":"1.0","Sid":"invalid","Answer":"test"}`)) + r, err := http.NewRequest("POST", "snowflake.broker/answer", data) So(err, ShouldBeNil) - r.Header.Set("X-Session-ID", "invalid") proxyAnswers(ctx, w, r) - So(w.Code, ShouldEqual, http.StatusGone) + So(w.Code, ShouldEqual, http.StatusOK) + b, err := ioutil.ReadAll(w.Body) + So(err, ShouldBeNil) + So(b, ShouldResemble, []byte(`{"Status":"client gone"}`)) + }) Convey("with error if the proxy gives invalid answer", func() { data := bytes.NewReader(nil) r, err := http.NewRequest("POST", "snowflake.broker/answer", data) - r.Header.Set("X-Session-ID", "test") So(err, ShouldBeNil) proxyAnswers(ctx, w, r) So(w.Code, ShouldEqual, http.StatusBadRequest) @@ -184,7 +185,6 @@ func TestBroker(t *testing.T) { Convey("with error if the proxy writes too much data", func() { data := bytes.NewReader(make([]byte, 100001)) r, err := http.NewRequest("POST", "snowflake.broker/answer", data) - r.Header.Set("X-Session-ID", "test") So(err, ShouldBeNil) proxyAnswers(ctx, w, r) So(w.Code, ShouldEqual, http.StatusBadRequest) @@ -199,11 +199,10 @@ func TestBroker(t *testing.T) { ctx := NewBrokerContext(NullLogger()) // Proxy polls with its ID first... - dataP := bytes.NewReader([]byte("test")) + dataP := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) wP := httptest.NewRecorder() rP, err := http.NewRequest("POST", "snowflake.broker/proxy", dataP) So(err, ShouldBeNil) - rP.Header.Set("X-Session-ID", "test") go func() { proxyPolls(ctx, wP, rP) polled <- true @@ -211,13 +210,13 @@ func TestBroker(t *testing.T) { // Manually do the Broker goroutine action here for full control. p := <-ctx.proxyPolls - So(p.id, ShouldEqual, "test") + So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") s := ctx.AddSnowflake(p.id) go func() { offer := <-s.offerChannel p.offerChannel <- offer }() - So(ctx.idToSnowflake["test"], ShouldNotBeNil) + So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil) // Client request blocks until proxy answer arrives. dataC := bytes.NewReader([]byte("fake offer")) @@ -231,20 +230,19 @@ func TestBroker(t *testing.T) { <-polled So(wP.Code, ShouldEqual, http.StatusOK) - So(wP.Body.String(), ShouldResemble, "fake offer") - So(ctx.idToSnowflake["test"], ShouldNotBeNil) + So(wP.Body.String(), ShouldResemble, `{"Status":"client match","Offer":"fake offer"}`) + So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil) // Follow up with the answer request afterwards wA := httptest.NewRecorder() - dataA := bytes.NewReader([]byte("fake answer")) - rA, err := http.NewRequest("POST", "snowflake.broker/proxy", dataA) + dataA := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"ymbcCMto7KHNGYlp","Answer":"test"}`)) + rA, err := http.NewRequest("POST", "snowflake.broker/answer", dataA) So(err, ShouldBeNil) - rA.Header.Set("X-Session-ID", "test") proxyAnswers(ctx, wA, rA) So(wA.Code, ShouldEqual, http.StatusOK) <-done So(wC.Code, ShouldEqual, http.StatusOK) - So(wC.Body.String(), ShouldEqual, "fake answer") + So(wC.Body.String(), ShouldEqual, "test") }) } @@ -408,7 +406,7 @@ func TestMetrics(t *testing.T) { //Test addition of proxy polls Convey("for proxy polls", func() { w := httptest.NewRecorder() - data := bytes.NewReader([]byte("test")) + data := bytes.NewReader([]byte("{\"Sid\":\"ymbcCMto7KHNGYlp\",\"Version\":\"1.0\"}")) r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) r.Header.Set("X-Session-ID", "test") r.RemoteAddr = "129.97.208.23:8888" //CA geoip @@ -492,7 +490,7 @@ func TestMetrics(t *testing.T) { //Test unique ip Convey("proxy counts by unique ip", func() { w := httptest.NewRecorder() - data := bytes.NewReader([]byte("test")) + data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) r.Header.Set("X-Session-ID", "test") r.RemoteAddr = "129.97.208.23:8888" //CA geoip @@ -505,7 +503,7 @@ func TestMetrics(t *testing.T) { p.offerChannel <- nil <-done - data = bytes.NewReader([]byte("test")) + data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) if err != nil { log.Printf("unable to get NewRequest with error: %v", err) diff --git a/common/messages/proxy.go b/common/messages/proxy.go new file mode 100644 index 0000000..141fbda --- /dev/null +++ b/common/messages/proxy.go @@ -0,0 +1,214 @@ +//Package for communication with the snowflake broker + +//import "git.torproject.org/pluggable-transports/snowflake.git/common/messages" +package messages + +import ( + "encoding/json" + "fmt" +) + +const version = "1.0" + +/* Version 1.0 specification: + +== ProxyPollRequest == +{ + Sid: [generated session id of proxy] + Version: 1.0 +} + +== ProxyPollResponse == +1) If a client is matched: +HTTP 200 OK +{ + Status: "client match", + { + type: offer, + sdp: [WebRTC SDP] + } +} + +2) If a client is not matched: +HTTP 200 OK + +{ + Status: "no proxies" +} + +3) If the request is malformed: +HTTP 400 BadRequest + +== ProxyAnswerRequest == +{ + Sid: [generated session id of proxy] + Version: 1.0 + Answer: + { + type: answer + sdp: [WebRTC SDP] + } +} + +== ProxyAnswerResponse == +1) If the client retrieved the answer: +HTTP 200 OK + +{ + Status: "success" +} + +2) If the client left: +HTTP 200 OK + +{ + Status: "client gone" +} + +3) If the request is malformed: +HTTP 400 BadRequest + +*/ + +type ProxyPollRequest struct { + Sid string + Version string +} + +func EncodePollRequest(sid string) ([]byte, error) { + return json.Marshal(ProxyPollRequest{ + Sid: sid, + Version: version, + }) +} + +// Decodes a poll message from a snowflake proxy and returns the +// sid of the proxy on success and an error if it failed +func DecodePollRequest(data []byte) (string, error) { + var message ProxyPollRequest + + err := json.Unmarshal(data, &message) + if err != nil { + return "", err + } + if message.Version != "1.0" { + return "", fmt.Errorf("using unknown version") + } + + // Version 1.0 requires an Sid + if message.Sid == "" { + return "", fmt.Errorf("no supplied session id") + } + + return message.Sid, nil +} + +type ProxyPollResponse struct { + Status string + Offer string +} + +func EncodePollResponse(offer string, success bool) ([]byte, error) { + if success { + return json.Marshal(ProxyPollResponse{ + Status: "client match", + Offer: offer, + }) + + } + return json.Marshal(ProxyPollResponse{ + Status: "no match", + }) +} + +// Decodes a poll response from the broker and returns an offer +// If there is a client match, the returned offer string will be non-empty +func DecodePollResponse(data []byte) (string, error) { + var message ProxyPollResponse + + err := json.Unmarshal(data, &message) + if err != nil { + return "", err + } + if message.Status == "" { + return "", fmt.Errorf("received invalid data") + } + + if message.Status == "client match" { + if message.Offer == "" { + return "", fmt.Errorf("no supplied offer") + } + } else { + message.Offer = "" + } + + return message.Offer, nil +} + +type ProxyAnswerRequest struct { + Version string + Sid string + Answer string +} + +func EncodeAnswerRequest(answer string, sid string) ([]byte, error) { + return json.Marshal(ProxyAnswerRequest{ + Version: "1.0", + Sid: sid, + Answer: answer, + }) +} + +// Returns the sdp answer and proxy sid +func DecodeAnswerRequest(data []byte) (string, string, error) { + var message ProxyAnswerRequest + + err := json.Unmarshal(data, &message) + if err != nil { + return "", "", err + } + if message.Version != "1.0" { + return "", "", fmt.Errorf("using unknown version") + } + + if message.Sid == "" || message.Answer == "" { + return "", "", fmt.Errorf("no supplied sid or answer") + } + + return message.Answer, message.Sid, nil +} + +type ProxyAnswerResponse struct { + Status string +} + +func EncodeAnswerResponse(success bool) ([]byte, error) { + if success { + return json.Marshal(ProxyAnswerResponse{ + Status: "success", + }) + + } + return json.Marshal(ProxyAnswerResponse{ + Status: "client gone", + }) +} + +func DecodeAnswerResponse(data []byte) (bool, error) { + var message ProxyAnswerResponse + var success bool + + err := json.Unmarshal(data, &message) + if err != nil { + return success, err + } + if message.Status == "" { + return success, fmt.Errorf("received invalid data") + } + + if message.Status == "success" { + success = true + } + + return success, nil +} diff --git a/common/messages/proxy_test.go b/common/messages/proxy_test.go new file mode 100644 index 0000000..f2f006e --- /dev/null +++ b/common/messages/proxy_test.go @@ -0,0 +1,161 @@ +package messages + +import ( + "encoding/json" + "fmt" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestDecodeProxyPollRequest(t *testing.T) { + Convey("Context", t, func() { + for _, test := range []struct { + sid string + data string + err error + }{ + { + //Version 1.0 proxy message + "ymbcCMto7KHNGYlp", + `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`, + nil, + }, + { + //Version 0.X proxy message: + "", + "ymbcCMto7KHNGYlp", + &json.SyntaxError{}, + }, + { + "", + `{"Sid":"ymbcCMto7KHNGYlp"}`, + fmt.Errorf(""), + }, + { + "", + "{}", + fmt.Errorf(""), + }, + { + "", + `{"Version":"1.0"}`, + fmt.Errorf(""), + }, + { + "", + `{"Version":"2.0"}`, + fmt.Errorf(""), + }, + } { + sid, err := DecodePollRequest([]byte(test.data)) + So(sid, ShouldResemble, test.sid) + So(err, ShouldHaveSameTypeAs, test.err) + } + + }) +} + +func TestEncodeProxyPollRequests(t *testing.T) { + Convey("Context", t, func() { + b, err := EncodePollRequest("ymbcCMto7KHNGYlp") + So(err, ShouldEqual, nil) + sid, err := DecodePollRequest(b) + So(sid, ShouldEqual, "ymbcCMto7KHNGYlp") + So(err, ShouldEqual, nil) + }) +} + +func TestDecodeProxyAnswerRequest(t *testing.T) { + Convey("Context", t, func() { + for _, test := range []struct { + answer string + sid string + data string + err error + }{ + { + "test", + "test", + `{"Version":"1.0","Sid":"test","Answer":"test"}`, + nil, + }, + { + "", + "", + `{"type":"offer","sdp":"v=0\r\no=- 4358805017720277108 2 IN IP4 [scrubbed]\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 [scrubbed]\r\na=candidate:3769337065 1 udp 2122260223 [scrubbed] 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 [scrubbed] 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"}`, + fmt.Errorf(""), + }, + { + "", + "", + `{"Version":"1.0","Answer":"test"}`, + fmt.Errorf(""), + }, + { + "", + "", + `{"Version":"1.0","Sid":"test"}`, + fmt.Errorf(""), + }, + } { + answer, sid, err := DecodeAnswerRequest([]byte(test.data)) + So(answer, ShouldResemble, test.answer) + So(sid, ShouldResemble, test.sid) + So(err, ShouldHaveSameTypeAs, test.err) + } + + }) +} + +func TestEncodeProxyAnswerRequest(t *testing.T) { + Convey("Context", t, func() { + b, err := EncodeAnswerRequest("test answer", "test sid") + So(err, ShouldEqual, nil) + answer, sid, err := DecodeAnswerRequest(b) + So(answer, ShouldEqual, "test answer") + So(sid, ShouldEqual, "test sid") + So(err, ShouldEqual, nil) + }) +} + +func TestDecodeProxyAnswerResponse(t *testing.T) { + Convey("Context", t, func() { + for _, test := range []struct { + success bool + data string + err error + }{ + { + true, + `{"Status":"success"}`, + nil, + }, + { + false, + `{"Status":"client gone"}`, + nil, + }, + { + false, + `{"Test":"test"}`, + fmt.Errorf(""), + }, + } { + success, err := DecodeAnswerResponse([]byte(test.data)) + So(success, ShouldResemble, test.success) + So(err, ShouldHaveSameTypeAs, test.err) + } + + }) +} + +func TestEncodeProxyAnswerResponse(t *testing.T) { + Convey("Context", t, func() { + b, err := EncodeAnswerResponse(true) + So(err, ShouldEqual, nil) + success, err := DecodeAnswerResponse(b) + So(success, ShouldEqual, true) + So(err, ShouldEqual, nil) + }) +} diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index 9b7dad2..ea2a986 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -19,6 +19,7 @@ import ( "sync" "time" + "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" "github.com/pion/webrtc" "golang.org/x/net/websocket" @@ -168,7 +169,12 @@ func pollOffer(sid string) *webrtc.SessionDescription { timeOfNextPoll = now } - req, _ := http.NewRequest("POST", broker.String(), bytes.NewBuffer([]byte(sid))) + b, err := messages.EncodePollRequest(sid) + if err != nil { + log.Printf("Error encoding poll message: %s", err.Error()) + return nil + } + req, _ := http.NewRequest("POST", broker.String(), bytes.NewBuffer(b)) req.Header.Set("X-Session-ID", sid) resp, err := client.Do(req) if err != nil { @@ -182,7 +188,16 @@ func pollOffer(sid string) *webrtc.SessionDescription { if err != nil { log.Printf("error reading broker response: %s", err) } else { - return deserializeSessionDescription(string(body)) + + offer, err := messages.DecodePollResponse(body) + if err != nil { + log.Printf("error reading broker response: %s", err.Error()) + log.Printf("body: %s", body) + return nil + } + if offer != "" { + return deserializeSessionDescription(offer) + } } } } @@ -191,9 +206,12 @@ func pollOffer(sid string) *webrtc.SessionDescription { func sendAnswer(sid string, pc *webrtc.PeerConnection) error { broker := brokerURL.ResolveReference(&url.URL{Path: "answer"}) - body := bytes.NewBuffer([]byte(serializeSessionDescription(pc.LocalDescription()))) - req, _ := http.NewRequest("POST", broker.String(), body) - req.Header.Set("X-Session-ID", sid) + answer := string([]byte(serializeSessionDescription(pc.LocalDescription()))) + b, err := messages.EncodeAnswerRequest(answer, sid) + if err != nil { + return err + } + req, _ := http.NewRequest("POST", broker.String(), bytes.NewBuffer(b)) resp, err := client.Do(req) if err != nil { return err @@ -201,6 +219,19 @@ func sendAnswer(sid string, pc *webrtc.PeerConnection) error { if resp.StatusCode != http.StatusOK { return fmt.Errorf("broker returned %d", resp.StatusCode) } + + body, err := limitedRead(resp.Body, readLimit) + if err != nil { + return fmt.Errorf("error reading broker response: %s", err) + } + success, err := messages.DecodeAnswerResponse(body) + if err != nil { + return err + } + if !success { + return fmt.Errorf("broker returned client timeout") + } + return nil } diff --git a/proxy/translation b/proxy/translation index 120578e..bbf11bb 160000 --- a/proxy/translation +++ b/proxy/translation @@ -1 +1 @@ -Subproject commit 120578ec9dbf0975fc9ac573130282f628b9747a +Subproject commit bbf11bb0c9f1aca4f6b18c6505645f85e2fa1986 From b4b538a17fde49708b291d5f8cb7c444d06c47b2 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 25 Oct 2019 11:29:42 -0400 Subject: [PATCH 009/385] Implemented new broker messages for browser proxy --- common/messages/proxy.go | 2 +- proxy/broker.js | 38 ++++++++++++++++++++++++-------------- proxy/spec/broker.spec.js | 16 ++++++++-------- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/common/messages/proxy.go b/common/messages/proxy.go index 141fbda..042caf9 100644 --- a/common/messages/proxy.go +++ b/common/messages/proxy.go @@ -33,7 +33,7 @@ HTTP 200 OK HTTP 200 OK { - Status: "no proxies" + Status: "no match" } 3) If the request is malformed: diff --git a/proxy/broker.js b/proxy/broker.js index 7b5b7e4..551110b 100644 --- a/proxy/broker.js +++ b/proxy/broker.js @@ -46,10 +46,16 @@ class Broker { return; } switch (xhr.status) { - case Broker.STATUS.OK: - return fulfill(xhr.responseText); // Should contain offer. - case Broker.STATUS.GATEWAY_TIMEOUT: - return reject(Broker.MESSAGE.TIMEOUT); + case Broker.CODE.OK: + var response = JSON.parse(xhr.responseText); + if (response.Status == Broker.STATUS.MATCH) { + return fulfill(response.Offer); // Should contain offer. + } else if (response.Status == Broker.STATUS.TIMEOUT) { + return reject(Broker.MESSAGE.TIMEOUT); + } else { + log('Broker ERROR: Unexpected ' + response.Status); + return reject(Broker.MESSAGE.UNEXPECTED); + } default: log('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText); snowflake.ui.setStatus(' failure. Please refresh.'); @@ -57,7 +63,8 @@ class Broker { } }; this._xhr = xhr; // Used by spec to fake async Broker interaction - return this._postRequest(id, xhr, 'proxy', id); + var data = {"Version": "1.0", "Sid": id} + return this._postRequest(xhr, 'proxy', JSON.stringify(data)); }); } @@ -73,26 +80,24 @@ class Broker { return; } switch (xhr.status) { - case Broker.STATUS.OK: + case Broker.CODE.OK: dbg('Broker: Successfully replied with answer.'); return dbg(xhr.responseText); - case Broker.STATUS.GONE: - return dbg('Broker: No longer valid to reply with answer.'); default: dbg('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText); return snowflake.ui.setStatus(' failure. Please refresh.'); } }; - return this._postRequest(id, xhr, 'answer', JSON.stringify(answer)); + var data = {"Version": "1.0", "Sid": id, "Answer": JSON.stringify(answer)}; + return this._postRequest(xhr, 'answer', JSON.stringify(data)); } // urlSuffix for the broker is different depending on what action // is desired. - _postRequest(id, xhr, urlSuffix, payload) { + _postRequest(xhr, urlSuffix, payload) { var err; try { xhr.open('POST', this.url + urlSuffix); - xhr.setRequestHeader('X-Session-ID', id); } catch (error) { err = error; /* @@ -109,10 +114,15 @@ class Broker { } -Broker.STATUS = { +Broker.CODE = { OK: 200, - GONE: 410, - GATEWAY_TIMEOUT: 504 + BAD_REQUEST: 400, + INTERNAL_SERVER_ERROR: 500 +}; + +Broker.STATUS = { + MATCH: "client match", + TIMEOUT: "no match" }; Broker.MESSAGE = { diff --git a/proxy/spec/broker.spec.js b/proxy/spec/broker.spec.js index 4eb3029..6ab9691 100644 --- a/proxy/spec/broker.spec.js +++ b/proxy/spec/broker.spec.js @@ -35,8 +35,8 @@ describe('Broker', function() { // fake successful request and response from broker. spyOn(b, '_postRequest').and.callFake(function() { b._xhr.readyState = b._xhr.DONE; - b._xhr.status = Broker.STATUS.OK; - b._xhr.responseText = 'fake offer'; + b._xhr.status = Broker.CODE.OK; + b._xhr.responseText = '{"Status":"client match","Offer":"fake offer"}'; return b._xhr.onreadystatechange(); }); poll = b.getClientOffer(); @@ -46,7 +46,7 @@ describe('Broker', function() { expect(desc).toEqual('fake offer'); return done(); }).catch(function() { - fail('should not reject on Broker.STATUS.OK'); + fail('should not reject on Broker.CODE.OK'); return done(); }); }); @@ -57,14 +57,15 @@ describe('Broker', function() { // fake timed-out request from broker spyOn(b, '_postRequest').and.callFake(function() { b._xhr.readyState = b._xhr.DONE; - b._xhr.status = Broker.STATUS.GATEWAY_TIMEOUT; + b._xhr.status = Broker.CODE.OK; + b._xhr.responseText = '{"Status":"no match"}'; return b._xhr.onreadystatechange(); }); poll = b.getClientOffer(); expect(poll).not.toBeNull(); expect(b._postRequest).toHaveBeenCalled(); return poll.then(function(desc) { - fail('should not fulfill on Broker.STATUS.GATEWAY_TIMEOUT'); + fail('should not fulfill with "Status: no match"'); return done(); }, function(err) { expect(err).toBe(Broker.MESSAGE.TIMEOUT); @@ -101,7 +102,7 @@ describe('Broker', function() { var b = new Broker('fake'); spyOn(b, '_postRequest'); b.sendAnswer('fake id', 123); - expect(b._postRequest).toHaveBeenCalledWith('fake id', jasmine.any(Object), 'answer', '123'); + expect(b._postRequest).toHaveBeenCalledWith(jasmine.any(Object), 'answer', '{"Version":"1.0","Sid":"fake id","Answer":"123"}'); }); it('POST XMLHttpRequests to the broker', function() { @@ -110,9 +111,8 @@ describe('Broker', function() { spyOn(b._xhr, 'open'); spyOn(b._xhr, 'setRequestHeader'); spyOn(b._xhr, 'send'); - b._postRequest(0, b._xhr, 'test', 'data'); + b._postRequest(b._xhr, 'test', 'data'); expect(b._xhr.open).toHaveBeenCalled(); - expect(b._xhr.setRequestHeader).toHaveBeenCalled(); expect(b._xhr.send).toHaveBeenCalled(); }); From a7040e2eee6aa9c0f8606bd9d6e2d18256399372 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 13 Nov 2019 11:39:33 -0500 Subject: [PATCH 010/385] Update travis to use go v1.13.x --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2f02f74..b986122 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ addons: - gcc-5 go: - - 1.10.x + - 1.13.x env: - TRAVIS_NODE_VERSION="8" CC="gcc-5" CXX="g++-5" From 2f37a73e71e6fa314196e276a51162f4731a7183 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 13 Nov 2019 13:36:30 -0500 Subject: [PATCH 011/385] bump version to 0.1.0 --- proxy/translation | 2 +- proxy/webext/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/translation b/proxy/translation index bbf11bb..c145dfe 160000 --- a/proxy/translation +++ b/proxy/translation @@ -1 +1 @@ -Subproject commit bbf11bb0c9f1aca4f6b18c6505645f85e2fa1986 +Subproject commit c145dfe5b308085a700264e96509799355a52fa6 diff --git a/proxy/webext/manifest.json b/proxy/webext/manifest.json index b3c757a..90d3922 100644 --- a/proxy/webext/manifest.json +++ b/proxy/webext/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Snowflake", - "version": "0.0.13", + "version": "0.1.0", "description": "__MSG_appDesc__", "default_locale": "en_US", "background": { From 3ec2e8b89e6e8e1e8ac1315fc9431870003bfb9d Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 12 Jun 2019 11:33:11 -0400 Subject: [PATCH 012/385] Renamed existing test file --- proxy-go/{webrtc_test.go => proxy-go_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename proxy-go/{webrtc_test.go => proxy-go_test.go} (100%) diff --git a/proxy-go/webrtc_test.go b/proxy-go/proxy-go_test.go similarity index 100% rename from proxy-go/webrtc_test.go rename to proxy-go/proxy-go_test.go From 32bec89a848b4b5d2c9be20420e39a50190930d5 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 4 Nov 2019 13:48:22 -0500 Subject: [PATCH 013/385] Add tests for session descripion functions Also removed some unnecessary code --- proxy-go/proxy-go_test.go | 82 +++++++++++++++++++++++++++++++++++++++ proxy-go/snowflake.go | 4 -- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/proxy-go/proxy-go_test.go b/proxy-go/proxy-go_test.go index 2413207..c6df31c 100644 --- a/proxy-go/proxy-go_test.go +++ b/proxy-go/proxy-go_test.go @@ -4,6 +4,9 @@ import ( "net" "strings" "testing" + + "github.com/pion/webrtc" + . "github.com/smartystreets/goconvey/convey" ) func TestRemoteIPFromSDP(t *testing.T) { @@ -107,3 +110,82 @@ a=sctpmap:5000 webrtc-datachannel 1024 } } } + +func TestSessionDescriptions(t *testing.T) { + Convey("Session description deserialization", t, func() { + for _, test := range []struct { + msg string + ret *webrtc.SessionDescription + }{ + { + "test", + nil, + }, + { + `{"type":"answer"}`, + nil, + }, + { + `{"sdp":"test"}`, + nil, + }, + { + `{"type":"test", "sdp":"test"}`, + nil, + }, + { + `{"type":"answer", "sdp":"test"}`, + &webrtc.SessionDescription{ + Type: webrtc.SDPTypeAnswer, + SDP: "test", + }, + }, + { + `{"type":"pranswer", "sdp":"test"}`, + &webrtc.SessionDescription{ + Type: webrtc.SDPTypePranswer, + SDP: "test", + }, + }, + { + `{"type":"rollback", "sdp":"test"}`, + &webrtc.SessionDescription{ + Type: webrtc.SDPTypeRollback, + SDP: "test", + }, + }, + { + `{"type":"offer", "sdp":"test"}`, + &webrtc.SessionDescription{ + Type: webrtc.SDPTypeOffer, + SDP: "test", + }, + }, + } { + desc := deserializeSessionDescription(test.msg) + So(desc, ShouldResemble, test.ret) + } + }) + Convey("Session description serialization", t, func() { + for _, test := range []struct { + desc *webrtc.SessionDescription + ret string + }{ + { + &webrtc.SessionDescription{ + Type: webrtc.SDPTypeOffer, + SDP: "test", + }, + `{"type":"offer","sdp":"test"}`, + }, + } { + msg := serializeSessionDescription(test.desc) + So(msg, ShouldResemble, test.ret) + } + }) +} + +func TestUtilityFuncs(t *testing.T) { + Convey("LimitedRead", t, func() { + }) +} diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index ea2a986..9e52fd6 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -493,10 +493,6 @@ func deserializeSessionDescription(msg string) *webrtc.SessionDescription { stype = webrtc.SDPTypeRollback } - if err != nil { - log.Println(err) - return nil - } return &webrtc.SessionDescription{ Type: stype, SDP: parsed["sdp"].(string), From 574c57cc98bd10571bc8c6feb6d3889de533f70e Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 4 Nov 2019 15:09:25 -0500 Subject: [PATCH 014/385] Created tests for proxy-go utility functions --- proxy-go/proxy-go_test.go | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/proxy-go/proxy-go_test.go b/proxy-go/proxy-go_test.go index c6df31c..d969acb 100644 --- a/proxy-go/proxy-go_test.go +++ b/proxy-go/proxy-go_test.go @@ -1,6 +1,7 @@ package main import ( + "io" "net" "strings" "testing" @@ -187,5 +188,67 @@ func TestSessionDescriptions(t *testing.T) { func TestUtilityFuncs(t *testing.T) { Convey("LimitedRead", t, func() { + c, s := net.Pipe() + Convey("Successful read", func() { + go func() { + bytes := make([]byte, 50) + c.Write(bytes) + c.Close() + }() + bytes, err := limitedRead(s, 60) + So(len(bytes), ShouldEqual, 50) + So(err, ShouldBeNil) + }) + Convey("Large read", func() { + go func() { + bytes := make([]byte, 50) + c.Write(bytes) + c.Close() + }() + bytes, err := limitedRead(s, 49) + So(len(bytes), ShouldEqual, 49) + So(err, ShouldEqual, io.ErrUnexpectedEOF) + }) + Convey("Failed read", func() { + s.Close() + bytes, err := limitedRead(s, 49) + So(len(bytes), ShouldEqual, 0) + So(err, ShouldEqual, io.ErrClosedPipe) + }) + }) + Convey("Tokens", t, func() { + tokens = make(chan bool, 2) + for i := uint(0); i < 2; i++ { + tokens <- true + } + So(len(tokens), ShouldEqual, 2) + getToken() + So(len(tokens), ShouldEqual, 1) + retToken() + So(len(tokens), ShouldEqual, 2) + }) + Convey("SessionID Generation", t, func() { + sid1 := genSessionID() + sid2 := genSessionID() + So(sid1, ShouldNotEqual, sid2) + }) + Convey("CopyLoop", t, func() { + c1, s1 := net.Pipe() + c2, s2 := net.Pipe() + go CopyLoop(s1, s2) + go func() { + bytes := []byte("Hello!") + c1.Write(bytes) + }() + bytes := make([]byte, 6) + n, err := c2.Read(bytes) + So(n, ShouldEqual, 6) + So(err, ShouldEqual, nil) + So(bytes, ShouldResemble, []byte("Hello!")) + s1.Close() + + //Check that copy loop has closed other connection + _, err = s2.Write(bytes) + So(err, ShouldNotBeNil) }) } From 446f39a9e5a0e47f70234bc3fbce422a34fb040a Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 12 Jun 2019 14:15:21 -0400 Subject: [PATCH 015/385] Use http.RoundTripper for connections to broker This change makes it easier for us to write tests with mock transports --- proxy-go/snowflake.go | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index 9e52fd6..c4b2f0b 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -36,7 +36,7 @@ const dataChannelTimeout = 20 * time.Second const readLimit = 100000 //Maximum number of bytes to be read from an HTTP request -var brokerURL *url.URL +var broker *Broker var relayURL string const ( @@ -68,6 +68,11 @@ func remoteIPFromSDP(sdp string) net.IP { return nil } +type Broker struct { + url *url.URL + transport http.RoundTripper +} + type webRTCConn struct { dc *webrtc.DataChannel pc *webrtc.PeerConnection @@ -154,8 +159,8 @@ func limitedRead(r io.Reader, limit int64) ([]byte, error) { return p, err } -func pollOffer(sid string) *webrtc.SessionDescription { - broker := brokerURL.ResolveReference(&url.URL{Path: "proxy"}) +func (b *Broker) pollOffer(sid string) *webrtc.SessionDescription { + brokerPath := b.url.ResolveReference(&url.URL{Path: "proxy"}) timeOfNextPoll := time.Now() for { // Sleep until we're scheduled to poll again. @@ -169,14 +174,13 @@ func pollOffer(sid string) *webrtc.SessionDescription { timeOfNextPoll = now } - b, err := messages.EncodePollRequest(sid) + body, err := messages.EncodePollRequest(sid) if err != nil { log.Printf("Error encoding poll message: %s", err.Error()) return nil } - req, _ := http.NewRequest("POST", broker.String(), bytes.NewBuffer(b)) - req.Header.Set("X-Session-ID", sid) - resp, err := client.Do(req) + req, _ := http.NewRequest("POST", brokerPath.String(), bytes.NewBuffer(body)) + resp, err := b.transport.RoundTrip(req) if err != nil { log.Printf("error polling broker: %s", err) } else { @@ -204,15 +208,15 @@ func pollOffer(sid string) *webrtc.SessionDescription { } } -func sendAnswer(sid string, pc *webrtc.PeerConnection) error { - broker := brokerURL.ResolveReference(&url.URL{Path: "answer"}) +func (b *Broker) sendAnswer(sid string, pc *webrtc.PeerConnection) error { + brokerPath := b.url.ResolveReference(&url.URL{Path: "answer"}) answer := string([]byte(serializeSessionDescription(pc.LocalDescription()))) - b, err := messages.EncodeAnswerRequest(answer, sid) + body, err := messages.EncodeAnswerRequest(answer, sid) if err != nil { return err } - req, _ := http.NewRequest("POST", broker.String(), bytes.NewBuffer(b)) - resp, err := client.Do(req) + req, _ := http.NewRequest("POST", brokerPath.String(), bytes.NewBuffer(body)) + resp, err := b.transport.RoundTrip(req) if err != nil { return err } @@ -220,7 +224,7 @@ func sendAnswer(sid string, pc *webrtc.PeerConnection) error { return fmt.Errorf("broker returned %d", resp.StatusCode) } - body, err := limitedRead(resp.Body, readLimit) + body, err = limitedRead(resp.Body, readLimit) if err != nil { return fmt.Errorf("error reading broker response: %s", err) } @@ -364,7 +368,7 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, config webrtc.C } func runSession(sid string) { - offer := pollOffer(sid) + offer := broker.pollOffer(sid) if offer == nil { log.Printf("bad offer from broker") retToken() @@ -377,7 +381,7 @@ func runSession(sid string) { retToken() return } - err = sendAnswer(sid, pc) + err = broker.sendAnswer(sid, pc) if err != nil { log.Printf("error sending answer to client through broker: %s", err) if inerr := pc.Close(); inerr != nil { @@ -430,7 +434,8 @@ func main() { log.Println("starting") var err error - brokerURL, err = url.Parse(rawBrokerURL) + broker = new(Broker) + broker.url, err = url.Parse(rawBrokerURL) if err != nil { log.Fatalf("invalid broker url: %s", err) } @@ -443,6 +448,7 @@ func main() { log.Fatalf("invalid relay url: %s", err) } + broker.transport = http.DefaultTransport.(*http.Transport) config = webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { From 459286c143fbcaaa76e183362acd0bf69245ba79 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 12 Jun 2019 15:38:57 -0400 Subject: [PATCH 016/385] Test proxy-go interactions with broker --- proxy-go/proxy-go_test.go | 143 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/proxy-go/proxy-go_test.go b/proxy-go/proxy-go_test.go index d969acb..a09dcc3 100644 --- a/proxy-go/proxy-go_test.go +++ b/proxy-go/proxy-go_test.go @@ -1,15 +1,56 @@ package main import ( + "bytes" + "fmt" "io" + "io/ioutil" "net" + "net/http" + "net/url" "strings" "testing" + "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "github.com/pion/webrtc" . "github.com/smartystreets/goconvey/convey" ) +// Set up a mock broker to communicate with +type MockTransport struct { + statusOverride int + body []byte +} + +// Just returns a response with fake SDP answer. +func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + s := ioutil.NopCloser(bytes.NewReader(m.body)) + r := &http.Response{ + StatusCode: m.statusOverride, + Body: s, + } + return r, nil +} + +// Set up a mock faulty transport +type FaultyTransport struct { + statusOverride int + body []byte +} + +// Just returns a response with fake SDP answer. +func (f *FaultyTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("TransportFailed") +} + +const SDP = "v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\na=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 8.8.8.8 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n" + +const sampleSDP = `"v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\na=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 8.8.8.8 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"` + +var sampleOffer = `{"type":"offer","sdp":` + sampleSDP + `}` + +const sampleAnswer = `{"type":"answer","sdp":` + sampleSDP + `}` + func TestRemoteIPFromSDP(t *testing.T) { tests := []struct { sdp string @@ -186,6 +227,108 @@ func TestSessionDescriptions(t *testing.T) { }) } +func TestBrokerInteractions(t *testing.T) { + Convey("Proxy connections to broker", t, func() { + broker := new(Broker) + broker.url, _ = url.Parse("localhost") + + //Mock peerConnection + config = webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{"stun:stun.l.google.com:19302"}, + }, + }, + } + pc, _ := webrtc.NewPeerConnection(config) + offer := deserializeSessionDescription(sampleOffer) + pc.SetRemoteDescription(*offer) + answer, _ := pc.CreateAnswer(nil) + pc.SetLocalDescription(answer) + + Convey("polls broker correctly", func() { + var err error + + b, err := messages.EncodePollResponse(sampleOffer, true) + So(err, ShouldEqual, nil) + broker.transport = &MockTransport{ + http.StatusOK, + b, + } + + sdp := broker.pollOffer(sampleOffer) + So(sdp.SDP, ShouldEqual, SDP) + }) + Convey("handles poll error", func() { + var err error + + b := []byte("test") + So(err, ShouldEqual, nil) + broker.transport = &MockTransport{ + http.StatusOK, + b, + } + + sdp := broker.pollOffer(sampleOffer) + So(sdp, ShouldBeNil) + }) + Convey("sends answer to broker", func() { + var err error + + b, err := messages.EncodeAnswerResponse(true) + So(err, ShouldEqual, nil) + broker.transport = &MockTransport{ + http.StatusOK, + b, + } + + err = broker.sendAnswer(sampleAnswer, pc) + So(err, ShouldEqual, nil) + + b, err = messages.EncodeAnswerResponse(false) + So(err, ShouldEqual, nil) + broker.transport = &MockTransport{ + http.StatusOK, + b, + } + + err = broker.sendAnswer(sampleAnswer, pc) + So(err, ShouldNotBeNil) + }) + Convey("handles answer error", func() { + //Error if faulty transport + broker.transport = &FaultyTransport{} + err := broker.sendAnswer(sampleAnswer, pc) + So(err, ShouldNotBeNil) + + //Error if status code is not ok + broker.transport = &MockTransport{ + http.StatusGone, + []byte(""), + } + err = broker.sendAnswer("test", pc) + So(err, ShouldNotEqual, nil) + So(err.Error(), ShouldResemble, "broker returned 410") + + //Error if we can't parse broker message + broker.transport = &MockTransport{ + http.StatusOK, + []byte("test"), + } + err = broker.sendAnswer("test", pc) + So(err, ShouldNotBeNil) + + //Error if broker message surpasses read limit + broker.transport = &MockTransport{ + http.StatusOK, + make([]byte, 100001), + } + err = broker.sendAnswer("test", pc) + So(err, ShouldNotBeNil) + }) + }) +} + func TestUtilityFuncs(t *testing.T) { Convey("LimitedRead", t, func() { c, s := net.Pipe() From 742070a7fbeef8e8dc0c68060a12266fe60e0ba0 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 13 Nov 2019 14:31:55 -0500 Subject: [PATCH 017/385] Clean up proxy-go tests --- proxy-go/proxy-go_test.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/proxy-go/proxy-go_test.go b/proxy-go/proxy-go_test.go index a09dcc3..ebe4381 100644 --- a/proxy-go/proxy-go_test.go +++ b/proxy-go/proxy-go_test.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "net/url" + "strconv" "strings" "testing" @@ -43,14 +44,6 @@ func (f *FaultyTransport) RoundTrip(req *http.Request) (*http.Response, error) { return nil, fmt.Errorf("TransportFailed") } -const SDP = "v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\na=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 8.8.8.8 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n" - -const sampleSDP = `"v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\na=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 8.8.8.8 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"` - -var sampleOffer = `{"type":"offer","sdp":` + sampleSDP + `}` - -const sampleAnswer = `{"type":"answer","sdp":` + sampleSDP + `}` - func TestRemoteIPFromSDP(t *testing.T) { tests := []struct { sdp string @@ -228,6 +221,11 @@ func TestSessionDescriptions(t *testing.T) { } func TestBrokerInteractions(t *testing.T) { + const sampleSDP = `"v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\na=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 8.8.8.8 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"` + + const sampleOffer = `{"type":"offer","sdp":` + sampleSDP + `}` + const sampleAnswer = `{"type":"answer","sdp":` + sampleSDP + `}` + Convey("Proxy connections to broker", t, func() { broker := new(Broker) broker.url, _ = url.Parse("localhost") @@ -257,7 +255,8 @@ func TestBrokerInteractions(t *testing.T) { } sdp := broker.pollOffer(sampleOffer) - So(sdp.SDP, ShouldEqual, SDP) + expectedSDP, _ := strconv.Unquote(sampleSDP) + So(sdp.SDP, ShouldResemble, expectedSDP) }) Convey("handles poll error", func() { var err error From 7557e96a8d41c778a0b039b03969c66f91bd108a Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 13 Nov 2019 15:01:03 -0500 Subject: [PATCH 018/385] Remove unnecessary logging at broker --- broker/broker.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index 4343de8..13d2575 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -161,7 +161,6 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { sid, err := messages.DecodePollRequest(body) if err != nil { - log.Println("Invalid data.") w.WriteHeader(http.StatusBadRequest) return } @@ -259,7 +258,6 @@ func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { answer, id, err := messages.DecodeAnswerRequest(body) if err != nil || answer == "" { - log.Println("Invalid data.") w.WriteHeader(http.StatusBadRequest) return } From 30b5ef8a9e9c7a5b306e9285d1a8db323f8f22b2 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Wed, 20 Nov 2019 19:33:28 -0500 Subject: [PATCH 019/385] Use gorilla websocket in proxy-go too Trac: 32465 --- common/websocketconn/websocketconn.go | 89 +++++++++++++++++++ common/websocketconn/websocketconn_test.go | 30 +++++++ proxy-go/proxy-go_test.go | 19 ----- proxy-go/snowflake.go | 25 ++---- server/server.go | 99 +--------------------- 5 files changed, 128 insertions(+), 134 deletions(-) create mode 100644 common/websocketconn/websocketconn.go create mode 100644 common/websocketconn/websocketconn_test.go diff --git a/common/websocketconn/websocketconn.go b/common/websocketconn/websocketconn.go new file mode 100644 index 0000000..399cbaa --- /dev/null +++ b/common/websocketconn/websocketconn.go @@ -0,0 +1,89 @@ +package websocketconn + +import ( + "io" + "log" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +// An abstraction that makes an underlying WebSocket connection look like an +// io.ReadWriteCloser. +type WebSocketConn struct { + Ws *websocket.Conn + r io.Reader +} + +// Implements io.Reader. +func (conn *WebSocketConn) Read(b []byte) (n int, err error) { + var opCode int + if conn.r == nil { + // New message + var r io.Reader + for { + if opCode, r, err = conn.Ws.NextReader(); err != nil { + return + } + if opCode != websocket.BinaryMessage && opCode != websocket.TextMessage { + continue + } + + conn.r = r + break + } + } + + n, err = conn.r.Read(b) + if err == io.EOF { + // Message finished + conn.r = nil + err = nil + } + return +} + +// Implements io.Writer. +func (conn *WebSocketConn) Write(b []byte) (n int, err error) { + var w io.WriteCloser + if w, err = conn.Ws.NextWriter(websocket.BinaryMessage); err != nil { + return + } + if n, err = w.Write(b); err != nil { + return + } + err = w.Close() + return +} + +// Implements io.Closer. +func (conn *WebSocketConn) Close() error { + // Ignore any error in trying to write a Close frame. + _ = conn.Ws.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second)) + return conn.Ws.Close() +} + +// Create a new WebSocketConn. +func NewWebSocketConn(ws *websocket.Conn) WebSocketConn { + var conn WebSocketConn + conn.Ws = ws + return conn +} + +// Copy from WebSocket to socket and vice versa. +func CopyLoop(c1 io.ReadWriteCloser, c2 io.ReadWriteCloser) { + var wg sync.WaitGroup + copyer := func(dst io.ReadWriteCloser, src io.ReadWriteCloser) { + defer wg.Done() + if _, err := io.Copy(dst, src); err != nil { + log.Printf("io.Copy inside CopyLoop generated an error: %v", err) + } + dst.Close() + src.Close() + } + wg.Add(2) + go copyer(c1, c2) + go copyer(c2, c1) + wg.Wait() +} diff --git a/common/websocketconn/websocketconn_test.go b/common/websocketconn/websocketconn_test.go new file mode 100644 index 0000000..3293165 --- /dev/null +++ b/common/websocketconn/websocketconn_test.go @@ -0,0 +1,30 @@ +package websocketconn + +import ( + "net" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestWebsocketConn(t *testing.T) { + Convey("CopyLoop", t, func() { + c1, s1 := net.Pipe() + c2, s2 := net.Pipe() + go CopyLoop(s1, s2) + go func() { + bytes := []byte("Hello!") + c1.Write(bytes) + }() + bytes := make([]byte, 6) + n, err := c2.Read(bytes) + So(n, ShouldEqual, 6) + So(err, ShouldEqual, nil) + So(bytes, ShouldResemble, []byte("Hello!")) + s1.Close() + + // Check that copy loop has closed other connection + _, err = s2.Write(bytes) + So(err, ShouldNotBeNil) + }) +} diff --git a/proxy-go/proxy-go_test.go b/proxy-go/proxy-go_test.go index ebe4381..538957b 100644 --- a/proxy-go/proxy-go_test.go +++ b/proxy-go/proxy-go_test.go @@ -374,23 +374,4 @@ func TestUtilityFuncs(t *testing.T) { sid2 := genSessionID() So(sid1, ShouldNotEqual, sid2) }) - Convey("CopyLoop", t, func() { - c1, s1 := net.Pipe() - c2, s2 := net.Pipe() - go CopyLoop(s1, s2) - go func() { - bytes := []byte("Hello!") - c1.Write(bytes) - }() - bytes := make([]byte, 6) - n, err := c2.Read(bytes) - So(n, ShouldEqual, 6) - So(err, ShouldEqual, nil) - So(bytes, ShouldResemble, []byte("Hello!")) - s1.Close() - - //Check that copy loop has closed other connection - _, err = s2.Write(bytes) - So(err, ShouldNotBeNil) - }) } diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index c4b2f0b..0e14eb2 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -21,8 +21,9 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" + "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" + "github.com/gorilla/websocket" "github.com/pion/webrtc" - "golang.org/x/net/websocket" ) const defaultBrokerURL = "https://snowflake-broker.bamsoftware.com/" @@ -239,22 +240,6 @@ func (b *Broker) sendAnswer(sid string, pc *webrtc.PeerConnection) error { return nil } -func CopyLoop(c1 net.Conn, c2 net.Conn) { - var wg sync.WaitGroup - copyer := func(dst io.ReadWriteCloser, src io.ReadWriteCloser) { - defer wg.Done() - if _, err := io.Copy(dst, src); err != nil { - log.Printf("io.Copy inside CopyLoop generated an error: %v", err) - } - dst.Close() - src.Close() - } - wg.Add(2) - go copyer(c1, c2) - go copyer(c2, c1) - wg.Wait() -} - // We pass conn.RemoteAddr() as an additional parameter, rather than calling // conn.RemoteAddr() inside this function, as a workaround for a hang that // otherwise occurs inside of conn.pc.RemoteDescription() (called by @@ -279,15 +264,15 @@ func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) { log.Printf("no remote address given in websocket") } - wsConn, err := websocket.Dial(u.String(), "", relayURL) + ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { log.Printf("error dialing relay: %s", err) return } + wsConn := websocketconn.NewWebSocketConn(ws) log.Printf("connected to relay") defer wsConn.Close() - wsConn.PayloadType = websocket.BinaryFrame - CopyLoop(conn, wsConn) + websocketconn.CopyLoop(conn, &wsConn) log.Printf("datachannelHandler ends") } diff --git a/server/server.go b/server/server.go index ce804fc..d950ddc 100644 --- a/server/server.go +++ b/server/server.go @@ -15,12 +15,12 @@ import ( "os/signal" "path/filepath" "strings" - "sync" "syscall" "time" pt "git.torproject.org/pluggable-transports/goptlib.git" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" + "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" "github.com/gorilla/websocket" "golang.org/x/crypto/acme/autocert" "golang.org/x/net/http2" @@ -50,97 +50,6 @@ additional HTTP listener on port 80 to work with ACME. flag.PrintDefaults() } -// An abstraction that makes an underlying WebSocket connection look like an -// io.ReadWriteCloser. -type webSocketConn struct { - Ws *websocket.Conn - r io.Reader -} - -// Implements io.Reader. -func (conn *webSocketConn) Read(b []byte) (n int, err error) { - var opCode int - if conn.r == nil { - // New message - var r io.Reader - for { - if opCode, r, err = conn.Ws.NextReader(); err != nil { - return - } - if opCode != websocket.BinaryMessage && opCode != websocket.TextMessage { - continue - } - - conn.r = r - break - } - } - - n, err = conn.r.Read(b) - if err == io.EOF { - // Message finished - conn.r = nil - err = nil - } - return -} - -// Implements io.Writer. -func (conn *webSocketConn) Write(b []byte) (n int, err error) { - var w io.WriteCloser - if w, err = conn.Ws.NextWriter(websocket.BinaryMessage); err != nil { - return - } - if n, err = w.Write(b); err != nil { - return - } - err = w.Close() - return -} - -// Implements io.Closer. -func (conn *webSocketConn) Close() error { - // Ignore any error in trying to write a Close frame. - _ = conn.Ws.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second)) - return conn.Ws.Close() -} - -// Create a new webSocketConn. -func newWebSocketConn(ws *websocket.Conn) webSocketConn { - var conn webSocketConn - conn.Ws = ws - return conn -} - -// Copy from WebSocket to socket and vice versa. -func proxy(local *net.TCPConn, conn *webSocketConn) { - var wg sync.WaitGroup - wg.Add(2) - - go func() { - if _, err := io.Copy(conn, local); err != nil { - log.Printf("error copying ORPort to WebSocket %v", err) - } - if err := local.CloseRead(); err != nil { - log.Printf("error closing read after copying ORPort to WebSocket %v", err) - } - conn.Close() - wg.Done() - }() - go func() { - if _, err := io.Copy(local, conn); err != nil { - log.Printf("error copying WebSocket to ORPort") - } - if err := local.CloseWrite(); err != nil { - log.Printf("error closing write after copying WebSocket to ORPort %v", err) - } - conn.Close() - wg.Done() - }() - - wg.Wait() -} - // Return an address string suitable to pass into pt.DialOr. func clientAddr(clientIPParam string) string { if clientIPParam == "" { @@ -166,8 +75,8 @@ func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - conn := newWebSocketConn(ws) - defer conn.Close() + wsConn := websocketconn.NewWebSocketConn(ws) + defer wsConn.Close() // Pass the address of client as the remote address of incoming connection clientIPParam := r.URL.Query().Get("client_ip") @@ -184,7 +93,7 @@ func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } defer or.Close() - proxy(or, &conn) + websocketconn.CopyLoop(or, &wsConn) } func initServer(addr *net.TCPAddr, From 7092b2cb2c24759286f3ecc7713ad30115415e41 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 21 Nov 2019 19:33:39 -0500 Subject: [PATCH 020/385] Revert abstracting copyloop --- common/websocketconn/websocketconn.go | 19 ------------ common/websocketconn/websocketconn_test.go | 30 ------------------ proxy-go/proxy-go_test.go | 19 ++++++++++++ proxy-go/snowflake.go | 18 ++++++++++- server/server.go | 36 ++++++++++++++++++++-- 5 files changed, 69 insertions(+), 53 deletions(-) delete mode 100644 common/websocketconn/websocketconn_test.go diff --git a/common/websocketconn/websocketconn.go b/common/websocketconn/websocketconn.go index 399cbaa..7e12abf 100644 --- a/common/websocketconn/websocketconn.go +++ b/common/websocketconn/websocketconn.go @@ -2,8 +2,6 @@ package websocketconn import ( "io" - "log" - "sync" "time" "github.com/gorilla/websocket" @@ -70,20 +68,3 @@ func NewWebSocketConn(ws *websocket.Conn) WebSocketConn { conn.Ws = ws return conn } - -// Copy from WebSocket to socket and vice versa. -func CopyLoop(c1 io.ReadWriteCloser, c2 io.ReadWriteCloser) { - var wg sync.WaitGroup - copyer := func(dst io.ReadWriteCloser, src io.ReadWriteCloser) { - defer wg.Done() - if _, err := io.Copy(dst, src); err != nil { - log.Printf("io.Copy inside CopyLoop generated an error: %v", err) - } - dst.Close() - src.Close() - } - wg.Add(2) - go copyer(c1, c2) - go copyer(c2, c1) - wg.Wait() -} diff --git a/common/websocketconn/websocketconn_test.go b/common/websocketconn/websocketconn_test.go deleted file mode 100644 index 3293165..0000000 --- a/common/websocketconn/websocketconn_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package websocketconn - -import ( - "net" - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestWebsocketConn(t *testing.T) { - Convey("CopyLoop", t, func() { - c1, s1 := net.Pipe() - c2, s2 := net.Pipe() - go CopyLoop(s1, s2) - go func() { - bytes := []byte("Hello!") - c1.Write(bytes) - }() - bytes := make([]byte, 6) - n, err := c2.Read(bytes) - So(n, ShouldEqual, 6) - So(err, ShouldEqual, nil) - So(bytes, ShouldResemble, []byte("Hello!")) - s1.Close() - - // Check that copy loop has closed other connection - _, err = s2.Write(bytes) - So(err, ShouldNotBeNil) - }) -} diff --git a/proxy-go/proxy-go_test.go b/proxy-go/proxy-go_test.go index 538957b..ebe4381 100644 --- a/proxy-go/proxy-go_test.go +++ b/proxy-go/proxy-go_test.go @@ -374,4 +374,23 @@ func TestUtilityFuncs(t *testing.T) { sid2 := genSessionID() So(sid1, ShouldNotEqual, sid2) }) + Convey("CopyLoop", t, func() { + c1, s1 := net.Pipe() + c2, s2 := net.Pipe() + go CopyLoop(s1, s2) + go func() { + bytes := []byte("Hello!") + c1.Write(bytes) + }() + bytes := make([]byte, 6) + n, err := c2.Read(bytes) + So(n, ShouldEqual, 6) + So(err, ShouldEqual, nil) + So(bytes, ShouldResemble, []byte("Hello!")) + s1.Close() + + //Check that copy loop has closed other connection + _, err = s2.Write(bytes) + So(err, ShouldNotBeNil) + }) } diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index 0e14eb2..c10093a 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -240,6 +240,22 @@ func (b *Broker) sendAnswer(sid string, pc *webrtc.PeerConnection) error { return nil } +func CopyLoop(c1 io.ReadWriteCloser, c2 io.ReadWriteCloser) { + var wg sync.WaitGroup + copyer := func(dst io.ReadWriteCloser, src io.ReadWriteCloser) { + defer wg.Done() + if _, err := io.Copy(dst, src); err != nil { + log.Printf("io.Copy inside CopyLoop generated an error: %v", err) + } + dst.Close() + src.Close() + } + wg.Add(2) + go copyer(c1, c2) + go copyer(c2, c1) + wg.Wait() +} + // We pass conn.RemoteAddr() as an additional parameter, rather than calling // conn.RemoteAddr() inside this function, as a workaround for a hang that // otherwise occurs inside of conn.pc.RemoteDescription() (called by @@ -272,7 +288,7 @@ func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) { wsConn := websocketconn.NewWebSocketConn(ws) log.Printf("connected to relay") defer wsConn.Close() - websocketconn.CopyLoop(conn, &wsConn) + CopyLoop(conn, &wsConn) log.Printf("datachannelHandler ends") } diff --git a/server/server.go b/server/server.go index d950ddc..e3f4c6f 100644 --- a/server/server.go +++ b/server/server.go @@ -15,6 +15,7 @@ import ( "os/signal" "path/filepath" "strings" + "sync" "syscall" "time" @@ -50,6 +51,35 @@ additional HTTP listener on port 80 to work with ACME. flag.PrintDefaults() } +// Copy from WebSocket to socket and vice versa. +func proxy(local *net.TCPConn, conn *websocketconn.WebSocketConn) { + var wg sync.WaitGroup + wg.Add(2) + + go func() { + if _, err := io.Copy(conn, local); err != nil { + log.Printf("error copying ORPort to WebSocket %v", err) + } + if err := local.CloseRead(); err != nil { + log.Printf("error closing read after copying ORPort to WebSocket %v", err) + } + conn.Close() + wg.Done() + }() + go func() { + if _, err := io.Copy(local, conn); err != nil { + log.Printf("error copying WebSocket to ORPort") + } + if err := local.CloseWrite(); err != nil { + log.Printf("error closing write after copying WebSocket to ORPort %v", err) + } + conn.Close() + wg.Done() + }() + + wg.Wait() +} + // Return an address string suitable to pass into pt.DialOr. func clientAddr(clientIPParam string) string { if clientIPParam == "" { @@ -75,8 +105,8 @@ func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - wsConn := websocketconn.NewWebSocketConn(ws) - defer wsConn.Close() + conn := websocketconn.NewWebSocketConn(ws) + defer conn.Close() // Pass the address of client as the remote address of incoming connection clientIPParam := r.URL.Query().Get("client_ip") @@ -93,7 +123,7 @@ func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } defer or.Close() - websocketconn.CopyLoop(or, &wsConn) + proxy(or, &conn) } func initServer(addr *net.TCPAddr, From 7277bb37cd8a96afd8516870cc286b3845fa48bb Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 20 Nov 2019 12:41:53 -0500 Subject: [PATCH 021/385] Update broker--proxy protocol with proxy type Proxies now include information about what type they are when they poll for client offers. The broker saves this information along with snowflake ids and outputs it on the /debug page. --- broker/broker.go | 26 +++++++++++++------ broker/snowflake-broker_test.go | 14 +++++------ broker/snowflake-heap.go | 1 + common/messages/proxy.go | 44 +++++++++++++++++++-------------- common/messages/proxy_test.go | 28 ++++++++++++++++----- proxy-go/snowflake.go | 2 +- 6 files changed, 75 insertions(+), 40 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index 13d2575..3edfe84 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -97,14 +97,16 @@ func (mh MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Proxies may poll for client offers concurrently. type ProxyPoll struct { id string + ptype string offerChannel chan []byte } // Registers a Snowflake and waits for some Client to send an offer, // as part of the polling logic of the proxy handler. -func (ctx *BrokerContext) RequestOffer(id string) []byte { +func (ctx *BrokerContext) RequestOffer(id string, ptype string) []byte { request := new(ProxyPoll) request.id = id + request.ptype = ptype request.offerChannel = make(chan []byte) ctx.proxyPolls <- request // Block until an offer is available, or timeout which sends a nil offer. @@ -117,7 +119,7 @@ func (ctx *BrokerContext) RequestOffer(id string) []byte { // client offer or nil on timeout / none are available. func (ctx *BrokerContext) Broker() { for request := range ctx.proxyPolls { - snowflake := ctx.AddSnowflake(request.id) + snowflake := ctx.AddSnowflake(request.id, request.ptype) // Wait for a client to avail an offer to the snowflake. go func(request *ProxyPoll) { select { @@ -137,10 +139,11 @@ func (ctx *BrokerContext) Broker() { // Create and add a Snowflake to the heap. // Required to keep track of proxies between providing them // with an offer and awaiting their second POST with an answer. -func (ctx *BrokerContext) AddSnowflake(id string) *Snowflake { +func (ctx *BrokerContext) AddSnowflake(id string, ptype string) *Snowflake { snowflake := new(Snowflake) snowflake.id = id snowflake.clients = 0 + snowflake.ptype = ptype snowflake.offerChannel = make(chan []byte) snowflake.answerChannel = make(chan []byte) heap.Push(ctx.snowflakes, snowflake) @@ -159,7 +162,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { return } - sid, err := messages.DecodePollRequest(body) + sid, ptype, err := messages.DecodePollRequest(body) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -174,7 +177,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { } // Wait for a client to avail an offer to the snowflake, or timeout if nil. - offer := ctx.RequestOffer(sid) + offer := ctx.RequestOffer(sid, ptype) var b []byte if nil == offer { ctx.metrics.proxyIdleCount++ @@ -286,16 +289,23 @@ func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { func debugHandler(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { s := fmt.Sprintf("current snowflakes available: %d\n", ctx.snowflakes.Len()) - var browsers, standalones int + var webexts, browsers, standalones, unknowns int for _, snowflake := range ctx.idToSnowflake { - if len(snowflake.id) < 16 { + if snowflake.ptype == "badge" { browsers++ - } else { + } else if snowflake.ptype == "webext" { + webexts++ + } else if snowflake.ptype == "standalone" { standalones++ + } else { + unknowns++ } + } s += fmt.Sprintf("\tstandalone proxies: %d", standalones) s += fmt.Sprintf("\n\tbrowser proxies: %d", browsers) + s += fmt.Sprintf("\n\twebext proxies: %d", webexts) + s += fmt.Sprintf("\n\tunknown proxies: %d", unknowns) if _, err := w.Write([]byte(s)); err != nil { log.Printf("writing proxy information returned error: %v ", err) } diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index c35c1d6..cb5f34f 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -29,7 +29,7 @@ func TestBroker(t *testing.T) { Convey("Adds Snowflake", func() { So(ctx.snowflakes.Len(), ShouldEqual, 0) So(len(ctx.idToSnowflake), ShouldEqual, 0) - ctx.AddSnowflake("foo") + ctx.AddSnowflake("foo", "") So(ctx.snowflakes.Len(), ShouldEqual, 1) So(len(ctx.idToSnowflake), ShouldEqual, 1) }) @@ -55,7 +55,7 @@ func TestBroker(t *testing.T) { Convey("Request an offer from the Snowflake Heap", func() { done := make(chan []byte) go func() { - offer := ctx.RequestOffer("test") + offer := ctx.RequestOffer("test", "") done <- offer }() request := <-ctx.proxyPolls @@ -79,7 +79,7 @@ func TestBroker(t *testing.T) { Convey("with a proxy answer if available.", func() { done := make(chan bool) // Prepare a fake proxy to respond with. - snowflake := ctx.AddSnowflake("fake") + snowflake := ctx.AddSnowflake("fake", "") go func() { clientOffers(ctx, w, r) done <- true @@ -97,7 +97,7 @@ func TestBroker(t *testing.T) { return } done := make(chan bool) - snowflake := ctx.AddSnowflake("fake") + snowflake := ctx.AddSnowflake("fake", "") go func() { clientOffers(ctx, w, r) // Takes a few seconds here... @@ -147,7 +147,7 @@ func TestBroker(t *testing.T) { }) Convey("Responds to proxy answers...", func() { - s := ctx.AddSnowflake("test") + s := ctx.AddSnowflake("test", "") w := httptest.NewRecorder() data := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"test","Answer":"test"}`)) @@ -211,7 +211,7 @@ func TestBroker(t *testing.T) { // Manually do the Broker goroutine action here for full control. p := <-ctx.proxyPolls So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") - s := ctx.AddSnowflake(p.id) + s := ctx.AddSnowflake(p.id, "") go func() { offer := <-s.offerChannel p.offerChannel <- offer @@ -449,7 +449,7 @@ func TestMetrics(t *testing.T) { So(err, ShouldBeNil) // Prepare a fake proxy to respond with. - snowflake := ctx.AddSnowflake("fake") + snowflake := ctx.AddSnowflake("fake", "") go func() { clientOffers(ctx, w, r) done <- true diff --git a/broker/snowflake-heap.go b/broker/snowflake-heap.go index 419956f..cf209ec 100644 --- a/broker/snowflake-heap.go +++ b/broker/snowflake-heap.go @@ -10,6 +10,7 @@ over the offer and answer channels. */ type Snowflake struct { id string + ptype string offerChannel chan []byte answerChannel chan []byte clients int diff --git a/common/messages/proxy.go b/common/messages/proxy.go index 042caf9..7ebab1d 100644 --- a/common/messages/proxy.go +++ b/common/messages/proxy.go @@ -6,16 +6,18 @@ package messages import ( "encoding/json" "fmt" + "strings" ) -const version = "1.0" +const version = "1.1" -/* Version 1.0 specification: +/* Version 1.1 specification: == ProxyPollRequest == { - Sid: [generated session id of proxy] - Version: 1.0 + Sid: [generated session id of proxy], + Version: 1.1, + Type: [badge|webext|standalone] } == ProxyPollResponse == @@ -41,11 +43,11 @@ HTTP 400 BadRequest == ProxyAnswerRequest == { - Sid: [generated session id of proxy] - Version: 1.0 + Sid: [generated session id of proxy], + Version: 1.1, Answer: { - type: answer + type: answer, sdp: [WebRTC SDP] } } @@ -73,34 +75,38 @@ HTTP 400 BadRequest type ProxyPollRequest struct { Sid string Version string + Type string } -func EncodePollRequest(sid string) ([]byte, error) { +func EncodePollRequest(sid string, ptype string) ([]byte, error) { return json.Marshal(ProxyPollRequest{ Sid: sid, Version: version, + Type: ptype, }) } // Decodes a poll message from a snowflake proxy and returns the // sid of the proxy on success and an error if it failed -func DecodePollRequest(data []byte) (string, error) { +func DecodePollRequest(data []byte) (string, string, error) { var message ProxyPollRequest err := json.Unmarshal(data, &message) if err != nil { - return "", err - } - if message.Version != "1.0" { - return "", fmt.Errorf("using unknown version") + return "", "", err } - // Version 1.0 requires an Sid + majorVersion := strings.Split(message.Version, ".")[0] + if majorVersion != "1" { + return "", "", fmt.Errorf("using unknown version") + } + + // Version 1.x requires an Sid if message.Sid == "" { - return "", fmt.Errorf("no supplied session id") + return "", "", fmt.Errorf("no supplied session id") } - return message.Sid, nil + return message.Sid, message.Type, nil } type ProxyPollResponse struct { @@ -153,7 +159,7 @@ type ProxyAnswerRequest struct { func EncodeAnswerRequest(answer string, sid string) ([]byte, error) { return json.Marshal(ProxyAnswerRequest{ - Version: "1.0", + Version: "1.1", Sid: sid, Answer: answer, }) @@ -167,7 +173,9 @@ func DecodeAnswerRequest(data []byte) (string, string, error) { if err != nil { return "", "", err } - if message.Version != "1.0" { + + majorVersion := strings.Split(message.Version, ".")[0] + if majorVersion != "1" { return "", "", fmt.Errorf("using unknown version") } diff --git a/common/messages/proxy_test.go b/common/messages/proxy_test.go index f2f006e..83553a0 100644 --- a/common/messages/proxy_test.go +++ b/common/messages/proxy_test.go @@ -11,45 +11,60 @@ import ( func TestDecodeProxyPollRequest(t *testing.T) { Convey("Context", t, func() { for _, test := range []struct { - sid string - data string - err error + sid string + ptype string + data string + err error }{ { //Version 1.0 proxy message "ymbcCMto7KHNGYlp", + "", `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`, nil, }, + { + //Version 1.1 proxy message + "ymbcCMto7KHNGYlp", + "standalone", + `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.1","Type":"standalone"}`, + nil, + }, { //Version 0.X proxy message: "", + "", "ymbcCMto7KHNGYlp", &json.SyntaxError{}, }, { + "", "", `{"Sid":"ymbcCMto7KHNGYlp"}`, fmt.Errorf(""), }, { + "", "", "{}", fmt.Errorf(""), }, { + "", "", `{"Version":"1.0"}`, fmt.Errorf(""), }, { + "", "", `{"Version":"2.0"}`, fmt.Errorf(""), }, } { - sid, err := DecodePollRequest([]byte(test.data)) + sid, ptype, err := DecodePollRequest([]byte(test.data)) So(sid, ShouldResemble, test.sid) + So(ptype, ShouldResemble, test.ptype) So(err, ShouldHaveSameTypeAs, test.err) } @@ -58,10 +73,11 @@ func TestDecodeProxyPollRequest(t *testing.T) { func TestEncodeProxyPollRequests(t *testing.T) { Convey("Context", t, func() { - b, err := EncodePollRequest("ymbcCMto7KHNGYlp") + b, err := EncodePollRequest("ymbcCMto7KHNGYlp", "standalone") So(err, ShouldEqual, nil) - sid, err := DecodePollRequest(b) + sid, ptype, err := DecodePollRequest(b) So(sid, ShouldEqual, "ymbcCMto7KHNGYlp") + So(ptype, ShouldEqual, "standalone") So(err, ShouldEqual, nil) }) } diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index c10093a..dce7b70 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -175,7 +175,7 @@ func (b *Broker) pollOffer(sid string) *webrtc.SessionDescription { timeOfNextPoll = now } - body, err := messages.EncodePollRequest(sid) + body, err := messages.EncodePollRequest(sid, "standalone") if err != nil { log.Printf("Error encoding poll message: %s", err.Error()) return nil From 8ab81fc6cdbd34083a567429b51375ca5512fd50 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 20 Nov 2019 13:09:11 -0500 Subject: [PATCH 022/385] Update proxy config to take proxy type This allows badge and standalone proxies to tell the broker what proxy type they are. --- proxy/broker.js | 7 ++++--- proxy/config.js | 2 ++ proxy/init-badge.js | 3 ++- proxy/init-node.js | 2 +- proxy/init-testing.js | 2 +- proxy/init-webext.js | 3 ++- proxy/spec/broker.spec.js | 24 ++++++++++++++++++------ 7 files changed, 30 insertions(+), 13 deletions(-) diff --git a/proxy/broker.js b/proxy/broker.js index 551110b..42293ae 100644 --- a/proxy/broker.js +++ b/proxy/broker.js @@ -14,11 +14,12 @@ class Broker { // ID so the Broker can keep track of each proxy's signalling channels. // On construction, this Broker object does not do anything until // |getClientOffer| is called. - constructor(url) { + constructor(config) { this.getClientOffer = this.getClientOffer.bind(this); this._postRequest = this._postRequest.bind(this); - this.url = url; + this.config = config + this.url = config.brokerUrl; this.clients = 0; if (0 === this.url.indexOf('localhost', 0)) { // Ensure url has the right protocol + trailing slash. @@ -63,7 +64,7 @@ class Broker { } }; this._xhr = xhr; // Used by spec to fake async Broker interaction - var data = {"Version": "1.0", "Sid": id} + var data = {"Version": "1.1", "Sid": id, "Type": this.config.proxyType} return this._postRequest(xhr, 'proxy', JSON.stringify(data)); }); } diff --git a/proxy/config.js b/proxy/config.js index 9564f82..2b698a6 100644 --- a/proxy/config.js +++ b/proxy/config.js @@ -24,6 +24,8 @@ Config.prototype.defaultBrokerPollInterval = 300.0 * 1000; Config.prototype.maxNumClients = 1; +Config.prototype.proxyType = ""; + // TODO: Different ICE servers. Config.prototype.pcConfig = { iceServers: [ diff --git a/proxy/init-badge.js b/proxy/init-badge.js index 2cc5b07..2e0a261 100644 --- a/proxy/init-badge.js +++ b/proxy/init-badge.js @@ -170,10 +170,11 @@ var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotific } config = new Config; + config.proxyType = "badge"; if ('off' !== query.get('ratelimit')) { config.rateLimitBytes = Params.getByteCount(query, 'ratelimit', config.rateLimitBytes); } - broker = new Broker(config.brokerUrl); + broker = new Broker(config); snowflake = new Snowflake(config, ui, broker); log('== snowflake proxy =='); update(); diff --git a/proxy/init-node.js b/proxy/init-node.js index 789e6e3..73c25dc 100644 --- a/proxy/init-node.js +++ b/proxy/init-node.js @@ -8,7 +8,7 @@ var config = new Config; var ui = new UI(); -var broker = new Broker(config.brokerUrl); +var broker = new Broker(config); var snowflake = new Snowflake(config, ui, broker); diff --git a/proxy/init-testing.js b/proxy/init-testing.js index 90026a9..f553f12 100644 --- a/proxy/init-testing.js +++ b/proxy/init-testing.js @@ -89,7 +89,7 @@ var snowflake, query, debug, ui, silenceNotifications, log, dbg, init; } else { ui = new UI(); } - broker = new Broker(config.brokerUrl); + broker = new Broker(config); snowflake = new Snowflake(config, ui, broker); log('== snowflake proxy =='); if (Util.snowflakeIsDisabled(config.cookieName)) { diff --git a/proxy/init-webext.js b/proxy/init-webext.js index ad345fa..afa9aee 100644 --- a/proxy/init-webext.js +++ b/proxy/init-webext.js @@ -172,8 +172,9 @@ var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotific init = function() { config = new Config; + config.proxyType = "webext"; ui = new WebExtUI(); - broker = new Broker(config.brokerUrl); + broker = new Broker(config); snowflake = new Snowflake(config, ui, broker); log('== snowflake proxy =='); ui.initToggle(); diff --git a/proxy/spec/broker.spec.js b/proxy/spec/broker.spec.js index 6ab9691..28a66c4 100644 --- a/proxy/spec/broker.spec.js +++ b/proxy/spec/broker.spec.js @@ -22,7 +22,9 @@ describe('Broker', function() { it('can be created', function() { var b; - b = new Broker('fake'); + var config = new Config; + config.brokerUrl = 'fake'; + b = new Broker(config); expect(b.url).toEqual('https://fake/'); expect(b.id).not.toBeNull(); }); @@ -31,7 +33,9 @@ describe('Broker', function() { it('polls and promises a client offer', function(done) { var b, poll; - b = new Broker('fake'); + var config = new Config; + config.brokerUrl = 'fake'; + b = new Broker(config); // fake successful request and response from broker. spyOn(b, '_postRequest').and.callFake(function() { b._xhr.readyState = b._xhr.DONE; @@ -53,7 +57,9 @@ describe('Broker', function() { it('rejects if the broker timed-out', function(done) { var b, poll; - b = new Broker('fake'); + var config = new Config; + config.brokerUrl = 'fake'; + b = new Broker(config); // fake timed-out request from broker spyOn(b, '_postRequest').and.callFake(function() { b._xhr.readyState = b._xhr.DONE; @@ -75,7 +81,9 @@ describe('Broker', function() { it('rejects on any other status', function(done) { var b, poll; - b = new Broker('fake'); + var config = new Config; + config.brokerUrl = 'fake'; + b = new Broker(config); // fake timed-out request from broker spyOn(b, '_postRequest').and.callFake(function() { b._xhr.readyState = b._xhr.DONE; @@ -99,14 +107,18 @@ describe('Broker', function() { }); it('responds to the broker with answer', function() { - var b = new Broker('fake'); + var config = new Config; + config.brokerUrl = 'fake'; + var b = new Broker(config); spyOn(b, '_postRequest'); b.sendAnswer('fake id', 123); expect(b._postRequest).toHaveBeenCalledWith(jasmine.any(Object), 'answer', '{"Version":"1.0","Sid":"fake id","Answer":"123"}'); }); it('POST XMLHttpRequests to the broker', function() { - var b = new Broker('fake'); + var config = new Config; + config.brokerUrl = 'fake'; + var b = new Broker(config); b._xhr = new XMLHttpRequest(); spyOn(b._xhr, 'open'); spyOn(b._xhr, 'setRequestHeader'); From 981abffbd93673c483a0d41631e472303b3fd0aa Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 20 Nov 2019 13:27:04 -0500 Subject: [PATCH 023/385] Add proxy type to stats exported by broker --- broker/broker.go | 2 +- broker/metrics.go | 73 ++++++++++++++++++++++++++++----- broker/snowflake-broker_test.go | 58 +++++++++++++++++++++----- 3 files changed, 111 insertions(+), 22 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index 3edfe84..e897ffd 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -173,7 +173,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { if err != nil { log.Println("Error processing proxy IP: ", err.Error()) } else { - ctx.metrics.UpdateCountryStats(remoteIP) + ctx.metrics.UpdateCountryStats(remoteIP, ptype) } // Wait for a client to avail an offer to the snowflake, or timeout if nil. diff --git a/broker/metrics.go b/broker/metrics.go index 15a4a88..b5c423c 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -19,6 +19,24 @@ We export metrics in the following format: A count of the total number of unique IP addresses of snowflake proxies that have polled. + "snowflake-ips-standalone" NUM NL + [At most once.] + + A count of the total number of unique IP addresses of snowflake + proxies of type "standalone" that have polled. + + "snowflake-ips-badge" NUM NL + [At most once.] + + A count of the total number of unique IP addresses of snowflake + proxies of type "badge" that have polled. + + "snowflake-ips-webext" NUM NL + [At most once.] + + A count of the total number of unique IP addresses of snowflake + proxies of type "webext" that have polled. + "snowflake-idle-count" NUM NL [At most once.] @@ -58,8 +76,11 @@ var ( const metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds type CountryStats struct { - addrs map[string]bool - counts map[string]int + standalone map[string]bool + badge map[string]bool + webext map[string]bool + unknown map[string]bool + counts map[string]int } // Implements Observable @@ -89,13 +110,27 @@ func (s CountryStats) Display() string { return output } -func (m *Metrics) UpdateCountryStats(addr string) { +func (m *Metrics) UpdateCountryStats(addr string, ptype string) { var country string var ok bool - if m.countryStats.addrs[addr] { - return + if ptype == "standalone" { + if m.countryStats.standalone[addr] { + return + } + } else if ptype == "badge" { + if m.countryStats.badge[addr] { + return + } + } else if ptype == "webext" { + if m.countryStats.webext[addr] { + return + } + } else { + if m.countryStats.unknown[addr] { + return + } } ip := net.ParseIP(addr) @@ -118,7 +153,15 @@ func (m *Metrics) UpdateCountryStats(addr string) { //update map of unique ips and counts m.countryStats.counts[country]++ - m.countryStats.addrs[addr] = true + if ptype == "standalone" { + m.countryStats.standalone[addr] = true + } else if ptype == "badge" { + m.countryStats.badge[addr] = true + } else if ptype == "webext" { + m.countryStats.webext[addr] = true + } else { + m.countryStats.unknown[addr] = true + } } @@ -148,8 +191,11 @@ func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) { m := new(Metrics) m.countryStats = CountryStats{ - counts: make(map[string]int), - addrs: make(map[string]bool), + counts: make(map[string]int), + standalone: make(map[string]bool), + badge: make(map[string]bool), + webext: make(map[string]bool), + unknown: make(map[string]bool), } m.logger = metricsLogger @@ -172,7 +218,11 @@ func (m *Metrics) logMetrics() { func (m *Metrics) printMetrics() { m.logger.Println("snowflake-stats-end", time.Now().UTC().Format("2006-01-02 15:04:05"), fmt.Sprintf("(%d s)", int(metricsResolution.Seconds()))) m.logger.Println("snowflake-ips", m.countryStats.Display()) - m.logger.Println("snowflake-ips-total", len(m.countryStats.addrs)) + m.logger.Println("snowflake-ips-total", len(m.countryStats.standalone)+ + len(m.countryStats.badge)+len(m.countryStats.webext)+len(m.countryStats.unknown)) + m.logger.Println("snowflake-ips-standalone", len(m.countryStats.standalone)) + m.logger.Println("snowflake-ips-badge", len(m.countryStats.badge)) + m.logger.Println("snowflake-ips-webext", len(m.countryStats.webext)) m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount)) m.logger.Println("client-denied-count", binCount(m.clientDeniedCount)) m.logger.Println("client-snowflake-match-count", binCount(m.clientProxyMatchCount)) @@ -184,7 +234,10 @@ func (m *Metrics) zeroMetrics() { m.clientDeniedCount = 0 m.clientProxyMatchCount = 0 m.countryStats.counts = make(map[string]int) - m.countryStats.addrs = make(map[string]bool) + m.countryStats.standalone = make(map[string]bool) + m.countryStats.badge = make(map[string]bool) + m.countryStats.webext = make(map[string]bool) + m.countryStats.unknown = make(map[string]bool) } // Rounds up a count to the nearest multiple of 8. diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index cb5f34f..b23e688 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -387,7 +387,7 @@ func TestGeoip(t *testing.T) { if err := ctx.metrics.LoadGeoipDatabases("invalid_filename", "invalid_filename6"); err != nil { log.Printf("loading geo ip databases returned error: %v", err) } - ctx.metrics.UpdateCountryStats("127.0.0.1") + ctx.metrics.UpdateCountryStats("127.0.0.1", "") So(ctx.metrics.tablev4, ShouldEqual, nil) }) @@ -408,7 +408,6 @@ func TestMetrics(t *testing.T) { w := httptest.NewRecorder() data := bytes.NewReader([]byte("{\"Sid\":\"ymbcCMto7KHNGYlp\",\"Version\":\"1.0\"}")) r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) - r.Header.Set("X-Session-ID", "test") r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) go func(ctx *BrokerContext) { @@ -419,8 +418,47 @@ func TestMetrics(t *testing.T) { p.offerChannel <- nil <-done + w = httptest.NewRecorder() + data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","Type":"standalone"}`)) + r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) + r.RemoteAddr = "129.97.208.23:8888" //CA geoip + So(err, ShouldBeNil) + go func(ctx *BrokerContext) { + proxyPolls(ctx, w, r) + done <- true + }(ctx) + p = <-ctx.proxyPolls //manually unblock poll + p.offerChannel <- nil + <-done + + w = httptest.NewRecorder() + data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","Type":"badge"}`)) + r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) + r.RemoteAddr = "129.97.208.23:8888" //CA geoip + So(err, ShouldBeNil) + go func(ctx *BrokerContext) { + proxyPolls(ctx, w, r) + done <- true + }(ctx) + p = <-ctx.proxyPolls //manually unblock poll + p.offerChannel <- nil + <-done + + w = httptest.NewRecorder() + data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0","Type":"webext"}`)) + r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) + r.RemoteAddr = "129.97.208.23:8888" //CA geoip + So(err, ShouldBeNil) + go func(ctx *BrokerContext) { + proxyPolls(ctx, w, r) + done <- true + }(ctx) + p = <-ctx.proxyPolls //manually unblock poll + p.offerChannel <- nil + <-done ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips CA=1\nsnowflake-ips-total 1\nsnowflake-idle-count 8\nclient-denied-count 0\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips CA=4\nsnowflake-ips-total 4\nsnowflake-ips-standalone 1\nsnowflake-ips-badge 1\nsnowflake-ips-webext 1\nsnowflake-idle-count 8\nclient-denied-count 0\nclient-snowflake-match-count 0\n") + }) //Test addition of client failures @@ -433,13 +471,13 @@ func TestMetrics(t *testing.T) { clientOffers(ctx, w, r) ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-idle-count 0\nclient-denied-count 8\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 8\nclient-snowflake-match-count 0\n") // Test reset buf.Reset() ctx.metrics.zeroMetrics() ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-idle-count 0\nclient-denied-count 0\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 0\nclient-snowflake-match-count 0\n") }) //Test addition of client matches Convey("for client-proxy match", func() { @@ -460,7 +498,7 @@ func TestMetrics(t *testing.T) { <-done ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-idle-count 0\nclient-denied-count 0\nclient-snowflake-match-count 8\n") + So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 0\nclient-snowflake-match-count 8\n") }) //Test rounding boundary Convey("binning boundary", func() { @@ -479,12 +517,12 @@ func TestMetrics(t *testing.T) { clientOffers(ctx, w, r) ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-idle-count 0\nclient-denied-count 8\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 8\nclient-snowflake-match-count 0\n") clientOffers(ctx, w, r) buf.Reset() ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-idle-count 0\nclient-denied-count 16\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 16\nclient-snowflake-match-count 0\n") }) //Test unique ip @@ -492,7 +530,6 @@ func TestMetrics(t *testing.T) { w := httptest.NewRecorder() data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) - r.Header.Set("X-Session-ID", "test") r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) go func(ctx *BrokerContext) { @@ -508,7 +545,6 @@ func TestMetrics(t *testing.T) { if err != nil { log.Printf("unable to get NewRequest with error: %v", err) } - r.Header.Set("X-Session-ID", "test") r.RemoteAddr = "129.97.208.23:8888" //CA geoip go func(ctx *BrokerContext) { proxyPolls(ctx, w, r) @@ -519,7 +555,7 @@ func TestMetrics(t *testing.T) { <-done ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips CA=1\nsnowflake-ips-total 1\nsnowflake-idle-count 8\nclient-denied-count 0\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips CA=1\nsnowflake-ips-total 1\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 8\nclient-denied-count 0\nclient-snowflake-match-count 0\n") }) }) } From 97554e03e4081cecb7609721e43ad4d4ce408dd4 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 26 Nov 2019 10:36:27 -0500 Subject: [PATCH 024/385] Updated proxyType variable name for readability --- broker/broker.go | 24 ++++++++++++------------ broker/metrics.go | 14 +++++++------- broker/snowflake-heap.go | 2 +- common/messages/proxy.go | 4 ++-- common/messages/proxy_test.go | 16 ++++++++-------- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index e897ffd..c166f1a 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -97,16 +97,16 @@ func (mh MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Proxies may poll for client offers concurrently. type ProxyPoll struct { id string - ptype string + proxyType string offerChannel chan []byte } // Registers a Snowflake and waits for some Client to send an offer, // as part of the polling logic of the proxy handler. -func (ctx *BrokerContext) RequestOffer(id string, ptype string) []byte { +func (ctx *BrokerContext) RequestOffer(id string, proxyType string) []byte { request := new(ProxyPoll) request.id = id - request.ptype = ptype + request.proxyType = proxyType request.offerChannel = make(chan []byte) ctx.proxyPolls <- request // Block until an offer is available, or timeout which sends a nil offer. @@ -119,7 +119,7 @@ func (ctx *BrokerContext) RequestOffer(id string, ptype string) []byte { // client offer or nil on timeout / none are available. func (ctx *BrokerContext) Broker() { for request := range ctx.proxyPolls { - snowflake := ctx.AddSnowflake(request.id, request.ptype) + snowflake := ctx.AddSnowflake(request.id, request.proxyType) // Wait for a client to avail an offer to the snowflake. go func(request *ProxyPoll) { select { @@ -139,11 +139,11 @@ func (ctx *BrokerContext) Broker() { // Create and add a Snowflake to the heap. // Required to keep track of proxies between providing them // with an offer and awaiting their second POST with an answer. -func (ctx *BrokerContext) AddSnowflake(id string, ptype string) *Snowflake { +func (ctx *BrokerContext) AddSnowflake(id string, proxyType string) *Snowflake { snowflake := new(Snowflake) snowflake.id = id snowflake.clients = 0 - snowflake.ptype = ptype + snowflake.proxyType = proxyType snowflake.offerChannel = make(chan []byte) snowflake.answerChannel = make(chan []byte) heap.Push(ctx.snowflakes, snowflake) @@ -162,7 +162,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { return } - sid, ptype, err := messages.DecodePollRequest(body) + sid, proxyType, err := messages.DecodePollRequest(body) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -173,11 +173,11 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { if err != nil { log.Println("Error processing proxy IP: ", err.Error()) } else { - ctx.metrics.UpdateCountryStats(remoteIP, ptype) + ctx.metrics.UpdateCountryStats(remoteIP, proxyType) } // Wait for a client to avail an offer to the snowflake, or timeout if nil. - offer := ctx.RequestOffer(sid, ptype) + offer := ctx.RequestOffer(sid, proxyType) var b []byte if nil == offer { ctx.metrics.proxyIdleCount++ @@ -291,11 +291,11 @@ func debugHandler(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { var webexts, browsers, standalones, unknowns int for _, snowflake := range ctx.idToSnowflake { - if snowflake.ptype == "badge" { + if snowflake.proxyType == "badge" { browsers++ - } else if snowflake.ptype == "webext" { + } else if snowflake.proxyType == "webext" { webexts++ - } else if snowflake.ptype == "standalone" { + } else if snowflake.proxyType == "standalone" { standalones++ } else { unknowns++ diff --git a/broker/metrics.go b/broker/metrics.go index b5c423c..c23a170 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -110,20 +110,20 @@ func (s CountryStats) Display() string { return output } -func (m *Metrics) UpdateCountryStats(addr string, ptype string) { +func (m *Metrics) UpdateCountryStats(addr string, proxyType string) { var country string var ok bool - if ptype == "standalone" { + if proxyType == "standalone" { if m.countryStats.standalone[addr] { return } - } else if ptype == "badge" { + } else if proxyType == "badge" { if m.countryStats.badge[addr] { return } - } else if ptype == "webext" { + } else if proxyType == "webext" { if m.countryStats.webext[addr] { return } @@ -153,11 +153,11 @@ func (m *Metrics) UpdateCountryStats(addr string, ptype string) { //update map of unique ips and counts m.countryStats.counts[country]++ - if ptype == "standalone" { + if proxyType == "standalone" { m.countryStats.standalone[addr] = true - } else if ptype == "badge" { + } else if proxyType == "badge" { m.countryStats.badge[addr] = true - } else if ptype == "webext" { + } else if proxyType == "webext" { m.countryStats.webext[addr] = true } else { m.countryStats.unknown[addr] = true diff --git a/broker/snowflake-heap.go b/broker/snowflake-heap.go index cf209ec..19a64b2 100644 --- a/broker/snowflake-heap.go +++ b/broker/snowflake-heap.go @@ -10,7 +10,7 @@ over the offer and answer channels. */ type Snowflake struct { id string - ptype string + proxyType string offerChannel chan []byte answerChannel chan []byte clients int diff --git a/common/messages/proxy.go b/common/messages/proxy.go index 7ebab1d..d57af1e 100644 --- a/common/messages/proxy.go +++ b/common/messages/proxy.go @@ -78,11 +78,11 @@ type ProxyPollRequest struct { Type string } -func EncodePollRequest(sid string, ptype string) ([]byte, error) { +func EncodePollRequest(sid string, proxyType string) ([]byte, error) { return json.Marshal(ProxyPollRequest{ Sid: sid, Version: version, - Type: ptype, + Type: proxyType, }) } diff --git a/common/messages/proxy_test.go b/common/messages/proxy_test.go index 83553a0..6783874 100644 --- a/common/messages/proxy_test.go +++ b/common/messages/proxy_test.go @@ -11,10 +11,10 @@ import ( func TestDecodeProxyPollRequest(t *testing.T) { Convey("Context", t, func() { for _, test := range []struct { - sid string - ptype string - data string - err error + sid string + proxyType string + data string + err error }{ { //Version 1.0 proxy message @@ -62,9 +62,9 @@ func TestDecodeProxyPollRequest(t *testing.T) { fmt.Errorf(""), }, } { - sid, ptype, err := DecodePollRequest([]byte(test.data)) + sid, proxyType, err := DecodePollRequest([]byte(test.data)) So(sid, ShouldResemble, test.sid) - So(ptype, ShouldResemble, test.ptype) + So(proxyType, ShouldResemble, test.proxyType) So(err, ShouldHaveSameTypeAs, test.err) } @@ -75,9 +75,9 @@ func TestEncodeProxyPollRequests(t *testing.T) { Convey("Context", t, func() { b, err := EncodePollRequest("ymbcCMto7KHNGYlp", "standalone") So(err, ShouldEqual, nil) - sid, ptype, err := DecodePollRequest(b) + sid, proxyType, err := DecodePollRequest(b) So(sid, ShouldEqual, "ymbcCMto7KHNGYlp") - So(ptype, ShouldEqual, "standalone") + So(proxyType, ShouldEqual, "standalone") So(err, ShouldEqual, nil) }) } From 94de69aa369ebdee0cee5b683a42ebc8811a796d Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 26 Nov 2019 10:44:40 -0500 Subject: [PATCH 025/385] Updated broker specification and comments --- broker/metrics.go | 59 ++-------------------------------------- common/messages/proxy.go | 4 +-- doc/broker-spec.txt | 18 ++++++++++++ 3 files changed, 22 insertions(+), 59 deletions(-) diff --git a/broker/metrics.go b/broker/metrics.go index c23a170..bf5ce29 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -1,66 +1,11 @@ /* -We export metrics in the following format: - - "snowflake-stats-end" YYYY-MM-DD HH:MM:SS (NSEC s) NL - [At most once.] - - YYYY-MM-DD HH:MM:SS defines the end of the included measurement - interval of length NSEC seconds (86400 seconds by default). - - "snowflake-ips" CC=NUM,CC=NUM,... NL - [At most once.] - - List of mappings from two-letter country codes to the number of - unique IP addresses of snowflake proxies that have polled. - - "snowflake-ips-total" NUM NL - [At most once.] - - A count of the total number of unique IP addresses of snowflake - proxies that have polled. - - "snowflake-ips-standalone" NUM NL - [At most once.] - - A count of the total number of unique IP addresses of snowflake - proxies of type "standalone" that have polled. - - "snowflake-ips-badge" NUM NL - [At most once.] - - A count of the total number of unique IP addresses of snowflake - proxies of type "badge" that have polled. - - "snowflake-ips-webext" NUM NL - [At most once.] - - A count of the total number of unique IP addresses of snowflake - proxies of type "webext" that have polled. - - "snowflake-idle-count" NUM NL - [At most once.] - - A count of the number of times a proxy has polled but received - no client offer, rounded up to the nearest multiple of 8. - - "client-denied-count" NUM NL - [At most once.] - - A count of the number of times a client has requested a proxy - from the broker but no proxies were available, rounded up to - the nearest multiple of 8. - - "client-snowflake-match-count" NUM NL - [At most once.] - - A count of the number of times a client successfully received a - proxy from the broker, rounded up to the nearest multiple of 8. +We export metrics in the format specified in our broker spec: +https://gitweb.torproject.org/pluggable-transports/snowflake.git/tree/doc/broker-spec.txt */ package main import ( - // "golang.org/x/net/internal/timeseries" "fmt" "log" "math" diff --git a/common/messages/proxy.go b/common/messages/proxy.go index d57af1e..89dd43c 100644 --- a/common/messages/proxy.go +++ b/common/messages/proxy.go @@ -17,7 +17,7 @@ const version = "1.1" { Sid: [generated session id of proxy], Version: 1.1, - Type: [badge|webext|standalone] + Type: ["badge"|"webext"|"standalone"] } == ProxyPollResponse == @@ -87,7 +87,7 @@ func EncodePollRequest(sid string, proxyType string) ([]byte, error) { } // Decodes a poll message from a snowflake proxy and returns the -// sid of the proxy on success and an error if it failed +// sid and proxy type of the proxy on success and an error if it failed func DecodePollRequest(data []byte) (string, string, error) { var message ProxyPollRequest diff --git a/doc/broker-spec.txt b/doc/broker-spec.txt index 2877784..eba3347 100644 --- a/doc/broker-spec.txt +++ b/doc/broker-spec.txt @@ -31,6 +31,24 @@ Metrics data from the Snowflake broker can be retrieved by sending an HTTP GET r A count of the total number of unique IP addresses of Snowflake proxies that have polled. + "snowflake-ips-standalone" NUM NL + [At most once.] + + A count of the total number of unique IP addresses of snowflake + proxies of type "standalone" that have polled. + + "snowflake-ips-badge" NUM NL + [At most once.] + + A count of the total number of unique IP addresses of snowflake + proxies of type "badge" that have polled. + + "snowflake-ips-webext" NUM NL + [At most once.] + + A count of the total number of unique IP addresses of snowflake + proxies of type "webext" that have polled. + "snowflake-idle-count" NUM NL [At most once.] From 07f2cd8073168f26044aba1f9971469355d30e53 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 3 Dec 2019 14:09:05 -0500 Subject: [PATCH 026/385] bump version to 0.2.0 --- proxy/translation | 2 +- proxy/webext/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/translation b/proxy/translation index c145dfe..488054e 160000 --- a/proxy/translation +++ b/proxy/translation @@ -1 +1 @@ -Subproject commit c145dfe5b308085a700264e96509799355a52fa6 +Subproject commit 488054eda4c4d9c16fc4bddf439136178d0c769e diff --git a/proxy/webext/manifest.json b/proxy/webext/manifest.json index 90d3922..2894db6 100644 --- a/proxy/webext/manifest.json +++ b/proxy/webext/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Snowflake", - "version": "0.1.0", + "version": "0.2.0", "description": "__MSG_appDesc__", "default_locale": "en_US", "background": { From dccc15a6e9d620298f77fb7ae14692723b434306 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 22 Nov 2019 17:15:06 -0500 Subject: [PATCH 027/385] Add synchronization to prevent race in broker There's a race condition in the broker where both the proxy and the client processes try to pop/remove the same snowflake from the heap. This patch adds synchronization to prevent simultaneous accesses to snowflakes. --- broker/broker.go | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index c166f1a..a5b0edf 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -18,6 +18,7 @@ import ( "os" "os/signal" "strings" + "sync" "syscall" "time" @@ -37,6 +38,8 @@ type BrokerContext struct { // Map keeping track of snowflakeIDs required to match SDP answers from // the second http POST. idToSnowflake map[string]*Snowflake + // Synchronization for the + snowflakeLock sync.Mutex proxyPolls chan *ProxyPoll metrics *Metrics } @@ -127,10 +130,13 @@ func (ctx *BrokerContext) Broker() { request.offerChannel <- offer case <-time.After(time.Second * ProxyTimeout): // This snowflake is no longer available to serve clients. - // TODO: Fix race using a delete channel - heap.Remove(ctx.snowflakes, snowflake.index) - delete(ctx.idToSnowflake, snowflake.id) - request.offerChannel <- nil + ctx.snowflakeLock.Lock() + defer ctx.snowflakeLock.Unlock() + if snowflake.index != -1 { + heap.Remove(ctx.snowflakes, snowflake.index) + delete(ctx.idToSnowflake, snowflake.id) + close(request.offerChannel) + } } }(request) } @@ -146,7 +152,9 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string) *Snowflake { snowflake.proxyType = proxyType snowflake.offerChannel = make(chan []byte) snowflake.answerChannel = make(chan []byte) + ctx.snowflakeLock.Lock() heap.Push(ctx.snowflakes, snowflake) + ctx.snowflakeLock.Unlock() ctx.idToSnowflake[id] = snowflake return snowflake } @@ -215,15 +223,19 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { return } // Immediately fail if there are no snowflakes available. - if ctx.snowflakes.Len() <= 0 { + ctx.snowflakeLock.Lock() + numSnowflakes := ctx.snowflakes.Len() + ctx.snowflakeLock.Unlock() + if numSnowflakes <= 0 { ctx.metrics.clientDeniedCount++ w.WriteHeader(http.StatusServiceUnavailable) return } // Otherwise, find the most available snowflake proxy, and pass the offer to it. // Delete must be deferred in order to correctly process answer request later. + ctx.snowflakeLock.Lock() snowflake := heap.Pop(ctx.snowflakes).(*Snowflake) - defer delete(ctx.idToSnowflake, snowflake.id) + ctx.snowflakeLock.Unlock() snowflake.offerChannel <- offer // Wait for the answer to be returned on the channel or timeout. @@ -243,6 +255,10 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { log.Printf("unable to write timeout error, failed with error: %v", err) } } + + ctx.snowflakeLock.Lock() + delete(ctx.idToSnowflake, snowflake.id) + ctx.snowflakeLock.Unlock() } /* @@ -266,7 +282,9 @@ func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { } var success = true + ctx.snowflakeLock.Lock() snowflake, ok := ctx.idToSnowflake[id] + ctx.snowflakeLock.Unlock() if !ok || nil == snowflake { // The snowflake took too long to respond with an answer, so its client // disappeared / the snowflake is no longer recognized by the Broker. @@ -287,9 +305,10 @@ func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { } func debugHandler(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { - s := fmt.Sprintf("current snowflakes available: %d\n", ctx.snowflakes.Len()) var webexts, browsers, standalones, unknowns int + ctx.snowflakeLock.Lock() + s := fmt.Sprintf("current snowflakes available: %d\n", len(ctx.idToSnowflake)) for _, snowflake := range ctx.idToSnowflake { if snowflake.proxyType == "badge" { browsers++ @@ -302,6 +321,7 @@ func debugHandler(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { } } + ctx.snowflakeLock.Unlock() s += fmt.Sprintf("\tstandalone proxies: %d", standalones) s += fmt.Sprintf("\n\tbrowser proxies: %d", browsers) s += fmt.Sprintf("\n\twebext proxies: %d", webexts) From 42e16021c49b59433450c8f5b5a54e449f9dc522 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 25 Nov 2019 14:00:54 -0500 Subject: [PATCH 028/385] Add tests to check for data race in broker We had some data races in the broker that occur when proxies and clients modify the heap/snowflake map at the same time. This test has a client and proxy access the broker simultaneously to check for data races. --- broker/snowflake-broker_test.go | 136 ++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 43 deletions(-) diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index b23e688..18b83dd 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -191,58 +191,108 @@ func TestBroker(t *testing.T) { }) }) + }) Convey("End-To-End", t, func() { - done := make(chan bool) - polled := make(chan bool) ctx := NewBrokerContext(NullLogger()) - // Proxy polls with its ID first... - dataP := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) - wP := httptest.NewRecorder() - rP, err := http.NewRequest("POST", "snowflake.broker/proxy", dataP) - So(err, ShouldBeNil) - go func() { - proxyPolls(ctx, wP, rP) - polled <- true - }() + Convey("Check for client/proxy data race", func() { + proxy_done := make(chan bool) + client_done := make(chan bool) - // Manually do the Broker goroutine action here for full control. - p := <-ctx.proxyPolls - So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") - s := ctx.AddSnowflake(p.id, "") - go func() { - offer := <-s.offerChannel - p.offerChannel <- offer - }() - So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil) + go ctx.Broker() - // Client request blocks until proxy answer arrives. - dataC := bytes.NewReader([]byte("fake offer")) - wC := httptest.NewRecorder() - rC, err := http.NewRequest("POST", "snowflake.broker/client", dataC) - So(err, ShouldBeNil) - go func() { - clientOffers(ctx, wC, rC) - done <- true - }() + // Make proxy poll + wp := httptest.NewRecorder() + datap := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) + rp, err := http.NewRequest("POST", "snowflake.broker/proxy", datap) + So(err, ShouldBeNil) - <-polled - So(wP.Code, ShouldEqual, http.StatusOK) - So(wP.Body.String(), ShouldResemble, `{"Status":"client match","Offer":"fake offer"}`) - So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil) - // Follow up with the answer request afterwards - wA := httptest.NewRecorder() - dataA := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"ymbcCMto7KHNGYlp","Answer":"test"}`)) - rA, err := http.NewRequest("POST", "snowflake.broker/answer", dataA) - So(err, ShouldBeNil) - proxyAnswers(ctx, wA, rA) - So(wA.Code, ShouldEqual, http.StatusOK) + go func(ctx *BrokerContext) { + proxyPolls(ctx, wp, rp) + proxy_done <- true + }(ctx) - <-done - So(wC.Code, ShouldEqual, http.StatusOK) - So(wC.Body.String(), ShouldEqual, "test") + // Client offer + wc := httptest.NewRecorder() + datac := bytes.NewReader([]byte("test")) + rc, err := http.NewRequest("POST", "snowflake.broker/client", datac) + So(err, ShouldBeNil) + + go func() { + clientOffers(ctx, wc, rc) + client_done <- true + }() + + <-proxy_done + So(wp.Code, ShouldEqual, http.StatusOK) + + // Proxy answers + wp = httptest.NewRecorder() + datap = bytes.NewReader([]byte(`{"Version":"1.0","Sid":"ymbcCMto7KHNGYlp","Answer":"test"}`)) + rp, err = http.NewRequest("POST", "snowflake.broker/answer", datap) + So(err, ShouldBeNil) + go func(ctx *BrokerContext) { + proxyAnswers(ctx, wp, rp) + proxy_done <- true + }(ctx) + + <-proxy_done + <-client_done + + }) + + Convey("Ensure correct snowflake brokering", func() { + done := make(chan bool) + polled := make(chan bool) + + // Proxy polls with its ID first... + dataP := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`)) + wP := httptest.NewRecorder() + rP, err := http.NewRequest("POST", "snowflake.broker/proxy", dataP) + So(err, ShouldBeNil) + go func() { + proxyPolls(ctx, wP, rP) + polled <- true + }() + + // Manually do the Broker goroutine action here for full control. + p := <-ctx.proxyPolls + So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") + s := ctx.AddSnowflake(p.id, "") + go func() { + offer := <-s.offerChannel + p.offerChannel <- offer + }() + So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil) + + // Client request blocks until proxy answer arrives. + dataC := bytes.NewReader([]byte("fake offer")) + wC := httptest.NewRecorder() + rC, err := http.NewRequest("POST", "snowflake.broker/client", dataC) + So(err, ShouldBeNil) + go func() { + clientOffers(ctx, wC, rC) + done <- true + }() + + <-polled + So(wP.Code, ShouldEqual, http.StatusOK) + So(wP.Body.String(), ShouldResemble, `{"Status":"client match","Offer":"fake offer"}`) + So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil) + // Follow up with the answer request afterwards + wA := httptest.NewRecorder() + dataA := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"ymbcCMto7KHNGYlp","Answer":"test"}`)) + rA, err := http.NewRequest("POST", "snowflake.broker/answer", dataA) + So(err, ShouldBeNil) + proxyAnswers(ctx, wA, rA) + So(wA.Code, ShouldEqual, http.StatusOK) + + <-done + So(wC.Code, ShouldEqual, http.StatusOK) + So(wC.Body.String(), ShouldEqual, "test") + }) }) } From 06298eec730aa2664bb61d4cce4ef56dfce91ee3 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 2 Dec 2019 13:22:05 -0500 Subject: [PATCH 029/385] Added another lock to protect broker stats Added another lock to the metrics struct to synchronize accesses to the broker stats. There's a possible race condition if stats are updated at the same time they are being logged. --- broker/broker.go | 10 +++++++++- broker/metrics.go | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/broker/broker.go b/broker/broker.go index a5b0edf..17c677e 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -38,7 +38,7 @@ type BrokerContext struct { // Map keeping track of snowflakeIDs required to match SDP answers from // the second http POST. idToSnowflake map[string]*Snowflake - // Synchronization for the + // Synchronization for the snowflake map and heap snowflakeLock sync.Mutex proxyPolls chan *ProxyPoll metrics *Metrics @@ -181,14 +181,18 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { if err != nil { log.Println("Error processing proxy IP: ", err.Error()) } else { + ctx.metrics.lock.Lock() ctx.metrics.UpdateCountryStats(remoteIP, proxyType) + ctx.metrics.lock.Unlock() } // Wait for a client to avail an offer to the snowflake, or timeout if nil. offer := ctx.RequestOffer(sid, proxyType) var b []byte if nil == offer { + ctx.metrics.lock.Lock() ctx.metrics.proxyIdleCount++ + ctx.metrics.lock.Unlock() b, err = messages.EncodePollResponse("", false) if err != nil { @@ -227,7 +231,9 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { numSnowflakes := ctx.snowflakes.Len() ctx.snowflakeLock.Unlock() if numSnowflakes <= 0 { + ctx.metrics.lock.Lock() ctx.metrics.clientDeniedCount++ + ctx.metrics.lock.Unlock() w.WriteHeader(http.StatusServiceUnavailable) return } @@ -241,7 +247,9 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { // Wait for the answer to be returned on the channel or timeout. select { case answer := <-snowflake.answerChannel: + ctx.metrics.lock.Lock() ctx.metrics.clientProxyMatchCount++ + ctx.metrics.lock.Unlock() if _, err := w.Write(answer); err != nil { log.Printf("unable to write answer with error: %v", err) } diff --git a/broker/metrics.go b/broker/metrics.go index bf5ce29..ea4d220 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -39,6 +39,9 @@ type Metrics struct { proxyIdleCount uint clientDeniedCount uint clientProxyMatchCount uint + + //synchronization for access to snowflake metrics + lock sync.Mutex } func (s CountryStats) Display() string { @@ -161,6 +164,7 @@ func (m *Metrics) logMetrics() { } func (m *Metrics) printMetrics() { + m.lock.Lock() m.logger.Println("snowflake-stats-end", time.Now().UTC().Format("2006-01-02 15:04:05"), fmt.Sprintf("(%d s)", int(metricsResolution.Seconds()))) m.logger.Println("snowflake-ips", m.countryStats.Display()) m.logger.Println("snowflake-ips-total", len(m.countryStats.standalone)+ @@ -171,6 +175,7 @@ func (m *Metrics) printMetrics() { m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount)) m.logger.Println("client-denied-count", binCount(m.clientDeniedCount)) m.logger.Println("client-snowflake-match-count", binCount(m.clientProxyMatchCount)) + m.lock.Unlock() } // Restores all metrics to original values From dabdd847cefa0988af14584d98965c5af838325e Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 26 Nov 2019 11:45:34 -0500 Subject: [PATCH 030/385] Expanded snowflake server tests Now tests the proxy and initServer functionalities. The tests use the same websocket library as the server and proxy-go implementations. --- server/server_test.go | 174 +++++++++++++++++++++++++++++++++--------- 1 file changed, 138 insertions(+), 36 deletions(-) diff --git a/server/server_test.go b/server/server_test.go index 84ac7ba..7a72014 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2,48 +2,150 @@ package main import ( "net" + "net/http" "strconv" "testing" + + "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" + "github.com/gorilla/websocket" + . "github.com/smartystreets/goconvey/convey" ) func TestClientAddr(t *testing.T) { - // good tests - for _, test := range []struct { - input string - expected net.IP - }{ - {"1.2.3.4", net.ParseIP("1.2.3.4")}, - {"1:2::3:4", net.ParseIP("1:2::3:4")}, - } { - useraddr := clientAddr(test.input) - host, port, err := net.SplitHostPort(useraddr) - if err != nil { - t.Errorf("clientAddr(%q) → SplitHostPort error %v", test.input, err) - continue + Convey("Testing clientAddr", t, func() { + // good tests + for _, test := range []struct { + input string + expected net.IP + }{ + {"1.2.3.4", net.ParseIP("1.2.3.4")}, + {"1:2::3:4", net.ParseIP("1:2::3:4")}, + } { + useraddr := clientAddr(test.input) + host, port, err := net.SplitHostPort(useraddr) + if err != nil { + t.Errorf("clientAddr(%q) → SplitHostPort error %v", test.input, err) + continue + } + if !test.expected.Equal(net.ParseIP(host)) { + t.Errorf("clientAddr(%q) → host %q, not %v", test.input, host, test.expected) + } + portNo, err := strconv.Atoi(port) + if err != nil { + t.Errorf("clientAddr(%q) → port %q", test.input, port) + continue + } + if portNo == 0 { + t.Errorf("clientAddr(%q) → port %d", test.input, portNo) + } } - if !test.expected.Equal(net.ParseIP(host)) { - t.Errorf("clientAddr(%q) → host %q, not %v", test.input, host, test.expected) - } - portNo, err := strconv.Atoi(port) - if err != nil { - t.Errorf("clientAddr(%q) → port %q", test.input, port) - continue - } - if portNo == 0 { - t.Errorf("clientAddr(%q) → port %d", test.input, portNo) - } - } - // bad tests - for _, input := range []string{ - "", - "abc", - "1.2.3.4.5", - "[12::34]", - } { - useraddr := clientAddr(input) - if useraddr != "" { - t.Errorf("clientAddr(%q) → %q, not %q", input, useraddr, "") + // bad tests + for _, input := range []string{ + "", + "abc", + "1.2.3.4.5", + "[12::34]", + } { + useraddr := clientAddr(input) + if useraddr != "" { + t.Errorf("clientAddr(%q) → %q, not %q", input, useraddr, "") + } } - } + }) +} + +type StubHandler struct{} + +func (handler *StubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ws, _ := upgrader.Upgrade(w, r, nil) + + conn := websocketconn.NewWebSocketConn(ws) + defer conn.Close() + + //dial stub OR + or, _ := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP("localhost"), Port: 8889}) + + proxy(or, &conn) +} + +func Test(t *testing.T) { + Convey("Websocket server", t, func() { + //Set up the snowflake web server + ipStr, portStr, _ := net.SplitHostPort(":8888") + port, _ := strconv.ParseUint(portStr, 10, 16) + addr := &net.TCPAddr{IP: net.ParseIP(ipStr), Port: int(port)} + Convey("We don't listen on port 0", func() { + addr = &net.TCPAddr{IP: net.ParseIP(ipStr), Port: 0} + server, err := initServer(addr, nil, + func(server *http.Server, errChan chan<- error) { + return + }) + So(err, ShouldNotBeNil) + So(server, ShouldBeNil) + }) + + Convey("Plain HTTP server accepts connections", func(c C) { + server, err := startServer(addr) + So(err, ShouldBeNil) + + ws, _, err := websocket.DefaultDialer.Dial("ws://localhost:8888", nil) + wsConn := websocketconn.NewWebSocketConn(ws) + So(err, ShouldEqual, nil) + So(wsConn, ShouldNotEqual, nil) + + server.Close() + wsConn.Close() + + }) + Convey("Handler proxies data", func(c C) { + + laddr := &net.TCPAddr{IP: net.ParseIP("localhost"), Port: 8889} + + go func() { + + //stub OR + listener, err := net.ListenTCP("tcp", laddr) + c.So(err, ShouldBeNil) + conn, err := listener.Accept() + c.So(err, ShouldBeNil) + + b := make([]byte, 5) + n, err := conn.Read(b) + c.So(err, ShouldBeNil) + c.So(n, ShouldEqual, 5) + c.So(b, ShouldResemble, []byte("Hello")) + + n, err = conn.Write([]byte("world!")) + c.So(n, ShouldEqual, 6) + c.So(err, ShouldBeNil) + }() + + //overwite handler + server, err := initServer(addr, nil, + func(server *http.Server, errChan chan<- error) { + server.ListenAndServe() + }) + So(err, ShouldBeNil) + + var handler StubHandler + server.Handler = &handler + + ws, _, err := websocket.DefaultDialer.Dial("ws://localhost:8888", nil) + So(err, ShouldEqual, nil) + wsConn := websocketconn.NewWebSocketConn(ws) + So(wsConn, ShouldNotEqual, nil) + + wsConn.Write([]byte("Hello")) + b := make([]byte, 6) + n, err := wsConn.Read(b) + So(n, ShouldEqual, 6) + So(b, ShouldResemble, []byte("world!")) + + wsConn.Close() + server.Close() + + }) + + }) } From 0f99c5ab12edfaac2e95d5d403297cd26d6229bc Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 26 Nov 2019 15:04:48 -0500 Subject: [PATCH 031/385] Touched up snowflake client tests There were a few tests that needed refreshing since the introduction of the pion library. Also added a few tests for the ICE server parsing function in the client. --- client/client_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++ client/lib/lib_test.go | 38 +++++++++++++++++++++------ client/snowflake.go | 1 + 3 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 client/client_test.go diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000..aeaf979 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestICEServerParser(t *testing.T) { + Convey("Test parsing of ICE servers", t, func() { + for _, test := range []struct { + input string + urls [][]string + length int + }{ + { + "", + nil, + 0, + }, + { + " ", + nil, + 0, + }, + { + "stun:stun.l.google.com:19302", + [][]string{[]string{"stun:stun.l.google.com:19302"}}, + 1, + }, + { + "stun:stun.l.google.com:19302,stun.ekiga.net", + [][]string{[]string{"stun:stun.l.google.com:19302"}, []string{"stun.ekiga.net"}}, + 2, + }, + { + "stun:stun.l.google.com:19302, stun.ekiga.net", + [][]string{[]string{"stun:stun.l.google.com:19302"}, []string{"stun.ekiga.net"}}, + 2, + }, + } { + servers := parseIceServers(test.input) + + if test.urls == nil { + So(servers, ShouldBeNil) + } else { + So(servers, ShouldNotBeNil) + } + + So(len(servers), ShouldEqual, test.length) + + for i, server := range servers { + So(server.URLs, ShouldResemble, test.urls[i]) + } + + } + + }) +} diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 4e9e2c7..12368f3 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net" "net/http" + "sync" "testing" "github.com/pion/webrtc" @@ -72,6 +73,10 @@ func (f FakePeers) Collect() (Snowflake, error) { return &WebRTCPeer{}, nil } func (f FakePeers) Pop() Snowflake { return nil } func (f FakePeers) Melted() <-chan struct{} { return nil } +const sampleSDP = `"v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\na=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 8.8.8.8 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"` + +const sampleAnswer = `{"type":"answer","sdp":` + sampleSDP + `}` + func TestSnowflakeClient(t *testing.T) { Convey("Peers", t, func() { @@ -219,23 +224,40 @@ func TestSnowflakeClient(t *testing.T) { c.answerChannel = make(chan *webrtc.SessionDescription, 1) c.config = &webrtc.Configuration{} - c.preparePeerConnection() + c.pc, _ = webrtc.NewPeerConnection(*c.config) + offer, _ := c.pc.CreateOffer(nil) + err := c.pc.SetLocalDescription(offer) + So(err, ShouldBeNil) c.offerChannel <- nil - answer := deserializeSessionDescription( - `{"type":"answer","sdp":""}`) + answer := deserializeSessionDescription(sampleAnswer) + So(answer, ShouldNotBeNil) c.answerChannel <- answer - c.exchangeSDP() + err = c.exchangeSDP() + So(err, ShouldBeNil) }) - SkipConvey("Exchange SDP fails on nil answer", func() { - c.reset = make(chan struct{}) + Convey("Exchange SDP keeps trying on nil answer", func(ctx C) { + var wg sync.WaitGroup + wg.Add(1) + c.offerChannel = make(chan *webrtc.SessionDescription, 1) c.answerChannel = make(chan *webrtc.SessionDescription, 1) + c.config = &webrtc.Configuration{} + c.pc, _ = webrtc.NewPeerConnection(*c.config) + offer, _ := c.pc.CreateOffer(nil) + c.pc.SetLocalDescription(offer) + c.offerChannel <- nil c.answerChannel <- nil - c.exchangeSDP() - <-c.reset + go func() { + err := c.exchangeSDP() + ctx.So(err, ShouldBeNil) + wg.Done() + }() + answer := deserializeSessionDescription(sampleAnswer) + c.answerChannel <- answer + wg.Wait() }) }) diff --git a/client/snowflake.go b/client/snowflake.go index 959f83c..bb7de46 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -77,6 +77,7 @@ func parseIceServers(s string) []webrtc.ICEServer { urls := strings.Split(s, ",") log.Printf("Using ICE Servers:") for _, url := range urls { + url = strings.TrimSpace(url) log.Printf("url: %s", url) servers = append(servers, webrtc.ICEServer{ URLs: []string{url}, From 3bdcc3408ea2e5946dd27699e0f77e3f0f3816b2 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 27 Nov 2019 10:55:48 -0500 Subject: [PATCH 032/385] Increased test coverage for messages library --- common/messages/proxy_test.go | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/common/messages/proxy_test.go b/common/messages/proxy_test.go index 6783874..1570d4f 100644 --- a/common/messages/proxy_test.go +++ b/common/messages/proxy_test.go @@ -82,6 +82,57 @@ func TestEncodeProxyPollRequests(t *testing.T) { }) } +func TestDecodeProxyPollResponse(t *testing.T) { + Convey("Context", t, func() { + for _, test := range []struct { + offer string + data string + err error + }{ + { + "fake offer", + `{"Status":"client match","Offer":"fake offer"}`, + nil, + }, + { + "", + `{"Status":"no match"}`, + nil, + }, + { + "", + `{"Status":"client match"}`, + fmt.Errorf("no supplied offer"), + }, + { + "", + `{"Test":"test"}`, + fmt.Errorf(""), + }, + } { + offer, err := DecodePollResponse([]byte(test.data)) + So(offer, ShouldResemble, test.offer) + So(err, ShouldHaveSameTypeAs, test.err) + } + + }) +} + +func TestEncodeProxyPollResponse(t *testing.T) { + Convey("Context", t, func() { + b, err := EncodePollResponse("fake offer", true) + So(err, ShouldEqual, nil) + offer, err := DecodePollResponse(b) + So(offer, ShouldEqual, "fake offer") + So(err, ShouldEqual, nil) + + b, err = EncodePollResponse("", false) + So(err, ShouldEqual, nil) + offer, err = DecodePollResponse(b) + So(offer, ShouldEqual, "") + So(err, ShouldEqual, nil) + }) +} func TestDecodeProxyAnswerRequest(t *testing.T) { Convey("Context", t, func() { for _, test := range []struct { @@ -173,5 +224,11 @@ func TestEncodeProxyAnswerResponse(t *testing.T) { success, err := DecodeAnswerResponse(b) So(success, ShouldEqual, true) So(err, ShouldEqual, nil) + + b, err = EncodeAnswerResponse(false) + So(err, ShouldEqual, nil) + success, err = DecodeAnswerResponse(b) + So(success, ShouldEqual, false) + So(err, ShouldEqual, nil) }) } From af4cc52dc2eb46585d5f0da3ecc285c914e22414 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Fri, 22 Nov 2019 17:17:22 -0500 Subject: [PATCH 033/385] Add a build step / documentation for code reuse Trac: 32499 --- .gitignore | 1 + proxy/.eslintignore | 1 + proxy/README.md | 64 +++++++++++++++++++++++++++++++++++++++++- proxy/make.js | 10 ++++++- proxy/package.json | 1 + proxy/webext/README.md | 11 -------- 6 files changed, 75 insertions(+), 13 deletions(-) delete mode 100644 proxy/webext/README.md diff --git a/.gitignore b/.gitignore index 1bae622..2d31939 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ snowflake.log proxy/test proxy/build proxy/node_modules +proxy/snowflake-library.js proxy/spec/support proxy/webext/snowflake.js proxy/webext/popup.js diff --git a/proxy/.eslintignore b/proxy/.eslintignore index f580632..c249199 100644 --- a/proxy/.eslintignore +++ b/proxy/.eslintignore @@ -1,6 +1,7 @@ build/ test/ webext/snowflake.js +snowflake-library.js # FIXME: Whittle these away spec/ diff --git a/proxy/README.md b/proxy/README.md index 61468c0..fedfa20 100644 --- a/proxy/README.md +++ b/proxy/README.md @@ -7,12 +7,26 @@ See https://snowflake.torproject.org/ for more info: ``` -### Building +### Building the badge / snowflake.torproject.org ``` +npm install npm run build ``` +which outputs to the `build/` directory. + +### Building the webextension + +``` +npm install +npm run webext +``` + +and then load the `webext/` directory as an unpacked extension. + * https://developer.mozilla.org/en-US/docs/Tools/about:debugging#Loading_a_temporary_extension + * https://developer.chrome.com/extensions/getstarted#manifest + ### Testing Unit testing with Jasmine are available with: @@ -44,6 +58,7 @@ IdentityFile ~/.ssh/tor ### Deploying ``` +npm install npm run build ``` @@ -73,3 +88,50 @@ With no parameters, snowflake uses the default relay `snowflake.freehaven.net:443` and uses automatic signaling with the default broker at `https://snowflake-broker.freehaven.net/`. + +### Reuse as a library + +The badge and the webextension make use of the same underlying library and +only differ in their UI. That same library can be produced for use with other +interfaces, such as [Cupcake][1], by running, + +``` +npm install +npm run library +``` + +which outputs a `./snowflake-library.js`. + +You'd then want to create a subclass of `UI` to perform various actions as +the state of the snowflake changes, + +``` +class MyUI extends UI { + ... +} +``` + +See `WebExtUI` in `init-webext.js` and `BadgeUI` in `init-badge.js` for +examples. + +Finally, initialize the snowflake with, + +``` +var log = function(msg) { + return console.log('Snowflake: ' + msg); +}; +var dbg = log; + +var config = new Config(); +var ui = new MyUI(); // NOTE: Using the class defined above +var broker = new Broker(config.brokerUrl); + +var snowflake = new Snowflake(config, ui, broker); + +snowflake.setRelayAddr(config.relayAddr); +snowflake.beginWebRTC(); +``` + +This minimal setup is pretty much what's currently in `init-node.js`. + +[1]: https://chrome.google.com/webstore/detail/cupcake/dajjbehmbnbppjkcnpdkaniapgdppdnc diff --git a/proxy/make.js b/proxy/make.js index c7be058..f8b2192 100755 --- a/proxy/make.js +++ b/proxy/make.js @@ -39,7 +39,10 @@ var SHARED_FILES = [ ]; var concatJS = function(outDir, init, outFile, pre) { - var files = FILES.concat(`init-${init}.js`); + var files = FILES; + if (init) { + files = files.concat(`init-${init}.js`); + } var outPath = `${outDir}/${outFile}`; writeFileSync(outPath, pre, 'utf8'); execSync(`cat ${files.join(' ')} >> ${outPath}`); @@ -176,6 +179,11 @@ task('clean', 'remove all built files', function() { execSync('rm -rf build test spec/support'); }); +task('library', 'build the library', function() { + concatJS('.', '', 'snowflake-library.js', ''); + console.log('Library prepared.'); +}); + var cmd = process.argv[2]; if (tasks.has(cmd)) { diff --git a/proxy/package.json b/proxy/package.json index 6946691..772746e 100644 --- a/proxy/package.json +++ b/proxy/package.json @@ -10,6 +10,7 @@ "test": "node make.js test", "build": "node make.js build", "webext": "node make.js webext", + "library": "node make.js library", "pack-webext": "node make.js pack-webext", "clean": "node make.js clean", "prepublish": "node make.js node", diff --git a/proxy/webext/README.md b/proxy/webext/README.md deleted file mode 100644 index cd53ff1..0000000 --- a/proxy/webext/README.md +++ /dev/null @@ -1,11 +0,0 @@ -Build it, - -``` -cd .. -npm install -npm run webext -``` - -and then load this directory as an unpacked extension. - * https://developer.mozilla.org/en-US/docs/Tools/about:debugging#Loading_a_temporary_extension - * https://developer.chrome.com/extensions/getstarted#manifest From 1e45d48a3c4ef05434916d963f0c00d8c0246ac9 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Fri, 6 Dec 2019 17:54:54 -0500 Subject: [PATCH 034/385] Document setting the proxyType for metrics Trac: 32499 --- proxy/README.md | 8 +++++++- proxy/config.js | 6 +++++- proxy/init-badge.js | 3 +-- proxy/init-node.js | 2 +- proxy/init-testing.js | 2 +- proxy/init-webext.js | 3 +-- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/proxy/README.md b/proxy/README.md index fedfa20..33b8738 100644 --- a/proxy/README.md +++ b/proxy/README.md @@ -122,7 +122,7 @@ var log = function(msg) { }; var dbg = log; -var config = new Config(); +var config = new Config("myui"); // NOTE: Set a unique proxy type for metrics var ui = new MyUI(); // NOTE: Using the class defined above var broker = new Broker(config.brokerUrl); @@ -134,4 +134,10 @@ snowflake.beginWebRTC(); This minimal setup is pretty much what's currently in `init-node.js`. +When configuring the snowflake, set a unique `proxyType` (first argument +to `Config`) that will be used when recording metrics at the broker. Also, +it would be helpful to get in touch with the [Anti-Censorship Team][2] at the +Tor Project to let them know about your tool. + [1]: https://chrome.google.com/webstore/detail/cupcake/dajjbehmbnbppjkcnpdkaniapgdppdnc +[2]: https://trac.torproject.org/projects/tor/wiki/org/teams/AntiCensorshipTeam diff --git a/proxy/config.js b/proxy/config.js index 2b698a6..39c2b15 100644 --- a/proxy/config.js +++ b/proxy/config.js @@ -1,5 +1,9 @@ -class Config {} +class Config { + constructor(proxyType) { + this.proxyType = proxyType || ''; + } +} Config.prototype.brokerUrl = 'snowflake-broker.freehaven.net'; diff --git a/proxy/init-badge.js b/proxy/init-badge.js index 2e0a261..cb066e8 100644 --- a/proxy/init-badge.js +++ b/proxy/init-badge.js @@ -169,8 +169,7 @@ var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotific return; } - config = new Config; - config.proxyType = "badge"; + config = new Config("badge"); if ('off' !== query.get('ratelimit')) { config.rateLimitBytes = Params.getByteCount(query, 'ratelimit', config.rateLimitBytes); } diff --git a/proxy/init-node.js b/proxy/init-node.js index 73c25dc..b5a60d8 100644 --- a/proxy/init-node.js +++ b/proxy/init-node.js @@ -4,7 +4,7 @@ Entry point. */ -var config = new Config; +var config = new Config("node"); var ui = new UI(); diff --git a/proxy/init-testing.js b/proxy/init-testing.js index f553f12..01b6147 100644 --- a/proxy/init-testing.js +++ b/proxy/init-testing.js @@ -79,7 +79,7 @@ var snowflake, query, debug, ui, silenceNotifications, log, dbg, init; init = function() { var broker, config, ui; - config = new Config; + config = new Config("testing"); if ('off' !== query['ratelimit']) { config.rateLimitBytes = Params.getByteCount(query, 'ratelimit', config.rateLimitBytes); } diff --git a/proxy/init-webext.js b/proxy/init-webext.js index afa9aee..3eb42dd 100644 --- a/proxy/init-webext.js +++ b/proxy/init-webext.js @@ -171,8 +171,7 @@ var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotific }; init = function() { - config = new Config; - config.proxyType = "webext"; + config = new Config("webext"); ui = new WebExtUI(); broker = new Broker(config); snowflake = new Snowflake(config, ui, broker); From 37aaaffa1521b4ff6166a2a013d8e6e5c6e8fbce Mon Sep 17 00:00:00 2001 From: Jascha Date: Fri, 13 Dec 2019 04:47:50 +0100 Subject: [PATCH 035/385] proxy/make.js: add help output --- proxy/make.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/proxy/make.js b/proxy/make.js index f8b2192..6111913 100755 --- a/proxy/make.js +++ b/proxy/make.js @@ -192,4 +192,10 @@ if (tasks.has(cmd)) { t.func(); } else { console.error('Command not supported.'); + + console.log('Commands:'); + + tasks.forEach(function(value, key) { + console.log(key + ' - ' + value.msg); + }) } From 5ff75e1034c8d4ae4ba64f92d9aef364d09c27ac Mon Sep 17 00:00:00 2001 From: David Fifield Date: Mon, 20 Jan 2020 23:57:31 -0700 Subject: [PATCH 036/385] Remove erroneous logging around pt.*Error calls. These functions are called for their side effect of sending a PT error message on stdout; they also return a representation of the error message as an error object for the caller to use if it wishes. These functions *always* return a non-nil error object; it is not something to be logged, any more than the return value of errors.New is. The mistaken logging was added in https://bugs.torproject.org/31794 b26c7a7a7330586c3be3ece02c68999bb279ff40 3ec9dd19faa8584dd76ba3b85eb71a03b8ee25c0 ed3d42e1ec3ff852f8c4751eb4cf5e9ed4dd4a68 --- client/snowflake.go | 12 +++--------- server-webrtc/snowflake.go | 4 +--- server/server.go | 12 +++--------- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/client/snowflake.go b/client/snowflake.go index bb7de46..3c496e0 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -157,9 +157,7 @@ func main() { log.Fatal(err) } if ptInfo.ProxyURL != nil { - if err := pt.ProxyError("proxy is not supported"); err != nil { - log.Printf("call to pt.ProxyError generated error: %v", err) - } + pt.ProxyError("proxy is not supported") os.Exit(1) } listeners := make([]net.Listener, 0) @@ -169,18 +167,14 @@ func main() { // TODO: Be able to recover when SOCKS dies. ln, err := pt.ListenSocks("tcp", "127.0.0.1:0") if err != nil { - if inerr := pt.CmethodError(methodName, err.Error()); inerr != nil { - log.Printf("handling error generated by pt.ListenSocks with pt.CmethodError generated error: %v", inerr) - } + pt.CmethodError(methodName, err.Error()) break } go socksAcceptLoop(ln, snowflakes) pt.Cmethod(methodName, ln.Version(), ln.Addr()) listeners = append(listeners, ln) default: - if err := pt.CmethodError(methodName, "no such method"); err != nil { - log.Printf("calling pt.CmethodError generated error: %v", err) - } + pt.CmethodError(methodName, "no such method") } } pt.CmethodsDone() diff --git a/server-webrtc/snowflake.go b/server-webrtc/snowflake.go index d5b1604..acdf5b1 100644 --- a/server-webrtc/snowflake.go +++ b/server-webrtc/snowflake.go @@ -230,9 +230,7 @@ func main() { bindaddr.Addr.Port = 12345 // lies!!! pt.Smethod(bindaddr.MethodName, bindaddr.Addr) default: - if err := pt.SmethodError(bindaddr.MethodName, "no such method"); err != nil { - log.Printf("SmethodError returned error: %v", err) - } + pt.SmethodError(bindaddr.MethodName, "no such method") } } pt.SmethodsDone() diff --git a/server/server.go b/server/server.go index e3f4c6f..785b545 100644 --- a/server/server.go +++ b/server/server.go @@ -282,9 +282,7 @@ func main() { servers := make([]*http.Server, 0) for _, bindaddr := range ptInfo.Bindaddrs { if bindaddr.MethodName != ptMethodName { - if err = pt.SmethodError(bindaddr.MethodName, "no such method"); err != nil { - log.Printf("pt.SmethodError returned error: %v", err) - } + pt.SmethodError(bindaddr.MethodName, "no such method") continue } @@ -296,9 +294,7 @@ func main() { lnHTTP01, err = net.ListenTCP("tcp", &addr) if err != nil { log.Printf("error opening HTTP-01 ACME listener: %s", err) - if inerr := pt.SmethodError(bindaddr.MethodName, "HTTP-01 ACME listener: "+err.Error()); inerr != nil { - log.Printf("pt.SmethodError returned error: %v", inerr) - } + pt.SmethodError(bindaddr.MethodName, "HTTP-01 ACME listener: "+err.Error()) continue } server := &http.Server{ @@ -326,9 +322,7 @@ func main() { } if err != nil { log.Printf("error opening listener: %s", err) - if inerr := pt.SmethodError(bindaddr.MethodName, err.Error()); inerr != nil { - log.Printf("pt.SmethodError returned error: %v", inerr) - } + pt.SmethodError(bindaddr.MethodName, err.Error()) continue } pt.SmethodArgs(bindaddr.MethodName, bindaddr.Addr, args) From e27709080ab423163c46e4ebf685a0b37e60862e Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 24 Dec 2019 19:16:10 -0700 Subject: [PATCH 037/385] Update a comment: we no longer keep track of handlers. --- client/snowflake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/snowflake.go b/client/snowflake.go index 3c496e0..28bf7e9 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -194,7 +194,7 @@ func main() { }() } - // keep track of handlers and wait for a signal + // wait for a signal <-sigChan // signal received, shut down From d6467ff5854627dcaefbef89bdd348bfe461da12 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 24 Dec 2019 18:01:17 -0700 Subject: [PATCH 038/385] Formatting improvements. --- client/lib/interfaces.go | 1 - client/lib/peers.go | 9 ++++----- client/lib/rendezvous.go | 8 +++----- client/snowflake.go | 22 +++++++++++----------- server/server.go | 4 ++-- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/client/lib/interfaces.go b/client/lib/interfaces.go index 609e610..f6e8240 100644 --- a/client/lib/interfaces.go +++ b/client/lib/interfaces.go @@ -30,7 +30,6 @@ type Tongue interface { // Interface for collecting some number of Snowflakes, for passing along // ultimately to the SOCKS handler. type SnowflakeCollector interface { - // Add a Snowflake to the collection. // Implementation should decide how to connect and maintain the webRTCConn. Collect() (Snowflake, error) diff --git a/client/lib/peers.go b/client/lib/peers.go index 5493bfd..12213e1 100644 --- a/client/lib/peers.go +++ b/client/lib/peers.go @@ -44,8 +44,7 @@ func (p *Peers) Collect() (Snowflake, error) { cnt := p.Count() s := fmt.Sprintf("Currently at [%d/%d]", cnt, p.capacity) if cnt >= p.capacity { - s = fmt.Sprintf("At capacity [%d/%d]", cnt, p.capacity) - return nil, errors.New(s) + return nil, fmt.Errorf("At capacity [%d/%d]", cnt, p.capacity) } log.Println("WebRTC: Collecting a new Snowflake.", s) // Engage the Snowflake Catching interface, which must be available. @@ -68,12 +67,12 @@ func (p *Peers) Pop() Snowflake { // Blocks until an available, valid snowflake appears. var snowflake Snowflake var ok bool - for nil == snowflake { + for snowflake == nil { snowflake, ok = <-p.snowflakeChan - conn := snowflake.(*WebRTCPeer) if !ok { return nil } + conn := snowflake.(*WebRTCPeer) if conn.closed { snowflake = nil } @@ -120,5 +119,5 @@ func (p *Peers) End() { p.activePeers.Remove(e) e = next } - log.Println("WebRTC: melted all", cnt, "snowflakes.") + log.Printf("WebRTC: melted all %d snowflakes.", cnt) } diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 0c7225b..0b15f68 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -50,13 +50,13 @@ func CreateBrokerTransport() http.RoundTripper { // to clients, and |front| is the option fronting domain. func NewBrokerChannel(broker string, front string, transport http.RoundTripper) *BrokerChannel { targetURL, err := url.Parse(broker) - if nil != err { + if err != nil { return nil } log.Println("Rendezvous using Broker at:", broker) bc := new(BrokerChannel) bc.url = targetURL - if "" != front { // Optional front domain. + if front != "" { // Optional front domain. log.Println("Domain fronting using:", front) bc.Host = bc.url.Host bc.url.Host = front @@ -109,7 +109,6 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( } answer := deserializeSessionDescription(string(body)) return answer, nil - case http.StatusServiceUnavailable: return nil, errors.New(BrokerError503) case http.StatusBadRequest: @@ -125,8 +124,7 @@ type WebRTCDialer struct { webrtcConfig *webrtc.Configuration } -func NewWebRTCDialer( - broker *BrokerChannel, iceServers []webrtc.ICEServer) *WebRTCDialer { +func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer) *WebRTCDialer { var config webrtc.Configuration if iceServers != nil { config = webrtc.Configuration{ diff --git a/client/snowflake.go b/client/snowflake.go index 28bf7e9..bd53f65 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -30,9 +30,9 @@ func ConnectLoop(snowflakes sf.SnowflakeCollector) { for { // Check if ending is necessary. _, err := snowflakes.Collect() - if nil != err { - log.Println("WebRTC:", err, - " Retrying in", sf.ReconnectTimeout, "seconds...") + if err != nil { + log.Printf("WebRTC: %v Retrying in %v seconds...", + err, sf.ReconnectTimeout) } select { case <-time.After(time.Second * sf.ReconnectTimeout): @@ -52,7 +52,7 @@ func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { log.Println("SOCKS listening...") conn, err := ln.AcceptSocks() if err != nil { - if e, ok := err.(net.Error); ok && e.Temporary() { + if err, ok := err.(net.Error); ok && err.Temporary() { continue } log.Printf("SOCKS accept error: %s", err) @@ -66,7 +66,7 @@ func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { } } -//s is a comma-separated list of ICE server URLs +// s is a comma-separated list of ICE server URLs. func parseIceServers(s string) []webrtc.ICEServer { var servers []webrtc.ICEServer log.Println(s) @@ -98,9 +98,9 @@ func main() { log.SetFlags(log.LstdFlags | log.LUTC) - // Don't write to stderr; versions of tor earlier than about - // 0.3.5.6 do not read from the pipe, and eventually we will - // deadlock because the buffer is full. + // Don't write to stderr; versions of tor earlier than about 0.3.5.6 do + // not read from the pipe, and eventually we will deadlock because the + // buffer is full. // https://bugs.torproject.org/26360 // https://bugs.torproject.org/25600#comment:14 var logOutput = ioutil.Discard @@ -120,7 +120,7 @@ func main() { defer logFile.Close() logOutput = logFile } - //We want to send the log output through our scrubber first + // We want to send the log output through our scrubber first log.SetOutput(&safelog.LogScrubber{Output: logOutput}) log.Println("\n\n\n --- Starting Snowflake Client ---") @@ -194,10 +194,10 @@ func main() { }() } - // wait for a signal + // Wait for a signal. <-sigChan - // signal received, shut down + // Signal received, shut down. for _, ln := range listeners { ln.Close() } diff --git a/server/server.go b/server/server.go index 785b545..5ed56d3 100644 --- a/server/server.go +++ b/server/server.go @@ -345,10 +345,10 @@ func main() { }() } - // wait for a signal + // Wait for a signal. sig := <-sigChan - // signal received, shut down + // Signal received, shut down. log.Printf("caught signal %q, exiting", sig) for _, server := range servers { server.Close() From 509f634506d90db2c113de70d50fe8894c542c3a Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 24 Dec 2019 18:00:59 -0700 Subject: [PATCH 039/385] NewWebRTCDialer cannot return an error. --- client/snowflake.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/snowflake.go b/client/snowflake.go index bd53f65..99226b8 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -134,10 +134,6 @@ func main() { broker := sf.NewBrokerChannel(*brokerURL, *frontDomain, sf.CreateBrokerTransport()) snowflakes.Tongue = sf.NewWebRTCDialer(broker, iceServers) - if nil == snowflakes.Tongue { - log.Fatal("Unable to prepare rendezvous method.") - return - } // Use a real logger to periodically output how much traffic is happening. snowflakes.BytesLogger = &sf.BytesSyncLogger{ InboundChan: make(chan int, 5), From aa3999857f99e5aa51b7a37366e666e1d1e30af3 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 24 Dec 2019 18:19:28 -0700 Subject: [PATCH 040/385] Move ICE server logging out of parseIceServers. --- client/snowflake.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/snowflake.go b/client/snowflake.go index 99226b8..b807c8d 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -69,16 +69,13 @@ func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { // s is a comma-separated list of ICE server URLs. func parseIceServers(s string) []webrtc.ICEServer { var servers []webrtc.ICEServer - log.Println(s) s = strings.TrimSpace(s) if len(s) == 0 { return nil } urls := strings.Split(s, ",") - log.Printf("Using ICE Servers:") for _, url := range urls { url = strings.TrimSpace(url) - log.Printf("url: %s", url) servers = append(servers, webrtc.ICEServer{ URLs: []string{url}, }) @@ -126,6 +123,10 @@ func main() { log.Println("\n\n\n --- Starting Snowflake Client ---") iceServers := parseIceServers(*iceServersCommas) + log.Printf("Using ICE servers:") + for _, server := range iceServers { + log.Printf("url: %v", strings.Join(server.URLs, " ")) + } // Prepare to collect remote WebRTC peers. snowflakes := sf.NewPeers(*max) From febb4936f6cf1d066ef87a936e3f92d28c4295e1 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 24 Dec 2019 19:02:01 -0700 Subject: [PATCH 041/385] Refactor SOCKS-related logging. --- client/snowflake.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/snowflake.go b/client/snowflake.go index b807c8d..1d7907b 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -47,9 +47,7 @@ func ConnectLoop(snowflakes sf.SnowflakeCollector) { // Accept local SOCKS connections and pass them to the handler. func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { defer ln.Close() - log.Println("Started SOCKS listener.") for { - log.Println("SOCKS listening...") conn, err := ln.AcceptSocks() if err != nil { if err, ok := err.(net.Error); ok && err.Temporary() { @@ -58,7 +56,7 @@ func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { log.Printf("SOCKS accept error: %s", err) break } - log.Println("SOCKS accepted: ", conn.Req) + log.Printf("SOCKS accepted: %v", conn.Req) err = sf.Handler(conn, snowflakes) if err != nil { log.Printf("handler error: %s", err) @@ -167,6 +165,7 @@ func main() { pt.CmethodError(methodName, err.Error()) break } + log.Printf("Started SOCKS listener at %v.", ln.Addr()) go socksAcceptLoop(ln, snowflakes) pt.Cmethod(methodName, ln.Version(), ln.Addr()) listeners = append(listeners, ln) From f1ab65b1c050ee603454f3d9836753512863e21a Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 24 Dec 2019 19:22:48 -0700 Subject: [PATCH 042/385] Close the melt channel, don't just send once on it. Closing the channel makes it always immediately selectable. --- client/lib/peers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lib/peers.go b/client/lib/peers.go index 12213e1..d385971 100644 --- a/client/lib/peers.go +++ b/client/lib/peers.go @@ -35,7 +35,7 @@ func NewPeers(max int) *Peers { // Use buffered go channel to pass snowflakes onwards to the SOCKS handler. p.snowflakeChan = make(chan Snowflake, max) p.activePeers = list.New() - p.melt = make(chan struct{}, 1) + p.melt = make(chan struct{}) return p } @@ -110,7 +110,7 @@ func (p *Peers) purgeClosedPeers() { // Close all Peers contained here. func (p *Peers) End() { close(p.snowflakeChan) - p.melt <- struct{}{} + close(p.melt) cnt := p.Count() for e := p.activePeers.Front(); e != nil; { next := e.Next() From 2fb52c86399b343b296f985a3aa6d568b7b09c96 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 24 Dec 2019 19:57:01 -0700 Subject: [PATCH 043/385] Check for an invalid broker URL at a higher level. Instead of returning nil from NewBrokerChannel and having WebRTCDialer.Catch check for nil, let NewBrokerChannel return an error and bail out before calling WebRTCDialer.Catch. Suggested by cohosh. https://bugs.torproject.org/33040#comment:3 --- client/lib/rendezvous.go | 9 +++------ client/snowflake.go | 5 ++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 0b15f68..fef0eb5 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -48,10 +48,10 @@ func CreateBrokerTransport() http.RoundTripper { // Construct a new BrokerChannel, where: // |broker| is the full URL of the facilitating program which assigns proxies // to clients, and |front| is the option fronting domain. -func NewBrokerChannel(broker string, front string, transport http.RoundTripper) *BrokerChannel { +func NewBrokerChannel(broker string, front string, transport http.RoundTripper) (*BrokerChannel, error) { targetURL, err := url.Parse(broker) if err != nil { - return nil + return nil, err } log.Println("Rendezvous using Broker at:", broker) bc := new(BrokerChannel) @@ -63,7 +63,7 @@ func NewBrokerChannel(broker string, front string, transport http.RoundTripper) } bc.transport = transport - return bc + return bc, nil } func limitedRead(r io.Reader, limit int64) ([]byte, error) { @@ -141,9 +141,6 @@ func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer) *WebR // Initialize a WebRTC Connection by signaling through the broker. func (w WebRTCDialer) Catch() (Snowflake, error) { - if nil == w.BrokerChannel { - return nil, errors.New("cannot Dial WebRTC without a BrokerChannel") - } // TODO: [#3] Fetch ICE server information from Broker. // TODO: [#18] Consider TURN servers here too. connection := NewWebRTCPeer(w.webrtcConfig, w.BrokerChannel) diff --git a/client/snowflake.go b/client/snowflake.go index 1d7907b..7cb9451 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -130,7 +130,10 @@ func main() { snowflakes := sf.NewPeers(*max) // Use potentially domain-fronting broker to rendezvous. - broker := sf.NewBrokerChannel(*brokerURL, *frontDomain, sf.CreateBrokerTransport()) + broker, err := sf.NewBrokerChannel(*brokerURL, *frontDomain, sf.CreateBrokerTransport()) + if err != nil { + log.Fatalf("parsing broker URL: %v", err) + } snowflakes.Tongue = sf.NewWebRTCDialer(broker, iceServers) // Use a real logger to periodically output how much traffic is happening. From db1ba4791b101b7a83060261bb44769dd7db8e25 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 24 Dec 2019 20:03:11 -0700 Subject: [PATCH 044/385] Simplify NewWebRTCDialer. --- client/lib/rendezvous.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index fef0eb5..d117ebc 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -125,13 +125,8 @@ type WebRTCDialer struct { } func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer) *WebRTCDialer { - var config webrtc.Configuration - if iceServers != nil { - config = webrtc.Configuration{ - ICEServers: iceServers, - } - } else { - config = webrtc.Configuration{} + config := webrtc.Configuration{ + ICEServers: iceServers, } return &WebRTCDialer{ BrokerChannel: broker, From bc5498cb4b605f4e0777b6348738ace89f88f953 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 28 Jan 2020 02:55:17 -0700 Subject: [PATCH 045/385] Fix the order of arguments of client copyLoop to match the call. The call was copyLoop(socks, snowflake) but the function signature was func copyLoop(WebRTC, SOCKS io.ReadWriter) { The mistake was mostly harmless, because both arguments were treated the same, except that error logs would have reported the wrong direction. --- client/lib/snowflake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 2e68e36..50070af 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -48,7 +48,7 @@ func Handler(socks SocksConnector, snowflakes SnowflakeCollector) error { // Exchanges bytes between two ReadWriters. // (In this case, between a SOCKS and WebRTC connection.) -func copyLoop(WebRTC, SOCKS io.ReadWriter) { +func copyLoop(SOCKS, WebRTC io.ReadWriter) { var wg sync.WaitGroup wg.Add(2) go func() { From 57d4b0b5bdac10875e8f57cca71a37a74d42f387 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 28 Jan 2020 03:01:48 -0700 Subject: [PATCH 046/385] Use lowercase variable names in copyLoop. --- client/lib/snowflake.go | 6 +++--- server-webrtc/snowflake.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 50070af..a27c6a5 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -48,17 +48,17 @@ func Handler(socks SocksConnector, snowflakes SnowflakeCollector) error { // Exchanges bytes between two ReadWriters. // (In this case, between a SOCKS and WebRTC connection.) -func copyLoop(SOCKS, WebRTC io.ReadWriter) { +func copyLoop(socks, webRTC io.ReadWriter) { var wg sync.WaitGroup wg.Add(2) go func() { - if _, err := io.Copy(SOCKS, WebRTC); err != nil { + if _, err := io.Copy(socks, webRTC); err != nil { log.Printf("copying WebRTC to SOCKS resulted in error: %v", err) } wg.Done() }() go func() { - if _, err := io.Copy(WebRTC, SOCKS); err != nil { + if _, err := io.Copy(webRTC, socks); err != nil { log.Printf("copying SOCKS to WebRTC resulted in error: %v", err) } wg.Done() diff --git a/server-webrtc/snowflake.go b/server-webrtc/snowflake.go index acdf5b1..9ca82cb 100644 --- a/server-webrtc/snowflake.go +++ b/server-webrtc/snowflake.go @@ -21,17 +21,17 @@ var ptMethodName = "snowflake" var ptInfo pt.ServerInfo var logFile *os.File -func copyLoop(WebRTC, ORPort net.Conn) { +func copyLoop(webRTC, orPort net.Conn) { var wg sync.WaitGroup wg.Add(2) go func() { - if _, err := io.Copy(ORPort, WebRTC); err != nil { + if _, err := io.Copy(orPort, webRTC); err != nil { log.Printf("copy WebRTC to ORPort error in copyLoop: %v", err) } wg.Done() }() go func() { - if _, err := io.Copy(WebRTC, ORPort); err != nil { + if _, err := io.Copy(webRTC, orPort); err != nil { log.Printf("copy ORPort to WebRTC error in copyLoop: %v", err) } wg.Done() From 7682986a451deb3a4d28240fa7d8b06ed5d7a5dd Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 29 Jan 2020 11:27:44 -0500 Subject: [PATCH 047/385] Update client tests for NewBrokerChannel errors We changed NewBrokerChannel to return an error value on failure. This updates the tests to check that value. --- client/lib/lib_test.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 12368f3..d48c301 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -294,22 +294,25 @@ func TestSnowflakeClient(t *testing.T) { fakeOffer := deserializeSessionDescription(`{"type":"offer","sdp":"test"}`) Convey("Construct BrokerChannel with no front domain", func() { - b := NewBrokerChannel("test.broker", "", transport) + b, err := NewBrokerChannel("test.broker", "", transport) So(b.url, ShouldNotBeNil) + So(err, ShouldBeNil) So(b.url.Path, ShouldResemble, "test.broker") So(b.transport, ShouldNotBeNil) }) Convey("Construct BrokerChannel *with* front domain", func() { - b := NewBrokerChannel("test.broker", "front", transport) + b, err := NewBrokerChannel("test.broker", "front", transport) So(b.url, ShouldNotBeNil) + So(err, ShouldBeNil) So(b.url.Path, ShouldResemble, "test.broker") So(b.url.Host, ShouldResemble, "front") So(b.transport, ShouldNotBeNil) }) Convey("BrokerChannel.Negotiate responds with answer", func() { - b := NewBrokerChannel("test.broker", "", transport) + b, err := NewBrokerChannel("test.broker", "", transport) + So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldBeNil) So(answer, ShouldNotBeNil) @@ -317,8 +320,9 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("BrokerChannel.Negotiate fails with 503", func() { - b := NewBrokerChannel("test.broker", "", + b, err := NewBrokerChannel("test.broker", "", &MockTransport{http.StatusServiceUnavailable, []byte("\n")}) + So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldNotBeNil) So(answer, ShouldBeNil) @@ -326,8 +330,9 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("BrokerChannel.Negotiate fails with 400", func() { - b := NewBrokerChannel("test.broker", "", + b, err := NewBrokerChannel("test.broker", "", &MockTransport{http.StatusBadRequest, []byte("\n")}) + So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldNotBeNil) So(answer, ShouldBeNil) @@ -335,8 +340,9 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("BrokerChannel.Negotiate fails with large read", func() { - b := NewBrokerChannel("test.broker", "", + b, err := NewBrokerChannel("test.broker", "", &MockTransport{http.StatusOK, make([]byte, 100001, 100001)}) + So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldNotBeNil) So(answer, ShouldBeNil) @@ -344,8 +350,9 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("BrokerChannel.Negotiate fails with unexpected error", func() { - b := NewBrokerChannel("test.broker", "", + b, err := NewBrokerChannel("test.broker", "", &MockTransport{123, []byte("")}) + So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldNotBeNil) So(answer, ShouldBeNil) From 50673d49437c55aba2a5f7f31819bcae60406b07 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 29 Jan 2020 11:40:29 -0500 Subject: [PATCH 048/385] Remove client test with nil broker We are no longer checking for nil BrokerChannels in Catch because this case is caught from the return values of NewBrokerChannel. This change caused a no longer necessary unit test to hang. --- client/lib/lib_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index d48c301..91a9809 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -271,12 +271,6 @@ func TestSnowflakeClient(t *testing.T) { So(d.BrokerChannel, ShouldNotBeNil) So(d.BrokerChannel.Host, ShouldEqual, "test") }) - Convey("WebRTCDialer cannot Catch a snowflake with nil broker.", func() { - d := NewWebRTCDialer(nil, nil) - conn, err := d.Catch() - So(conn, ShouldBeNil) - So(err, ShouldNotBeNil) - }) SkipConvey("WebRTCDialer can Catch a snowflake.", func() { broker := &BrokerChannel{Host: "test"} d := NewWebRTCDialer(broker, nil) From a4287095c05addaa204c605c691f147da477c2f5 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 30 Jan 2020 10:15:12 -0700 Subject: [PATCH 049/385] Also show message in the "error copying WebSocket to ORPort" case. This was the only case out of the three not to show it. --- server/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index 5ed56d3..9023252 100644 --- a/server/server.go +++ b/server/server.go @@ -68,7 +68,7 @@ func proxy(local *net.TCPConn, conn *websocketconn.WebSocketConn) { }() go func() { if _, err := io.Copy(local, conn); err != nil { - log.Printf("error copying WebSocket to ORPort") + log.Printf("error copying WebSocket to ORPort %v", err) } if err := local.CloseWrite(); err != nil { log.Printf("error closing write after copying WebSocket to ORPort %v", err) From 5b01df903085fbba96e52277e598c395bce27d88 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 28 Jan 2020 00:09:38 -0700 Subject: [PATCH 050/385] Initialize the global upgrader.CheckOrigin statically. Only once, not again on every call to initServer. --- server/server.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/server.go b/server/server.go index 9023252..acb260a 100644 --- a/server/server.go +++ b/server/server.go @@ -94,7 +94,9 @@ func clientAddr(clientIPParam string) string { return (&net.TCPAddr{IP: clientIP, Port: 1, Zone: ""}).String() } -var upgrader = websocket.Upgrader{} +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, +} type HTTPHandler struct{} @@ -139,8 +141,6 @@ func initServer(addr *net.TCPAddr, return nil, fmt.Errorf("cannot listen on port %d; configure a port using ServerTransportListenAddr", addr.Port) } - upgrader.CheckOrigin = func(r *http.Request) bool { return true } - var handler HTTPHandler server := &http.Server{ Addr: addr.String(), From e47dd5e2b44161b6699ca6560878ddc9730a63b1 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 28 Jan 2020 00:54:26 -0700 Subject: [PATCH 051/385] Remove some redundancy in websocketconn naming. Rename websocketconn.WebSocketConn to websocketconn.Conn, and websocketconn.NewWebSocketConn to websocketconn.New Following the guidelines at https://blog.golang.org/package-names#TOC_3%2e --- common/websocketconn/websocketconn.go | 14 +++++++------- proxy-go/snowflake.go | 2 +- server/server.go | 4 ++-- server/server_test.go | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/common/websocketconn/websocketconn.go b/common/websocketconn/websocketconn.go index 7e12abf..8a9e015 100644 --- a/common/websocketconn/websocketconn.go +++ b/common/websocketconn/websocketconn.go @@ -9,13 +9,13 @@ import ( // An abstraction that makes an underlying WebSocket connection look like an // io.ReadWriteCloser. -type WebSocketConn struct { +type Conn struct { Ws *websocket.Conn r io.Reader } // Implements io.Reader. -func (conn *WebSocketConn) Read(b []byte) (n int, err error) { +func (conn *Conn) Read(b []byte) (n int, err error) { var opCode int if conn.r == nil { // New message @@ -43,7 +43,7 @@ func (conn *WebSocketConn) Read(b []byte) (n int, err error) { } // Implements io.Writer. -func (conn *WebSocketConn) Write(b []byte) (n int, err error) { +func (conn *Conn) Write(b []byte) (n int, err error) { var w io.WriteCloser if w, err = conn.Ws.NextWriter(websocket.BinaryMessage); err != nil { return @@ -56,15 +56,15 @@ func (conn *WebSocketConn) Write(b []byte) (n int, err error) { } // Implements io.Closer. -func (conn *WebSocketConn) Close() error { +func (conn *Conn) Close() error { // Ignore any error in trying to write a Close frame. _ = conn.Ws.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second)) return conn.Ws.Close() } -// Create a new WebSocketConn. -func NewWebSocketConn(ws *websocket.Conn) WebSocketConn { - var conn WebSocketConn +// Create a new Conn. +func New(ws *websocket.Conn) Conn { + var conn Conn conn.Ws = ws return conn } diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index dce7b70..675d76a 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -285,7 +285,7 @@ func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) { log.Printf("error dialing relay: %s", err) return } - wsConn := websocketconn.NewWebSocketConn(ws) + wsConn := websocketconn.New(ws) log.Printf("connected to relay") defer wsConn.Close() CopyLoop(conn, &wsConn) diff --git a/server/server.go b/server/server.go index acb260a..739a55a 100644 --- a/server/server.go +++ b/server/server.go @@ -52,7 +52,7 @@ additional HTTP listener on port 80 to work with ACME. } // Copy from WebSocket to socket and vice versa. -func proxy(local *net.TCPConn, conn *websocketconn.WebSocketConn) { +func proxy(local *net.TCPConn, conn *websocketconn.Conn) { var wg sync.WaitGroup wg.Add(2) @@ -107,7 +107,7 @@ func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - conn := websocketconn.NewWebSocketConn(ws) + conn := websocketconn.New(ws) defer conn.Close() // Pass the address of client as the remote address of incoming connection diff --git a/server/server_test.go b/server/server_test.go index 7a72014..bbc9ba9 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -60,7 +60,7 @@ type StubHandler struct{} func (handler *StubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ws, _ := upgrader.Upgrade(w, r, nil) - conn := websocketconn.NewWebSocketConn(ws) + conn := websocketconn.New(ws) defer conn.Close() //dial stub OR @@ -90,7 +90,7 @@ func Test(t *testing.T) { So(err, ShouldBeNil) ws, _, err := websocket.DefaultDialer.Dial("ws://localhost:8888", nil) - wsConn := websocketconn.NewWebSocketConn(ws) + wsConn := websocketconn.New(ws) So(err, ShouldEqual, nil) So(wsConn, ShouldNotEqual, nil) @@ -133,7 +133,7 @@ func Test(t *testing.T) { ws, _, err := websocket.DefaultDialer.Dial("ws://localhost:8888", nil) So(err, ShouldEqual, nil) - wsConn := websocketconn.NewWebSocketConn(ws) + wsConn := websocketconn.New(ws) So(wsConn, ShouldNotEqual, nil) wsConn.Write([]byte("Hello")) From 20ac2029fd671ccfa8af240fe9c91d9653715ebc Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 28 Jan 2020 02:37:10 -0700 Subject: [PATCH 052/385] Have websocketconn.New return a pointer. This makes the return type satisfy the io.ReadWriteCloser interface directly. --- common/websocketconn/websocketconn.go | 4 ++-- proxy-go/snowflake.go | 2 +- server/server.go | 2 +- server/server_test.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/websocketconn/websocketconn.go b/common/websocketconn/websocketconn.go index 8a9e015..b87e657 100644 --- a/common/websocketconn/websocketconn.go +++ b/common/websocketconn/websocketconn.go @@ -63,8 +63,8 @@ func (conn *Conn) Close() error { } // Create a new Conn. -func New(ws *websocket.Conn) Conn { +func New(ws *websocket.Conn) *Conn { var conn Conn conn.Ws = ws - return conn + return &conn } diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index 675d76a..e964a07 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -288,7 +288,7 @@ func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) { wsConn := websocketconn.New(ws) log.Printf("connected to relay") defer wsConn.Close() - CopyLoop(conn, &wsConn) + CopyLoop(conn, wsConn) log.Printf("datachannelHandler ends") } diff --git a/server/server.go b/server/server.go index 739a55a..5d3bfc6 100644 --- a/server/server.go +++ b/server/server.go @@ -125,7 +125,7 @@ func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } defer or.Close() - proxy(or, &conn) + proxy(or, conn) } func initServer(addr *net.TCPAddr, diff --git a/server/server_test.go b/server/server_test.go index bbc9ba9..d4ada6e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -66,7 +66,7 @@ func (handler *StubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { //dial stub OR or, _ := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP("localhost"), Port: 8889}) - proxy(or, &conn) + proxy(or, conn) } func Test(t *testing.T) { From dfb83c6606fe129d57f101d837ebe32133d79c61 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 28 Jan 2020 03:10:54 -0700 Subject: [PATCH 053/385] Allow handling multiple SOCKS connections simultaneously. Close the SOCKS connection in the same function that opens it. --- client/lib/snowflake.go | 1 - client/snowflake.go | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index a27c6a5..9ab6fc6 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -25,7 +25,6 @@ func Handler(socks SocksConnector, snowflakes SnowflakeCollector) error { return errors.New("handler: Received invalid Snowflake") } - defer socks.Close() defer snowflake.Close() log.Println("---- Handler: snowflake assigned ----") err := socks.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0}) diff --git a/client/snowflake.go b/client/snowflake.go index 7cb9451..af416be 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -57,10 +57,13 @@ func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { break } log.Printf("SOCKS accepted: %v", conn.Req) - err = sf.Handler(conn, snowflakes) - if err != nil { - log.Printf("handler error: %s", err) - } + go func() { + defer conn.Close() + err = sf.Handler(conn, snowflakes) + if err != nil { + log.Printf("handler error: %s", err) + } + }() } } From a2292ce35be6ba4b63e7c8dbac8b3c7bc220822a Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 24 Dec 2019 18:39:43 -0700 Subject: [PATCH 054/385] Make timeout constants into time.Duration values. This slightly changes some log messages. --- client/lib/snowflake.go | 5 +++-- client/lib/webrtc.go | 10 +++++----- client/snowflake.go | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 9ab6fc6..2065f73 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -6,11 +6,12 @@ import ( "log" "net" "sync" + "time" ) const ( - ReconnectTimeout = 10 - SnowflakeTimeout = 30 + ReconnectTimeout = 10 * time.Second + SnowflakeTimeout = 30 * time.Second ) // Given an accepted SOCKS connection, establish a WebRTC connection to the diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 9d1ba37..8e06d98 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -117,9 +117,9 @@ func (c *WebRTCPeer) checkForStaleness() { if c.closed { return } - if time.Since(c.lastReceive).Seconds() > SnowflakeTimeout { - log.Println("WebRTC: No messages received for", SnowflakeTimeout, - "seconds -- closing stale connection.") + if time.Since(c.lastReceive) > SnowflakeTimeout { + log.Printf("WebRTC: No messages received for %v -- closing stale connection.", + SnowflakeTimeout) c.Close() return } @@ -314,8 +314,8 @@ func (c *WebRTCPeer) exchangeSDP() error { go c.sendOfferToBroker() answer, ok = <-c.answerChannel // Blocks... if !ok || nil == answer { - log.Printf("Failed to retrieve answer. Retrying in %d seconds", ReconnectTimeout) - <-time.After(time.Second * ReconnectTimeout) + log.Printf("Failed to retrieve answer. Retrying in %v", ReconnectTimeout) + <-time.After(ReconnectTimeout) answer = nil } } diff --git a/client/snowflake.go b/client/snowflake.go index af416be..edcbd4a 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -31,11 +31,11 @@ func ConnectLoop(snowflakes sf.SnowflakeCollector) { // Check if ending is necessary. _, err := snowflakes.Collect() if err != nil { - log.Printf("WebRTC: %v Retrying in %v seconds...", + log.Printf("WebRTC: %v Retrying in %v...", err, sf.ReconnectTimeout) } select { - case <-time.After(time.Second * sf.ReconnectTimeout): + case <-time.After(sf.ReconnectTimeout): continue case <-snowflakes.Melted(): log.Println("ConnectLoop: stopped.") From 564d1c83634f26341c423815387e728f14c0e61d Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 31 Jan 2020 00:15:11 -0700 Subject: [PATCH 055/385] Remove unused maxMessageSize constant. --- server/server.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/server.go b/server/server.go index 5d3bfc6..85bf4f5 100644 --- a/server/server.go +++ b/server/server.go @@ -30,8 +30,6 @@ import ( const ptMethodName = "snowflake" const requestTimeout = 10 * time.Second -const maxMessageSize = 64 * 1024 - // How long to wait for ListenAndServe or ListenAndServeTLS to return an error // before deciding that it's not going to return. const listenAndServeErrorTimeout = 100 * time.Millisecond From 310890aa1461ed1d18bc9c1c70755cc1862b81e3 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 3 Feb 2020 09:49:34 -0500 Subject: [PATCH 056/385] bump version to 0.2.1 --- proxy/translation | 2 +- proxy/webext/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/translation b/proxy/translation index 488054e..2d92ad1 160000 --- a/proxy/translation +++ b/proxy/translation @@ -1 +1 @@ -Subproject commit 488054eda4c4d9c16fc4bddf439136178d0c769e +Subproject commit 2d92ad194c9d54bd7164b680f5c41c80e1aab87d diff --git a/proxy/webext/manifest.json b/proxy/webext/manifest.json index 2894db6..9578bc0 100644 --- a/proxy/webext/manifest.json +++ b/proxy/webext/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Snowflake", - "version": "0.2.0", + "version": "0.2.1", "description": "__MSG_appDesc__", "default_locale": "en_US", "background": { From 5708a1d57b53d6d586d0e98be9a0b5a964e7c6c3 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Mon, 3 Feb 2020 12:06:08 -0700 Subject: [PATCH 057/385] websocketconn tests. https://bugs.torproject.org/33144 --- common/websocketconn/websocketconn_test.go | 235 +++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 common/websocketconn/websocketconn_test.go diff --git a/common/websocketconn/websocketconn_test.go b/common/websocketconn/websocketconn_test.go new file mode 100644 index 0000000..ad6f100 --- /dev/null +++ b/common/websocketconn/websocketconn_test.go @@ -0,0 +1,235 @@ +package websocketconn + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "sync" + "testing" + "time" + + "github.com/gorilla/websocket" +) + +// Returns a (server, client) pair of websocketconn.Conns. +func connPair() (*Conn, *Conn, error) { + // Will be assigned inside server.Handler. + var serverConn *Conn + + // Start up a web server to receive the request. + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, nil, err + } + defer ln.Close() + errCh := make(chan error) + server := http.Server{ + Handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + upgrader := websocket.Upgrader{ + CheckOrigin: func(*http.Request) bool { return true }, + } + ws, err := upgrader.Upgrade(rw, req, nil) + if err != nil { + errCh <- err + return + } + serverConn = New(ws) + close(errCh) + }), + } + defer server.Close() + go func() { + err := server.Serve(ln) + if err != nil && err != http.ErrServerClosed { + errCh <- err + } + }() + + // Make a request to the web server. + urlStr := (&url.URL{Scheme: "ws", Host: ln.Addr().String()}).String() + ws, _, err := (&websocket.Dialer{}).Dial(urlStr, nil) + if err != nil { + return nil, nil, err + } + clientConn := New(ws) + + // The server is finished when errCh is written to or closed. + err = <-errCh + if err != nil { + return nil, nil, err + } + return serverConn, clientConn, nil +} + +// Test that you can write in chunks and read the result concatenated. +func TestWrite(t *testing.T) { + tests := [][][]byte{ + {}, + {[]byte("foo")}, + {[]byte("foo"), []byte("bar")}, + {{}, []byte("foo"), {}, {}, []byte("bar")}, + } + + for _, test := range tests { + s, c, err := connPair() + if err != nil { + t.Fatal(err) + } + + // This is a little awkward because we need to read to and write + // from both ends of the Conn, and we need to do it in separate + // goroutines because otherwise a Write may block waiting for + // someone to Read it. Here we set up a loop in a separate + // goroutine, reading from the Conn s and writing to the dataCh + // and errCh channels, whose ultimate effect in the select loop + // below is like + // data, err := ioutil.ReadAll(s) + dataCh := make(chan []byte) + errCh := make(chan error) + go func() { + for { + var buf [1024]byte + n, err := s.Read(buf[:]) + if err != nil { + errCh <- err + return + } + p := make([]byte, n) + copy(p, buf[:]) + dataCh <- p + } + }() + + // Write the data to the client side of the Conn, one chunk at a + // time. + for i, chunk := range test { + n, err := c.Write(chunk) + if err != nil || n != len(chunk) { + t.Fatalf("%+q Write chunk %d: got (%d, %v), expected (%d, %v)", + test, i, n, err, len(chunk), nil) + } + } + // We cannot immediately c.Close here, because that closes the + // connection right away, without waiting for buffered data to + // be sent. + + // Pull data and err from the server goroutine above. + var data []byte + err = nil + loop: + for { + select { + case p := <-dataCh: + data = append(data, p...) + case err = <-errCh: + break loop + case <-time.After(100 * time.Millisecond): + break loop + } + } + s.Close() + c.Close() + + // Now data and err contain the result of reading everything + // from s. + expected := bytes.Join(test, []byte{}) + if err != nil || !bytes.Equal(data, expected) { + t.Fatalf("%+q ReadAll: got (%+q, %v), expected (%+q, %v)", + test, data, err, expected, nil) + } + } +} + +// Test that multiple goroutines may call Read on a Conn simultaneously. Run +// this with +// go test -race +func TestConcurrentRead(t *testing.T) { + s, c, err := connPair() + if err != nil { + t.Fatal(err) + } + defer s.Close() + + // Set up multiple threads reading from the same conn. + errCh := make(chan error, 2) + var wg sync.WaitGroup + wg.Add(2) + for i := 0; i < 2; i++ { + go func() { + defer wg.Done() + _, err := io.Copy(ioutil.Discard, s) + if err != nil { + errCh <- err + } + }() + } + + // Write a bunch of data to the other end. + for i := 0; i < 2000; i++ { + _, err := fmt.Fprintf(c, "%d", i) + if err != nil { + c.Close() + t.Fatalf("Write: %v", err) + } + } + c.Close() + + wg.Wait() + close(errCh) + + err = <-errCh + if err != nil { + t.Fatalf("Read: %v", err) + } +} + +// Test that multiple goroutines may call Write on a Conn simultaneously. Run +// this with +// go test -race +func TestConcurrentWrite(t *testing.T) { + s, c, err := connPair() + if err != nil { + t.Fatal(err) + } + + // Set up multiple threads writing to the same conn. + errCh := make(chan error, 3) + var wg sync.WaitGroup + wg.Add(2) + for i := 0; i < 2; i++ { + go func() { + defer wg.Done() + for j := 0; j < 1000; j++ { + _, err := fmt.Fprintf(s, "%d", j) + if err != nil { + errCh <- err + break + } + } + }() + } + go func() { + wg.Wait() + err := s.Close() + if err != nil { + errCh <- err + } + close(errCh) + }() + + // Read from the other end. + _, err = io.Copy(ioutil.Discard, c) + c.Close() + if err != nil { + t.Fatalf("Read: %v", err) + } + + err = <-errCh + if err != nil { + t.Fatalf("Write: %v", err) + } +} From 01e28aa4604fea7a2af0259c8b18be1bd5f9b3d7 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Mon, 3 Feb 2020 12:31:00 -0700 Subject: [PATCH 058/385] Rewrite websocketconn with synchronous pipes. Makes the following changes: * permits concurrent Read/Write/Close * converts certain CloseErrors into io.EOF https://bugs.torproject.org/33144 --- common/websocketconn/websocketconn.go | 122 +++++++++++++++++--------- 1 file changed, 82 insertions(+), 40 deletions(-) diff --git a/common/websocketconn/websocketconn.go b/common/websocketconn/websocketconn.go index b87e657..fa2b0da 100644 --- a/common/websocketconn/websocketconn.go +++ b/common/websocketconn/websocketconn.go @@ -10,61 +10,103 @@ import ( // An abstraction that makes an underlying WebSocket connection look like an // io.ReadWriteCloser. type Conn struct { - Ws *websocket.Conn - r io.Reader + ws *websocket.Conn + Reader io.Reader + Writer io.Writer } // Implements io.Reader. func (conn *Conn) Read(b []byte) (n int, err error) { - var opCode int - if conn.r == nil { - // New message - var r io.Reader - for { - if opCode, r, err = conn.Ws.NextReader(); err != nil { - return - } - if opCode != websocket.BinaryMessage && opCode != websocket.TextMessage { - continue - } - - conn.r = r - break - } - } - - n, err = conn.r.Read(b) - if err == io.EOF { - // Message finished - conn.r = nil - err = nil - } - return + return conn.Reader.Read(b) } // Implements io.Writer. func (conn *Conn) Write(b []byte) (n int, err error) { - var w io.WriteCloser - if w, err = conn.Ws.NextWriter(websocket.BinaryMessage); err != nil { - return - } - if n, err = w.Write(b); err != nil { - return - } - err = w.Close() - return + return conn.Writer.Write(b) } // Implements io.Closer. func (conn *Conn) Close() error { // Ignore any error in trying to write a Close frame. - _ = conn.Ws.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second)) - return conn.Ws.Close() + _ = conn.ws.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second)) + return conn.ws.Close() +} + +func readLoop(w io.Writer, ws *websocket.Conn) error { + for { + messageType, r, err := ws.NextReader() + if err != nil { + return err + } + if messageType != websocket.BinaryMessage && messageType != websocket.TextMessage { + continue + } + _, err = io.Copy(w, r) + if err != nil { + return err + } + } + return nil +} + +func writeLoop(ws *websocket.Conn, r io.Reader) error { + for { + var buf [2048]byte + n, err := r.Read(buf[:]) + if err != nil { + return err + } + data := buf[:n] + w, err := ws.NextWriter(websocket.BinaryMessage) + if err != nil { + return err + } + n, err = w.Write(data) + if err != nil { + return err + } + err = w.Close() + if err != nil { + return err + } + } +} + +// websocket.Conn methods start returning websocket.CloseError after the +// connection has been closed. We want to instead interpret that as io.EOF, just +// as you would find with a normal net.Conn. This only converts +// websocket.CloseErrors with known codes; other codes like CloseProtocolError +// and CloseAbnormalClosure will still be reported as anomalous. +func closeErrorToEOF(err error) error { + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + err = io.EOF + } + return err } // Create a new Conn. func New(ws *websocket.Conn) *Conn { - var conn Conn - conn.Ws = ws - return &conn + // Set up synchronous pipes to serialize reads and writes to the + // underlying websocket.Conn. + // + // https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency + // "Connections support one concurrent reader and one concurrent writer. + // Applications are responsible for ensuring that no more than one + // goroutine calls the write methods (NextWriter, etc.) concurrently and + // that no more than one goroutine calls the read methods (NextReader, + // etc.) concurrently. The Close and WriteControl methods can be called + // concurrently with all other methods." + pr1, pw1 := io.Pipe() + go func() { + pw1.CloseWithError(closeErrorToEOF(readLoop(pw1, ws))) + }() + pr2, pw2 := io.Pipe() + go func() { + pr2.CloseWithError(closeErrorToEOF(writeLoop(ws, pr2))) + }() + return &Conn{ + ws: ws, + Reader: pr1, + Writer: pw2, + } } From 256959ca6594dddd9fe5b012680b4f0235401cd9 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Mon, 3 Feb 2020 13:22:03 -0700 Subject: [PATCH 059/385] Implement net.Conn for websocketconn.Conn. We had already implemented Read, Write, and Close. Pass RemoteAddr, LocalAddr, SetReadDeadline, and SetWriteDeadline through to the underlying *websocket.Conn. Implement SetDeadline by calling both SetReadDeadline and SetWriteDeadline. https://bugs.torproject.org/33144 --- common/websocketconn/websocketconn.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/common/websocketconn/websocketconn.go b/common/websocketconn/websocketconn.go index fa2b0da..73c2b25 100644 --- a/common/websocketconn/websocketconn.go +++ b/common/websocketconn/websocketconn.go @@ -7,29 +7,36 @@ import ( "github.com/gorilla/websocket" ) -// An abstraction that makes an underlying WebSocket connection look like an -// io.ReadWriteCloser. +// An abstraction that makes an underlying WebSocket connection look like a +// net.Conn. type Conn struct { - ws *websocket.Conn + *websocket.Conn Reader io.Reader Writer io.Writer } -// Implements io.Reader. func (conn *Conn) Read(b []byte) (n int, err error) { return conn.Reader.Read(b) } -// Implements io.Writer. func (conn *Conn) Write(b []byte) (n int, err error) { return conn.Writer.Write(b) } -// Implements io.Closer. func (conn *Conn) Close() error { // Ignore any error in trying to write a Close frame. - _ = conn.ws.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second)) - return conn.ws.Close() + _ = conn.Conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second)) + return conn.Conn.Close() +} + +func (conn *Conn) SetDeadline(t time.Time) error { + errRead := conn.Conn.SetReadDeadline(t) + errWrite := conn.Conn.SetWriteDeadline(t) + err := errRead + if err == nil { + err = errWrite + } + return err } func readLoop(w io.Writer, ws *websocket.Conn) error { @@ -105,7 +112,7 @@ func New(ws *websocket.Conn) *Conn { pr2.CloseWithError(closeErrorToEOF(writeLoop(ws, pr2))) }() return &Conn{ - ws: ws, + Conn: ws, Reader: pr1, Writer: pw2, } From ca9ae12c383405bc9a755e1bc902e9755495c1f1 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 4 Feb 2020 22:35:12 -0700 Subject: [PATCH 060/385] Simplify a conditional. --- server/server.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server/server.go b/server/server.go index 85bf4f5..c484a19 100644 --- a/server/server.go +++ b/server/server.go @@ -111,11 +111,7 @@ func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Pass the address of client as the remote address of incoming connection clientIPParam := r.URL.Query().Get("client_ip") addr := clientAddr(clientIPParam) - if addr == "" { - statsChannel <- false - } else { - statsChannel <- true - } + statsChannel <- addr != "" or, err := pt.DialOr(&ptInfo, addr, ptMethodName) if err != nil { log.Printf("failed to connect to ORPort: %s", err) From 28cf70bb444f0745d4a5221850ad213ca6799e7d Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Sat, 8 Feb 2020 10:12:43 -0500 Subject: [PATCH 061/385] Remove unreachable code go vet was complaining, common/websocketconn/websocketconn.go:56:2: unreachable code --- common/websocketconn/websocketconn.go | 1 - 1 file changed, 1 deletion(-) diff --git a/common/websocketconn/websocketconn.go b/common/websocketconn/websocketconn.go index 73c2b25..46bb977 100644 --- a/common/websocketconn/websocketconn.go +++ b/common/websocketconn/websocketconn.go @@ -53,7 +53,6 @@ func readLoop(w io.Writer, ws *websocket.Conn) error { return err } } - return nil } func writeLoop(ws *websocket.Conn, r io.Reader) error { From 0fae4ee8ea487c3b4384217e193e5b9a9088e7de Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Fri, 31 Jan 2020 00:17:50 -0500 Subject: [PATCH 062/385] Remove local LAN address ICE candidates Unfortunately, the "public" RTCIceTransportPolicy was removed. https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration#RTCIceTransportPolicy_enum Trac: 19026 --- client/lib/lib_test.go | 35 +++++++++++++++++----- client/lib/rendezvous.go | 63 ++++++++++++++++++++++++++++++++++++---- client/snowflake.go | 3 +- client/torrc-localhost | 1 + 4 files changed, 89 insertions(+), 13 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 91a9809..adfc9ec 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -288,7 +288,7 @@ func TestSnowflakeClient(t *testing.T) { fakeOffer := deserializeSessionDescription(`{"type":"offer","sdp":"test"}`) Convey("Construct BrokerChannel with no front domain", func() { - b, err := NewBrokerChannel("test.broker", "", transport) + b, err := NewBrokerChannel("test.broker", "", transport, false) So(b.url, ShouldNotBeNil) So(err, ShouldBeNil) So(b.url.Path, ShouldResemble, "test.broker") @@ -296,7 +296,7 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("Construct BrokerChannel *with* front domain", func() { - b, err := NewBrokerChannel("test.broker", "front", transport) + b, err := NewBrokerChannel("test.broker", "front", transport, false) So(b.url, ShouldNotBeNil) So(err, ShouldBeNil) So(b.url.Path, ShouldResemble, "test.broker") @@ -305,7 +305,7 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("BrokerChannel.Negotiate responds with answer", func() { - b, err := NewBrokerChannel("test.broker", "", transport) + b, err := NewBrokerChannel("test.broker", "", transport, false) So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldBeNil) @@ -315,7 +315,8 @@ func TestSnowflakeClient(t *testing.T) { Convey("BrokerChannel.Negotiate fails with 503", func() { b, err := NewBrokerChannel("test.broker", "", - &MockTransport{http.StatusServiceUnavailable, []byte("\n")}) + &MockTransport{http.StatusServiceUnavailable, []byte("\n")}, + false) So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldNotBeNil) @@ -325,7 +326,8 @@ func TestSnowflakeClient(t *testing.T) { Convey("BrokerChannel.Negotiate fails with 400", func() { b, err := NewBrokerChannel("test.broker", "", - &MockTransport{http.StatusBadRequest, []byte("\n")}) + &MockTransport{http.StatusBadRequest, []byte("\n")}, + false) So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldNotBeNil) @@ -335,7 +337,8 @@ func TestSnowflakeClient(t *testing.T) { Convey("BrokerChannel.Negotiate fails with large read", func() { b, err := NewBrokerChannel("test.broker", "", - &MockTransport{http.StatusOK, make([]byte, 100001, 100001)}) + &MockTransport{http.StatusOK, make([]byte, 100001, 100001)}, + false) So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldNotBeNil) @@ -345,7 +348,7 @@ func TestSnowflakeClient(t *testing.T) { Convey("BrokerChannel.Negotiate fails with unexpected error", func() { b, err := NewBrokerChannel("test.broker", "", - &MockTransport{123, []byte("")}) + &MockTransport{123, []byte("")}, false) So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldNotBeNil) @@ -353,4 +356,22 @@ func TestSnowflakeClient(t *testing.T) { So(err.Error(), ShouldResemble, BrokerErrorUnexpected) }) }) + + Convey("Strip", t, func() { + const offerStart = `{"type":"offer","sdp":"v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\n` + const goodCandidate = `a=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + const offerEnd = `a=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"}` + + offer := offerStart + goodCandidate + + `a=candidate:3769337065 1 udp 2122260223 192.168.0.100 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsLocal IPv4 + `a=candidate:3769337065 1 udp 2122260223 fdf8:f53b:82e4::53 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsLocal IPv6 + `a=candidate:3769337065 1 udp 2122260223 0.0.0.0 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsUnspecified IPv4 + `a=candidate:3769337065 1 udp 2122260223 :: 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsUnspecified IPv6 + `a=candidate:3769337065 1 udp 2122260223 127.0.0.1 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsLoopback IPv4 + `a=candidate:3769337065 1 udp 2122260223 ::1 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsLoopback IPv6 + offerEnd + + So(stripLocalAddresses(offer), ShouldEqual, offerStart+goodCandidate+offerEnd) + }) + } diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index d117ebc..bd8ff00 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -14,9 +14,12 @@ import ( "io" "io/ioutil" "log" + "net" "net/http" "net/url" + "regexp" + "github.com/pion/sdp" "github.com/pion/webrtc" ) @@ -31,9 +34,10 @@ const ( type BrokerChannel struct { // The Host header to put in the HTTP request (optional and may be // different from the host name in URL). - Host string - url *url.URL - transport http.RoundTripper // Used to make all requests. + Host string + url *url.URL + transport http.RoundTripper // Used to make all requests. + keepLocalAddresses bool } // We make a copy of DefaultTransport because we want the default Dial @@ -48,7 +52,7 @@ func CreateBrokerTransport() http.RoundTripper { // Construct a new BrokerChannel, where: // |broker| is the full URL of the facilitating program which assigns proxies // to clients, and |front| is the option fronting domain. -func NewBrokerChannel(broker string, front string, transport http.RoundTripper) (*BrokerChannel, error) { +func NewBrokerChannel(broker string, front string, transport http.RoundTripper, keepLocalAddresses bool) (*BrokerChannel, error) { targetURL, err := url.Parse(broker) if err != nil { return nil, err @@ -63,6 +67,7 @@ func NewBrokerChannel(broker string, front string, transport http.RoundTripper) } bc.transport = transport + bc.keepLocalAddresses = keepLocalAddresses return bc, nil } @@ -76,6 +81,41 @@ func limitedRead(r io.Reader, limit int64) ([]byte, error) { return p, err } +// Stolen from https://github.com/golang/go/pull/30278 +func IsLocal(ip net.IP) bool { + if ip4 := ip.To4(); ip4 != nil { + // Local IPv4 addresses are defined in https://tools.ietf.org/html/rfc1918 + return ip4[0] == 10 || + (ip4[0] == 172 && ip4[1]&0xf0 == 16) || + (ip4[0] == 192 && ip4[1] == 168) + } + // Local IPv6 addresses are defined in https://tools.ietf.org/html/rfc4193 + return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc +} + +// Removes local LAN address ICE candidates +func stripLocalAddresses(str string) string { + re := regexp.MustCompile(`a=candidate:.*?\\r\\n`) + return re.ReplaceAllStringFunc(str, func(s string) string { + t := s[len("a=candidate:") : len(s)-len("\\r\\n")] + var ice sdp.ICECandidate + err := ice.Unmarshal(t) + if err != nil { + return s + } + if ice.Typ == "host" { + ip := net.ParseIP(ice.Address) + if ip == nil { + return s + } + if IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback() { + return "" + } + } + return s + }) +} + // Roundtrip HTTP POST using WebRTC SessionDescriptions. // // Send an SDP offer to the broker, which assigns a proxy and responds @@ -84,7 +124,20 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( *webrtc.SessionDescription, error) { log.Println("Negotiating via BrokerChannel...\nTarget URL: ", bc.Host, "\nFront URL: ", bc.url.Host) - data := bytes.NewReader([]byte(serializeSessionDescription(offer))) + str := serializeSessionDescription(offer) + // Ideally, we could specify an `RTCIceTransportPolicy` that would handle + // this for us. However, "public" was removed from the draft spec. + // See https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration#RTCIceTransportPolicy_enum + // + // FIXME: We are stripping local addresses from the JSON serialized string, + // which is expedient but unsatisfying. We could advocate upstream to + // implement a non-standard ICE transport policy, or to somehow alter + // APIs to avoid adding the undesirable candidates or a method to filter + // them from the marshalled session description. + if !bc.keepLocalAddresses { + str = stripLocalAddresses(str) + } + data := bytes.NewReader([]byte(str)) // Suffix with broker's client registration handler. clientURL := bc.url.ResolveReference(&url.URL{Path: "client"}) request, err := http.NewRequest("POST", clientURL.String(), data) diff --git a/client/snowflake.go b/client/snowflake.go index edcbd4a..7cf8a9d 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -90,6 +90,7 @@ func main() { frontDomain := flag.String("front", "", "front domain") logFilename := flag.String("log", "", "name of log file") logToStateDir := flag.Bool("logToStateDir", false, "resolve the log file relative to tor's pt state dir") + keepLocalAddresses := flag.Bool("keepLocalAddresses", false, "keep local LAN address ICE candidates") max := flag.Int("max", DefaultSnowflakeCapacity, "capacity for number of multiplexed WebRTC peers") flag.Parse() @@ -133,7 +134,7 @@ func main() { snowflakes := sf.NewPeers(*max) // Use potentially domain-fronting broker to rendezvous. - broker, err := sf.NewBrokerChannel(*brokerURL, *frontDomain, sf.CreateBrokerTransport()) + broker, err := sf.NewBrokerChannel(*brokerURL, *frontDomain, sf.CreateBrokerTransport(), *keepLocalAddresses) if err != nil { log.Fatalf("parsing broker URL: %v", err) } diff --git a/client/torrc-localhost b/client/torrc-localhost index 7d539fb..9afb033 100644 --- a/client/torrc-localhost +++ b/client/torrc-localhost @@ -3,5 +3,6 @@ DataDirectory datadir ClientTransportPlugin snowflake exec ./client \ -url http://localhost:8080/ \ +-keepLocalAddresses Bridge snowflake 0.0.3.0:1 From 846473b3549c264245354a3117d87b96a945f287 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Fri, 7 Feb 2020 15:39:44 -0500 Subject: [PATCH 063/385] Unmarshal the SDP to filter attributes Instead of string manipulation. --- client/lib/lib_test.go | 18 +++++------ client/lib/rendezvous.go | 64 +++++++++++++++++++++++----------------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index adfc9ec..b413508 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -358,17 +358,17 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("Strip", t, func() { - const offerStart = `{"type":"offer","sdp":"v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\n` - const goodCandidate = `a=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\n` - const offerEnd = `a=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"}` + const offerStart = "v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\n" + const goodCandidate = "a=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + const offerEnd = "a=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n" offer := offerStart + goodCandidate + - `a=candidate:3769337065 1 udp 2122260223 192.168.0.100 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsLocal IPv4 - `a=candidate:3769337065 1 udp 2122260223 fdf8:f53b:82e4::53 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsLocal IPv6 - `a=candidate:3769337065 1 udp 2122260223 0.0.0.0 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsUnspecified IPv4 - `a=candidate:3769337065 1 udp 2122260223 :: 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsUnspecified IPv6 - `a=candidate:3769337065 1 udp 2122260223 127.0.0.1 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsLoopback IPv4 - `a=candidate:3769337065 1 udp 2122260223 ::1 56688 typ host generation 0 network-id 1 network-cost 50\r\n` + // IsLoopback IPv6 + "a=candidate:3769337065 1 udp 2122260223 192.168.0.100 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLocal IPv4 + "a=candidate:3769337065 1 udp 2122260223 fdf8:f53b:82e4::53 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLocal IPv6 + "a=candidate:3769337065 1 udp 2122260223 0.0.0.0 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsUnspecified IPv4 + "a=candidate:3769337065 1 udp 2122260223 :: 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsUnspecified IPv6 + "a=candidate:3769337065 1 udp 2122260223 127.0.0.1 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLoopback IPv4 + "a=candidate:3769337065 1 udp 2122260223 ::1 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLoopback IPv6 offerEnd So(stripLocalAddresses(offer), ShouldEqual, offerStart+goodCandidate+offerEnd) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index bd8ff00..190df66 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -17,7 +17,6 @@ import ( "net" "net/http" "net/url" - "regexp" "github.com/pion/sdp" "github.com/pion/webrtc" @@ -95,25 +94,40 @@ func IsLocal(ip net.IP) bool { // Removes local LAN address ICE candidates func stripLocalAddresses(str string) string { - re := regexp.MustCompile(`a=candidate:.*?\\r\\n`) - return re.ReplaceAllStringFunc(str, func(s string) string { - t := s[len("a=candidate:") : len(s)-len("\\r\\n")] - var ice sdp.ICECandidate - err := ice.Unmarshal(t) - if err != nil { - return s - } - if ice.Typ == "host" { - ip := net.ParseIP(ice.Address) - if ip == nil { - return s - } - if IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback() { - return "" + var desc sdp.SessionDescription + err := desc.Unmarshal([]byte(str)) + if err != nil { + return str + } + for _, m := range desc.MediaDescriptions { + attrs := make([]sdp.Attribute, 0) + for _, a := range m.Attributes { + if a.IsICECandidate() { + ice, err := a.ToICECandidate() + if err != nil { + attrs = append(attrs, a) + continue + } + if ice.Typ == "host" { + ip := net.ParseIP(ice.Address) + if ip == nil { + attrs = append(attrs, a) + continue + } + if IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback() { + continue + } + } } + attrs = append(attrs, a) } - return s - }) + m.Attributes = attrs + } + bts, err := desc.Marshal() + if err != nil { + return str + } + return string(bts) } // Roundtrip HTTP POST using WebRTC SessionDescriptions. @@ -124,20 +138,16 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( *webrtc.SessionDescription, error) { log.Println("Negotiating via BrokerChannel...\nTarget URL: ", bc.Host, "\nFront URL: ", bc.url.Host) - str := serializeSessionDescription(offer) // Ideally, we could specify an `RTCIceTransportPolicy` that would handle // this for us. However, "public" was removed from the draft spec. // See https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration#RTCIceTransportPolicy_enum - // - // FIXME: We are stripping local addresses from the JSON serialized string, - // which is expedient but unsatisfying. We could advocate upstream to - // implement a non-standard ICE transport policy, or to somehow alter - // APIs to avoid adding the undesirable candidates or a method to filter - // them from the marshalled session description. if !bc.keepLocalAddresses { - str = stripLocalAddresses(str) + offer = &webrtc.SessionDescription{ + Type: offer.Type, + SDP: stripLocalAddresses(offer.SDP), + } } - data := bytes.NewReader([]byte(str)) + data := bytes.NewReader([]byte(serializeSessionDescription(offer))) // Suffix with broker's client registration handler. clientURL := bc.url.ResolveReference(&url.URL{Path: "client"}) request, err := http.NewRequest("POST", clientURL.String(), data) From 1220853a67c8bd09c171c49097944e120ab8c9d4 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Fri, 7 Feb 2020 19:02:53 -0500 Subject: [PATCH 064/385] Restructure a bit based on review --- client/lib/rendezvous.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 190df66..17c024f 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -104,17 +104,10 @@ func stripLocalAddresses(str string) string { for _, a := range m.Attributes { if a.IsICECandidate() { ice, err := a.ToICECandidate() - if err != nil { - attrs = append(attrs, a) - continue - } - if ice.Typ == "host" { + if err == nil && ice.Typ == "host" { ip := net.ParseIP(ice.Address) - if ip == nil { - attrs = append(attrs, a) - continue - } - if IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback() { + if ip != nil && (IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback()) { + /* no append in this case */ continue } } From 380b133155ad725126bc418d0e66b3c550b4c555 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 18 Feb 2020 14:10:47 -0700 Subject: [PATCH 065/385] Close internal Pipes in websocketconn.Conn Close. Unless something externally called Write after Close, the writeLoop(ws, pr2) goroutine would run forever, because nothing would ever close pw2/pr2. https://bugs.torproject.org/33367#comment:4 --- common/websocketconn/websocketconn.go | 2 ++ common/websocketconn/websocketconn_test.go | 28 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/common/websocketconn/websocketconn.go b/common/websocketconn/websocketconn.go index 46bb977..c745522 100644 --- a/common/websocketconn/websocketconn.go +++ b/common/websocketconn/websocketconn.go @@ -24,6 +24,8 @@ func (conn *Conn) Write(b []byte) (n int, err error) { } func (conn *Conn) Close() error { + conn.Reader.(*io.PipeReader).Close() + conn.Writer.(*io.PipeWriter).Close() // Ignore any error in trying to write a Close frame. _ = conn.Conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(time.Second)) return conn.Conn.Close() diff --git a/common/websocketconn/websocketconn_test.go b/common/websocketconn/websocketconn_test.go index ad6f100..92774d4 100644 --- a/common/websocketconn/websocketconn_test.go +++ b/common/websocketconn/websocketconn_test.go @@ -233,3 +233,31 @@ func TestConcurrentWrite(t *testing.T) { t.Fatalf("Write: %v", err) } } + +// Test that Read and Write methods return errors after Close. +func TestClose(t *testing.T) { + s, c, err := connPair() + if err != nil { + t.Fatal(err) + } + defer c.Close() + + err = s.Close() + if err != nil { + t.Fatal(err) + } + + var buf [10]byte + n, err := s.Read(buf[:]) + if n != 0 || err == nil { + t.Fatalf("Read after Close returned (%v, %v), expected (%v, non-nil)", n, err, 0) + } + + _, err = s.Write([]byte{1, 2, 3}) + // Here we break the abstraction a little and look for a specific error, + // io.ErrClosedPipe. This is because we know the Conn uses an io.Pipe + // internally. + if err != io.ErrClosedPipe { + t.Fatalf("Write after Close returned %v, expected %v", err, io.ErrClosedPipe) + } +} From c124e8c643f2de5730af9079d326b9cabc3b264a Mon Sep 17 00:00:00 2001 From: David Fifield Date: Wed, 19 Feb 2020 10:44:35 -0700 Subject: [PATCH 066/385] In server, treat a client IP address of 0.0.0.0 as missing. Some proxies currently send ?client_ip=0.0.0.0 because of an error in how they attempt to grep the address from the client's SDP. That's inflating our "%d/%d connections had client_ip" logs. Instead, treat these cases as if the IP address were absent. https://bugs.torproject.org/33157 https://bugs.torproject.org/33385 --- server/server.go | 5 +++++ server/server_test.go | 2 ++ 2 files changed, 7 insertions(+) diff --git a/server/server.go b/server/server.go index c484a19..6e9fb19 100644 --- a/server/server.go +++ b/server/server.go @@ -88,6 +88,11 @@ func clientAddr(clientIPParam string) string { if clientIP == nil { return "" } + // Check if client addr is 0.0.0.0 or [::]. Some proxies erroneously + // report an address of 0.0.0.0: https://bugs.torproject.org/33157. + if clientIP.IsUnspecified() { + return "" + } // Add a dummy port number. USERADDR requires a port number. return (&net.TCPAddr{IP: clientIP, Port: 1, Zone: ""}).String() } diff --git a/server/server_test.go b/server/server_test.go index d4ada6e..ba00d16 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -46,6 +46,8 @@ func TestClientAddr(t *testing.T) { "abc", "1.2.3.4.5", "[12::34]", + "0.0.0.0", + "[::]", } { useraddr := clientAddr(input) if useraddr != "" { From c2a12c25d1dd740b055aff736379a4a0c45b51d6 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Mon, 24 Feb 2020 00:15:54 -0700 Subject: [PATCH 067/385] Update appengine for the Go 1.11 runtime. https://cloud.google.com/appengine/docs/standard/go111/go-differences This is untested, because I wasn't actually able to deploy without enabling Cloud Build and setting up a billing account. --- appengine/.gcloudignore | 25 +++++++++++++++++++++++++ appengine/app.yaml | 5 ++--- appengine/reflect.go | 2 +- 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 appengine/.gcloudignore diff --git a/appengine/.gcloudignore b/appengine/.gcloudignore new file mode 100644 index 0000000..199e6d9 --- /dev/null +++ b/appengine/.gcloudignore @@ -0,0 +1,25 @@ +# This file specifies files that are *not* uploaded to Google Cloud Platform +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +# Test binary, build with `go test -c` +*.test +# Output of the go coverage tool, specifically when used with LiteIDE +*.out \ No newline at end of file diff --git a/appengine/app.yaml b/appengine/app.yaml index 44df436..5d0dcf0 100644 --- a/appengine/app.yaml +++ b/appengine/app.yaml @@ -1,7 +1,6 @@ -runtime: go -api_version: go1 +runtime: go111 handlers: - url: /.* - script: _go_app secure: always + script: auto diff --git a/appengine/reflect.go b/appengine/reflect.go index 58d8a67..e09c09e 100644 --- a/appengine/reflect.go +++ b/appengine/reflect.go @@ -1,6 +1,6 @@ // A web app for Google App Engine that proxies HTTP requests and responses to // the Snowflake broker. -package reflect +package main import ( "context" From 2e9e8071787398d9713395d94d93b5427ac8d452 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 19 Feb 2020 17:26:29 -0500 Subject: [PATCH 068/385] Remove unecessary log messages Ever since we started scrubbing log messages, with the help of regexes for https://bugs.torproject.org/21304 logging has become more CPU intensive due to our use of regular expressions. Logging the byte count of every incoming and outgoing message at the proxy-go instances was taking up a lot of CPU and contrubuting to the high CPU usage seen in https://bugs.torproject.org/33211. --- proxy-go/snowflake.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index e964a07..5e56842 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -90,8 +90,6 @@ func (c *webRTCConn) Read(b []byte) (int, error) { func (c *webRTCConn) Write(b []byte) (int, error) { c.lock.Lock() defer c.lock.Unlock() - // log.Printf("webrtc Write %d %+q", len(b), string(b)) - log.Printf("Write %d bytes --> WebRTC", len(b)) if c.dc != nil { c.dc.Send(b) } @@ -320,7 +318,6 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, config webrtc.C pw.Close() }) dc.OnMessage(func(msg webrtc.DataChannelMessage) { - log.Printf("OnMessage <--- %d bytes", len(msg.Data)) var n int n, err = pw.Write(msg.Data) if err != nil { From 125e71fa6ee27a929438dafc82bba1ddc0172814 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sat, 29 Feb 2020 17:29:28 -0700 Subject: [PATCH 069/385] Remove the now-unused appengine directory. https://bugs.torproject.org/33429 --- .travis.yml | 1 - appengine/.gcloudignore | 25 --------- appengine/README | 28 ---------- appengine/app.yaml | 6 --- appengine/reflect.go | 111 ---------------------------------------- 5 files changed, 171 deletions(-) delete mode 100644 appengine/.gcloudignore delete mode 100644 appengine/README delete mode 100644 appengine/app.yaml delete mode 100644 appengine/reflect.go diff --git a/.travis.yml b/.travis.yml index b986122..39e777a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,6 @@ install: - go get -u github.com/dchest/uniuri - go get -u github.com/gorilla/websocket - go get -u git.torproject.org/pluggable-transports/goptlib.git - - go get -u google.golang.org/appengine - go get -u golang.org/x/crypto/acme/autocert - go get -u golang.org/x/net/http2 - pushd proxy diff --git a/appengine/.gcloudignore b/appengine/.gcloudignore deleted file mode 100644 index 199e6d9..0000000 --- a/appengine/.gcloudignore +++ /dev/null @@ -1,25 +0,0 @@ -# This file specifies files that are *not* uploaded to Google Cloud Platform -# using gcloud. It follows the same syntax as .gitignore, with the addition of -# "#!include" directives (which insert the entries of the given .gitignore-style -# file at that point). -# -# For more information, run: -# $ gcloud topic gcloudignore -# -.gcloudignore -# If you would like to upload your .git directory, .gitignore file or files -# from your .gitignore file, remove the corresponding line -# below: -.git -.gitignore - -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib -# Test binary, build with `go test -c` -*.test -# Output of the go coverage tool, specifically when used with LiteIDE -*.out \ No newline at end of file diff --git a/appengine/README b/appengine/README deleted file mode 100644 index 797b0f9..0000000 --- a/appengine/README +++ /dev/null @@ -1,28 +0,0 @@ -This component runs on Google App Engine. It reflects domain-fronted -requests from a client to the Snowflake broker. - -You need the Go App Engine SDK in order to deploy the app. - https://cloud.google.com/sdk/docs/#linux -After unpacking, install the app-engine-go component: - google-cloud-sdk/bin/gcloud components install app-engine-go - -To test locally, run - google-cloud-sdk/bin/dev_appserver.py app.yaml -The app will be running at http://127.0.0.1:8080/. - -To deploy to App Engine, first create a new project and app. You have to -think of a unique name (marked as "" in the commands). You only -have to do the "create" step once; subsequent times you can go straight -to the "deploy" step. The "gcloud auth login" command will open a -browser window so you can log in to a Google account. - google-cloud-sdk/bin/gcloud auth login - google-cloud-sdk/bin/gcloud projects create - google-cloud-sdk/bin/gcloud app create --project= -Then to deploy the project, run: - google-cloud-sdk/bin/gcloud app deploy --project= - -To configure the Snowflake client to talk to the App Engine app, provide -"https://.appspot.com/" as the --url option. - UseBridges 1 - Bridge snowflake 0.0.2.0:1 - ClientTransportPlugin snowflake exec ./client -url https://.appspot.com/ -front www.google.com diff --git a/appengine/app.yaml b/appengine/app.yaml deleted file mode 100644 index 5d0dcf0..0000000 --- a/appengine/app.yaml +++ /dev/null @@ -1,6 +0,0 @@ -runtime: go111 - -handlers: -- url: /.* - secure: always - script: auto diff --git a/appengine/reflect.go b/appengine/reflect.go deleted file mode 100644 index e09c09e..0000000 --- a/appengine/reflect.go +++ /dev/null @@ -1,111 +0,0 @@ -// A web app for Google App Engine that proxies HTTP requests and responses to -// the Snowflake broker. -package main - -import ( - "context" - "io" - "net/http" - "net/url" - "time" - - "google.golang.org/appengine" - "google.golang.org/appengine/log" - "google.golang.org/appengine/urlfetch" -) - -const ( - forwardURL = "https://snowflake-broker.bamsoftware.com/" - // A timeout of 0 means to use the App Engine default (5 seconds). - urlFetchTimeout = 20 * time.Second -) - -var ctx context.Context - -// Join two URL paths. -func pathJoin(a, b string) string { - if len(a) > 0 && a[len(a)-1] == '/' { - a = a[:len(a)-1] - } - if len(b) == 0 || b[0] != '/' { - b = "/" + b - } - return a + b -} - -// We reflect only a whitelisted set of header fields. Otherwise, we may copy -// headers like Transfer-Encoding that interfere with App Engine's own -// hop-by-hop headers. -var reflectedHeaderFields = []string{ - "Content-Type", - "X-Session-Id", -} - -// Make a copy of r, with the URL being changed to be relative to forwardURL, -// and including only the headers in reflectedHeaderFields. -func copyRequest(r *http.Request) (*http.Request, error) { - u, err := url.Parse(forwardURL) - if err != nil { - return nil, err - } - // Append the requested path to the path in forwardURL, so that - // forwardURL can be something like "https://example.com/reflect". - u.Path = pathJoin(u.Path, r.URL.Path) - c, err := http.NewRequest(r.Method, u.String(), r.Body) - if err != nil { - return nil, err - } - for _, key := range reflectedHeaderFields { - values, ok := r.Header[key] - if ok { - for _, value := range values { - c.Header.Add(key, value) - } - } - } - return c, nil -} - -func handler(w http.ResponseWriter, r *http.Request) { - ctx = appengine.NewContext(r) - fr, err := copyRequest(r) - if err != nil { - log.Errorf(ctx, "copyRequest: %s", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if urlFetchTimeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, urlFetchTimeout) - defer cancel() - } - // Use urlfetch.Transport directly instead of urlfetch.Client because we - // want only a single HTTP transaction, not following redirects. - transport := urlfetch.Transport{ - Context: ctx, - } - resp, err := transport.RoundTrip(fr) - if err != nil { - log.Errorf(ctx, "RoundTrip: %s", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer resp.Body.Close() - for _, key := range reflectedHeaderFields { - values, ok := resp.Header[key] - if ok { - for _, value := range values { - w.Header().Add(key, value) - } - } - } - w.WriteHeader(resp.StatusCode) - n, err := io.Copy(w, resp.Body) - if err != nil { - log.Errorf(ctx, "io.Copy after %d bytes: %s", n, err) - } -} - -func init() { - http.HandleFunc("/", handler) -} From 03315dde029d37d93f894c6ae4932851fd1460a1 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 4 Mar 2020 16:20:34 -0500 Subject: [PATCH 070/385] bump version to 0.2.2 --- proxy/translation | 2 +- proxy/webext/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/translation b/proxy/translation index 2d92ad1..9ce163a 160000 --- a/proxy/translation +++ b/proxy/translation @@ -1 +1 @@ -Subproject commit 2d92ad194c9d54bd7164b680f5c41c80e1aab87d +Subproject commit 9ce163a282fcd35e3cc6b642b0236e237dc56159 diff --git a/proxy/webext/manifest.json b/proxy/webext/manifest.json index 9578bc0..c3ccfa7 100644 --- a/proxy/webext/manifest.json +++ b/proxy/webext/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Snowflake", - "version": "0.2.1", + "version": "0.2.2", "description": "__MSG_appDesc__", "default_locale": "en_US", "background": { From 920f6791f3ec8e7467c43ee0cefffe63200bed2b Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 2 Mar 2020 14:16:19 -0500 Subject: [PATCH 071/385] Add a go.mod and go.sum for snowflake --- client/lib/lib_test.go | 2 +- client/lib/rendezvous.go | 4 +- client/lib/util.go | 2 +- client/lib/webrtc.go | 2 +- client/snowflake.go | 2 +- go.mod | 16 +++++ go.sum | 126 ++++++++++++++++++++++++++++++++++++++ proxy-go/proxy-go_test.go | 2 +- proxy-go/snowflake.go | 2 +- 9 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 go.mod create mode 100644 go.sum diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index b413508..a0e77cb 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -9,7 +9,7 @@ import ( "sync" "testing" - "github.com/pion/webrtc" + "github.com/pion/webrtc/v2" . "github.com/smartystreets/goconvey/convey" ) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 17c024f..d35c813 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -18,8 +18,8 @@ import ( "net/http" "net/url" - "github.com/pion/sdp" - "github.com/pion/webrtc" + "github.com/pion/sdp/v2" + "github.com/pion/webrtc/v2" ) const ( diff --git a/client/lib/util.go b/client/lib/util.go index f93bcbe..1b5f592 100644 --- a/client/lib/util.go +++ b/client/lib/util.go @@ -5,7 +5,7 @@ import ( "log" "time" - "github.com/pion/webrtc" + "github.com/pion/webrtc/v2" ) const ( diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 8e06d98..0b3bb16 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -9,7 +9,7 @@ import ( "time" "github.com/dchest/uniuri" - "github.com/pion/webrtc" + "github.com/pion/webrtc/v2" ) // Remote WebRTC peer. diff --git a/client/snowflake.go b/client/snowflake.go index 7cf8a9d..4076fff 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -17,7 +17,7 @@ import ( pt "git.torproject.org/pluggable-transports/goptlib.git" sf "git.torproject.org/pluggable-transports/snowflake.git/client/lib" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" - "github.com/pion/webrtc" + "github.com/pion/webrtc/v2" ) const ( diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d01fb0b --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module git.torproject.org/pluggable-transports/snowflake.git + +go 1.13 + +require ( + git.torproject.org/pluggable-transports/goptlib.git v1.1.0 + github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 + github.com/gorilla/websocket v1.4.1 + github.com/keroserene/go-webrtc v0.0.0-20190528223128-68a6fb1b4353 + github.com/pion/sdp/v2 v2.3.4 + github.com/pion/webrtc/v2 v2.2.2 + github.com/smartystreets/goconvey v1.6.4 + golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d + golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa + google.golang.org/appengine v1.6.5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a62b8a9 --- /dev/null +++ b/go.sum @@ -0,0 +1,126 @@ +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= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= +github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/keroserene/go-webrtc v0.0.0-20190528223128-68a6fb1b4353 h1:UVuvNMp4EiqRl2jDS0MchQmfolWCGzG9+y6vBy9S/aw= +github.com/keroserene/go-webrtc v0.0.0-20190528223128-68a6fb1b4353/go.mod h1:Wl6nMWlHBurzYryPggNZfgTsvRaAH2P3Q6alLg3VxLA= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc= +github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= +github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA= +github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pion/datachannel v1.4.15 h1:DrizUL97f9evDyoskyMLrFFFmOWCfXFGiGSbxmQyYt4= +github.com/pion/datachannel v1.4.15/go.mod h1:yixWvOWPime7vRVuihP1GzZPBELQkO/ZM1mrBc2BNM8= +github.com/pion/dtls/v2 v2.0.0-rc.7 h1:LDAIQDt1pcuAIJs7Q2EZ3PSl8MseCFA2nCW0YYSYCx0= +github.com/pion/dtls/v2 v2.0.0-rc.7/go.mod h1:U199DvHpRBN0muE9+tVN4TMy1jvEhZIZ63lk4xkvVSk= +github.com/pion/ice v0.7.9 h1:RKol/0RFu3TIE8ZLIFV1A1e/QW22B6BZKvSG9sfawEM= +github.com/pion/ice v0.7.9/go.mod h1:8BCwuq/EqAKhtUb8CIw2fWjVLotWOu13XJY09H3RVxA= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= +github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= +github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA= +github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k= +github.com/pion/rtcp v1.2.1 h1:S3yG4KpYAiSmBVqKAfgRa5JdwBNj4zK3RLUa8JYdhak= +github.com/pion/rtcp v1.2.1/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM= +github.com/pion/rtp v1.3.0/go.mod h1:q9wPnA96pu2urCcW/sK/RiDn597bhGoAQQ+y2fDwHuY= +github.com/pion/rtp v1.3.2 h1:Yfzf1mU4Zmg7XWHitzYe2i+l+c68iO+wshzIUW44p1c= +github.com/pion/rtp v1.3.2/go.mod h1:q9wPnA96pu2urCcW/sK/RiDn597bhGoAQQ+y2fDwHuY= +github.com/pion/sctp v1.7.5 h1:ognJDlxP7dN2xMUEHEea5pqjdD78o5UAMcLoP1JIp1g= +github.com/pion/sctp v1.7.5/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8= +github.com/pion/sdp/v2 v2.3.4 h1:+f3F5Xl7ynVhc9Il8Dc7BFroYJWG3PMbfWtwFlVI+kg= +github.com/pion/sdp/v2 v2.3.4/go.mod h1:jccXVYW0fuK6ds2pwKr89SVBDYlCjhgMI6nucl5R5rA= +github.com/pion/srtp v1.2.7 h1:UYyLs5MXwbFtXWduBA5+RUWhaEBX7GmetXDZSKP+uPM= +github.com/pion/srtp v1.2.7/go.mod h1:KIgLSadhg/ioogO/LqIkRjZrwuJo0c9RvKIaGQj4Yew= +github.com/pion/stun v0.3.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw= +github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= +github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= +github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk= +github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= +github.com/pion/turn/v2 v2.0.3 h1:SJUUIbcPoehlyZgMyIUbBBDhI03sBx32x3JuSIBKBWA= +github.com/pion/turn/v2 v2.0.3/go.mod h1:kl1hmT3NxcLynpXVnwJgObL8C9NaCyPTeqI2DcCpSZs= +github.com/pion/webrtc/v2 v2.2.2 h1:ace9itTe8YND8m3lv5ndQurfk/DsChj+4pBzVJeBA04= +github.com/pion/webrtc/v2 v2.2.2/go.mod h1:oftEPcdfIvZVC1J0VP1OpyVCwB9tDkRXSYAszkL/2k4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U= +golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/proxy-go/proxy-go_test.go b/proxy-go/proxy-go_test.go index ebe4381..2429d1e 100644 --- a/proxy-go/proxy-go_test.go +++ b/proxy-go/proxy-go_test.go @@ -13,7 +13,7 @@ import ( "testing" "git.torproject.org/pluggable-transports/snowflake.git/common/messages" - "github.com/pion/webrtc" + "github.com/pion/webrtc/v2" . "github.com/smartystreets/goconvey/convey" ) diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index 5e56842..0b91059 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -23,7 +23,7 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" "github.com/gorilla/websocket" - "github.com/pion/webrtc" + "github.com/pion/webrtc/v2" ) const defaultBrokerURL = "https://snowflake-broker.bamsoftware.com/" From 58b52eb9f7987789d8e90a982a6df16013e200dd Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 2 Mar 2020 14:22:42 -0500 Subject: [PATCH 072/385] Remove go get commands from travis.yml We no longer need standalone get commands now that we are using go modules. --- .travis.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 39e777a..56d612c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,14 +22,6 @@ before_install: - nvm install $TRAVIS_NODE_VERSION install: - - go get -u github.com/smartystreets/goconvey - - go get -u github.com/keroserene/go-webrtc - - go get -u github.com/pion/webrtc - - go get -u github.com/dchest/uniuri - - go get -u github.com/gorilla/websocket - - go get -u git.torproject.org/pluggable-transports/goptlib.git - - go get -u golang.org/x/crypto/acme/autocert - - go get -u golang.org/x/net/http2 - pushd proxy - npm install - popd From 6054c09949dfeb807a898e369c546344e26373dd Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 17 Mar 2020 12:36:12 -0400 Subject: [PATCH 073/385] Remove the abandoned server-webrtc test code This existed solely for testing purposes and is no longer being maintained. --- README.md | 7 +- server-webrtc/README.md | 26 ---- server-webrtc/http.go | 74 ----------- server-webrtc/snowflake.go | 255 ------------------------------------- server-webrtc/torrc | 8 -- 5 files changed, 1 insertion(+), 369 deletions(-) delete mode 100644 server-webrtc/README.md delete mode 100644 server-webrtc/http.go delete mode 100644 server-webrtc/snowflake.go delete mode 100644 server-webrtc/torrc diff --git a/README.md b/README.md index fdb039c..05fb5f7 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Pluggable Transport using WebRTC, inspired by Flashproxy. - [Test Environment](#test-environment) - [FAQ](#faq) - [Appendix](#appendix) - - [-- Testing directly via WebRTC Server --](#---testing-directly-via-webrtc-server---) + - [-- Testing with Standalone Proxy --](#---testing-with-standalone-proxy---) @@ -142,11 +142,6 @@ go build ./proxy-go ``` -##### -- Testing directly via WebRTC Server -- - -See server-webrtc/README.md for information on connecting directly to a -WebRTC server transport plugin, bypassing the Broker and browser proxy. - More documentation on the way. Also available at: diff --git a/server-webrtc/README.md b/server-webrtc/README.md deleted file mode 100644 index 53cad14..0000000 --- a/server-webrtc/README.md +++ /dev/null @@ -1,26 +0,0 @@ -Ordinarily, the WebRTC client plugin speaks with a Broker which helps -match and signal with a browser proxy, which ultimately speaks with a default -websocket server. - - -However, this directory contains a WebRTC server plugin which uses an -HTTP server that simulates the interaction that a client would have with -the broker, for direct testing. - -Edit server-webrtc/torrc and add "-http 127.0.0.1:8080" to the end of the -ServerTransportPlugin line: -``` -ServerTransportPlugin snowflake exec ./server-webrtc -http 127.0.0.1:8080 -``` - -``` -cd server-webrtc/ -go build -tor -f torrc -``` - -Edit client/torrc and add "-url http://127.0.0.1:8080" to the end of the -ClientTransportPlugin line: -``` -ClientTransportPlugin snowflake exec ./client -url http://127.0.0.1:8080/ -``` diff --git a/server-webrtc/http.go b/server-webrtc/http.go deleted file mode 100644 index 1232239..0000000 --- a/server-webrtc/http.go +++ /dev/null @@ -1,74 +0,0 @@ -// An HTTP-based signaling channel for the WebRTC server. It imitates the -// broker as seen by clients, but it doesn't connect them to an -// intermediate WebRTC proxy, rather connects them directly to this WebRTC -// server. This code should be deleted when we have proxies in place. - -package main - -import ( - "fmt" - "io/ioutil" - "log" - "net/http" - - "github.com/keroserene/go-webrtc" -) - -type httpHandler struct { - config *webrtc.Configuration -} - -func (h *httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - switch req.Method { - case "GET": - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte(`HTTP signaling channel - -Send a POST request containing an SDP offer. The response will -contain an SDP answer. -`)) - if err != nil { - log.Printf("GET request write failed with error: %v", err) - } - return - case "POST": - break - default: - http.Error(w, "Bad request.", http.StatusBadRequest) - return - } - - // POST handling begins here. - body, err := ioutil.ReadAll(http.MaxBytesReader(w, req.Body, 100000)) - if err != nil { - http.Error(w, "Bad request.", http.StatusBadRequest) - return - } - offer := webrtc.DeserializeSessionDescription(string(body)) - if offer == nil { - http.Error(w, "Bad request.", http.StatusBadRequest) - return - } - - pc, err := makePeerConnectionFromOffer(offer, h.config) - if err != nil { - http.Error(w, fmt.Sprintf("Cannot create offer: %s", err), http.StatusInternalServerError) - return - } - - log.Println("answering HTTP POST") - - w.WriteHeader(http.StatusOK) - _, err = w.Write([]byte(pc.LocalDescription().Serialize())) - if err != nil { - log.Printf("answering HTTP POST write failed with error %v", err) - } - -} - -func receiveSignalsHTTP(addr string, config *webrtc.Configuration) error { - http.Handle("/", &httpHandler{config}) - log.Printf("listening HTTP on %s", addr) - return http.ListenAndServe(addr, nil) -} diff --git a/server-webrtc/snowflake.go b/server-webrtc/snowflake.go deleted file mode 100644 index 9ca82cb..0000000 --- a/server-webrtc/snowflake.go +++ /dev/null @@ -1,255 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "io" - "io/ioutil" - "log" - "net" - "os" - "os/signal" - "sync" - "syscall" - "time" - - pt "git.torproject.org/pluggable-transports/goptlib.git" - "github.com/keroserene/go-webrtc" -) - -var ptMethodName = "snowflake" -var ptInfo pt.ServerInfo -var logFile *os.File - -func copyLoop(webRTC, orPort net.Conn) { - var wg sync.WaitGroup - wg.Add(2) - go func() { - if _, err := io.Copy(orPort, webRTC); err != nil { - log.Printf("copy WebRTC to ORPort error in copyLoop: %v", err) - } - wg.Done() - }() - go func() { - if _, err := io.Copy(webRTC, orPort); err != nil { - log.Printf("copy ORPort to WebRTC error in copyLoop: %v", err) - } - wg.Done() - }() - wg.Wait() -} - -type webRTCConn struct { - dc *webrtc.DataChannel - pc *webrtc.PeerConnection - pr *io.PipeReader - - lock sync.Mutex // Synchronization for DataChannel destruction - once sync.Once // Synchronization for PeerConnection destruction -} - -func (c *webRTCConn) Read(b []byte) (int, error) { - return c.pr.Read(b) -} - -func (c *webRTCConn) Write(b []byte) (int, error) { - c.lock.Lock() - defer c.lock.Unlock() - // log.Printf("webrtc Write %d %+q", len(b), string(b)) - log.Printf("Write %d bytes --> WebRTC", len(b)) - if c.dc != nil { - c.dc.Send(b) - } - return len(b), nil -} - -func (c *webRTCConn) Close() (err error) { - c.once.Do(func() { - err = c.pc.Destroy() - }) - return -} - -func (c *webRTCConn) LocalAddr() net.Addr { - return nil -} - -func (c *webRTCConn) RemoteAddr() net.Addr { - return nil -} - -func (c *webRTCConn) SetDeadline(t time.Time) error { - // nolint:golint - return fmt.Errorf("SetDeadline not implemented") -} - -func (c *webRTCConn) SetReadDeadline(t time.Time) error { - // nolint:golint - return fmt.Errorf("SetReadDeadline not implemented") -} - -func (c *webRTCConn) SetWriteDeadline(t time.Time) error { - // nolint:golint - return fmt.Errorf("SetWriteDeadline not implemented") -} - -func datachannelHandler(conn *webRTCConn) { - defer conn.Close() - - or, err := pt.DialOr(&ptInfo, "", ptMethodName) // TODO: Extended OR - if err != nil { - log.Printf("Failed to connect to ORPort: " + err.Error()) - return - } - defer or.Close() - - copyLoop(conn, or) -} - -// Create a PeerConnection from an SDP offer. Blocks until the gathering of ICE -// candidates is complete and the answer is available in LocalDescription. -// Installs an OnDataChannel callback that creates a webRTCConn and passes it to -// datachannelHandler. -func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, config *webrtc.Configuration) (*webrtc.PeerConnection, error) { - pc, err := webrtc.NewPeerConnection(config) - if err != nil { - return nil, fmt.Errorf("accept: NewPeerConnection: %s", err) - } - pc.OnNegotiationNeeded = func() { - panic("OnNegotiationNeeded") - } - pc.OnDataChannel = func(dc *webrtc.DataChannel) { - log.Println("OnDataChannel") - - pr, pw := io.Pipe() - conn := &webRTCConn{pc: pc, dc: dc, pr: pr} - - dc.OnOpen = func() { - log.Println("OnOpen channel") - } - dc.OnClose = func() { - conn.lock.Lock() - defer conn.lock.Unlock() - log.Println("OnClose channel") - conn.dc = nil - pc.DeleteDataChannel(dc) - pw.Close() - } - dc.OnMessage = func(msg []byte) { - log.Printf("OnMessage <--- %d bytes", len(msg)) - var n int - n, err = pw.Write(msg) - if err != nil { - if inerr := pw.CloseWithError(err); inerr != nil { - log.Printf("close with error returned error: %v", inerr) - } - } - if n != len(msg) { - panic("short write") - } - } - - go datachannelHandler(conn) - } - - err = pc.SetRemoteDescription(sdp) - if err != nil { - if err = pc.Destroy(); err != nil { - log.Printf("pc.Destroy returned an error: %v", err) - } - return nil, fmt.Errorf("accept: SetRemoteDescription: %s", err) - } - log.Println("sdp offer successfully received.") - - log.Println("Generating answer...") - answer, err := pc.CreateAnswer() - if err != nil { - if err = pc.Destroy(); err != nil { - log.Printf("pc.Destroy returned an error: %v", err) - } - return nil, err - } - - if answer == nil { - if err = pc.Destroy(); err != nil { - log.Printf("pc.Destroy returned an error: %v", err) - } - return nil, fmt.Errorf("failed gathering ICE candidates") - } - - err = pc.SetLocalDescription(answer) - if err != nil { - if err = pc.Destroy(); err != nil { - log.Printf("pc.Destroy returned an error: %v", err) - } - return nil, err - } - - return pc, nil -} - -func main() { - var httpAddr string - var logFilename string - - flag.StringVar(&httpAddr, "http", "", "listen for HTTP signaling") - flag.StringVar(&logFilename, "log", "", "log file to write to") - flag.Parse() - - log.SetFlags(log.LstdFlags | log.LUTC) - if logFilename != "" { - f, err := os.OpenFile(logFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) - if err != nil { - log.Fatalf("can't open log file: %s", err) - } - defer logFile.Close() - log.SetOutput(f) - } - - log.Println("starting") - webrtc.SetLoggingVerbosity(1) - var err error - ptInfo, err = pt.ServerSetup(nil) - if err != nil { - log.Fatal(err) - } - - webRTCConfig := webrtc.NewConfiguration(webrtc.OptionIceServer("stun:stun.l.google.com:19302")) - - // Start HTTP-based signaling receiver. - go func() { - err := receiveSignalsHTTP(httpAddr, webRTCConfig) - if err != nil { - log.Printf("receiveSignalsHTTP: %s", err) - } - }() - - for _, bindaddr := range ptInfo.Bindaddrs { - switch bindaddr.MethodName { - case ptMethodName: - bindaddr.Addr.Port = 12345 // lies!!! - pt.Smethod(bindaddr.MethodName, bindaddr.Addr) - default: - pt.SmethodError(bindaddr.MethodName, "no such method") - } - } - pt.SmethodsDone() - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGTERM) - - if os.Getenv("TOR_PT_EXIT_ON_STDIN_CLOSE") == "1" { - // This environment variable means we should treat EOF on stdin - // just like SIGTERM: https://bugs.torproject.org/15435. - go func() { - if _, err := io.Copy(ioutil.Discard, os.Stdin); err != nil { - log.Printf("error copying os.Stdin to ioutil.Discard: %v", err) - } - log.Printf("synthesizing SIGTERM because of stdin close") - sigChan <- syscall.SIGTERM - }() - } - - // wait for a signal - <-sigChan -} diff --git a/server-webrtc/torrc b/server-webrtc/torrc deleted file mode 100644 index e037c97..0000000 --- a/server-webrtc/torrc +++ /dev/null @@ -1,8 +0,0 @@ -BridgeRelay 1 -ORPort 9001 -ExtORPort auto -SocksPort 0 -ExitPolicy reject *:* -DataDirectory datadir - -ServerTransportPlugin snowflake exec ./server-webrtc From c11461d3391febd62ba5f7fb5517aa65dbcf5c59 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 17 Mar 2020 14:22:20 -0400 Subject: [PATCH 074/385] Update go.mod and go.sum --- go.mod | 4 ++-- go.sum | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index d01fb0b..b203976 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,12 @@ go 1.13 require ( git.torproject.org/pluggable-transports/goptlib.git v1.1.0 github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 + github.com/golang/protobuf v1.3.1 // indirect github.com/gorilla/websocket v1.4.1 - github.com/keroserene/go-webrtc v0.0.0-20190528223128-68a6fb1b4353 github.com/pion/sdp/v2 v2.3.4 github.com/pion/webrtc/v2 v2.2.2 github.com/smartystreets/goconvey v1.6.4 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa - google.golang.org/appengine v1.6.5 + golang.org/x/text v0.3.2 // indirect ) diff --git a/go.sum b/go.sum index a62b8a9..af45818 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,6 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/keroserene/go-webrtc v0.0.0-20190528223128-68a6fb1b4353 h1:UVuvNMp4EiqRl2jDS0MchQmfolWCGzG9+y6vBy9S/aw= -github.com/keroserene/go-webrtc v0.0.0-20190528223128-68a6fb1b4353/go.mod h1:Wl6nMWlHBurzYryPggNZfgTsvRaAH2P3Q6alLg3VxLA= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -93,7 +91,6 @@ golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -112,8 +109,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From d10af300c128955599aefabba10ac8db7027e063 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Tue, 17 Mar 2020 15:18:25 -0400 Subject: [PATCH 075/385] Refactor (De)SerializeSessionDescription as common utils --- client/lib/lib_test.go | 7 +++-- client/lib/rendezvous.go | 5 ++-- client/lib/util.go | 52 ----------------------------------- common/util/util.go | 58 +++++++++++++++++++++++++++++++++++++++ proxy-go/proxy-go_test.go | 7 +++-- proxy-go/snowflake.go | 52 ++--------------------------------- 6 files changed, 72 insertions(+), 109 deletions(-) create mode 100644 common/util/util.go diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index a0e77cb..4b1a9fa 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -9,6 +9,7 @@ import ( "sync" "testing" + "git.torproject.org/pluggable-transports/snowflake.git/common/util" "github.com/pion/webrtc/v2" . "github.com/smartystreets/goconvey/convey" ) @@ -230,7 +231,7 @@ func TestSnowflakeClient(t *testing.T) { So(err, ShouldBeNil) c.offerChannel <- nil - answer := deserializeSessionDescription(sampleAnswer) + answer := util.DeserializeSessionDescription(sampleAnswer) So(answer, ShouldNotBeNil) c.answerChannel <- answer err = c.exchangeSDP() @@ -255,7 +256,7 @@ func TestSnowflakeClient(t *testing.T) { ctx.So(err, ShouldBeNil) wg.Done() }() - answer := deserializeSessionDescription(sampleAnswer) + answer := util.DeserializeSessionDescription(sampleAnswer) c.answerChannel <- answer wg.Wait() }) @@ -285,7 +286,7 @@ func TestSnowflakeClient(t *testing.T) { http.StatusOK, []byte(`{"type":"answer","sdp":"fake"}`), } - fakeOffer := deserializeSessionDescription(`{"type":"offer","sdp":"test"}`) + fakeOffer := util.DeserializeSessionDescription(`{"type":"offer","sdp":"test"}`) Convey("Construct BrokerChannel with no front domain", func() { b, err := NewBrokerChannel("test.broker", "", transport, false) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index d35c813..85f6f1a 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -18,6 +18,7 @@ import ( "net/http" "net/url" + "git.torproject.org/pluggable-transports/snowflake.git/common/util" "github.com/pion/sdp/v2" "github.com/pion/webrtc/v2" ) @@ -140,7 +141,7 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( SDP: stripLocalAddresses(offer.SDP), } } - data := bytes.NewReader([]byte(serializeSessionDescription(offer))) + data := bytes.NewReader([]byte(util.SerializeSessionDescription(offer))) // Suffix with broker's client registration handler. clientURL := bc.url.ResolveReference(&url.URL{Path: "client"}) request, err := http.NewRequest("POST", clientURL.String(), data) @@ -163,7 +164,7 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( if nil != err { return nil, err } - answer := deserializeSessionDescription(string(body)) + answer := util.DeserializeSessionDescription(string(body)) return answer, nil case http.StatusServiceUnavailable: return nil, errors.New(BrokerError503) diff --git a/client/lib/util.go b/client/lib/util.go index 1b5f592..cacf1d7 100644 --- a/client/lib/util.go +++ b/client/lib/util.go @@ -1,11 +1,8 @@ package lib import ( - "encoding/json" "log" "time" - - "github.com/pion/webrtc/v2" ) const ( @@ -86,52 +83,3 @@ func (b *BytesSyncLogger) AddInbound(amount int) { } b.InboundChan <- amount } -func deserializeSessionDescription(msg string) *webrtc.SessionDescription { - var parsed map[string]interface{} - err := json.Unmarshal([]byte(msg), &parsed) - if nil != err { - log.Println(err) - return nil - } - if _, ok := parsed["type"]; !ok { - log.Println("Cannot deserialize SessionDescription without type field.") - return nil - } - if _, ok := parsed["sdp"]; !ok { - log.Println("Cannot deserialize SessionDescription without sdp field.") - return nil - } - - var stype webrtc.SDPType - switch parsed["type"].(string) { - default: - log.Println("Unknown SDP type") - return nil - case "offer": - stype = webrtc.SDPTypeOffer - case "pranswer": - stype = webrtc.SDPTypePranswer - case "answer": - stype = webrtc.SDPTypeAnswer - case "rollback": - stype = webrtc.SDPTypeRollback - } - - if err != nil { - log.Println(err) - return nil - } - return &webrtc.SessionDescription{ - Type: stype, - SDP: parsed["sdp"].(string), - } -} - -func serializeSessionDescription(desc *webrtc.SessionDescription) string { - bytes, err := json.Marshal(*desc) - if nil != err { - log.Println(err) - return "" - } - return string(bytes) -} diff --git a/common/util/util.go b/common/util/util.go new file mode 100644 index 0000000..0a86241 --- /dev/null +++ b/common/util/util.go @@ -0,0 +1,58 @@ +package util + +import ( + "encoding/json" + "log" + + "github.com/pion/webrtc/v2" +) + +func SerializeSessionDescription(desc *webrtc.SessionDescription) string { + bytes, err := json.Marshal(*desc) + if nil != err { + log.Println(err) + return "" + } + return string(bytes) +} + +func DeserializeSessionDescription(msg string) *webrtc.SessionDescription { + var parsed map[string]interface{} + err := json.Unmarshal([]byte(msg), &parsed) + if nil != err { + log.Println(err) + return nil + } + if _, ok := parsed["type"]; !ok { + log.Println("Cannot deserialize SessionDescription without type field.") + return nil + } + if _, ok := parsed["sdp"]; !ok { + log.Println("Cannot deserialize SessionDescription without sdp field.") + return nil + } + + var stype webrtc.SDPType + switch parsed["type"].(string) { + default: + log.Println("Unknown SDP type") + return nil + case "offer": + stype = webrtc.SDPTypeOffer + case "pranswer": + stype = webrtc.SDPTypePranswer + case "answer": + stype = webrtc.SDPTypeAnswer + case "rollback": + stype = webrtc.SDPTypeRollback + } + + if err != nil { + log.Println(err) + return nil + } + return &webrtc.SessionDescription{ + Type: stype, + SDP: parsed["sdp"].(string), + } +} diff --git a/proxy-go/proxy-go_test.go b/proxy-go/proxy-go_test.go index 2429d1e..bed00f2 100644 --- a/proxy-go/proxy-go_test.go +++ b/proxy-go/proxy-go_test.go @@ -13,6 +13,7 @@ import ( "testing" "git.torproject.org/pluggable-transports/snowflake.git/common/messages" + "git.torproject.org/pluggable-transports/snowflake.git/common/util" "github.com/pion/webrtc/v2" . "github.com/smartystreets/goconvey/convey" ) @@ -197,7 +198,7 @@ func TestSessionDescriptions(t *testing.T) { }, }, } { - desc := deserializeSessionDescription(test.msg) + desc := util.DeserializeSessionDescription(test.msg) So(desc, ShouldResemble, test.ret) } }) @@ -214,7 +215,7 @@ func TestSessionDescriptions(t *testing.T) { `{"type":"offer","sdp":"test"}`, }, } { - msg := serializeSessionDescription(test.desc) + msg := util.SerializeSessionDescription(test.desc) So(msg, ShouldResemble, test.ret) } }) @@ -239,7 +240,7 @@ func TestBrokerInteractions(t *testing.T) { }, } pc, _ := webrtc.NewPeerConnection(config) - offer := deserializeSessionDescription(sampleOffer) + offer := util.DeserializeSessionDescription(sampleOffer) pc.SetRemoteDescription(*offer) answer, _ := pc.CreateAnswer(nil) pc.SetLocalDescription(answer) diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index 0b91059..264d4f2 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/rand" "encoding/base64" - "encoding/json" "flag" "fmt" "io" @@ -21,6 +20,7 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" + "git.torproject.org/pluggable-transports/snowflake.git/common/util" "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" "github.com/gorilla/websocket" "github.com/pion/webrtc/v2" @@ -199,7 +199,7 @@ func (b *Broker) pollOffer(sid string) *webrtc.SessionDescription { return nil } if offer != "" { - return deserializeSessionDescription(offer) + return util.DeserializeSessionDescription(offer) } } } @@ -209,7 +209,7 @@ func (b *Broker) pollOffer(sid string) *webrtc.SessionDescription { func (b *Broker) sendAnswer(sid string, pc *webrtc.PeerConnection) error { brokerPath := b.url.ResolveReference(&url.URL{Path: "answer"}) - answer := string([]byte(serializeSessionDescription(pc.LocalDescription()))) + answer := string([]byte(util.SerializeSessionDescription(pc.LocalDescription()))) body, err := messages.EncodeAnswerRequest(answer, sid) if err != nil { return err @@ -465,49 +465,3 @@ func main() { runSession(sessionID) } } - -func deserializeSessionDescription(msg string) *webrtc.SessionDescription { - var parsed map[string]interface{} - err := json.Unmarshal([]byte(msg), &parsed) - if nil != err { - log.Println(err) - return nil - } - if _, ok := parsed["type"]; !ok { - log.Println("Cannot deserialize SessionDescription without type field.") - return nil - } - if _, ok := parsed["sdp"]; !ok { - log.Println("Cannot deserialize SessionDescription without sdp field.") - return nil - } - - var stype webrtc.SDPType - switch parsed["type"].(string) { - default: - log.Println("Unknown SDP type") - return nil - case "offer": - stype = webrtc.SDPTypeOffer - case "pranswer": - stype = webrtc.SDPTypePranswer - case "answer": - stype = webrtc.SDPTypeAnswer - case "rollback": - stype = webrtc.SDPTypeRollback - } - - return &webrtc.SessionDescription{ - Type: stype, - SDP: parsed["sdp"].(string), - } -} - -func serializeSessionDescription(desc *webrtc.SessionDescription) string { - bytes, err := json.Marshal(*desc) - if nil != err { - log.Println(err) - return "" - } - return string(bytes) -} From e521a7217a0888dbb893c64636e841244bbc56c3 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 19 Mar 2020 15:40:11 -0400 Subject: [PATCH 076/385] Update license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index f700c98..42f6296 100644 --- a/LICENSE +++ b/LICENSE @@ -3,7 +3,7 @@ ================================================================================ Copyright (c) 2016, Serene Han, Arlo Breault -All rights reserved. +Copyright (c) 2019-2020, The Tor Project, Inc Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: From f58c865d82fa5d3670c1df9a587d61450aeb664b Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Fri, 7 Feb 2020 15:45:26 -0500 Subject: [PATCH 077/385] Add unsafe logging --- broker/broker.go | 10 ++++++++-- client/snowflake.go | 9 +++++++-- proxy-go/snowflake.go | 10 ++++++++-- server/server.go | 10 ++++++++-- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index 17c677e..d9ef111 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -376,6 +376,7 @@ func main() { var certFilename, keyFilename string var disableGeoip bool var metricsFilename string + var unsafeLogging bool flag.StringVar(&acmeEmail, "acme-email", "", "optional contact email for Let's Encrypt notifications") flag.StringVar(&acmeHostnamesCommas, "acme-hostnames", "", "comma-separated hostnames for TLS certificate") @@ -388,13 +389,18 @@ func main() { flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS") flag.BoolVar(&disableGeoip, "disable-geoip", false, "don't use geoip for stats collection") flag.StringVar(&metricsFilename, "metrics-log", "", "path to metrics logging output") + flag.BoolVar(&unsafeLogging, "unsafe-logging", false, "prevent logs from being scrubbed") flag.Parse() var err error var metricsFile io.Writer var logOutput io.Writer = os.Stderr - //We want to send the log output through our scrubber first - log.SetOutput(&safelog.LogScrubber{Output: logOutput}) + if unsafeLogging { + log.SetOutput(logOutput) + } else { + // We want to send the log output through our scrubber first + log.SetOutput(&safelog.LogScrubber{Output: logOutput}) + } log.SetFlags(log.LstdFlags | log.LUTC) diff --git a/client/snowflake.go b/client/snowflake.go index 4076fff..8acb8f3 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -91,6 +91,7 @@ func main() { logFilename := flag.String("log", "", "name of log file") logToStateDir := flag.Bool("logToStateDir", false, "resolve the log file relative to tor's pt state dir") keepLocalAddresses := flag.Bool("keepLocalAddresses", false, "keep local LAN address ICE candidates") + unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed") max := flag.Int("max", DefaultSnowflakeCapacity, "capacity for number of multiplexed WebRTC peers") flag.Parse() @@ -119,8 +120,12 @@ func main() { defer logFile.Close() logOutput = logFile } - // We want to send the log output through our scrubber first - log.SetOutput(&safelog.LogScrubber{Output: logOutput}) + if *unsafeLogging { + log.SetOutput(logOutput) + } else { + // We want to send the log output through our scrubber first + log.SetOutput(&safelog.LogScrubber{Output: logOutput}) + } log.Println("\n\n\n --- Starting Snowflake Client ---") diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index 264d4f2..69fef9d 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -408,12 +408,14 @@ func main() { var stunURL string var logFilename string var rawBrokerURL string + var unsafeLogging bool flag.UintVar(&capacity, "capacity", 10, "maximum concurrent clients") flag.StringVar(&rawBrokerURL, "broker", defaultBrokerURL, "broker URL") flag.StringVar(&relayURL, "relay", defaultRelayURL, "websocket relay URL") flag.StringVar(&stunURL, "stun", defaultSTUNURL, "stun URL") flag.StringVar(&logFilename, "log", "", "log filename") + flag.BoolVar(&unsafeLogging, "unsafe-logging", false, "prevent logs from being scrubbed") flag.Parse() var logOutput io.Writer = os.Stderr @@ -426,8 +428,12 @@ func main() { defer f.Close() logOutput = io.MultiWriter(os.Stderr, f) } - //We want to send the log output through our scrubber first - log.SetOutput(&safelog.LogScrubber{Output: logOutput}) + if unsafeLogging { + log.SetOutput(logOutput) + } else { + // We want to send the log output through our scrubber first + log.SetOutput(&safelog.LogScrubber{Output: logOutput}) + } log.Println("starting") diff --git a/server/server.go b/server/server.go index 6e9fb19..c03e41c 100644 --- a/server/server.go +++ b/server/server.go @@ -214,12 +214,14 @@ func main() { var acmeHostnamesCommas string var disableTLS bool var logFilename string + var unsafeLogging bool flag.Usage = usage flag.StringVar(&acmeEmail, "acme-email", "", "optional contact email for Let's Encrypt notifications") flag.StringVar(&acmeHostnamesCommas, "acme-hostnames", "", "comma-separated hostnames for TLS certificate") flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS") flag.StringVar(&logFilename, "log", "", "log file to write to") + flag.BoolVar(&unsafeLogging, "unsafe-logging", false, "prevent logs from being scrubbed") flag.Parse() log.SetFlags(log.LstdFlags | log.LUTC) @@ -233,8 +235,12 @@ func main() { defer f.Close() logOutput = f } - //We want to send the log output through our scrubber first - log.SetOutput(&safelog.LogScrubber{Output: logOutput}) + if unsafeLogging { + log.SetOutput(logOutput) + } else { + // We want to send the log output through our scrubber first + log.SetOutput(&safelog.LogScrubber{Output: logOutput}) + } if !disableTLS && acmeHostnamesCommas == "" { log.Fatal("the --acme-hostnames option is required") From 5fa757865507e340b6117c784e4ac0bd88ac7858 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 19 Mar 2020 16:15:19 -0400 Subject: [PATCH 078/385] Rename logToStateDir/keepLocalAddresses to kebab case https://en.wikipedia.org/wiki/Letter_case#Special_case_styles --- client/snowflake.go | 15 +++++++++++---- client/torrc-localhost | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/client/snowflake.go b/client/snowflake.go index 8acb8f3..af8447c 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -89,11 +89,16 @@ func main() { brokerURL := flag.String("url", "", "URL of signaling broker") frontDomain := flag.String("front", "", "front domain") logFilename := flag.String("log", "", "name of log file") - logToStateDir := flag.Bool("logToStateDir", false, "resolve the log file relative to tor's pt state dir") - keepLocalAddresses := flag.Bool("keepLocalAddresses", false, "keep local LAN address ICE candidates") + logToStateDir := flag.Bool("log-to-state-dir", false, "resolve the log file relative to tor's pt state dir") + keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates") unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed") max := flag.Int("max", DefaultSnowflakeCapacity, "capacity for number of multiplexed WebRTC peers") + + // Deprecated + oldLogToStateDir := flag.Bool("logToStateDir", false, "use -log-to-state-dir instead") + oldKeepLocalAddresses := flag.Bool("keepLocalAddresses", false, "use -keep-local-addresses instead") + flag.Parse() log.SetFlags(log.LstdFlags | log.LUTC) @@ -105,7 +110,7 @@ func main() { // https://bugs.torproject.org/25600#comment:14 var logOutput = ioutil.Discard if *logFilename != "" { - if *logToStateDir { + if *logToStateDir || *oldLogToStateDir { stateDir, err := pt.MakeStateDir() if err != nil { log.Fatal(err) @@ -139,7 +144,9 @@ func main() { snowflakes := sf.NewPeers(*max) // Use potentially domain-fronting broker to rendezvous. - broker, err := sf.NewBrokerChannel(*brokerURL, *frontDomain, sf.CreateBrokerTransport(), *keepLocalAddresses) + broker, err := sf.NewBrokerChannel( + *brokerURL, *frontDomain, sf.CreateBrokerTransport(), + *keepLocalAddresses || *oldKeepLocalAddresses) if err != nil { log.Fatalf("parsing broker URL: %v", err) } diff --git a/client/torrc-localhost b/client/torrc-localhost index 9afb033..95746e0 100644 --- a/client/torrc-localhost +++ b/client/torrc-localhost @@ -3,6 +3,6 @@ DataDirectory datadir ClientTransportPlugin snowflake exec ./client \ -url http://localhost:8080/ \ --keepLocalAddresses +-keep-local-addresses Bridge snowflake 0.0.3.0:1 From 670e4ba4380b3fa5cf82043559dcb8c2ca790a7d Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 26 Mar 2020 13:05:24 -0400 Subject: [PATCH 079/385] Move StripLocalAddresses to a common util Trac: 19026 --- client/lib/lib_test.go | 17 --------------- client/lib/rendezvous.go | 47 +--------------------------------------- common/util/util.go | 45 ++++++++++++++++++++++++++++++++++++++ common/util/util_test.go | 26 ++++++++++++++++++++++ 4 files changed, 72 insertions(+), 63 deletions(-) create mode 100644 common/util/util_test.go diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 4b1a9fa..1cdc2c6 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -358,21 +358,4 @@ func TestSnowflakeClient(t *testing.T) { }) }) - Convey("Strip", t, func() { - const offerStart = "v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\n" - const goodCandidate = "a=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\n" - const offerEnd = "a=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n" - - offer := offerStart + goodCandidate + - "a=candidate:3769337065 1 udp 2122260223 192.168.0.100 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLocal IPv4 - "a=candidate:3769337065 1 udp 2122260223 fdf8:f53b:82e4::53 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLocal IPv6 - "a=candidate:3769337065 1 udp 2122260223 0.0.0.0 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsUnspecified IPv4 - "a=candidate:3769337065 1 udp 2122260223 :: 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsUnspecified IPv6 - "a=candidate:3769337065 1 udp 2122260223 127.0.0.1 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLoopback IPv4 - "a=candidate:3769337065 1 udp 2122260223 ::1 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLoopback IPv6 - offerEnd - - So(stripLocalAddresses(offer), ShouldEqual, offerStart+goodCandidate+offerEnd) - }) - } diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 85f6f1a..1f98e26 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -14,12 +14,10 @@ import ( "io" "io/ioutil" "log" - "net" "net/http" "net/url" "git.torproject.org/pluggable-transports/snowflake.git/common/util" - "github.com/pion/sdp/v2" "github.com/pion/webrtc/v2" ) @@ -81,49 +79,6 @@ func limitedRead(r io.Reader, limit int64) ([]byte, error) { return p, err } -// Stolen from https://github.com/golang/go/pull/30278 -func IsLocal(ip net.IP) bool { - if ip4 := ip.To4(); ip4 != nil { - // Local IPv4 addresses are defined in https://tools.ietf.org/html/rfc1918 - return ip4[0] == 10 || - (ip4[0] == 172 && ip4[1]&0xf0 == 16) || - (ip4[0] == 192 && ip4[1] == 168) - } - // Local IPv6 addresses are defined in https://tools.ietf.org/html/rfc4193 - return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc -} - -// Removes local LAN address ICE candidates -func stripLocalAddresses(str string) string { - var desc sdp.SessionDescription - err := desc.Unmarshal([]byte(str)) - if err != nil { - return str - } - for _, m := range desc.MediaDescriptions { - attrs := make([]sdp.Attribute, 0) - for _, a := range m.Attributes { - if a.IsICECandidate() { - ice, err := a.ToICECandidate() - if err == nil && ice.Typ == "host" { - ip := net.ParseIP(ice.Address) - if ip != nil && (IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback()) { - /* no append in this case */ - continue - } - } - } - attrs = append(attrs, a) - } - m.Attributes = attrs - } - bts, err := desc.Marshal() - if err != nil { - return str - } - return string(bts) -} - // Roundtrip HTTP POST using WebRTC SessionDescriptions. // // Send an SDP offer to the broker, which assigns a proxy and responds @@ -138,7 +93,7 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( if !bc.keepLocalAddresses { offer = &webrtc.SessionDescription{ Type: offer.Type, - SDP: stripLocalAddresses(offer.SDP), + SDP: util.StripLocalAddresses(offer.SDP), } } data := bytes.NewReader([]byte(util.SerializeSessionDescription(offer))) diff --git a/common/util/util.go b/common/util/util.go index 0a86241..fa62fd7 100644 --- a/common/util/util.go +++ b/common/util/util.go @@ -3,7 +3,9 @@ package util import ( "encoding/json" "log" + "net" + "github.com/pion/sdp/v2" "github.com/pion/webrtc/v2" ) @@ -56,3 +58,46 @@ func DeserializeSessionDescription(msg string) *webrtc.SessionDescription { SDP: parsed["sdp"].(string), } } + +// Stolen from https://github.com/golang/go/pull/30278 +func IsLocal(ip net.IP) bool { + if ip4 := ip.To4(); ip4 != nil { + // Local IPv4 addresses are defined in https://tools.ietf.org/html/rfc1918 + return ip4[0] == 10 || + (ip4[0] == 172 && ip4[1]&0xf0 == 16) || + (ip4[0] == 192 && ip4[1] == 168) + } + // Local IPv6 addresses are defined in https://tools.ietf.org/html/rfc4193 + return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc +} + +// Removes local LAN address ICE candidates +func StripLocalAddresses(str string) string { + var desc sdp.SessionDescription + err := desc.Unmarshal([]byte(str)) + if err != nil { + return str + } + for _, m := range desc.MediaDescriptions { + attrs := make([]sdp.Attribute, 0) + for _, a := range m.Attributes { + if a.IsICECandidate() { + ice, err := a.ToICECandidate() + if err == nil && ice.Typ == "host" { + ip := net.ParseIP(ice.Address) + if ip != nil && (IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback()) { + /* no append in this case */ + continue + } + } + } + attrs = append(attrs, a) + } + m.Attributes = attrs + } + bts, err := desc.Marshal() + if err != nil { + return str + } + return string(bts) +} diff --git a/common/util/util_test.go b/common/util/util_test.go new file mode 100644 index 0000000..271619a --- /dev/null +++ b/common/util/util_test.go @@ -0,0 +1,26 @@ +package util + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestUtil(t *testing.T) { + Convey("Strip", t, func() { + const offerStart = "v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\n" + const goodCandidate = "a=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + const offerEnd = "a=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n" + + offer := offerStart + goodCandidate + + "a=candidate:3769337065 1 udp 2122260223 192.168.0.100 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLocal IPv4 + "a=candidate:3769337065 1 udp 2122260223 fdf8:f53b:82e4::53 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLocal IPv6 + "a=candidate:3769337065 1 udp 2122260223 0.0.0.0 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsUnspecified IPv4 + "a=candidate:3769337065 1 udp 2122260223 :: 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsUnspecified IPv6 + "a=candidate:3769337065 1 udp 2122260223 127.0.0.1 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLoopback IPv4 + "a=candidate:3769337065 1 udp 2122260223 ::1 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLoopback IPv6 + offerEnd + + So(StripLocalAddresses(offer), ShouldEqual, offerStart+goodCandidate+offerEnd) + }) +} From 1867f89562fb25bf9a3c2172a7b6f0a198c81adb Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 26 Mar 2020 14:04:29 -0400 Subject: [PATCH 080/385] Remove local LAN address ICE candidates in proxy-go answer Trac: 19026 --- proxy-go/snowflake.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/proxy-go/snowflake.go b/proxy-go/snowflake.go index 69fef9d..422cf7e 100644 --- a/proxy-go/snowflake.go +++ b/proxy-go/snowflake.go @@ -70,8 +70,9 @@ func remoteIPFromSDP(sdp string) net.IP { } type Broker struct { - url *url.URL - transport http.RoundTripper + url *url.URL + transport http.RoundTripper + keepLocalAddresses bool } type webRTCConn struct { @@ -209,7 +210,14 @@ func (b *Broker) pollOffer(sid string) *webrtc.SessionDescription { func (b *Broker) sendAnswer(sid string, pc *webrtc.PeerConnection) error { brokerPath := b.url.ResolveReference(&url.URL{Path: "answer"}) - answer := string([]byte(util.SerializeSessionDescription(pc.LocalDescription()))) + ld := pc.LocalDescription() + if !b.keepLocalAddresses { + ld = &webrtc.SessionDescription{ + Type: ld.Type, + SDP: util.StripLocalAddresses(ld.SDP), + } + } + answer := string([]byte(util.SerializeSessionDescription(ld))) body, err := messages.EncodeAnswerRequest(answer, sid) if err != nil { return err @@ -409,6 +417,7 @@ func main() { var logFilename string var rawBrokerURL string var unsafeLogging bool + var keepLocalAddresses bool flag.UintVar(&capacity, "capacity", 10, "maximum concurrent clients") flag.StringVar(&rawBrokerURL, "broker", defaultBrokerURL, "broker URL") @@ -416,6 +425,7 @@ func main() { flag.StringVar(&stunURL, "stun", defaultSTUNURL, "stun URL") flag.StringVar(&logFilename, "log", "", "log filename") flag.BoolVar(&unsafeLogging, "unsafe-logging", false, "prevent logs from being scrubbed") + flag.BoolVar(&keepLocalAddresses, "keep-local-addresses", false, "keep local LAN address ICE candidates") flag.Parse() var logOutput io.Writer = os.Stderr @@ -439,6 +449,7 @@ func main() { var err error broker = new(Broker) + broker.keepLocalAddresses = keepLocalAddresses broker.url, err = url.Parse(rawBrokerURL) if err != nil { log.Fatalf("invalid broker url: %s", err) From ea01bf41c3011590938b079ed96c7b35cb40588b Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 1 Apr 2020 12:55:37 -0400 Subject: [PATCH 081/385] Change dummy address for snowflake This will prevent a bug where tor skips bandwidth events for local addresses (see https://bugs.torproject.org/33693) --- client/torrc | 2 +- client/torrc-localhost | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/torrc b/client/torrc index 6acf1c4..9e3946e 100644 --- a/client/torrc +++ b/client/torrc @@ -7,4 +7,4 @@ ClientTransportPlugin snowflake exec ./client \ -ice stun:stun.l.google.com:19302 \ -max 3 -Bridge snowflake 0.0.3.0:1 +Bridge snowflake 192.0.2.3:1 diff --git a/client/torrc-localhost b/client/torrc-localhost index 95746e0..b2a6d05 100644 --- a/client/torrc-localhost +++ b/client/torrc-localhost @@ -5,4 +5,4 @@ ClientTransportPlugin snowflake exec ./client \ -url http://localhost:8080/ \ -keep-local-addresses -Bridge snowflake 0.0.3.0:1 +Bridge snowflake 192.0.2.3:1 From 237fed1151f900c08edf83441cb2edebf8743978 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 2 Apr 2020 12:36:09 -0600 Subject: [PATCH 082/385] Update GitHub issue numbers to Trac ticket numbers. --- client/lib/rendezvous.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 1f98e26..c82fc9e 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -148,8 +148,8 @@ func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer) *WebR // Initialize a WebRTC Connection by signaling through the broker. func (w WebRTCDialer) Catch() (Snowflake, error) { - // TODO: [#3] Fetch ICE server information from Broker. - // TODO: [#18] Consider TURN servers here too. + // TODO: [#25591] Fetch ICE server information from Broker. + // TODO: [#25596] Consider TURN servers here too. connection := NewWebRTCPeer(w.webrtcConfig, w.BrokerChannel) err := connection.Connect() return connection, err From 8eef3b63482deb50d988e3703b7a718802cdd2f1 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 2 Apr 2020 13:51:06 -0600 Subject: [PATCH 083/385] Remove uniuri dependency. https://bugs.torproject.org/33800 --- client/lib/webrtc.go | 11 +++++++++-- go.mod | 1 - go.sum | 2 -- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 0b3bb16..5aa7aec 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -2,13 +2,14 @@ package lib import ( "bytes" + "crypto/rand" + "encoding/hex" "errors" "io" "log" "sync" "time" - "github.com/dchest/uniuri" "github.com/pion/webrtc/v2" ) @@ -46,7 +47,13 @@ type WebRTCPeer struct { func NewWebRTCPeer(config *webrtc.Configuration, broker *BrokerChannel) *WebRTCPeer { connection := new(WebRTCPeer) - connection.id = "snowflake-" + uniuri.New() + { + var buf [8]byte + if _, err := rand.Read(buf[:]); err != nil { + panic(err) + } + connection.id = "snowflake-" + hex.EncodeToString(buf[:]) + } connection.config = config connection.broker = broker connection.offerChannel = make(chan *webrtc.SessionDescription, 1) diff --git a/go.mod b/go.mod index b203976..4366d6a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.13 require ( git.torproject.org/pluggable-transports/goptlib.git v1.1.0 - github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 github.com/golang/protobuf v1.3.1 // indirect github.com/gorilla/websocket v1.4.1 github.com/pion/sdp/v2 v2.3.4 diff --git a/go.sum b/go.sum index af45818..3708fc0 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,6 @@ github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wX github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= -github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= From 6f89fc14f6aa8692b44dc71bd7b04cbe2cf20aa8 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 19 Mar 2020 12:17:04 -0400 Subject: [PATCH 084/385] Remove proxy/translation submodule We're moving all web proxy code to another repository. --- .gitmodules | 4 ---- proxy/translation | 1 - 2 files changed, 5 deletions(-) delete mode 160000 proxy/translation diff --git a/.gitmodules b/.gitmodules index 5f79304..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +0,0 @@ -[submodule "proxy/translation"] - path = proxy/translation - url = https://git.torproject.org/translation.git - branch = snowflakeaddon-messages.json_completed diff --git a/proxy/translation b/proxy/translation deleted file mode 160000 index 9ce163a..0000000 --- a/proxy/translation +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9ce163a282fcd35e3cc6b642b0236e237dc56159 From 51b0b7ed2e02b9444eebf75612d53c3a32301f6f Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 19 Mar 2020 12:17:56 -0400 Subject: [PATCH 085/385] Remove proxy/ subdirectory We're moving all web proxy code to a different repsitory. --- proxy/.eslintignore | 8 - proxy/.eslintrc.json | 13 - proxy/README.md | 143 -------- proxy/broker.js | 134 ------- proxy/config.js | 40 --- proxy/init-badge.js | 223 ------------ proxy/init-node.js | 27 -- proxy/init-testing.js | 125 ------- proxy/init-webext.js | 203 ----------- proxy/make.js | 201 ----------- proxy/package.json | 35 -- proxy/proxypair.js | 249 ------------- proxy/shims.js | 31 -- proxy/snowflake.js | 165 --------- proxy/spec/broker.spec.js | 131 ------- proxy/spec/init.spec.js | 34 -- proxy/spec/proxypair.spec.js | 163 --------- proxy/spec/snowflake.spec.js | 103 ------ proxy/spec/ui.spec.js | 68 ---- proxy/spec/util.spec.js | 252 ------------- proxy/spec/websocket.spec.js | 41 --- proxy/static/.htaccess | 5 - proxy/static/SourceSansPro-Regular.ttf | Bin 293516 -> 0 bytes proxy/static/_locales/en_US/messages.json | 80 ----- proxy/static/assets/arrowhead-right-12.svg | 4 - .../static/assets/arrowhead-right-dark-12.svg | 4 - proxy/static/assets/favicon.ico | Bin 1150 -> 0 bytes proxy/static/assets/status-off-dark.svg | 11 - proxy/static/assets/status-off.svg | 7 - proxy/static/assets/status-on-dark.svg | 11 - proxy/static/assets/status-on.svg | 7 - proxy/static/assets/status-running.svg | 7 - proxy/static/assets/toolbar-off-48.png | Bin 3657 -> 0 bytes proxy/static/assets/toolbar-off-96.png | Bin 7214 -> 0 bytes proxy/static/assets/toolbar-off.ico | Bin 4286 -> 0 bytes proxy/static/assets/toolbar-off.svg | 14 - proxy/static/assets/toolbar-on-48.png | Bin 3674 -> 0 bytes proxy/static/assets/toolbar-on-96.png | Bin 7355 -> 0 bytes proxy/static/assets/toolbar-on.ico | Bin 4286 -> 0 bytes proxy/static/assets/toolbar-on.svg | 14 - proxy/static/assets/toolbar-running-48.png | Bin 3660 -> 0 bytes proxy/static/assets/toolbar-running-96.png | Bin 7385 -> 0 bytes proxy/static/assets/toolbar-running.ico | Bin 4286 -> 0 bytes proxy/static/assets/toolbar-running.svg | 14 - proxy/static/bootstrap.css | 334 ------------------ proxy/static/chrome150.jpg | Bin 5321 -> 0 bytes proxy/static/embed.css | 150 -------- proxy/static/embed.html | 30 -- proxy/static/firefox150.jpg | Bin 44930 -> 0 bytes proxy/static/index.css | 94 ----- proxy/static/index.html | 116 ------ proxy/static/index.js | 83 ----- proxy/static/popup.js | 50 --- proxy/static/screenshot.png | Bin 377507 -> 0 bytes proxy/static/tor-logo@2x.png | Bin 10042 -> 0 bytes proxy/ui.js | 17 - proxy/util.js | 216 ----------- proxy/webext/embed.js | 48 --- proxy/webext/manifest.json | 24 -- proxy/websocket.js | 78 ---- 60 files changed, 3807 deletions(-) delete mode 100644 proxy/.eslintignore delete mode 100644 proxy/.eslintrc.json delete mode 100644 proxy/README.md delete mode 100644 proxy/broker.js delete mode 100644 proxy/config.js delete mode 100644 proxy/init-badge.js delete mode 100644 proxy/init-node.js delete mode 100644 proxy/init-testing.js delete mode 100644 proxy/init-webext.js delete mode 100755 proxy/make.js delete mode 100644 proxy/package.json delete mode 100644 proxy/proxypair.js delete mode 100644 proxy/shims.js delete mode 100644 proxy/snowflake.js delete mode 100644 proxy/spec/broker.spec.js delete mode 100644 proxy/spec/init.spec.js delete mode 100644 proxy/spec/proxypair.spec.js delete mode 100644 proxy/spec/snowflake.spec.js delete mode 100644 proxy/spec/ui.spec.js delete mode 100644 proxy/spec/util.spec.js delete mode 100644 proxy/spec/websocket.spec.js delete mode 100644 proxy/static/.htaccess delete mode 100644 proxy/static/SourceSansPro-Regular.ttf delete mode 100644 proxy/static/_locales/en_US/messages.json delete mode 100644 proxy/static/assets/arrowhead-right-12.svg delete mode 100644 proxy/static/assets/arrowhead-right-dark-12.svg delete mode 100644 proxy/static/assets/favicon.ico delete mode 100644 proxy/static/assets/status-off-dark.svg delete mode 100644 proxy/static/assets/status-off.svg delete mode 100644 proxy/static/assets/status-on-dark.svg delete mode 100644 proxy/static/assets/status-on.svg delete mode 100644 proxy/static/assets/status-running.svg delete mode 100644 proxy/static/assets/toolbar-off-48.png delete mode 100644 proxy/static/assets/toolbar-off-96.png delete mode 100644 proxy/static/assets/toolbar-off.ico delete mode 100644 proxy/static/assets/toolbar-off.svg delete mode 100644 proxy/static/assets/toolbar-on-48.png delete mode 100644 proxy/static/assets/toolbar-on-96.png delete mode 100644 proxy/static/assets/toolbar-on.ico delete mode 100644 proxy/static/assets/toolbar-on.svg delete mode 100644 proxy/static/assets/toolbar-running-48.png delete mode 100644 proxy/static/assets/toolbar-running-96.png delete mode 100644 proxy/static/assets/toolbar-running.ico delete mode 100644 proxy/static/assets/toolbar-running.svg delete mode 100644 proxy/static/bootstrap.css delete mode 100644 proxy/static/chrome150.jpg delete mode 100644 proxy/static/embed.css delete mode 100644 proxy/static/embed.html delete mode 100644 proxy/static/firefox150.jpg delete mode 100644 proxy/static/index.css delete mode 100644 proxy/static/index.html delete mode 100644 proxy/static/index.js delete mode 100644 proxy/static/popup.js delete mode 100644 proxy/static/screenshot.png delete mode 100644 proxy/static/tor-logo@2x.png delete mode 100644 proxy/ui.js delete mode 100644 proxy/util.js delete mode 100644 proxy/webext/embed.js delete mode 100644 proxy/webext/manifest.json delete mode 100644 proxy/websocket.js diff --git a/proxy/.eslintignore b/proxy/.eslintignore deleted file mode 100644 index c249199..0000000 --- a/proxy/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -build/ -test/ -webext/snowflake.js -snowflake-library.js - -# FIXME: Whittle these away -spec/ -shims.js diff --git a/proxy/.eslintrc.json b/proxy/.eslintrc.json deleted file mode 100644 index 406f9e5..0000000 --- a/proxy/.eslintrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "env": { - "browser": true, - "es6": true - }, - "extends": "eslint:recommended", - "rules": { - "indent": ["error", 2, { - "SwitchCase": 1, - "MemberExpression": 0 - }] - } -} diff --git a/proxy/README.md b/proxy/README.md deleted file mode 100644 index 33b8738..0000000 --- a/proxy/README.md +++ /dev/null @@ -1,143 +0,0 @@ -This is the browser proxy component of Snowflake. - -### Embedding - -See https://snowflake.torproject.org/ for more info: -``` - -``` - -### Building the badge / snowflake.torproject.org - -``` -npm install -npm run build -``` - -which outputs to the `build/` directory. - -### Building the webextension - -``` -npm install -npm run webext -``` - -and then load the `webext/` directory as an unpacked extension. - * https://developer.mozilla.org/en-US/docs/Tools/about:debugging#Loading_a_temporary_extension - * https://developer.chrome.com/extensions/getstarted#manifest - -### Testing - -Unit testing with Jasmine are available with: -``` -npm install -npm test -``` - -To run locally, start an http server in `build/` and navigate to `/embed.html`. - -### Preparing to deploy - -Background information: - * https://bugs.torproject.org/23947#comment:8 - * https://help.torproject.org/tsa/doc/static-sites/ - * https://help.torproject.org/tsa/doc/ssh-jump-host/ - -You need to be in LDAP group "snowflake" and have set up an SSH key with your LDAP account. -In your ~/.ssh/config file, you should have something like: - -``` -Host staticiforme -HostName staticiforme.torproject.org -User -ProxyJump people.torproject.org -IdentityFile ~/.ssh/tor -``` - -### Deploying - -``` -npm install -npm run build -``` - -Do a "dry run" rsync with `-n` to check that only expected files are being changed. If you don't understand why a file would be updated, you can add the `-i` option to see the reason. - -``` -rsync -n --chown=:snowflake --chmod ug=rw,D+x --perms --delete -crv build/ staticiforme:/srv/snowflake.torproject.org/htdocs/ -``` - -If it looks good, then repeat the rsync without `-n`. - -``` -rsync --chown=:snowflake --chmod ug=rw,D+x --perms --delete -crv build/ staticiforme:/srv/snowflake.torproject.org/htdocs/ -``` - -You can ignore errors of the form `rsync: failed to set permissions on "/": Operation not permitted (1)`. - -Then run the command to copy the new files to the live web servers: - -``` -ssh staticiforme 'static-update-component snowflake.torproject.org' -``` - -### Parameters - -With no parameters, -snowflake uses the default relay `snowflake.freehaven.net:443` and -uses automatic signaling with the default broker at -`https://snowflake-broker.freehaven.net/`. - -### Reuse as a library - -The badge and the webextension make use of the same underlying library and -only differ in their UI. That same library can be produced for use with other -interfaces, such as [Cupcake][1], by running, - -``` -npm install -npm run library -``` - -which outputs a `./snowflake-library.js`. - -You'd then want to create a subclass of `UI` to perform various actions as -the state of the snowflake changes, - -``` -class MyUI extends UI { - ... -} -``` - -See `WebExtUI` in `init-webext.js` and `BadgeUI` in `init-badge.js` for -examples. - -Finally, initialize the snowflake with, - -``` -var log = function(msg) { - return console.log('Snowflake: ' + msg); -}; -var dbg = log; - -var config = new Config("myui"); // NOTE: Set a unique proxy type for metrics -var ui = new MyUI(); // NOTE: Using the class defined above -var broker = new Broker(config.brokerUrl); - -var snowflake = new Snowflake(config, ui, broker); - -snowflake.setRelayAddr(config.relayAddr); -snowflake.beginWebRTC(); -``` - -This minimal setup is pretty much what's currently in `init-node.js`. - -When configuring the snowflake, set a unique `proxyType` (first argument -to `Config`) that will be used when recording metrics at the broker. Also, -it would be helpful to get in touch with the [Anti-Censorship Team][2] at the -Tor Project to let them know about your tool. - -[1]: https://chrome.google.com/webstore/detail/cupcake/dajjbehmbnbppjkcnpdkaniapgdppdnc -[2]: https://trac.torproject.org/projects/tor/wiki/org/teams/AntiCensorshipTeam diff --git a/proxy/broker.js b/proxy/broker.js deleted file mode 100644 index 42293ae..0000000 --- a/proxy/broker.js +++ /dev/null @@ -1,134 +0,0 @@ -/* global log, dbg, snowflake */ - -/* -Communication with the snowflake broker. - -Browser snowflakes must register with the broker in order -to get assigned to clients. -*/ - -// Represents a broker running remotely. -class Broker { - - // When interacting with the Broker, snowflake must generate a unique session - // ID so the Broker can keep track of each proxy's signalling channels. - // On construction, this Broker object does not do anything until - // |getClientOffer| is called. - constructor(config) { - this.getClientOffer = this.getClientOffer.bind(this); - this._postRequest = this._postRequest.bind(this); - - this.config = config - this.url = config.brokerUrl; - this.clients = 0; - if (0 === this.url.indexOf('localhost', 0)) { - // Ensure url has the right protocol + trailing slash. - this.url = 'http://' + this.url; - } - if (0 !== this.url.indexOf('http', 0)) { - this.url = 'https://' + this.url; - } - if ('/' !== this.url.substr(-1)) { - this.url += '/'; - } - } - - // Promises some client SDP Offer. - // Registers this Snowflake with the broker using an HTTP POST request, and - // waits for a response containing some client offer that the Broker chooses - // for this proxy.. - // TODO: Actually support multiple clients. - getClientOffer(id) { - return new Promise((fulfill, reject) => { - var xhr; - xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function() { - if (xhr.DONE !== xhr.readyState) { - return; - } - switch (xhr.status) { - case Broker.CODE.OK: - var response = JSON.parse(xhr.responseText); - if (response.Status == Broker.STATUS.MATCH) { - return fulfill(response.Offer); // Should contain offer. - } else if (response.Status == Broker.STATUS.TIMEOUT) { - return reject(Broker.MESSAGE.TIMEOUT); - } else { - log('Broker ERROR: Unexpected ' + response.Status); - return reject(Broker.MESSAGE.UNEXPECTED); - } - default: - log('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText); - snowflake.ui.setStatus(' failure. Please refresh.'); - return reject(Broker.MESSAGE.UNEXPECTED); - } - }; - this._xhr = xhr; // Used by spec to fake async Broker interaction - var data = {"Version": "1.1", "Sid": id, "Type": this.config.proxyType} - return this._postRequest(xhr, 'proxy', JSON.stringify(data)); - }); - } - - // Assumes getClientOffer happened, and a WebRTC SDP answer has been generated. - // Sends it back to the broker, which passes it to back to the original client. - sendAnswer(id, answer) { - var xhr; - dbg(id + ' - Sending answer back to broker...\n'); - dbg(answer.sdp); - xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function() { - if (xhr.DONE !== xhr.readyState) { - return; - } - switch (xhr.status) { - case Broker.CODE.OK: - dbg('Broker: Successfully replied with answer.'); - return dbg(xhr.responseText); - default: - dbg('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText); - return snowflake.ui.setStatus(' failure. Please refresh.'); - } - }; - var data = {"Version": "1.0", "Sid": id, "Answer": JSON.stringify(answer)}; - return this._postRequest(xhr, 'answer', JSON.stringify(data)); - } - - // urlSuffix for the broker is different depending on what action - // is desired. - _postRequest(xhr, urlSuffix, payload) { - var err; - try { - xhr.open('POST', this.url + urlSuffix); - } catch (error) { - err = error; - /* - An exception happens here when, for example, NoScript allows the domain - on which the proxy badge runs, but not the domain to which it's trying - to make the HTTP xhr. The exception message is like "Component - returned failure code: 0x805e0006 [nsIXMLHttpRequest.open]" on Firefox. - */ - log('Broker: exception while connecting: ' + err.message); - return; - } - return xhr.send(payload); - } - -} - -Broker.CODE = { - OK: 200, - BAD_REQUEST: 400, - INTERNAL_SERVER_ERROR: 500 -}; - -Broker.STATUS = { - MATCH: "client match", - TIMEOUT: "no match" -}; - -Broker.MESSAGE = { - TIMEOUT: 'Timed out waiting for a client offer.', - UNEXPECTED: 'Unexpected status.' -}; - -Broker.prototype.clients = 0; diff --git a/proxy/config.js b/proxy/config.js deleted file mode 100644 index 39c2b15..0000000 --- a/proxy/config.js +++ /dev/null @@ -1,40 +0,0 @@ - -class Config { - constructor(proxyType) { - this.proxyType = proxyType || ''; - } -} - -Config.prototype.brokerUrl = 'snowflake-broker.freehaven.net'; - -Config.prototype.relayAddr = { - host: 'snowflake.freehaven.net', - port: '443' -}; - -// Original non-wss relay: -// host: '192.81.135.242' -// port: 9902 -Config.prototype.cookieName = "snowflake-allow"; - -// Bytes per second. Set to undefined to disable limit. -Config.prototype.rateLimitBytes = void 0; - -Config.prototype.minRateLimit = 10 * 1024; - -Config.prototype.rateLimitHistory = 5.0; - -Config.prototype.defaultBrokerPollInterval = 300.0 * 1000; - -Config.prototype.maxNumClients = 1; - -Config.prototype.proxyType = ""; - -// TODO: Different ICE servers. -Config.prototype.pcConfig = { - iceServers: [ - { - urls: ['stun:stun.l.google.com:19302'] - } - ] -}; diff --git a/proxy/init-badge.js b/proxy/init-badge.js deleted file mode 100644 index cb066e8..0000000 --- a/proxy/init-badge.js +++ /dev/null @@ -1,223 +0,0 @@ -/* global Util, Params, Config, UI, Broker, Snowflake, Popup, Parse, availableLangs, WS */ - -/* -UI -*/ - -class Messages { - constructor(json) { - this.json = json; - } - getMessage(m, ...rest) { - let message = this.json[m].message; - return message.replace(/\$(\d+)/g, (...args) => { - return rest[Number(args[1]) - 1]; - }); - } -} - -let messages = null; - -class BadgeUI extends UI { - - constructor() { - super(); - this.popup = new Popup(); - } - - setStatus() {} - - missingFeature(missing) { - this.popup.setEnabled(false); - this.popup.setActive(false); - this.popup.setStatusText(messages.getMessage('popupStatusOff')); - this.setIcon('off'); - this.popup.setStatusDesc(missing, true); - this.popup.hideButton(); - } - - turnOn() { - const clients = this.active ? 1 : 0; - this.popup.setChecked(true); - if (clients > 0) { - this.popup.setStatusText(messages.getMessage('popupStatusOn', String(clients))); - this.setIcon('running'); - } else { - this.popup.setStatusText(messages.getMessage('popupStatusReady')); - this.setIcon('on'); - } - // FIXME: Share stats from webext - this.popup.setStatusDesc(''); - this.popup.setEnabled(true); - this.popup.setActive(this.active); - } - - turnOff() { - this.popup.setChecked(false); - this.popup.setStatusText(messages.getMessage('popupStatusOff')); - this.setIcon('off'); - this.popup.setStatusDesc(''); - this.popup.setEnabled(false); - this.popup.setActive(false); - } - - setActive(connected) { - super.setActive(connected); - this.turnOn(); - } - - setIcon(status) { - document.getElementById('icon').href = `assets/toolbar-${status}.ico`; - } - -} - -BadgeUI.prototype.popup = null; - - -/* -Entry point. -*/ - -// Defaults to opt-in. -var COOKIE_NAME = "snowflake-allow"; -var COOKIE_LIFETIME = "Thu, 01 Jan 2038 00:00:00 GMT"; -var COOKIE_EXPIRE = "Thu, 01 Jan 1970 00:00:01 GMT"; - -function setSnowflakeCookie(val, expires) { - document.cookie = `${COOKIE_NAME}=${val}; path=/; expires=${expires};`; -} - -const defaultLang = 'en_US'; - -// Resolve as in, -// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization#Localized_string_selection -function getLang() { - let lang = navigator.language || defaultLang; - lang = lang.replace(/-/g, '_'); - if (availableLangs.has(lang)) { - return lang; - } - lang = lang.split('_')[0]; - if (availableLangs.has(lang)) { - return lang; - } - return defaultLang; -} - -var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotifications, query; - -(function() { - - snowflake = null; - - query = new URLSearchParams(location.search); - - debug = Params.getBool(query, 'debug', false); - - silenceNotifications = Params.getBool(query, 'silent', false); - - // Log to both console and UI if applicable. - // Requires that the snowflake and UI objects are hooked up in order to - // log to console. - log = function(msg) { - console.log('Snowflake: ' + msg); - return snowflake != null ? snowflake.ui.log(msg) : void 0; - }; - - dbg = function(msg) { - if (debug) { log(msg); } - }; - - update = function() { - const cookies = Parse.cookie(document.cookie); - if (cookies[COOKIE_NAME] !== '1') { - ui.turnOff(); - snowflake.disable(); - log('Currently not active.'); - return; - } - - if (!Util.hasWebRTC()) { - ui.missingFeature(messages.getMessage('popupWebRTCOff')); - snowflake.disable(); - return; - } - - WS.probeWebsocket(config.relayAddr) - .then( - () => { - ui.turnOn(); - dbg('Contacting Broker at ' + broker.url); - log('Starting snowflake'); - snowflake.setRelayAddr(config.relayAddr); - snowflake.beginWebRTC(); - }, - () => { - ui.missingFeature(messages.getMessage('popupBridgeUnreachable')); - snowflake.disable(); - log('Could not connect to bridge.'); - } - ); - }; - - init = function() { - ui = new BadgeUI(); - - if (!Util.hasCookies()) { - ui.missingFeature(messages.getMessage('badgeCookiesOff')); - return; - } - - config = new Config("badge"); - if ('off' !== query.get('ratelimit')) { - config.rateLimitBytes = Params.getByteCount(query, 'ratelimit', config.rateLimitBytes); - } - broker = new Broker(config); - snowflake = new Snowflake(config, ui, broker); - log('== snowflake proxy =='); - update(); - - document.getElementById('enabled').addEventListener('change', (event) => { - if (event.target.checked) { - setSnowflakeCookie('1', COOKIE_LIFETIME); - } else { - setSnowflakeCookie('', COOKIE_EXPIRE); - } - update(); - }) - }; - - // Notification of closing tab with active proxy. - window.onbeforeunload = function() { - if ( - !silenceNotifications && - snowflake !== null && - ui.active - ) { - return Snowflake.MESSAGE.CONFIRMATION; - } - return null; - }; - - window.onunload = function() { - if (snowflake !== null) { snowflake.disable(); } - return null; - }; - - window.onload = function() { - fetch(`./_locales/${getLang()}/messages.json`) - .then((res) => { - if (!res.ok) { return; } - return res.json(); - }) - .then((json) => { - messages = new Messages(json); - Popup.fill(document.body, (m) => { - return messages.getMessage(m); - }); - init(); - }); - } - -}()); diff --git a/proxy/init-node.js b/proxy/init-node.js deleted file mode 100644 index b5a60d8..0000000 --- a/proxy/init-node.js +++ /dev/null @@ -1,27 +0,0 @@ -/* global Config, UI, Broker, Snowflake */ - -/* -Entry point. -*/ - -var config = new Config("node"); - -var ui = new UI(); - -var broker = new Broker(config); - -var snowflake = new Snowflake(config, ui, broker); - -var log = function(msg) { - return console.log('Snowflake: ' + msg); -}; - -var dbg = log; - -log('== snowflake proxy =='); - -dbg('Contacting Broker at ' + broker.url); - -snowflake.setRelayAddr(config.relayAddr); - -snowflake.beginWebRTC(); diff --git a/proxy/init-testing.js b/proxy/init-testing.js deleted file mode 100644 index 01b6147..0000000 --- a/proxy/init-testing.js +++ /dev/null @@ -1,125 +0,0 @@ -/* global TESTING, Util, Params, Config, UI, Broker, Snowflake */ - -/* -UI -*/ - -class DebugUI extends UI { - - constructor() { - super(); - // Setup other DOM handlers if it's debug mode. - this.$status = document.getElementById('status'); - this.$msglog = document.getElementById('msglog'); - this.$msglog.value = ''; - } - - // Status bar - setStatus(msg) { - var txt; - txt = document.createTextNode('Status: ' + msg); - while (this.$status.firstChild) { - this.$status.removeChild(this.$status.firstChild); - } - return this.$status.appendChild(txt); - } - - setActive(connected) { - super.setActive(connected); - return this.$msglog.className = connected ? 'active' : ''; - } - - log(msg) { - // Scroll to latest - this.$msglog.value += msg + '\n'; - return this.$msglog.scrollTop = this.$msglog.scrollHeight; - } - -} - -// DOM elements references. -DebugUI.prototype.$msglog = null; - -DebugUI.prototype.$status = null; - -/* -Entry point. -*/ - -var snowflake, query, debug, ui, silenceNotifications, log, dbg, init; - -(function() { - - if (((typeof TESTING === "undefined" || TESTING === null) || !TESTING) && !Util.featureDetect()) { - console.log('webrtc feature not detected. shutting down'); - return; - } - - snowflake = null; - - query = new URLSearchParams(location.search); - - debug = Params.getBool(query, 'debug', false); - - silenceNotifications = Params.getBool(query, 'silent', false); - - // Log to both console and UI if applicable. - // Requires that the snowflake and UI objects are hooked up in order to - // log to console. - log = function(msg) { - console.log('Snowflake: ' + msg); - return snowflake != null ? snowflake.ui.log(msg) : void 0; - }; - - dbg = function(msg) { - if (debug || ((snowflake != null ? snowflake.ui : void 0) instanceof DebugUI)) { - return log(msg); - } - }; - - init = function() { - var broker, config, ui; - config = new Config("testing"); - if ('off' !== query['ratelimit']) { - config.rateLimitBytes = Params.getByteCount(query, 'ratelimit', config.rateLimitBytes); - } - ui = null; - if (document.getElementById('status') !== null) { - ui = new DebugUI(); - } else { - ui = new UI(); - } - broker = new Broker(config); - snowflake = new Snowflake(config, ui, broker); - log('== snowflake proxy =='); - if (Util.snowflakeIsDisabled(config.cookieName)) { - // Do not activate the proxy if any number of conditions are true. - log('Currently not active.'); - return; - } - // Otherwise, begin setting up WebRTC and acting as a proxy. - dbg('Contacting Broker at ' + broker.url); - snowflake.setRelayAddr(config.relayAddr); - return snowflake.beginWebRTC(); - }; - - // Notification of closing tab with active proxy. - window.onbeforeunload = function() { - if ( - !silenceNotifications && - snowflake !== null && - ui.active - ) { - return Snowflake.MESSAGE.CONFIRMATION; - } - return null; - }; - - window.onunload = function() { - if (snowflake !== null) { snowflake.disable(); } - return null; - }; - - window.onload = init; - -}()); diff --git a/proxy/init-webext.js b/proxy/init-webext.js deleted file mode 100644 index 3eb42dd..0000000 --- a/proxy/init-webext.js +++ /dev/null @@ -1,203 +0,0 @@ -/* global Util, chrome, Config, UI, Broker, Snowflake, WS */ -/* eslint no-unused-vars: 0 */ - -/* -UI -*/ - -class WebExtUI extends UI { - - constructor() { - super(); - this.onConnect = this.onConnect.bind(this); - this.onMessage = this.onMessage.bind(this); - this.onDisconnect = this.onDisconnect.bind(this); - this.initStats(); - chrome.runtime.onConnect.addListener(this.onConnect); - } - - initStats() { - this.stats = [0]; - setInterval((() => { - this.stats.unshift(0); - this.stats.splice(24); - this.postActive(); - }), 60 * 60 * 1000); - } - - initToggle() { - // First, check if we have our status stored - (new Promise((resolve) => { - chrome.storage.local.get(["snowflake-enabled"], resolve); - })) - .then((result) => { - let enabled = this.enabled; - if (result['snowflake-enabled'] !== void 0) { - enabled = result['snowflake-enabled']; - } else { - log("Toggle state not yet saved"); - } - // If it isn't enabled, stop - if (!enabled) { - this.setEnabled(enabled); - return; - } - // Otherwise, do feature checks - if (!Util.hasWebRTC()) { - this.missingFeature = 'popupWebRTCOff'; - this.setEnabled(false); - return; - } - WS.probeWebsocket(config.relayAddr) - .then( - () => { - this.setEnabled(true); - }, - () => { - log('Could not connect to bridge.'); - this.missingFeature = 'popupBridgeUnreachable'; - this.setEnabled(false); - } - ); - }); - } - - postActive() { - this.setIcon(); - if (!this.port) { return; } - this.port.postMessage({ - active: this.active, - total: this.stats.reduce((function(t, c) { - return t + c; - }), 0), - enabled: this.enabled, - missingFeature: this.missingFeature, - }); - } - - onConnect(port) { - this.port = port; - port.onDisconnect.addListener(this.onDisconnect); - port.onMessage.addListener(this.onMessage); - this.postActive(); - } - - onMessage(m) { - (new Promise((resolve) => { - chrome.storage.local.set({ "snowflake-enabled": m.enabled }, resolve); - })) - .then(() => { - log("Stored toggle state"); - this.initToggle(); - }); - } - - onDisconnect() { - this.port = null; - } - - setActive(connected) { - super.setActive(connected); - if (connected) { - this.stats[0] += 1; - } - this.postActive(); - } - - setEnabled(enabled) { - this.enabled = enabled; - this.postActive(); - update(); - } - - setIcon() { - let path = null; - if (!this.enabled) { - path = { - 48: "assets/toolbar-off-48.png", - 96: "assets/toolbar-off-96.png" - }; - } else if (this.active) { - path = { - 48: "assets/toolbar-running-48.png", - 96: "assets/toolbar-running-96.png" - }; - } else { - path = { - 48: "assets/toolbar-on-48.png", - 96: "assets/toolbar-on-96.png" - }; - } - chrome.browserAction.setIcon({ - path: path, - }); - } - -} - -WebExtUI.prototype.port = null; - -WebExtUI.prototype.stats = null; - -WebExtUI.prototype.enabled = true; - -/* -Entry point. -*/ - -var debug, snowflake, config, broker, ui, log, dbg, init, update, silenceNotifications; - -(function () { - - silenceNotifications = false; - debug = false; - snowflake = null; - config = null; - broker = null; - ui = null; - - // Log to both console and UI if applicable. - // Requires that the snowflake and UI objects are hooked up in order to - // log to console. - log = function(msg) { - console.log('Snowflake: ' + msg); - return snowflake != null ? snowflake.ui.log(msg) : void 0; - }; - - dbg = function(msg) { - if (debug) { - return log(msg); - } - }; - - init = function() { - config = new Config("webext"); - ui = new WebExtUI(); - broker = new Broker(config); - snowflake = new Snowflake(config, ui, broker); - log('== snowflake proxy =='); - ui.initToggle(); - }; - - update = function() { - if (!ui.enabled) { - // Do not activate the proxy if any number of conditions are true. - snowflake.disable(); - log('Currently not active.'); - return; - } - // Otherwise, begin setting up WebRTC and acting as a proxy. - dbg('Contacting Broker at ' + broker.url); - log('Starting snowflake'); - snowflake.setRelayAddr(config.relayAddr); - return snowflake.beginWebRTC(); - }; - - window.onunload = function() { - if (snowflake !== null) { snowflake.disable(); } - return null; - }; - - window.onload = init; - -}()); diff --git a/proxy/make.js b/proxy/make.js deleted file mode 100755 index 6111913..0000000 --- a/proxy/make.js +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env node - -/* global require, process */ - -var { writeFileSync, readdirSync, statSync } = require('fs'); -var { execSync, spawn } = require('child_process'); -var cldr = require('cldr'); - -// All files required. -var FILES = [ - 'broker.js', - 'config.js', - 'proxypair.js', - 'snowflake.js', - 'ui.js', - 'util.js', - 'websocket.js', - 'shims.js' -]; - -var FILES_SPEC = [ - 'spec/broker.spec.js', - 'spec/init.spec.js', - 'spec/proxypair.spec.js', - 'spec/snowflake.spec.js', - 'spec/ui.spec.js', - 'spec/util.spec.js', - 'spec/websocket.spec.js' -]; - -var STATIC = 'static'; - -var SHARED_FILES = [ - 'embed.html', - 'embed.css', - 'popup.js', - 'assets', - '_locales', -]; - -var concatJS = function(outDir, init, outFile, pre) { - var files = FILES; - if (init) { - files = files.concat(`init-${init}.js`); - } - var outPath = `${outDir}/${outFile}`; - writeFileSync(outPath, pre, 'utf8'); - execSync(`cat ${files.join(' ')} >> ${outPath}`); -}; - -var copyTranslations = function(outDir) { - execSync('git submodule update --init -- translation') - execSync(`cp -rf translation/* ${outDir}/_locales/`); -}; - -var getDisplayName = function(locale) { - var code = locale.split("_")[0]; - try { - var name = cldr.extractLanguageDisplayNames(code)[code]; - } - catch(e) { - return ''; - } - if (name === undefined) { - return ''; - } - return name; -} - -var availableLangs = function() { - let out = "const availableLangs = new Set([\n"; - let dirs = readdirSync('translation').filter((f) => { - const s = statSync(`translation/${f}`); - return s.isDirectory(); - }); - dirs.push('en_US'); - dirs.sort(); - dirs = dirs.map(d => ` '${d}',`); - out += dirs.join("\n"); - out += "\n]);\n\n"; - return out; -}; - -var translatedLangs = function() { - let out = "const availableLangs = {\n"; - let dirs = readdirSync('translation').filter((f) => { - const s = statSync(`translation/${f}`); - return s.isDirectory(); - }); - dirs.push('en_US'); - dirs.sort(); - dirs = dirs.map(d => `'${d}': {"name": '${getDisplayName(d)}'},`); - out += dirs.join("\n"); - out += "\n};\n\n"; - return out; -}; -var tasks = new Map(); - -var task = function(key, msg, func) { - tasks.set(key, { - msg, func - }); -}; - -task('test', 'snowflake unit tests', function() { - var jasmineFiles, outFile, proc; - execSync('mkdir -p test'); - execSync('jasmine init >&-'); - // Simply concat all the files because we're not using node exports. - jasmineFiles = FILES.concat('init-testing.js', FILES_SPEC); - outFile = 'test/bundle.spec.js'; - execSync('echo "TESTING = true" > ' + outFile); - execSync('cat ' + jasmineFiles.join(' ') + ' | cat >> ' + outFile); - proc = spawn('jasmine', ['test/bundle.spec.js'], { - stdio: 'inherit' - }); - proc.on("exit", function(code) { - process.exit(code); - }); -}); - -task('build', 'build the snowflake proxy', function() { - const outDir = 'build'; - execSync(`rm -rf ${outDir}`); - execSync(`cp -r ${STATIC}/ ${outDir}/`); - copyTranslations(outDir); - concatJS(outDir, 'badge', 'embed.js', availableLangs()); - writeFileSync(`${outDir}/index.js`, translatedLangs(), 'utf8'); - execSync(`cat ${STATIC}/index.js >> ${outDir}/index.js`); - console.log('Snowflake prepared.'); -}); - -task('webext', 'build the webextension', function() { - const outDir = 'webext'; - execSync(`git clean -f -x -d ${outDir}/`); - execSync(`cp -r ${STATIC}/{${SHARED_FILES.join(',')}} ${outDir}/`, { shell: '/bin/bash' }); - copyTranslations(outDir); - concatJS(outDir, 'webext', 'snowflake.js', ''); - console.log('Webextension prepared.'); -}); - -task('node', 'build the node binary', function() { - execSync('mkdir -p build'); - concatJS('build', 'node', 'snowflake.js', ''); - console.log('Node prepared.'); -}); - -task('pack-webext', 'pack the webextension for deployment', function() { - try { - execSync(`rm -f source.zip`); - execSync(`rm -f webext/webext.zip`); - } catch (error) { - //Usually this happens because the zip files were removed previously - console.log('Error removing zip files'); - } - execSync(`git submodule update --remote`); - var version = process.argv[3]; - console.log(version); - var manifest = require('./webext/manifest.json') - manifest.version = version; - writeFileSync('./webext/manifest.json', JSON.stringify(manifest, null, 2), 'utf8'); - execSync(`git commit -am "bump version to ${version}"`); - try { - execSync(`git tag webext-${version}`); - } catch (error) { - console.log('Error creating git tag'); - // Revert changes - execSync(`git reset HEAD~`); - execSync(`git checkout ./webext/manifest.json`); - execSync(`git submodule update`); - return; - } - execSync(`git archive -o source.zip HEAD .`); - execSync(`npm run webext`); - execSync(`cd webext && zip -Xr webext.zip ./*`); -}); - -task('clean', 'remove all built files', function() { - execSync('rm -rf build test spec/support'); -}); - -task('library', 'build the library', function() { - concatJS('.', '', 'snowflake-library.js', ''); - console.log('Library prepared.'); -}); - -var cmd = process.argv[2]; - -if (tasks.has(cmd)) { - var t = tasks.get(cmd); - console.log(t.msg); - t.func(); -} else { - console.error('Command not supported.'); - - console.log('Commands:'); - - tasks.forEach(function(value, key) { - console.log(key + ' - ' + value.msg); - }) -} diff --git a/proxy/package.json b/proxy/package.json deleted file mode 100644 index 772746e..0000000 --- a/proxy/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "snowflake-pt", - "version": "0.0.0-git", - "description": "Snowflake is a WebRTC pluggable transport for Tor.", - "main": "build/snowflake.js", - "directories": { - "test": "test" - }, - "scripts": { - "test": "node make.js test", - "build": "node make.js build", - "webext": "node make.js webext", - "library": "node make.js library", - "pack-webext": "node make.js pack-webext", - "clean": "node make.js clean", - "prepublish": "node make.js node", - "start": "node build/snowflake.js", - "lint": "eslint . --ext .js" - }, - "bin": { - "snowflake": "build/snowflake.js" - }, - "author": "Serene Han", - "license": "BSD-3-Clause", - "devDependencies": { - "eslint": "^6.0.1", - "jasmine": "2.5.2" - }, - "dependencies": { - "cldr": "^5.4.1", - "wrtc": "^0.0.61", - "ws": "^3.3.1", - "xmlhttprequest": "^1.8.0" - } -} diff --git a/proxy/proxypair.js b/proxy/proxypair.js deleted file mode 100644 index 25eaa9d..0000000 --- a/proxy/proxypair.js +++ /dev/null @@ -1,249 +0,0 @@ -/* global snowflake, log, dbg, Util, PeerConnection, Parse, WS */ - -/* -Represents a single: - - client <-- webrtc --> snowflake <-- websocket --> relay - -Every ProxyPair has a Snowflake ID, which is necessary when responding to the -Broker with an WebRTC answer. -*/ - -class ProxyPair { - - /* - Constructs a ProxyPair where: - - @relayAddr is the destination relay - - @rateLimit specifies a rate limit on traffic - */ - constructor(relayAddr, rateLimit, pcConfig) { - this.prepareDataChannel = this.prepareDataChannel.bind(this); - this.connectRelay = this.connectRelay.bind(this); - this.onClientToRelayMessage = this.onClientToRelayMessage.bind(this); - this.onRelayToClientMessage = this.onRelayToClientMessage.bind(this); - this.onError = this.onError.bind(this); - this.flush = this.flush.bind(this); - - this.relayAddr = relayAddr; - this.rateLimit = rateLimit; - this.pcConfig = pcConfig; - this.id = Util.genSnowflakeID(); - this.c2rSchedule = []; - this.r2cSchedule = []; - } - - // Prepare a WebRTC PeerConnection and await for an SDP offer. - begin() { - this.pc = new PeerConnection(this.pcConfig, { - optional: [ - { - DtlsSrtpKeyAgreement: true - }, - { - RtpDataChannels: false - } - ] - }); - this.pc.onicecandidate = (evt) => { - // Browser sends a null candidate once the ICE gathering completes. - if (null === evt.candidate) { - // TODO: Use a promise.all to tell Snowflake about all offers at once, - // once multiple proxypairs are supported. - dbg('Finished gathering ICE candidates.'); - return snowflake.broker.sendAnswer(this.id, this.pc.localDescription); - } - }; - // OnDataChannel triggered remotely from the client when connection succeeds. - return this.pc.ondatachannel = (dc) => { - var channel; - channel = dc.channel; - dbg('Data Channel established...'); - this.prepareDataChannel(channel); - return this.client = channel; - }; - } - - receiveWebRTCOffer(offer) { - if ('offer' !== offer.type) { - log('Invalid SDP received -- was not an offer.'); - return false; - } - try { - this.pc.setRemoteDescription(offer); - } catch (error) { - log('Invalid SDP message.'); - return false; - } - dbg('SDP ' + offer.type + ' successfully received.'); - return true; - } - - // Given a WebRTC DataChannel, prepare callbacks. - prepareDataChannel(channel) { - channel.onopen = () => { - log('WebRTC DataChannel opened!'); - snowflake.ui.setActive(true); - // This is the point when the WebRTC datachannel is done, so the next step - // is to establish websocket to the server. - return this.connectRelay(); - }; - channel.onclose = () => { - log('WebRTC DataChannel closed.'); - snowflake.ui.setStatus('disconnected by webrtc.'); - snowflake.ui.setActive(false); - this.flush(); - return this.close(); - }; - channel.onerror = function() { - return log('Data channel error!'); - }; - channel.binaryType = "arraybuffer"; - return channel.onmessage = this.onClientToRelayMessage; - } - - // Assumes WebRTC datachannel is connected. - connectRelay() { - var params, peer_ip, ref; - dbg('Connecting to relay...'); - // Get a remote IP address from the PeerConnection, if possible. Add it to - // the WebSocket URL's query string if available. - // MDN marks remoteDescription as "experimental". However the other two - // options, currentRemoteDescription and pendingRemoteDescription, which - // are not marked experimental, were undefined when I tried them in Firefox - // 52.2.0. - // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/remoteDescription - peer_ip = Parse.ipFromSDP((ref = this.pc.remoteDescription) != null ? ref.sdp : void 0); - params = []; - if (peer_ip != null) { - params.push(["client_ip", peer_ip]); - } - var relay = this.relay = WS.makeWebsocket(this.relayAddr, params); - this.relay.label = 'websocket-relay'; - this.relay.onopen = () => { - if (this.timer) { - clearTimeout(this.timer); - this.timer = 0; - } - log(relay.label + ' connected!'); - return snowflake.ui.setStatus('connected'); - }; - this.relay.onclose = () => { - log(relay.label + ' closed.'); - snowflake.ui.setStatus('disconnected.'); - snowflake.ui.setActive(false); - this.flush(); - return this.close(); - }; - this.relay.onerror = this.onError; - this.relay.onmessage = this.onRelayToClientMessage; - // TODO: Better websocket timeout handling. - return this.timer = setTimeout((() => { - if (0 === this.timer) { - return; - } - log(relay.label + ' timed out connecting.'); - return relay.onclose(); - }), 5000); - } - - // WebRTC --> websocket - onClientToRelayMessage(msg) { - dbg('WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes'); - this.c2rSchedule.push(msg.data); - return this.flush(); - } - - // websocket --> WebRTC - onRelayToClientMessage(event) { - dbg('websocket --> WebRTC data: ' + event.data.byteLength + ' bytes'); - this.r2cSchedule.push(event.data); - return this.flush(); - } - - onError(event) { - var ws; - ws = event.target; - log(ws.label + ' error.'); - return this.close(); - } - - // Close both WebRTC and websocket. - close() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = 0; - } - if (this.webrtcIsReady()) { - this.client.close(); - } - if (this.peerConnOpen()) { - this.pc.close(); - } - if (this.relayIsReady()) { - this.relay.close(); - } - this.onCleanup(); - } - - // Send as much data in both directions as the rate limit currently allows. - flush() { - var busy, checkChunks; - if (this.flush_timeout_id) { - clearTimeout(this.flush_timeout_id); - } - this.flush_timeout_id = null; - busy = true; - checkChunks = () => { - var chunk; - busy = false; - // WebRTC --> websocket - if (this.relayIsReady() && this.relay.bufferedAmount < this.MAX_BUFFER && this.c2rSchedule.length > 0) { - chunk = this.c2rSchedule.shift(); - this.rateLimit.update(chunk.byteLength); - this.relay.send(chunk); - busy = true; - } - // websocket --> WebRTC - if (this.webrtcIsReady() && this.client.bufferedAmount < this.MAX_BUFFER && this.r2cSchedule.length > 0) { - chunk = this.r2cSchedule.shift(); - this.rateLimit.update(chunk.byteLength); - this.client.send(chunk); - return busy = true; - } - }; - while (busy && !this.rateLimit.isLimited()) { - checkChunks(); - } - if (this.r2cSchedule.length > 0 || this.c2rSchedule.length > 0 || (this.relayIsReady() && this.relay.bufferedAmount > 0) || (this.webrtcIsReady() && this.client.bufferedAmount > 0)) { - return this.flush_timeout_id = setTimeout(this.flush, this.rateLimit.when() * 1000); - } - } - - webrtcIsReady() { - return null !== this.client && 'open' === this.client.readyState; - } - - relayIsReady() { - return (null !== this.relay) && (WebSocket.OPEN === this.relay.readyState); - } - - isClosed(ws) { - return void 0 === ws || WebSocket.CLOSED === ws.readyState; - } - - peerConnOpen() { - return (null !== this.pc) && ('closed' !== this.pc.connectionState); - } - -} - -ProxyPair.prototype.MAX_BUFFER = 10 * 1024 * 1024; - -ProxyPair.prototype.pc = null; -ProxyPair.prototype.client = null; // WebRTC Data channel -ProxyPair.prototype.relay = null; // websocket - -ProxyPair.prototype.timer = 0; -ProxyPair.prototype.flush_timeout_id = null; - -ProxyPair.prototype.onCleanup = null; diff --git a/proxy/shims.js b/proxy/shims.js deleted file mode 100644 index 5d93183..0000000 --- a/proxy/shims.js +++ /dev/null @@ -1,31 +0,0 @@ -/* global module, require */ - -/* -WebRTC shims for multiple browsers. -*/ - -if (typeof module !== "undefined" && module !== null ? module.exports : void 0) { - window = {}; - document = { - getElementById: function() { - return null; - } - }; - chrome = {}; - location = { search: '' }; - ({ URLSearchParams } = require('url')); - if ((typeof TESTING === "undefined" || TESTING === null) || !TESTING) { - webrtc = require('wrtc'); - PeerConnection = webrtc.RTCPeerConnection; - IceCandidate = webrtc.RTCIceCandidate; - SessionDescription = webrtc.RTCSessionDescription; - WebSocket = require('ws'); - ({ XMLHttpRequest } = require('xmlhttprequest')); - } -} else { - PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; - IceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate; - SessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription; - WebSocket = window.WebSocket; - XMLHttpRequest = window.XMLHttpRequest; -} diff --git a/proxy/snowflake.js b/proxy/snowflake.js deleted file mode 100644 index 0e9730e..0000000 --- a/proxy/snowflake.js +++ /dev/null @@ -1,165 +0,0 @@ -/* global log, dbg, DummyRateLimit, BucketRateLimit, SessionDescription, ProxyPair */ - -/* -A JavaScript WebRTC snowflake proxy - -Uses WebRTC from the client, and Websocket to the server. - -Assume that the webrtc client plugin is always the offerer, in which case -this proxy must always act as the answerer. - -TODO: More documentation -*/ - -// Minimum viable snowflake for now - just 1 client. -class Snowflake { - - // Prepare the Snowflake with a Broker (to find clients) and optional UI. - constructor(config, ui, broker) { - this.receiveOffer = this.receiveOffer.bind(this); - - this.config = config; - this.ui = ui; - this.broker = broker; - this.proxyPairs = []; - if (void 0 === this.config.rateLimitBytes) { - this.rateLimit = new DummyRateLimit(); - } else { - this.rateLimit = new BucketRateLimit(this.config.rateLimitBytes * this.config.rateLimitHistory, this.config.rateLimitHistory); - } - this.retries = 0; - } - - // Set the target relay address spec, which is expected to be websocket. - // TODO: Should potentially fetch the target from broker later, or modify - // entirely for the Tor-independent version. - setRelayAddr(relayAddr) { - this.relayAddr = relayAddr; - log('Using ' + relayAddr.host + ':' + relayAddr.port + ' as Relay.'); - return true; - } - - // Initialize WebRTC PeerConnection, which requires beginning the signalling - // process. |pollBroker| automatically arranges signalling. - beginWebRTC() { - this.pollBroker(); - return this.pollInterval = setInterval((() => { - return this.pollBroker(); - }), this.config.defaultBrokerPollInterval); - } - - // Regularly poll Broker for clients to serve until this snowflake is - // serving at capacity, at which point stop polling. - pollBroker() { - var msg, pair, recv; - // Poll broker for clients. - pair = this.makeProxyPair(); - if (!pair) { - log('At client capacity.'); - return; - } - log('Polling broker..'); - // Do nothing until a new proxyPair is available. - msg = 'Polling for client ... '; - if (this.retries > 0) { - msg += '[retries: ' + this.retries + ']'; - } - this.ui.setStatus(msg); - recv = this.broker.getClientOffer(pair.id); - recv.then((desc) => { - if (!this.receiveOffer(pair, desc)) { - return pair.close(); - } - //set a timeout for channel creation - return setTimeout((() => { - if (!pair.webrtcIsReady()) { - log('proxypair datachannel timed out waiting for open'); - return pair.close(); - } - }), 20000); // 20 second timeout - }, function() { - //on error, close proxy pair - return pair.close(); - }); - return this.retries++; - } - - // Receive an SDP offer from some client assigned by the Broker, - // |pair| - an available ProxyPair. - receiveOffer(pair, desc) { - var e, offer, sdp; - try { - offer = JSON.parse(desc); - dbg('Received:\n\n' + offer.sdp + '\n'); - sdp = new SessionDescription(offer); - if (pair.receiveWebRTCOffer(sdp)) { - this.sendAnswer(pair); - return true; - } else { - return false; - } - } catch (error) { - e = error; - log('ERROR: Unable to receive Offer: ' + e); - return false; - } - } - - sendAnswer(pair) { - var fail, next; - next = function(sdp) { - dbg('webrtc: Answer ready.'); - return pair.pc.setLocalDescription(sdp).catch(fail); - }; - fail = function() { - pair.close(); - return dbg('webrtc: Failed to create or set Answer'); - }; - return pair.pc.createAnswer().then(next).catch(fail); - } - - makeProxyPair() { - if (this.proxyPairs.length >= this.config.maxNumClients) { - return null; - } - var pair; - pair = new ProxyPair(this.relayAddr, this.rateLimit, this.config.pcConfig); - this.proxyPairs.push(pair); - - log('Snowflake IDs: ' + (this.proxyPairs.map(function(p) { - return p.id; - })).join(' | ')); - - pair.onCleanup = () => { - var ind; - // Delete from the list of proxy pairs. - ind = this.proxyPairs.indexOf(pair); - if (ind > -1) { - return this.proxyPairs.splice(ind, 1); - } - }; - pair.begin(); - return pair; - } - - // Stop all proxypairs. - disable() { - var results; - log('Disabling Snowflake.'); - clearInterval(this.pollInterval); - results = []; - while (this.proxyPairs.length > 0) { - results.push(this.proxyPairs.pop().close()); - } - return results; - } - -} - -Snowflake.prototype.relayAddr = null; -Snowflake.prototype.rateLimit = null; -Snowflake.prototype.pollInterval = null; - -Snowflake.MESSAGE = { - CONFIRMATION: 'You\'re currently serving a Tor user via Snowflake.' -}; diff --git a/proxy/spec/broker.spec.js b/proxy/spec/broker.spec.js deleted file mode 100644 index 28a66c4..0000000 --- a/proxy/spec/broker.spec.js +++ /dev/null @@ -1,131 +0,0 @@ -/* global expect, it, describe, spyOn, Broker */ - -/* -jasmine tests for Snowflake broker -*/ - -// fake xhr -// class XMLHttpRequest -class XMLHttpRequest { - constructor() { - this.onreadystatechange = null; - } - open() {} - setRequestHeader() {} - send() {} -}; - -XMLHttpRequest.prototype.DONE = 1; - - -describe('Broker', function() { - - it('can be created', function() { - var b; - var config = new Config; - config.brokerUrl = 'fake'; - b = new Broker(config); - expect(b.url).toEqual('https://fake/'); - expect(b.id).not.toBeNull(); - }); - - describe('getClientOffer', function() { - - it('polls and promises a client offer', function(done) { - var b, poll; - var config = new Config; - config.brokerUrl = 'fake'; - b = new Broker(config); - // fake successful request and response from broker. - spyOn(b, '_postRequest').and.callFake(function() { - b._xhr.readyState = b._xhr.DONE; - b._xhr.status = Broker.CODE.OK; - b._xhr.responseText = '{"Status":"client match","Offer":"fake offer"}'; - return b._xhr.onreadystatechange(); - }); - poll = b.getClientOffer(); - expect(poll).not.toBeNull(); - expect(b._postRequest).toHaveBeenCalled(); - return poll.then(function(desc) { - expect(desc).toEqual('fake offer'); - return done(); - }).catch(function() { - fail('should not reject on Broker.CODE.OK'); - return done(); - }); - }); - - it('rejects if the broker timed-out', function(done) { - var b, poll; - var config = new Config; - config.brokerUrl = 'fake'; - b = new Broker(config); - // fake timed-out request from broker - spyOn(b, '_postRequest').and.callFake(function() { - b._xhr.readyState = b._xhr.DONE; - b._xhr.status = Broker.CODE.OK; - b._xhr.responseText = '{"Status":"no match"}'; - return b._xhr.onreadystatechange(); - }); - poll = b.getClientOffer(); - expect(poll).not.toBeNull(); - expect(b._postRequest).toHaveBeenCalled(); - return poll.then(function(desc) { - fail('should not fulfill with "Status: no match"'); - return done(); - }, function(err) { - expect(err).toBe(Broker.MESSAGE.TIMEOUT); - return done(); - }); - }); - - it('rejects on any other status', function(done) { - var b, poll; - var config = new Config; - config.brokerUrl = 'fake'; - b = new Broker(config); - // fake timed-out request from broker - spyOn(b, '_postRequest').and.callFake(function() { - b._xhr.readyState = b._xhr.DONE; - b._xhr.status = 1337; - return b._xhr.onreadystatechange(); - }); - poll = b.getClientOffer(); - expect(poll).not.toBeNull(); - expect(b._postRequest).toHaveBeenCalled(); - return poll.then(function(desc) { - fail('should not fulfill on non-OK status'); - return done(); - }, function(err) { - expect(err).toBe(Broker.MESSAGE.UNEXPECTED); - expect(b._xhr.status).toBe(1337); - return done(); - }); - - }); - - }); - - it('responds to the broker with answer', function() { - var config = new Config; - config.brokerUrl = 'fake'; - var b = new Broker(config); - spyOn(b, '_postRequest'); - b.sendAnswer('fake id', 123); - expect(b._postRequest).toHaveBeenCalledWith(jasmine.any(Object), 'answer', '{"Version":"1.0","Sid":"fake id","Answer":"123"}'); - }); - - it('POST XMLHttpRequests to the broker', function() { - var config = new Config; - config.brokerUrl = 'fake'; - var b = new Broker(config); - b._xhr = new XMLHttpRequest(); - spyOn(b._xhr, 'open'); - spyOn(b._xhr, 'setRequestHeader'); - spyOn(b._xhr, 'send'); - b._postRequest(b._xhr, 'test', 'data'); - expect(b._xhr.open).toHaveBeenCalled(); - expect(b._xhr.send).toHaveBeenCalled(); - }); - -}); diff --git a/proxy/spec/init.spec.js b/proxy/spec/init.spec.js deleted file mode 100644 index 593add9..0000000 --- a/proxy/spec/init.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -/* global expect, it, describe, Snowflake, UI */ - -// Fake snowflake to interact with - -var snowflake = { - ui: new UI, - broker: { - sendAnswer: function() {} - } -}; - -describe('Init', function() { - - it('gives a dialog when closing, only while active', function() { - silenceNotifications = false; - ui.setActive(true); - var msg = window.onbeforeunload(); - expect(ui.active).toBe(true); - expect(msg).toBe(Snowflake.MESSAGE.CONFIRMATION); - ui.setActive(false); - msg = window.onbeforeunload(); - expect(ui.active).toBe(false); - expect(msg).toBe(null); - }); - - it('does not give a dialog when silent flag is on', function() { - silenceNotifications = true; - ui.setActive(true); - var msg = window.onbeforeunload(); - expect(ui.active).toBe(true); - expect(msg).toBe(null); - }); - -}); diff --git a/proxy/spec/proxypair.spec.js b/proxy/spec/proxypair.spec.js deleted file mode 100644 index f15d6d2..0000000 --- a/proxy/spec/proxypair.spec.js +++ /dev/null @@ -1,163 +0,0 @@ -/* global expect, it, describe, spyOn */ - -/* -jasmine tests for Snowflake proxypair -*/ - -// Replacement for MessageEvent constructor. -// https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/MessageEvent -var MessageEvent = function(type, init) { - return init; -}; - -// Asymmetic matcher that checks that two arrays have the same contents. -var arrayMatching = function(sample) { - return { - asymmetricMatch: function(other) { - var _, a, b, i, j, len; - a = new Uint8Array(sample); - b = new Uint8Array(other); - if (a.length !== b.length) { - return false; - } - for (i = j = 0, len = a.length; j < len; i = ++j) { - _ = a[i]; - if (a[i] !== b[i]) { - return false; - } - } - return true; - }, - jasmineToString: function() { - return ''; - } - }; -}; - -describe('ProxyPair', function() { - - var config, destination, fakeRelay, pp, rateLimit; - fakeRelay = Parse.address('0.0.0.0:12345'); - rateLimit = new DummyRateLimit; - config = new Config; - destination = []; - - // Using the mock PeerConnection definition from spec/snowflake.spec.js - var pp = new ProxyPair(fakeRelay, rateLimit, config.pcConfig); - - beforeEach(function() { - return pp.begin(); - }); - - it('begins webrtc connection', function() { - return expect(pp.pc).not.toBeNull(); - }); - - describe('accepts WebRTC offer from some client', function() { - - beforeEach(function() { - return pp.begin(); - }); - - it('rejects invalid offers', function() { - expect(typeof pp.pc.setRemoteDescription).toBe("function"); - expect(pp.pc).not.toBeNull(); - expect(pp.receiveWebRTCOffer({})).toBe(false); - expect(pp.receiveWebRTCOffer({ - type: 'answer' - })).toBe(false); - }); - - it('accepts valid offers', function() { - expect(pp.pc).not.toBeNull(); - expect(pp.receiveWebRTCOffer({ - type: 'offer', - sdp: 'foo' - })).toBe(true); - }); - - }); - - it('responds with a WebRTC answer correctly', function() { - spyOn(snowflake.broker, 'sendAnswer'); - pp.pc.onicecandidate({ - candidate: null - }); - expect(snowflake.broker.sendAnswer).toHaveBeenCalled(); - }); - - it('handles a new data channel correctly', function() { - expect(pp.client).toBeNull(); - pp.pc.ondatachannel({ - channel: {} - }); - expect(pp.client).not.toBeNull(); - expect(pp.client.onopen).not.toBeNull(); - expect(pp.client.onclose).not.toBeNull(); - expect(pp.client.onerror).not.toBeNull(); - expect(pp.client.onmessage).not.toBeNull(); - }); - - it('connects to the relay once datachannel opens', function() { - spyOn(pp, 'connectRelay'); - pp.active = true; - pp.client.onopen(); - expect(pp.connectRelay).toHaveBeenCalled(); - }); - - it('connects to a relay', function() { - pp.connectRelay(); - expect(pp.relay.onopen).not.toBeNull(); - expect(pp.relay.onclose).not.toBeNull(); - expect(pp.relay.onerror).not.toBeNull(); - expect(pp.relay.onmessage).not.toBeNull(); - }); - - describe('flushes data between client and relay', function() { - - it('proxies data from client to relay', function() { - var msg; - pp.pc.ondatachannel({ - channel: { - bufferedAmount: 0, - readyState: "open", - send: function(data) {} - } - }); - spyOn(pp.client, 'send'); - spyOn(pp.relay, 'send'); - msg = new MessageEvent("message", { - data: Uint8Array.from([1, 2, 3]).buffer - }); - pp.onClientToRelayMessage(msg); - pp.flush(); - expect(pp.client.send).not.toHaveBeenCalled(); - expect(pp.relay.send).toHaveBeenCalledWith(arrayMatching([1, 2, 3])); - }); - - it('proxies data from relay to client', function() { - var msg; - spyOn(pp.client, 'send'); - spyOn(pp.relay, 'send'); - msg = new MessageEvent("message", { - data: Uint8Array.from([4, 5, 6]).buffer - }); - pp.onRelayToClientMessage(msg); - pp.flush(); - expect(pp.client.send).toHaveBeenCalledWith(arrayMatching([4, 5, 6])); - expect(pp.relay.send).not.toHaveBeenCalled(); - }); - - it('sends nothing with nothing to flush', function() { - spyOn(pp.client, 'send'); - spyOn(pp.relay, 'send'); - pp.flush(); - expect(pp.client.send).not.toHaveBeenCalled(); - expect(pp.relay.send).not.toHaveBeenCalled(); - }); - - }); - -}); - -// TODO: rate limit tests diff --git a/proxy/spec/snowflake.spec.js b/proxy/spec/snowflake.spec.js deleted file mode 100644 index 970947b..0000000 --- a/proxy/spec/snowflake.spec.js +++ /dev/null @@ -1,103 +0,0 @@ -/* global expect, it, describe, spyOn, Snowflake, Config, UI */ - -/* -jasmine tests for Snowflake -*/ - -// Fake browser functionality: -class PeerConnection { - setRemoteDescription() { - return true; - } - send() {} -} - -class SessionDescription {} -SessionDescription.prototype.type = 'offer'; - -class WebSocket { - constructor() { - this.bufferedAmount = 0; - } - send() {} -} -WebSocket.prototype.OPEN = 1; -WebSocket.prototype.CLOSED = 0; - -var log = function() {}; - -var config = new Config(); - -var ui = new UI(); - -class FakeBroker { - getClientOffer() { - return new Promise(function() { - return {}; - }); - } -} - -describe('Snowflake', function() { - - it('constructs correctly', function() { - var s; - s = new Snowflake(config, ui, { - fake: 'broker' - }); - expect(s.rateLimit).not.toBeNull(); - expect(s.broker).toEqual({ - fake: 'broker' - }); - expect(s.ui).not.toBeNull(); - expect(s.retries).toBe(0); - }); - - it('sets relay address correctly', function() { - var s; - s = new Snowflake(config, ui, null); - s.setRelayAddr('foo'); - expect(s.relayAddr).toEqual('foo'); - }); - - it('initalizes WebRTC connection', function() { - var s; - s = new Snowflake(config, ui, new FakeBroker()); - spyOn(s.broker, 'getClientOffer').and.callThrough(); - s.beginWebRTC(); - expect(s.retries).toBe(1); - expect(s.broker.getClientOffer).toHaveBeenCalled(); - }); - - it('receives SDP offer and sends answer', function() { - var pair, s; - s = new Snowflake(config, ui, new FakeBroker()); - pair = { - receiveWebRTCOffer: function() {} - }; - spyOn(pair, 'receiveWebRTCOffer').and.returnValue(true); - spyOn(s, 'sendAnswer'); - s.receiveOffer(pair, '{"type":"offer","sdp":"foo"}'); - expect(s.sendAnswer).toHaveBeenCalled(); - }); - - it('does not send answer when receiving invalid offer', function() { - var pair, s; - s = new Snowflake(config, ui, new FakeBroker()); - pair = { - receiveWebRTCOffer: function() {} - }; - spyOn(pair, 'receiveWebRTCOffer').and.returnValue(false); - spyOn(s, 'sendAnswer'); - s.receiveOffer(pair, '{"type":"not a good offer","sdp":"foo"}'); - expect(s.sendAnswer).not.toHaveBeenCalled(); - }); - - it('can make a proxypair', function() { - var s; - s = new Snowflake(config, ui, new FakeBroker()); - s.makeProxyPair(); - expect(s.proxyPairs.length).toBe(1); - }); - -}); diff --git a/proxy/spec/ui.spec.js b/proxy/spec/ui.spec.js deleted file mode 100644 index dc9aa35..0000000 --- a/proxy/spec/ui.spec.js +++ /dev/null @@ -1,68 +0,0 @@ -/* global expect, it, describe, spyOn, DebugUI */ -/* eslint no-redeclare: 0 */ - -/* -jasmine tests for Snowflake UI -*/ - -var document = { - getElementById: function() { - return {}; - }, - createTextNode: function(txt) { - return txt; - } -}; - -describe('UI', function() { - - it('activates debug mode when badge does not exist', function() { - var u; - spyOn(document, 'getElementById').and.callFake(function(id) { - if ('badge' === id) { - return null; - } - return {}; - }); - u = new DebugUI(); - expect(document.getElementById.calls.count()).toEqual(2); - expect(u.$status).not.toBeNull(); - expect(u.$msglog).not.toBeNull(); - }); - - it('sets status message when in debug mode', function() { - var u; - u = new DebugUI(); - u.$status = { - innerHTML: '', - appendChild: function(txt) { - return this.innerHTML = txt; - } - }; - u.setStatus('test'); - expect(u.$status.innerHTML).toEqual('Status: test'); - }); - - it('sets message log css correctly for debug mode', function() { - var u; - u = new DebugUI(); - u.setActive(true); - expect(u.$msglog.className).toEqual('active'); - u.setActive(false); - expect(u.$msglog.className).toEqual(''); - }); - - it('logs to the textarea correctly when debug mode', function() { - var u; - u = new DebugUI(); - u.$msglog = { - value: '', - scrollTop: 0, - scrollHeight: 1337 - }; - u.log('test'); - expect(u.$msglog.value).toEqual('test\n'); - expect(u.$msglog.scrollTop).toEqual(1337); - }); - -}); diff --git a/proxy/spec/util.spec.js b/proxy/spec/util.spec.js deleted file mode 100644 index 6eb5be4..0000000 --- a/proxy/spec/util.spec.js +++ /dev/null @@ -1,252 +0,0 @@ -/* global expect, it, describe, Parse, Params */ - -/* -jasmine tests for Snowflake utils -*/ - -describe('Parse', function() { - - describe('cookie', function() { - - it('parses correctly', function() { - expect(Parse.cookie('')).toEqual({}); - expect(Parse.cookie('a=b')).toEqual({ - a: 'b' - }); - expect(Parse.cookie('a=b=c')).toEqual({ - a: 'b=c' - }); - expect(Parse.cookie('a=b; c=d')).toEqual({ - a: 'b', - c: 'd' - }); - expect(Parse.cookie('a=b ; c=d')).toEqual({ - a: 'b', - c: 'd' - }); - expect(Parse.cookie('a= b')).toEqual({ - a: 'b' - }); - expect(Parse.cookie('a=')).toEqual({ - a: '' - }); - expect(Parse.cookie('key')).toBeNull(); - expect(Parse.cookie('key=%26%20')).toEqual({ - key: '& ' - }); - expect(Parse.cookie('a=\'\'')).toEqual({ - a: '\'\'' - }); - }); - - }); - - describe('address', function() { - - it('parses IPv4', function() { - expect(Parse.address('')).toBeNull(); - expect(Parse.address('3.3.3.3:4444')).toEqual({ - host: '3.3.3.3', - port: 4444 - }); - expect(Parse.address('3.3.3.3')).toBeNull(); - expect(Parse.address('3.3.3.3:0x1111')).toBeNull(); - expect(Parse.address('3.3.3.3:-4444')).toBeNull(); - expect(Parse.address('3.3.3.3:65536')).toBeNull(); - }); - - it('parses IPv6', function() { - expect(Parse.address('[1:2::a:f]:4444')).toEqual({ - host: '1:2::a:f', - port: 4444 - }); - expect(Parse.address('[1:2::a:f]')).toBeNull(); - expect(Parse.address('[1:2::a:f]:0x1111')).toBeNull(); - expect(Parse.address('[1:2::a:f]:-4444')).toBeNull(); - expect(Parse.address('[1:2::a:f]:65536')).toBeNull(); - expect(Parse.address('[1:2::ffff:1.2.3.4]:4444')).toEqual({ - host: '1:2::ffff:1.2.3.4', - port: 4444 - }); - }); - - }); - - describe('byte count', function() { - - it('returns null for bad inputs', function() { - expect(Parse.byteCount("")).toBeNull(); - expect(Parse.byteCount("x")).toBeNull(); - expect(Parse.byteCount("1x")).toBeNull(); - expect(Parse.byteCount("1.x")).toBeNull(); - expect(Parse.byteCount("1.2x")).toBeNull(); - expect(Parse.byteCount("toString")).toBeNull(); - expect(Parse.byteCount("1toString")).toBeNull(); - expect(Parse.byteCount("1.toString")).toBeNull(); - expect(Parse.byteCount("1.2toString")).toBeNull(); - expect(Parse.byteCount("k")).toBeNull(); - expect(Parse.byteCount("m")).toBeNull(); - expect(Parse.byteCount("g")).toBeNull(); - expect(Parse.byteCount("K")).toBeNull(); - expect(Parse.byteCount("M")).toBeNull(); - expect(Parse.byteCount("G")).toBeNull(); - expect(Parse.byteCount("-1")).toBeNull(); - expect(Parse.byteCount("-1k")).toBeNull(); - expect(Parse.byteCount("1.2.3")).toBeNull(); - expect(Parse.byteCount("1.2.3k")).toBeNull(); - }); - - it('handles numbers without a suffix', function() { - expect(Parse.byteCount("10")).toEqual(10); - expect(Parse.byteCount("10.")).toEqual(10); - expect(Parse.byteCount("1.5")).toEqual(1.5); - }); - - it('handles lowercase suffixes', function() { - expect(Parse.byteCount("10k")).toEqual(10*1024); - expect(Parse.byteCount("10m")).toEqual(10*1024*1024); - expect(Parse.byteCount("10g")).toEqual(10*1024*1024*1024); - expect(Parse.byteCount("10.k")).toEqual(10*1024); - expect(Parse.byteCount("10.m")).toEqual(10*1024*1024); - expect(Parse.byteCount("10.g")).toEqual(10*1024*1024*1024); - expect(Parse.byteCount("1.5k")).toEqual(1.5*1024); - expect(Parse.byteCount("1.5m")).toEqual(1.5*1024*1024); - expect(Parse.byteCount("1.5G")).toEqual(1.5*1024*1024*1024); - }); - - it('handles uppercase suffixes', function() { - expect(Parse.byteCount("10K")).toEqual(10*1024); - expect(Parse.byteCount("10M")).toEqual(10*1024*1024); - expect(Parse.byteCount("10G")).toEqual(10*1024*1024*1024); - expect(Parse.byteCount("10.K")).toEqual(10*1024); - expect(Parse.byteCount("10.M")).toEqual(10*1024*1024); - expect(Parse.byteCount("10.G")).toEqual(10*1024*1024*1024); - expect(Parse.byteCount("1.5K")).toEqual(1.5*1024); - expect(Parse.byteCount("1.5M")).toEqual(1.5*1024*1024); - expect(Parse.byteCount("1.5G")).toEqual(1.5*1024*1024*1024); - }); - - }); - - describe('ipFromSDP', function() { - - var testCases = [ - { - // https://tools.ietf.org/html/rfc4566#section-5 - sdp: "v=0\no=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\ns=SDP Seminar\ni=A Seminar on the session description protocol\nu=http://www.example.com/seminars/sdp.pdf\ne=j.doe@example.com (Jane Doe)\nc=IN IP4 224.2.17.12/127\nt=2873397496 2873404696\na=recvonly\nm=audio 49170 RTP/AVP 0\nm=video 51372 RTP/AVP 99\na=rtpmap:99 h263-1998/90000", - expected: '224.2.17.12' - }, - { - // Missing c= line - sdp: "v=0\no=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\ns=SDP Seminar\ni=A Seminar on the session description protocol\nu=http://www.example.com/seminars/sdp.pdf\ne=j.doe@example.com (Jane Doe)\nt=2873397496 2873404696\na=recvonly\nm=audio 49170 RTP/AVP 0\nm=video 51372 RTP/AVP 99\na=rtpmap:99 h263-1998/90000", - expected: void 0 - }, - { - // Single line, IP address only - sdp: "c=IN IP4 224.2.1.1\n", - expected: '224.2.1.1' - }, - { - // Same, with TTL - sdp: "c=IN IP4 224.2.1.1/127\n", - expected: '224.2.1.1' - }, - { - // Same, with TTL and multicast addresses - sdp: "c=IN IP4 224.2.1.1/127/3\n", - expected: '224.2.1.1' - }, - { - // IPv6, address only - sdp: "c=IN IP6 FF15::101\n", - expected: 'ff15::101' - }, - { - // Same, with multicast addresses - sdp: "c=IN IP6 FF15::101/3\n", - expected: 'ff15::101' - }, - { - // Multiple c= lines - sdp: "c=IN IP4 1.2.3.4\nc=IN IP4 5.6.7.8", - expected: '1.2.3.4' - }, - { - // Modified from SDP sent by snowflake-client. - sdp: "v=0\no=- 7860378660295630295 2 IN IP4 127.0.0.1\ns=-\nt=0 0\na=group:BUNDLE data\na=msid-semantic: WMS\nm=application 54653 DTLS/SCTP 5000\nc=IN IP4 1.2.3.4\na=candidate:3581707038 1 udp 2122260223 192.168.0.1 54653 typ host generation 0 network-id 1 network-cost 50\na=candidate:2617212910 1 tcp 1518280447 192.168.0.1 59673 typ host tcptype passive generation 0 network-id 1 network-cost 50\na=candidate:2082671819 1 udp 1686052607 1.2.3.4 54653 typ srflx raddr 192.168.0.1 rport 54653 generation 0 network-id 1 network-cost 50\na=ice-ufrag:IBdf\na=ice-pwd:G3lTrrC9gmhQx481AowtkhYz\na=fingerprint:sha-256 53:F8:84:D9:3C:1F:A0:44:AA:D6:3C:65:80:D3:CB:6F:23:90:17:41:06:F9:9C:10:D8:48:4A:A8:B6:FA:14:A1\na=setup:actpass\na=mid:data\na=sctpmap:5000 webrtc-datachannel 1024", - expected: '1.2.3.4' - }, - { - // Improper character within IPv4 - sdp: "c=IN IP4 224.2z.1.1", - expected: void 0 - }, - { - // Improper character within IPv6 - sdp: "c=IN IP6 ff15:g::101", - expected: void 0 - }, - { - // Bogus "IP7" addrtype - sdp: "c=IN IP7 1.2.3.4\n", - expected: void 0 - } - ]; - - it('parses SDP', function() { - var i, len, ref, ref1, results, test; - results = []; - for (i = 0, len = testCases.length; i < len; i++) { - test = testCases[i]; - // https://tools.ietf.org/html/rfc4566#section-5: "The sequence # CRLF - // (0x0d0a) is used to end a record, although parsers SHOULD be tolerant - // and also accept records terminated with a single newline character." - // We represent the test cases with LF line endings for convenience, and - // test them both that way and with CRLF line endings. - expect((ref = Parse.ipFromSDP(test.sdp)) != null ? ref.toLowerCase() : void 0).toEqual(test.expected); - results.push(expect((ref1 = Parse.ipFromSDP(test.sdp.replace(/\n/, "\r\n"))) != null ? ref1.toLowerCase() : void 0).toEqual(test.expected)); - } - return results; - }); - - }); - -}); - -describe('Params', function() { - - describe('bool', function() { - - var getBool = function(query) { - return Params.getBool(new URLSearchParams(query), 'param', false); - }; - - it('parses correctly', function() { - expect(getBool('param=true')).toBe(true); - expect(getBool('param')).toBe(true); - expect(getBool('param=')).toBe(true); - expect(getBool('param=1')).toBe(true); - expect(getBool('param=0')).toBe(false); - expect(getBool('param=false')).toBe(false); - expect(getBool('param=unexpected')).toBeNull(); - expect(getBool('pram=true')).toBe(false); - }); - - }); - - describe('byteCount', function() { - - var DEFAULT = 77; - var getByteCount = function(query) { - return Params.getByteCount(new URLSearchParams(query), 'param', DEFAULT); - }; - - it('supports default values', function() { - expect(getByteCount('param=x')).toBeNull(); - expect(getByteCount('param=10')).toEqual(10); - expect(getByteCount('foo=10k')).toEqual(DEFAULT); - }); - - }); - -}); diff --git a/proxy/spec/websocket.spec.js b/proxy/spec/websocket.spec.js deleted file mode 100644 index 6c2ef2e..0000000 --- a/proxy/spec/websocket.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -/* global expect, it, describe, WS */ - -/* -jasmine tests for Snowflake websocket -*/ - -describe('BuildUrl', function() { - - it('should parse just protocol and host', function() { - expect(WS.buildUrl('http', 'example.com')).toBe('http://example.com'); - }); - - it('should handle different ports', function() { - expect(WS.buildUrl('http', 'example.com', 80)).toBe('http://example.com'); - expect(WS.buildUrl('http', 'example.com', 81)).toBe('http://example.com:81'); - expect(WS.buildUrl('http', 'example.com', 443)).toBe('http://example.com:443'); - expect(WS.buildUrl('http', 'example.com', 444)).toBe('http://example.com:444'); - }); - - it('should handle paths', function() { - expect(WS.buildUrl('http', 'example.com', 80, '/')).toBe('http://example.com/'); - expect(WS.buildUrl('http', 'example.com', 80, '/test?k=%#v')).toBe('http://example.com/test%3Fk%3D%25%23v'); - expect(WS.buildUrl('http', 'example.com', 80, '/test')).toBe('http://example.com/test'); - }); - - it('should handle params', function() { - expect(WS.buildUrl('http', 'example.com', 80, '/test', [['k', '%#v']])).toBe('http://example.com/test?k=%25%23v'); - expect(WS.buildUrl('http', 'example.com', 80, '/test', [['a', 'b'], ['c', 'd']])).toBe('http://example.com/test?a=b&c=d'); - }); - - it('should handle ips', function() { - expect(WS.buildUrl('http', '1.2.3.4')).toBe('http://1.2.3.4'); - expect(WS.buildUrl('http', '1:2::3:4')).toBe('http://[1:2::3:4]'); - }); - - it('should handle bogus', function() { - expect(WS.buildUrl('http', 'bog][us')).toBe('http://bog%5D%5Bus'); - expect(WS.buildUrl('http', 'bog:u]s')).toBe('http://bog%3Au%5Ds'); - }); - -}); diff --git a/proxy/static/.htaccess b/proxy/static/.htaccess deleted file mode 100644 index 1a8277f..0000000 --- a/proxy/static/.htaccess +++ /dev/null @@ -1,5 +0,0 @@ - - Header always unset X-Frame-Options - - -Redirect permanent /snowflake.html / diff --git a/proxy/static/SourceSansPro-Regular.ttf b/proxy/static/SourceSansPro-Regular.ttf deleted file mode 100644 index 278ad8aa0a09592a759e39f8fc06d0fa9d986fb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 293516 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%0ACauZ%OI`!GPd*?{ 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 !Ih$)5?K~W=vG3ozB3)0nf!55x3ZA5aar=Ra8Fj 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 WZJ|m11)WV1+;LQ8al|documentation wiki." - }, - "docWiki": { - "message": "documentation wiki" - }, - "browser": { - "message": "Browser" - }, - "censoredUsers": { - "message": "If your internet access is censored, you should download Tor Browser." - }, - "extension": { - "message": "Extension" - }, - "installExtension": { - "message": "If your internet access is not censored, you should consider installing the Snowflake extension to help users in censored networks. There is no need to worry about which websites people are accessing through your proxy. Their visible browsing IP address will match their Tor exit node, not yours." - }, - "installFirefox": { - "message": "Install in Firefox" - }, - "installChrome": { - "message": "Install in Chrome" - }, - "reportingBugs": { - "message": "Reporting Bugs" - }, - "fileBug": { - "message": "If you encounter problems with Snowflake as a client or a proxy, please consider filing a bug. To do so, you will have to," - }, - "sharedAccount": { - "message": "Either create an account or log in using the shared cypherpunks account with password writecode." - }, - "bugTracker": { - "message": "File a ticket using our bug tracker." - }, - "descriptive": { - "message": "Please try to be as descriptive as possible with your ticket and if possible include log messages that will help us reproduce the bug. Consider adding keywords snowflake-webextension or snowflake-client to let us know how which part of the Snowflake system is experiencing problems." - }, - "embed": { - "message": "Embed" - }, - "possible": { - "message": "It is now possible to embed the Snowflake badge on any website:" - }, - "looksLike": { - "message": "Which looks like this:" - } -} diff --git a/proxy/static/assets/arrowhead-right-12.svg b/proxy/static/assets/arrowhead-right-12.svg deleted file mode 100644 index 3f7e664..0000000 --- a/proxy/static/assets/arrowhead-right-12.svg +++ /dev/null @@ -1,4 +0,0 @@ - - \ No newline at end of file diff --git a/proxy/static/assets/arrowhead-right-dark-12.svg b/proxy/static/assets/arrowhead-right-dark-12.svg deleted file mode 100644 index 6534fd0..0000000 --- a/proxy/static/assets/arrowhead-right-dark-12.svg +++ /dev/null @@ -1,4 +0,0 @@ - - \ No newline at end of file diff --git a/proxy/static/assets/favicon.ico b/proxy/static/assets/favicon.ico deleted file mode 100644 index 48060b1ed06825aa9d58e3589ac208329567abc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmah|T}YEr7=A*h&TVS=hq1QVrhU_Nmj9E@T&6QLZ8m?%NtBeNjWqf(GZTM8D6k+z z=pq6^BM9ooi%Po42<;{V@giznL{N~57lE^JL^f@QW<&$UbOb`CjZh6qLW35{z+8E3ggUOJ zp^3IED-Rc!(VV!Ord<*xMJ!VgNryg-(NLS^zBR4|l;wFvyxQJDzu(XIGjsIVJv77_ zn5H-Lm@13eh^JL2Q4|s^-$Q~X*^-pX`Hw#OK73|tVI!wrzGYQz1(OmCJQ;0cOKdT- z)H+tC=XvG+3@9?3;+wxNE^+SlERScq`0T|Di=-l^C+*>B-9cV39c7ME$7Cq$aTF%@*S4gYnfq8<#|gjJNJ5+rr9C<5jk?d8wiR;`2EhFey)i6 z7k@0V@7yg`yU(&f%4BcR88(`othCrRt$%I5d}x)S2)}#p!6;W&SNZhKJ2rO=vQ&76 z++DOAEHpqP$_0^N7Cj^P1adp#6Kh=c%=gw2W8Nk_Z2M>L z271@x5u8#uZeQwGHCl - - - Fill-4 - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/proxy/static/assets/status-off.svg b/proxy/static/assets/status-off.svg deleted file mode 100644 index 843b278..0000000 --- a/proxy/static/assets/status-off.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - status-off - - - - \ No newline at end of file diff --git a/proxy/static/assets/status-on-dark.svg b/proxy/static/assets/status-on-dark.svg deleted file mode 100644 index bfc9894..0000000 --- a/proxy/static/assets/status-on-dark.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - Fill-4 - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/proxy/static/assets/status-on.svg b/proxy/static/assets/status-on.svg deleted file mode 100644 index 4cd2be8..0000000 --- a/proxy/static/assets/status-on.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - status-on - - - - \ No newline at end of file diff --git a/proxy/static/assets/status-running.svg b/proxy/static/assets/status-running.svg deleted file mode 100644 index dffb7ea..0000000 --- a/proxy/static/assets/status-running.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - status-on - - - - diff --git a/proxy/static/assets/toolbar-off-48.png b/proxy/static/assets/toolbar-off-48.png deleted file mode 100644 index 9a28a6f64fabc0688891d39b4dae5674d4732796..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3657 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 Ak^lez diff --git a/proxy/static/assets/toolbar-off-96.png b/proxy/static/assets/toolbar-off-96.png deleted file mode 100644 index d022b51507c463254275f88b3929fe9dd756b7bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7214 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 diff --git a/proxy/static/assets/toolbar-off.svg b/proxy/static/assets/toolbar-off.svg deleted file mode 100644 index 2b35669..0000000 --- a/proxy/static/assets/toolbar-off.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - toolbar_icon_grey - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/proxy/static/assets/toolbar-on-48.png b/proxy/static/assets/toolbar-on-48.png deleted file mode 100644 index 990ab302bf7f96720f6a46070cebfba490f8a11d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3674 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 diff --git a/proxy/static/assets/toolbar-on-96.png b/proxy/static/assets/toolbar-on-96.png deleted file mode 100644 index d0226b6c6d771650b5abe5ae5ba80cd398155248..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7355 zcmZ`;byQSev>s|;B&EAUK!)yyp*utYDd}#Io*_h92BeXY5|Hj3ln!YDsS!k)L6jcg z&2PQG-XHI-b??3>_Bv zot5>J0f5FdLbM$orp#)uX{-kTgmMD_&{zQA22%yy0{{Yr0DuEq06-=O0HEHr7vE87q1~0;m6!X>J=6Zva#J|E z{m;%L`=9Na7Rm%_pE8m2azOzIqCgJ|s)jOc7p@Jel#vufytU zag;^p;}zgb09XJv*gG>p5Y8TIL1Z`1JgggP^n~f&ik!g^w-#3c_nXpIg64~TYeX>i z3E)I^{{m8z^F5Jp8K(z?#w)KQSBI!*iz5uMDzMT@iE5-H4f}EJEbR6?ybye_bN=<=5AjI zd*D~m@7XZjTZkFHo8BKwTdEmKL2?VL#G>>&E+IlqJx=lzagzJjhro;Q5FoU~r0Xv- ze#v%9*UfRvt{m$HmK7iTx{eq$AB@jBLW4lZEhgi{DnywD~>+Q?atWbKSdojTGH zzYo$!eTGm~8-*4pwxe#dB5Vp$_RD*A_9SV{722Wjh-W#;OY*(#uxIj9N{7= z`i_%fu8K_`-Lz$4wq}Fan6~Hnx!jH!J#jW)Fz#wk{D?<`RaM{GxQlY`xEPS@ zdpK2^r2fhF6a7cVOwJIo5mk`DjE+v~k#C32hvg5@I1T*5zY#reR9>)lDc$qtk~#u2 z2GPV7Wg7bT@n$WJ0Y7L2j#g=?4C21tM|Z>2p0xYuZnYfR21vjVvwQ5)@(KIXMB6tb zKzn^JW)O}3!k=#j?ds*Jsonz5WugomMX$IA%B1f)&pRKNmF4^X2zV)xAY`o_tobr^ zdEqT=$Q#Cj#ZFJo&W@--uoN$+e%kFWkL!g)GhfV2u3goXtep(oeq`wD}-IZ`~?mNdj8Kfq&^Ki(ZD>aTJ9#E;C0%WS~C(i68~2NrIspf?u6 z$n6{RI{xglt(|tDG^K)DvZzQsc4lt=!g#t=eEgj8yr%__zv>>boYr^NE4B;JE4#_+ zAhp@7b>`}T>h|acLOwRoDPJ+Z+lL~WHVDX=)XI0_F>3@uvnemvmO@`lXKu_xRMizf+<=$Fi1En-o2%NlCeRs=$5$;-g1`hmTi(}-#fPEJS@ zi6#~`AmeL9W8K>D4cAa6Q?=c%{vDyM3Pv&it2(Nxlq^#U$$LM7E}gS$6e+SvM-11Q z)a>s7S|!h|kN~IgN0P+-*)qwv!Qw46>Ri5a?H)Z!jbtFY!s5f;s+Z+b!z|^MFUSht zLB!Py?8@{dbdHB1XG-;WRLuZN;~!kQMrh}F2Y>bb=@XS~88vsE4*Nfv+-vjoam~6N z0!5OvSc7o4-xW5bYQb84S4A-$jO4K5o0l2W+ifyDdV#4<1mZbD-7M{b%vx@3akg9W zQAs7Ax;gW+n0D^2;}*;$N(~H$iij0-Gv+7PdVFY2m>HpCwG~xZdQg_D;2xpG7DHsbx%?q` ztfHOr>ioTguFIbB?$J+r_Llljz@Qk_2wqk!>km^S2@n)RPHK;IeDVdarl|q3dNdi9 z644)H-hC&p5GUG&lp&*6C-_@D*MTbhfz9krGwYfYe!joXHmp6urCL_S}{@a@;_?d>uZ8?u_^{O7mL6n6O@Sitjr ze(W+5hHNIsK(bSum1Y9YDMXWr(quUO;^Yhwa@@t;FGGlp5;T!odUz#7HZ?w*mphml z51gs1p>jpH96Ig1xz5hm5KfGURAgWc%={7J=PWl6Jn;-zA+RAnL$C)sJ0I)`VNQ+p zHe+;Ote*Fn4flZYYJGi`(%0$r1UGsd4paz>WC`5y#b?2p2s6w#>w6JGKR-DcVEfwL zEe6TCVBKKUaDYpaUqkm+NE8lv8&wF7PyIOip5Sy{RG=8MB|; ze$q-Kxba-)jnOxvU^C8UygFbcewIr8nOte5g#H_2b!v|sq71~R|J{Eg>dnH&7{U`V zLfcriQSC&=XC_*m;WZ zS$G-%6$DZQQiU`TdXjM2t2pDefyDN`cZMo zFmq5Nwcu&*I^l2^;krxOM(O)|McAg6#;e90I=N1MEnYikdn@AFbmLIdcT^oX!7BiA4E%g-c4Rx4V;HY0!X^ zxR#5tJ{Yf_>fQ%_@ggLOPo6`#DaP{<>LM<8}t;D~!yDhpX*Kxu)ff8hu zUYxvSVbc6IQ)_6$;$_>nsfVC7Er>Mal{NiZ9kMDNRZT*yy#ImEsqce9_C1}j__Dkj z9(D$+?Uyu5QeDkc8(O+-nM4J!-Hz&N$LlVVguJTByT4b42UUbZ^vfuTFWUs7m-Y1x zCeQ6gV&A;6edrR&nieQN^3j_lChdF`*>o;ZB7hJ?yMs_@5B{CXL-}RXObKL59Feau zUCu`=bHt1f)j2JE=+sr-N6=af7rzf|KeUEDPQvvz){OS|JEUML>6NUa%+z?1XL)HE z7IEuyJ1t?~ov42oD`-;ouh6H&fj12X9p09!T)w*#A87-145rsKRDEpkq&Gg8AXZ{N z^Vq2uXwF!Pl<7z_%S7$QSB6|VuU0)d$KG_?s=!mty;6d}qI+Q<=ntN_KTq z_$LOl{7^3^y>jxh5Q;raG0Yo8;????1*~-CxO)3=s7(eJaRXU`4bU}Ny;)<8^3n;z&piblk z8_)I=T-uztVX+1@$K`$O%E)?!v7)HH!hg4TD88<9Wv5r)<&6!f6!*2NJRBgrg{w1L5X^FFY3R&r%>znmn3+APbXsuyWP&kxwyGjCJT0O+Z`RP5rutB z?Ai0Fl=M{5-e-RS%>tVTZMN6im>o%z(EzKVn{ex>Hg!mBhB#e(m&ziI)-{+j<79vO zsQkL5=VM*Nqo;G(L6XN0oHD{6;Y@g@$|@%D#S;dL{`Z=+RGf&}U!@n}8PhYFn#Rpf z%bYBJ7~++;S>tJGO~r9fPmT+4C#iRXd57PazPrFKymc13t_=Bk0W3(T`$>;~zQdN> z0IiVWDUxfBKV~U7L-(haxqAT>!$ZX6-#C48dAr>3c7R~F?P8E{)vV}OhV!LujW9Sy#}_avPwi&6M-Z zf$2&l?`i+AWzs+&q@qbrP}*+SClHj2>SIi6P6gJr?bj=Uk>Q zKp&Fg?=MIe@}Kn7wsJ1zVK&)oLFFxKj`U%Ad=`P_q32OD4uh!fut@G^Yj372=F2Y> z0yM8w2&OF%4&Q~$al!Tiw%!4L4%>KhvYhk&kf2FU7jESuei>UWVI%aET0sz1$D-*j za^}rnhrnjIs925o{5tvkmpDRi4&Za#s`G)o%A^;UH;<=PG&SY~rU4H>zQb5X%L_Kx za#8YPP{NS|mntJ)bxb_U{inq2eDU1Q+at)5aU}qjocf22O?v7`HTv_w*v}Lim!%=? zX*=PJ21`{(nq&)lzTh!{zGVl+oO}Tf&Qa1)~R|;Y`)q5&! zTt+%skFI0Q(ru}AyZRM_pBm#o^i0^u_rIH(NK;c);qXDoWAkx@lz#d)rqRDKbR!u) zfD(V2zvBIC9yOi{!c!z4u_)DucA|bMD%oD%C91A6P-Fn8d8q*Ztl1 z4>P|!u$z80igQ2b`){iMA2KZC-Do<-&nQmzN0^c#G*qlQQ=X-v9I}@JSQ9$WY~X8~y0CY?^?3&cAp)PQM-NxZ`#a{rwW%PEOOlz5i)oD;1+D z?(dg-Kj{OX&yf35_n5KMk1i+dm1_G(a_eBi3&^(_j|v5mcLMyW85pDxm?rY>9f;sp zs&!(FsgUEuO+tjt5yW_?S>lCnoG?N+ALC#6RL9@d$ z)O#j*QChaOfsUyt8tj?V7ODj;mSA|LaQo$}$>ZJ`U3Pd%jur4XGd8_ZLajH!dFEK{ ztl5&WZN_IBn z(*_dC#*G4I-0$(BbFNFDbAnwNo?iVhz8t^OEUh64UpX6Ja$4`^JQK+N_9v79hO& zObQcOiKOZ*%LH8b$yFBlZI4I=w-3T@VXMc-3Xz8r4qt?*hR1S$iTQJKypD%8j?`UE zzaKXOY5fH?8F`Di|00@t6pW2)DDOOxxXpO=JoiAV1ko`#7w*RFaZaKT5y|4ZLOnb( z4c@Ez61r3g#V0h`B`&q1sc{A~fuCEl`u+$knhMEQ z9oe9D^zS`bNdL#Ql)g%xqWKmzia685v1IwfhDgd?L}a~=uCK^DnW8Mx{`ji6ad;p* zN(vHa{?VDwGNLWLUvNWXUCnZE=Zai#wk0;ys2za1wOfM&1Ums}$%C`#E2y_e1;J;> z4$U%&;>I5X@h1X?fRnRi9lFH;P!^#A1Ln>Tb&GdvB0b zfNd5@em0AV5hc~dOx%rY-|yzr0jrhP_j;6d0twv6!*?({lUj3%%Mw3!$=Dh#g1x-# z1a7gHd&&de>AC8$!<%L(hvP5n;nyRo9~v?cSYsc)f;>W33$OWbL0I!VVOZO(`|zy7 zWI`rAmw<_h3Mmvf=T_*3ZL!M(|I3p{et`?z&B=0szpFLxkI?paXS0{ zDycYhbfdWRxbNz#vcY6JWUemheT4{S(+kMxx`elwoU z8nG!cx^zSgzP~uKzQ4YW={V}M6a25$o#y2C7toH<#9q!M6Xwj@ynE>zuF)x@@+&&U zn~DRM4^|U(u}0vT=0u&Gw~>+lko6IoeAHPF-6c9w$!0moww`>CZ9twNPZs;{aMbpM zK+spL>ndQ@`uGARG`h6&k$Lr=C(nuqS z>ib^D!VjTpN$5p^A605Z_ja}lk2eNs6y$n4p~qD(M}PW{YOviOH%oS0t$JUcrEMOM zxVi)Tb^3{%`XsWjC}i=4vEdWytCtYQkt0Pc-^$wF$ zd(7l>#A z?N$^xYi%qh;|-2W8-LiDuFOfa8uJl*MC(iAXbtjACS!9QG)+_*(eP*pz{o2eu{A78 zUmw;7|Gn1flkYb{1)SWQC}c%8C zB1Cd9_EoJ*V+?QqH-$nRVNjJ3zNi~*(sa_w5q>|1C3CJ-88IzZu;)S$nsx%^ZKFs# z`S1V~Ui;1zn4*rSzi|F6jl5iM^uTVXO9OvaUNC9mP0MaqNX)Emv{jVg(9S>cj%I6? ziFb#k_?xO< z8`Hd+*D$>@{stDf6zrF@M;uG`GQtM@)q7moC3+Ify9X|`R&ACx^Lb1oBOTM!dHd`u zvd#F$Y9-xV=PUy3cPK5!pp%!NY|>RtcC#EXZI_(1_SX2gB@ZC^&iM0!g6dFb57dGu z)-%D0+#%5W8|f<%_ce5WNx)tq_3X1Z_g=W?DCFXA>&1jl{t**o;QNwY>fSxy48&4Z z%Rkw*7lX<6lLlRu+hMu8*tQa$5xKjUU4NU!m)^}uWQTw?1e&(|a-G*MKBDWIt(0PE z6bJcu+MYL@7`RzUT@T2Grg>!?dK9UyH{Gpwe*13OqyCk3fvj-g!D^4un0(H!e%1%a zts;QyLAGA(A?r8)R6bHRvb?kXb;_I6DtEaP)t!mtYwKtUa+(Hw@z$3qjTuzl$aVN> z4z3`3SIOD#X54r#-B2KnC%Z&38CBL_w#PE_7W|&nlv`?$Fw;kut5B^f&Wdb45zW$y z83x19d;a8(LI@5jOXqA0NJqV(>-vZ<`^<-uF2T$QoN2KVmuP%X<90Yh;)&m533k=Z zTdHcn&c=FPTzdAVge0w9GLpt!mTUaAS@G7N;EzOyBuLz0Gk43F6M;=wF~aFjSF;qw zd&4#sz9K7dVzF0McbKT5#9;54Uva=|fpO6g>Oep*?P7OYF$u?avY4+=)6X8To+`{* z(6AGGV(#d;0@PjvIM@X^%GmokVgx`$NLX4xNJ>CR!c;_5MqENhL?d^OV!QOt(`6qJpm`(sK MHGN2(if#1&0CMmDegFUf diff --git a/proxy/static/assets/toolbar-on.ico b/proxy/static/assets/toolbar-on.ico deleted file mode 100644 index d015872d4bc1275bddc95ee944cf127b9517006c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmcIoXK+y>srl=bqiOXZJfx45JnPb?f09hRTn7vU+5y&D|3P;rd{4be zKppUJH?`wLyOvkPE0ds~nPl;DV%WIV5p|*GkFhD>|OMC`5KWHbrW#l}dxtB^Opj zN&K)aQhYK?YOhzgzt*M!zrjFv;3gYvWAxK#pcJr#`u6mmFNsrqCG@$u68YM4DLZ#f zQWo!)U5`(d%8cvKy-)lfnjnXUtdsDU7R%Wcd#&$nok;X88yF9GwqppMOE{mtuCJ(N zSBercCGg268?;6O9-S;Fr};tmL^(cYyOf>JmEdkOC23ZG ze%nyz6#CC5^h2|=z%y;=1)Kp?a-+`4!4H3xqT?A-oRlp=PfeGM?ML-pUi5jj!Kckg z@oqIjeDD26j*Z$X_2o5En_n)$U1!RvIlIJFTQA8Ag5~&FFS`vq3&w*zCfnk9lP>Q5 zq0htmE>d4I#)3Lem-MYitSPxsXCDzAT=iH=O{u1!W zB&oQ}<(fqGcb}!_#n?POFUZOdbB?=ZZngvN0d(&?^UE7zCFYaW67$h-68q`z68_3k z@ivBuPwNqyYwmBgp>L_9I!8lB$Evc6)ZXpJ*An=|R5>D{-H%k=W1HNNCTw687REDU46&R257A6?L8N zljpwkfpMBAd!Jt*3iyz1)LSd16xn%{_+yJ^hXSA!5I)Wb$S+s zN7Gcl-3Ckl)e@a^YHpzLyid&ulxmFC7E~w?aL-klg&L!bX){)!Hxuv#R2<+8zGoh1 zc*UE;R?EFpKT({RDde9(H6GH+U7%21DhCQL``m~WCtWD$1oCmtEPQDcbvH{;jhA2ot)q#t?g(-MOuM` zJU!d0saWT*reXaQ1_$V=9EiKrtOljnbmRDY1Gsh4*<>#;1q6MFuvMWwN^**bV& zp0T#K_T551$9n+HY~Y=Cd1I{R5A)*ecVSwmF}KL^j8)dX`}%*cIeu~VpVoO`EmK$| zv9@mmSbL{_iaF+P=?9*L6%Xi3RaTK)3s14`P~Ic^2mE0DUxWG5!q@za!r&MTv<7a{ z-DJT{HZUGM0Oo~tc<5W=tf~`7VTJm_8f@Mv#^WZSH(>42)OV}aKF{4}ZZcQMQ9Qrf z33-L}B=rsiSli!`Zo579Z`^(6%liQDi(r8FCeOzO6acKFJOSF;2Vh-o>1F>`;xLSE kWroo<%P`u+;7 - - - toolbar_icon_purple - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/proxy/static/assets/toolbar-running-48.png b/proxy/static/assets/toolbar-running-48.png deleted file mode 100644 index 9df54760d59bebaacc58e5d5aa1694126bc3ed5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3660 zcmZ`+cQjmIv>u}bi63IZD2Xm3g3((ry1@(vK?o)cW}=tqoe0rug6JiQ5+pi_7NWP& zYe>`(C3?xbS?`bc*Sl-o`>lJ<{?6HFop0}bPLv*6gNB-g8UzB-AT?Dn0K)&hloY_% z7dvUu4tNI%Di!K}fat#u*t2nV}nvaGQaAQ^43ZcW4#Bl&|WP)DiuBU@=NEso3KVs8f$ zcXE!=A&Z#y$QWO~V2{?O?YewD#KHoL}HjK3|fyS47K%2<4OgrAhQdlcB#^hS7j zuYnsoQ#4>o3N?PxUJpKjEnRDM<>34@bdewCF8|>sQ!1iv`{wP5>pD$kY;UEN_ut>= zxyfl5$a^oCIX!jKEN69j_*zQw?_B5ivDzx^)!e_aZVXc4=tR;FX7KXCsPb3qjqpAlW4WUEgZ5`ayboXuYC`|kdO zCv8`ABYvYYu&W_^5gUi3>ITiUzZtFw;x00`EebW{LKt2Pu}*9~Kd0tQ7}X9O<@?WQ zJh84ZWg_wKcbzQ*9hq*rnZ=l=q5lS49u7)&w9r7|1W!w)zVG?yg@@|;nuh4Gq=j)p z!MS@9qip z%2qVLV#;vxN7VYMXq%nAy zW>(5Xy7;Yfd)^&L(Jm7~vzpQ6r4uMWv&nYn525g9^K50(TE70OPgvY0fB5o9Vb<0LREwh>6b*Y&q&082EUQ8&Q!5y?MUs>s#{3B)KwM+@PWs0p^5k zo7tz!*mFlD#fdh3%ls^JoIB??S+lpBRKW=cJ2~Im1^H$BIl# zGTzT*ii&Km7vwc4tVayI7&0i2-p#a2gLV)ffaB>>3pgL!ei|6c3-+7iuZe?5HmWy)03#TeK(^*pfoOfVBF2oecw#{3i%0Uq__meK@7WWo!c(umr1zZa-{)p`FFxdF zY;lUl+-ooP8+i_W#uSt@P0q@FWC6(|6ptxr7dIr#e_gygLl-t6)9g@&u=3K~dwA~d zVWN1qBc+@4YmkO{is^6XXf(A^fhuRwpZo~OokcF^T6vYebsxy%Nb;8pyLk1+dhuNkkRkYCtx#2y})S%(YS{q~@OiYE!lyeqLzb!8XnJ5;&Lf&+8 zX9lX`XXdfS7@kModXLpt^r*s4g} z_#i6N`ql^CxJizCDv>zqb%mjje zU#GA%D*Y;{?JHCi|L3F`j_p2tp<{#BVJdMd0s=C~K8UeS3ifvwjG`u_PBD6G&{I!S zZJjus($`i@2PCJ`#yLLRuOv*-!=f5*z5YhrU$2hwixTO2oPhB3jz9rl0gjD7Kqu4y|_+K>{b$BcKTvN|r#k~K)Y{nrGP`137)R?G-t z5)9R`S`ikZ^ve3A8lPNPkXM@n4uez3GLg5v2lTtQxd{PRmwFU~hs|1(s>wT~7q(-H zQI-;bx<6x}Ejl+-Yc7tKv1i3I>zPhtYkKk*qmRxDhJ^%<)Y(t2&F4wTduhF;NMA z1YxO+7A9F!(TJV$+sGlM>KdJKL6o1!#nzE1t-)wN7pj14Kpe&Sy=Bh(lbJzUWg>CV zj(bN=S5)INP0Y2&yEy}%=9vzrN!dKGYE+b2$7;-*JpHwp=Nn^mWo=4ct+8W>+jLo} zWP%w)q9z_Bae>}qjQa7-{Hoy_tiTqh_CcX z4{>BI;TWxck)B_|axp1MFICug(cK3QLjiQ>?L0onqQurA`z1?`_B(D<@}jX(YL2-y z(#UVwZGd?Bk)?cH(C$|&viM^2qm&0-n?}ryzTXm<J#eztZa4ryqHqxzLAbP_h>W3#h^#nVR$M|5E-4F#W1JZN z-O~P#;OK0FxApn|3EX0(>HvZH-wX!MwjN$sHyp^z%S#CF^wixN>w*(+(EkC-M8c5( diff --git a/proxy/static/assets/toolbar-running-96.png b/proxy/static/assets/toolbar-running-96.png deleted file mode 100644 index 956c7d17a34532548bcfe0e6e45fa601981f11ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7385 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 diff --git a/proxy/static/assets/toolbar-running.ico b/proxy/static/assets/toolbar-running.ico deleted file mode 100644 index c4145206f428c1e979ceefef2a553477a926dc97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmcIoX>3+i5Pd)b@s}nh{L(=Dr4UhYk06Mkf=k>06*VA)O`#%-Y_e|xRZ=NC5&{YW zcBN2Sy3sGI^zU!+ zw{_$p%j#=c*5d#kSt9^GSHaxBV|~85Eu&d1pd(u=NgQM0}((gpwiafCJn83%}B4zmt*;7?PskX^X}J5aa|5P z4BXQO$6POrCj-TRiVHdu%5x>4@SH42*e$s=*Y)|1>|lBA)Iuq1s21uz_~#i}k$gb1 zstTpDvDW>zUfS^6ALtKsvBB|-ewqeU0k%-z2NA1ebNVrP@5~DMJmv?vT3akTb55xK z8};R?`<0MyW#Oeg@^REU*_(UHe%~<&#n`rie89aE<8fcf_1w>$mIe+bv#LNwhc1$k z>lY>IcAmU@X1Pp`-6Hehi&sz17w`1rLfvaqepS21ge{S+nST0ib6cypT02pee&`en z+|!09fOJ45zAQ`TT<{gfIJ>$?UWfkRq6<2Au`EYz7!o*31_#WP=YzhM)t3)R9qhf` zSR-$qULro30n*ggEWX)6vM%*E*kJR9u^=Cuoa}@9z4~$YuiWbEG9hZM`toRgjJ$Dr zu>=*x+e>KQTVczfe}=5Od{And>vfJ?p#F_%M^yK)khwyhw7?GzAt#v^#`nzC3eLB+ zw@YJdvm&89N5)02QQbpNeA7uk*Z!2@CnMnhqB~5r4x#?BVM{e$BTp@mZCQbKewa1x zD|3_ep*Ns=XUzX7h?Th)cZk=8?YQogPop=;QvuUuaNta#ADprsz9s)lTuK?8Dt1+1 z|BGdy-&7eD`kkyvIi$SZl=i#q&IysKraR^nG@AP!2o&;RQ(KGd0;j2;JZ7%VOW36# z@5dt^X_eQwRE4qS9*ut12A@oSc^SUq`i~-4E9gUxr^I?o4&v$%)=4iw$&pucQ$C4a zuQi3X5GEEW6<1|^vCH0P8?C_I$JcwsMr5He7+; zd|*GIQrT3eoM$dE*F1%KjSZQoF{b|H*Oh3Tshc)9Yb3wHJ@7fdOPiaqFNefB#%lNM zcwb!`?BKgu!+e~8`Z5u`m>s`OhM$}#%Te=2;7YDhms9Fn8nqTsw{KQZmwBu$G3&XO z{7uC^$vf`HA2pZ0EOm8&^Wis>WDGb?PBQ`85u?_}lbhz7ywWc6>HUb6l30GlImNNJ zd5~9gLu==v#C^^j)oG`0+Q4(o_<@|GPfc!iX9K_e#o0~rCC*Jp{v9?4Nr`YeY)_)eWQKrWIsJ#Pu>5*?- z%bH{MZhC-8c-GNGU=WFK(s#6h=R121qn~{adm8pXtP#w=bEO%0OO#s^p?^c_5z#XX zeBfEm+Uk%9j2U;Mn^*P%djOSNIMZnZ{lGkAPsg5$b&9@F1dI_LuDK?@PDS9_KsmG4o|C!1KZ%;JL~8v;w67 z`>6c@ZDmi}8?g0q+?_O9R=;Y?>Qi7@y`u52f|dMt!T%umt_AdnhMofGgU$zAq5ChV CH?^Pu diff --git a/proxy/static/assets/toolbar-running.svg b/proxy/static/assets/toolbar-running.svg deleted file mode 100644 index 5599c87..0000000 --- a/proxy/static/assets/toolbar-running.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - toolbar_icon_grey - Created with Sketch. - - - - - - - - - diff --git a/proxy/static/bootstrap.css b/proxy/static/bootstrap.css deleted file mode 100644 index fa704df..0000000 --- a/proxy/static/bootstrap.css +++ /dev/null @@ -1,334 +0,0 @@ -/* This is a subset of bootstrap.css */ -.navbar-brand img { - max-width: 4em; } - -.navbar { -display: none; } - -.navbar { - position: relative; - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - padding: 0.5rem 1rem; } - .navbar > .container, - .navbar > .container-fluid { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; } - -.navbar-brand { - display: inline-block; - padding-top: 0.3125rem; - padding-bottom: 0.3125rem; - margin-right: 1rem; - font-size: 1.25rem; - line-height: inherit; - white-space: nowrap; } - .navbar-brand:focus, .navbar-brand:hover { - text-decoration: none; } - -.navbar-dark .navbar-brand { - color: #FFFFFF; } - .navbar-dark .navbar-brand:focus, .navbar-dark .navbar-brand:hover { - color: #FFFFFF; } -.navbar-dark .navbar-nav .nav-link { - color: #FFFFFF; } - .navbar-dark .navbar-nav .nav-link:focus, .navbar-dark .navbar-nav .nav-link:hover { - color: rgba(255, 255, 255, 0.75); } - .navbar-dark .navbar-nav .nav-link.disabled { - color: rgba(255, 255, 255, 0.25); } -.navbar-dark .navbar-nav .show > .nav-link, -.navbar-dark .navbar-nav .active > .nav-link, -.navbar-dark .navbar-nav .nav-link.show, -.navbar-dark .navbar-nav .nav-link.active { - color: #FFFFFF; } -.navbar-dark .navbar-toggler { - color: #FFFFFF; - border-color: rgba(255, 255, 255, 0.1); } -.navbar-dark .navbar-toggler-icon { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='%23FFFFFF' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); } -.navbar-dark .navbar-text { - color: #FFFFFF; } - .navbar-dark .navbar-text a { - color: #FFFFFF; } - .navbar-dark .navbar-text a:focus, .navbar-dark .navbar-text a:hover { - color: #FFFFFF; } - -.navbar { - background-image: url("./images/onion-bg.svg"); - background-repeat: no-repeat; - background-position: 10px 12px; } - -.navbar-brand span { - font-size: 0.6em; - display: flex; } - -.no-gutters { - margin-right: 0; - margin-left: 0; } - .no-gutters > .col, - .no-gutters > [class*="col-"] { - padding-right: 0; - padding-left: 0; } - -.no-gutters { - margin-bottom: 0 !important; } - -.no-background { - background-image: none !important; } - -.bg-dark { - background-color: #59316B !important; } - -a.bg-dark:focus, a.bg-dark:hover { - background-color: #3c2148 !important; } - -.p-4 { - padding: 1.5rem !important; } - -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-flex; - vertical-align: middle; } - .btn-group > .btn, - .btn-group-vertical > .btn { - position: relative; - flex: 0 1 auto; } - .btn-group > .btn:hover, - .btn-group-vertical > .btn:hover { - z-index: 2; } - .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, - .btn-group-vertical > .btn:focus, - .btn-group-vertical > .btn:active, - .btn-group-vertical > .btn.active { - z-index: 2; } - .btn-group .btn + .btn, - .btn-group .btn + .btn-group, - .btn-group .btn-group + .btn, - .btn-group .btn-group + .btn-group, - .btn-group-vertical .btn + .btn, - .btn-group-vertical .btn + .btn-group, - .btn-group-vertical .btn-group + .btn, - .btn-group-vertical .btn-group + .btn-group { - margin-left: -1px; } - -.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { - border-radius: 0; } - -.btn-group > .btn:first-child { - margin-left: 0; } - .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; } - -.btn-group > .btn:last-child:not(:first-child), -.btn-group > .dropdown-toggle:not(:first-child) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; } - -.btn-group > .btn-group { - float: left; } - -.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; } - -.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, -.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { - border-top-right-radius: 0; - border-bottom-right-radius: 0; } - -.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { - border-top-left-radius: 0; - border-bottom-left-radius: 0; } - -.dropup, -.dropdown { - position: relative; } - -.dropdown-toggle::after { - display: inline-block; - width: 0; - height: 0; - margin-left: 0.255em; - vertical-align: 0.255em; - content: ""; - border-top: 0.3em solid; - border-right: 0.3em solid transparent; - border-bottom: 0; - border-left: 0.3em solid transparent; } -.dropdown-toggle:empty::after { - margin-left: 0; } - -.dropup .dropdown-toggle::after { - display: inline-block; - width: 0; - height: 0; - margin-left: 0.255em; - vertical-align: 0.255em; - content: ""; - border-top: 0; - border-right: 0.3em solid transparent; - border-bottom: 0.3em solid; - border-left: 0.3em solid transparent; } -.dropup .dropdown-toggle:empty::after { - margin-left: 0; } - -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 10rem; - padding: 0.5rem 0; - margin: 0.125rem 0 0; - font-size: 1rem; - color: #212529; - text-align: left; - list-style: none; - background-color: #FFFFFF; - background-clip: padding-box; - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 0.25rem; } - -.dropup .dropdown-menu { - margin-top: 0; - margin-bottom: 0.125rem; } -.dropup .dropdown-toggle::after { - display: inline-block; - width: 0; - height: 0; - margin-left: 0.255em; - vertical-align: 0.255em; - content: ""; - border-top: 0; - border-right: 0.3em solid transparent; - border-bottom: 0.3em solid; - border-left: 0.3em solid transparent; } -.dropup .dropdown-toggle:empty::after { - margin-left: 0; } - -.dropdown-menu.show { - display: block; } - -.dropdown-item { - display: block; - width: 100%; - padding: 0.25rem 1.5rem; - clear: both; - font-weight: 400; - color: #212529; - text-align: inherit; - white-space: nowrap; - background: none; - border: 0; } - .dropdown-item:focus, .dropdown-item:hover { - color: #16181b; - text-decoration: none; - background-color: #F8F9FA; } - .dropdown-item.active, .dropdown-item:active { - color: #FFFFFF; - text-decoration: none; - background-color: #7D4698; } - .dropdown-item.disabled, .dropdown-item:disabled { - color: #848E97; - background-color: transparent; } - -.btn { - display: inline-block; - font-weight: 400; - text-align: center; - white-space: nowrap; - vertical-align: middle; - user-select: none; - border: 1px solid transparent; - padding: 0.375rem 0.75rem; - font-size: 1rem; - line-height: 1.5; - border-radius: 0.25rem; - transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } - .btn:focus, .btn:hover { - text-decoration: none; } - .btn:focus, .btn.focus { - outline: 0; - box-shadow: 0 0 0 0.2rem rgba(125, 70, 152, 0.25); } - .btn.disabled, .btn:disabled { - opacity: .65; } - .btn:not([disabled]):not(.disabled):active, .btn:not([disabled]):not(.disabled).active { - background-image: none; } - -a.btn.disabled, -fieldset[disabled] a.btn { - pointer-events: none; } - -.btn-dark { - color: #fff; - background-color: #59316B; - border-color: #59316B; } - .btn-dark:hover { - color: #fff; - background-color: #432551; - border-color: #3c2148; } - .btn-dark:focus, .btn-dark.focus { - box-shadow: 0 0 0 0.2rem rgba(89, 49, 107, 0.5); } - .btn-dark.disabled, .btn-dark:disabled { - background-color: #59316B; - border-color: #59316B; } - .btn-dark:not([disabled]):not(.disabled):active, .btn-dark:not([disabled]):not(.disabled).active, .show > .btn-dark.dropdown-toggle { - color: #fff; - background-color: #3c2148; - border-color: #351d3f; - box-shadow: 0 0 0 0.2rem rgba(89, 49, 107, 0.5); } - - -.btn-block { - display: block; - width: 100%; } - -.btn-block + .btn-block { - margin-top: 0.5rem; } - -input[type="submit"].btn-block, -input[type="reset"].btn-block, -input[type="button"].btn-block { - width: 100%; } - -*, -*::before, -*::after { - box-sizing: border-box; } - -a, -area, -button, -[role="button"], -input:not([type="range"]), -label, -select, -summary, -textarea { - touch-action: manipulation; } - -a { - color: #7D4698; - text-decoration: none; - background-color: transparent; - -webkit-text-decoration-skip: objects; } - a:hover { - color: #522e64; - text-decoration: underline; } - -a:not([href]):not([tabindex]) { - color: inherit; - text-decoration: none; } - a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover { - color: inherit; - text-decoration: none; } - a:not([href]):not([tabindex]):focus { - outline: 0; } - diff --git a/proxy/static/chrome150.jpg b/proxy/static/chrome150.jpg deleted file mode 100644 index fc8a83f07bb8fc31ccd083b08d38f6f984c31be2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5321 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 diff --git a/proxy/static/embed.css b/proxy/static/embed.css deleted file mode 100644 index 162521a..0000000 --- a/proxy/static/embed.css +++ /dev/null @@ -1,150 +0,0 @@ -body { - color: black; - margin: 10px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; - width: 300px; - font-size: 12px; -} - -#active { - margin: 20px 0; - text-align: center; -} - -#statusimg { - background-image: url("assets/status-off.svg"); - background-repeat: no-repeat; - background-position: center center; - min-height: 60px; -} -#statusimg.on { - background-image: url("assets/status-on.svg"); -} -#statusimg.on.running { - background-image: url("assets/status-running.svg"); -} - -.b { - border-top: 1px solid gainsboro; - padding: 10px; - position: relative; -} - -.b a { - color: inherit; - display: inline-block; - text-decoration: none; -} - -.error { - color: firebrick; -} - -.learn:before { - content : " "; - display: block; - position: absolute; - top: 12px; - background-image: url('assets/arrowhead-right-12.svg'); - width: 12px; - height: 12px; - opacity : 0.6; - z-index: 9999; - right: 0px; - margin-right: 10px; -} - -/* Snowflake Status */ - -.transfering { - -webkit-animation:spin 8s linear infinite; - -moz-animation:spin 8s linear infinite; - animation:spin 8s linear infinite; - fill: BlueViolet; -} -@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } -@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } -@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } - -/* Toggle */ - -.switch { - position: relative; - display: inline-block; - width: 30px; - height: 17px; - float: right; -} - -.switch input { - opacity: 0; - width: 0; - height: 0; -} - -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - -webkit-transition: .4s; - transition: .4s; - border-radius: 17px; -} - -.slider:before { - position: absolute; - content: ""; - height: 13px; - width: 13px; - left: 2px; - bottom: 2px; - background-color: white; - -webkit-transition: .4s; - transition: .4s; - border-radius: 50%; -} - -input:checked + .slider { - background-color: BlueViolet; -} - -input:focus + .slider { - box-shadow: 0 0 1px BlueViolet; -} - -input:checked + .slider:before { - -webkit-transform: translateX(13px); - -ms-transform: translateX(13px); - transform: translateX(13px); -} - -/* Dark Mode */ -@media (prefers-color-scheme: dark) { - body { - /* https://design.firefox.com/photon/visuals/color.html#dark-theme */ - color: white; - background-color: #38383d; - } - #statusimg { - background-image: url("assets/status-off-dark.svg"); - } - #statusimg.on { - background-image: url("assets/status-on-dark.svg"); - } - #statusimg.on.running { - background-image: url("assets/status-running.svg"); - } - input:checked + .slider { - background-color: #cc80ff; - } - input:focus + .slider { - box-shadow: 0 0 1px #cc80ff; - } - .learn:before { - background-image: url('assets/arrowhead-right-dark-12.svg'); - } -} diff --git a/proxy/static/embed.html b/proxy/static/embed.html deleted file mode 100644 index b3ca800..0000000 --- a/proxy/static/embed.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - Snowflake - - - - - - -
-
-

__MSG_popupStatusOff__

-

-
-
- - -
- - - diff --git a/proxy/static/firefox150.jpg b/proxy/static/firefox150.jpg deleted file mode 100644 index 1eda5439abae4996027b568273fc819738411f7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44930 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

gcjQOuLY*pyEq)S_0a?3F2 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 ?W$2a8i-+iI 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 CgNE(^ diff --git a/proxy/static/index.css b/proxy/static/index.css deleted file mode 100644 index b23e1ad..0000000 --- a/proxy/static/index.css +++ /dev/null @@ -1,94 +0,0 @@ -@font-face { - font-family: Source Sans Pro; - src: url("SourceSansPro-Regular.ttf"); -} - -body { - margin: 0; - font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - font-size: 1.3rem; - font-weight: 400; - line-height: 1.5; - color: #212529; -} - -header { - margin: 0; - background-color: #59316B; - padding: 0 5.2rem; -} - -#content { - max-width: 90rem; - margin: 0 auto 2.6rem auto; - padding: 2.6rem 5.2rem; - background-color: #FFFFFF; -} - -@media only screen and (max-width: 600px) { - #content { - padding: 2.6rem 1.3rem; - } -} - -section { - margin: 1.3rem 0; -} - -h1 { - margin: 0; - font-size: 2.6rem; - color: #7D4698; - text-align: center; -} - -h2 { - margin: 0; - font-size: 2rem; - color: #7D4698; -} - -.sidebyside { - display: flex; - flex-flow: row wrap; - align-items: flex-start; -} - -.sidebyside section { - flex: 1 1 15rem; - padding: 0 1.3rem; -} - -.addon { - margin-top: 2.6rem 0; - text-align: center; -} - -.addon a { - display: inline-block; - padding: 0 1.3rem; -} - -.diagram, .screenshot { - padding: 2.6rem 5.2rem; - text-align: center; -} - -.diagram img, .screenshot img { - max-width: 100%; -} - -textarea { - max-width: 100%; - width: 600px; -} - -.dropdown:hover .dropdown-menu { - display: block; - height: 350px; - overflow: auto; -} - -.pull-right { - float: right !important; -} diff --git a/proxy/static/index.html b/proxy/static/index.html deleted file mode 100644 index 32cdcde..0000000 --- a/proxy/static/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Snowflake - - - - - - -
-
-

SNOWFLAKE

- -

Diagram

- -

Snowflake is a system to defeat internet censorship. People who are - censored can use Snowflake to access the internet. Their connection goes - through Snowflake proxies, which are run by volunteers. For more detailed - information about how Snowflake works see our - documentation wiki.

- -
- -
-

Browser

- -

If your internet access is censored, you should download - Tor Browser.

- -

Tor Browser screenshot

-
- -
-

Extension

- -

If your internet access is not censored, you should - consider installing the Snowflake extension to help users in censored - networks. There is no need to worry about which websites people are - accessing through your proxy. Their visible browsing IP address will - match their Tor exit node, not yours.

- -

- - Install in Firefox
- Install in Firefox -
- - Install in Chrome
- Install in Chrome -
-

-
- -
- -
-

Reporting Bugs

- -

If you encounter problems with Snowflake as a client or a proxy, - please consider filing a bug. To do so, you will have to,

- -
    -
  1. - Either create an - account or log in - using the shared cypherpunks account with password writecode.
  2. -
  3. - File a ticket - using our bug tracker.
  4. -
- -

- Please try to be as descriptive as possible with your ticket and if - possible include log messages that will help us reproduce the bug. - Consider adding keywords snowflake-webextension or snowflake-client - to let us know how which part of the Snowflake system is experiencing - problems. -

-
- -
-

Embed

- -

It is now possible to embed the Snowflake badge on any website:

- - - -

Which looks like this:

- - -
- -
-
- - - diff --git a/proxy/static/index.js b/proxy/static/index.js deleted file mode 100644 index 80a3aeb..0000000 --- a/proxy/static/index.js +++ /dev/null @@ -1,83 +0,0 @@ -/* global availableLangs */ - -class Messages { - constructor(json) { - this.json = json; - } - getMessage(m, ...rest) { - if (Object.prototype.hasOwnProperty.call(this.json, m)) { - let message = this.json[m].message; - return message.replace(/\$(\d+)/g, (...args) => { - return rest[Number(args[1]) - 1]; - }); - } - } -} - - -var defaultLang = "en_US"; - -var getLang = function() { - let lang = navigator.language || defaultLang; - lang = lang.replace(/-/g, '_'); - - //prioritize override language - var url_string = window.location.href; //window.location.href - var url = new URL(url_string); - var override_lang = url.searchParams.get("lang"); - if (override_lang != null) { - lang = override_lang; - } - - if (Object.prototype.hasOwnProperty.call(availableLangs, lang)) { - return lang; - } - lang = lang.split('_')[0]; - if (Object.prototype.hasOwnProperty.call(availableLangs, lang)) { - return lang; - } - return defaultLang; -} - -var fill = function(n, func) { - switch(n.nodeType) { - case 1: // Node.ELEMENT_NODE - { - const m = /^__MSG_([^_]*)__$/.exec(n.dataset.msgid); - if (m) { - var val = func(m[1]); - if (val != undefined) { - n.innerHTML = val - } - } - n.childNodes.forEach(c => fill(c, func)); - break; - } - } -} - - -fetch(`./_locales/${getLang()}/messages.json`) -.then((res) => { - if (!res.ok) { return; } - return res.json(); -}) -.then((json) => { - var language = document.getElementById('language-switcher'); - var lang = `${getLang()}` - language.innerText = availableLangs[lang].name + ' (' + lang + ')'; - var messages = new Messages(json); - fill(document.body, (m) => { - return messages.getMessage(m); - }); -}); - -// Populate language switcher list -for (var lang in availableLangs) { - var languageList = document.getElementById('supported-languages'); - var link = document.createElement('a'); - link.setAttribute('href', '?lang='+lang); - link.setAttribute('class', "dropdown-item"); - link.innerText = availableLangs[lang].name + ' (' + lang + ')'; - languageList.lastChild.after(link); -} diff --git a/proxy/static/popup.js b/proxy/static/popup.js deleted file mode 100644 index 80cbcc6..0000000 --- a/proxy/static/popup.js +++ /dev/null @@ -1,50 +0,0 @@ -/* exported Popup */ - -// Add or remove a class from elem.classList, depending on cond. -function setClass(elem, className, cond) { - if (cond) { - elem.classList.add(className); - } else { - elem.classList.remove(className); - } -} - -class Popup { - constructor() { - this.div = document.getElementById('active'); - this.statustext = document.getElementById('statustext'); - this.statusdesc = document.getElementById('statusdesc'); - this.img = document.getElementById('statusimg'); - } - setEnabled(enabled) { - setClass(this.img, 'on', enabled); - } - setActive(active) { - setClass(this.img, 'running', active); - } - setStatusText(txt) { - this.statustext.innerText = txt; - } - setStatusDesc(desc, error) { - this.statusdesc.innerText = desc; - setClass(this.statusdesc, 'error', error); - } - hideButton() { - document.querySelector('.button').style.display = 'none'; - } - setChecked(checked) { - document.getElementById('enabled').checked = checked; - } - static fill(n, func) { - switch(n.nodeType) { - case 3: { // Node.TEXT_NODE - const m = /^__MSG_([^_]*)__$/.exec(n.nodeValue); - if (m) { n.nodeValue = func(m[1]); } - break; - } - case 1: // Node.ELEMENT_NODE - n.childNodes.forEach(c => Popup.fill(c, func)); - break; - } - } -} diff --git a/proxy/static/screenshot.png b/proxy/static/screenshot.png deleted file mode 100644 index 58c0540baf852821333af73e140f3dd41ce56f49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 377507 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

\n"))
+			if err != nil {
+				return total, err
+			}
+		}
+
+		n := bytesPerChunk - enc.chunkCounter
+		if n > len(p) {
+			n = len(p)
+		}
+		nn, err := enc.w.Write(p[:n])
+		if err != nil {
+			return total, err
+		}
+		total += nn
+		p = p[n:]
+
+		enc.chunkCounter += n
+		if enc.chunkCounter >= bytesPerChunk {
+			enc.chunkCounter = 0
+			enc.elementCounter += 1
+			nn, err = enc.w.Write([]byte("\n"))
+			if err != nil {
+				return total, err
+			}
+			total += nn
+		}
+
+		if enc.elementCounter >= chunksPerElement {
+			enc.elementCounter = 0
+			nn, err = enc.w.Write([]byte("
\n")) + if err != nil { + return total, err + } + total += nn + } + } + return total, nil +} + +func (enc *elementEncoder) Close() error { + var err error + if !(enc.elementCounter == 0 && enc.chunkCounter == 0) { + if enc.chunkCounter == 0 { + _, err = enc.w.Write([]byte("\n")) + } else { + _, err = enc.w.Write([]byte("\n\n")) + } + } + return err +} diff --git a/common/amp/armor_test.go b/common/amp/armor_test.go new file mode 100644 index 0000000..594ae65 --- /dev/null +++ b/common/amp/armor_test.go @@ -0,0 +1,227 @@ +package amp + +import ( + "crypto/rand" + "io" + "io/ioutil" + "strings" + "testing" +) + +func armorDecodeToString(src string) (string, error) { + dec, err := NewArmorDecoder(strings.NewReader(src)) + if err != nil { + return "", err + } + p, err := ioutil.ReadAll(dec) + return string(p), err +} + +func TestArmorDecoder(t *testing.T) { + for _, test := range []struct { + input string + expectedOutput string + expectedErr bool + }{ + {` +
+0
+
+`, + "", + false, + }, + {` +
+0aGVsbG8gd29ybGQK
+
+`, + "hello world\n", + false, + }, + // bad version indicator + {` +
+1aGVsbG8gd29ybGQK
+
+`, + "", + true, + }, + // text outside
 elements
+		{`
+0aGVsbG8gd29ybGQK
+blah blah blah
+
+0aGVsbG8gd29ybGQK
+
+0aGVsbG8gd29ybGQK +blah blah blah +`, + "hello world\n", + false, + }, + {` +
+0QUJDREV
+GR0hJSkt
+MTU5PUFF
+SU1RVVld
+
+junk +
+YWVowMTI
+zNDU2Nzg
+5Cg
+=
+
+
+=
+
+`, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n", + false, + }, + // no
 elements, hence no version indicator
+		{`
+aGVsbG8gd29ybGQK
+blah blah blah
+aGVsbG8gd29ybGQK
+aGVsbG8gd29ybGQK
+blah blah blah
+`,
+			"",
+			true,
+		},
+		// empty 
 elements, hence no version indicator
+		{`
+aGVsbG8gd29ybGQK
+blah blah blah
+
   
+aGVsbG8gd29ybGQK +aGVsbG8gd29ybGQK

+blah blah blah
+`,
+			"",
+			true,
+		},
+		// other elements inside 
+		{
+			"blah 
0aGVsb

G8gd29

ybGQK
", + "hello world\n", + false, + }, + // HTML comment + { + "blah ", + "", + true, + }, + // all kinds of ASCII whitespace + { + "blah
\x200\x09aG\x0aV\x0csb\x0dG8\x20gd29ybGQK
", + "hello world\n", + false, + }, + + // bad padding + {` +
+0QUJDREV
+GR0hJSkt
+MTU5PUFF
+SU1RVVld
+
+junk +
+YWVowMTI
+zNDU2Nzg
+5Cg
+=
+
+`, + "", + true, + }, + /* + // per-chunk base64 + // test disabled because Go stdlib handles this incorrectly: + // https://github.com/golang/go/issues/31626 + { + "
QQ==
Qg==
", + "", + true, + }, + */ + // missing
+ { + "blah
0aGVsbG8gd29ybGQK",
+			"",
+			true,
+		},
+		// nested 
+		{
+			"blah 
0aGVsb
G8gd29
ybGQK
", + "", + true, + }, + } { + output, err := armorDecodeToString(test.input) + if test.expectedErr && err == nil { + t.Errorf("%+q → (%+q, %v), expected error", test.input, output, err) + continue + } + if !test.expectedErr && err != nil { + t.Errorf("%+q → (%+q, %v), expected no error", test.input, output, err) + continue + } + if !test.expectedErr && output != test.expectedOutput { + t.Errorf("%+q → (%+q, %v), expected (%+q, %v)", + test.input, output, err, test.expectedOutput, nil) + continue + } + } +} + +func armorRoundTrip(s string) (string, error) { + var encoded strings.Builder + enc, err := NewArmorEncoder(&encoded) + if err != nil { + return "", err + } + _, err = io.Copy(enc, strings.NewReader(s)) + if err != nil { + return "", err + } + err = enc.Close() + if err != nil { + return "", err + } + return armorDecodeToString(encoded.String()) +} + +func TestArmorRoundTrip(t *testing.T) { + lengths := make([]int, 0) + // Test short strings and lengths around elementSizeLimit thresholds. + for i := 0; i < bytesPerChunk*2; i++ { + lengths = append(lengths, i) + } + for i := -10; i < +10; i++ { + lengths = append(lengths, elementSizeLimit+i) + lengths = append(lengths, 2*elementSizeLimit+i) + } + for _, n := range lengths { + buf := make([]byte, n) + rand.Read(buf) + input := string(buf) + output, err := armorRoundTrip(input) + if err != nil { + t.Errorf("length %d → error %v", n, err) + continue + } + if output != input { + t.Errorf("length %d → %+q", n, output) + continue + } + } +} diff --git a/common/amp/cache.go b/common/amp/cache.go new file mode 100644 index 0000000..102993f --- /dev/null +++ b/common/amp/cache.go @@ -0,0 +1,178 @@ +package amp + +import ( + "crypto/sha256" + "encoding/base32" + "fmt" + "net" + "net/url" + "path" + "strings" + + "golang.org/x/net/idna" +) + +// domainPrefixBasic does the basic domain prefix conversion. Does not do any +// IDNA mapping, such as https://www.unicode.org/reports/tr46/. +// +// https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/#basic-algorithm +func domainPrefixBasic(domain string) (string, error) { + // 1. Punycode Decode the publisher domain. + prefix, err := idna.ToUnicode(domain) + if err != nil { + return "", err + } + + // 2. Replace any "-" (hyphen) character in the output of step 1 with + // "--" (two hyphens). + prefix = strings.Replace(prefix, "-", "--", -1) + + // 3. Replace any "." (dot) character in the output of step 2 with "-" + // (hyphen). + prefix = strings.Replace(prefix, ".", "-", -1) + + // 4. If the output of step 3 has a "-" (hyphen) at both positions 3 and + // 4, then to the output of step 3, add a prefix of "0-" and add a + // suffix of "-0". + if len(prefix) >= 4 && prefix[2] == '-' && prefix[3] == '-' { + prefix = "0-" + prefix + "-0" + } + + // 5. Punycode Encode the output of step 3. + return idna.ToASCII(prefix) +} + +// Lower-case base32 without padding. +var fallbackBase32Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding) + +// domainPrefixFallback does the fallback domain prefix conversion. The returned +// base32 domain uses lower-case letters. +// +// https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/#fallback-algorithm +func domainPrefixFallback(domain string) string { + // The algorithm specification does not say what, exactly, we are to + // take the SHA-256 of. domain is notionally an abstract Unicode + // string, not a byte sequence. While + // https://github.com/ampproject/amp-toolbox/blob/84cb3057e5f6c54d64369ddd285db1cb36237ee8/packages/cache-url/lib/AmpCurlUrlGenerator.js#L62 + // says "Take the SHA256 of the punycode view of the domain," in reality + // it hashes the UTF-8 encoding of the domain, without Punycode: + // https://github.com/ampproject/amp-toolbox/blob/84cb3057e5f6c54d64369ddd285db1cb36237ee8/packages/cache-url/lib/AmpCurlUrlGenerator.js#L141 + // https://github.com/ampproject/amp-toolbox/blob/84cb3057e5f6c54d64369ddd285db1cb36237ee8/packages/cache-url/lib/browser/Sha256.js#L24 + // We do the same here, hashing the raw bytes of domain, presumed to be + // UTF-8. + + // 1. Hash the publisher's domain using SHA256. + h := sha256.Sum256([]byte(domain)) + + // 2. Base32 Escape the output of step 1. + // 3. Remove the last 4 characters from the output of step 2, which are + // always "=" (equals) characters. + return fallbackBase32Encoding.EncodeToString(h[:]) +} + +// domainPrefix computes the domain prefix of an AMP cache URL. +// +// https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/#domain-name-prefix +func domainPrefix(domain string) string { + // https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/#combined-algorithm + // 1. Run the Basic Algorithm. If the output is a valid DNS label, + // [append the Cache domain suffix and] return. Otherwise continue to + // step 2. + prefix, err := domainPrefixBasic(domain) + // "A domain prefix is not a valid DNS label if it is longer than 63 + // characters" + if err == nil && len(prefix) <= 63 { + return prefix + } + // 2. Run the Fallback Algorithm. [Append the Cache domain suffix and] + // return. + return domainPrefixFallback(domain) +} + +// CacheURL computes the AMP cache URL for the publisher URL pubURL, using the +// AMP cache at cacheURL. contentType is a string such as "c" or "i" that +// indicates what type of serving the AMP cache is to perform. The Scheme of +// pubURL must be "http" or "https". The Port of pubURL, if any, must match the +// default for the scheme. cacheURL may not have RawQuery, Fragment, or +// RawFragment set, because the resulting URL's query and fragment are taken +// from the publisher URL. +// +// https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/ +func CacheURL(pubURL, cacheURL *url.URL, contentType string) (*url.URL, error) { + // The cache URL subdomain, including the domain prefix corresponding to + // the publisher URL's domain. + resultHost := domainPrefix(pubURL.Hostname()) + "." + cacheURL.Hostname() + if cacheURL.Port() != "" { + resultHost = net.JoinHostPort(resultHost, cacheURL.Port()) + } + + // https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/#url-path + // The first part of the path is the cache URL's own path, if any. + pathComponents := []string{cacheURL.EscapedPath()} + // The next path component is the content type. We cannot encode an + // empty content type, because it would result in consecutive path + // separators, which would semantically combine into a single separator. + if contentType == "" { + return nil, fmt.Errorf("invalid content type %+q", contentType) + } + pathComponents = append(pathComponents, url.PathEscape(contentType)) + // Then, we add an "s" path component, if the publisher URL scheme is + // "https". + switch pubURL.Scheme { + case "http": + // Do nothing. + case "https": + pathComponents = append(pathComponents, "s") + default: + return nil, fmt.Errorf("invalid scheme %+q in publisher URL", pubURL.Scheme) + } + // The next path component is the publisher URL's host. The AMP cache + // URL format specification is not clear about whether other + // subcomponents of the authority (namely userinfo and port) may appear + // here. We adopt a policy of forbidding userinfo, and requiring that + // the port be the default for the scheme (and then we omit the port + // entirely from the returned URL). + if pubURL.User != nil { + return nil, fmt.Errorf("publisher URL may not contain userinfo") + } + if port := pubURL.Port(); port != "" { + if !((pubURL.Scheme == "http" && port == "80") || (pubURL.Scheme == "https" && port == "443")) { + return nil, fmt.Errorf("publisher URL port %+q is not the default for scheme %+q", port, pubURL.Scheme) + } + } + // As with the content type, we cannot encode an empty host, because + // that would result in an empty path component. + if pubURL.Hostname() == "" { + return nil, fmt.Errorf("invalid host %+q in publisher URL", pubURL.Hostname()) + } + pathComponents = append(pathComponents, url.PathEscape(pubURL.Hostname())) + // Finally, we append the remainder of the original escaped path from + // the publisher URL. + pathComponents = append(pathComponents, pubURL.EscapedPath()) + + resultRawPath := path.Join(pathComponents...) + resultPath, err := url.PathUnescape(resultRawPath) + if err != nil { + return nil, err + } + + // The query and fragment of the returned URL always come from pubURL. + // Any query or fragment of cacheURL would be ignored. Return an error + // if either is set. + if cacheURL.RawQuery != "" { + return nil, fmt.Errorf("cache URL may not contain a query") + } + if cacheURL.Fragment != "" { + return nil, fmt.Errorf("cache URL may not contain a fragment") + } + + return &url.URL{ + Scheme: cacheURL.Scheme, + User: cacheURL.User, + Host: resultHost, + Path: resultPath, + RawPath: resultRawPath, + RawQuery: pubURL.RawQuery, + Fragment: pubURL.Fragment, + }, nil +} diff --git a/common/amp/cache_test.go b/common/amp/cache_test.go new file mode 100644 index 0000000..45950fd --- /dev/null +++ b/common/amp/cache_test.go @@ -0,0 +1,320 @@ +package amp + +import ( + "bytes" + "net/url" + "testing" + + "golang.org/x/net/idna" +) + +func TestDomainPrefixBasic(t *testing.T) { + // Tests expecting no error. + for _, test := range []struct { + domain, expected string + }{ + {"", ""}, + {"xn--", ""}, + {"...", "---"}, + + // Should not apply mappings such as case folding and + // normalization. + {"b\u00fccher.de", "xn--bcher-de-65a"}, + {"B\u00fccher.de", "xn--Bcher-de-65a"}, + {"bu\u0308cher.de", "xn--bucher-de-hkf"}, + + // Check some that differ between IDNA 2003 and IDNA 2008. + // https://unicode.org/reports/tr46/#Deviations + // https://util.unicode.org/UnicodeJsps/idna.jsp + {"faß.de", "xn--fa-de-mqa"}, + {"βόλοσ.com", "xn---com-4ld8c2a6a8e"}, + + // Lengths of 63 and 64. 64 is too long for a DNS label, but + // domainPrefixBasic is not expected to check for that. + {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + + // https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/#basic-algorithm + {"example.com", "example-com"}, + {"foo.example.com", "foo-example-com"}, + {"foo-example.com", "foo--example-com"}, + {"xn--57hw060o.com", "xn---com-p33b41770a"}, + {"\u26a1\U0001f60a.com", "xn---com-p33b41770a"}, + {"en-us.example.com", "0-en--us-example-com-0"}, + } { + output, err := domainPrefixBasic(test.domain) + if err != nil || output != test.expected { + t.Errorf("%+q → (%+q, %v), expected (%+q, %v)", + test.domain, output, err, test.expected, nil) + } + } + + // Tests expecting an error. + for _, domain := range []string{ + "xn---", + } { + output, err := domainPrefixBasic(domain) + if err == nil || output != "" { + t.Errorf("%+q → (%+q, %v), expected (%+q, non-nil)", + domain, output, err, "") + } + } +} + +func TestDomainPrefixFallback(t *testing.T) { + for _, test := range []struct { + domain, expected string + }{ + { + "", + "4oymiquy7qobjgx36tejs35zeqt24qpemsnzgtfeswmrw6csxbkq", + }, + { + "example.com", + "un42n5xov642kxrxrqiyanhcoupgql5lt4wtbkyt2ijflbwodfdq", + }, + + // These checked against the output of + // https://github.com/ampproject/amp-toolbox/tree/84cb3057e5f6c54d64369ddd285db1cb36237ee8/packages/cache-url, + // using the widget at + // https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/#url-format. + { + "000000000000000000000000000000000000000000000000000000000000.com", + "stejanx4hsijaoj4secyecy4nvqodk56kw72whwcmvdbtucibf5a", + }, + { + "00000000000000000000000000000000000000000000000000000000000a.com", + "jdcvbsorpnc3hcjrhst56nfm6ymdpovlawdbm2efyxpvlt4cpbya", + }, + { + "00000000000000000000000000000000000000000000000000000000000\u03bb.com", + "qhzqeumjkfpcpuic3vqruyjswcr7y7gcm3crqyhhywvn3xrhchfa", + }, + } { + output := domainPrefixFallback(test.domain) + if output != test.expected { + t.Errorf("%+q → %+q, expected %+q", + test.domain, output, test.expected) + } + } +} + +// Checks that domainPrefix chooses domainPrefixBasic or domainPrefixFallback as +// appropriate; i.e., always returns string that is a valid DNS label and is +// IDNA-decodable. +func TestDomainPrefix(t *testing.T) { + // A validating IDNA profile, which checks label length and that the + // label contains only certain ASCII characters. It does not do the + // ValidateLabels check, because that depends on the input having + // certain properties. + profile := idna.New( + idna.VerifyDNSLength(true), + idna.StrictDomainName(true), + ) + for _, domain := range []string{ + "example.com", + "\u0314example.com", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // 63 bytes + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // 64 bytes + "xn--57hw060o.com", + "a b c", + } { + output := domainPrefix(domain) + if bytes.IndexByte([]byte(output), '.') != -1 { + t.Errorf("%+q → %+q contains a dot", domain, output) + } + _, err := profile.ToUnicode(output) + if err != nil { + t.Errorf("%+q → error %v", domain, err) + } + } +} + +func mustParseURL(rawurl string) *url.URL { + u, err := url.Parse(rawurl) + if err != nil { + panic(err) + } + return u +} + +func TestCacheURL(t *testing.T) { + // Tests expecting no error. + for _, test := range []struct { + pub string + cache string + contentType string + expected string + }{ + // With or without trailing slash on pubURL. + { + "http://example.com/", + "https://amp.cache/", + "c", + "https://example-com.amp.cache/c/example.com", + }, + { + "http://example.com", + "https://amp.cache/", + "c", + "https://example-com.amp.cache/c/example.com", + }, + // https pubURL. + { + "https://example.com/", + "https://amp.cache/", + "c", + "https://example-com.amp.cache/c/s/example.com", + }, + // The content type should be escaped if necessary. + { + "http://example.com/", + "https://amp.cache/", + "/", + "https://example-com.amp.cache/%2F/example.com", + }, + // Retain pubURL path, query, and fragment, including escaping. + { + "http://example.com/my%2Fpath/index.html?a=1#fragment", + "https://amp.cache/", + "c", + "https://example-com.amp.cache/c/example.com/my%2Fpath/index.html?a=1#fragment", + }, + // Retain scheme, userinfo, port, and path of cacheURL, escaping + // whatever is necessary. + { + "http://example.com", + "http://cache%2Fuser:cache%40pass@amp.cache:123/with/../../path/..%2f../", + "c", + "http://cache%2Fuser:cache%40pass@example-com.amp.cache:123/path/..%2f../c/example.com", + }, + // Port numbers in pubURL are allowed, if they're the default + // for scheme. + { + "http://example.com:80/", + "https://amp.cache/", + "c", + "https://example-com.amp.cache/c/example.com", + }, + { + "https://example.com:443/", + "https://amp.cache/", + "c", + "https://example-com.amp.cache/c/s/example.com", + }, + // "?" at the end of cacheURL is okay, as long as the query is + // empty. + { + "http://example.com/", + "https://amp.cache/?", + "c", + "https://example-com.amp.cache/c/example.com", + }, + + // https://developers.google.com/amp/cache/overview#example-requesting-document-using-tls + { + "https://example.com/amp_document.html", + "https://cdn.ampproject.org/", + "c", + "https://example-com.cdn.ampproject.org/c/s/example.com/amp_document.html", + }, + // https://developers.google.com/amp/cache/overview#example-requesting-image-using-plain-http + { + "http://example.com/logo.png", + "https://cdn.ampproject.org/", + "i", + "https://example-com.cdn.ampproject.org/i/example.com/logo.png", + }, + // https://developers.google.com/amp/cache/overview#query-parameter-example + { + "https://example.com/g?value=Hello%20World", + "https://cdn.ampproject.org/", + "c", + "https://example-com.cdn.ampproject.org/c/s/example.com/g?value=Hello%20World", + }, + } { + pubURL := mustParseURL(test.pub) + cacheURL := mustParseURL(test.cache) + outputURL, err := CacheURL(pubURL, cacheURL, test.contentType) + if err != nil { + t.Errorf("%+q %+q %+q → error %v", + test.pub, test.cache, test.contentType, err) + continue + } + if outputURL.String() != test.expected { + t.Errorf("%+q %+q %+q → %+q, expected %+q", + test.pub, test.cache, test.contentType, outputURL, test.expected) + continue + } + } + + // Tests expecting an error. + for _, test := range []struct { + pub string + cache string + contentType string + }{ + // Empty content type. + { + "http://example.com/", + "https://amp.cache/", + "", + }, + // Empty host. + { + "http:///index.html", + "https://amp.cache/", + "c", + }, + // Empty scheme. + { + "//example.com/", + "https://amp.cache/", + "c", + }, + // Unrecognized scheme. + { + "ftp://example.com/", + "https://amp.cache/", + "c", + }, + // Wrong port number for scheme. + { + "http://example.com:443/", + "https://amp.cache/", + "c", + }, + // userinfo in pubURL. + { + "http://user@example.com/", + "https://amp.cache/", + "c", + }, + { + "http://user:pass@example.com/", + "https://amp.cache/", + "c", + }, + // cacheURL may not contain a query. + { + "http://example.com/", + "https://amp.cache/?a=1", + "c", + }, + // cacheURL may not contain a fragment. + { + "http://example.com/", + "https://amp.cache/#fragment", + "c", + }, + } { + pubURL := mustParseURL(test.pub) + cacheURL := mustParseURL(test.cache) + outputURL, err := CacheURL(pubURL, cacheURL, test.contentType) + if err == nil { + t.Errorf("%+q %+q %+q → %+q, expected error", + test.pub, test.cache, test.contentType, outputURL) + continue + } + } +} diff --git a/common/amp/doc.go b/common/amp/doc.go new file mode 100644 index 0000000..1387114 --- /dev/null +++ b/common/amp/doc.go @@ -0,0 +1,88 @@ +/* +Package amp provides functions for working with the AMP (Accelerated Mobile +Pages) subset of HTML, and conveying binary data through an AMP cache. + +AMP cache + +The CacheURL function takes a plain URL and converts it to be accessed through a +given AMP cache. + +The EncodePath and DecodePath functions provide a way to encode data into the +suffix of a URL path. AMP caches do not support HTTP POST, but encoding data +into a URL path with GET is an alternative means of sending data to the server. +The format of an encoded path is: + 0<0 or more bytes, including slash>/ +That is: +* "0", a format version number, which controls the interpretation of the rest of +the path. Only the first byte matters as a version indicator (not the whole +first path component). +* Any number of slash or non-slash bytes. These may be used as padding or to +prevent cache collisions in the AMP cache. +* A final slash. +* base64 encoding of the data, using the URL-safe alphabet (which does not +include slash). + +For example, an encoding of the string "This is path-encoded data." is the +following. The "lgWHcwhXFjUm" following the format version number is random +padding that will be ignored on decoding. + 0lgWHcwhXFjUm/VGhpcyBpcyBwYXRoLWVuY29kZWQgZGF0YS4 + +It is the caller's responsibility to add or remove any directory path prefix +before calling EncodePath or DecodePath. + +AMP armor + +AMP armor is a data encoding scheme that that satisfies the requirements of the +AMP (Accelerated Mobile Pages) subset of HTML, and survives modification by an +AMP cache. For the requirements of AMP HTML, see +https://amp.dev/documentation/guides-and-tutorials/learn/spec/amphtml/. +For modifications that may be made by an AMP cache, see +https://github.com/ampproject/amphtml/blob/main/docs/spec/amp-cache-modifications.md. + +The encoding is based on ones created by Ivan Markin. See codec/amp/ in +https://github.com/nogoegst/amper and discussion at +https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/25985. + +The encoding algorithm works as follows. Base64-encode the input. Prepend the +input with the byte '0'; this is a protocol version indicator that the decoder +can use to determine how to interpret the bytes that follow. Split the base64 +into fixed-size chunks separated by whitespace. Take up to 1024 chunks at a +time, and wrap them in a pre element. Then, situate the markup so far within the +body of the AMP HTML boilerplate. The decoding algorithm is to scan the HTML for +pre elements, split their text contents on whitespace and concatenate, then +base64 decode. The base64 encoding uses the standard alphabet, with normal "=" +padding (https://tools.ietf.org/html/rfc4648#section-4). + +The reason for splitting the base64 into chunks is that AMP caches reportedly +truncate long strings that are not broken by whitespace: +https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/25985#note_2592348. +The characters that may separate the chunks are the ASCII whitespace characters +(https://infra.spec.whatwg.org/#ascii-whitespace) "\x09", "\x0a", "\x0c", +"\x0d", and "\x20". The reason for separating the chunks into pre elements is to +limit the amount of text a decoder may have to buffer while parsing the HTML. +Each pre element may contain at most 64 KB of text. pre elements may not be +nested. + +Example + +The following is the result of encoding the string +"This was encoded with AMP armor.": + + + + + + + + + + + +
+	0VGhpcyB3YXMgZW5jb2RlZCB3aXRoIEF
+	NUCBhcm1vci4=
+	
+ + +*/ +package amp diff --git a/common/amp/path.go b/common/amp/path.go new file mode 100644 index 0000000..5903694 --- /dev/null +++ b/common/amp/path.go @@ -0,0 +1,44 @@ +package amp + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "strings" +) + +// EncodePath encodes data in a way that is suitable for the suffix of an AMP +// cache URL. +func EncodePath(data []byte) string { + var cacheBreaker [9]byte + _, err := rand.Read(cacheBreaker[:]) + if err != nil { + panic(err) + } + b64 := base64.RawURLEncoding.EncodeToString + return "0" + b64(cacheBreaker[:]) + "/" + b64(data) +} + +// DecodePath decodes data from a path suffix as encoded by EncodePath. The path +// must have already been trimmed of any directory prefix (as might be present +// in, e.g., an HTTP request). That is, the first character of path should be +// the "0" message format indicator. +func DecodePath(path string) ([]byte, error) { + if len(path) < 1 { + return nil, fmt.Errorf("missing format indicator") + } + version := path[0] + rest := path[1:] + switch version { + case '0': + // Ignore everything else up to and including the final slash + // (there must be at least one slash). + i := strings.LastIndexByte(rest, '/') + if i == -1 { + return nil, fmt.Errorf("missing data") + } + return base64.RawURLEncoding.DecodeString(rest[i+1:]) + default: + return nil, fmt.Errorf("unknown format indicator %q", version) + } +} diff --git a/common/amp/path_test.go b/common/amp/path_test.go new file mode 100644 index 0000000..20e4ccf --- /dev/null +++ b/common/amp/path_test.go @@ -0,0 +1,54 @@ +package amp + +import ( + "testing" +) + +func TestDecodePath(t *testing.T) { + for _, test := range []struct { + path string + expectedData string + expectedErrStr string + }{ + {"", "", "missing format indicator"}, + {"0", "", "missing data"}, + {"0foobar", "", "missing data"}, + {"/0/YWJj", "", "unknown format indicator '/'"}, + + {"0/", "", ""}, + {"0foobar/", "", ""}, + {"0/YWJj", "abc", ""}, + {"0///YWJj", "abc", ""}, + {"0foobar/YWJj", "abc", ""}, + {"0/foobar/YWJj", "abc", ""}, + } { + data, err := DecodePath(test.path) + if test.expectedErrStr != "" { + if err == nil || err.Error() != test.expectedErrStr { + t.Errorf("%+q expected error %+q, got %+q", + test.path, test.expectedErrStr, err) + } + } else if err != nil { + t.Errorf("%+q expected no error, got %+q", test.path, err) + } else if string(data) != test.expectedData { + t.Errorf("%+q expected data %+q, got %+q", + test.path, test.expectedData, data) + } + } +} + +func TestPathRoundTrip(t *testing.T) { + for _, data := range []string{ + "", + "\x00", + "/", + "hello world", + } { + decoded, err := DecodePath(EncodePath([]byte(data))) + if err != nil { + t.Errorf("%+q roundtripped with error %v", data, err) + } else if string(decoded) != data { + t.Errorf("%+q roundtripped to %+q", data, decoded) + } + } +} From c13810192d243690dab4c0e890a1f50273a22ca1 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sun, 18 Jul 2021 14:18:32 -0600 Subject: [PATCH 217/385] Skeleton of ampCacheRendezvous. Currently the same as httpRendezvous, but activated using the -ampcache command-line option. --- client/lib/rendezvous.go | 13 +++++- client/lib/rendezvous_ampcache.go | 78 +++++++++++++++++++++++++++++++ client/lib/snowflake.go | 7 +-- client/snowflake.go | 3 +- 4 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 client/lib/rendezvous_ampcache.go diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 8568120..8af638f 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -59,13 +59,22 @@ func CreateBrokerTransport() http.RoundTripper { // Construct a new BrokerChannel, where: // |broker| is the full URL of the facilitating program which assigns proxies // to clients, and |front| is the option fronting domain. -func NewBrokerChannel(broker string, front string, transport http.RoundTripper, keepLocalAddresses bool) (*BrokerChannel, error) { +func NewBrokerChannel(broker, ampCache, front string, transport http.RoundTripper, keepLocalAddresses bool) (*BrokerChannel, error) { log.Println("Rendezvous using Broker at:", broker) + if ampCache != "" { + log.Println("Through AMP cache at:", ampCache) + } if front != "" { log.Println("Domain fronting using:", front) } - rendezvous, err := newHTTPRendezvous(broker, front, transport) + var rendezvous rendezvousMethod + var err error + if ampCache != "" { + rendezvous, err = newAMPCacheRendezvous(broker, ampCache, front, transport) + } else { + rendezvous, err = newHTTPRendezvous(broker, front, transport) + } if err != nil { return nil, err } diff --git a/client/lib/rendezvous_ampcache.go b/client/lib/rendezvous_ampcache.go new file mode 100644 index 0000000..89745f4 --- /dev/null +++ b/client/lib/rendezvous_ampcache.go @@ -0,0 +1,78 @@ +package lib + +import ( + "bytes" + "errors" + "log" + "net/http" + "net/url" +) + +// ampCacheRendezvous is a rendezvousMethod that communicates with the +// .../amp/client route of the broker, optionally over an AMP cache proxy, and +// with optional domain fronting. +type ampCacheRendezvous struct { + brokerURL *url.URL + cacheURL *url.URL // Optional AMP cache URL. + front string // Optional front domain to replace url.Host in requests. + transport http.RoundTripper // Used to make all requests. +} + +// newAMPCacheRendezvous creates a new ampCacheRendezvous that contacts the +// broker at the given URL, optionally proxying through an AMP cache, and with +// an optional front domain. transport is the http.RoundTripper used to make all +// requests. +func newAMPCacheRendezvous(broker, cache, front string, transport http.RoundTripper) (*ampCacheRendezvous, error) { + brokerURL, err := url.Parse(broker) + if err != nil { + return nil, err + } + var cacheURL *url.URL + if cache != "" { + var err error + cacheURL, err = url.Parse(cache) + if err != nil { + return nil, err + } + } + return &CacheRendezvous{ + brokerURL: brokerURL, + cacheURL: cacheURL, + front: front, + transport: transport, + }, nil +} + +func (r *ampCacheRendezvous) Exchange(encPollReq []byte) ([]byte, error) { + log.Println("Negotiating via AMP cache rendezvous...") + log.Println("Broker URL:", r.brokerURL) + log.Println("AMP cache URL:", r.cacheURL) + log.Println("Front domain:", r.front) + + // Suffix the path with the broker's client registration handler. + reqURL := r.brokerURL.ResolveReference(&url.URL{Path: "client"}) + req, err := http.NewRequest("POST", reqURL.String(), bytes.NewReader(encPollReq)) + if err != nil { + return nil, err + } + + if r.front != "" { + // Do domain fronting. Replace the domain in the URL's with the + // front, and store the original domain the HTTP Host header. + req.Host = req.URL.Host + req.URL.Host = r.front + } + + resp, err := r.transport.RoundTrip(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + log.Printf("AMP cache rendezvous response: %s", resp.Status) + if resp.StatusCode != http.StatusOK { + return nil, errors.New(BrokerErrorUnexpected) + } + + return limitedRead(resp.Body, readLimit) +} diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index f643c0a..0fc7671 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -39,7 +39,8 @@ type Transport struct { // iceAddresses are the STUN/TURN urls needed for WebRTC negotiation // keepLocalAddresses is a flag to enable sending local network addresses (for testing purposes) // max is the maximum number of snowflakes the client should gather for each SOCKS connection -func NewSnowflakeClient(brokerURL, frontDomain string, iceAddresses []string, keepLocalAddresses bool, max int) (*Transport, error) { +func NewSnowflakeClient(brokerURL, ampCacheURL, frontDomain string, + iceAddresses []string, keepLocalAddresses bool, max int) (*Transport, error) { log.Println("\n\n\n --- Starting Snowflake Client ---") @@ -57,9 +58,9 @@ func NewSnowflakeClient(brokerURL, frontDomain string, iceAddresses []string, ke log.Printf("url: %v", strings.Join(server.URLs, " ")) } - // Use potentially domain-fronting broker to rendezvous. + // Rendezvous with broker using the given parameters. broker, err := NewBrokerChannel( - brokerURL, frontDomain, CreateBrokerTransport(), + brokerURL, ampCacheURL, frontDomain, CreateBrokerTransport(), keepLocalAddresses) if err != nil { return nil, err diff --git a/client/snowflake.go b/client/snowflake.go index af9c2e4..ef06a2d 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -94,6 +94,7 @@ func main() { iceServersCommas := flag.String("ice", "", "comma-separated list of ICE servers") brokerURL := flag.String("url", "", "URL of signaling broker") frontDomain := flag.String("front", "", "front domain") + ampCacheURL := flag.String("ampcache", "", "URL of AMP cache to use as a proxy for signaling") logFilename := flag.String("log", "", "name of log file") logToStateDir := flag.Bool("log-to-state-dir", false, "resolve the log file relative to tor's pt state dir") keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates") @@ -140,7 +141,7 @@ func main() { iceAddresses := strings.Split(strings.TrimSpace(*iceServersCommas), ",") - transport, err := sf.NewSnowflakeClient(*brokerURL, *frontDomain, iceAddresses, + transport, err := sf.NewSnowflakeClient(*brokerURL, *ampCacheURL, *frontDomain, iceAddresses, *keepLocalAddresses || *oldKeepLocalAddresses, *max) if err != nil { log.Fatal("Failed to start snowflake transport: ", err) From 5adb99402861569a0c3d0e46299d48a583f48725 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sun, 18 Jul 2021 14:57:45 -0600 Subject: [PATCH 218/385] Implement ampCacheRendezvous. --- client/lib/rendezvous_ampcache.go | 56 +++++++++++-- client/lib/rendezvous_test.go | 132 +++++++++++++++++++++++++++++- 2 files changed, 181 insertions(+), 7 deletions(-) diff --git a/client/lib/rendezvous_ampcache.go b/client/lib/rendezvous_ampcache.go index 89745f4..4856893 100644 --- a/client/lib/rendezvous_ampcache.go +++ b/client/lib/rendezvous_ampcache.go @@ -1,11 +1,14 @@ package lib import ( - "bytes" "errors" + "io" + "io/ioutil" "log" "net/http" "net/url" + + "git.torproject.org/pluggable-transports/snowflake.git/common/amp" ) // ampCacheRendezvous is a rendezvousMethod that communicates with the @@ -49,9 +52,22 @@ func (r *ampCacheRendezvous) Exchange(encPollReq []byte) ([]byte, error) { log.Println("AMP cache URL:", r.cacheURL) log.Println("Front domain:", r.front) - // Suffix the path with the broker's client registration handler. - reqURL := r.brokerURL.ResolveReference(&url.URL{Path: "client"}) - req, err := http.NewRequest("POST", reqURL.String(), bytes.NewReader(encPollReq)) + // We cannot POST a body through an AMP cache, so instead we GET and + // encode the client poll request message into the URL. + reqURL := r.brokerURL.ResolveReference(&url.URL{ + Path: "amp/client/" + amp.EncodePath(encPollReq), + }) + + if r.cacheURL != nil { + // Rewrite reqURL to its AMP cache version. + var err error + reqURL, err = amp.CacheURL(reqURL, r.cacheURL, "c") + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest("GET", reqURL.String(), nil) if err != nil { return nil, err } @@ -71,8 +87,38 @@ func (r *ampCacheRendezvous) Exchange(encPollReq []byte) ([]byte, error) { log.Printf("AMP cache rendezvous response: %s", resp.Status) if resp.StatusCode != http.StatusOK { + // A non-200 status indicates an error: + // * If the broker returns a page with invalid AMP, then the AMP + // cache returns a redirect that would bypass the cache. + // * If the broker returns a 5xx status, the AMP cache + // translates it to a 404. + // https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/#redirect-%26-error-handling + return nil, errors.New(BrokerErrorUnexpected) + } + if _, err := resp.Location(); err == nil { + // The Google AMP Cache may return a "silent redirect" with + // status 200, a Location header set, and a JavaScript redirect + // in the body. The redirect points directly at the origin + // server for the request (bypassing the AMP cache). We do not + // follow redirects nor execute JavaScript, but in any case we + // cannot extract information from this response and can only + // treat it as an error. return nil, errors.New(BrokerErrorUnexpected) } - return limitedRead(resp.Body, readLimit) + lr := io.LimitReader(resp.Body, readLimit+1) + dec, err := amp.NewArmorDecoder(lr) + if err != nil { + return nil, err + } + encPollResp, err := ioutil.ReadAll(dec) + if err != nil { + return nil, err + } + if lr.(*io.LimitedReader).N == 0 { + // We hit readLimit while decoding AMP armor, that's an error. + return nil, io.ErrUnexpectedEOF + } + + return encPollResp, err } diff --git a/client/lib/rendezvous_test.go b/client/lib/rendezvous_test.go index c263e37..6a1a071 100644 --- a/client/lib/rendezvous_test.go +++ b/client/lib/rendezvous_test.go @@ -9,6 +9,7 @@ import ( "net/http" "testing" + "git.torproject.org/pluggable-transports/snowflake.git/common/amp" "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "git.torproject.org/pluggable-transports/snowflake.git/common/nat" . "github.com/smartystreets/goconvey/convey" @@ -64,6 +65,8 @@ func makeEncPollResp(answer, errorStr string) []byte { return encPollResp } +var fakeEncPollReq = makeEncPollReq(`{"type":"offer","sdp":"test"}`) + func TestHTTPRendezvous(t *testing.T) { Convey("HTTP rendezvous", t, func() { Convey("Construct httpRendezvous with no front domain", func() { @@ -86,8 +89,6 @@ func TestHTTPRendezvous(t *testing.T) { So(rend.transport, ShouldEqual, transport) }) - fakeEncPollReq := makeEncPollReq(`{"type":"offer","sdp":"test"}`) - Convey("httpRendezvous.Exchange responds with answer", func() { fakeEncPollResp := makeEncPollResp( `{"answer": "{\"type\":\"answer\",\"sdp\":\"fake\"}" }`, @@ -143,3 +144,130 @@ func TestHTTPRendezvous(t *testing.T) { }) }) } + +func ampArmorEncode(p []byte) []byte { + var buf bytes.Buffer + enc, err := amp.NewArmorEncoder(&buf) + if err != nil { + panic(err) + } + _, err = enc.Write(p) + if err != nil { + panic(err) + } + err = enc.Close() + if err != nil { + panic(err) + } + return buf.Bytes() +} + +func TestAMPCacheRendezvous(t *testing.T) { + Convey("AMP cache rendezvous", t, func() { + Convey("Construct ampCacheRendezvous with no cache and no front domain", func() { + transport := &mockTransport{http.StatusOK, []byte{}} + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", transport) + So(err, ShouldBeNil) + So(rend.brokerURL, ShouldNotBeNil) + So(rend.brokerURL.String(), ShouldResemble, "http://test.broker") + So(rend.cacheURL, ShouldBeNil) + So(rend.front, ShouldResemble, "") + So(rend.transport, ShouldEqual, transport) + }) + + Convey("Construct ampCacheRendezvous with cache and no front domain", func() { + transport := &mockTransport{http.StatusOK, []byte{}} + rend, err := newAMPCacheRendezvous("http://test.broker", "https://amp.cache/", "", transport) + So(err, ShouldBeNil) + So(rend.brokerURL, ShouldNotBeNil) + So(rend.brokerURL.String(), ShouldResemble, "http://test.broker") + So(rend.cacheURL, ShouldNotBeNil) + So(rend.cacheURL.String(), ShouldResemble, "https://amp.cache/") + So(rend.front, ShouldResemble, "") + So(rend.transport, ShouldEqual, transport) + }) + + Convey("Construct ampCacheRendezvous with no cache and front domain", func() { + transport := &mockTransport{http.StatusOK, []byte{}} + rend, err := newAMPCacheRendezvous("http://test.broker", "", "front", transport) + So(err, ShouldBeNil) + So(rend.brokerURL, ShouldNotBeNil) + So(rend.brokerURL.String(), ShouldResemble, "http://test.broker") + So(rend.cacheURL, ShouldBeNil) + So(rend.front, ShouldResemble, "front") + So(rend.transport, ShouldEqual, transport) + }) + + Convey("Construct ampCacheRendezvous with cache and front domain", func() { + transport := &mockTransport{http.StatusOK, []byte{}} + rend, err := newAMPCacheRendezvous("http://test.broker", "https://amp.cache/", "front", transport) + So(err, ShouldBeNil) + So(rend.brokerURL, ShouldNotBeNil) + So(rend.brokerURL.String(), ShouldResemble, "http://test.broker") + So(rend.cacheURL, ShouldNotBeNil) + So(rend.cacheURL.String(), ShouldResemble, "https://amp.cache/") + So(rend.front, ShouldResemble, "front") + So(rend.transport, ShouldEqual, transport) + }) + + Convey("ampCacheRendezvous.Exchange responds with answer", func() { + fakeEncPollResp := makeEncPollResp( + `{"answer": "{\"type\":\"answer\",\"sdp\":\"fake\"}" }`, + "", + ) + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", + &mockTransport{http.StatusOK, ampArmorEncode(fakeEncPollResp)}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldBeNil) + So(answer, ShouldResemble, fakeEncPollResp) + }) + + Convey("ampCacheRendezvous.Exchange responds with no answer", func() { + fakeEncPollResp := makeEncPollResp( + "", + `{"error": "no snowflake proxies currently available"}`, + ) + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", + &mockTransport{http.StatusOK, ampArmorEncode(fakeEncPollResp)}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldBeNil) + So(answer, ShouldResemble, fakeEncPollResp) + }) + + Convey("ampCacheRendezvous.Exchange fails with unexpected HTTP status code", func() { + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", + &mockTransport{http.StatusInternalServerError, []byte{}}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldNotBeNil) + So(answer, ShouldBeNil) + So(err.Error(), ShouldResemble, BrokerErrorUnexpected) + }) + + Convey("ampCacheRendezvous.Exchange fails with error", func() { + transportErr := errors.New("error") + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", + &errorTransport{err: transportErr}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldEqual, transportErr) + So(answer, ShouldBeNil) + }) + + Convey("ampCacheRendezvous.Exchange fails with large read", func() { + // readLimit should apply to the raw HTTP body, not the + // encoded bytes. Encode readLimit bytes—the encoded + // size will be larger—and try to read the body. It + // should fail. + rend, err := newAMPCacheRendezvous("http://test.broker", "", "", + &mockTransport{http.StatusOK, ampArmorEncode(make([]byte, readLimit))}) + So(err, ShouldBeNil) + _, err = rend.Exchange(fakeEncPollReq) + // We may get io.ErrUnexpectedEOF here, or something + // like "missing
tag". + So(err, ShouldNotBeNil) + }) + }) +} From e833119befa052e4837fe147f8bc2766a4ca7c54 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sun, 18 Jul 2021 23:37:41 -0600 Subject: [PATCH 219/385] Broker /amp/client route (AMP cache client registration). --- broker/amp.go | 76 ++++++++++++++++++++++++++++++++ broker/broker.go | 2 + broker/snowflake-broker_test.go | 78 ++++++++++++++++++++++++++++++++- 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 broker/amp.go diff --git a/broker/amp.go b/broker/amp.go new file mode 100644 index 0000000..8641e51 --- /dev/null +++ b/broker/amp.go @@ -0,0 +1,76 @@ +package main + +import ( + "log" + "net/http" + "strings" + + "git.torproject.org/pluggable-transports/snowflake.git/common/amp" + "git.torproject.org/pluggable-transports/snowflake.git/common/messages" +) + +// ampClientOffers is the AMP-speaking endpoint for client poll messages, +// intended for access via an AMP cache. In contrast to the other clientOffers, +// the client's encoded poll message is stored in the URL path rather than the +// HTTP request body (because an AMP cache does not support POST), and the +// encoded client poll response is sent back as AMP-armored HTML. +func ampClientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { + // The encoded client poll message immediately follows the /amp/client/ + // path prefix, so this function unfortunately needs to be aware of and + // remote its own routing prefix. + path := strings.TrimPrefix(r.URL.Path, "/amp/client/") + if path == r.URL.Path { + // The path didn't start with the expected prefix. This probably + // indicates an internal bug. + log.Println("ampClientOffers: unexpected prefix in path") + w.WriteHeader(http.StatusInternalServerError) + return + } + + var encPollReq []byte + var response []byte + var err error + + encPollReq, err = amp.DecodePath(path) + if err == nil { + arg := messages.Arg{ + Body: encPollReq, + RemoteAddr: "", + } + err = i.ClientOffers(arg, &response) + } else { + response, err = (&messages.ClientPollResponse{ + Error: "cannot decode URL path", + }).EncodePollResponse() + } + + if err != nil { + // We couldn't even construct a JSON object containing an error + // message :( Nothing to do but signal an error at the HTTP + // layer. The AMP cache will translate this 500 status into a + // 404 status. + // https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/#redirect-%26-error-handling + log.Printf("ampClientOffers: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html") + // Attempt to hint to an AMP cache not to waste resources caching this + // document. "The Google AMP Cache considers any document fresh for at + // least 15 seconds." + // https://developers.google.com/amp/cache/overview#google-amp-cache-updates + w.Header().Set("Cache-Control", "max-age=15") + w.WriteHeader(http.StatusOK) + + enc, err := amp.NewArmorEncoder(w) + if err != nil { + log.Printf("amp.NewArmorEncoder: %v", err) + return + } + defer enc.Close() + + if _, err := enc.Write(response); err != nil { + log.Printf("ampClientOffers: unable to write answer: %v", err) + } +} diff --git a/broker/broker.go b/broker/broker.go index 437a4d1..6c855f3 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -218,6 +218,8 @@ func main() { http.Handle("/metrics", MetricsHandler{metricsFilename, metricsHandler}) http.Handle("/prometheus", promhttp.HandlerFor(ctx.metrics.promMetrics.registry, promhttp.HandlerOpts{})) + http.Handle("/amp/client/", SnowflakeHandler{i, ampClientOffers}) + server := http.Server{ Addr: addr, } diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 9e1c9f1..233cfea 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "container/heap" + "io" "io/ioutil" "log" "net" @@ -13,6 +14,7 @@ import ( "testing" "time" + "git.torproject.org/pluggable-transports/snowflake.git/common/amp" . "github.com/smartystreets/goconvey/convey" ) @@ -24,6 +26,15 @@ func NullLogger() *log.Logger { var promOnce sync.Once +func decodeAMPArmorToString(r io.Reader) (string, error) { + dec, err := amp.NewArmorDecoder(r) + if err != nil { + return "", err + } + p, err := ioutil.ReadAll(dec) + return string(p), err +} + func TestBroker(t *testing.T) { Convey("Context", t, func() { @@ -69,7 +80,7 @@ func TestBroker(t *testing.T) { So(offer.sdp, ShouldResemble, []byte("test offer")) }) - Convey("Responds to client offers...", func() { + Convey("Responds to HTTP client offers...", func() { w := httptest.NewRecorder() data := bytes.NewReader( []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"unknown\"}")) @@ -117,7 +128,7 @@ func TestBroker(t *testing.T) { }) }) - Convey("Responds to legacy client offers...", func() { + Convey("Responds to HTTP legacy client offers...", func() { w := httptest.NewRecorder() data := bytes.NewReader([]byte("{test}")) r, err := http.NewRequest("POST", "snowflake.broker/client", data) @@ -165,6 +176,69 @@ func TestBroker(t *testing.T) { }) + Convey("Responds to AMP client offers...", func() { + w := httptest.NewRecorder() + encPollReq := []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"unknown\"}") + r, err := http.NewRequest("GET", "/amp/client/"+amp.EncodePath(encPollReq), nil) + So(err, ShouldBeNil) + + Convey("with status 200 when request is badly formatted.", func() { + r, err := http.NewRequest("GET", "/amp/client/bad", nil) + So(err, ShouldBeNil) + ampClientOffers(i, w, r) + body, err := decodeAMPArmorToString(w.Body) + So(err, ShouldBeNil) + So(body, ShouldEqual, `{"error":"cannot decode URL path"}`) + }) + + Convey("with error when no snowflakes are available.", func() { + ampClientOffers(i, w, r) + So(w.Code, ShouldEqual, http.StatusOK) + body, err := decodeAMPArmorToString(w.Body) + So(err, ShouldBeNil) + So(body, ShouldEqual, `{"error":"no snowflake proxies currently available"}`) + }) + + Convey("with a proxy answer if available.", func() { + done := make(chan bool) + // Prepare a fake proxy to respond with. + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) + go func() { + ampClientOffers(i, w, r) + done <- true + }() + offer := <-snowflake.offerChannel + So(offer.sdp, ShouldResemble, []byte("fake")) + snowflake.answerChannel <- "fake answer" + <-done + body, err := decodeAMPArmorToString(w.Body) + So(err, ShouldBeNil) + So(body, ShouldEqual, `{"answer":"fake answer"}`) + So(w.Code, ShouldEqual, http.StatusOK) + }) + + Convey("Times out when no proxy responds.", func() { + if testing.Short() { + return + } + done := make(chan bool) + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) + go func() { + ampClientOffers(i, w, r) + // Takes a few seconds here... + done <- true + }() + offer := <-snowflake.offerChannel + So(offer.sdp, ShouldResemble, []byte("fake")) + <-done + So(w.Code, ShouldEqual, http.StatusOK) + body, err := decodeAMPArmorToString(w.Body) + So(err, ShouldBeNil) + So(body, ShouldEqual, `{"error":"timed out waiting for answer!"}`) + }) + + }) + Convey("Responds to proxy polls...", func() { done := make(chan bool) w := httptest.NewRecorder() From 521eb4d4d6d76a1d57d3c8fc5c3a8261c171ea4e Mon Sep 17 00:00:00 2001 From: David Fifield Date: Mon, 19 Jul 2021 09:01:17 -0600 Subject: [PATCH 220/385] Add info about rendezvous methods to client README. --- client/README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/client/README.md b/client/README.md index aed11c3..0680408 100644 --- a/client/README.md +++ b/client/README.md @@ -52,3 +52,59 @@ To bootstrap Tor, run: tor -f torrc ``` This should start the client plugin, bootstrapping to 100% using WebRTC. + +### Registration methods + +The Snowflake client supports a few different ways of communicating with the broker. +This initial step is sometimes called rendezvous. + +#### Domain fronting HTTPS + +For domain fronting rendezvous, use the `-url` and `-front` command-line options together. +[Domain fronting](https://www.bamsoftware.com/papers/fronting/) +hides the externally visible domain name from an external observer, +making it appear that the Snowflake client is communicating with some server +other than the Snowflake broker. + +* `-url` is the HTTPS URL of a forwarder to the broker, on some service that supports domain fronting, such as a CDN. +* `-front` is the domain name to show externally. It must be another domain on the same service. + +Example: +``` +-url https://snowflake-broker.torproject.net.global.prod.fastly.net/ \ +-front cdn.sstatic.net \ +``` + +#### AMP cache + +For AMP cache rendezvous, use the `-url`, `-ampcache`, and `-front` command-line options together. +[AMP](https://amp.dev/documentation/) is a standard for web pages for mobile computers. +An [AMP cache](https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/how_amp_pages_are_cached/) +is a cache and proxy specialized for AMP pages. +The Snowflake broker has the ability to make its client registration responses look like AMP pages, +so it can be accessed through an AMP cache. +When you use AMP cache rendezvous, it appears to an observer that the Snowflake client +is accessing an AMP cache, or some other domain operated by the same organization. +You still need to use the `-front` command-line option, because the +[format of AMP cache URLs](https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/) +would otherwise reveal the domain name of the broker. + +There is only one AMP cache that works with this option, +the Google AMP cache at https://cdn.ampproject.org/. + +* `-url` is the HTTPS URL of the broker. +* `-ampcache` is `https://cdn.ampproject.org/`. +* `-front` is any Google domain, such as `www.google.com`. + +Example: +``` +-url https://snowflake-broker.torproject.net/ \ +-ampcache https://cdn.ampproject.org/ \ +-front www.google.com \ +``` + +#### Direct access + +It is also possible to access the broker directly using HTTPS, without domain fronting, +for testing purposes. This mode is not suitable for circumvention, because the +broker is easily blocked by its address. From f2dc41d77891816b3f6aec78cf9491fad6999388 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Mon, 26 Jul 2021 10:23:12 -0600 Subject: [PATCH 221/385] Document /amp/client in broker-spec.txt. --- doc/broker-spec.txt | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/doc/broker-spec.txt b/doc/broker-spec.txt index f2cd231..f25be79 100644 --- a/doc/broker-spec.txt +++ b/doc/broker-spec.txt @@ -107,6 +107,11 @@ through the exchange of WebRTC SDP information with its endpoints. 2.1. Client interactions with the broker +The broker offers multiple ways for clients to exchange registration +messages. + +2.1.1. HTTPS POST + Clients interact with the broker by making a POST request to `/client` with the offer SDP in the request body: ``` @@ -130,6 +135,38 @@ If no proxies were available, they receive a 503 status code: HTTP 503 Service Unavailable ``` +2.1.2. AMP + +The broker's /amp/client endpoint receives client poll messages encoded +into the URL path, and sends client poll responses encoded as HTML that +conforms to the requirements of AMP (Accelerated Mobile Pages). This +endpoint is intended to be accessed through an AMP cache, using the +-ampcache option of snowflake-client. + +The client encodes its poll message into a GET request as follows: +``` +GET /amp/client/0[0 or more bytes]/[base64 of client poll message] +``` +The components of the path are as follows: +* "/amp/client/", the root of the endpoint. +* "0", a format version number, which controls the interpretation of the + rest of the path. Only the first byte matters as a version indicator + (not the whole first path component). +* Any number of slash or non-slash bytes. These may be used as padding + or to prevent cache collisions in the AMP cache. +* A final slash. +* base64 encoding of the client poll message, using the URL-safe + alphabet (which does not include slash). + +The broker returns a client poll response message in the HTTP response. +The message is encoded using AMP armor, an AMP-compatible HTML encoding. +The data stream is notionally a "0" byte (a format version indicator) +followed by the base64 encoding of the message (using the standard +alphabet, with "=" padding). This stream is broken into +whitespace-separated chunks, which are then bundled into HTML
+elements. The 
 elements are then surrounded by AMP boilerplate. To
+decode, search the HTML for 
 elements, concatenate their contents
+and join on whitespace, discard the "0" prefix, and base64 decode.
 
 2.2 Proxy interactions with the broker
 

From b203a75c41df5fca3b4b8bd41e7c98d0360f1575 Mon Sep 17 00:00:00 2001
From: David Fifield 
Date: Mon, 26 Jul 2021 10:24:47 -0600
Subject: [PATCH 222/385] Document -ampcache in snowflake-client man page.

---
 doc/snowflake-client.1 | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/doc/snowflake-client.1 b/doc/snowflake-client.1
index 977fc70..122ada0 100644
--- a/doc/snowflake-client.1
+++ b/doc/snowflake-client.1
@@ -1,4 +1,4 @@
-.TH SNOWFLAKE-CLIENT "1" "June 2021" "snowflake-client" "User Commands"
+.TH SNOWFLAKE-CLIENT "1" "July 2021" "snowflake-client" "User Commands"
 .SH NAME
 snowflake-client \- WebRTC pluggable transport client for Tor
 .SH DESCRIPTION
@@ -7,6 +7,10 @@ connection to volunteer proxies. These proxies relay Tor traffic to a
 Snowflake bridge and then through the Tor network.
 .SS "Usage of snowflake-client:"
 .HP
+\fB\-ampcache\fR string
+.IP
+URL of AMP cache to use as a proxy for signaling
+.HP
 \fB\-front\fR string
 .IP
 front domain

From e6715cb4ee3e577c83bb4edc40fcc5018ac70bb7 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Wed, 14 Jul 2021 14:42:17 -0400
Subject: [PATCH 223/385] Increase smux and QueuePacketConn buffer sizes

This should increase the maximum amount of inflight data and hopefully
the performance of Snowflake, especially for clients geographically
distant from proxies and the server.
---
 client/lib/snowflake.go      | 7 ++++++-
 common/turbotunnel/consts.go | 2 +-
 server/lib/snowflake.go      | 8 +++++++-
 3 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 0fc7671..1987cbc 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -21,6 +21,9 @@ const (
 	SnowflakeTimeout = 20 * time.Second
 	// How long to wait for the OnOpen callback on a DataChannel.
 	DataChannelTimeout = 10 * time.Second
+
+	WindowSize = 65535
+	StreamSize = 1048576 //1MB
 )
 
 type dummyAddr struct{}
@@ -224,7 +227,7 @@ func newSession(snowflakes SnowflakeCollector) (net.PacketConn, *smux.Session, e
 	conn.SetStreamMode(true)
 	// Set the maximum send and receive window sizes to a high number
 	// Removes KCP bottlenecks: https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40026
-	conn.SetWindowSize(65535, 65535)
+	conn.SetWindowSize(WindowSize, WindowSize)
 	// Disable the dynamic congestion window (limit only by the
 	// maximum of local and remote static windows).
 	conn.SetNoDelay(
@@ -237,6 +240,8 @@ func newSession(snowflakes SnowflakeCollector) (net.PacketConn, *smux.Session, e
 	smuxConfig := smux.DefaultConfig()
 	smuxConfig.Version = 2
 	smuxConfig.KeepAliveTimeout = 10 * time.Minute
+	smuxConfig.MaxStreamBuffer = StreamSize
+
 	sess, err := smux.Client(conn, smuxConfig)
 	if err != nil {
 		conn.Close()
diff --git a/common/turbotunnel/consts.go b/common/turbotunnel/consts.go
index 80f70af..34c474f 100644
--- a/common/turbotunnel/consts.go
+++ b/common/turbotunnel/consts.go
@@ -11,7 +11,7 @@ import "errors"
 var Token = [8]byte{0x12, 0x93, 0x60, 0x5d, 0x27, 0x81, 0x75, 0xf5}
 
 // The size of receive and send queues.
-const queueSize = 32
+const queueSize = 2048
 
 var errClosedPacketConn = errors.New("operation on closed connection")
 var errNotImplemented = errors.New("not implemented")
diff --git a/server/lib/snowflake.go b/server/lib/snowflake.go
index 48c6d9e..aa1872f 100644
--- a/server/lib/snowflake.go
+++ b/server/lib/snowflake.go
@@ -16,6 +16,11 @@ import (
 	"golang.org/x/net/http2"
 )
 
+const (
+	WindowSize = 65535
+	StreamSize = 1048576 //1MB
+)
+
 // Transport is a structure with methods that conform to the Go PT v2.1 API
 // https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/master/releases/PTSpecV2.1/Pluggable%20Transport%20Specification%20v2.1%20-%20Go%20Transport%20API.pdf
 type Transport struct {
@@ -168,6 +173,7 @@ func (l *SnowflakeListener) acceptStreams(conn *kcp.UDPSession) error {
 	smuxConfig := smux.DefaultConfig()
 	smuxConfig.Version = 2
 	smuxConfig.KeepAliveTimeout = 10 * time.Minute
+	smuxConfig.MaxStreamBuffer = StreamSize
 	sess, err := smux.Server(conn, smuxConfig)
 	if err != nil {
 		return err
@@ -201,7 +207,7 @@ func (l *SnowflakeListener) acceptSessions(ln *kcp.Listener) error {
 		conn.SetStreamMode(true)
 		// Set the maximum send and receive window sizes to a high number
 		// Removes KCP bottlenecks: https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40026
-		conn.SetWindowSize(65535, 65535)
+		conn.SetWindowSize(WindowSize, WindowSize)
 		// Disable the dynamic congestion window (limit only by the
 		// maximum of local and remote static windows).
 		conn.SetNoDelay(

From 4acc08cc60d46ba1ffce9b4492b974eff385e46b Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Fri, 13 Aug 2021 10:23:46 -0400
Subject: [PATCH 224/385] Use a config struct for snowflake client options

---
 client/lib/snowflake.go | 20 ++++++++++++++------
 client/snowflake.go     | 11 +++++++++--
 2 files changed, 23 insertions(+), 8 deletions(-)

diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 1987cbc..8b01d88 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -37,17 +37,25 @@ type Transport struct {
 	dialer *WebRTCDialer
 }
 
+type ClientConfig struct {
+	BrokerURL          string
+	AmpCacheURL        string
+	FrontDomain        string
+	ICEAddresses       []string
+	KeepLocalAddresses bool
+	Max                int
+}
+
 // Create a new Snowflake transport client that can spawn multiple Snowflake connections.
 // brokerURL and frontDomain are the urls for the broker host and domain fronting host
 // iceAddresses are the STUN/TURN urls needed for WebRTC negotiation
 // keepLocalAddresses is a flag to enable sending local network addresses (for testing purposes)
 // max is the maximum number of snowflakes the client should gather for each SOCKS connection
-func NewSnowflakeClient(brokerURL, ampCacheURL, frontDomain string,
-	iceAddresses []string, keepLocalAddresses bool, max int) (*Transport, error) {
+func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
 
 	log.Println("\n\n\n --- Starting Snowflake Client ---")
 
-	iceServers := parseIceServers(iceAddresses)
+	iceServers := parseIceServers(config.ICEAddresses)
 	// chooses a random subset of servers from inputs
 	rand.Seed(time.Now().UnixNano())
 	rand.Shuffle(len(iceServers), func(i, j int) {
@@ -63,14 +71,14 @@ func NewSnowflakeClient(brokerURL, ampCacheURL, frontDomain string,
 
 	// Rendezvous with broker using the given parameters.
 	broker, err := NewBrokerChannel(
-		brokerURL, ampCacheURL, frontDomain, CreateBrokerTransport(),
-		keepLocalAddresses)
+		config.BrokerURL, config.AmpCacheURL, config.FrontDomain, CreateBrokerTransport(),
+		config.KeepLocalAddresses)
 	if err != nil {
 		return nil, err
 	}
 	go updateNATType(iceServers, broker)
 
-	transport := &Transport{dialer: NewWebRTCDialer(broker, iceServers, max)}
+	transport := &Transport{dialer: NewWebRTCDialer(broker, iceServers, config.Max)}
 
 	return transport, nil
 }
diff --git a/client/snowflake.go b/client/snowflake.go
index ef06a2d..04ebf48 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -141,8 +141,15 @@ func main() {
 
 	iceAddresses := strings.Split(strings.TrimSpace(*iceServersCommas), ",")
 
-	transport, err := sf.NewSnowflakeClient(*brokerURL, *ampCacheURL, *frontDomain, iceAddresses,
-		*keepLocalAddresses || *oldKeepLocalAddresses, *max)
+	config := sf.ClientConfig{
+		BrokerURL:          *brokerURL,
+		AmpCacheURL:        *ampCacheURL,
+		FrontDomain:        *frontDomain,
+		ICEAddresses:       iceAddresses,
+		KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses,
+		Max:                *max,
+	}
+	transport, err := sf.NewSnowflakeClient(config)
 	if err != nil {
 		log.Fatal("Failed to start snowflake transport: ", err)
 	}

From e762f58a31de9167933fbd75047a48d2e1cdeb36 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Fri, 13 Aug 2021 10:55:52 -0400
Subject: [PATCH 225/385] Parse SOCKS arguments and prefer over command line
 options

Parsing the Snowflake client options from SOCKS allow us to specify
snowflake client settings in the bridge lines.
---
 client/snowflake.go | 33 +++++++++++++++++++++++++++------
 1 file changed, 27 insertions(+), 6 deletions(-)

diff --git a/client/snowflake.go b/client/snowflake.go
index 04ebf48..d6bad0e 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -10,6 +10,7 @@ import (
 	"os"
 	"os/signal"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"sync"
 	"syscall"
@@ -44,7 +45,7 @@ func copyLoop(socks, sfconn io.ReadWriter) {
 }
 
 // Accept local SOCKS connections and connect to a Snowflake connection
-func socksAcceptLoop(ln *pt.SocksListener, transport *sf.Transport, shutdown chan struct{}, wg *sync.WaitGroup) {
+func socksAcceptLoop(ln *pt.SocksListener, config sf.ClientConfig, shutdown chan struct{}, wg *sync.WaitGroup) {
 	defer ln.Close()
 	for {
 		conn, err := ln.AcceptSocks()
@@ -67,6 +68,30 @@ func socksAcceptLoop(ln *pt.SocksListener, transport *sf.Transport, shutdown cha
 				return
 			}
 
+			// Check to see if our command line options are overriden by SOCKS options
+			if arg, ok := conn.Req.Args.Get("ampcache"); ok {
+				config.AmpCacheURL = arg
+			}
+			if arg, ok := conn.Req.Args.Get("front"); ok {
+				config.FrontDomain = arg
+			}
+			if arg, ok := conn.Req.Args.Get("ice"); ok {
+				config.ICEAddresses = strings.Split(strings.TrimSpace(arg), ",")
+			}
+			if arg, ok := conn.Req.Args.Get("max"); ok {
+				max, err := strconv.Atoi(arg)
+				if err == nil {
+					config.Max = max
+				}
+			}
+			if arg, ok := conn.Req.Args.Get("url"); ok {
+				config.BrokerURL = arg
+			}
+			transport, err := sf.NewSnowflakeClient(config)
+			if err != nil {
+				log.Fatal("Failed to start snowflake transport: ", err)
+			}
+
 			handler := make(chan struct{})
 			go func() {
 				defer close(handler)
@@ -149,10 +174,6 @@ func main() {
 		KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses,
 		Max:                *max,
 	}
-	transport, err := sf.NewSnowflakeClient(config)
-	if err != nil {
-		log.Fatal("Failed to start snowflake transport: ", err)
-	}
 
 	// Begin goptlib client process.
 	ptInfo, err := pt.ClientSetup(nil)
@@ -176,7 +197,7 @@ func main() {
 				break
 			}
 			log.Printf("Started SOCKS listener at %v.", ln.Addr())
-			go socksAcceptLoop(ln, transport, shutdown, &wg)
+			go socksAcceptLoop(ln, config, shutdown, &wg)
 			pt.Cmethod(methodName, ln.Version(), ln.Addr())
 			listeners = append(listeners, ln)
 		default:

From 97175a91a59216d7eddade4be5c92c82bd225f86 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Fri, 13 Aug 2021 12:29:48 -0400
Subject: [PATCH 226/385] Modify torrc example to pass client args in bridge
 line

---
 client/README.md       | 2 +-
 client/torrc           | 8 +++-----
 client/torrc-localhost | 8 --------
 client/torrc.localhost | 6 ++++++
 4 files changed, 10 insertions(+), 14 deletions(-)
 delete mode 100644 client/torrc-localhost
 create mode 100644 client/torrc.localhost

diff --git a/client/README.md b/client/README.md
index 0680408..c6f4bda 100644
--- a/client/README.md
+++ b/client/README.md
@@ -29,7 +29,7 @@ go build
 
 ### Running the Snowflake client with Tor
 
-We have an example `torrc` file in this repository. The client uses these following `torrc` options by default:
+The Snowflake client can be configured with either command line options or SOCKS options. We have a few example `torrc` files in this directory. We recommend the following `torrc` options by default:
 ```
 UseBridges 1
 
diff --git a/client/torrc b/client/torrc
index 1328adc..039653f 100644
--- a/client/torrc
+++ b/client/torrc
@@ -1,10 +1,8 @@
 UseBridges 1
 DataDirectory datadir
 
-ClientTransportPlugin snowflake exec ./client \
--url https://snowflake-broker.torproject.net.global.prod.fastly.net/ \
--front cdn.sstatic.net \
--ice stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478
+ClientTransportPlugin snowflake exec ./client -log snowflake.log
+
+Bridge snowflake 192.0.2.3:1 url=https://snowflake-broker.torproject.net.global.prod.fastly.net/ front=cdn.sstatic.net ice=stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478
 
-Bridge snowflake 192.0.2.3:1
 SocksPort auto
diff --git a/client/torrc-localhost b/client/torrc-localhost
deleted file mode 100644
index b2a6d05..0000000
--- a/client/torrc-localhost
+++ /dev/null
@@ -1,8 +0,0 @@
-UseBridges 1
-DataDirectory datadir
-
-ClientTransportPlugin snowflake exec ./client \
--url http://localhost:8080/ \
--keep-local-addresses
-
-Bridge snowflake 192.0.2.3:1
diff --git a/client/torrc.localhost b/client/torrc.localhost
new file mode 100644
index 0000000..e09f94c
--- /dev/null
+++ b/client/torrc.localhost
@@ -0,0 +1,6 @@
+UseBridges 1
+DataDirectory datadir
+
+ClientTransportPlugin snowflake exec ./client -keep-local-addresses
+
+Bridge snowflake 192.0.2.3:1 url=http://localhost:8080/

From a39d6693e15f8a839014f288a749fcc4180b71ea Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 19 Aug 2021 21:31:51 -0400
Subject: [PATCH 227/385] Call conn.Reject() if SOCKS arguments are invalid

---
 client/snowflake.go | 22 +++++++++++++---------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/client/snowflake.go b/client/snowflake.go
index d6bad0e..4ed4fd6 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -62,12 +62,6 @@ func socksAcceptLoop(ln *pt.SocksListener, config sf.ClientConfig, shutdown chan
 			defer wg.Done()
 			defer conn.Close()
 
-			err := conn.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0})
-			if err != nil {
-				log.Printf("conn.Grant error: %s", err)
-				return
-			}
-
 			// Check to see if our command line options are overriden by SOCKS options
 			if arg, ok := conn.Req.Args.Get("ampcache"); ok {
 				config.AmpCacheURL = arg
@@ -80,16 +74,26 @@ func socksAcceptLoop(ln *pt.SocksListener, config sf.ClientConfig, shutdown chan
 			}
 			if arg, ok := conn.Req.Args.Get("max"); ok {
 				max, err := strconv.Atoi(arg)
-				if err == nil {
-					config.Max = max
+				if err != nil {
+					conn.Reject()
+					log.Println("Invalid SOCKS arg: max=", arg)
+					return
 				}
+				config.Max = max
 			}
 			if arg, ok := conn.Req.Args.Get("url"); ok {
 				config.BrokerURL = arg
 			}
 			transport, err := sf.NewSnowflakeClient(config)
 			if err != nil {
-				log.Fatal("Failed to start snowflake transport: ", err)
+				conn.Reject()
+				log.Println("Failed to start snowflake transport: ", err)
+				return
+			}
+			err := conn.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0})
+			if err != nil {
+				log.Printf("conn.Grant error: %s", err)
+				return
 			}
 
 			handler := make(chan struct{})

From ace8df37ed39d77f9db97ead6e9ebac5a8148285 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Tue, 24 Aug 2021 10:27:24 -0400
Subject: [PATCH 228/385] Fix compile bug in client, caught by CI

---
 client/snowflake.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/snowflake.go b/client/snowflake.go
index 4ed4fd6..d952275 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -90,7 +90,7 @@ func socksAcceptLoop(ln *pt.SocksListener, config sf.ClientConfig, shutdown chan
 				log.Println("Failed to start snowflake transport: ", err)
 				return
 			}
-			err := conn.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0})
+			err = conn.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0})
 			if err != nil {
 				log.Printf("conn.Grant error: %s", err)
 				return

From cbd863d6b1c7dfcb86321452782aba29ccce8b5d Mon Sep 17 00:00:00 2001
From: meskio 
Date: Thu, 2 Sep 2021 12:01:15 +0200
Subject: [PATCH 229/385] Fix proxy test

The broker is a global object.
---
 proxy/proxy-go_test.go | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/proxy/proxy-go_test.go b/proxy/proxy-go_test.go
index 183b1b4..6fb5a0b9 100644
--- a/proxy/proxy-go_test.go
+++ b/proxy/proxy-go_test.go
@@ -336,7 +336,8 @@ func TestBrokerInteractions(t *testing.T) {
 	const sampleAnswer = `{"type":"answer","sdp":` + sampleSDP + `}`
 
 	Convey("Proxy connections to broker", t, func() {
-		broker, err := newSignalingServer("localhost", false)
+		var err error
+		broker, err = newSignalingServer("localhost", false)
 		So(err, ShouldEqual, nil)
 		tokens = newTokens(0)
 

From c8136f4534003cc53d95480fbf0151a21859d11b Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Fri, 10 Sep 2021 10:15:15 -0400
Subject: [PATCH 230/385] Update version of go used in .gitlab-ci.yml

---
 .gitlab-ci.yml | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e1a391c..497462f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -80,12 +80,12 @@ android:
         gnupg
         wget
     - cd /usr/local
-    - export gotarball="go1.15.10.linux-amd64.tar.gz"
+    - export gotarball="go1.16.8.linux-amd64.tar.gz"
     - wget -q https://dl.google.com/go/${gotarball}
     - wget -q https://dl.google.com/go/${gotarball}.asc
     - curl https://dl.google.com/linux/linux_signing_key.pub | gpg --import
     - gpg --verify ${gotarball}.asc
-    - echo "4aa1267517df32f2bf1cc3d55dfc27d0c6b2c2b0989449c96dd19273ccca051d  ${gotarball}" | sha256sum -c
+    - echo "f32501aeb8b7b723bc7215f6c373abb6981bbc7e1c7b44e9f07317e1a300dce2  ${gotarball}" | sha256sum -c
     - tar -xzf ${gotarball}
     - export PATH="/usr/local/go/bin:$GOPATH/bin:$PATH"  # putting this in 'variables:' cause weird runner errors
     - cd $CI_PROJECT_DIR
@@ -97,8 +97,8 @@ android:
 
     - go get golang.org/x/mobile/cmd/gomobile
     - go get golang.org/x/mobile/cmd/gobind
-    - go install golang.org/x/mobile/cmd/gomobile
     - go install golang.org/x/mobile/cmd/gobind
+    - go install golang.org/x/mobile/cmd/gomobile
     - echo y | $ANDROID_HOME/tools/bin/sdkmanager 'ndk-bundle' > /dev/null
     - echo y | $ANDROID_HOME/tools/bin/sdkmanager "platforms;android-${ANDROID_VERSION}" > /dev/null
     - gomobile init
@@ -108,6 +108,7 @@ android:
     - cd $CI_PROJECT_DIR/client
     # gomobile builds a shared library not a CLI executable
     - sed -i 's,^package main$,package snowflakeclient,' snowflake.go
+    - go get golang.org/x/mobile/bind
     - gomobile bind -v -target=android .
   <<: *test-template
 

From 8c6f0dbae714908094b828e6f31e9729a6daafe5 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 26 Aug 2021 12:41:20 -0400
Subject: [PATCH 231/385] Check error for calls to preparePeerConnection

---
 client/lib/webrtc.go | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index 72a3d64..2f931ac 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -127,7 +127,9 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel
 	log.Println(c.id, " connecting...")
 	// TODO: When go-webrtc is more stable, it's possible that a new
 	// PeerConnection won't need to be re-prepared each time.
-	c.preparePeerConnection(config)
+	if err := c.preparePeerConnection(config); err != nil {
+		return err
+	}
 	answer, err := broker.Negotiate(c.pc.LocalDescription())
 	if err != nil {
 		return err

From 4396d505a3b872fda43ca6cf43264d0f25cd8e9f Mon Sep 17 00:00:00 2001
From: meskio 
Date: Thu, 30 Sep 2021 12:10:59 +0200
Subject: [PATCH 232/385] Use tpo geoip library

Now the geoip implmentation has being moved to it's own library to be
shared between projects.
---
 broker/geoip.go                 | 240 --------------------------------
 broker/metrics.go               |  38 ++---
 broker/snowflake-broker_test.go |  96 +------------
 go.mod                          |   1 +
 go.sum                          |   2 +
 5 files changed, 13 insertions(+), 364 deletions(-)
 delete mode 100644 broker/geoip.go

diff --git a/broker/geoip.go b/broker/geoip.go
deleted file mode 100644
index 708cdad..0000000
--- a/broker/geoip.go
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
-This code is for loading database data that maps ip addresses to countries
-for collecting and presenting statistics on snowflake use that might alert us
-to censorship events.
-
-The functions here are heavily based off of how tor maintains and searches their
-geoip database
-
-The tables used for geoip data must be structured as follows:
-
-Recognized line format for IPv4 is:
-    INTIPLOW,INTIPHIGH,CC
-        where INTIPLOW and INTIPHIGH are IPv4 addresses encoded as big-endian 4-byte unsigned
-        integers, and CC is a country code.
-
-Note that the IPv4 line format
-    "INTIPLOW","INTIPHIGH","CC","CC3","COUNTRY NAME"
-is not currently supported.
-
-Recognized line format for IPv6 is:
-    IPV6LOW,IPV6HIGH,CC
-        where IPV6LOW and IPV6HIGH are IPv6 addresses and CC is a country code.
-
-It also recognizes, and skips over, blank lines and lines that start
-with '#' (comments).
-
-*/
-package main
-
-import (
-	"bufio"
-	"bytes"
-	"crypto/sha1"
-	"encoding/hex"
-	"fmt"
-	"io"
-	"log"
-	"net"
-	"os"
-	"sort"
-	"strconv"
-	"strings"
-	"sync"
-)
-
-type GeoIPTable interface {
-	parseEntry(string) (*GeoIPEntry, error)
-	Len() int
-	Append(GeoIPEntry)
-	ElementAt(int) GeoIPEntry
-	Lock()
-	Unlock()
-}
-
-type GeoIPEntry struct {
-	ipLow   net.IP
-	ipHigh  net.IP
-	country string
-}
-
-type GeoIPv4Table struct {
-	table []GeoIPEntry
-
-	lock sync.Mutex // synchronization for geoip table accesses and reloads
-}
-
-type GeoIPv6Table struct {
-	table []GeoIPEntry
-
-	lock sync.Mutex // synchronization for geoip table accesses and reloads
-}
-
-func (table *GeoIPv4Table) Len() int { return len(table.table) }
-func (table *GeoIPv6Table) Len() int { return len(table.table) }
-
-func (table *GeoIPv4Table) Append(entry GeoIPEntry) {
-	(*table).table = append(table.table, entry)
-}
-func (table *GeoIPv6Table) Append(entry GeoIPEntry) {
-	(*table).table = append(table.table, entry)
-}
-
-func (table *GeoIPv4Table) ElementAt(i int) GeoIPEntry { return table.table[i] }
-func (table *GeoIPv6Table) ElementAt(i int) GeoIPEntry { return table.table[i] }
-
-func (table *GeoIPv4Table) Lock() { (*table).lock.Lock() }
-func (table *GeoIPv6Table) Lock() { (*table).lock.Lock() }
-
-func (table *GeoIPv4Table) Unlock() { (*table).lock.Unlock() }
-func (table *GeoIPv6Table) Unlock() { (*table).lock.Unlock() }
-
-// Convert a geoip IP address represented as a big-endian unsigned integer to net.IP
-func geoipStringToIP(ipStr string) (net.IP, error) {
-	ip, err := strconv.ParseUint(ipStr, 10, 32)
-	if err != nil {
-		return net.IPv4(0, 0, 0, 0), fmt.Errorf("error parsing IP %s", ipStr)
-	}
-	var bytes [4]byte
-	bytes[0] = byte(ip & 0xFF)
-	bytes[1] = byte((ip >> 8) & 0xFF)
-	bytes[2] = byte((ip >> 16) & 0xFF)
-	bytes[3] = byte((ip >> 24) & 0xFF)
-
-	return net.IPv4(bytes[3], bytes[2], bytes[1], bytes[0]), nil
-}
-
-//Parses a line in the provided geoip file that corresponds
-//to an address range and a two character country code
-func (table *GeoIPv4Table) parseEntry(candidate string) (*GeoIPEntry, error) {
-
-	if candidate[0] == '#' {
-		return nil, nil
-	}
-
-	parsedCandidate := strings.Split(candidate, ",")
-
-	if len(parsedCandidate) != 3 {
-		return nil, fmt.Errorf("provided geoip file is incorrectly formatted. Could not parse line:\n%s", parsedCandidate)
-	}
-
-	low, err := geoipStringToIP(parsedCandidate[0])
-	if err != nil {
-		return nil, err
-	}
-	high, err := geoipStringToIP(parsedCandidate[1])
-	if err != nil {
-		return nil, err
-	}
-
-	geoipEntry := &GeoIPEntry{
-		ipLow:   low,
-		ipHigh:  high,
-		country: parsedCandidate[2],
-	}
-
-	return geoipEntry, nil
-}
-
-//Parses a line in the provided geoip file that corresponds
-//to an address range and a two character country code
-func (table *GeoIPv6Table) parseEntry(candidate string) (*GeoIPEntry, error) {
-
-	if candidate[0] == '#' {
-		return nil, nil
-	}
-
-	parsedCandidate := strings.Split(candidate, ",")
-
-	if len(parsedCandidate) != 3 {
-		return nil, fmt.Errorf("")
-	}
-
-	low := net.ParseIP(parsedCandidate[0])
-	if low == nil {
-		return nil, fmt.Errorf("")
-	}
-	high := net.ParseIP(parsedCandidate[1])
-	if high == nil {
-		return nil, fmt.Errorf("")
-	}
-
-	geoipEntry := &GeoIPEntry{
-		ipLow:   low,
-		ipHigh:  high,
-		country: parsedCandidate[2],
-	}
-
-	return geoipEntry, nil
-}
-
-//Loads provided geoip file into our tables
-//Entries are stored in a table
-func GeoIPLoadFile(table GeoIPTable, pathname string) error {
-	//open file
-	geoipFile, err := os.Open(pathname)
-	if err != nil {
-		return err
-	}
-	defer geoipFile.Close()
-
-	hash := sha1.New()
-
-	table.Lock()
-	defer table.Unlock()
-
-	hashedFile := io.TeeReader(geoipFile, hash)
-
-	//read in strings and call parse function
-	scanner := bufio.NewScanner(hashedFile)
-	for scanner.Scan() {
-		entry, err := table.parseEntry(scanner.Text())
-		if err != nil {
-			return fmt.Errorf("provided geoip file is incorrectly formatted. Line is: %+q", scanner.Text())
-		}
-
-		if entry != nil {
-			table.Append(*entry)
-		}
-
-	}
-	if err := scanner.Err(); err != nil {
-		return err
-	}
-
-	sha1Hash := hex.EncodeToString(hash.Sum(nil))
-	log.Println("Using geoip file ", pathname, " with checksum", sha1Hash)
-	log.Println("Loaded ", table.Len(), " entries into table")
-
-	return nil
-}
-
-//Returns the country location of an IPv4 or IPv6 address, and a boolean value
-//that indicates whether the IP address was present in the geoip database
-func GetCountryByAddr(table GeoIPTable, ip net.IP) (string, bool) {
-
-	table.Lock()
-	defer table.Unlock()
-
-	//look IP up in database
-	index := sort.Search(table.Len(), func(i int) bool {
-		entry := table.ElementAt(i)
-		return (bytes.Compare(ip.To16(), entry.ipHigh.To16()) <= 0)
-	})
-
-	if index == table.Len() {
-		return "", false
-	}
-
-	// check to see if addr is in the range specified by the returned index
-	// search on IPs in invalid ranges (e.g., 127.0.0.0/8) will return the
-	//country code of the next highest range
-	entry := table.ElementAt(index)
-	if !(bytes.Compare(ip.To16(), entry.ipLow.To16()) >= 0 &&
-		bytes.Compare(ip.To16(), entry.ipHigh.To16()) <= 0) {
-		return "", false
-	}
-
-	return table.ElementAt(index).country, true
-
-}
diff --git a/broker/metrics.go b/broker/metrics.go
index e8a6b0c..8229e0f 100644
--- a/broker/metrics.go
+++ b/broker/metrics.go
@@ -15,6 +15,7 @@ import (
 	"time"
 
 	"github.com/prometheus/client_golang/prometheus"
+	"gitlab.torproject.org/tpo/anti-censorship/geoip"
 )
 
 const (
@@ -38,8 +39,7 @@ type CountryStats struct {
 // Implements Observable
 type Metrics struct {
 	logger  *log.Logger
-	tablev4 *GeoIPv4Table
-	tablev6 *GeoIPv6Table
+	geoipdb *geoip.Geoip
 
 	countryStats                  CountryStats
 	clientRoundtripEstimate       time.Duration
@@ -115,19 +115,10 @@ func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType stri
 	}
 
 	ip := net.ParseIP(addr)
-	if ip.To4() != nil {
-		//This is an IPv4 address
-		if m.tablev4 == nil {
-			return
-		}
-		country, ok = GetCountryByAddr(m.tablev4, ip)
-	} else {
-		if m.tablev6 == nil {
-			return
-		}
-		country, ok = GetCountryByAddr(m.tablev6, ip)
+	if m.geoipdb == nil {
+		return
 	}
-
+	country, ok = m.geoipdb.GetCountryByAddr(ip)
 	if !ok {
 		country = "??"
 	}
@@ -164,23 +155,10 @@ func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType stri
 func (m *Metrics) LoadGeoipDatabases(geoipDB string, geoip6DB string) error {
 
 	// Load geoip databases
+	var err error
 	log.Println("Loading geoip databases")
-	tablev4 := new(GeoIPv4Table)
-	err := GeoIPLoadFile(tablev4, geoipDB)
-	if err != nil {
-		m.tablev4 = nil
-		return err
-	}
-	m.tablev4 = tablev4
-
-	tablev6 := new(GeoIPv6Table)
-	err = GeoIPLoadFile(tablev6, geoip6DB)
-	if err != nil {
-		m.tablev6 = nil
-		return err
-	}
-	m.tablev6 = tablev6
-	return nil
+	m.geoipdb, err = geoip.New(geoipDB, geoip6DB)
+	return err
 }
 
 func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) {
diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go
index 233cfea..25a947c 100644
--- a/broker/snowflake-broker_test.go
+++ b/broker/snowflake-broker_test.go
@@ -6,7 +6,6 @@ import (
 	"io"
 	"io/ioutil"
 	"log"
-	"net"
 	"net/http"
 	"net/http/httptest"
 	"os"
@@ -473,106 +472,15 @@ func TestSnowflakeHeap(t *testing.T) {
 	})
 }
 
-func TestGeoip(t *testing.T) {
+func TestInvalidGeoipFile(t *testing.T) {
 	Convey("Geoip", t, func() {
-		tv4 := new(GeoIPv4Table)
-		err := GeoIPLoadFile(tv4, "test_geoip")
-		So(err, ShouldEqual, nil)
-		tv6 := new(GeoIPv6Table)
-		err = GeoIPLoadFile(tv6, "test_geoip6")
-		So(err, ShouldEqual, nil)
-
-		Convey("IPv4 Country Mapping Tests", func() {
-			for _, test := range []struct {
-				addr, cc string
-				ok       bool
-			}{
-				{
-					"129.97.208.23", //uwaterloo
-					"CA",
-					true,
-				},
-				{
-					"127.0.0.1",
-					"",
-					false,
-				},
-				{
-					"255.255.255.255",
-					"",
-					false,
-				},
-				{
-					"0.0.0.0",
-					"",
-					false,
-				},
-				{
-					"223.252.127.255", //test high end of range
-					"JP",
-					true,
-				},
-				{
-					"223.252.127.255", //test low end of range
-					"JP",
-					true,
-				},
-			} {
-				country, ok := GetCountryByAddr(tv4, net.ParseIP(test.addr))
-				So(country, ShouldEqual, test.cc)
-				So(ok, ShouldResemble, test.ok)
-			}
-		})
-
-		Convey("IPv6 Country Mapping Tests", func() {
-			for _, test := range []struct {
-				addr, cc string
-				ok       bool
-			}{
-				{
-					"2620:101:f000:0:250:56ff:fe80:168e", //uwaterloo
-					"CA",
-					true,
-				},
-				{
-					"fd00:0:0:0:0:0:0:1",
-					"",
-					false,
-				},
-				{
-					"0:0:0:0:0:0:0:0",
-					"",
-					false,
-				},
-				{
-					"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
-					"",
-					false,
-				},
-				{
-					"2a07:2e47:ffff:ffff:ffff:ffff:ffff:ffff", //test high end of range
-					"FR",
-					true,
-				},
-				{
-					"2a07:2e40::", //test low end of range
-					"FR",
-					true,
-				},
-			} {
-				country, ok := GetCountryByAddr(tv6, net.ParseIP(test.addr))
-				So(country, ShouldEqual, test.cc)
-				So(ok, ShouldResemble, test.ok)
-			}
-		})
-
 		// Make sure things behave properly if geoip file fails to load
 		ctx := NewBrokerContext(NullLogger())
 		if err := ctx.metrics.LoadGeoipDatabases("invalid_filename", "invalid_filename6"); err != nil {
 			log.Printf("loading geo ip databases returned error: %v", err)
 		}
 		ctx.metrics.UpdateCountryStats("127.0.0.1", "", NATUnrestricted)
-		So(ctx.metrics.tablev4, ShouldEqual, nil)
+		So(ctx.metrics.geoipdb, ShouldEqual, nil)
 
 	})
 }
diff --git a/go.mod b/go.mod
index 36585aa..9d6b6ac 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ require (
 	github.com/smartystreets/goconvey v1.6.4
 	github.com/xtaci/kcp-go/v5 v5.6.1
 	github.com/xtaci/smux v1.5.15
+	gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01
 	golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
 	golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4
 	golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e // indirect
diff --git a/go.sum b/go.sum
index f0b3927..34bc936 100644
--- a/go.sum
+++ b/go.sum
@@ -358,6 +358,8 @@ github.com/xtaci/smux v1.5.15 h1:6hMiXswcleXj5oNfcJc+DXS8Vj36XX2LaX98udog6Kc=
 github.com/xtaci/smux v1.5.15/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01 h1:4949mHh9Vj2/okk48yG8nhP6TosFWOUfSfSr502sKGE=
+gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01/go.mod h1:K3LOI4H8fa6j+7E10ViHeGEQV10304FG4j94ypmKLjY=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=

From 624750d5a8a0b0acedd495168bcb2b5fc627fcb8 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 9 Sep 2021 11:34:07 -0400
Subject: [PATCH 233/385] Stop exporting code that should be internal

---
 client/lib/interfaces.go  | 11 -----------
 client/lib/peers.go       |  4 ++--
 client/lib/rendezvous.go  | 16 ++++++++--------
 client/lib/snowflake.go   |  9 ++++-----
 client/lib/turbotunnel.go | 22 +++++++++++-----------
 client/lib/util.go        | 30 +++++++++++++++---------------
 client/lib/webrtc.go      |  8 ++++----
 7 files changed, 44 insertions(+), 56 deletions(-)

diff --git a/client/lib/interfaces.go b/client/lib/interfaces.go
index 5378f4a..0a871b8 100644
--- a/client/lib/interfaces.go
+++ b/client/lib/interfaces.go
@@ -1,9 +1,5 @@
 package lib
 
-import (
-	"net"
-)
-
 // Interface for catching Snowflakes. (aka the remote dialer)
 type Tongue interface {
 	Catch() (*WebRTCPeer, error)
@@ -25,10 +21,3 @@ type SnowflakeCollector interface {
 	// Signal when the collector has stopped collecting.
 	Melted() <-chan struct{}
 }
-
-// Interface to adapt to goptlib's SocksConn struct.
-type SocksConnector interface {
-	Grant(*net.TCPAddr) error
-	Reject() error
-	net.Conn
-}
diff --git a/client/lib/peers.go b/client/lib/peers.go
index 7fba572..1abcd95 100644
--- a/client/lib/peers.go
+++ b/client/lib/peers.go
@@ -21,7 +21,7 @@ import (
 // version of Snowflake)
 type Peers struct {
 	Tongue
-	BytesLogger BytesLogger
+	bytesLogger bytesLogger
 
 	snowflakeChan chan *WebRTCPeer
 	activePeers   *list.List
@@ -88,7 +88,7 @@ func (p *Peers) Pop() *WebRTCPeer {
 			continue
 		}
 		// Set to use the same rate-limited traffic logger to keep consistency.
-		snowflake.BytesLogger = p.BytesLogger
+		snowflake.bytesLogger = p.bytesLogger
 		return snowflake
 	}
 }
diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index 8af638f..d58e729 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -42,14 +42,14 @@ type rendezvousMethod interface {
 type BrokerChannel struct {
 	rendezvous         rendezvousMethod
 	keepLocalAddresses bool
-	NATType            string
+	natType            string
 	lock               sync.Mutex
 }
 
 // We make a copy of DefaultTransport because we want the default Dial
 // and TLSHandshakeTimeout settings. But we want to disable the default
 // ProxyFromEnvironment setting.
-func CreateBrokerTransport() http.RoundTripper {
+func createBrokerTransport() http.RoundTripper {
 	transport := http.DefaultTransport.(*http.Transport)
 	transport.Proxy = nil
 	transport.ResponseHeaderTimeout = 15 * time.Second
@@ -59,7 +59,7 @@ func CreateBrokerTransport() http.RoundTripper {
 // Construct a new BrokerChannel, where:
 // |broker| is the full URL of the facilitating program which assigns proxies
 // to clients, and |front| is the option fronting domain.
-func NewBrokerChannel(broker, ampCache, front string, transport http.RoundTripper, keepLocalAddresses bool) (*BrokerChannel, error) {
+func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (*BrokerChannel, error) {
 	log.Println("Rendezvous using Broker at:", broker)
 	if ampCache != "" {
 		log.Println("Through AMP cache at:", ampCache)
@@ -71,9 +71,9 @@ func NewBrokerChannel(broker, ampCache, front string, transport http.RoundTrippe
 	var rendezvous rendezvousMethod
 	var err error
 	if ampCache != "" {
-		rendezvous, err = newAMPCacheRendezvous(broker, ampCache, front, transport)
+		rendezvous, err = newAMPCacheRendezvous(broker, ampCache, front, createBrokerTransport())
 	} else {
-		rendezvous, err = newHTTPRendezvous(broker, front, transport)
+		rendezvous, err = newHTTPRendezvous(broker, front, createBrokerTransport())
 	}
 	if err != nil {
 		return nil, err
@@ -82,7 +82,7 @@ func NewBrokerChannel(broker, ampCache, front string, transport http.RoundTrippe
 	return &BrokerChannel{
 		rendezvous:         rendezvous,
 		keepLocalAddresses: keepLocalAddresses,
-		NATType:            nat.NATUnknown,
+		natType:            nat.NATUnknown,
 	}, nil
 }
 
@@ -110,7 +110,7 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) (
 	bc.lock.Lock()
 	req := &messages.ClientPollRequest{
 		Offer: offerSDP,
-		NAT:   bc.NATType,
+		NAT:   bc.natType,
 	}
 	encReq, err := req.EncodePollRequest()
 	bc.lock.Unlock()
@@ -138,7 +138,7 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) (
 
 func (bc *BrokerChannel) SetNATType(NATType string) {
 	bc.lock.Lock()
-	bc.NATType = NATType
+	bc.natType = NATType
 	bc.lock.Unlock()
 	log.Printf("NAT Type: %s", NATType)
 }
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 8b01d88..fb7fab9 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -71,8 +71,7 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
 
 	// Rendezvous with broker using the given parameters.
 	broker, err := NewBrokerChannel(
-		config.BrokerURL, config.AmpCacheURL, config.FrontDomain, CreateBrokerTransport(),
-		config.KeepLocalAddresses)
+		config.BrokerURL, config.AmpCacheURL, config.FrontDomain, config.KeepLocalAddresses)
 	if err != nil {
 		return nil, err
 	}
@@ -103,7 +102,7 @@ func (t *Transport) Dial() (net.Conn, error) {
 	cleanup = append(cleanup, func() { snowflakes.End() })
 
 	// Use a real logger to periodically output how much traffic is happening.
-	snowflakes.BytesLogger = NewBytesSyncLogger()
+	snowflakes.bytesLogger = newBytesSyncLogger()
 
 	log.Printf("---- SnowflakeConn: begin collecting snowflakes ---")
 	go connectLoop(snowflakes)
@@ -198,7 +197,7 @@ func newSession(snowflakes SnowflakeCollector) (net.PacketConn, *smux.Session, e
 	// We build a persistent KCP session on a sequence of ephemeral WebRTC
 	// connections. This dialContext tells RedialPacketConn how to get a new
 	// WebRTC connection when the previous one dies. Inside each WebRTC
-	// connection, we use EncapsulationPacketConn to encode packets into a
+	// connection, we use encapsulationPacketConn to encode packets into a
 	// stream.
 	dialContext := func(ctx context.Context) (net.PacketConn, error) {
 		log.Printf("redialing on same connection")
@@ -218,7 +217,7 @@ func newSession(snowflakes SnowflakeCollector) (net.PacketConn, *smux.Session, e
 		if err != nil {
 			return nil, err
 		}
-		return NewEncapsulationPacketConn(dummyAddr{}, dummyAddr{}, conn), nil
+		return newEncapsulationPacketConn(dummyAddr{}, dummyAddr{}, conn), nil
 	}
 	pconn := turbotunnel.NewRedialPacketConn(dummyAddr{}, dummyAddr{}, dialContext)
 
diff --git a/client/lib/turbotunnel.go b/client/lib/turbotunnel.go
index aad2e6a..49a011c 100644
--- a/client/lib/turbotunnel.go
+++ b/client/lib/turbotunnel.go
@@ -12,10 +12,10 @@ import (
 
 var errNotImplemented = errors.New("not implemented")
 
-// EncapsulationPacketConn implements the net.PacketConn interface over an
+// encapsulationPacketConn implements the net.PacketConn interface over an
 // io.ReadWriteCloser stream, using the encapsulation package to represent
 // packets in a stream.
-type EncapsulationPacketConn struct {
+type encapsulationPacketConn struct {
 	io.ReadWriteCloser
 	localAddr  net.Addr
 	remoteAddr net.Addr
@@ -23,11 +23,11 @@ type EncapsulationPacketConn struct {
 }
 
 // NewEncapsulationPacketConn makes
-func NewEncapsulationPacketConn(
+func newEncapsulationPacketConn(
 	localAddr, remoteAddr net.Addr,
 	conn io.ReadWriteCloser,
-) *EncapsulationPacketConn {
-	return &EncapsulationPacketConn{
+) *encapsulationPacketConn {
+	return &encapsulationPacketConn{
 		ReadWriteCloser: conn,
 		localAddr:       localAddr,
 		remoteAddr:      remoteAddr,
@@ -36,7 +36,7 @@ func NewEncapsulationPacketConn(
 }
 
 // ReadFrom reads an encapsulated packet from the stream.
-func (c *EncapsulationPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
+func (c *encapsulationPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
 	data, err := encapsulation.ReadData(c.ReadWriteCloser)
 	if err != nil {
 		return 0, c.remoteAddr, err
@@ -45,7 +45,7 @@ func (c *EncapsulationPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
 }
 
 // WriteTo writes an encapsulated packet to the stream.
-func (c *EncapsulationPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
+func (c *encapsulationPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
 	// addr is ignored.
 	_, err := encapsulation.WriteData(c.bw, p)
 	if err == nil {
@@ -59,10 +59,10 @@ func (c *EncapsulationPacketConn) WriteTo(p []byte, addr net.Addr) (int, error)
 
 // LocalAddr returns the localAddr value that was passed to
 // NewEncapsulationPacketConn.
-func (c *EncapsulationPacketConn) LocalAddr() net.Addr {
+func (c *encapsulationPacketConn) LocalAddr() net.Addr {
 	return c.localAddr
 }
 
-func (c *EncapsulationPacketConn) SetDeadline(t time.Time) error      { return errNotImplemented }
-func (c *EncapsulationPacketConn) SetReadDeadline(t time.Time) error  { return errNotImplemented }
-func (c *EncapsulationPacketConn) SetWriteDeadline(t time.Time) error { return errNotImplemented }
+func (c *encapsulationPacketConn) SetDeadline(t time.Time) error      { return errNotImplemented }
+func (c *encapsulationPacketConn) SetReadDeadline(t time.Time) error  { return errNotImplemented }
+func (c *encapsulationPacketConn) SetWriteDeadline(t time.Time) error { return errNotImplemented }
diff --git a/client/lib/util.go b/client/lib/util.go
index 0eb8ddd..00b3709 100644
--- a/client/lib/util.go
+++ b/client/lib/util.go
@@ -9,27 +9,27 @@ const (
 	LogTimeInterval = 5 * time.Second
 )
 
-type BytesLogger interface {
-	AddOutbound(int)
-	AddInbound(int)
+type bytesLogger interface {
+	addOutbound(int)
+	addInbound(int)
 }
 
-// Default BytesLogger does nothing.
-type BytesNullLogger struct{}
+// Default bytesLogger does nothing.
+type bytesNullLogger struct{}
 
-func (b BytesNullLogger) AddOutbound(amount int) {}
-func (b BytesNullLogger) AddInbound(amount int)  {}
+func (b bytesNullLogger) addOutbound(amount int) {}
+func (b bytesNullLogger) addInbound(amount int)  {}
 
-// BytesSyncLogger uses channels to safely log from multiple sources with output
+// bytesSyncLogger uses channels to safely log from multiple sources with output
 // occuring at reasonable intervals.
-type BytesSyncLogger struct {
+type bytesSyncLogger struct {
 	outboundChan chan int
 	inboundChan  chan int
 }
 
-// NewBytesSyncLogger returns a new BytesSyncLogger and starts it loggin.
-func NewBytesSyncLogger() *BytesSyncLogger {
-	b := &BytesSyncLogger{
+// newBytesSyncLogger returns a new bytesSyncLogger and starts it loggin.
+func newBytesSyncLogger() *bytesSyncLogger {
+	b := &bytesSyncLogger{
 		outboundChan: make(chan int, 5),
 		inboundChan:  make(chan int, 5),
 	}
@@ -37,7 +37,7 @@ func NewBytesSyncLogger() *BytesSyncLogger {
 	return b
 }
 
-func (b *BytesSyncLogger) log() {
+func (b *bytesSyncLogger) log() {
 	var outbound, inbound, outEvents, inEvents int
 	ticker := time.NewTicker(LogTimeInterval)
 	for {
@@ -61,10 +61,10 @@ func (b *BytesSyncLogger) log() {
 	}
 }
 
-func (b *BytesSyncLogger) AddOutbound(amount int) {
+func (b *bytesSyncLogger) addOutbound(amount int) {
 	b.outboundChan <- amount
 }
 
-func (b *BytesSyncLogger) AddInbound(amount int) {
+func (b *bytesSyncLogger) addInbound(amount int) {
 	b.inboundChan <- amount
 }
diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index 2f931ac..6fc24c0 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -32,7 +32,7 @@ type WebRTCPeer struct {
 
 	once sync.Once // Synchronization for PeerConnection destruction
 
-	BytesLogger BytesLogger
+	bytesLogger bytesLogger
 }
 
 // Construct a WebRTC PeerConnection.
@@ -49,7 +49,7 @@ func NewWebRTCPeer(config *webrtc.Configuration,
 	connection.closed = make(chan struct{})
 
 	// Override with something that's not NullLogger to have real logging.
-	connection.BytesLogger = &BytesNullLogger{}
+	connection.bytesLogger = &bytesNullLogger{}
 
 	// Pipes remain the same even when DataChannel gets switched.
 	connection.recvPipe, connection.writePipe = io.Pipe()
@@ -75,7 +75,7 @@ func (c *WebRTCPeer) Write(b []byte) (int, error) {
 	if err != nil {
 		return 0, err
 	}
-	c.BytesLogger.AddOutbound(len(b))
+	c.bytesLogger.addOutbound(len(b))
 	return len(b), nil
 }
 
@@ -186,7 +186,7 @@ func (c *WebRTCPeer) preparePeerConnection(config *webrtc.Configuration) error {
 			log.Println("0 length message---")
 		}
 		n, err := c.writePipe.Write(msg.Data)
-		c.BytesLogger.AddInbound(n)
+		c.bytesLogger.addInbound(n)
 		if err != nil {
 			// TODO: Maybe shouldn't actually close.
 			log.Println("Error writing to SOCKS pipe")

From 99887cd05d830896d2b2cda9809e4ff1a2836c93 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 9 Sep 2021 12:54:31 -0400
Subject: [PATCH 234/385] Add package functions to define and set the
 rendezvous method

Add exported functions to the snowflake client library to allow calling
programs to define and set their own custom broker rendezvous methods.
---
 client/lib/rendezvous.go           | 23 ++++++-------
 client/lib/rendezvous_ampcache.go  |  2 +-
 client/lib/rendezvous_http.go      |  2 +-
 client/lib/snowflake.go            |  4 +++
 doc/using-the-snowflake-library.md | 54 ++++++++++++++++++++++++++++++
 5 files changed, 71 insertions(+), 14 deletions(-)

diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index d58e729..cf67e09 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -26,21 +26,20 @@ const (
 	readLimit                    = 100000 //Maximum number of bytes to be read from an HTTP response
 )
 
-// rendezvousMethod represents a way of communicating with the broker: sending
+// RendezvousMethod represents a way of communicating with the broker: sending
 // an encoded client poll request (SDP offer) and receiving an encoded client
-// poll response (SDP answer) in return. rendezvousMethod is used by
+// poll response (SDP answer) in return. RendezvousMethod is used by
 // BrokerChannel, which is in charge of encoding and decoding, and all other
 // tasks that are independent of the rendezvous method.
-type rendezvousMethod interface {
+type RendezvousMethod interface {
 	Exchange([]byte) ([]byte, error)
 }
 
-// BrokerChannel contains a rendezvousMethod, as well as data that is not
-// specific to any rendezvousMethod. BrokerChannel has the responsibility of
-// encoding and decoding SDP offers and answers; rendezvousMethod is responsible
-// for the exchange of encoded information.
+// BrokerChannel uses a RendezvousMethod to communicate with the Snowflake broker.
+// The BrokerChannel is responsible for encoding and decoding SDP offers and answers;
+// RendezvousMethod is responsible for the exchange of encoded information.
 type BrokerChannel struct {
-	rendezvous         rendezvousMethod
+	Rendezvous         RendezvousMethod
 	keepLocalAddresses bool
 	natType            string
 	lock               sync.Mutex
@@ -68,7 +67,7 @@ func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (
 		log.Println("Domain fronting using:", front)
 	}
 
-	var rendezvous rendezvousMethod
+	var rendezvous RendezvousMethod
 	var err error
 	if ampCache != "" {
 		rendezvous, err = newAMPCacheRendezvous(broker, ampCache, front, createBrokerTransport())
@@ -80,7 +79,7 @@ func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (
 	}
 
 	return &BrokerChannel{
-		rendezvous:         rendezvous,
+		Rendezvous:         rendezvous,
 		keepLocalAddresses: keepLocalAddresses,
 		natType:            nat.NATUnknown,
 	}, nil
@@ -118,8 +117,8 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) (
 		return nil, err
 	}
 
-	// Do the exchange using our rendezvousMethod.
-	encResp, err := bc.rendezvous.Exchange(encReq)
+	// Do the exchange using our RendezvousMethod.
+	encResp, err := bc.Rendezvous.Exchange(encReq)
 	if err != nil {
 		return nil, err
 	}
diff --git a/client/lib/rendezvous_ampcache.go b/client/lib/rendezvous_ampcache.go
index 4856893..2f1fb9f 100644
--- a/client/lib/rendezvous_ampcache.go
+++ b/client/lib/rendezvous_ampcache.go
@@ -11,7 +11,7 @@ import (
 	"git.torproject.org/pluggable-transports/snowflake.git/common/amp"
 )
 
-// ampCacheRendezvous is a rendezvousMethod that communicates with the
+// ampCacheRendezvous is a RendezvousMethod that communicates with the
 // .../amp/client route of the broker, optionally over an AMP cache proxy, and
 // with optional domain fronting.
 type ampCacheRendezvous struct {
diff --git a/client/lib/rendezvous_http.go b/client/lib/rendezvous_http.go
index 01219cb..e020077 100644
--- a/client/lib/rendezvous_http.go
+++ b/client/lib/rendezvous_http.go
@@ -10,7 +10,7 @@ import (
 	"net/url"
 )
 
-// httpRendezvous is a rendezvousMethod that communicates with the .../client
+// httpRendezvous is a RendezvousMethod that communicates with the .../client
 // route of the broker over HTTP or HTTPS, with optional domain fronting.
 type httpRendezvous struct {
 	brokerURL *url.URL
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index fb7fab9..e0591a7 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -132,6 +132,10 @@ func (t *Transport) Dial() (net.Conn, error) {
 	return &SnowflakeConn{Stream: stream, sess: sess, pconn: pconn, snowflakes: snowflakes}, nil
 }
 
+func (t *Transport) SetRendezvousMethod(r RendezvousMethod) {
+	t.dialer.Rendezvous = r
+}
+
 type SnowflakeConn struct {
 	*smux.Stream
 	sess       *smux.Session
diff --git a/doc/using-the-snowflake-library.md b/doc/using-the-snowflake-library.md
index 9308cdc..4dc47fc 100644
--- a/doc/using-the-snowflake-library.md
+++ b/doc/using-the-snowflake-library.md
@@ -38,6 +38,60 @@ func main() {
 }
 ```
 
+#### Using your own rendezvous method
+
+You can define and use your own rendezvous method to communicate with a Snowflake broker by implementing the `RendezvousMethod` interface.
+
+```Golang
+
+package main
+
+import (
+    "log"
+
+    sf "git.torproject.org/pluggable-transports/snowflake.git/client/lib"
+)
+
+type StubMethod struct {
+}
+
+func (m *StubMethod) Exchange(pollReq []byte) ([]byte, error) {
+    var brokerResponse []byte
+    var err error
+
+    //Implement the logic you need to communicate with the Snowflake broker here
+
+    return brokerResponse, err
+}
+
+func main() {
+    config := sf.ClientConfig{
+        ICEAddresses:       []string{
+            "stun:stun.voip.blackberry.com:3478",
+            "stun:stun.stunprotocol.org:3478"},
+    }
+    transport, err := sf.NewSnowflakeClient(config)
+    if err != nil {
+        log.Fatal("Failed to start snowflake transport: ", err)
+    }
+
+    // custom rendezvous methods can be set with `SetRendezvousMethod`
+    rendezvous := &StubMethod{}
+    transport.SetRendezvousMethod(rendezvous)
+
+    // transport implements the ClientFactory interface and returns a net.Conn
+    conn, err := transport.Dial()
+    if err != nil {
+        log.Printf("dial error: %s", err)
+        return
+    }
+    defer conn.Close()
+
+    // ...
+
+}
+```
+
 ### Server library
 
 The Snowflake server library contains functions for running a Snowflake server.

From 638ec6c222327ca02950338b2919ce8b22e1f900 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 9 Sep 2021 16:01:38 -0400
Subject: [PATCH 235/385] Update Snowflake client library documentation

Follow best practices for documenting the exported pieces of the
Snowflake client library.
---
 client/lib/interfaces.go          | 17 ++++++-----
 client/lib/peers.go               | 18 +++++++-----
 client/lib/rendezvous.go          | 23 ++++++---------
 client/lib/rendezvous_ampcache.go |  4 +--
 client/lib/rendezvous_http.go     |  2 +-
 client/lib/rendezvous_test.go     |  4 +--
 client/lib/snowflake.go           | 49 +++++++++++++++++++++++++------
 client/lib/webrtc.go              | 16 ++++++----
 8 files changed, 83 insertions(+), 50 deletions(-)

diff --git a/client/lib/interfaces.go b/client/lib/interfaces.go
index 0a871b8..66b9d56 100644
--- a/client/lib/interfaces.go
+++ b/client/lib/interfaces.go
@@ -1,23 +1,24 @@
 package lib
 
-// Interface for catching Snowflakes. (aka the remote dialer)
+// Tongue is an interface for catching Snowflakes. (aka the remote dialer)
 type Tongue interface {
+	// Catch makes a connection to a new snowflake.
 	Catch() (*WebRTCPeer, error)
 
-	// Get the maximum number of snowflakes
+	// GetMax returns the maximum number of snowflakes a client can have.
 	GetMax() int
 }
 
-// Interface for collecting some number of Snowflakes, for passing along
-// ultimately to the SOCKS handler.
+// SnowflakeCollector is an interface for managing a client's collection of snowflakes.
 type SnowflakeCollector interface {
-	// Add a Snowflake to the collection.
-	// Implementation should decide how to connect and maintain the webRTCConn.
+	// Collect adds a snowflake to the collection.
+	// The implementation of Collect should decide how to connect to and maintain
+	// the connection to the WebRTCPeer.
 	Collect() (*WebRTCPeer, error)
 
-	// Remove and return the most available Snowflake from the collection.
+	// Pop removes and returns the most available snowflake from the collection.
 	Pop() *WebRTCPeer
 
-	// Signal when the collector has stopped collecting.
+	// Melted returns a channel that will signal when the collector has stopped.
 	Melted() <-chan struct{}
 }
diff --git a/client/lib/peers.go b/client/lib/peers.go
index 1abcd95..6bddbf2 100644
--- a/client/lib/peers.go
+++ b/client/lib/peers.go
@@ -8,7 +8,7 @@ import (
 	"sync"
 )
 
-// Container which keeps track of multiple WebRTC remote peers.
+// Peers is a container that keeps track of multiple WebRTC remote peers.
 // Implements |SnowflakeCollector|.
 //
 // Maintaining a set of pre-connected Peers with fresh but inactive datachannels
@@ -31,7 +31,7 @@ type Peers struct {
 	collectLock sync.Mutex
 }
 
-// Construct a fresh container of remote peers.
+// NewPeers constructs a fresh container of remote peers.
 func NewPeers(tongue Tongue) (*Peers, error) {
 	p := &Peers{}
 	// Use buffered go channel to pass snowflakes onwards to the SOCKS handler.
@@ -45,7 +45,7 @@ func NewPeers(tongue Tongue) (*Peers, error) {
 	return p, nil
 }
 
-// As part of |SnowflakeCollector| interface.
+// Collect connects to and adds a new remote peer as part of |SnowflakeCollector| interface.
 func (p *Peers) Collect() (*WebRTCPeer, error) {
 	// Engage the Snowflake Catching interface, which must be available.
 	p.collectLock.Lock()
@@ -76,8 +76,8 @@ func (p *Peers) Collect() (*WebRTCPeer, error) {
 	return connection, nil
 }
 
-// Pop blocks until an available, valid snowflake appears. Returns nil after End
-// has been called.
+// Pop blocks until an available, valid snowflake appears.
+// Pop will return nil after End has been called.
 func (p *Peers) Pop() *WebRTCPeer {
 	for {
 		snowflake, ok := <-p.snowflakeChan
@@ -93,12 +93,13 @@ func (p *Peers) Pop() *WebRTCPeer {
 	}
 }
 
-// As part of |SnowflakeCollector| interface.
+// Melted returns a channel that will close when peers stop being collected.
+// Melted is a necessary part of |SnowflakeCollector| interface.
 func (p *Peers) Melted() <-chan struct{} {
 	return p.melt
 }
 
-// Returns total available Snowflakes (including the active one)
+// Count returns the total available Snowflakes (including the active ones)
 // The count only reduces when connections themselves close, rather than when
 // they are popped.
 func (p *Peers) Count() int {
@@ -118,7 +119,8 @@ func (p *Peers) purgeClosedPeers() {
 	}
 }
 
-// Close all Peers contained here.
+// End closes all active connections to Peers contained here, and stops the
+// collection of future Peers.
 func (p *Peers) End() {
 	close(p.melt)
 	p.collectLock.Lock()
diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index cf67e09..689e51c 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -1,10 +1,5 @@
 // WebRTC rendezvous requires the exchange of SessionDescriptions between
 // peers in order to establish a PeerConnection.
-//
-// This file contains the one method currently available to Snowflake:
-//
-// - Domain-fronted HTTP signaling. The Broker automatically exchange offers
-//   and answers between this client and some remote WebRTC proxy.
 
 package lib
 
@@ -22,7 +17,7 @@ import (
 )
 
 const (
-	BrokerErrorUnexpected string = "Unexpected error, no answer."
+	brokerErrorUnexpected string = "Unexpected error, no answer."
 	readLimit                    = 100000 //Maximum number of bytes to be read from an HTTP response
 )
 
@@ -55,7 +50,7 @@ func createBrokerTransport() http.RoundTripper {
 	return transport
 }
 
-// Construct a new BrokerChannel, where:
+// NewBrokerChannel construct a new BrokerChannel, where:
 // |broker| is the full URL of the facilitating program which assigns proxies
 // to clients, and |front| is the option fronting domain.
 func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (*BrokerChannel, error) {
@@ -85,10 +80,8 @@ func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (
 	}, nil
 }
 
-// Roundtrip HTTP POST using WebRTC SessionDescriptions.
-//
-// Send an SDP offer to the broker, which assigns a proxy and responds
-// with an SDP answer from a designated remote WebRTC peer.
+// Negotiate uses a RendezvousMethod to send the client's WebRTC SDP offer
+// and receive a snowflake proxy WebRTC SDP answer in return.
 func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) (
 	*webrtc.SessionDescription, error) {
 	// Ideally, we could specify an `RTCIceTransportPolicy` that would handle
@@ -135,6 +128,7 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) (
 	return util.DeserializeSessionDescription(resp.Answer)
 }
 
+// SetNATType sets the NAT type of the client so we can send it to the WebRTC broker.
 func (bc *BrokerChannel) SetNATType(NATType string) {
 	bc.lock.Lock()
 	bc.natType = NATType
@@ -142,13 +136,14 @@ func (bc *BrokerChannel) SetNATType(NATType string) {
 	log.Printf("NAT Type: %s", NATType)
 }
 
-// Implements the |Tongue| interface to catch snowflakes, using BrokerChannel.
+// WebRTCDialer implements the |Tongue| interface to catch snowflakes, using BrokerChannel.
 type WebRTCDialer struct {
 	*BrokerChannel
 	webrtcConfig *webrtc.Configuration
 	max          int
 }
 
+// NewWebRTCDialer constructs a new WebRTCDialer.
 func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int) *WebRTCDialer {
 	config := webrtc.Configuration{
 		ICEServers: iceServers,
@@ -161,14 +156,14 @@ func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max i
 	}
 }
 
-// Initialize a WebRTC Connection by signaling through the broker.
+// Catch initializes a WebRTC Connection by signaling through the BrokerChannel.
 func (w WebRTCDialer) Catch() (*WebRTCPeer, error) {
 	// TODO: [#25591] Fetch ICE server information from Broker.
 	// TODO: [#25596] Consider TURN servers here too.
 	return NewWebRTCPeer(w.webrtcConfig, w.BrokerChannel)
 }
 
-// Returns the maximum number of snowflakes to collect
+// GetMax returns the maximum number of snowflakes to collect.
 func (w WebRTCDialer) GetMax() int {
 	return w.max
 }
diff --git a/client/lib/rendezvous_ampcache.go b/client/lib/rendezvous_ampcache.go
index 2f1fb9f..6ac99b3 100644
--- a/client/lib/rendezvous_ampcache.go
+++ b/client/lib/rendezvous_ampcache.go
@@ -93,7 +93,7 @@ func (r *ampCacheRendezvous) Exchange(encPollReq []byte) ([]byte, error) {
 		// * If the broker returns a 5xx status, the AMP cache
 		//   translates it to a 404.
 		// https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/#redirect-%26-error-handling
-		return nil, errors.New(BrokerErrorUnexpected)
+		return nil, errors.New(brokerErrorUnexpected)
 	}
 	if _, err := resp.Location(); err == nil {
 		// The Google AMP Cache may return a "silent redirect" with
@@ -103,7 +103,7 @@ func (r *ampCacheRendezvous) Exchange(encPollReq []byte) ([]byte, error) {
 		// follow redirects nor execute JavaScript, but in any case we
 		// cannot extract information from this response and can only
 		// treat it as an error.
-		return nil, errors.New(BrokerErrorUnexpected)
+		return nil, errors.New(brokerErrorUnexpected)
 	}
 
 	lr := io.LimitReader(resp.Body, readLimit+1)
diff --git a/client/lib/rendezvous_http.go b/client/lib/rendezvous_http.go
index e020077..43ed075 100644
--- a/client/lib/rendezvous_http.go
+++ b/client/lib/rendezvous_http.go
@@ -60,7 +60,7 @@ func (r *httpRendezvous) Exchange(encPollReq []byte) ([]byte, error) {
 
 	log.Printf("HTTP rendezvous response: %s", resp.Status)
 	if resp.StatusCode != http.StatusOK {
-		return nil, errors.New(BrokerErrorUnexpected)
+		return nil, errors.New(brokerErrorUnexpected)
 	}
 
 	return limitedRead(resp.Body, readLimit)
diff --git a/client/lib/rendezvous_test.go b/client/lib/rendezvous_test.go
index 6a1a071..4bc5766 100644
--- a/client/lib/rendezvous_test.go
+++ b/client/lib/rendezvous_test.go
@@ -122,7 +122,7 @@ func TestHTTPRendezvous(t *testing.T) {
 			answer, err := rend.Exchange(fakeEncPollReq)
 			So(err, ShouldNotBeNil)
 			So(answer, ShouldBeNil)
-			So(err.Error(), ShouldResemble, BrokerErrorUnexpected)
+			So(err.Error(), ShouldResemble, brokerErrorUnexpected)
 		})
 
 		Convey("httpRendezvous.Exchange fails with error", func() {
@@ -243,7 +243,7 @@ func TestAMPCacheRendezvous(t *testing.T) {
 			answer, err := rend.Exchange(fakeEncPollReq)
 			So(err, ShouldNotBeNil)
 			So(answer, ShouldBeNil)
-			So(err.Error(), ShouldResemble, BrokerErrorUnexpected)
+			So(err.Error(), ShouldResemble, brokerErrorUnexpected)
 		})
 
 		Convey("ampCacheRendezvous.Exchange fails with error", func() {
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index e0591a7..4b17f0b 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -17,12 +17,21 @@ import (
 )
 
 const (
+	// ReconnectTimeout is the time a Snowflake client will wait before collecting
+	// more snowflakes.
 	ReconnectTimeout = 10 * time.Second
+	// SnowflakeTimeout is the time a Snowflake client will wait before determining that
+	// a remote snowflake has been disconnected. If no new messages are sent or received
+	// in this time period, the client will terminate the connection with the remote
+	// peer and collect a new snowflake.
 	SnowflakeTimeout = 20 * time.Second
-	// How long to wait for the OnOpen callback on a DataChannel.
+	// DataChannelTimeout is how long the client will wait for the OnOpen callback
+	// on a newly created DataChannel.
 	DataChannelTimeout = 10 * time.Second
 
+	// WindowSize is the number of packets in the send and receive window of a KCP connection.
 	WindowSize = 65535
+	// StreamSize controls the maximum amount of in flight data between a client and server.
 	StreamSize = 1048576 //1MB
 )
 
@@ -37,16 +46,31 @@ type Transport struct {
 	dialer *WebRTCDialer
 }
 
+// ClientConfig defines how the SnowflakeClient will connect to the broker and Snowflake proxies.
 type ClientConfig struct {
-	BrokerURL          string
-	AmpCacheURL        string
-	FrontDomain        string
-	ICEAddresses       []string
+	// BrokerURL is the full URL of the Snowflake broker that the client will connect to.
+	BrokerURL string
+	// AmpCacheURL is the full URL of a valid AMP cache. A nonzero value indicates
+	// that AMP cache will be used as the rendezvous method with the broker.
+	AmpCacheURL string
+	// FrontDomain is a the full URL of an optional front domain that can be used with either
+	// the AMP cache or HTTP domain fronting rendezvous method.
+	FrontDomain string
+	// ICEAddresses are a slice of ICE server URLs that will be used for NAT traversal and
+	// the creation of the client's WebRTC SDP offer.
+	ICEAddresses []string
+	// KeepLocalAddresses is an optional setting that will prevent the removal of local or
+	// invalid addresses from the client's SDP offer. This is useful for local deployments
+	// and testing.
 	KeepLocalAddresses bool
-	Max                int
+	// Max is the maximum number of snowflake proxy peers that the client should attempt to
+	// connect to.
+	Max int
 }
 
-// Create a new Snowflake transport client that can spawn multiple Snowflake connections.
+// NewSnowflakeClient creates a new Snowflake transport client that can spawn multiple
+// Snowflake connections.
+//
 // brokerURL and frontDomain are the urls for the broker host and domain fronting host
 // iceAddresses are the STUN/TURN urls needed for WebRTC negotiation
 // keepLocalAddresses is a flag to enable sending local network addresses (for testing purposes)
@@ -82,8 +106,10 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
 	return transport, nil
 }
 
-// Create a new Snowflake connection. Starts the collection of snowflakes and returns a
-// smux Stream.
+// Dial creates a new Snowflake connection.
+// Dial starts the collection of snowflakes and returns a SnowflakeConn that is a
+// wrapper around a smux.Stream that will reliably deliver data to a Snowflake
+// server through one or more snowflake proxies.
 func (t *Transport) Dial() (net.Conn, error) {
 	// Cleanup functions to run before returning, in case of an error.
 	var cleanup []func()
@@ -132,10 +158,12 @@ func (t *Transport) Dial() (net.Conn, error) {
 	return &SnowflakeConn{Stream: stream, sess: sess, pconn: pconn, snowflakes: snowflakes}, nil
 }
 
+// SetRendezvousMethod sets the rendezvous method to the Snowflake broker.
 func (t *Transport) SetRendezvousMethod(r RendezvousMethod) {
 	t.dialer.Rendezvous = r
 }
 
+// SnowflakeConn is a reliable connection to a snowflake server that implements net.Conn.
 type SnowflakeConn struct {
 	*smux.Stream
 	sess       *smux.Session
@@ -143,6 +171,9 @@ type SnowflakeConn struct {
 	snowflakes *Peers
 }
 
+// Close closes the connection.
+//
+// The collection of snowflake proxies for this connection is stopped.
 func (conn *SnowflakeConn) Close() error {
 	log.Printf("---- SnowflakeConn: closed stream %v ---", conn.ID())
 	conn.Stream.Close()
diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index 6fc24c0..538cc8b 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -12,10 +12,9 @@ import (
 	"github.com/pion/webrtc/v3"
 )
 
-// Remote WebRTC peer.
+// WebRTCPeer represents a WebRTC connection to a remote snowflake proxy.
 //
-// Handles preparation of go-webrtc PeerConnection. Only ever has
-// one DataChannel.
+// Each WebRTCPeer only ever has one DataChannel that is used as the peer's transport.
 type WebRTCPeer struct {
 	id        string
 	pc        *webrtc.PeerConnection
@@ -35,7 +34,11 @@ type WebRTCPeer struct {
 	bytesLogger bytesLogger
 }
 
-// Construct a WebRTC PeerConnection.
+// NewWebRTCPeer constructs a WebRTC PeerConnection to a snowflake proxy.
+//
+// The creation of the peer handles the signaling to the Snowflake broker, including
+// the exchange of SDP information, the creation of a PeerConnection, and the establishment
+// of a DataChannel to the Snowflake proxy.
 func NewWebRTCPeer(config *webrtc.Configuration,
 	broker *BrokerChannel) (*WebRTCPeer, error) {
 	connection := new(WebRTCPeer)
@@ -79,7 +82,7 @@ func (c *WebRTCPeer) Write(b []byte) (int, error) {
 	return len(b), nil
 }
 
-//Returns a boolean indicated whether the peer is closed
+// Closed returns a boolean indicated whether the peer is closed.
 func (c *WebRTCPeer) Closed() bool {
 	select {
 	case <-c.closed:
@@ -89,6 +92,7 @@ func (c *WebRTCPeer) Closed() bool {
 	return false
 }
 
+// Close closes the connection the snowflake proxy.
 func (c *WebRTCPeer) Close() error {
 	c.once.Do(func() {
 		close(c.closed)
@@ -225,7 +229,7 @@ func (c *WebRTCPeer) preparePeerConnection(config *webrtc.Configuration) error {
 	return nil
 }
 
-// Close all channels and transports
+// cleanup closes all channels and transports
 func (c *WebRTCPeer) cleanup() {
 	// Close this side of the SOCKS pipe.
 	if c.writePipe != nil { // c.writePipe can be nil in tests.

From 767c07dc58c97382e32d3a8388ed468b32ccf382 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 9 Sep 2021 16:11:01 -0400
Subject: [PATCH 236/385] Update client library usage documentation

---
 doc/using-the-snowflake-library.md | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/doc/using-the-snowflake-library.md b/doc/using-the-snowflake-library.md
index 4dc47fc..2c74296 100644
--- a/doc/using-the-snowflake-library.md
+++ b/doc/using-the-snowflake-library.md
@@ -17,10 +17,15 @@ import (
 
 func main() {
 
-    transport, err := sf.NewSnowflakeClient("https://snowflake-broker.example.com",
-        "https://friendlyfrontdomain.net",
-        []string{"stun:stun.voip.blackberry.com:3478", "stun:stun.stunprotocol.org:3478"},
-        false, 1)
+    config := sf.ClientConfig{
+        BrokerURL:   "https://snowflake-broker.example.com",
+        FrontDomain: "https://friendlyfrontdomain.net",
+        ICEAddresses: []string{
+            "stun:stun.voip.blackberry.com:3478",
+            "stun:stun.stunprotocol.org:3478"},
+        Max: 1,
+    }
+    transport, err := sf.NewSnowflakeClient(config)
     if err != nil {
         log.Fatal("Failed to start snowflake transport: ", err)
     }

From 6c6a2e44abdd71e1f369d32bc3f1b9f7ed00102c Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 9 Sep 2021 16:25:07 -0400
Subject: [PATCH 237/385] Change package name and add a package comment

---
 client/lib/interfaces.go          |  2 +-
 client/lib/lib_test.go            |  2 +-
 client/lib/peers.go               |  2 +-
 client/lib/rendezvous.go          |  2 +-
 client/lib/rendezvous_ampcache.go |  2 +-
 client/lib/rendezvous_http.go     |  2 +-
 client/lib/rendezvous_test.go     |  2 +-
 client/lib/snowflake.go           | 29 ++++++++++++++++++++++++++++-
 client/lib/turbotunnel.go         |  2 +-
 client/lib/util.go                |  2 +-
 client/lib/webrtc.go              |  2 +-
 11 files changed, 38 insertions(+), 11 deletions(-)

diff --git a/client/lib/interfaces.go b/client/lib/interfaces.go
index 66b9d56..e8a5cf6 100644
--- a/client/lib/interfaces.go
+++ b/client/lib/interfaces.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_client
 
 // Tongue is an interface for catching Snowflakes. (aka the remote dialer)
 type Tongue interface {
diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go
index 86601b1..f741775 100644
--- a/client/lib/lib_test.go
+++ b/client/lib/lib_test.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_client
 
 import (
 	"fmt"
diff --git a/client/lib/peers.go b/client/lib/peers.go
index 6bddbf2..1c39425 100644
--- a/client/lib/peers.go
+++ b/client/lib/peers.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_client
 
 import (
 	"container/list"
diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index 689e51c..ffc0358 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -1,7 +1,7 @@
 // WebRTC rendezvous requires the exchange of SessionDescriptions between
 // peers in order to establish a PeerConnection.
 
-package lib
+package snowflake_client
 
 import (
 	"errors"
diff --git a/client/lib/rendezvous_ampcache.go b/client/lib/rendezvous_ampcache.go
index 6ac99b3..3c3780a 100644
--- a/client/lib/rendezvous_ampcache.go
+++ b/client/lib/rendezvous_ampcache.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_client
 
 import (
 	"errors"
diff --git a/client/lib/rendezvous_http.go b/client/lib/rendezvous_http.go
index 43ed075..fd80e7f 100644
--- a/client/lib/rendezvous_http.go
+++ b/client/lib/rendezvous_http.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_client
 
 import (
 	"bytes"
diff --git a/client/lib/rendezvous_test.go b/client/lib/rendezvous_test.go
index 4bc5766..0b3288b 100644
--- a/client/lib/rendezvous_test.go
+++ b/client/lib/rendezvous_test.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_client
 
 import (
 	"bytes"
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 4b17f0b..3ac75b0 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -1,4 +1,31 @@
-package lib
+/*
+Package snowflake_client implements functionality necessary for a client to establish a connection
+to a server using Snowflake.
+
+Included in the package is a Transport type that implements the Pluggable Transports v2.1 Go API
+specification. To use Snowflake, you must first create a client from a configuration:
+
+	config := snowflake_client.ClientConfig{
+		BrokerURL:   "https://snowflake-broker.example.com",
+		FrontDomain: "https://friendlyfrontdomain.net",
+		Max: 1,
+		// ...
+	}
+	transport, err := snowflake_client.NewSnowflakeClient(config)
+	if err != nil {
+		// handle error
+	}
+
+The Dial function connects to a Snowflake server:
+
+	conn, err := transport.Dial()
+	if err != nil {
+		// handle error
+	}
+	defer conn.Close()
+
+*/
+package snowflake_client
 
 import (
 	"context"
diff --git a/client/lib/turbotunnel.go b/client/lib/turbotunnel.go
index 49a011c..71f01a0 100644
--- a/client/lib/turbotunnel.go
+++ b/client/lib/turbotunnel.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_client
 
 import (
 	"bufio"
diff --git a/client/lib/util.go b/client/lib/util.go
index 00b3709..42c8f97 100644
--- a/client/lib/util.go
+++ b/client/lib/util.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_client
 
 import (
 	"log"
diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index 538cc8b..f4b775c 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_client
 
 import (
 	"crypto/rand"

From 5927c2bdf9266f70856602a666928da397f19bdb Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Wed, 29 Sep 2021 15:48:31 -0400
Subject: [PATCH 238/385] Default to a maximum value of 1 Snowflake peer

---
 client/lib/snowflake.go | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 3ac75b0..0096759 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -8,7 +8,6 @@ specification. To use Snowflake, you must first create a client from a configura
 	config := snowflake_client.ClientConfig{
 		BrokerURL:   "https://snowflake-broker.example.com",
 		FrontDomain: "https://friendlyfrontdomain.net",
-		Max: 1,
 		// ...
 	}
 	transport, err := snowflake_client.NewSnowflakeClient(config)
@@ -91,7 +90,7 @@ type ClientConfig struct {
 	// and testing.
 	KeepLocalAddresses bool
 	// Max is the maximum number of snowflake proxy peers that the client should attempt to
-	// connect to.
+	// connect to. Defaults to 1.
 	Max int
 }
 
@@ -128,7 +127,11 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
 	}
 	go updateNATType(iceServers, broker)
 
-	transport := &Transport{dialer: NewWebRTCDialer(broker, iceServers, config.Max)}
+	max := 1
+	if config.Max > max {
+		max = config.Max
+	}
+	transport := &Transport{dialer: NewWebRTCDialer(broker, iceServers, max)}
 
 	return transport, nil
 }

From 5339ed2dd772e54912bd80d4ff828f56bd854272 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Fri, 1 Oct 2021 13:24:16 -0400
Subject: [PATCH 239/385] Stop exporting internal code

---
 server/lib/http.go      | 6 +++---
 server/lib/snowflake.go | 6 +++---
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/server/lib/http.go b/server/lib/http.go
index 3dff45c..13855c5 100644
--- a/server/lib/http.go
+++ b/server/lib/http.go
@@ -60,14 +60,14 @@ func (conn *overrideReadConn) Read(p []byte) (int, error) {
 	return conn.Reader.Read(p)
 }
 
-type HTTPHandler struct {
+type httpHandler struct {
 	// pconn is the adapter layer between stream-oriented WebSocket
 	// connections and the packet-oriented KCP layer.
 	pconn *turbotunnel.QueuePacketConn
 	ln    *SnowflakeListener
 }
 
-func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (handler *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ws, err := upgrader.Upgrade(w, r, nil)
 	if err != nil {
 		log.Println(err)
@@ -114,7 +114,7 @@ func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 // of their stream. These clients use the WebSocket as a raw pipe, and expect
 // their session to begin and end when this single WebSocket does.
 func oneshotMode(conn net.Conn, addr net.Addr, ln *SnowflakeListener) error {
-	return ln.QueueConn(&SnowflakeClientConn{Conn: conn, address: addr})
+	return ln.queueConn(&SnowflakeClientConn{Conn: conn, address: addr})
 }
 
 // turbotunnelMode handles clients that sent turbotunnel.Token at the start of
diff --git a/server/lib/snowflake.go b/server/lib/snowflake.go
index aa1872f..6c2375f 100644
--- a/server/lib/snowflake.go
+++ b/server/lib/snowflake.go
@@ -35,7 +35,7 @@ func NewSnowflakeServer(getCertificate func(*tls.ClientHelloInfo) (*tls.Certific
 func (t *Transport) Listen(addr net.Addr) (*SnowflakeListener, error) {
 	listener := &SnowflakeListener{addr: addr, queue: make(chan net.Conn, 65534)}
 
-	handler := HTTPHandler{
+	handler := httpHandler{
 		// pconn is shared among all connections to this server. It
 		// overlays packet-based client sessions on top of ephemeral
 		// WebSocket connections.
@@ -187,7 +187,7 @@ func (l *SnowflakeListener) acceptStreams(conn *kcp.UDPSession) error {
 			}
 			return err
 		}
-		l.QueueConn(&SnowflakeClientConn{Conn: stream, address: addr})
+		l.queueConn(&SnowflakeClientConn{Conn: stream, address: addr})
 	}
 }
 
@@ -226,7 +226,7 @@ func (l *SnowflakeListener) acceptSessions(ln *kcp.Listener) error {
 	}
 }
 
-func (l *SnowflakeListener) QueueConn(conn net.Conn) error {
+func (l *SnowflakeListener) queueConn(conn net.Conn) error {
 	select {
 	case <-l.closed:
 		return fmt.Errorf("accepted connection on closed listener")

From 4623c7d3e163f8384d4c8ce74b3bf3126b630306 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Fri, 1 Oct 2021 13:34:48 -0400
Subject: [PATCH 240/385] Add documentation where necessary for exported items

---
 server/lib/http.go      |  1 +
 server/lib/snowflake.go | 18 +++++++++++++-----
 2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/server/lib/http.go b/server/lib/http.go
index 13855c5..8184c06 100644
--- a/server/lib/http.go
+++ b/server/lib/http.go
@@ -193,6 +193,7 @@ func turbotunnelMode(conn net.Conn, addr net.Addr, pconn *turbotunnel.QueuePacke
 	return nil
 }
 
+// ClientMapAddr is a string that represents a connecting client.
 type ClientMapAddr string
 
 func (addr ClientMapAddr) Network() string {
diff --git a/server/lib/snowflake.go b/server/lib/snowflake.go
index 6c2375f..8f81353 100644
--- a/server/lib/snowflake.go
+++ b/server/lib/snowflake.go
@@ -17,7 +17,9 @@ import (
 )
 
 const (
+	// WindowSize is the number of packets in the send and receive window of a KCP connection.
 	WindowSize = 65535
+	// StreamSize controls the maximum amount of in flight data between a client and server.
 	StreamSize = 1048576 //1MB
 )
 
@@ -27,11 +29,14 @@ type Transport struct {
 	getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error)
 }
 
+// NewSnowflakeServer returns a new server-side Transport for Snowflake.
 func NewSnowflakeServer(getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error)) *Transport {
 
 	return &Transport{getCertificate: getCertificate}
 }
 
+// Listen starts a listener on addr that will accept both turbotunnel
+// and legacy Snowflake connections.
 func (t *Transport) Listen(addr net.Addr) (*SnowflakeListener, error) {
 	listener := &SnowflakeListener{addr: addr, queue: make(chan net.Conn, 65534)}
 
@@ -129,9 +134,9 @@ type SnowflakeListener struct {
 	closeOnce sync.Once
 }
 
-// Allows the caller to accept incoming Snowflake connections
+// Accept allows the caller to accept incoming Snowflake connections.
 // We accept connections from a queue to accommodate both incoming
-// smux Streams and legacy non-turbotunnel connections
+// smux Streams and legacy non-turbotunnel connections.
 func (l *SnowflakeListener) Accept() (net.Conn, error) {
 	select {
 	case <-l.closed:
@@ -142,10 +147,12 @@ func (l *SnowflakeListener) Accept() (net.Conn, error) {
 	}
 }
 
+// Addr returns the address of the SnowflakeListener
 func (l *SnowflakeListener) Addr() net.Addr {
 	return l.addr
 }
 
+// Close closes the Snowflake connection.
 func (l *SnowflakeListener) Close() error {
 	// Close our HTTP server and our KCP listener
 	l.closeOnce.Do(func() {
@@ -235,14 +242,15 @@ func (l *SnowflakeListener) queueConn(conn net.Conn) error {
 	}
 }
 
-// A wrapper for the underlying oneshot or turbotunnel conn
-// because we need to reference our mapping to determine the client
-// address
+// SnowflakeClientConn is a wrapper for the underlying oneshot or turbotunnel
+// conn. We need to reference our client address map to determine the
+// remote address
 type SnowflakeClientConn struct {
 	net.Conn
 	address net.Addr
 }
 
+// RemoteAddr returns the mapped client address of the Snowflake connection
 func (conn *SnowflakeClientConn) RemoteAddr() net.Addr {
 	return conn.address
 }

From 04ba50a531f118b710edd6722e8e07fc9f3a9b3c Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Fri, 1 Oct 2021 13:44:31 -0400
Subject: [PATCH 241/385] Change package name and add a package comment

---
 server/lib/http.go             |  2 +-
 server/lib/server_test.go      |  2 +-
 server/lib/snowflake.go        | 38 +++++++++++++++++++++++++++++++++-
 server/lib/turbotunnel.go      |  2 +-
 server/lib/turbotunnel_test.go |  2 +-
 5 files changed, 41 insertions(+), 5 deletions(-)

diff --git a/server/lib/http.go b/server/lib/http.go
index 8184c06..55849c5 100644
--- a/server/lib/http.go
+++ b/server/lib/http.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_server
 
 import (
 	"bufio"
diff --git a/server/lib/server_test.go b/server/lib/server_test.go
index 65d31d1..8e0deb4 100644
--- a/server/lib/server_test.go
+++ b/server/lib/server_test.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_server
 
 import (
 	"net"
diff --git a/server/lib/snowflake.go b/server/lib/snowflake.go
index 8f81353..93d3955 100644
--- a/server/lib/snowflake.go
+++ b/server/lib/snowflake.go
@@ -1,4 +1,40 @@
-package lib
+/*
+Package snowflake_server implements the functionality necessary to accept Snowflake
+connections from Snowflake clients.
+
+Included in the package is a Transport type that implements the Pluggable Transports v2.1 Go API
+specification. To start a TLS Snowflake server using the golang.org/x/crypto/acme/autocert
+library, configure a certificate manager for the server's domain name and then create a new
+Transport as follows:
+
+	// The snowflake server runs a websocket server. To run this securely, you will
+	// need a valid certificate.
+	certManager := &autocert.Manager{
+		Prompt:     autocert.AcceptTOS,
+		HostPolicy: autocert.HostWhitelist("snowflake.yourdomain.com"),
+		Email:      "you@yourdomain.com",
+	}
+
+	transport := snowflake_server.NewSnowflakeServer(certManager.GetCertificate)
+
+
+The Listen function starts a new listener, and Accept will return incoming Snowflake connections:
+
+	ln, err := transport.Listen(addr)
+	if err != nil {
+		// handle error
+	}
+	for {
+		conn, err := ln.Accept()
+		if err != nil {
+			// handle error
+		}
+		// handle conn
+	}
+
+
+*/
+package snowflake_server
 
 import (
 	"crypto/tls"
diff --git a/server/lib/turbotunnel.go b/server/lib/turbotunnel.go
index 741992d..1e9bb58 100644
--- a/server/lib/turbotunnel.go
+++ b/server/lib/turbotunnel.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_server
 
 import (
 	"net"
diff --git a/server/lib/turbotunnel_test.go b/server/lib/turbotunnel_test.go
index 85404af..ac79c05 100644
--- a/server/lib/turbotunnel_test.go
+++ b/server/lib/turbotunnel_test.go
@@ -1,4 +1,4 @@
-package lib
+package snowflake_server
 
 import (
 	"encoding/binary"

From 54ab79384f94b52edc444063408de1394e507417 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 14 Oct 2021 11:14:22 -0400
Subject: [PATCH 242/385] Unify broker/bridge domains to torproject.net

---
 doc/snowflake-proxy.1 | 4 ++--
 proxy/README.md       | 4 ++--
 proxy/snowflake.go    | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/doc/snowflake-proxy.1 b/doc/snowflake-proxy.1
index ccdd9a2..67f71b6 100644
--- a/doc/snowflake-proxy.1
+++ b/doc/snowflake-proxy.1
@@ -9,7 +9,7 @@ Snowflake bridge and then through the Tor network.
 .HP
 \fB\-broker\fR string
 .IP
-broker URL (default "https://snowflake\-broker.bamsoftware.com/")
+broker URL (default "https://snowflake\-broker.torproject.net/")
 .HP
 \fB\-capacity\fR uint
 .IP
@@ -25,7 +25,7 @@ log filename
 .HP
 \fB\-relay\fR string
 .IP
-websocket relay URL (default "wss://snowflake.bamsoftware.com/")
+websocket relay URL (default "wss://snowflake.torproject.net/")
 .HP
 \fB\-stun\fR string
 .IP
diff --git a/proxy/README.md b/proxy/README.md
index a7496da..e736667 100644
--- a/proxy/README.md
+++ b/proxy/README.md
@@ -30,7 +30,7 @@ The Snowflake proxy can be run with the following options:
 ```
 Usage of ./proxy:
   -broker string
-        broker URL (default "https://snowflake-broker.bamsoftware.com/")
+        broker URL (default "https://snowflake-broker.torproject.net/")
   -capacity uint
         maximum concurrent clients
   -keep-local-addresses
@@ -38,7 +38,7 @@ Usage of ./proxy:
   -log string
         log filename
   -relay string
-        websocket relay URL (default "wss://snowflake.bamsoftware.com/")
+        websocket relay URL (default "wss://snowflake.torproject.net/")
   -stun string
         stun URL (default "stun:stun.stunprotocol.org:3478")
   -unsafe-logging
diff --git a/proxy/snowflake.go b/proxy/snowflake.go
index d694471..7d7f9a2 100644
--- a/proxy/snowflake.go
+++ b/proxy/snowflake.go
@@ -25,9 +25,9 @@ import (
 	"github.com/pion/webrtc/v3"
 )
 
-const defaultBrokerURL = "https://snowflake-broker.bamsoftware.com/"
+const defaultBrokerURL = "https://snowflake-broker.torproject.net/"
 const defaultProbeURL = "https://snowflake-broker.torproject.net:8443/probe"
-const defaultRelayURL = "wss://snowflake.bamsoftware.com/"
+const defaultRelayURL = "wss://snowflake.torproject.net/"
 const defaultSTUNURL = "stun:stun.stunprotocol.org:3478"
 const pollInterval = 5 * time.Second
 const (

From 50e4f4fd61596bab254cb34e850c9ae63d82f891 Mon Sep 17 00:00:00 2001
From: idk 
Date: Mon, 25 Oct 2021 22:51:40 -0400
Subject: [PATCH 243/385] Turn the proxy code into a library

Allow other go programs to easily import the snowflake proxy library and
start/stop a snowflake proxy.
---
 proxy/{ => lib}/proxy-go_test.go |   8 +-
 proxy/{ => lib}/snowflake.go     | 203 +++++++++++++++++--------------
 proxy/{ => lib}/tokens.go        |   2 +-
 proxy/{ => lib}/tokens_test.go   |   2 +-
 proxy/{ => lib}/util.go          |  18 ++-
 proxy/{ => lib}/webrtcconn.go    |   2 +-
 proxy/main.go                    |  48 ++++++++
 7 files changed, 184 insertions(+), 99 deletions(-)
 rename proxy/{ => lib}/proxy-go_test.go (98%)
 rename proxy/{ => lib}/snowflake.go (72%)
 rename proxy/{ => lib}/tokens.go (97%)
 rename proxy/{ => lib}/tokens_test.go (96%)
 rename proxy/{ => lib}/util.go (71%)
 rename proxy/{ => lib}/webrtcconn.go (99%)
 create mode 100644 proxy/main.go

diff --git a/proxy/proxy-go_test.go b/proxy/lib/proxy-go_test.go
similarity index 98%
rename from proxy/proxy-go_test.go
rename to proxy/lib/proxy-go_test.go
index 6fb5a0b9..af71648 100644
--- a/proxy/proxy-go_test.go
+++ b/proxy/lib/proxy-go_test.go
@@ -1,4 +1,4 @@
-package main
+package snowflake
 
 import (
 	"bytes"
@@ -365,7 +365,7 @@ func TestBrokerInteractions(t *testing.T) {
 				b,
 			}
 
-			sdp := broker.pollOffer(sampleOffer)
+			sdp := broker.pollOffer(sampleOffer, nil)
 			expectedSDP, _ := strconv.Unquote(sampleSDP)
 			So(sdp.SDP, ShouldResemble, expectedSDP)
 		})
@@ -379,7 +379,7 @@ func TestBrokerInteractions(t *testing.T) {
 				b,
 			}
 
-			sdp := broker.pollOffer(sampleOffer)
+			sdp := broker.pollOffer(sampleOffer, nil)
 			So(sdp, ShouldBeNil)
 		})
 		Convey("sends answer to broker", func() {
@@ -478,7 +478,7 @@ func TestUtilityFuncs(t *testing.T) {
 	Convey("CopyLoop", t, func() {
 		c1, s1 := net.Pipe()
 		c2, s2 := net.Pipe()
-		go CopyLoop(s1, s2)
+		go copyLoop(s1, s2, nil)
 		go func() {
 			bytes := []byte("Hello!")
 			c1.Write(bytes)
diff --git a/proxy/snowflake.go b/proxy/lib/snowflake.go
similarity index 72%
rename from proxy/snowflake.go
rename to proxy/lib/snowflake.go
index 7d7f9a2..e35eabd 100644
--- a/proxy/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -1,10 +1,9 @@
-package main
+package snowflake
 
 import (
 	"bytes"
 	"crypto/rand"
 	"encoding/base64"
-	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -12,27 +11,44 @@ import (
 	"net"
 	"net/http"
 	"net/url"
-	"os"
 	"strings"
 	"sync"
 	"time"
 
 	"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
 	"git.torproject.org/pluggable-transports/snowflake.git/common/util"
 	"git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn"
 	"github.com/gorilla/websocket"
 	"github.com/pion/webrtc/v3"
 )
 
-const defaultBrokerURL = "https://snowflake-broker.torproject.net/"
-const defaultProbeURL = "https://snowflake-broker.torproject.net:8443/probe"
-const defaultRelayURL = "wss://snowflake.torproject.net/"
-const defaultSTUNURL = "stun:stun.stunprotocol.org:3478"
+// DefaultBrokerURL is the bamsoftware.com broker, https://snowflake-broker.bamsoftware.com
+// Changing this will change the default broker. The recommended way of changing
+// the broker that gets used is by passing an argument to Main.
+const DefaultBrokerURL = "https://snowflake-broker.bamsoftware.com/"
+
+// DefaultProbeURL is the torproject.org  ProbeURL, https://snowflake-broker.torproject.net:8443/probe
+// Changing this will change the default Probe URL. The recommended way of changing
+// the probe that gets used is by passing an argument to Main.
+const DefaultProbeURL = "https://snowflake-broker.torproject.net:8443/probe"
+
+// DefaultRelayURL is the bamsoftware.com  Websocket Relay, wss://snowflake.bamsoftware.com/
+// Changing this will change the default Relay URL. The recommended way of changing
+// the relay that gets used is by passing an argument to Main.
+const DefaultRelayURL = "wss://snowflake.bamsoftware.com/"
+
+// DefaultSTUNURL is a stunprotocol.org STUN URL. stun:stun.stunprotocol.org:3478
+// Changing this will change the default STUN URL. The recommended way of changing
+// the STUN Server that gets used is by passing an argument to Main.
+const DefaultSTUNURL = "stun:stun.stunprotocol.org:3478"
 const pollInterval = 5 * time.Second
+
 const (
-	NATUnknown      = "unknown"
-	NATRestricted   = "restricted"
+	// NATUnknown represents a NAT type which is unknown.
+	NATUnknown = "unknown"
+	// NATRestricted represents a restricted NAT.
+	NATRestricted = "restricted"
+	// NATUnrestricted represents an unrestricted NAT.
 	NATUnrestricted = "unrestricted"
 )
 
@@ -43,7 +59,6 @@ const dataChannelTimeout = 20 * time.Second
 const readLimit = 100000 //Maximum number of bytes to be read from an HTTP request
 
 var broker *SignalingServer
-var relayURL string
 
 var currentNATType = NATUnknown
 
@@ -57,6 +72,18 @@ var (
 	client http.Client
 )
 
+// SnowflakeProxy is a structure which is used to configure an embedded
+// Snowflake in another Go application.
+type SnowflakeProxy struct {
+	Capacity           uint
+	StunURL            string
+	RawBrokerURL       string
+	KeepLocalAddresses bool
+	RelayURL           string
+	LogOutput          io.Writer
+	shutdown           chan struct{}
+}
+
 // Checks whether an IP address is a remote address for the client
 func isRemoteAddress(ip net.IP) bool {
 	return !(util.IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback())
@@ -81,6 +108,7 @@ func limitedRead(r io.Reader, limit int64) ([]byte, error) {
 	return p, err
 }
 
+// SignalingServer keeps track of the SignalingServer in use by the Snowflake
 type SignalingServer struct {
 	url                *url.URL
 	transport          http.RoundTripper
@@ -102,6 +130,7 @@ func newSignalingServer(rawURL string, keepLocalAddresses bool) (*SignalingServe
 	return s, nil
 }
 
+// Post sends a POST request to the SignalingServer
 func (s *SignalingServer) Post(path string, payload io.Reader) ([]byte, error) {
 
 	req, err := http.NewRequest("POST", path, payload)
@@ -121,7 +150,7 @@ func (s *SignalingServer) Post(path string, payload io.Reader) ([]byte, error) {
 	return limitedRead(resp.Body, readLimit)
 }
 
-func (s *SignalingServer) pollOffer(sid string) *webrtc.SessionDescription {
+func (s *SignalingServer) pollOffer(sid string, shutdown chan struct{}) *webrtc.SessionDescription {
 	brokerPath := s.url.ResolveReference(&url.URL{Path: "proxy"})
 
 	ticker := time.NewTicker(pollInterval)
@@ -129,31 +158,36 @@ func (s *SignalingServer) pollOffer(sid string) *webrtc.SessionDescription {
 
 	// Run the loop once before hitting the ticker
 	for ; true; <-ticker.C {
-		numClients := int((tokens.count() / 8) * 8) // Round down to 8
-		body, err := messages.EncodePollRequest(sid, "standalone", currentNATType, numClients)
-		if err != nil {
-			log.Printf("Error encoding poll message: %s", err.Error())
+		select {
+		case <-shutdown:
 			return nil
-		}
-		resp, err := s.Post(brokerPath.String(), bytes.NewBuffer(body))
-		if err != nil {
-			log.Printf("error polling broker: %s", err.Error())
-		}
-
-		offer, _, err := messages.DecodePollResponse(resp)
-		if err != nil {
-			log.Printf("Error reading broker response: %s", err.Error())
-			log.Printf("body: %s", resp)
-			return nil
-		}
-		if offer != "" {
-			offer, err := util.DeserializeSessionDescription(offer)
+		default:
+			numClients := int((tokens.count() / 8) * 8) // Round down to 8
+			body, err := messages.EncodePollRequest(sid, "standalone", currentNATType, numClients)
 			if err != nil {
-				log.Printf("Error processing session description: %s", err.Error())
+				log.Printf("Error encoding poll message: %s", err.Error())
 				return nil
 			}
-			return offer
+			resp, err := s.Post(brokerPath.String(), bytes.NewBuffer(body))
+			if err != nil {
+				log.Printf("error polling broker: %s", err.Error())
+			}
 
+			offer, _, err := messages.DecodePollResponse(resp)
+			if err != nil {
+				log.Printf("Error reading broker response: %s", err.Error())
+				log.Printf("body: %s", resp)
+				return nil
+			}
+			if offer != "" {
+				offer, err := util.DeserializeSessionDescription(offer)
+				if err != nil {
+					log.Printf("Error processing session description: %s", err.Error())
+					return nil
+				}
+				return offer
+
+			}
 		}
 	}
 	return nil
@@ -192,33 +226,41 @@ func (s *SignalingServer) sendAnswer(sid string, pc *webrtc.PeerConnection) erro
 	return nil
 }
 
-func CopyLoop(c1 io.ReadWriteCloser, c2 io.ReadWriteCloser) {
-	var wg sync.WaitGroup
+func copyLoop(c1 io.ReadWriteCloser, c2 io.ReadWriteCloser, shutdown chan struct{}) {
+	var once sync.Once
+	defer c2.Close()
+	defer c1.Close()
+	done := make(chan struct{})
 	copyer := func(dst io.ReadWriteCloser, src io.ReadWriteCloser) {
-		defer wg.Done()
 		// Ignore io.ErrClosedPipe because it is likely caused by the
 		// termination of copyer in the other direction.
 		if _, err := io.Copy(dst, src); err != nil && err != io.ErrClosedPipe {
 			log.Printf("io.Copy inside CopyLoop generated an error: %v", err)
 		}
-		dst.Close()
-		src.Close()
+		once.Do(func() {
+			close(done)
+		})
 	}
-	wg.Add(2)
+
 	go copyer(c1, c2)
 	go copyer(c2, c1)
-	wg.Wait()
+
+	select {
+	case <-done:
+	case <-shutdown:
+	}
+	log.Println("copy loop ended")
 }
 
 // We pass conn.RemoteAddr() as an additional parameter, rather than calling
 // conn.RemoteAddr() inside this function, as a workaround for a hang that
 // otherwise occurs inside of conn.pc.RemoteDescription() (called by
 // RemoteAddr). https://bugs.torproject.org/18628#comment:8
-func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) {
+func (sf *SnowflakeProxy) datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) {
 	defer conn.Close()
 	defer tokens.ret()
 
-	u, err := url.Parse(relayURL)
+	u, err := url.Parse(sf.RelayURL)
 	if err != nil {
 		log.Fatalf("invalid relay url: %s", err)
 	}
@@ -241,7 +283,7 @@ func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) {
 	wsConn := websocketconn.New(ws)
 	log.Printf("connected to relay")
 	defer wsConn.Close()
-	CopyLoop(conn, wsConn)
+	copyLoop(conn, wsConn, sf.shutdown)
 	log.Printf("datachannelHandler ends")
 }
 
@@ -249,7 +291,7 @@ func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) {
 // candidates is complete and the answer is available in LocalDescription.
 // Installs an OnDataChannel callback that creates a webRTCConn and passes it to
 // datachannelHandler.
-func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription,
+func (sf *SnowflakeProxy) makePeerConnectionFromOffer(sdp *webrtc.SessionDescription,
 	config webrtc.Configuration,
 	dataChan chan struct{},
 	handler func(conn *webRTCConn, remoteAddr net.Addr)) (*webrtc.PeerConnection, error) {
@@ -333,7 +375,7 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription,
 
 // Create a new PeerConnection. Blocks until the gathering of ICE
 // candidates is complete and the answer is available in LocalDescription.
-func makeNewPeerConnection(config webrtc.Configuration,
+func (sf *SnowflakeProxy) makeNewPeerConnection(config webrtc.Configuration,
 	dataChan chan struct{}) (*webrtc.PeerConnection, error) {
 
 	pc, err := webrtc.NewPeerConnection(config)
@@ -383,15 +425,15 @@ func makeNewPeerConnection(config webrtc.Configuration,
 	return pc, nil
 }
 
-func runSession(sid string) {
-	offer := broker.pollOffer(sid)
+func (sf *SnowflakeProxy) runSession(sid string) {
+	offer := broker.pollOffer(sid, sf.shutdown)
 	if offer == nil {
 		log.Printf("bad offer from broker")
 		tokens.ret()
 		return
 	}
 	dataChan := make(chan struct{})
-	pc, err := makePeerConnectionFromOffer(offer, config, dataChan, datachannelHandler)
+	pc, err := sf.makePeerConnectionFromOffer(offer, config, dataChan, sf.datachannelHandler)
 	if err != nil {
 		log.Printf("error making WebRTC connection: %s", err)
 		tokens.ret()
@@ -421,53 +463,28 @@ func runSession(sid string) {
 	}
 }
 
-func main() {
-	var capacity uint
-	var stunURL string
-	var logFilename string
-	var rawBrokerURL string
-	var unsafeLogging bool
-	var keepLocalAddresses bool
+// Start configures and starts a Snowflake, fully formed and special. In the
+// case of an empty map, defaults are configured automatically and can be
+// found in the GoDoc and in main.go
+func (sf *SnowflakeProxy) Start() {
 
-	flag.UintVar(&capacity, "capacity", 0, "maximum concurrent clients")
-	flag.StringVar(&rawBrokerURL, "broker", defaultBrokerURL, "broker URL")
-	flag.StringVar(&relayURL, "relay", defaultRelayURL, "websocket relay URL")
-	flag.StringVar(&stunURL, "stun", defaultSTUNURL, "stun URL")
-	flag.StringVar(&logFilename, "log", "", "log filename")
-	flag.BoolVar(&unsafeLogging, "unsafe-logging", false, "prevent logs from being scrubbed")
-	flag.BoolVar(&keepLocalAddresses, "keep-local-addresses", false, "keep local LAN address ICE candidates")
-	flag.Parse()
+	sf.shutdown = make(chan struct{})
 
-	var logOutput io.Writer = os.Stderr
 	log.SetFlags(log.LstdFlags | log.LUTC)
-	if logFilename != "" {
-		f, err := os.OpenFile(logFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
-		if err != nil {
-			log.Fatal(err)
-		}
-		defer f.Close()
-		logOutput = io.MultiWriter(os.Stderr, f)
-	}
-	if unsafeLogging {
-		log.SetOutput(logOutput)
-	} else {
-		// We want to send the log output through our scrubber first
-		log.SetOutput(&safelog.LogScrubber{Output: logOutput})
-	}
 
 	log.Println("starting")
 
 	var err error
-	broker, err = newSignalingServer(rawBrokerURL, keepLocalAddresses)
+	broker, err = newSignalingServer(sf.RawBrokerURL, sf.KeepLocalAddresses)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	_, err = url.Parse(stunURL)
+	_, err = url.Parse(sf.StunURL)
 	if err != nil {
 		log.Fatalf("invalid stun url: %s", err)
 	}
-	_, err = url.Parse(relayURL)
+	_, err = url.Parse(sf.RelayURL)
 	if err != nil {
 		log.Fatalf("invalid relay url: %s", err)
 	}
@@ -475,27 +492,37 @@ func main() {
 	config = webrtc.Configuration{
 		ICEServers: []webrtc.ICEServer{
 			{
-				URLs: []string{stunURL},
+				URLs: []string{sf.StunURL},
 			},
 		},
 	}
-	tokens = newTokens(capacity)
+	tokens = newTokens(sf.Capacity)
 
 	// use probetest to determine NAT compatability
-	checkNATType(config, defaultProbeURL)
+	sf.checkNATType(config, DefaultProbeURL)
 	log.Printf("NAT type: %s", currentNATType)
 
 	ticker := time.NewTicker(pollInterval)
 	defer ticker.Stop()
 
 	for ; true; <-ticker.C {
-		tokens.get()
-		sessionID := genSessionID()
-		runSession(sessionID)
+		select {
+		case <-sf.shutdown:
+			return
+		default:
+			tokens.get()
+			sessionID := genSessionID()
+			sf.runSession(sessionID)
+		}
 	}
 }
 
-func checkNATType(config webrtc.Configuration, probeURL string) {
+// Stop calls close on the sf.shutdown channel shutting down the Snowflake.
+func (sf *SnowflakeProxy) Stop() {
+	close(sf.shutdown)
+}
+
+func (sf *SnowflakeProxy) checkNATType(config webrtc.Configuration, probeURL string) {
 
 	probe, err := newSignalingServer(probeURL, false)
 	if err != nil {
@@ -504,7 +531,7 @@ func checkNATType(config webrtc.Configuration, probeURL string) {
 
 	// create offer
 	dataChan := make(chan struct{})
-	pc, err := makeNewPeerConnection(config, dataChan)
+	pc, err := sf.makeNewPeerConnection(config, dataChan)
 	if err != nil {
 		log.Printf("error making WebRTC connection: %s", err)
 		return
diff --git a/proxy/tokens.go b/proxy/lib/tokens.go
similarity index 97%
rename from proxy/tokens.go
rename to proxy/lib/tokens.go
index fedb8f7..1331778 100644
--- a/proxy/tokens.go
+++ b/proxy/lib/tokens.go
@@ -1,4 +1,4 @@
-package main
+package snowflake
 
 import (
 	"sync/atomic"
diff --git a/proxy/tokens_test.go b/proxy/lib/tokens_test.go
similarity index 96%
rename from proxy/tokens_test.go
rename to proxy/lib/tokens_test.go
index 622cc05..702a887 100644
--- a/proxy/tokens_test.go
+++ b/proxy/lib/tokens_test.go
@@ -1,4 +1,4 @@
-package main
+package snowflake
 
 import (
 	"testing"
diff --git a/proxy/util.go b/proxy/lib/util.go
similarity index 71%
rename from proxy/util.go
rename to proxy/lib/util.go
index d737056..c6613d9 100644
--- a/proxy/util.go
+++ b/proxy/lib/util.go
@@ -1,21 +1,28 @@
-package main
+package snowflake
 
 import (
 	"fmt"
 	"time"
 )
 
+// BytesLogger is an interface which is used to allow logging the throughput
+// of the Snowflake. A default BytesLogger(BytesNullLogger) does nothing.
 type BytesLogger interface {
 	AddOutbound(int)
 	AddInbound(int)
 	ThroughputSummary() string
 }
 
-// Default BytesLogger does nothing.
+// BytesNullLogger Default BytesLogger does nothing.
 type BytesNullLogger struct{}
 
-func (b BytesNullLogger) AddOutbound(amount int)    {}
-func (b BytesNullLogger) AddInbound(amount int)     {}
+// AddOutbound in BytesNullLogger does nothing
+func (b BytesNullLogger) AddOutbound(amount int) {}
+
+// AddInbound in BytesNullLogger does nothing
+func (b BytesNullLogger) AddInbound(amount int) {}
+
+// ThroughputSummary in BytesNullLogger does nothing
 func (b BytesNullLogger) ThroughputSummary() string { return "" }
 
 // BytesSyncLogger uses channels to safely log from multiple sources with output
@@ -50,14 +57,17 @@ func (b *BytesSyncLogger) log() {
 	}
 }
 
+// AddOutbound add a number of bytes to the outbound total reported by the logger
 func (b *BytesSyncLogger) AddOutbound(amount int) {
 	b.outboundChan <- amount
 }
 
+// AddInbound add a number of bytes to the inbound total reported by the logger
 func (b *BytesSyncLogger) AddInbound(amount int) {
 	b.inboundChan <- amount
 }
 
+// ThroughputSummary view a formatted summary of the throughput totals
 func (b *BytesSyncLogger) ThroughputSummary() string {
 	var inUnit, outUnit string
 	units := []string{"B", "KB", "MB", "GB"}
diff --git a/proxy/webrtcconn.go b/proxy/lib/webrtcconn.go
similarity index 99%
rename from proxy/webrtcconn.go
rename to proxy/lib/webrtcconn.go
index 5d95919..5c6192b 100644
--- a/proxy/webrtcconn.go
+++ b/proxy/lib/webrtcconn.go
@@ -1,4 +1,4 @@
-package main
+package snowflake
 
 import (
 	"fmt"
diff --git a/proxy/main.go b/proxy/main.go
new file mode 100644
index 0000000..12b3752
--- /dev/null
+++ b/proxy/main.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+	"flag"
+	"io"
+	"log"
+	"os"
+
+	"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
+	"git.torproject.org/pluggable-transports/snowflake.git/proxy/lib"
+)
+
+func main() {
+	capacity := flag.Int("capacity", 10, "maximum concurrent clients")
+	stunURL := flag.String("stun", snowflake.DefaultSTUNURL, "broker URL")
+	logFilename := flag.String("log", "", "log filename")
+	rawBrokerURL := flag.String("broker", snowflake.DefaultBrokerURL, "broker URL")
+	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
+	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
+	relayURL := flag.String("relay", snowflake.DefaultRelayURL, "websocket relay URL")
+
+	flag.Parse()
+
+	sf := snowflake.SnowflakeProxy{
+		Capacity:           uint(*capacity),
+		StunURL:            *stunURL,
+		RawBrokerURL:       *rawBrokerURL,
+		KeepLocalAddresses: *keepLocalAddresses,
+		RelayURL:           *relayURL,
+		LogOutput:          os.Stderr,
+	}
+
+	if *logFilename != "" {
+		f, err := os.OpenFile(*logFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
+		if err != nil {
+			log.Fatal(err)
+		}
+		defer f.Close()
+		sf.LogOutput = io.MultiWriter(os.Stderr, f)
+	}
+	if *unsafeLogging {
+		log.SetOutput(sf.LogOutput)
+	} else {
+		log.SetOutput(&safelog.LogScrubber{Output: sf.LogOutput})
+	}
+
+	sf.Start()
+}

From b2edf948e21c2420d3b3696c680856f106dae9f4 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Tue, 26 Oct 2021 14:52:17 -0400
Subject: [PATCH 244/385] Remove BytesLoggers from exported functions

---
 proxy/lib/snowflake.go  |  2 +-
 proxy/lib/util.go       | 40 ++++++++++++++++++++--------------------
 proxy/lib/webrtcconn.go |  2 +-
 3 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index e35eabd..793fa2b 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -306,7 +306,7 @@ func (sf *SnowflakeProxy) makePeerConnectionFromOffer(sdp *webrtc.SessionDescrip
 
 		pr, pw := io.Pipe()
 		conn := &webRTCConn{pc: pc, dc: dc, pr: pr}
-		conn.bytesLogger = NewBytesSyncLogger()
+		conn.bytesLogger = newBytesSyncLogger()
 
 		dc.OnOpen(func() {
 			log.Println("OnOpen channel")
diff --git a/proxy/lib/util.go b/proxy/lib/util.go
index c6613d9..2df23eb 100644
--- a/proxy/lib/util.go
+++ b/proxy/lib/util.go
@@ -5,37 +5,37 @@ import (
 	"time"
 )
 
-// BytesLogger is an interface which is used to allow logging the throughput
-// of the Snowflake. A default BytesLogger(BytesNullLogger) does nothing.
-type BytesLogger interface {
+// bytesLogger is an interface which is used to allow logging the throughput
+// of the Snowflake. A default bytesLogger(bytesNullLogger) does nothing.
+type bytesLogger interface {
 	AddOutbound(int)
 	AddInbound(int)
 	ThroughputSummary() string
 }
 
-// BytesNullLogger Default BytesLogger does nothing.
-type BytesNullLogger struct{}
+// bytesNullLogger Default bytesLogger does nothing.
+type bytesNullLogger struct{}
 
-// AddOutbound in BytesNullLogger does nothing
-func (b BytesNullLogger) AddOutbound(amount int) {}
+// AddOutbound in bytesNullLogger does nothing
+func (b bytesNullLogger) AddOutbound(amount int) {}
 
-// AddInbound in BytesNullLogger does nothing
-func (b BytesNullLogger) AddInbound(amount int) {}
+// AddInbound in bytesNullLogger does nothing
+func (b bytesNullLogger) AddInbound(amount int) {}
 
-// ThroughputSummary in BytesNullLogger does nothing
-func (b BytesNullLogger) ThroughputSummary() string { return "" }
+// ThroughputSummary in bytesNullLogger does nothing
+func (b bytesNullLogger) ThroughputSummary() string { return "" }
 
-// BytesSyncLogger uses channels to safely log from multiple sources with output
+// bytesSyncLogger uses channels to safely log from multiple sources with output
 // occuring at reasonable intervals.
-type BytesSyncLogger struct {
+type bytesSyncLogger struct {
 	outboundChan, inboundChan              chan int
 	outbound, inbound, outEvents, inEvents int
 	start                                  time.Time
 }
 
-// NewBytesSyncLogger returns a new BytesSyncLogger and starts it loggin.
-func NewBytesSyncLogger() *BytesSyncLogger {
-	b := &BytesSyncLogger{
+// newBytesSyncLogger returns a new bytesSyncLogger and starts it loggin.
+func newBytesSyncLogger() *bytesSyncLogger {
+	b := &bytesSyncLogger{
 		outboundChan: make(chan int, 5),
 		inboundChan:  make(chan int, 5),
 	}
@@ -44,7 +44,7 @@ func NewBytesSyncLogger() *BytesSyncLogger {
 	return b
 }
 
-func (b *BytesSyncLogger) log() {
+func (b *bytesSyncLogger) log() {
 	for {
 		select {
 		case amount := <-b.outboundChan:
@@ -58,17 +58,17 @@ func (b *BytesSyncLogger) log() {
 }
 
 // AddOutbound add a number of bytes to the outbound total reported by the logger
-func (b *BytesSyncLogger) AddOutbound(amount int) {
+func (b *bytesSyncLogger) AddOutbound(amount int) {
 	b.outboundChan <- amount
 }
 
 // AddInbound add a number of bytes to the inbound total reported by the logger
-func (b *BytesSyncLogger) AddInbound(amount int) {
+func (b *bytesSyncLogger) AddInbound(amount int) {
 	b.inboundChan <- amount
 }
 
 // ThroughputSummary view a formatted summary of the throughput totals
-func (b *BytesSyncLogger) ThroughputSummary() string {
+func (b *bytesSyncLogger) ThroughputSummary() string {
 	var inUnit, outUnit string
 	units := []string{"B", "KB", "MB", "GB"}
 
diff --git a/proxy/lib/webrtcconn.go b/proxy/lib/webrtcconn.go
index 5c6192b..20b1172 100644
--- a/proxy/lib/webrtcconn.go
+++ b/proxy/lib/webrtcconn.go
@@ -29,7 +29,7 @@ type webRTCConn struct {
 	lock sync.Mutex // Synchronization for DataChannel destruction
 	once sync.Once  // Synchronization for PeerConnection destruction
 
-	bytesLogger BytesLogger
+	bytesLogger bytesLogger
 }
 
 func (c *webRTCConn) Read(b []byte) (int, error) {

From 84e8a183e59d0e4cea3dd572e87a221379d968f9 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Tue, 26 Oct 2021 15:10:59 -0400
Subject: [PATCH 245/385] Comment package and minor changes exports

---
 proxy/lib/proxy-go_test.go |  2 +-
 proxy/lib/snowflake.go     | 36 +++++++++++++++++++++++++++++-------
 proxy/lib/tokens.go        |  2 +-
 proxy/lib/tokens_test.go   |  2 +-
 proxy/lib/util.go          |  2 +-
 proxy/lib/webrtcconn.go    |  2 +-
 proxy/main.go              | 26 ++++++++++++++------------
 7 files changed, 48 insertions(+), 24 deletions(-)

diff --git a/proxy/lib/proxy-go_test.go b/proxy/lib/proxy-go_test.go
index af71648..3a81a1b 100644
--- a/proxy/lib/proxy-go_test.go
+++ b/proxy/lib/proxy-go_test.go
@@ -1,4 +1,4 @@
-package snowflake
+package snowflake_proxy
 
 import (
 	"bytes"
diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 793fa2b..85f86b2 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -1,4 +1,27 @@
-package snowflake
+/*
+Package snowflake_proxy provides functionality for creating, starting, and stopping a snowflake
+proxy.
+
+To run a proxy, you must first create a proxy configuration
+
+	proxy := snowflake_proxy.SnowflakeProxy{
+		BrokerURL: "https://snowflake-broker.example.com",
+		STUNURL: "stun:stun.stunprotocol.org:3478",
+		// ...
+	}
+
+You may then start and stop the proxy. Stopping the proxy will close existing connections and
+the proxy will not poll for more clients.
+
+	go func() {
+		proxy.Start()
+	}
+
+	// ...
+
+	proxy.Stop()
+*/
+package snowflake_proxy
 
 import (
 	"bytes"
@@ -76,11 +99,10 @@ var (
 // Snowflake in another Go application.
 type SnowflakeProxy struct {
 	Capacity           uint
-	StunURL            string
-	RawBrokerURL       string
+	STUNURL            string
+	BrokerURL          string
 	KeepLocalAddresses bool
 	RelayURL           string
-	LogOutput          io.Writer
 	shutdown           chan struct{}
 }
 
@@ -475,12 +497,12 @@ func (sf *SnowflakeProxy) Start() {
 	log.Println("starting")
 
 	var err error
-	broker, err = newSignalingServer(sf.RawBrokerURL, sf.KeepLocalAddresses)
+	broker, err = newSignalingServer(sf.BrokerURL, sf.KeepLocalAddresses)
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	_, err = url.Parse(sf.StunURL)
+	_, err = url.Parse(sf.STUNURL)
 	if err != nil {
 		log.Fatalf("invalid stun url: %s", err)
 	}
@@ -492,7 +514,7 @@ func (sf *SnowflakeProxy) Start() {
 	config = webrtc.Configuration{
 		ICEServers: []webrtc.ICEServer{
 			{
-				URLs: []string{sf.StunURL},
+				URLs: []string{sf.STUNURL},
 			},
 		},
 	}
diff --git a/proxy/lib/tokens.go b/proxy/lib/tokens.go
index 1331778..d312ecf 100644
--- a/proxy/lib/tokens.go
+++ b/proxy/lib/tokens.go
@@ -1,4 +1,4 @@
-package snowflake
+package snowflake_proxy
 
 import (
 	"sync/atomic"
diff --git a/proxy/lib/tokens_test.go b/proxy/lib/tokens_test.go
index 702a887..4393a21 100644
--- a/proxy/lib/tokens_test.go
+++ b/proxy/lib/tokens_test.go
@@ -1,4 +1,4 @@
-package snowflake
+package snowflake_proxy
 
 import (
 	"testing"
diff --git a/proxy/lib/util.go b/proxy/lib/util.go
index 2df23eb..5055187 100644
--- a/proxy/lib/util.go
+++ b/proxy/lib/util.go
@@ -1,4 +1,4 @@
-package snowflake
+package snowflake_proxy
 
 import (
 	"fmt"
diff --git a/proxy/lib/webrtcconn.go b/proxy/lib/webrtcconn.go
index 20b1172..6e16bec 100644
--- a/proxy/lib/webrtcconn.go
+++ b/proxy/lib/webrtcconn.go
@@ -1,4 +1,4 @@
-package snowflake
+package snowflake_proxy
 
 import (
 	"fmt"
diff --git a/proxy/main.go b/proxy/main.go
index 12b3752..aabac51 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -7,42 +7,44 @@ import (
 	"os"
 
 	"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
-	"git.torproject.org/pluggable-transports/snowflake.git/proxy/lib"
+	sf "git.torproject.org/pluggable-transports/snowflake.git/proxy/lib"
 )
 
 func main() {
 	capacity := flag.Int("capacity", 10, "maximum concurrent clients")
-	stunURL := flag.String("stun", snowflake.DefaultSTUNURL, "broker URL")
+	stunURL := flag.String("stun", sf.DefaultSTUNURL, "broker URL")
 	logFilename := flag.String("log", "", "log filename")
-	rawBrokerURL := flag.String("broker", snowflake.DefaultBrokerURL, "broker URL")
+	rawBrokerURL := flag.String("broker", sf.DefaultBrokerURL, "broker URL")
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
-	relayURL := flag.String("relay", snowflake.DefaultRelayURL, "websocket relay URL")
+	relayURL := flag.String("relay", sf.DefaultRelayURL, "websocket relay URL")
 
 	flag.Parse()
 
-	sf := snowflake.SnowflakeProxy{
+	proxy := sf.SnowflakeProxy{
 		Capacity:           uint(*capacity),
-		StunURL:            *stunURL,
-		RawBrokerURL:       *rawBrokerURL,
+		STUNURL:            *stunURL,
+		BrokerURL:          *rawBrokerURL,
 		KeepLocalAddresses: *keepLocalAddresses,
 		RelayURL:           *relayURL,
-		LogOutput:          os.Stderr,
 	}
 
+	var logOutput io.Writer = os.Stderr
+	log.SetFlags(log.LstdFlags | log.LUTC)
+
 	if *logFilename != "" {
 		f, err := os.OpenFile(*logFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
 		if err != nil {
 			log.Fatal(err)
 		}
 		defer f.Close()
-		sf.LogOutput = io.MultiWriter(os.Stderr, f)
+		logOutput = io.MultiWriter(os.Stderr, f)
 	}
 	if *unsafeLogging {
-		log.SetOutput(sf.LogOutput)
+		log.SetOutput(logOutput)
 	} else {
-		log.SetOutput(&safelog.LogScrubber{Output: sf.LogOutput})
+		log.SetOutput(&safelog.LogScrubber{Output: logOutput})
 	}
 
-	sf.Start()
+	proxy.Start()
 }

From 0e8d41ba4b694b334775fcd9f4330d9b5b31e85c Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Tue, 26 Oct 2021 15:28:27 -0400
Subject: [PATCH 246/385] Update comments for exported items

---
 proxy/lib/snowflake.go | 46 ++++++++++++++++++++----------------------
 1 file changed, 22 insertions(+), 24 deletions(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 85f86b2..bd50dc8 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -2,7 +2,8 @@
 Package snowflake_proxy provides functionality for creating, starting, and stopping a snowflake
 proxy.
 
-To run a proxy, you must first create a proxy configuration
+To run a proxy, you must first create a proxy configuration. Unconfigured fields
+will be set to the defined defaults.
 
 	proxy := snowflake_proxy.SnowflakeProxy{
 		BrokerURL: "https://snowflake-broker.example.com",
@@ -45,24 +46,16 @@ import (
 	"github.com/pion/webrtc/v3"
 )
 
-// DefaultBrokerURL is the bamsoftware.com broker, https://snowflake-broker.bamsoftware.com
-// Changing this will change the default broker. The recommended way of changing
-// the broker that gets used is by passing an argument to Main.
-const DefaultBrokerURL = "https://snowflake-broker.bamsoftware.com/"
+// DefaultBrokerURL is the snowflake broker run at https://snowflake-broker.torproject.net
+const DefaultBrokerURL = "https://snowflake-broker.torproject.net/"
 
-// DefaultProbeURL is the torproject.org  ProbeURL, https://snowflake-broker.torproject.net:8443/probe
-// Changing this will change the default Probe URL. The recommended way of changing
-// the probe that gets used is by passing an argument to Main.
+// DefaultProbeURL is run at https://snowflake-broker.torproject.net:8443/probe
 const DefaultProbeURL = "https://snowflake-broker.torproject.net:8443/probe"
 
-// DefaultRelayURL is the bamsoftware.com  Websocket Relay, wss://snowflake.bamsoftware.com/
-// Changing this will change the default Relay URL. The recommended way of changing
-// the relay that gets used is by passing an argument to Main.
+// DefaultRelayURL is run at wss://snowflake.torproject.net
 const DefaultRelayURL = "wss://snowflake.bamsoftware.com/"
 
-// DefaultSTUNURL is a stunprotocol.org STUN URL. stun:stun.stunprotocol.org:3478
-// Changing this will change the default STUN URL. The recommended way of changing
-// the STUN Server that gets used is by passing an argument to Main.
+// DefaultSTUNURL is run at stun:stun.stunprotocol.org:3478
 const DefaultSTUNURL = "stun:stun.stunprotocol.org:3478"
 const pollInterval = 5 * time.Second
 
@@ -95,15 +88,21 @@ var (
 	client http.Client
 )
 
-// SnowflakeProxy is a structure which is used to configure an embedded
+// SnowflakeProxy is used to configure an embedded
 // Snowflake in another Go application.
 type SnowflakeProxy struct {
-	Capacity           uint
-	STUNURL            string
-	BrokerURL          string
+	// Capacity is the maximum number of clients a Snowflake will serve.
+	// Proxies with a capacity of 0 will accept an unlimited number of clients.
+	Capacity uint
+	// STUNURL is the URL of the STUN server the proxy will use
+	STUNURL string
+	// BrokerURL is the URL of the Snowflake broker
+	BrokerURL string
+	// KeepLocalAddresses indicates whether local SDP candidates will be sent to the broker
 	KeepLocalAddresses bool
-	RelayURL           string
-	shutdown           chan struct{}
+	// RelayURL is the URL of the Snowflake server that all traffic will be relayed to
+	RelayURL string
+	shutdown chan struct{}
 }
 
 // Checks whether an IP address is a remote address for the client
@@ -485,9 +484,8 @@ func (sf *SnowflakeProxy) runSession(sid string) {
 	}
 }
 
-// Start configures and starts a Snowflake, fully formed and special. In the
-// case of an empty map, defaults are configured automatically and can be
-// found in the GoDoc and in main.go
+// Start configures and starts a Snowflake, fully formed and special. Configuration
+// values that are unset will default to their corresponding default values.
 func (sf *SnowflakeProxy) Start() {
 
 	sf.shutdown = make(chan struct{})
@@ -539,7 +537,7 @@ func (sf *SnowflakeProxy) Start() {
 	}
 }
 
-// Stop calls close on the sf.shutdown channel shutting down the Snowflake.
+// Stop closes all existing connections and shuts down the Snowflake.
 func (sf *SnowflakeProxy) Stop() {
 	close(sf.shutdown)
 }

From 3caa83d84de681b3cd38f4c61cfef2e9bd091176 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Tue, 26 Oct 2021 15:40:32 -0400
Subject: [PATCH 247/385] Modify handling of misconfigurations and defaults

---
 proxy/lib/snowflake.go | 36 +++++++++++++++++++++---------------
 proxy/main.go          |  8 ++++++--
 2 files changed, 27 insertions(+), 17 deletions(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index bd50dc8..734657a 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -15,7 +15,8 @@ You may then start and stop the proxy. Stopping the proxy will close existing co
 the proxy will not poll for more clients.
 
 	go func() {
-		proxy.Start()
+		err := proxy.Start()
+		// handle error
 	}
 
 	// ...
@@ -46,16 +47,12 @@ import (
 	"github.com/pion/webrtc/v3"
 )
 
-// DefaultBrokerURL is the snowflake broker run at https://snowflake-broker.torproject.net
 const DefaultBrokerURL = "https://snowflake-broker.torproject.net/"
 
-// DefaultProbeURL is run at https://snowflake-broker.torproject.net:8443/probe
 const DefaultProbeURL = "https://snowflake-broker.torproject.net:8443/probe"
 
-// DefaultRelayURL is run at wss://snowflake.torproject.net
 const DefaultRelayURL = "wss://snowflake.bamsoftware.com/"
 
-// DefaultSTUNURL is run at stun:stun.stunprotocol.org:3478
 const DefaultSTUNURL = "stun:stun.stunprotocol.org:3478"
 const pollInterval = 5 * time.Second
 
@@ -486,27 +483,35 @@ func (sf *SnowflakeProxy) runSession(sid string) {
 
 // Start configures and starts a Snowflake, fully formed and special. Configuration
 // values that are unset will default to their corresponding default values.
-func (sf *SnowflakeProxy) Start() {
-
-	sf.shutdown = make(chan struct{})
-
-	log.SetFlags(log.LstdFlags | log.LUTC)
+func (sf *SnowflakeProxy) Start() error {
+	var err error
 
 	log.Println("starting")
+	sf.shutdown = make(chan struct{})
+
+	// blank configurations revert to default
+	if sf.BrokerURL == "" {
+		sf.BrokerURL = DefaultBrokerURL
+	}
+	if sf.RelayURL == "" {
+		sf.RelayURL = DefaultRelayURL
+	}
+	if sf.STUNURL == "" {
+		sf.STUNURL = DefaultSTUNURL
+	}
 
-	var err error
 	broker, err = newSignalingServer(sf.BrokerURL, sf.KeepLocalAddresses)
 	if err != nil {
-		log.Fatal(err)
+		return fmt.Errorf("error configuring broker: %s", err)
 	}
 
 	_, err = url.Parse(sf.STUNURL)
 	if err != nil {
-		log.Fatalf("invalid stun url: %s", err)
+		return fmt.Errorf("invalid stun url: %s", err)
 	}
 	_, err = url.Parse(sf.RelayURL)
 	if err != nil {
-		log.Fatalf("invalid relay url: %s", err)
+		return fmt.Errorf("invalid relay url: %s", err)
 	}
 
 	config = webrtc.Configuration{
@@ -528,13 +533,14 @@ func (sf *SnowflakeProxy) Start() {
 	for ; true; <-ticker.C {
 		select {
 		case <-sf.shutdown:
-			return
+			return nil
 		default:
 			tokens.get()
 			sessionID := genSessionID()
 			sf.runSession(sessionID)
 		}
 	}
+	return nil
 }
 
 // Stop closes all existing connections and shuts down the Snowflake.
diff --git a/proxy/main.go b/proxy/main.go
index aabac51..368589c 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -11,7 +11,7 @@ import (
 )
 
 func main() {
-	capacity := flag.Int("capacity", 10, "maximum concurrent clients")
+	capacity := flag.Uint("capacity", 0, "maximum concurrent clients")
 	stunURL := flag.String("stun", sf.DefaultSTUNURL, "broker URL")
 	logFilename := flag.String("log", "", "log filename")
 	rawBrokerURL := flag.String("broker", sf.DefaultBrokerURL, "broker URL")
@@ -32,6 +32,7 @@ func main() {
 	var logOutput io.Writer = os.Stderr
 	log.SetFlags(log.LstdFlags | log.LUTC)
 
+	log.SetFlags(log.LstdFlags | log.LUTC)
 	if *logFilename != "" {
 		f, err := os.OpenFile(*logFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
 		if err != nil {
@@ -46,5 +47,8 @@ func main() {
 		log.SetOutput(&safelog.LogScrubber{Output: logOutput})
 	}
 
-	proxy.Start()
+	err := proxy.Start()
+	if err != nil {
+		log.Fatal(err)
+	}
 }

From 0a2598a1e854243b2f69dae05d713260b4816098 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Tue, 26 Oct 2021 15:43:36 -0400
Subject: [PATCH 248/385] Export ability to change the URL of NAT probe

---
 proxy/lib/snowflake.go | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 734657a..5f7bfd4 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -49,7 +49,7 @@ import (
 
 const DefaultBrokerURL = "https://snowflake-broker.torproject.net/"
 
-const DefaultProbeURL = "https://snowflake-broker.torproject.net:8443/probe"
+const DefaultNATProbeURL = "https://snowflake-broker.torproject.net:8443/probe"
 
 const DefaultRelayURL = "wss://snowflake.bamsoftware.com/"
 
@@ -99,7 +99,9 @@ type SnowflakeProxy struct {
 	KeepLocalAddresses bool
 	// RelayURL is the URL of the Snowflake server that all traffic will be relayed to
 	RelayURL string
-	shutdown chan struct{}
+	// NATProbeURL is the URL of the probe service we use for NAT checks
+	NATProbeURL string
+	shutdown    chan struct{}
 }
 
 // Checks whether an IP address is a remote address for the client
@@ -499,6 +501,9 @@ func (sf *SnowflakeProxy) Start() error {
 	if sf.STUNURL == "" {
 		sf.STUNURL = DefaultSTUNURL
 	}
+	if sf.NATProbeURL == "" {
+		sf.NATProbeURL = DefaultNATProbeURL
+	}
 
 	broker, err = newSignalingServer(sf.BrokerURL, sf.KeepLocalAddresses)
 	if err != nil {
@@ -524,7 +529,7 @@ func (sf *SnowflakeProxy) Start() error {
 	tokens = newTokens(sf.Capacity)
 
 	// use probetest to determine NAT compatability
-	sf.checkNATType(config, DefaultProbeURL)
+	sf.checkNATType(config, sf.NATProbeURL)
 	log.Printf("NAT type: %s", currentNATType)
 
 	ticker := time.NewTicker(pollInterval)

From f6b6342a3a0d38257cd492140fccc8c65c07310c Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 4 Nov 2021 10:34:34 -0400
Subject: [PATCH 249/385] Update ChangeLog for v2 release

---
 ChangeLog | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/ChangeLog b/ChangeLog
index 6c9b992..e4b3998 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,19 @@
+Changes in version v2.0.0 - 2021-11-04
+
+- Turn the standalone snowflake proxy code into a library
+- Clean up and reworked the snowflake client and server library code
+- Unify broker/bridge domains to *.torproject.net
+- Updates to the snowflake library documentation
+- New package functions to define and set a rendezvous method with the
+broker
+- Factor out the broker geoip code into its own external library
+- Bug fix to check error calls in preparePeerConnection
+- Bug fixes in snowflake tests
+- Issue 40059: add the ability to pass in snowflake arguments through SOCKS
+- Increase buffer sizes for sending and receiving snowflake data
+- Issue 25985: rendezvous with the broker using AMP cache
+- Issue 40055: wait for the full poll interval between proxy polls
+
 Changes in version v1.1.0 - 2021-07-13
 
 - Refactors of the Snowflake broker code

From ead5a960d7fa19dc890ccbfc0765c5ab6629eaa9 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 11 Nov 2021 10:14:49 -0500
Subject: [PATCH 250/385] Bump snowflake library imports and go.mod to v2

---
 .travis.yml                        |  2 +-
 broker/amp.go                      |  4 ++--
 broker/broker.go                   |  2 +-
 broker/http.go                     |  2 +-
 broker/ipc.go                      |  2 +-
 broker/snowflake-broker_test.go    |  2 +-
 client/lib/rendezvous.go           |  6 +++---
 client/lib/rendezvous_ampcache.go  |  2 +-
 client/lib/rendezvous_test.go      |  6 +++---
 client/lib/snowflake.go            |  4 ++--
 client/lib/turbotunnel.go          |  2 +-
 client/snowflake.go                |  4 ++--
 common/messages/client.go          |  2 +-
 common/messages/proxy.go           |  2 +-
 common/safelog/log.go              |  2 +-
 doc/using-the-snowflake-library.md |  6 +++---
 go.mod                             |  2 +-
 go.sum                             | 23 -----------------------
 probetest/probetest.go             |  6 +++---
 proxy/lib/proxy-go_test.go         |  4 ++--
 proxy/lib/snowflake.go             |  6 +++---
 proxy/main.go                      |  4 ++--
 server/lib/http.go                 |  6 +++---
 server/lib/snowflake.go            |  2 +-
 server/lib/turbotunnel.go          |  2 +-
 server/lib/turbotunnel_test.go     |  2 +-
 server/server.go                   |  4 ++--
 27 files changed, 44 insertions(+), 67 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 941df43..a2c8880 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,7 @@ language: go
 
 dist: xenial
 
-go_import_path: git.torproject.org/pluggable-transports/snowflake.git
+go_import_path: git.torproject.org/pluggable-transports/snowflake.git/v2
 
 go:
     - 1.13.x
diff --git a/broker/amp.go b/broker/amp.go
index 8641e51..36987a7 100644
--- a/broker/amp.go
+++ b/broker/amp.go
@@ -5,8 +5,8 @@ import (
 	"net/http"
 	"strings"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/amp"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/amp"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
 )
 
 // ampClientOffers is the AMP-speaking endpoint for client poll messages,
diff --git a/broker/broker.go b/broker/broker.go
index 6c855f3..7a29265 100644
--- a/broker/broker.go
+++ b/broker/broker.go
@@ -19,7 +19,7 @@ import (
 	"syscall"
 	"time"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"golang.org/x/crypto/acme/autocert"
diff --git a/broker/http.go b/broker/http.go
index 9ae2560..9ec95d8 100644
--- a/broker/http.go
+++ b/broker/http.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 	"os"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
 )
 
 const (
diff --git a/broker/ipc.go b/broker/ipc.go
index 7ab27af..c5d66e8 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -8,7 +8,7 @@ import (
 	"net"
 	"time"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
 	"github.com/prometheus/client_golang/prometheus"
 )
 
diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go
index 25a947c..9c975eb 100644
--- a/broker/snowflake-broker_test.go
+++ b/broker/snowflake-broker_test.go
@@ -13,7 +13,7 @@ import (
 	"testing"
 	"time"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/amp"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/amp"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index ffc0358..c3f0d7a 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -10,9 +10,9 @@ import (
 	"sync"
 	"time"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/nat"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/util"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/util"
 	"github.com/pion/webrtc/v3"
 )
 
diff --git a/client/lib/rendezvous_ampcache.go b/client/lib/rendezvous_ampcache.go
index 3c3780a..59456ae 100644
--- a/client/lib/rendezvous_ampcache.go
+++ b/client/lib/rendezvous_ampcache.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 	"net/url"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/amp"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/amp"
 )
 
 // ampCacheRendezvous is a RendezvousMethod that communicates with the
diff --git a/client/lib/rendezvous_test.go b/client/lib/rendezvous_test.go
index 0b3288b..582a979 100644
--- a/client/lib/rendezvous_test.go
+++ b/client/lib/rendezvous_test.go
@@ -9,9 +9,9 @@ import (
 	"net/http"
 	"testing"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/amp"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/nat"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/amp"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 0096759..56dd312 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -35,8 +35,8 @@ import (
 	"strings"
 	"time"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/nat"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/turbotunnel"
 	"github.com/pion/webrtc/v3"
 	"github.com/xtaci/kcp-go/v5"
 	"github.com/xtaci/smux"
diff --git a/client/lib/turbotunnel.go b/client/lib/turbotunnel.go
index 71f01a0..8cdce42 100644
--- a/client/lib/turbotunnel.go
+++ b/client/lib/turbotunnel.go
@@ -7,7 +7,7 @@ import (
 	"net"
 	"time"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/encapsulation"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/encapsulation"
 )
 
 var errNotImplemented = errors.New("not implemented")
diff --git a/client/snowflake.go b/client/snowflake.go
index d952275..d76efbf 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -16,8 +16,8 @@ import (
 	"syscall"
 
 	pt "git.torproject.org/pluggable-transports/goptlib.git"
-	sf "git.torproject.org/pluggable-transports/snowflake.git/client/lib"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
+	sf "git.torproject.org/pluggable-transports/snowflake.git/v2/client/lib"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
 )
 
 const (
diff --git a/common/messages/client.go b/common/messages/client.go
index 1918e34..b40c582 100644
--- a/common/messages/client.go
+++ b/common/messages/client.go
@@ -1,6 +1,6 @@
 //Package for communication with the snowflake broker
 
-//import "git.torproject.org/pluggable-transports/snowflake.git/common/messages"
+//import "git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
 package messages
 
 import (
diff --git a/common/messages/proxy.go b/common/messages/proxy.go
index 366e833..3817c04 100644
--- a/common/messages/proxy.go
+++ b/common/messages/proxy.go
@@ -1,6 +1,6 @@
 //Package for communication with the snowflake broker
 
-//import "git.torproject.org/pluggable-transports/snowflake.git/common/messages"
+//import "git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
 package messages
 
 import (
diff --git a/common/safelog/log.go b/common/safelog/log.go
index 9148e53..4a135ce 100644
--- a/common/safelog/log.go
+++ b/common/safelog/log.go
@@ -1,6 +1,6 @@
 //Package for a safer logging wrapper around the standard logging package
 
-//import "git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
+//import "git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
 package safelog
 
 import (
diff --git a/doc/using-the-snowflake-library.md b/doc/using-the-snowflake-library.md
index 2c74296..c3e83e0 100644
--- a/doc/using-the-snowflake-library.md
+++ b/doc/using-the-snowflake-library.md
@@ -12,7 +12,7 @@ package main
 import (
     "log"
 
-    sf "git.torproject.org/pluggable-transports/snowflake.git/client/lib"
+    sf "git.torproject.org/pluggable-transports/snowflake.git/v2/client/lib"
 )
 
 func main() {
@@ -54,7 +54,7 @@ package main
 import (
     "log"
 
-    sf "git.torproject.org/pluggable-transports/snowflake.git/client/lib"
+    sf "git.torproject.org/pluggable-transports/snowflake.git/v2/client/lib"
 )
 
 type StubMethod struct {
@@ -110,7 +110,7 @@ import (
     "log"
     "net"
 
-    sf "git.torproject.org/pluggable-transports/snowflake.git/server/lib"
+    sf "git.torproject.org/pluggable-transports/snowflake.git/v2/server/lib"
     "golang.org/x/crypto/acme/autocert"
 )
 
diff --git a/go.mod b/go.mod
index 9d6b6ac..1efb5a5 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module git.torproject.org/pluggable-transports/snowflake.git
+module git.torproject.org/pluggable-transports/snowflake.git/v2
 
 go 1.13
 
diff --git a/go.sum b/go.sum
index 34bc936..c229b37 100644
--- a/go.sum
+++ b/go.sum
@@ -56,7 +56,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
 github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -76,7 +75,6 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -100,7 +98,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I=
 github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
 github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -135,7 +132,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -227,7 +223,6 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
 github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
-github.com/pion/dtls/v2 v2.0.4 h1:WuUcqi6oYMu/noNTz92QrF1DaFj4eXbhQ6dzaaAwOiI=
 github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI=
 github.com/pion/dtls/v2 v2.0.8 h1:reGe8rNIMfO/UAeFLqO61tl64t154Qfkr4U3Gzu1tsg=
 github.com/pion/dtls/v2 v2.0.8/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10=
@@ -254,12 +249,10 @@ github.com/pion/srtp/v2 v2.0.2 h1:664iGzVmaY7KYS5M0gleY0DscRo9ReDfTxQrq4UgGoU=
 github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0=
 github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
 github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
-github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk=
 github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
 github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
 github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
 github.com/pion/transport v0.12.1/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
-github.com/pion/transport v0.12.2 h1:WYEjhloRHt1R86LhUKjC5y+P52Y11/QqEUalvtzVoys=
 github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
 github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw=
 github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
@@ -332,13 +325,10 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY=
 github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
 github.com/templexxx/cpu v0.0.7 h1:pUEZn8JBy/w5yzdYWgx+0m0xL9uk6j4K91C5kOViAzo=
 github.com/templexxx/cpu v0.0.7/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
@@ -382,9 +372,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo=
 golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
@@ -422,16 +410,13 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 h1:3uJsdck53FDIpWwLeAXlia9p4C8j0BO2xZrqzKpL0D8=
 golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E=
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -449,7 +434,6 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -466,9 +450,7 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -478,7 +460,6 @@ golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -503,7 +484,6 @@ golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123 h1:4JSJPND/+4555t1HfXYF4UE
 golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -539,16 +519,13 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/probetest/probetest.go b/probetest/probetest.go
index 4158fa5..4b8baa4 100644
--- a/probetest/probetest.go
+++ b/probetest/probetest.go
@@ -20,9 +20,9 @@ import (
 	"strings"
 	"time"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/util"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/util"
 
 	"github.com/pion/webrtc/v3"
 	"golang.org/x/crypto/acme/autocert"
diff --git a/proxy/lib/proxy-go_test.go b/proxy/lib/proxy-go_test.go
index 3a81a1b..86616c4 100644
--- a/proxy/lib/proxy-go_test.go
+++ b/proxy/lib/proxy-go_test.go
@@ -11,8 +11,8 @@ import (
 	"strings"
 	"testing"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/util"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/util"
 	"github.com/pion/webrtc/v3"
 	. "github.com/smartystreets/goconvey/convey"
 )
diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 5f7bfd4..e39fcfb 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -40,9 +40,9 @@ import (
 	"sync"
 	"time"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/util"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/util"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/websocketconn"
 	"github.com/gorilla/websocket"
 	"github.com/pion/webrtc/v3"
 )
diff --git a/proxy/main.go b/proxy/main.go
index 368589c..7dcbcda 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -6,8 +6,8 @@ import (
 	"log"
 	"os"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
-	sf "git.torproject.org/pluggable-transports/snowflake.git/proxy/lib"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
+	sf "git.torproject.org/pluggable-transports/snowflake.git/v2/proxy/lib"
 )
 
 func main() {
diff --git a/server/lib/http.go b/server/lib/http.go
index 55849c5..939a816 100644
--- a/server/lib/http.go
+++ b/server/lib/http.go
@@ -11,9 +11,9 @@ import (
 	"sync"
 	"time"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/encapsulation"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel"
-	"git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/encapsulation"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/turbotunnel"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/websocketconn"
 	"github.com/gorilla/websocket"
 )
 
diff --git a/server/lib/snowflake.go b/server/lib/snowflake.go
index 93d3955..8942286 100644
--- a/server/lib/snowflake.go
+++ b/server/lib/snowflake.go
@@ -46,7 +46,7 @@ import (
 	"sync"
 	"time"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/turbotunnel"
 	"github.com/xtaci/kcp-go/v5"
 	"github.com/xtaci/smux"
 	"golang.org/x/net/http2"
diff --git a/server/lib/turbotunnel.go b/server/lib/turbotunnel.go
index 1e9bb58..d8f9d23 100644
--- a/server/lib/turbotunnel.go
+++ b/server/lib/turbotunnel.go
@@ -4,7 +4,7 @@ import (
 	"net"
 	"sync"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/turbotunnel"
 )
 
 // clientIDMap is a fixed-capacity mapping from ClientIDs to a net.Addr.
diff --git a/server/lib/turbotunnel_test.go b/server/lib/turbotunnel_test.go
index ac79c05..2918844 100644
--- a/server/lib/turbotunnel_test.go
+++ b/server/lib/turbotunnel_test.go
@@ -5,7 +5,7 @@ import (
 	"net"
 	"testing"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/turbotunnel"
 )
 
 func TestClientIDMap(t *testing.T) {
diff --git a/server/server.go b/server/server.go
index 92d819f..820a0a5 100644
--- a/server/server.go
+++ b/server/server.go
@@ -17,11 +17,11 @@ import (
 	"sync"
 	"syscall"
 
-	"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
 	"golang.org/x/crypto/acme/autocert"
 
 	pt "git.torproject.org/pluggable-transports/goptlib.git"
-	sf "git.torproject.org/pluggable-transports/snowflake.git/server/lib"
+	sf "git.torproject.org/pluggable-transports/snowflake.git/v2/server/lib"
 )
 
 const ptMethodName = "snowflake"

From 04bc471a637bcda2d865e5f607c2e588cbe0b044 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 4 Nov 2021 19:55:48 +0000
Subject: [PATCH 251/385] Support recurring NAT Type measurement

currentNATType will from now on be guarded by currentNATTypeAccess for any access.

NAT Type update rule is flattened into state transfer lookup table to assist reading.
---
 proxy/lib/snowflake.go | 62 +++++++++++++++++++++++++++++++++++++++---
 1 file changed, 58 insertions(+), 4 deletions(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index e39fcfb..7d237de 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -73,6 +73,10 @@ const readLimit = 100000 //Maximum number of bytes to be read from an HTTP reque
 
 var broker *SignalingServer
 
+var currentNATTypeAccess = &sync.RWMutex{}
+
+// currentNATType describes local network environment.
+// Obtain currentNATTypeAccess before access.
 var currentNATType = NATUnknown
 
 const (
@@ -183,7 +187,10 @@ func (s *SignalingServer) pollOffer(sid string, shutdown chan struct{}) *webrtc.
 			return nil
 		default:
 			numClients := int((tokens.count() / 8) * 8) // Round down to 8
-			body, err := messages.EncodePollRequest(sid, "standalone", currentNATType, numClients)
+			currentNATTypeAccess.RLock()
+			currentNATTypeLoaded := currentNATType
+			currentNATTypeAccess.RUnlock()
+			body, err := messages.EncodePollRequest(sid, "standalone", currentNATTypeLoaded, numClients)
 			if err != nil {
 				log.Printf("Error encoding poll message: %s", err.Error())
 				return nil
@@ -530,7 +537,12 @@ func (sf *SnowflakeProxy) Start() error {
 
 	// use probetest to determine NAT compatability
 	sf.checkNATType(config, sf.NATProbeURL)
-	log.Printf("NAT type: %s", currentNATType)
+
+	currentNATTypeAccess.RLock()
+	currentNATTypeLoaded := currentNATType
+	currentNATTypeAccess.RUnlock()
+
+	log.Printf("NAT type: %s", currentNATTypeLoaded)
 
 	ticker := time.NewTicker(pollInterval)
 	defer ticker.Stop()
@@ -604,12 +616,54 @@ func (sf *SnowflakeProxy) checkNATType(config webrtc.Configuration, probeURL str
 		return
 	}
 
+	currentNATTypeAccess.RLock()
+	currentNATTypeLoaded := currentNATType
+	currentNATTypeAccess.RUnlock()
+
+	currentNATTypeTestResult := NATUnknown
 	select {
 	case <-dataChan:
-		currentNATType = NATUnrestricted
+		currentNATTypeTestResult = NATUnrestricted
 	case <-time.After(dataChannelTimeout):
-		currentNATType = NATRestricted
+		currentNATTypeTestResult = NATRestricted
 	}
+
+	currentNATTypeToStore := NATUnknown
+	switch currentNATTypeLoaded + "->" + currentNATTypeTestResult {
+	case NATUnknown + "->" + NATUnknown:
+		currentNATTypeToStore = NATUnknown
+
+	case NATUnknown + "->" + NATUnrestricted:
+		currentNATTypeToStore = NATUnrestricted
+
+	case NATUnknown + "->" + NATRestricted:
+		currentNATTypeToStore = NATRestricted
+
+	case NATUnrestricted + "->" + NATUnknown:
+		currentNATTypeToStore = NATUnrestricted
+
+	case NATUnrestricted + "->" + NATUnrestricted:
+		currentNATTypeToStore = NATUnrestricted
+
+	case NATUnrestricted + "->" + NATRestricted:
+		currentNATTypeToStore = NATRestricted
+
+	case NATRestricted + "->" + NATUnknown:
+		currentNATTypeToStore = NATRestricted
+
+	case NATRestricted + "->" + NATUnrestricted:
+		currentNATTypeToStore = NATUnrestricted
+
+	case NATRestricted + "->" + NATRestricted:
+		currentNATTypeToStore = NATRestricted
+	}
+
+	log.Printf("NAT Type measurement: %v -> %v = %v\n", currentNATTypeLoaded, currentNATTypeTestResult, currentNATTypeToStore)
+
+	currentNATTypeAccess.Lock()
+	currentNATType = currentNATTypeToStore
+	currentNATTypeAccess.Unlock()
+
 	if err := pc.Close(); err != nil {
 		log.Printf("error calling pc.Close: %v", err)
 	}

From 4c8a16617873bcf637d14674f0c0f985303f4d1f Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 4 Nov 2021 20:21:59 +0000
Subject: [PATCH 252/385] Port V2Ray periodic task standard library to
 snowflake

This is a mature implementation of periodic task that run a function at given interval. It allows task to be stopped, and deals with edge case like interval too short gracefully.

V2Ray/V2Fly is MIT licensed.
---
 common/task/periodic.go | 85 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 85 insertions(+)
 create mode 100644 common/task/periodic.go

diff --git a/common/task/periodic.go b/common/task/periodic.go
new file mode 100644
index 0000000..6abe41a
--- /dev/null
+++ b/common/task/periodic.go
@@ -0,0 +1,85 @@
+package task
+
+import (
+	"sync"
+	"time"
+)
+
+// Periodic is a task that runs periodically.
+type Periodic struct {
+	// Interval of the task being run
+	Interval time.Duration
+	// Execute is the task function
+	Execute func() error
+
+	access  sync.Mutex
+	timer   *time.Timer
+	running bool
+}
+
+func (t *Periodic) hasClosed() bool {
+	t.access.Lock()
+	defer t.access.Unlock()
+
+	return !t.running
+}
+
+func (t *Periodic) checkedExecute() error {
+	if t.hasClosed() {
+		return nil
+	}
+
+	if err := t.Execute(); err != nil {
+		t.access.Lock()
+		t.running = false
+		t.access.Unlock()
+		return err
+	}
+
+	t.access.Lock()
+	defer t.access.Unlock()
+
+	if !t.running {
+		return nil
+	}
+
+	t.timer = time.AfterFunc(t.Interval, func() {
+		t.checkedExecute()
+	})
+
+	return nil
+}
+
+// Start implements common.Runnable.
+func (t *Periodic) Start() error {
+	t.access.Lock()
+	if t.running {
+		t.access.Unlock()
+		return nil
+	}
+	t.running = true
+	t.access.Unlock()
+
+	if err := t.checkedExecute(); err != nil {
+		t.access.Lock()
+		t.running = false
+		t.access.Unlock()
+		return err
+	}
+
+	return nil
+}
+
+// Close implements common.Closable.
+func (t *Periodic) Close() error {
+	t.access.Lock()
+	defer t.access.Unlock()
+
+	t.running = false
+	if t.timer != nil {
+		t.timer.Stop()
+		t.timer = nil
+	}
+
+	return nil
+}

From ac97ce7136906287a86c6fc2d9cfdc6b7c313166 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 4 Nov 2021 20:25:50 +0000
Subject: [PATCH 253/385] Add NAT Type measurement command line flag

It is important to include unit in flag name to prevent user from making mistake.
---
 proxy/lib/snowflake.go | 4 +++-
 proxy/main.go          | 4 ++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 7d237de..f979ed0 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -105,7 +105,9 @@ type SnowflakeProxy struct {
 	RelayURL string
 	// NATProbeURL is the URL of the probe service we use for NAT checks
 	NATProbeURL string
-	shutdown    chan struct{}
+	// NATTypeMeasurementIntervalSecond is time in second before NAT type is retested
+	NATTypeMeasurementIntervalSecond uint
+	shutdown                         chan struct{}
 }
 
 // Checks whether an IP address is a remote address for the client
diff --git a/proxy/main.go b/proxy/main.go
index 7dcbcda..a8e56bb 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -18,6 +18,8 @@ func main() {
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	relayURL := flag.String("relay", sf.DefaultRelayURL, "websocket relay URL")
+	NATTypeMeasurementIntervalSecond := flag.Uint("nat-type-measurement-interval-second", 0,
+		"the time interval in second before NAT type is retested, 0 disables retest")
 
 	flag.Parse()
 
@@ -27,6 +29,8 @@ func main() {
 		BrokerURL:          *rawBrokerURL,
 		KeepLocalAddresses: *keepLocalAddresses,
 		RelayURL:           *relayURL,
+
+		NATTypeMeasurementIntervalSecond: *NATTypeMeasurementIntervalSecond,
 	}
 
 	var logOutput io.Writer = os.Stderr

From a6a53ff8ceb3ac48ac1fc1165255df8ebc9c685b Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 4 Nov 2021 20:34:32 +0000
Subject: [PATCH 254/385] Add NAT Type test periodic task

---
 proxy/lib/snowflake.go | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index f979ed0..b133c67 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -41,6 +41,7 @@ import (
 	"time"
 
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/task"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/util"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/websocketconn"
 	"github.com/gorilla/websocket"
@@ -546,6 +547,19 @@ func (sf *SnowflakeProxy) Start() error {
 
 	log.Printf("NAT type: %s", currentNATTypeLoaded)
 
+	NatRetestTask := task.Periodic{
+		Interval: time.Second * time.Duration(sf.NATTypeMeasurementIntervalSecond),
+		Execute: func() error {
+			sf.checkNATType(config, sf.NATProbeURL)
+			return nil
+		},
+	}
+
+	if sf.NATTypeMeasurementIntervalSecond != 0 {
+		NatRetestTask.Start()
+		defer NatRetestTask.Close()
+	}
+
 	ticker := time.NewTicker(pollInterval)
 	defer ticker.Stop()
 

From 2547883cf91839c40b937e354408e41fdf34c24e Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 9 Nov 2021 19:34:16 +0000
Subject: [PATCH 255/385] Extract function getCurrentNATType()

Adopted the change in according to the recommendation from

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/62#note_2759900
---
 proxy/lib/snowflake.go | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index b133c67..4cf25ee 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -80,6 +80,12 @@ var currentNATTypeAccess = &sync.RWMutex{}
 // Obtain currentNATTypeAccess before access.
 var currentNATType = NATUnknown
 
+func getCurrentNATType() string {
+	currentNATTypeAccess.RLock()
+	defer currentNATTypeAccess.RUnlock()
+	return currentNATType
+}
+
 const (
 	sessionIDLength = 16
 )
@@ -190,9 +196,7 @@ func (s *SignalingServer) pollOffer(sid string, shutdown chan struct{}) *webrtc.
 			return nil
 		default:
 			numClients := int((tokens.count() / 8) * 8) // Round down to 8
-			currentNATTypeAccess.RLock()
-			currentNATTypeLoaded := currentNATType
-			currentNATTypeAccess.RUnlock()
+			currentNATTypeLoaded := getCurrentNATType()
 			body, err := messages.EncodePollRequest(sid, "standalone", currentNATTypeLoaded, numClients)
 			if err != nil {
 				log.Printf("Error encoding poll message: %s", err.Error())
@@ -541,9 +545,7 @@ func (sf *SnowflakeProxy) Start() error {
 	// use probetest to determine NAT compatability
 	sf.checkNATType(config, sf.NATProbeURL)
 
-	currentNATTypeAccess.RLock()
-	currentNATTypeLoaded := currentNATType
-	currentNATTypeAccess.RUnlock()
+	currentNATTypeLoaded := getCurrentNATType()
 
 	log.Printf("NAT type: %s", currentNATTypeLoaded)
 
@@ -632,9 +634,7 @@ func (sf *SnowflakeProxy) checkNATType(config webrtc.Configuration, probeURL str
 		return
 	}
 
-	currentNATTypeAccess.RLock()
-	currentNATTypeLoaded := currentNATType
-	currentNATTypeAccess.RUnlock()
+	currentNATTypeLoaded := getCurrentNATType()
 
 	currentNATTypeTestResult := NATUnknown
 	select {

From 59af9927a5383587eb20893a48b51e312bdbe896 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 12 Nov 2021 10:28:01 +0000
Subject: [PATCH 256/385] Refactor state transfer logic to simplify it

Adopted the change in according to the recommendation from

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/62#note_2760514
---
 proxy/lib/snowflake.go | 22 ++--------------------
 1 file changed, 2 insertions(+), 20 deletions(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 4cf25ee..df6a256 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -646,32 +646,14 @@ func (sf *SnowflakeProxy) checkNATType(config webrtc.Configuration, probeURL str
 
 	currentNATTypeToStore := NATUnknown
 	switch currentNATTypeLoaded + "->" + currentNATTypeTestResult {
-	case NATUnknown + "->" + NATUnknown:
-		currentNATTypeToStore = NATUnknown
-
-	case NATUnknown + "->" + NATUnrestricted:
-		currentNATTypeToStore = NATUnrestricted
-
-	case NATUnknown + "->" + NATRestricted:
-		currentNATTypeToStore = NATRestricted
-
 	case NATUnrestricted + "->" + NATUnknown:
 		currentNATTypeToStore = NATUnrestricted
 
-	case NATUnrestricted + "->" + NATUnrestricted:
-		currentNATTypeToStore = NATUnrestricted
-
-	case NATUnrestricted + "->" + NATRestricted:
-		currentNATTypeToStore = NATRestricted
-
 	case NATRestricted + "->" + NATUnknown:
 		currentNATTypeToStore = NATRestricted
 
-	case NATRestricted + "->" + NATUnrestricted:
-		currentNATTypeToStore = NATUnrestricted
-
-	case NATRestricted + "->" + NATRestricted:
-		currentNATTypeToStore = NATRestricted
+	default:
+		currentNATTypeToStore = currentNATTypeTestResult
 	}
 
 	log.Printf("NAT Type measurement: %v -> %v = %v\n", currentNATTypeLoaded, currentNATTypeTestResult, currentNATTypeToStore)

From 1b79962ca81e6d1db0c790b2d2d5e54a806148f4 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 12 Nov 2021 10:43:05 +0000
Subject: [PATCH 257/385] Rename flag to nat-retest-seconds and retest daily by
 default

Adopted the change in according to the recommendation from

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/62#note_2759816

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/62#note_2760512
---
 proxy/main.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxy/main.go b/proxy/main.go
index a8e56bb..baac52b 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -18,7 +18,7 @@ func main() {
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	relayURL := flag.String("relay", sf.DefaultRelayURL, "websocket relay URL")
-	NATTypeMeasurementIntervalSecond := flag.Uint("nat-type-measurement-interval-second", 0,
+	NATTypeMeasurementIntervalSecond := flag.Uint("nat-retest-seconds", 86400,
 		"the time interval in second before NAT type is retested, 0 disables retest")
 
 	flag.Parse()

From d4fdb35ee8d1f7e43dbca2503355d191da8d00f0 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 12 Nov 2021 10:56:57 +0000
Subject: [PATCH 258/385] Add in source indicator of file origin

Adopted the change in according to the recommendation from

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/62#note_2759815
---
 common/task/periodic.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/common/task/periodic.go b/common/task/periodic.go
index 6abe41a..1ab34b3 100644
--- a/common/task/periodic.go
+++ b/common/task/periodic.go
@@ -1,3 +1,5 @@
+// Package task
+// Reused from https://github.com/v2fly/v2ray-core/blob/784775f68922f07d40c9eead63015b2026af2ade/common/task/periodic.go
 package task
 
 import (

From 9bdb87eaf355e4dacdf5e1b59b8e37e010b166ba Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 16 Nov 2021 11:16:54 +0000
Subject: [PATCH 259/385] Update nat-retest-seconds format to
 time.ParseDuration form

Adopted the change in according to the recommendation from

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/62#note_2761382
---
 proxy/main.go | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/proxy/main.go b/proxy/main.go
index baac52b..b0f6971 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -5,6 +5,7 @@ import (
 	"io"
 	"log"
 	"os"
+	"time"
 
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
 	sf "git.torproject.org/pluggable-transports/snowflake.git/v2/proxy/lib"
@@ -18,8 +19,8 @@ func main() {
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	relayURL := flag.String("relay", sf.DefaultRelayURL, "websocket relay URL")
-	NATTypeMeasurementIntervalSecond := flag.Uint("nat-retest-seconds", 86400,
-		"the time interval in second before NAT type is retested, 0 disables retest")
+	NATTypeMeasurementIntervalString := flag.String("nat-retest-seconds", "24h",
+		"the time interval in second before NAT type is retested, 0s disables retest. Valid time units are \"s\", \"m\", \"h\". ")
 
 	flag.Parse()
 
@@ -29,8 +30,12 @@ func main() {
 		BrokerURL:          *rawBrokerURL,
 		KeepLocalAddresses: *keepLocalAddresses,
 		RelayURL:           *relayURL,
+	}
 
-		NATTypeMeasurementIntervalSecond: *NATTypeMeasurementIntervalSecond,
+	if NATTypeMeasurementIntervalTime, err := time.ParseDuration(*NATTypeMeasurementIntervalString); err == nil {
+		proxy.NATTypeMeasurementIntervalSecond = uint(NATTypeMeasurementIntervalTime.Seconds())
+	} else {
+		log.Fatalf("unable to parse nat-retest-seconds: %v", err)
 	}
 
 	var logOutput io.Writer = os.Stderr

From efdb850d2ed7105959a53614b9e881bbee42fd28 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 16 Nov 2021 11:22:44 +0000
Subject: [PATCH 260/385] Update nat-retest-interval flag name to reflect the
 change

Adopted the change in according to the recommendation from

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/62#note_2761382
---
 proxy/main.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxy/main.go b/proxy/main.go
index b0f6971..399493b 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -19,7 +19,7 @@ func main() {
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	relayURL := flag.String("relay", sf.DefaultRelayURL, "websocket relay URL")
-	NATTypeMeasurementIntervalString := flag.String("nat-retest-seconds", "24h",
+	NATTypeMeasurementIntervalString := flag.String("nat-retest-interval", "24h",
 		"the time interval in second before NAT type is retested, 0s disables retest. Valid time units are \"s\", \"m\", \"h\". ")
 
 	flag.Parse()

From c49f72eb0c630fcdb03aea179f7b4b00d204ddfb Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 16 Nov 2021 15:58:57 +0000
Subject: [PATCH 261/385] Update nat-retest-interval type to duration

Adopted the change in according to the recommendation from

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/62#note_2761438
---
 proxy/main.go | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/proxy/main.go b/proxy/main.go
index 399493b..de33d42 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -19,7 +19,7 @@ func main() {
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	relayURL := flag.String("relay", sf.DefaultRelayURL, "websocket relay URL")
-	NATTypeMeasurementIntervalString := flag.String("nat-retest-interval", "24h",
+	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\". ")
 
 	flag.Parse()
@@ -30,12 +30,8 @@ func main() {
 		BrokerURL:          *rawBrokerURL,
 		KeepLocalAddresses: *keepLocalAddresses,
 		RelayURL:           *relayURL,
-	}
 
-	if NATTypeMeasurementIntervalTime, err := time.ParseDuration(*NATTypeMeasurementIntervalString); err == nil {
-		proxy.NATTypeMeasurementIntervalSecond = uint(NATTypeMeasurementIntervalTime.Seconds())
-	} else {
-		log.Fatalf("unable to parse nat-retest-seconds: %v", err)
+		NATTypeMeasurementIntervalSecond: uint(NATTypeMeasurementInterval.Seconds()),
 	}
 
 	var logOutput io.Writer = os.Stderr

From 0c62d806a4352c054f80bb0f604eb7de7f1430e0 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 16 Nov 2021 19:25:27 +0000
Subject: [PATCH 262/385] Represent NATTypeMeasurementInterval in time.Duration

Adopted the change in according to the recommendation from

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/62#note_2761566
---
 proxy/lib/snowflake.go | 10 +++++-----
 proxy/main.go          |  2 +-
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index df6a256..a9ac399 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -112,9 +112,9 @@ type SnowflakeProxy struct {
 	RelayURL string
 	// NATProbeURL is the URL of the probe service we use for NAT checks
 	NATProbeURL string
-	// NATTypeMeasurementIntervalSecond is time in second before NAT type is retested
-	NATTypeMeasurementIntervalSecond uint
-	shutdown                         chan struct{}
+	// NATTypeMeasurementInterval is time before NAT type is retested
+	NATTypeMeasurementInterval time.Duration
+	shutdown                   chan struct{}
 }
 
 // Checks whether an IP address is a remote address for the client
@@ -550,14 +550,14 @@ func (sf *SnowflakeProxy) Start() error {
 	log.Printf("NAT type: %s", currentNATTypeLoaded)
 
 	NatRetestTask := task.Periodic{
-		Interval: time.Second * time.Duration(sf.NATTypeMeasurementIntervalSecond),
+		Interval: sf.NATTypeMeasurementInterval,
 		Execute: func() error {
 			sf.checkNATType(config, sf.NATProbeURL)
 			return nil
 		},
 	}
 
-	if sf.NATTypeMeasurementIntervalSecond != 0 {
+	if sf.NATTypeMeasurementInterval != 0 {
 		NatRetestTask.Start()
 		defer NatRetestTask.Close()
 	}
diff --git a/proxy/main.go b/proxy/main.go
index de33d42..b85dde0 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -31,7 +31,7 @@ func main() {
 		KeepLocalAddresses: *keepLocalAddresses,
 		RelayURL:           *relayURL,
 
-		NATTypeMeasurementIntervalSecond: uint(NATTypeMeasurementInterval.Seconds()),
+		NATTypeMeasurementInterval: *NATTypeMeasurementInterval,
 	}
 
 	var logOutput io.Writer = os.Stderr

From 40f44d627223a45f7a5f512fa9179e60f82df23f Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 19 Nov 2021 15:55:30 +0000
Subject: [PATCH 263/385] Add V2Ray/V2Fly License for task

---
 common/task/periodic.go | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/common/task/periodic.go b/common/task/periodic.go
index 1ab34b3..37c56eb 100644
--- a/common/task/periodic.go
+++ b/common/task/periodic.go
@@ -1,5 +1,28 @@
 // Package task
 // Reused from https://github.com/v2fly/v2ray-core/blob/784775f68922f07d40c9eead63015b2026af2ade/common/task/periodic.go
+/*
+The MIT License (MIT)
+
+Copyright (c) 2015-2021 V2Ray & V2Fly Community
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
 package task
 
 import (

From c9399da566eeb525906762fa5693e50c9731f78f Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner 
Date: Thu, 16 Jul 2020 13:43:55 +0200
Subject: [PATCH 264/385] gitlab-ci: expire artifacts in 1 week, improve gradle
 caching, etc.

---
 .gitlab-ci.yml | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 497462f..c483873 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -8,7 +8,7 @@
     DEBIAN_FRONTEND: noninteractive
     GOPATH: /usr/share/gocode
   before_script:
-    - apt-get -qy update
+    - apt-get update
     - apt-get -qy install --no-install-recommends
         build-essential
         ca-certificates
@@ -35,13 +35,13 @@
   variables:
     DEBIAN_FRONTEND: noninteractive
   before_script:
-    - apt-get -qy update
+    - apt-get update
     - apt-get -qy install --no-install-recommends
         ca-certificates
         git
         lbzip2
 
-.go_test: &go-test
+.go-test: &go-test
   - test -z "$(go fmt ./...)"
   - go vet ./...
   - go test -v -race ./...
@@ -57,10 +57,13 @@
       - client/*.aar
       - client/*.jar
       - client/client
-    expire_in: 1 day
+    expire_in: 1 week
     when: on_success
   after_script:
     - echo "Download debug artifacts from https://gitlab.com/${CI_PROJECT_PATH}/-/jobs"
+    # this file changes every time but should not be cached
+    - rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
+    - rm -rf $GRADLE_USER_HOME/caches/*/plugin-resolution/
 
 # -- jobs ------------------------------------------------------------
 

From 1318b6a9ec69b1e776d8ed5e0a99ab20eef3576c Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner 
Date: Thu, 16 Jul 2020 14:02:15 +0200
Subject: [PATCH 265/385] stripped down Android build process for gitlab-ci and
 Vagrant

---
 .gitignore     |  6 +++++
 .gitlab-ci.yml | 64 ++++++++++++++++++++++++++++++++----------------
 README.md      | 10 ++++++++
 Vagrantfile    | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 125 insertions(+), 21 deletions(-)
 create mode 100644 Vagrantfile

diff --git a/.gitignore b/.gitignore
index 002f95e..b5474f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,9 @@ proxy/proxy
 probetest/probetest
 snowflake.log
 ignore/
+
+# from running the vagrant setup
+/.vagrant/
+/sdk-tools-linux-*.zip*
+/android-ndk-*
+/tools/
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c483873..35caa6c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,18 @@
 
+.apt-template: &apt-template
+- export LC_ALL=C.UTF-8
+- export DEBIAN_FRONTEND=noninteractive
+- echo Etc/UTC > /etc/timezone
+- echo 'quiet "1";'
+       'APT::Install-Recommends "0";'
+       'APT::Install-Suggests "0";'
+       'APT::Acquire::Retries "20";'
+       'APT::Get::Assume-Yes "true";'
+       'Dpkg::Use-Pty "0";'
+      > /etc/apt/apt.conf.d/99gitlab
+- apt-get update
+- apt-get dist-upgrade
+
 # Set things up to use the OS-native packages for Go.  Anything that
 # is downloaded by go during the `go fmt` stage is not coming from the
 # Debian/Ubuntu repo. So those would need to be packaged for this to
@@ -25,6 +39,7 @@
         golang-github-xtaci-smux-dev
         golang-golang-x-crypto-dev
         golang-golang-x-net-dev
+        golang-goptlib-dev
         golang-golang-x-sys-dev
         golang-golang-x-text-dev
         golang-golang-x-xerrors-dev
@@ -68,33 +83,45 @@
 # -- jobs ------------------------------------------------------------
 
 android:
-  image: registry.gitlab.com/fdroid/ci-images-client
+  image: debian:bullseye-backports
   variables:
+    ANDROID_HOME: /usr/lib/android-sdk
+    DEBIAN_FRONTEND: noninteractive
     GOPATH: "/go"
-    ANDROID_VERSION: 29
+    LANG: C.UTF-8
+    PATH: "/go/bin:/usr/lib/go-1.16/bin:/usr/bin:/bin"
   cache:
     paths:
       - .gradle/wrapper
       - .gradle/caches
+  <<: *test-template
   before_script:
-    - apt-get -qy update
-    - apt-get -qy install --no-install-recommends
+    - *apt-template
+    - apt-get install
+        android-sdk-platform-23
+        android-sdk-platform-tools
         build-essential
+        curl
+        default-jdk-headless
+        git
         gnupg
+        unzip
         wget
-    - cd /usr/local
-    - export gotarball="go1.16.8.linux-amd64.tar.gz"
-    - wget -q https://dl.google.com/go/${gotarball}
-    - wget -q https://dl.google.com/go/${gotarball}.asc
-    - curl https://dl.google.com/linux/linux_signing_key.pub | gpg --import
-    - gpg --verify ${gotarball}.asc
-    - echo "f32501aeb8b7b723bc7215f6c373abb6981bbc7e1c7b44e9f07317e1a300dce2  ${gotarball}" | sha256sum -c
-    - tar -xzf ${gotarball}
-    - export PATH="/usr/local/go/bin:$GOPATH/bin:$PATH"  # putting this in 'variables:' cause weird runner errors
-    - cd $CI_PROJECT_DIR
+    - apt-get install -t bullseye-backports golang-1.16
+
+    - ndk=android-ndk-r21e-linux-x86_64.zip
+    - wget --continue --no-verbose https://dl.google.com/android/repository/$ndk
+    - echo "ad7ce5467e18d40050dc51b8e7affc3e635c85bd8c59be62de32352328ed467e  $ndk" > $ndk.sha256
+    - sha256sum -c $ndk.sha256
+    - unzip -q $ndk
+    - rm ${ndk}*
+    - mv android-ndk-* $ANDROID_HOME/ndk-bundle/
+
+    - chmod -R a+rX $ANDROID_HOME
+
   script:
     - *go-test
-    - export GRADLE_USER_HOME=$PWD/.gradle
+    - export GRADLE_USER_HOME=$CI_PROJECT_DIR/.gradle
     - go version
     - go env
 
@@ -102,18 +129,13 @@ android:
     - go get golang.org/x/mobile/cmd/gobind
     - go install golang.org/x/mobile/cmd/gobind
     - go install golang.org/x/mobile/cmd/gomobile
-    - echo y | $ANDROID_HOME/tools/bin/sdkmanager 'ndk-bundle' > /dev/null
-    - echo y | $ANDROID_HOME/tools/bin/sdkmanager "platforms;android-${ANDROID_VERSION}" > /dev/null
     - gomobile init
 
-    - git -C $CI_PROJECT_DIR reset --hard
-    - git -C $CI_PROJECT_DIR clean -fdx
     - cd $CI_PROJECT_DIR/client
     # gomobile builds a shared library not a CLI executable
     - sed -i 's,^package main$,package snowflakeclient,' snowflake.go
     - go get golang.org/x/mobile/bind
-    - gomobile bind -v -target=android .
-  <<: *test-template
+    - gomobile bind -v -target=android -trimpath .
 
 go-1.13:
   image: golang:1.13-stretch
diff --git a/README.md b/README.md
index 0278c04..9a1c958 100644
--- a/README.md
+++ b/README.md
@@ -87,3 +87,13 @@ abundance of ephemeral and short-lived (and special!) volunteer proxies...
 ### More info and links
 
 We have more documentation in the [Snowflake wiki](https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/wikis/home) and at https://snowflake.torproject.org/.
+
+
+##### -- Android AAR Reproducible Build Setup  --
+
+Using `gomobile` it is possible to build snowflake as shared libraries for all
+the architectures supported by Android.  This is in the _.gitlab-ci.yml_, which
+runs in GitLab CI.  It is also possible to run this setup in a Virtual Machine
+using [vagrant](https://www.vagrantup.com/).  Just run `vagrant up` and it will
+create and provision the VM.  `vagrant ssh` to get into the VM to use it as a
+development environment.
diff --git a/Vagrantfile b/Vagrantfile
new file mode 100644
index 0000000..1b538d5
--- /dev/null
+++ b/Vagrantfile
@@ -0,0 +1,66 @@
+require 'pathname'
+require 'tempfile'
+require 'yaml'
+
+srvpath = Pathname.new(File.dirname(__FILE__)).realpath
+configfile = YAML.load_file(File.join(srvpath, "/.gitlab-ci.yml"))
+remote_url = 'https://git.torproject.org/pluggable-transports/snowflake.git'
+
+# set up essential environment variables
+env = configfile['android']['variables']
+env['CI_PROJECT_DIR'] = '/builds/tpo/anti-censorship/pluggable-transports/snowflake'
+env_file = Tempfile.new('env')
+File.chmod(0644, env_file.path)
+env.each do |k,v|
+    env_file.write("export #{k}='#{v}'\n")
+end
+env_file.rewind
+
+sourcepath = '/etc/profile.d/env.sh'
+header = "#!/bin/bash -ex\nsource #{sourcepath}\ncd $CI_PROJECT_DIR\n"
+
+before_script_file = Tempfile.new('before_script')
+File.chmod(0755, before_script_file.path)
+before_script_file.write(header)
+configfile['android']['before_script'].flatten.each do |line|
+    before_script_file.write(line)
+    before_script_file.write("\n")
+end
+before_script_file.rewind
+
+script_file = Tempfile.new('script')
+File.chmod(0755, script_file.path)
+script_file.write(header)
+configfile['android']['script'].flatten.each do |line|
+    script_file.write(line)
+    script_file.write("\n")
+end
+script_file.rewind
+
+Vagrant.configure("2") do |config|
+  config.vm.box = "debian/bullseye64"
+  config.vm.synced_folder '.', '/vagrant', disabled: true
+  config.vm.provision "file", source: env_file.path, destination: 'env.sh'
+  config.vm.provision :shell, inline: <<-SHELL
+    set -ex
+    mv ~vagrant/env.sh #{sourcepath}
+    source #{sourcepath}
+    test -d /go || mkdir /go
+    mkdir -p $(dirname $CI_PROJECT_DIR)
+    chown -R vagrant.vagrant $(dirname $CI_PROJECT_DIR)
+    apt-get update
+    apt-get -qy install --no-install-recommends git
+    git clone #{remote_url} $CI_PROJECT_DIR
+    chmod -R a+rX,u+w /go $CI_PROJECT_DIR
+    chown -R vagrant.vagrant /go $CI_PROJECT_DIR
+SHELL
+  config.vm.provision "file", source: before_script_file.path, destination: 'before_script.sh'
+  config.vm.provision "file", source: script_file.path, destination: 'script.sh'
+  config.vm.provision :shell, inline: '/home/vagrant/before_script.sh'
+  config.vm.provision :shell, privileged: false, inline: '/home/vagrant/script.sh'
+
+  # remove this or comment it out to use VirtualBox instead of libvirt
+  config.vm.provider :libvirt do |libvirt|
+    libvirt.memory = 1536
+  end
+end

From 51f2c026fde882c5c7b84b0aebe976703752f866 Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner 
Date: Thu, 16 Jul 2020 20:09:20 +0200
Subject: [PATCH 266/385] gitlab-ci: include flags to make reproducible builds

* https://github.com/golang/go/issues/33772
---
 .gitlab-ci.yml | 14 ++++++++------
 Vagrantfile    |  3 ++-
 2 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 35caa6c..c9dd50b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,9 @@
 
+variables:
+  DEBIAN_FRONTEND: noninteractive
+  REPRODUCIBLE_FLAGS: -trimpath -ldflags=-buildid=
+
+# set up apt for automated use
 .apt-template: &apt-template
 - export LC_ALL=C.UTF-8
 - export DEBIAN_FRONTEND=noninteractive
@@ -13,13 +18,13 @@
 - apt-get update
 - apt-get dist-upgrade
 
+
 # Set things up to use the OS-native packages for Go.  Anything that
 # is downloaded by go during the `go fmt` stage is not coming from the
 # Debian/Ubuntu repo. So those would need to be packaged for this to
 # make it into Debian and/or Ubuntu.
 .debian-native-template: &debian-native-template
   variables:
-    DEBIAN_FRONTEND: noninteractive
     GOPATH: /usr/share/gocode
   before_script:
     - apt-get update
@@ -47,8 +52,6 @@
 
 # use Go installed as part of the official, Debian-based Docker images
 .golang-docker-debian-template: &golang-docker-debian-template
-  variables:
-    DEBIAN_FRONTEND: noninteractive
   before_script:
     - apt-get update
     - apt-get -qy install --no-install-recommends
@@ -63,7 +66,7 @@
 
   - cd $CI_PROJECT_DIR/client/
   - go get
-  - go build
+  - go build $REPRODUCIBLE_FLAGS
 
 .test-template: &test-template
   artifacts:
@@ -86,7 +89,6 @@ android:
   image: debian:bullseye-backports
   variables:
     ANDROID_HOME: /usr/lib/android-sdk
-    DEBIAN_FRONTEND: noninteractive
     GOPATH: "/go"
     LANG: C.UTF-8
     PATH: "/go/bin:/usr/lib/go-1.16/bin:/usr/bin:/bin"
@@ -135,7 +137,7 @@ android:
     # gomobile builds a shared library not a CLI executable
     - sed -i 's,^package main$,package snowflakeclient,' snowflake.go
     - go get golang.org/x/mobile/bind
-    - gomobile bind -v -target=android -trimpath .
+    - gomobile bind -v -target=android $REPRODUCIBLE_FLAGS .
 
 go-1.13:
   image: golang:1.13-stretch
diff --git a/Vagrantfile b/Vagrantfile
index 1b538d5..36a31fe 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -7,7 +7,8 @@ configfile = YAML.load_file(File.join(srvpath, "/.gitlab-ci.yml"))
 remote_url = 'https://git.torproject.org/pluggable-transports/snowflake.git'
 
 # set up essential environment variables
-env = configfile['android']['variables']
+env = configfile['variables']
+env = env.merge(configfile['android']['variables'])
 env['CI_PROJECT_DIR'] = '/builds/tpo/anti-censorship/pluggable-transports/snowflake'
 env_file = Tempfile.new('env')
 File.chmod(0644, env_file.path)

From 221f1c41c9618907a022655d1df4eb6eef02ab0a Mon Sep 17 00:00:00 2001
From: Hans-Christoph Steiner 
Date: Mon, 20 Jul 2020 16:00:09 +0200
Subject: [PATCH 267/385] gitlab-ci: include job number in the artfacts zipball
 filename

---
 .gitlab-ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c9dd50b..0824267 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -70,7 +70,7 @@ variables:
 
 .test-template: &test-template
   artifacts:
-    name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
+    name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_JOB_ID}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
     paths:
       - client/*.aar
       - client/*.jar

From 738bd464eac631dd296ebf932f0a98f9bb9868e3 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Fri, 10 Dec 2021 10:29:47 -0500
Subject: [PATCH 268/385] Update version of DTLS library

Make sure we use a version of the DTLS library that contains the
following fingerprinting fixes:

Only send supported_groups extension in ClientHello
Do not include IP addresses as SNI values

These changes have been merged upstream into pion/dtls.
---
 go.mod |  7 ++++---
 go.sum | 14 ++++++++++++++
 2 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index 1efb5a5..5538110 100644
--- a/go.mod
+++ b/go.mod
@@ -17,8 +17,9 @@ require (
 	github.com/xtaci/kcp-go/v5 v5.6.1
 	github.com/xtaci/smux v1.5.15
 	gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01
-	golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
-	golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4
-	golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e // indirect
+	golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
+	golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c
 	google.golang.org/protobuf v1.23.0
 )
+
+replace github.com/pion/dtls/v2 => github.com/pion/dtls/v2 v2.0.12
diff --git a/go.sum b/go.sum
index c229b37..ecf91a3 100644
--- a/go.sum
+++ b/go.sum
@@ -226,6 +226,8 @@ github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBla
 github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI=
 github.com/pion/dtls/v2 v2.0.8 h1:reGe8rNIMfO/UAeFLqO61tl64t154Qfkr4U3Gzu1tsg=
 github.com/pion/dtls/v2 v2.0.8/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10=
+github.com/pion/dtls/v2 v2.0.12 h1:QMSvNht7FM/XDXij3Ic90SCbl5yL7kppeI4ghfF4in8=
+github.com/pion/dtls/v2 v2.0.12/go.mod h1:5Pe3QJI0Ajsx+uCfxREeewGFlKYBzLrXe9ku7Y0oRXM=
 github.com/pion/ice/v2 v2.0.15 h1:KZrwa2ciL9od8+TUVJiYTNsCW9J5lktBjGwW1MacEnQ=
 github.com/pion/ice/v2 v2.0.15/go.mod h1:ZIiVGevpgAxF/cXiIVmuIUtCb3Xs4gCzCbXB6+nFkSI=
 github.com/pion/interceptor v0.0.10 h1:dXFyFWRJFwmzQqyn0U8dUAbOJu+JJnMVAqxmvTu30B4=
@@ -260,6 +262,8 @@ github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
 github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
 github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
 github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
+github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
+github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
 github.com/pion/webrtc/v3 v3.0.15 h1:g8MMJohjQoj0+pTrU329tWM6dvCieNTgnjtqv1kmEdY=
 github.com/pion/webrtc/v3 v3.0.15/go.mod h1:uUt2nRSsCnK/nfzTAfOmaeLan26ZJ0aP9iwjc/gcC2Y=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -376,6 +380,8 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo=
 golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
+golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -415,6 +421,9 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E=
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c h1:WtYZ93XtWSO5KlOMgPZu7hXY9WhMZpprvlm5VwvAl8c=
+golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -457,12 +466,17 @@ golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M=
 golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

From 9c11e479d0d6fdcad47c9f3d55179dd907422a3a Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Fri, 10 Dec 2021 10:43:31 -0500
Subject: [PATCH 269/385] Update go versions in CI tests

Debian packages Go 1.15 and 1.17, and we use 1.16 in Tor Browser.
---
 .gitlab-ci.yml | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0824267..5ec32fd 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -139,15 +139,22 @@ android:
     - go get golang.org/x/mobile/bind
     - gomobile bind -v -target=android $REPRODUCIBLE_FLAGS .
 
-go-1.13:
-  image: golang:1.13-stretch
+go-1.15:
+  image: golang:1.15-stretch
   <<: *golang-docker-debian-template
   <<: *test-template
   script:
     - *go-test
 
-go-1.14:
-  image: golang:1.14-stretch
+go-1.16:
+  image: golang:1.16-stretch
+  <<: *golang-docker-debian-template
+  <<: *test-template
+  script:
+    - *go-test
+
+go-1.17:
+  image: golang:1.17-stretch
   <<: *golang-docker-debian-template
   <<: *test-template
   script:

From aeb0794d2843d0cf9dfba2d8d4d0a9719b5636cd Mon Sep 17 00:00:00 2001
From: David Fifield 
Date: Thu, 16 Dec 2021 09:46:55 -0700
Subject: [PATCH 270/385] Use `require` rather than `replace` for dtls version.

go mod edit -dropreplace=github.com/pion/dtls/v2
go get github.com/pion/dtls/v2@v2.0.12

This is an update to
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/66.
---
 go.mod | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index 5538110..e8038f1 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	git.torproject.org/pluggable-transports/goptlib.git v1.1.0
 	github.com/google/uuid v1.2.0 // indirect
 	github.com/gorilla/websocket v1.4.1
+	github.com/pion/dtls/v2 v2.0.12 // indirect
 	github.com/pion/ice/v2 v2.0.15
 	github.com/pion/sdp/v3 v3.0.4
 	github.com/pion/stun v0.3.5
@@ -21,5 +22,3 @@ require (
 	golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c
 	google.golang.org/protobuf v1.23.0
 )
-
-replace github.com/pion/dtls/v2 => github.com/pion/dtls/v2 v2.0.12

From b35a79ac247e53ca0a2dd25625e083e9bba395fa Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Wed, 12 Jan 2022 10:53:58 -0500
Subject: [PATCH 271/385] Validate client and proxy supplied strings

Malicious clients and proxies can provide potentially malicious strings
in the polls. This validates the NAT type and proxy type strings to
ensure that malformed strings are not displayed on a web page
or passed to any of our monitoring infrastructure.

If a client or proxy supplies an invalid NAT type, we return an error
message. If a proxy supplies an unknown proxy type, we set the proxy
type to unknown.
---
 common/messages/client.go        | 12 +++++++++--
 common/messages/messages_test.go |  2 +-
 common/messages/proxy.go         | 35 +++++++++++++++++++++++++++-----
 3 files changed, 41 insertions(+), 8 deletions(-)

diff --git a/common/messages/client.go b/common/messages/client.go
index b40c582..edb7115 100644
--- a/common/messages/client.go
+++ b/common/messages/client.go
@@ -6,6 +6,8 @@ package messages
 import (
 	"encoding/json"
 	"fmt"
+
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
 )
 
 const ClientVersion = "1.0"
@@ -73,8 +75,14 @@ func DecodeClientPollRequest(data []byte) (*ClientPollRequest, error) {
 		return nil, fmt.Errorf("no supplied offer")
 	}
 
-	if message.NAT == "" {
-		message.NAT = "unknown"
+	switch message.NAT {
+	case "":
+		message.NAT = nat.NATUnknown
+	case nat.NATUnknown:
+	case nat.NATRestricted:
+	case nat.NATUnrestricted:
+	default:
+		return nil, fmt.Errorf("invalid NAT type")
 	}
 
 	return &message, nil
diff --git a/common/messages/messages_test.go b/common/messages/messages_test.go
index abb978d..a38746b 100644
--- a/common/messages/messages_test.go
+++ b/common/messages/messages_test.go
@@ -22,7 +22,7 @@ func TestDecodeProxyPollRequest(t *testing.T) {
 			{
 				//Version 1.0 proxy message
 				"ymbcCMto7KHNGYlp",
-				"",
+				"unknown",
 				"unknown",
 				0,
 				`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`,
diff --git a/common/messages/proxy.go b/common/messages/proxy.go
index 3817c04..83606d3 100644
--- a/common/messages/proxy.go
+++ b/common/messages/proxy.go
@@ -7,9 +7,18 @@ import (
 	"encoding/json"
 	"fmt"
 	"strings"
+
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
 )
 
-const version = "1.2"
+const (
+	version = "1.2"
+
+	ProxyStandalone = "standalone"
+	ProxyWebext     = "webext"
+	ProxyBadge      = "badge"
+	ProxyUnknown    = "unknown"
+)
 
 /* Version 1.2 specification:
 
@@ -116,12 +125,28 @@ func DecodePollRequest(data []byte) (sid string, proxyType string, natType strin
 		return
 	}
 
-	natType = message.NAT
-	if natType == "" {
-		natType = "unknown"
+	switch message.NAT {
+	case "":
+		message.NAT = nat.NATUnknown
+	case nat.NATUnknown:
+	case nat.NATRestricted:
+	case nat.NATUnrestricted:
+	default:
+		err = fmt.Errorf("invalid NAT type")
+		return
 	}
 
-	return message.Sid, message.Type, natType, message.Clients, nil
+	// we don't reject polls with an unknown proxy type because we encourage
+	// projects that embed proxy code to include their own type
+	switch message.Type {
+	case ProxyStandalone:
+	case ProxyWebext:
+	case ProxyBadge:
+	default:
+		message.Type = ProxyUnknown
+	}
+
+	return message.Sid, message.Type, message.NAT, message.Clients, nil
 }
 
 type ProxyPollResponse struct {

From 50646698e3e213fd9e714c327463eed87c4fb5f3 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 6 Jan 2022 20:31:15 +0000
Subject: [PATCH 272/385] Suppress connection end log output

This is an amendment of https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/30
---
 server/lib/snowflake.go | 3 ++-
 server/server.go        | 5 +++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/server/lib/snowflake.go b/server/lib/snowflake.go
index 8942286..31b6a20 100644
--- a/server/lib/snowflake.go
+++ b/server/lib/snowflake.go
@@ -38,6 +38,7 @@ package snowflake_server
 
 import (
 	"crypto/tls"
+	"errors"
 	"fmt"
 	"io"
 	"log"
@@ -262,7 +263,7 @@ func (l *SnowflakeListener) acceptSessions(ln *kcp.Listener) error {
 		go func() {
 			defer conn.Close()
 			err := l.acceptStreams(conn)
-			if err != nil && err != io.ErrClosedPipe {
+			if err != nil && !errors.Is(err, io.ErrClosedPipe) {
 				log.Printf("acceptStreams: %v", err)
 			}
 		}()
diff --git a/server/server.go b/server/server.go
index 820a0a5..4b53c86 100644
--- a/server/server.go
+++ b/server/server.go
@@ -3,6 +3,7 @@
 package main
 
 import (
+	"errors"
 	"flag"
 	"fmt"
 	"io"
@@ -47,7 +48,7 @@ func proxy(local *net.TCPConn, conn net.Conn) {
 	wg.Add(2)
 
 	go func() {
-		if _, err := io.Copy(conn, local); err != nil && err != io.ErrClosedPipe {
+		if _, err := io.Copy(conn, local); err != nil && !errors.Is(err, io.ErrClosedPipe) {
 			log.Printf("error copying ORPort to WebSocket %v", err)
 		}
 		local.CloseRead()
@@ -55,7 +56,7 @@ func proxy(local *net.TCPConn, conn net.Conn) {
 		wg.Done()
 	}()
 	go func() {
-		if _, err := io.Copy(local, conn); err != nil && err != io.ErrClosedPipe {
+		if _, err := io.Copy(local, conn); err != nil && !errors.Is(err, io.ErrClosedPipe) {
 			log.Printf("error copying WebSocket to ORPort %v", err)
 		}
 		local.CloseWrite()

From d2f6ea5417566966bedf8fef261199f0ebc360a2 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 6 Jan 2022 20:40:21 +0000
Subject: [PATCH 273/385] increase clientIDAddrMapCapacity

See also:
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40084
---
 server/lib/http.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/lib/http.go b/server/lib/http.go
index 939a816..5f214a1 100644
--- a/server/lib/http.go
+++ b/server/lib/http.go
@@ -29,7 +29,7 @@ const clientMapTimeout = 1 * time.Minute
 // How big to make the map of ClientIDs to IP addresses. The map is used in
 // turbotunnelMode to store a reasonable IP address for a client session that
 // may outlive any single WebSocket connection.
-const clientIDAddrMapCapacity = 1024
+const clientIDAddrMapCapacity = 10240
 
 // How long to wait for ListenAndServe or ListenAndServeTLS to return an error
 // before deciding that it's not going to return.

From 75f770150d6c9943231cb6b8bde0b867137ea26f Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 26 Nov 2021 15:12:46 +0000
Subject: [PATCH 274/385] Add Snowflake Event API interface

---
 common/event/interface.go | 45 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)
 create mode 100644 common/event/interface.go

diff --git a/common/event/interface.go b/common/event/interface.go
new file mode 100644
index 0000000..697f2b7
--- /dev/null
+++ b/common/event/interface.go
@@ -0,0 +1,45 @@
+package event
+
+import "github.com/pion/webrtc/v3"
+
+type SnowflakeEvent interface {
+	IsSnowflakeEvent()
+	String() string
+}
+
+type EventOnOfferCreated struct {
+	SnowflakeEvent
+	WebRTCLocalDescription *webrtc.SessionDescription
+	Error                  error
+}
+
+type EventOnBrokerRendezvous struct {
+	SnowflakeEvent
+	WebRTCRemoteDescription *webrtc.SessionDescription
+	Error                   error
+}
+
+type EventOnSnowflakeConnected struct {
+	SnowflakeEvent
+}
+
+type EventOnSnowflakeConnectionFailed struct {
+	SnowflakeEvent
+	Error error
+}
+
+type SnowflakeEventReceiver interface {
+	// OnNewSnowflakeEvent notify receiver about a new event
+	// This method MUST not block
+	OnNewSnowflakeEvent(event SnowflakeEvent)
+}
+
+type SnowflakeEventDispatcher interface {
+	SnowflakeEventReceiver
+	// AddSnowflakeEventListener allow receiver(s) to receive event notification
+	// when OnNewSnowflakeEvent is called on the dispatcher.
+	// Every event listener added will be called when an event is received by the dispatcher.
+	// The order each listener is called is undefined.
+	AddSnowflakeEventListener(receiver SnowflakeEventReceiver)
+	RemoveSnowflakeEventListener(receiver SnowflakeEventReceiver)
+}

From 5f03f88d730c4b1c298c4c3eeceeba8023f056ff Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 26 Nov 2021 16:00:30 +0000
Subject: [PATCH 275/385] Add Event Bus Implementation

This event bus implementation favours simplicity over efficiency and is not suitable for frequent addition and removal of listeners.
---
 common/event/bus.go | 39 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)
 create mode 100644 common/event/bus.go

diff --git a/common/event/bus.go b/common/event/bus.go
new file mode 100644
index 0000000..7e45779
--- /dev/null
+++ b/common/event/bus.go
@@ -0,0 +1,39 @@
+package event
+
+import "sync"
+
+func NewSnowflakeEventDispatcher() SnowflakeEventDispatcher {
+	return &eventBus{lock: &sync.Mutex{}}
+}
+
+type eventBus struct {
+	lock      *sync.Mutex
+	listeners []SnowflakeEventReceiver
+}
+
+func (e *eventBus) OnNewSnowflakeEvent(event SnowflakeEvent) {
+	e.lock.Lock()
+	defer e.lock.Unlock()
+	for _, v := range e.listeners {
+		v.OnNewSnowflakeEvent(event)
+	}
+}
+
+func (e *eventBus) AddSnowflakeEventListener(receiver SnowflakeEventReceiver) {
+	e.lock.Lock()
+	defer e.lock.Unlock()
+	e.listeners = append(e.listeners, receiver)
+}
+
+func (e *eventBus) RemoveSnowflakeEventListener(receiver SnowflakeEventReceiver) {
+	e.lock.Lock()
+	defer e.lock.Unlock()
+	var newListeners []SnowflakeEventReceiver
+	for _, v := range e.listeners {
+		if v != receiver {
+			newListeners = append(newListeners, v)
+		}
+	}
+	e.listeners = newListeners
+	return
+}

From b5ef18803f08364db15ba66b0189371d5e79d821 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 26 Nov 2021 16:24:21 +0000
Subject: [PATCH 276/385] Add Event Bus Test

---
 common/event/bus_test.go | 32 ++++++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)
 create mode 100644 common/event/bus_test.go

diff --git a/common/event/bus_test.go b/common/event/bus_test.go
new file mode 100644
index 0000000..df40d3d
--- /dev/null
+++ b/common/event/bus_test.go
@@ -0,0 +1,32 @@
+package event
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+type stubReceiver struct {
+	counter int
+}
+
+func (s *stubReceiver) OnNewSnowflakeEvent(event SnowflakeEvent) {
+	s.counter++
+}
+
+func TestBusDispatch(t *testing.T) {
+	EventBus := NewSnowflakeEventDispatcher()
+	StubReceiverA := &stubReceiver{}
+	StubReceiverB := &stubReceiver{}
+	EventBus.AddSnowflakeEventListener(StubReceiverA)
+	EventBus.AddSnowflakeEventListener(StubReceiverB)
+	assert.Equal(t, 0, StubReceiverA.counter)
+	assert.Equal(t, 0, StubReceiverB.counter)
+	EventBus.OnNewSnowflakeEvent(EventOnSnowflakeConnected{})
+	assert.Equal(t, 1, StubReceiverA.counter)
+	assert.Equal(t, 1, StubReceiverB.counter)
+	EventBus.RemoveSnowflakeEventListener(StubReceiverB)
+	EventBus.OnNewSnowflakeEvent(EventOnSnowflakeConnected{})
+	assert.Equal(t, 2, StubReceiverA.counter)
+	assert.Equal(t, 1, StubReceiverB.counter)
+
+}

From cd6d837d85bb915d77bf122a82bb161c9d68cf20 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 29 Nov 2021 12:46:51 +0000
Subject: [PATCH 277/385] Add snowflake event handler to client config

---
 client/lib/snowflake.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 56dd312..fdcb457 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -35,6 +35,7 @@ import (
 	"strings"
 	"time"
 
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/turbotunnel"
 	"github.com/pion/webrtc/v3"
@@ -92,6 +93,9 @@ type ClientConfig struct {
 	// Max is the maximum number of snowflake proxy peers that the client should attempt to
 	// connect to. Defaults to 1.
 	Max int
+	// EventDispatcher is the event bus for snowflake events.
+	// When an important event happens, it will be distributed here.
+	EventDispatcher event.SnowflakeEventDispatcher
 }
 
 // NewSnowflakeClient creates a new Snowflake transport client that can spawn multiple

From c3f09994daa5a512a6d6cea026aeb15162d5d866 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 13 Dec 2021 14:10:06 +0000
Subject: [PATCH 278/385] Add Snowflake Event Reporter for Broker Communication

---
 client/lib/webrtc.go | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index f4b775c..3b496d0 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -9,6 +9,7 @@ import (
 	"sync"
 	"time"
 
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"github.com/pion/webrtc/v3"
 )
 
@@ -31,7 +32,8 @@ type WebRTCPeer struct {
 
 	once sync.Once // Synchronization for PeerConnection destruction
 
-	bytesLogger bytesLogger
+	bytesLogger  bytesLogger
+	eventsLogger event.SnowflakeEventReceiver
 }
 
 // NewWebRTCPeer constructs a WebRTC PeerConnection to a snowflake proxy.
@@ -131,10 +133,21 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel
 	log.Println(c.id, " connecting...")
 	// TODO: When go-webrtc is more stable, it's possible that a new
 	// PeerConnection won't need to be re-prepared each time.
-	if err := c.preparePeerConnection(config); err != nil {
+	err := c.preparePeerConnection(config)
+	localDescription := c.pc.LocalDescription()
+	c.eventsLogger.OnNewSnowflakeEvent(event.EventOnOfferCreated{
+		WebRTCLocalDescription: localDescription,
+		Error:                  err,
+	})
+	if err != nil {
 		return err
 	}
-	answer, err := broker.Negotiate(c.pc.LocalDescription())
+
+	answer, err := broker.Negotiate(localDescription)
+	c.eventsLogger.OnNewSnowflakeEvent(event.EventOnBrokerRendezvous{
+		WebRTCRemoteDescription: answer,
+		Error:                   err,
+	})
 	if err != nil {
 		return err
 	}

From 9a7fcdec03b3bda87a4f1269558816513ce79f66 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 13 Dec 2021 14:25:41 +0000
Subject: [PATCH 279/385] Add Snowflake Event Reporter for Peer Communication

---
 client/lib/webrtc.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index 3b496d0..4068eb8 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -191,6 +191,7 @@ func (c *WebRTCPeer) preparePeerConnection(config *webrtc.Configuration) error {
 		return err
 	}
 	dc.OnOpen(func() {
+		c.eventsLogger.OnNewSnowflakeEvent(&event.EventOnSnowflakeConnected{})
 		log.Println("WebRTC: DataChannel.OnOpen")
 		close(c.open)
 	})
@@ -198,6 +199,9 @@ func (c *WebRTCPeer) preparePeerConnection(config *webrtc.Configuration) error {
 		log.Println("WebRTC: DataChannel.OnClose")
 		c.Close()
 	})
+	dc.OnError(func(err error) {
+		c.eventsLogger.OnNewSnowflakeEvent(&event.EventOnSnowflakeConnectionFailed{Error: err})
+	})
 	dc.OnMessage(func(msg webrtc.DataChannelMessage) {
 		if len(msg.Data) <= 0 {
 			log.Println("0 length message---")

From 36ca610d6bf399a1cf6e6d35cec8e57e6eddf6b4 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 13 Dec 2021 15:05:38 +0000
Subject: [PATCH 280/385] Add NewWebRTCPeer3E Initializer

This name includes [E]vent to reduce merge conflict with forward proxy change set.
---
 client/lib/webrtc.go | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index 4068eb8..01b85f5 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -36,13 +36,22 @@ type WebRTCPeer struct {
 	eventsLogger event.SnowflakeEventReceiver
 }
 
-// NewWebRTCPeer constructs a WebRTC PeerConnection to a snowflake proxy.
+func NewWebRTCPeer(config *webrtc.Configuration,
+	broker *BrokerChannel) (*WebRTCPeer, error) {
+	return NewWebRTCPeer3E(config, broker, nil)
+}
+
+// NewWebRTCPeer3E constructs a WebRTC PeerConnection to a snowflake proxy.
 //
 // The creation of the peer handles the signaling to the Snowflake broker, including
 // the exchange of SDP information, the creation of a PeerConnection, and the establishment
 // of a DataChannel to the Snowflake proxy.
-func NewWebRTCPeer(config *webrtc.Configuration,
-	broker *BrokerChannel) (*WebRTCPeer, error) {
+func NewWebRTCPeer3E(config *webrtc.Configuration,
+	broker *BrokerChannel, eventsLogger event.SnowflakeEventReceiver) (*WebRTCPeer, error) {
+	if eventsLogger == nil {
+		eventsLogger = event.NewSnowflakeEventDispatcher()
+	}
+
 	connection := new(WebRTCPeer)
 	{
 		var buf [8]byte
@@ -59,6 +68,8 @@ func NewWebRTCPeer(config *webrtc.Configuration,
 	// Pipes remain the same even when DataChannel gets switched.
 	connection.recvPipe, connection.writePipe = io.Pipe()
 
+	connection.eventsLogger = eventsLogger
+
 	err := connection.connect(config, broker)
 	if err != nil {
 		connection.Close()

From ac64d17705a48cb6ace310503e14b62c629655d6 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 13 Dec 2021 15:39:59 +0000
Subject: [PATCH 281/385] Add PT Event Logger

---
 client/lib/pt_event_logger.go | 43 +++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)
 create mode 100644 client/lib/pt_event_logger.go

diff --git a/client/lib/pt_event_logger.go b/client/lib/pt_event_logger.go
new file mode 100644
index 0000000..b183005
--- /dev/null
+++ b/client/lib/pt_event_logger.go
@@ -0,0 +1,43 @@
+package snowflake_client
+
+import (
+	"fmt"
+
+	pt "git.torproject.org/pluggable-transports/goptlib.git"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
+)
+
+func NewPTEventLogger() event.SnowflakeEventReceiver {
+	return &ptEventLogger{}
+}
+
+type ptEventLogger struct {
+}
+
+func (p ptEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
+	switch e.(type) {
+	case event.EventOnOfferCreated:
+		e := e.(event.EventOnOfferCreated)
+		if e.Error != nil {
+			pt.Log(pt.LogSeverityError, fmt.Sprintf("offer creation failure %v", e.Error.Error()))
+		} else {
+			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("offer created %v", e.WebRTCLocalDescription))
+		}
+
+	case event.EventOnBrokerRendezvous:
+		e := e.(event.EventOnBrokerRendezvous)
+		if e.Error != nil {
+			pt.Log(pt.LogSeverityError, fmt.Sprintf("broker failure %v", e.Error.Error()))
+		} else {
+			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("broker rendezvous peer %v", e.WebRTCRemoteDescription))
+		}
+
+	case event.EventOnSnowflakeConnected:
+		pt.Log(pt.LogSeverityNotice, fmt.Sprintf("connected"))
+
+	case event.EventOnSnowflakeConnectionFailed:
+		e := e.(event.EventOnSnowflakeConnectionFailed)
+		pt.Log(pt.LogSeverityError, fmt.Sprintf("connection failed %v", e.Error.Error()))
+	}
+
+}

From 128936c82514f4c5511e3861a220e57b11a90cc6 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 13 Dec 2021 15:45:04 +0000
Subject: [PATCH 282/385] Enable PT Event Logger

---
 client/snowflake.go | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/client/snowflake.go b/client/snowflake.go
index d76efbf..0ab71a7 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -17,6 +17,7 @@ import (
 
 	pt "git.torproject.org/pluggable-transports/goptlib.git"
 	sf "git.torproject.org/pluggable-transports/snowflake.git/v2/client/lib"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
 )
 
@@ -170,6 +171,10 @@ func main() {
 
 	iceAddresses := strings.Split(strings.TrimSpace(*iceServersCommas), ",")
 
+	eventLogger := event.NewSnowflakeEventDispatcher()
+
+	eventLogger.AddSnowflakeEventListener(sf.NewPTEventLogger())
+
 	config := sf.ClientConfig{
 		BrokerURL:          *brokerURL,
 		AmpCacheURL:        *ampCacheURL,
@@ -177,6 +182,7 @@ func main() {
 		ICEAddresses:       iceAddresses,
 		KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses,
 		Max:                *max,
+		EventDispatcher:    eventLogger,
 	}
 
 	// Begin goptlib client process.

From 8d2f662c8c04b1e7e6abc626ac9bd3677055d1f8 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 13 Dec 2021 16:28:09 +0000
Subject: [PATCH 283/385] Emit non-pointer type event

---
 client/lib/webrtc.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index 01b85f5..328c3a9 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -202,7 +202,7 @@ func (c *WebRTCPeer) preparePeerConnection(config *webrtc.Configuration) error {
 		return err
 	}
 	dc.OnOpen(func() {
-		c.eventsLogger.OnNewSnowflakeEvent(&event.EventOnSnowflakeConnected{})
+		c.eventsLogger.OnNewSnowflakeEvent(event.EventOnSnowflakeConnected{})
 		log.Println("WebRTC: DataChannel.OnOpen")
 		close(c.open)
 	})
@@ -211,7 +211,7 @@ func (c *WebRTCPeer) preparePeerConnection(config *webrtc.Configuration) error {
 		c.Close()
 	})
 	dc.OnError(func(err error) {
-		c.eventsLogger.OnNewSnowflakeEvent(&event.EventOnSnowflakeConnectionFailed{Error: err})
+		c.eventsLogger.OnNewSnowflakeEvent(event.EventOnSnowflakeConnectionFailed{Error: err})
 	})
 	dc.OnMessage(func(msg webrtc.DataChannelMessage) {
 		if len(msg.Data) <= 0 {

From 7536dd6fb75697bdc85c10e0be6ed0829a338155 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 13 Dec 2021 16:29:10 +0000
Subject: [PATCH 284/385] Add Propagate EventLogger Setting

---
 client/lib/rendezvous.go | 13 +++++++++++--
 client/lib/snowflake.go  |  2 +-
 2 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index c3f0d7a..4c2b240 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -10,6 +10,7 @@ import (
 	"sync"
 	"time"
 
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/util"
@@ -141,10 +142,16 @@ type WebRTCDialer struct {
 	*BrokerChannel
 	webrtcConfig *webrtc.Configuration
 	max          int
+
+	eventLogger event.SnowflakeEventReceiver
 }
 
-// NewWebRTCDialer constructs a new WebRTCDialer.
 func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int) *WebRTCDialer {
+	return NewWebRTCDialer4E(broker, iceServers, max, nil)
+}
+
+// NewWebRTCDialer4E constructs a new WebRTCDialer.
+func NewWebRTCDialer4E(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int, eventLogger event.SnowflakeEventReceiver) *WebRTCDialer {
 	config := webrtc.Configuration{
 		ICEServers: iceServers,
 	}
@@ -153,6 +160,8 @@ func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max i
 		BrokerChannel: broker,
 		webrtcConfig:  &config,
 		max:           max,
+
+		eventLogger: eventLogger,
 	}
 }
 
@@ -160,7 +169,7 @@ func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max i
 func (w WebRTCDialer) Catch() (*WebRTCPeer, error) {
 	// TODO: [#25591] Fetch ICE server information from Broker.
 	// TODO: [#25596] Consider TURN servers here too.
-	return NewWebRTCPeer(w.webrtcConfig, w.BrokerChannel)
+	return NewWebRTCPeer3E(w.webrtcConfig, w.BrokerChannel, w.eventLogger)
 }
 
 // GetMax returns the maximum number of snowflakes to collect.
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index fdcb457..4f9e663 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -135,7 +135,7 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
 	if config.Max > max {
 		max = config.Max
 	}
-	transport := &Transport{dialer: NewWebRTCDialer(broker, iceServers, max)}
+	transport := &Transport{dialer: NewWebRTCDialer4E(broker, iceServers, max, config.EventDispatcher)}
 
 	return transport, nil
 }

From 55bf117d1ae5b362e4d4fc7dc3ff9a1788b84830 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 13 Dec 2021 16:47:23 +0000
Subject: [PATCH 285/385] Reduce PT Event Logger Verbosity

---
 client/lib/pt_event_logger.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/lib/pt_event_logger.go b/client/lib/pt_event_logger.go
index b183005..46b4e05 100644
--- a/client/lib/pt_event_logger.go
+++ b/client/lib/pt_event_logger.go
@@ -21,7 +21,7 @@ func (p ptEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 		if e.Error != nil {
 			pt.Log(pt.LogSeverityError, fmt.Sprintf("offer creation failure %v", e.Error.Error()))
 		} else {
-			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("offer created %v", e.WebRTCLocalDescription))
+			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("offer created"))
 		}
 
 	case event.EventOnBrokerRendezvous:
@@ -29,7 +29,7 @@ func (p ptEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 		if e.Error != nil {
 			pt.Log(pt.LogSeverityError, fmt.Sprintf("broker failure %v", e.Error.Error()))
 		} else {
-			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("broker rendezvous peer %v", e.WebRTCRemoteDescription))
+			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("broker rendezvous peer received"))
 		}
 
 	case event.EventOnSnowflakeConnected:

From 657aaa6ba8b5baadc0d34d9bd0df0914cbc813e3 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 20 Jan 2022 13:17:34 +0000
Subject: [PATCH 286/385] Refactor event logger setting into function call

See also:
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/67#note_2770482
---
 client/lib/snowflake.go | 17 +++++++++++++----
 client/snowflake.go     |  7 +------
 2 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 4f9e663..0f637f2 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -71,6 +71,10 @@ func (addr dummyAddr) String() string  { return "dummy" }
 // https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/master/releases/PTSpecV2.1/Pluggable%20Transport%20Specification%20v2.1%20-%20Go%20Transport%20API.pdf
 type Transport struct {
 	dialer *WebRTCDialer
+
+	// EventDispatcher is the event bus for snowflake events.
+	// When an important event happens, it will be distributed here.
+	eventDispatcher event.SnowflakeEventDispatcher
 }
 
 // ClientConfig defines how the SnowflakeClient will connect to the broker and Snowflake proxies.
@@ -93,9 +97,6 @@ type ClientConfig struct {
 	// Max is the maximum number of snowflake proxy peers that the client should attempt to
 	// connect to. Defaults to 1.
 	Max int
-	// EventDispatcher is the event bus for snowflake events.
-	// When an important event happens, it will be distributed here.
-	EventDispatcher event.SnowflakeEventDispatcher
 }
 
 // NewSnowflakeClient creates a new Snowflake transport client that can spawn multiple
@@ -135,7 +136,8 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
 	if config.Max > max {
 		max = config.Max
 	}
-	transport := &Transport{dialer: NewWebRTCDialer4E(broker, iceServers, max, config.EventDispatcher)}
+	eventsLogger := event.NewSnowflakeEventDispatcher()
+	transport := &Transport{dialer: NewWebRTCDialer4E(broker, iceServers, max, eventsLogger), eventDispatcher: eventsLogger}
 
 	return transport, nil
 }
@@ -191,6 +193,13 @@ func (t *Transport) Dial() (net.Conn, error) {
 	cleanup = nil
 	return &SnowflakeConn{Stream: stream, sess: sess, pconn: pconn, snowflakes: snowflakes}, nil
 }
+func (t *Transport) AddSnowflakeEventListener(receiver event.SnowflakeEventReceiver) {
+	t.eventDispatcher.AddSnowflakeEventListener(receiver)
+}
+
+func (t *Transport) RemoveSnowflakeEventListener(receiver event.SnowflakeEventReceiver) {
+	t.eventDispatcher.RemoveSnowflakeEventListener(receiver)
+}
 
 // SetRendezvousMethod sets the rendezvous method to the Snowflake broker.
 func (t *Transport) SetRendezvousMethod(r RendezvousMethod) {
diff --git a/client/snowflake.go b/client/snowflake.go
index 0ab71a7..5a00206 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -17,7 +17,6 @@ import (
 
 	pt "git.torproject.org/pluggable-transports/goptlib.git"
 	sf "git.torproject.org/pluggable-transports/snowflake.git/v2/client/lib"
-	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
 )
 
@@ -91,6 +90,7 @@ func socksAcceptLoop(ln *pt.SocksListener, config sf.ClientConfig, shutdown chan
 				log.Println("Failed to start snowflake transport: ", err)
 				return
 			}
+			transport.AddSnowflakeEventListener(sf.NewPTEventLogger())
 			err = conn.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0})
 			if err != nil {
 				log.Printf("conn.Grant error: %s", err)
@@ -171,10 +171,6 @@ func main() {
 
 	iceAddresses := strings.Split(strings.TrimSpace(*iceServersCommas), ",")
 
-	eventLogger := event.NewSnowflakeEventDispatcher()
-
-	eventLogger.AddSnowflakeEventListener(sf.NewPTEventLogger())
-
 	config := sf.ClientConfig{
 		BrokerURL:          *brokerURL,
 		AmpCacheURL:        *ampCacheURL,
@@ -182,7 +178,6 @@ func main() {
 		ICEAddresses:       iceAddresses,
 		KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses,
 		Max:                *max,
-		EventDispatcher:    eventLogger,
 	}
 
 	// Begin goptlib client process.

From 6cb82618a0f82a15dda617957af8bf08780a482b Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 24 Jan 2022 11:51:49 +0000
Subject: [PATCH 287/385] Refactor WebRTC Peer,Dialer's name to be readable

See also:
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/67#note_2771666
---
 client/lib/rendezvous.go | 8 ++++----
 client/lib/snowflake.go  | 2 +-
 client/lib/webrtc.go     | 6 +++---
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index 4c2b240..98cd4d6 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -147,11 +147,11 @@ type WebRTCDialer struct {
 }
 
 func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int) *WebRTCDialer {
-	return NewWebRTCDialer4E(broker, iceServers, max, nil)
+	return NewWebRTCDialerWithEvents(broker, iceServers, max, nil)
 }
 
-// NewWebRTCDialer4E constructs a new WebRTCDialer.
-func NewWebRTCDialer4E(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int, eventLogger event.SnowflakeEventReceiver) *WebRTCDialer {
+// NewWebRTCDialerWithEvents constructs a new WebRTCDialer.
+func NewWebRTCDialerWithEvents(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int, eventLogger event.SnowflakeEventReceiver) *WebRTCDialer {
 	config := webrtc.Configuration{
 		ICEServers: iceServers,
 	}
@@ -169,7 +169,7 @@ func NewWebRTCDialer4E(broker *BrokerChannel, iceServers []webrtc.ICEServer, max
 func (w WebRTCDialer) Catch() (*WebRTCPeer, error) {
 	// TODO: [#25591] Fetch ICE server information from Broker.
 	// TODO: [#25596] Consider TURN servers here too.
-	return NewWebRTCPeer3E(w.webrtcConfig, w.BrokerChannel, w.eventLogger)
+	return NewWebRTCPeerWithEvents(w.webrtcConfig, w.BrokerChannel, w.eventLogger)
 }
 
 // GetMax returns the maximum number of snowflakes to collect.
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 0f637f2..594c62c 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -137,7 +137,7 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
 		max = config.Max
 	}
 	eventsLogger := event.NewSnowflakeEventDispatcher()
-	transport := &Transport{dialer: NewWebRTCDialer4E(broker, iceServers, max, eventsLogger), eventDispatcher: eventsLogger}
+	transport := &Transport{dialer: NewWebRTCDialerWithEvents(broker, iceServers, max, eventsLogger), eventDispatcher: eventsLogger}
 
 	return transport, nil
 }
diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index 328c3a9..282b54d 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -38,15 +38,15 @@ type WebRTCPeer struct {
 
 func NewWebRTCPeer(config *webrtc.Configuration,
 	broker *BrokerChannel) (*WebRTCPeer, error) {
-	return NewWebRTCPeer3E(config, broker, nil)
+	return NewWebRTCPeerWithEvents(config, broker, nil)
 }
 
-// NewWebRTCPeer3E constructs a WebRTC PeerConnection to a snowflake proxy.
+// NewWebRTCPeerWithEvents constructs a WebRTC PeerConnection to a snowflake proxy.
 //
 // The creation of the peer handles the signaling to the Snowflake broker, including
 // the exchange of SDP information, the creation of a PeerConnection, and the establishment
 // of a DataChannel to the Snowflake proxy.
-func NewWebRTCPeer3E(config *webrtc.Configuration,
+func NewWebRTCPeerWithEvents(config *webrtc.Configuration,
 	broker *BrokerChannel, eventsLogger event.SnowflakeEventReceiver) (*WebRTCPeer, error) {
 	if eventsLogger == nil {
 		eventsLogger = event.NewSnowflakeEventDispatcher()

From 91379a42f3c9c36a09c8cfd0769bffed626ffe46 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 20 Dec 2021 12:29:23 +0000
Subject: [PATCH 288/385] Add Raw Data Output for bytesLogger

---
 proxy/lib/util.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/proxy/lib/util.go b/proxy/lib/util.go
index 5055187..a9ac8aa 100644
--- a/proxy/lib/util.go
+++ b/proxy/lib/util.go
@@ -11,6 +11,7 @@ type bytesLogger interface {
 	AddOutbound(int)
 	AddInbound(int)
 	ThroughputSummary() string
+	GetStat() (in int, out int)
 }
 
 // bytesNullLogger Default bytesLogger does nothing.
@@ -25,6 +26,8 @@ func (b bytesNullLogger) AddInbound(amount int) {}
 // ThroughputSummary in bytesNullLogger does nothing
 func (b bytesNullLogger) ThroughputSummary() string { return "" }
 
+func (b bytesNullLogger) GetStat() (in int, out int) { return -1, -1 }
+
 // bytesSyncLogger uses channels to safely log from multiple sources with output
 // occuring at reasonable intervals.
 type bytesSyncLogger struct {
@@ -92,3 +95,5 @@ func (b *bytesSyncLogger) ThroughputSummary() string {
 	t := time.Now()
 	return fmt.Sprintf("Traffic throughput (up|down): %d %s|%d %s -- (%d OnMessages, %d Sends, over %d seconds)", inbound, inUnit, outbound, outUnit, b.outEvents, b.inEvents, int(t.Sub(b.start).Seconds()))
 }
+
+func (b *bytesSyncLogger) GetStat() (in int, out int) { return b.inbound, b.outbound }

From d64af3139496481436f8870f4f43f43dca0d4552 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 20 Dec 2021 13:23:33 +0000
Subject: [PATCH 289/385] Add EventOnProxyConnectionOver Event

---
 common/event/interface.go | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/common/event/interface.go b/common/event/interface.go
index 697f2b7..b41d7c3 100644
--- a/common/event/interface.go
+++ b/common/event/interface.go
@@ -28,6 +28,12 @@ type EventOnSnowflakeConnectionFailed struct {
 	Error error
 }
 
+type EventOnProxyConnectionOver struct {
+	SnowflakeEvent
+	InboundTraffic  int
+	OutboundTraffic int
+}
+
 type SnowflakeEventReceiver interface {
 	// OnNewSnowflakeEvent notify receiver about a new event
 	// This method MUST not block

From e4305a4d2b01f248f70546f745f64b3373f84a2e Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 20 Dec 2021 13:36:22 +0000
Subject: [PATCH 290/385] Add EventOnProxyConnectionOver Reporting

---
 proxy/lib/snowflake.go  | 6 ++++++
 proxy/lib/webrtcconn.go | 2 ++
 2 files changed, 8 insertions(+)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index a9ac399..cef5644 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -40,6 +40,7 @@ import (
 	"sync"
 	"time"
 
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/task"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/util"
@@ -350,6 +351,11 @@ func (sf *SnowflakeProxy) makePeerConnectionFromOffer(sdp *webrtc.SessionDescrip
 			defer conn.lock.Unlock()
 			log.Println("OnClose channel")
 			log.Println(conn.bytesLogger.ThroughputSummary())
+			in, out := conn.bytesLogger.GetStat()
+			conn.eventLogger.OnNewSnowflakeEvent(event.EventOnProxyConnectionOver{
+				InboundTraffic:  in,
+				OutboundTraffic: out,
+			})
 			conn.dc = nil
 			dc.Close()
 			pw.Close()
diff --git a/proxy/lib/webrtcconn.go b/proxy/lib/webrtcconn.go
index 6e16bec..919a679 100644
--- a/proxy/lib/webrtcconn.go
+++ b/proxy/lib/webrtcconn.go
@@ -9,6 +9,7 @@ import (
 	"sync"
 	"time"
 
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"github.com/pion/ice/v2"
 	"github.com/pion/sdp/v3"
 	"github.com/pion/webrtc/v3"
@@ -30,6 +31,7 @@ type webRTCConn struct {
 	once sync.Once  // Synchronization for PeerConnection destruction
 
 	bytesLogger bytesLogger
+	eventLogger event.SnowflakeEventReceiver
 }
 
 func (c *webRTCConn) Read(b []byte) (int, error) {

From f12cfe6a9f682c479314440ac9d53389cb38a54f Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 20 Dec 2021 13:45:53 +0000
Subject: [PATCH 291/385] Add proxy event logger state propagate

---
 proxy/lib/snowflake.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index cef5644..8747f66 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -115,6 +115,7 @@ type SnowflakeProxy struct {
 	NATProbeURL string
 	// NATTypeMeasurementInterval is time before NAT type is retested
 	NATTypeMeasurementInterval time.Duration
+	EventDispatcher            event.SnowflakeEventDispatcher
 	shutdown                   chan struct{}
 }
 
@@ -340,7 +341,7 @@ func (sf *SnowflakeProxy) makePeerConnectionFromOffer(sdp *webrtc.SessionDescrip
 		close(dataChan)
 
 		pr, pw := io.Pipe()
-		conn := &webRTCConn{pc: pc, dc: dc, pr: pr}
+		conn := &webRTCConn{pc: pc, dc: dc, pr: pr, eventLogger: sf.EventDispatcher}
 		conn.bytesLogger = newBytesSyncLogger()
 
 		dc.OnOpen(func() {
@@ -524,6 +525,9 @@ func (sf *SnowflakeProxy) Start() error {
 	if sf.NATProbeURL == "" {
 		sf.NATProbeURL = DefaultNATProbeURL
 	}
+	if sf.EventDispatcher == nil {
+		sf.EventDispatcher = event.NewSnowflakeEventDispatcher()
+	}
 
 	broker, err = newSignalingServer(sf.BrokerURL, sf.KeepLocalAddresses)
 	if err != nil {

From 9208364475bddb186cb9d6fba6508a420ba1734d Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 20 Dec 2021 14:46:18 +0000
Subject: [PATCH 292/385] Extract traffic formatter

---
 proxy/lib/util.go | 34 +++++++++++++++++-----------------
 1 file changed, 17 insertions(+), 17 deletions(-)

diff --git a/proxy/lib/util.go b/proxy/lib/util.go
index a9ac8aa..c7b3684 100644
--- a/proxy/lib/util.go
+++ b/proxy/lib/util.go
@@ -72,28 +72,28 @@ func (b *bytesSyncLogger) AddInbound(amount int) {
 
 // ThroughputSummary view a formatted summary of the throughput totals
 func (b *bytesSyncLogger) ThroughputSummary() string {
-	var inUnit, outUnit string
-	units := []string{"B", "KB", "MB", "GB"}
-
 	inbound := b.inbound
 	outbound := b.outbound
 
-	for i, u := range units {
-		inUnit = u
-		if (inbound < 1000) || (i == len(units)-1) {
-			break
-		}
-		inbound = inbound / 1000
-	}
-	for i, u := range units {
-		outUnit = u
-		if (outbound < 1000) || (i == len(units)-1) {
-			break
-		}
-		outbound = outbound / 1000
-	}
+	inbound, inUnit := formatTraffic(inbound)
+	outbound, outUnit := formatTraffic(outbound)
+
 	t := time.Now()
 	return fmt.Sprintf("Traffic throughput (up|down): %d %s|%d %s -- (%d OnMessages, %d Sends, over %d seconds)", inbound, inUnit, outbound, outUnit, b.outEvents, b.inEvents, int(t.Sub(b.start).Seconds()))
 }
 
 func (b *bytesSyncLogger) GetStat() (in int, out int) { return b.inbound, b.outbound }
+
+func formatTraffic(amount int) (value int, unit string) {
+	value = amount
+	units := []string{"B", "KB", "MB", "GB"}
+	for i, u := range units {
+		unit = u
+		if (value < 1000) || (i == len(units)-1) {
+			break
+		}
+		value = value / 1000
+	}
+	return
+
+}

From 1116bc81c86580316e13b07ecd8d7f0a3ac5ffd5 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 20 Dec 2021 15:42:16 +0000
Subject: [PATCH 293/385] Add Proxy Event Logger

---
 proxy/lib/pt_event_logger.go | 49 ++++++++++++++++++++++++++++++++++++
 proxy/main.go                | 10 ++++++++
 2 files changed, 59 insertions(+)
 create mode 100644 proxy/lib/pt_event_logger.go

diff --git a/proxy/lib/pt_event_logger.go b/proxy/lib/pt_event_logger.go
new file mode 100644
index 0000000..5804614
--- /dev/null
+++ b/proxy/lib/pt_event_logger.go
@@ -0,0 +1,49 @@
+package snowflake_proxy
+
+import (
+	"fmt"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/task"
+	"time"
+
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
+)
+
+func NewProxyEventLogger(logPeriod time.Duration) event.SnowflakeEventReceiver {
+	el := &logEventLogger{}
+	el.task = &task.Periodic{Interval: logPeriod, Execute: el.logTick}
+	el.task.Start()
+	return el
+}
+
+type logEventLogger struct {
+	inboundSum      int
+	outboundSum     int
+	connectionCount int
+	logPeriod       time.Duration
+	task            *task.Periodic
+}
+
+func (p *logEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
+	switch e.(type) {
+	case event.EventOnProxyConnectionOver:
+		e := e.(event.EventOnProxyConnectionOver)
+		p.inboundSum += e.InboundTraffic
+		p.outboundSum += e.OutboundTraffic
+		p.connectionCount += 1
+	}
+}
+
+func (p *logEventLogger) logTick() error {
+	inbound, inboundUnit := formatTraffic(p.inboundSum)
+	outbound, outboundUnit := formatTraffic(p.inboundSum)
+	fmt.Printf("In the last %v, there are %v connections. Traffic Relaied ↑ %v %v, ↓ %v %v.",
+		p.logPeriod.String(), p.connectionCount, inbound, inboundUnit, outbound, outboundUnit)
+	p.outboundSum = 0
+	p.inboundSum = 0
+	p.connectionCount = 0
+	return nil
+}
+
+func (p *logEventLogger) Close() error {
+	return p.task.Close()
+}
diff --git a/proxy/main.go b/proxy/main.go
index b85dde0..de31913 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"flag"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"io"
 	"log"
 	"os"
@@ -21,9 +22,17 @@ func main() {
 	relayURL := flag.String("relay", sf.DefaultRelayURL, "websocket relay URL")
 	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\". ")
+	SummaryInterval := flag.Duration("summary-interval", time.Hour,
+		"the time interval to output summary, 0s disables retest. Valid time units are \"s\", \"m\", \"h\". ")
 
 	flag.Parse()
 
+	periodicEventLogger := sf.NewProxyEventLogger(*SummaryInterval)
+
+	eventLogger := event.NewSnowflakeEventDispatcher()
+
+	eventLogger.AddSnowflakeEventListener(periodicEventLogger)
+
 	proxy := sf.SnowflakeProxy{
 		Capacity:           uint(*capacity),
 		STUNURL:            *stunURL,
@@ -32,6 +41,7 @@ func main() {
 		RelayURL:           *relayURL,
 
 		NATTypeMeasurementInterval: *NATTypeMeasurementInterval,
+		EventDispatcher:            eventLogger,
 	}
 
 	var logOutput io.Writer = os.Stderr

From 88af9da4a25c26169cadaee9256ce7539db17b19 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 20 Dec 2021 16:22:55 +0000
Subject: [PATCH 294/385] Fix ProxyEventLogger output

---
 proxy/lib/pt_event_logger.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/proxy/lib/pt_event_logger.go b/proxy/lib/pt_event_logger.go
index 5804614..6c4f4e9 100644
--- a/proxy/lib/pt_event_logger.go
+++ b/proxy/lib/pt_event_logger.go
@@ -9,7 +9,7 @@ import (
 )
 
 func NewProxyEventLogger(logPeriod time.Duration) event.SnowflakeEventReceiver {
-	el := &logEventLogger{}
+	el := &logEventLogger{logPeriod: logPeriod}
 	el.task = &task.Periodic{Interval: logPeriod, Execute: el.logTick}
 	el.task.Start()
 	return el
@@ -36,7 +36,7 @@ func (p *logEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 func (p *logEventLogger) logTick() error {
 	inbound, inboundUnit := formatTraffic(p.inboundSum)
 	outbound, outboundUnit := formatTraffic(p.inboundSum)
-	fmt.Printf("In the last %v, there are %v connections. Traffic Relaied ↑ %v %v, ↓ %v %v.",
+	fmt.Printf("In the last %v, there are %v connections. Traffic Relaied ↑ %v %v, ↓ %v %v.\n",
 		p.logPeriod.String(), p.connectionCount, inbound, inboundUnit, outbound, outboundUnit)
 	p.outboundSum = 0
 	p.inboundSum = 0

From eb229d512b3b1015e28dfcb744000b66a66e648a Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 20 Dec 2021 16:27:12 +0000
Subject: [PATCH 295/385] Fix ProxyEventLogger output

---
 proxy/lib/pt_event_logger.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxy/lib/pt_event_logger.go b/proxy/lib/pt_event_logger.go
index 6c4f4e9..f6552c8 100644
--- a/proxy/lib/pt_event_logger.go
+++ b/proxy/lib/pt_event_logger.go
@@ -36,7 +36,7 @@ func (p *logEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 func (p *logEventLogger) logTick() error {
 	inbound, inboundUnit := formatTraffic(p.inboundSum)
 	outbound, outboundUnit := formatTraffic(p.inboundSum)
-	fmt.Printf("In the last %v, there are %v connections. Traffic Relaied ↑ %v %v, ↓ %v %v.\n",
+	fmt.Printf("In the last %v, there are %v connections. Traffic Relayed ↑ %v %v, ↓ %v %v.\n",
 		p.logPeriod.String(), p.connectionCount, inbound, inboundUnit, outbound, outboundUnit)
 	p.outboundSum = 0
 	p.inboundSum = 0

From bf3bd635f71fd5faae1ebaa05d5987cfa464b30b Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 26 Jan 2022 13:39:12 +0000
Subject: [PATCH 296/385] Fix build break in Go 1.16 for missing import

See also:
https://gitlab.torproject.org/shelikhoo/snowflake/-/jobs/86751
---
 go.mod | 1 +
 1 file changed, 1 insertion(+)

diff --git a/go.mod b/go.mod
index e8038f1..03541eb 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
 	github.com/prometheus/client_golang v1.10.0
 	github.com/prometheus/client_model v0.2.0
 	github.com/smartystreets/goconvey v1.6.4
+	github.com/stretchr/testify v1.7.0 // indirect
 	github.com/xtaci/kcp-go/v5 v5.6.1
 	github.com/xtaci/smux v1.5.15
 	gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01

From e828b0607662c7325f4d1bbf4c5072ce81f38fb9 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 28 Jan 2022 14:46:45 +0000
Subject: [PATCH 297/385] Use log instead of fmt in proxy event logger

See also:
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/72#note_2772839
---
 proxy/lib/pt_event_logger.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/proxy/lib/pt_event_logger.go b/proxy/lib/pt_event_logger.go
index f6552c8..b0dbf60 100644
--- a/proxy/lib/pt_event_logger.go
+++ b/proxy/lib/pt_event_logger.go
@@ -1,8 +1,8 @@
 package snowflake_proxy
 
 import (
-	"fmt"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/task"
+	"log"
 	"time"
 
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
@@ -36,7 +36,7 @@ func (p *logEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 func (p *logEventLogger) logTick() error {
 	inbound, inboundUnit := formatTraffic(p.inboundSum)
 	outbound, outboundUnit := formatTraffic(p.inboundSum)
-	fmt.Printf("In the last %v, there are %v connections. Traffic Relayed ↑ %v %v, ↓ %v %v.\n",
+	log.Printf("In the last %v, there are %v connections. Traffic Relayed ↑ %v %v, ↓ %v %v.\n",
 		p.logPeriod.String(), p.connectionCount, inbound, inboundUnit, outbound, outboundUnit)
 	p.outboundSum = 0
 	p.inboundSum = 0

From 00e8415d8eafb7fc4d75b6786706ef861b253849 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 2 Feb 2022 11:35:12 +0000
Subject: [PATCH 298/385] Add verbosity switch to suppress diagnostic output

---
 proxy/lib/pt_event_logger.go |  9 ++++++---
 proxy/main.go                | 18 ++++++++++++------
 2 files changed, 18 insertions(+), 9 deletions(-)

diff --git a/proxy/lib/pt_event_logger.go b/proxy/lib/pt_event_logger.go
index b0dbf60..7990a44 100644
--- a/proxy/lib/pt_event_logger.go
+++ b/proxy/lib/pt_event_logger.go
@@ -2,14 +2,16 @@ package snowflake_proxy
 
 import (
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/task"
+	"io"
 	"log"
 	"time"
 
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 )
 
-func NewProxyEventLogger(logPeriod time.Duration) event.SnowflakeEventReceiver {
-	el := &logEventLogger{logPeriod: logPeriod}
+func NewProxyEventLogger(logPeriod time.Duration, output io.Writer) event.SnowflakeEventReceiver {
+	logger := log.New(output, "", log.LstdFlags|log.LUTC)
+	el := &logEventLogger{logPeriod: logPeriod, logger: logger}
 	el.task = &task.Periodic{Interval: logPeriod, Execute: el.logTick}
 	el.task.Start()
 	return el
@@ -21,6 +23,7 @@ type logEventLogger struct {
 	connectionCount int
 	logPeriod       time.Duration
 	task            *task.Periodic
+	logger          *log.Logger
 }
 
 func (p *logEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
@@ -36,7 +39,7 @@ func (p *logEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 func (p *logEventLogger) logTick() error {
 	inbound, inboundUnit := formatTraffic(p.inboundSum)
 	outbound, outboundUnit := formatTraffic(p.inboundSum)
-	log.Printf("In the last %v, there are %v connections. Traffic Relayed ↑ %v %v, ↓ %v %v.\n",
+	p.logger.Printf("In the last %v, there are %v connections. Traffic Relayed ↑ %v %v, ↓ %v %v.\n",
 		p.logPeriod.String(), p.connectionCount, inbound, inboundUnit, outbound, outboundUnit)
 	p.outboundSum = 0
 	p.inboundSum = 0
diff --git a/proxy/main.go b/proxy/main.go
index de31913..7d025ea 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -4,6 +4,7 @@ import (
 	"flag"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"io"
+	"io/ioutil"
 	"log"
 	"os"
 	"time"
@@ -24,15 +25,12 @@ func main() {
 		"the time interval in second before NAT type is retested, 0s disables retest. Valid time units are \"s\", \"m\", \"h\". ")
 	SummaryInterval := flag.Duration("summary-interval", time.Hour,
 		"the time interval to output summary, 0s disables retest. Valid time units are \"s\", \"m\", \"h\". ")
+	verboseLogging := flag.Bool("verbose", false, "increase log verbosity")
 
 	flag.Parse()
 
-	periodicEventLogger := sf.NewProxyEventLogger(*SummaryInterval)
-
 	eventLogger := event.NewSnowflakeEventDispatcher()
 
-	eventLogger.AddSnowflakeEventListener(periodicEventLogger)
-
 	proxy := sf.SnowflakeProxy{
 		Capacity:           uint(*capacity),
 		STUNURL:            *stunURL,
@@ -45,16 +43,21 @@ func main() {
 	}
 
 	var logOutput io.Writer = os.Stderr
+	var eventlogOutput io.Writer = os.Stderr
 	log.SetFlags(log.LstdFlags | log.LUTC)
 
-	log.SetFlags(log.LstdFlags | log.LUTC)
+	if !*verboseLogging {
+		logOutput = ioutil.Discard
+	}
+
 	if *logFilename != "" {
 		f, err := os.OpenFile(*logFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
 		if err != nil {
 			log.Fatal(err)
 		}
 		defer f.Close()
-		logOutput = io.MultiWriter(os.Stderr, f)
+		logOutput = io.MultiWriter(logOutput, f)
+		eventlogOutput = io.MultiWriter(eventlogOutput, f)
 	}
 	if *unsafeLogging {
 		log.SetOutput(logOutput)
@@ -62,6 +65,9 @@ func main() {
 		log.SetOutput(&safelog.LogScrubber{Output: logOutput})
 	}
 
+	periodicEventLogger := sf.NewProxyEventLogger(*SummaryInterval, eventlogOutput)
+	eventLogger.AddSnowflakeEventListener(periodicEventLogger)
+
 	err := proxy.Start()
 	if err != nil {
 		log.Fatal(err)

From c0b35076c93b19de97989eb1fd5eed74f45635db Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Wed, 2 Feb 2022 19:13:03 -0500
Subject: [PATCH 299/385] Remove support for oneshot mode

Due to a bug (#40098), legacy oneshot connections have not worked for
awhile. Connections without the turbotunnel token would cause the server
to crash. This fixes that bug by removing support altogether and simply
closes the connection.
---
 server/lib/http.go      | 28 ++++------------------------
 server/lib/snowflake.go |  2 +-
 2 files changed, 5 insertions(+), 25 deletions(-)

diff --git a/server/lib/http.go b/server/lib/http.go
index 5f214a1..0aba81e 100644
--- a/server/lib/http.go
+++ b/server/lib/http.go
@@ -48,23 +48,10 @@ var upgrader = websocket.Upgrader{
 // attached to the WebSocket connection and every session.
 var clientIDAddrMap = newClientIDMap(clientIDAddrMapCapacity)
 
-// overrideReadConn is a net.Conn with an overridden Read method. Compare to
-// recordingConn at
-// https://dave.cheney.net/2015/05/22/struct-composition-with-go.
-type overrideReadConn struct {
-	net.Conn
-	io.Reader
-}
-
-func (conn *overrideReadConn) Read(p []byte) (int, error) {
-	return conn.Reader.Read(p)
-}
-
 type httpHandler struct {
 	// pconn is the adapter layer between stream-oriented WebSocket
 	// connections and the packet-oriented KCP layer.
 	pconn *turbotunnel.QueuePacketConn
-	ln    *SnowflakeListener
 }
 
 func (handler *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -99,10 +86,10 @@ func (handler *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	default:
 		// We didn't find a matching token, which means that we are
 		// dealing with a client that doesn't know about such things.
-		// "Unread" the token by constructing a new Reader and pass it
-		// to the old one-session-per-WebSocket mode.
-		conn2 := &overrideReadConn{Conn: conn, Reader: io.MultiReader(bytes.NewReader(token[:]), conn)}
-		err = oneshotMode(conn2, addr, handler.ln)
+		// Close the conn as we no longer support the old
+		// one-session-per-WebSocket mode.
+		log.Println("Received unsupported oneshot connection")
+		return
 	}
 	if err != nil {
 		log.Println(err)
@@ -110,13 +97,6 @@ func (handler *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-// oneshotMode handles clients that did not send turbotunnel.Token at the start
-// of their stream. These clients use the WebSocket as a raw pipe, and expect
-// their session to begin and end when this single WebSocket does.
-func oneshotMode(conn net.Conn, addr net.Addr, ln *SnowflakeListener) error {
-	return ln.queueConn(&SnowflakeClientConn{Conn: conn, address: addr})
-}
-
 // turbotunnelMode handles clients that sent turbotunnel.Token at the start of
 // their stream. These clients expect to send and receive encapsulated packets,
 // with a long-lived session identified by ClientID.
diff --git a/server/lib/snowflake.go b/server/lib/snowflake.go
index 31b6a20..a1051e0 100644
--- a/server/lib/snowflake.go
+++ b/server/lib/snowflake.go
@@ -279,7 +279,7 @@ func (l *SnowflakeListener) queueConn(conn net.Conn) error {
 	}
 }
 
-// SnowflakeClientConn is a wrapper for the underlying oneshot or turbotunnel
+// SnowflakeClientConn is a wrapper for the underlying turbotunnel
 // conn. We need to reference our client address map to determine the
 // remote address
 type SnowflakeClientConn struct {

From e6e5e20ae8b5883b37f5dea656540825e237d820 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Tue, 8 Feb 2022 10:56:19 -0500
Subject: [PATCH 300/385] Update ChangeLog for v2.1.0 release

---
 ChangeLog | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/ChangeLog b/ChangeLog
index e4b3998..00f71bf 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,15 @@
+Changes in version v2.1.0 - 2022-02-08
+
+- Issue 40098: Remove support for legacy one shot mode
+- Issue 40079: Make connection summary at proxy privacy preserving
+- Issue 40076: Add snowflake event API for notifications of connection events
+- Issue 40084: Increase capacity of client address map at the server
+- Issue 40060: Further clean up snowflake server logs
+- Issue 40089: Validate proxy and client supplied strings at broker
+- Issue 40014: Update version of DTLS library to include fingerprinting fixes
+- Issue 40075: Support recurring NAT type check in standalone proxy
+
+
 Changes in version v2.0.0 - 2021-11-04
 
 - Turn the standalone snowflake proxy code into a library

From bcc162898a9b085d8543e1aaeff7950b4431c5f3 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Tue, 8 Feb 2022 13:00:43 -0500
Subject: [PATCH 301/385] Initialize SnowflakeListener.closed

Fixes a bug where an uninitialized channel causes a panic when closed
(#40099).
---
 server/lib/snowflake.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/server/lib/snowflake.go b/server/lib/snowflake.go
index a1051e0..44f287f 100644
--- a/server/lib/snowflake.go
+++ b/server/lib/snowflake.go
@@ -75,7 +75,11 @@ func NewSnowflakeServer(getCertificate func(*tls.ClientHelloInfo) (*tls.Certific
 // Listen starts a listener on addr that will accept both turbotunnel
 // and legacy Snowflake connections.
 func (t *Transport) Listen(addr net.Addr) (*SnowflakeListener, error) {
-	listener := &SnowflakeListener{addr: addr, queue: make(chan net.Conn, 65534)}
+	listener := &SnowflakeListener{
+		addr:   addr,
+		queue:  make(chan net.Conn, 65534),
+		closed: make(chan struct{}),
+	}
 
 	handler := httpHandler{
 		// pconn is shared among all connections to this server. It

From 2c008d6589e37e77f01a364ab414d6a95218de36 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Mon, 14 Feb 2022 14:00:01 -0500
Subject: [PATCH 302/385] Add connection failure events for proxy timeouts

This change adds two new connection failure events for snowflake
proxies. One fires when the datachannel times out and another fires when
the connection to the proxy goes stale.
---
 client/lib/webrtc.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index 282b54d..d5264a9 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -129,6 +129,8 @@ func (c *WebRTCPeer) checkForStaleness(timeout time.Duration) {
 		if time.Since(lastReceive) > timeout {
 			log.Printf("WebRTC: No messages received for %v -- closing stale connection.",
 				timeout)
+			err := errors.New("no messages received, closing stale connection")
+			c.eventsLogger.OnNewSnowflakeEvent(event.EventOnSnowflakeConnectionFailed{Error: err})
 			c.Close()
 			return
 		}
@@ -174,7 +176,9 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel
 	case <-c.open:
 	case <-time.After(DataChannelTimeout):
 		c.transport.Close()
-		return errors.New("timeout waiting for DataChannel.OnOpen")
+		err = errors.New("timeout waiting for DataChannel.OnOpen")
+		c.eventsLogger.OnNewSnowflakeEvent(event.EventOnSnowflakeConnectionFailed{Error: err})
+		return err
 	}
 
 	go c.checkForStaleness(SnowflakeTimeout)

From 3547b284a9abf219053caaabee3a63f6793d8670 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Mon, 14 Feb 2022 14:09:16 -0500
Subject: [PATCH 303/385] Make all snowflake events LogSeverityNotice

Let's reserve Tor error logs for more severe events that indicate
a client-side bug or absolute failure. By default, tor logs at severity
level notice (and above).
---
 client/lib/pt_event_logger.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/client/lib/pt_event_logger.go b/client/lib/pt_event_logger.go
index 46b4e05..25883c4 100644
--- a/client/lib/pt_event_logger.go
+++ b/client/lib/pt_event_logger.go
@@ -19,7 +19,7 @@ func (p ptEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 	case event.EventOnOfferCreated:
 		e := e.(event.EventOnOfferCreated)
 		if e.Error != nil {
-			pt.Log(pt.LogSeverityError, fmt.Sprintf("offer creation failure %v", e.Error.Error()))
+			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("offer creation failure %v", e.Error.Error()))
 		} else {
 			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("offer created"))
 		}
@@ -27,7 +27,7 @@ func (p ptEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 	case event.EventOnBrokerRendezvous:
 		e := e.(event.EventOnBrokerRendezvous)
 		if e.Error != nil {
-			pt.Log(pt.LogSeverityError, fmt.Sprintf("broker failure %v", e.Error.Error()))
+			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("broker failure %v", e.Error.Error()))
 		} else {
 			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("broker rendezvous peer received"))
 		}
@@ -37,7 +37,7 @@ func (p ptEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 
 	case event.EventOnSnowflakeConnectionFailed:
 		e := e.(event.EventOnSnowflakeConnectionFailed)
-		pt.Log(pt.LogSeverityError, fmt.Sprintf("connection failed %v", e.Error.Error()))
+		pt.Log(pt.LogSeverityNotice, fmt.Sprintf("connection failed %v", e.Error.Error()))
 	}
 
 }

From 01ae5b56e8399d29aa18605dc9add913d84dc553 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Mon, 14 Feb 2022 15:11:41 -0500
Subject: [PATCH 304/385] Fix client library test

Initialize eventsLogger for WebRTCPeer in client library test.
---
 client/lib/lib_test.go | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go
index f741775..45e8fe2 100644
--- a/client/lib/lib_test.go
+++ b/client/lib/lib_test.go
@@ -171,7 +171,8 @@ func TestSnowflakeClient(t *testing.T) {
 
 func TestWebRTCPeer(t *testing.T) {
 	Convey("WebRTCPeer", t, func(c C) {
-		p := &WebRTCPeer{closed: make(chan struct{})}
+		eventsLogger := NewPTEventLogger()
+		p := &WebRTCPeer{closed: make(chan struct{}), eventsLogger: eventsLogger}
 		Convey("checks for staleness", func() {
 			go p.checkForStaleness(time.Second)
 			<-time.After(2 * time.Second)

From e18a4ac147a417991e91a6c30c355c23ba78b5ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Anna=20=E2=80=9CCyberTailor=E2=80=9D?= 
Date: Wed, 23 Feb 2022 04:07:59 +0500
Subject: [PATCH 305/385] Generate tarballs in release CI

The `generate_tarball` job vendors all Go modules to make packaging for
distributions easier.
---
 .gitlab-ci.yml | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5ec32fd..2ef29ef 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -166,3 +166,40 @@ debian-testing:
   <<: *test-template
   script:
     - *go-test
+
+generate_tarball:
+  stage: deploy
+  image: golang:1.17-stretch
+  rules:
+    - if: $CI_COMMIT_TAG
+  script:
+    - go mod vendor
+    - tar czf ${CI_PROJECT_NAME}-${CI_COMMIT_TAG#v}.tar.gz --transform "s,^,${CI_PROJECT_NAME}-${CI_COMMIT_TAG#v}/," *
+  after_script:
+    - echo TAR_JOB_ID=$CI_JOB_ID >> generate_tarball.env
+  artifacts:
+    paths:
+      - ${CI_PROJECT_NAME}-${CI_COMMIT_TAG#v}.tar.gz
+    reports:
+      dotenv: generate_tarball.env
+
+release-job:
+  stage: deploy
+  image: registry.gitlab.com/gitlab-org/release-cli:latest
+  rules:
+    - if: $CI_COMMIT_TAG
+  needs:
+    - job: generate_tarball
+      artifacts: true
+  script:
+    - echo "running release_job"
+  release:
+    name: 'Release $CI_COMMIT_TAG'
+    description: 'Created using the release-cli'
+    tag_name: '$CI_COMMIT_TAG'
+    ref: '$CI_COMMIT_TAG'
+    assets:
+      links:
+        - name: '${CI_PROJECT_NAME}-${CI_COMMIT_TAG#v}.tar.gz'
+          url: '${CI_PROJECT_URL}/-/jobs/${TAR_JOB_ID}/artifacts/file/${CI_PROJECT_NAME}-${CI_COMMIT_TAG#v}.tar.gz'
+ 

From df22114fced1ade605cf53d2c87ffd878ed17cfc Mon Sep 17 00:00:00 2001
From: pjsier 
Date: Mon, 28 Feb 2022 18:38:17 -0600
Subject: [PATCH 306/385] Fix proxy logging verb tense

---
 proxy/lib/pt_event_logger.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxy/lib/pt_event_logger.go b/proxy/lib/pt_event_logger.go
index 7990a44..e4effc2 100644
--- a/proxy/lib/pt_event_logger.go
+++ b/proxy/lib/pt_event_logger.go
@@ -39,7 +39,7 @@ func (p *logEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 func (p *logEventLogger) logTick() error {
 	inbound, inboundUnit := formatTraffic(p.inboundSum)
 	outbound, outboundUnit := formatTraffic(p.inboundSum)
-	p.logger.Printf("In the last %v, there are %v connections. Traffic Relayed ↑ %v %v, ↓ %v %v.\n",
+	p.logger.Printf("In the last %v, there were %v connections. Traffic Relayed ↑ %v %v, ↓ %v %v.\n",
 		p.logPeriod.String(), p.connectionCount, inbound, inboundUnit, outbound, outboundUnit)
 	p.outboundSum = 0
 	p.inboundSum = 0

From 99eb794a2057fc7d7f6549f4eb39bb456d006904 Mon Sep 17 00:00:00 2001
From: Jake Vossen 
Date: Tue, 1 Mar 2022 09:30:42 -0700
Subject: [PATCH 307/385] Fixed up/downstream metrics

---
 proxy/lib/pt_event_logger.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxy/lib/pt_event_logger.go b/proxy/lib/pt_event_logger.go
index e4effc2..df94b0a 100644
--- a/proxy/lib/pt_event_logger.go
+++ b/proxy/lib/pt_event_logger.go
@@ -38,7 +38,7 @@ func (p *logEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 
 func (p *logEventLogger) logTick() error {
 	inbound, inboundUnit := formatTraffic(p.inboundSum)
-	outbound, outboundUnit := formatTraffic(p.inboundSum)
+	outbound, outboundUnit := formatTraffic(p.outboundSum)
 	p.logger.Printf("In the last %v, there were %v connections. Traffic Relayed ↑ %v %v, ↓ %v %v.\n",
 		p.logPeriod.String(), p.connectionCount, inbound, inboundUnit, outbound, outboundUnit)
 	p.outboundSum = 0

From 006abdead41579022c36da337c23de45600966ab Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 9 Feb 2022 15:36:54 +0000
Subject: [PATCH 308/385] Add utls roundtripper

---
 common/utls/roundtripper.go      | 191 +++++++++++++++++++++++++++++++
 common/utls/roundtripper_test.go | 153 +++++++++++++++++++++++++
 go.mod                           |   1 +
 go.sum                           |   2 +
 4 files changed, 347 insertions(+)
 create mode 100644 common/utls/roundtripper.go
 create mode 100644 common/utls/roundtripper_test.go

diff --git a/common/utls/roundtripper.go b/common/utls/roundtripper.go
new file mode 100644
index 0000000..e2fc82b
--- /dev/null
+++ b/common/utls/roundtripper.go
@@ -0,0 +1,191 @@
+package utls
+
+import (
+	"context"
+	"crypto/tls"
+	"errors"
+	"net"
+	"net/http"
+	"sync"
+
+	utls "github.com/refraction-networking/utls"
+	"golang.org/x/net/http2"
+)
+
+func NewUTLSHTTPRoundTripper(clientHelloID utls.ClientHelloID, uTlsConfig *utls.Config,
+	backdropTransport http.RoundTripper, removeSNI bool) http.RoundTripper {
+	rtImpl := &uTLSHTTPRoundTripperImpl{
+		clientHelloID:     clientHelloID,
+		config:            uTlsConfig,
+		connectWithH1:     map[string]bool{},
+		backdropTransport: backdropTransport,
+		pendingConn:       map[pendingConnKey]net.Conn{},
+		removeSNI:         removeSNI,
+	}
+	rtImpl.init()
+	return rtImpl
+}
+
+type uTLSHTTPRoundTripperImpl struct {
+	clientHelloID utls.ClientHelloID
+	config        *utls.Config
+
+	accessConnectWithH1 sync.Mutex
+	connectWithH1       map[string]bool
+
+	httpsH1Transport  http.RoundTripper
+	httpsH2Transport  http.RoundTripper
+	backdropTransport http.RoundTripper
+
+	accessDialingConnection sync.Mutex
+	pendingConn             map[pendingConnKey]net.Conn
+
+	removeSNI bool
+}
+
+type pendingConnKey struct {
+	isH2 bool
+	dest string
+}
+
+var errEAGAIN = errors.New("incorrect ALPN negotiated, try again with another ALPN")
+var errEAGAINTooMany = errors.New("incorrect ALPN negotiated")
+
+func (r *uTLSHTTPRoundTripperImpl) RoundTrip(req *http.Request) (*http.Response, error) {
+	if req.URL.Scheme != "https" {
+		return r.backdropTransport.RoundTrip(req)
+	}
+	for retryCount := 0; retryCount < 5; retryCount++ {
+		if r.getShouldConnectWithH1(req.URL.Host) {
+			resp, err := r.httpsH1Transport.RoundTrip(req)
+			if errors.Is(err, errEAGAIN) {
+				continue
+			}
+			return resp, err
+		}
+		resp, err := r.httpsH2Transport.RoundTrip(req)
+		if errors.Is(err, errEAGAIN) {
+			continue
+		}
+		return resp, err
+	}
+	return nil, errEAGAINTooMany
+}
+
+func (r *uTLSHTTPRoundTripperImpl) getShouldConnectWithH1(domainName string) bool {
+	r.accessConnectWithH1.Lock()
+	defer r.accessConnectWithH1.Unlock()
+	if value, set := r.connectWithH1[domainName]; set {
+		return value
+	}
+	return false
+}
+
+func (r *uTLSHTTPRoundTripperImpl) setShouldConnectWithH1(domainName string) {
+	r.accessConnectWithH1.Lock()
+	defer r.accessConnectWithH1.Unlock()
+	r.connectWithH1[domainName] = true
+}
+
+func (r *uTLSHTTPRoundTripperImpl) clearShouldConnectWithH1(domainName string) {
+	r.accessConnectWithH1.Lock()
+	defer r.accessConnectWithH1.Unlock()
+	r.connectWithH1[domainName] = false
+}
+
+func getPendingConnectionID(dest string, alpnIsH2 bool) pendingConnKey {
+	return pendingConnKey{isH2: alpnIsH2, dest: dest}
+}
+
+func (r *uTLSHTTPRoundTripperImpl) putConn(addr string, alpnIsH2 bool, conn net.Conn) {
+	connId := getPendingConnectionID(addr, alpnIsH2)
+	r.pendingConn[connId] = conn
+}
+func (r *uTLSHTTPRoundTripperImpl) getConn(addr string, alpnIsH2 bool) net.Conn {
+	connId := getPendingConnectionID(addr, alpnIsH2)
+	if conn, ok := r.pendingConn[connId]; ok {
+		return conn
+	}
+	return nil
+}
+func (r *uTLSHTTPRoundTripperImpl) dialOrGetTLSWithExpectedALPN(ctx context.Context, addr string, expectedH2 bool) (net.Conn, error) {
+	r.accessDialingConnection.Lock()
+	defer r.accessDialingConnection.Unlock()
+
+	if r.getShouldConnectWithH1(addr) == expectedH2 {
+		return nil, errEAGAIN
+	}
+
+	//Get a cached connection if possible to reduce preflight connection closed without sending data
+	if gconn := r.getConn(addr, expectedH2); gconn != nil {
+		return gconn, nil
+	}
+
+	conn, err := r.dialTLS(ctx, addr)
+	if err != nil {
+		return nil, err
+	}
+
+	protocol := conn.ConnectionState().NegotiatedProtocol
+
+	protocolIsH2 := protocol == http2.NextProtoTLS
+
+	if protocolIsH2 == expectedH2 {
+		return conn, err
+	}
+
+	r.putConn(addr, protocolIsH2, conn)
+
+	if protocolIsH2 {
+		r.clearShouldConnectWithH1(addr)
+	} else {
+		r.setShouldConnectWithH1(addr)
+	}
+
+	return nil, errEAGAIN
+}
+
+// based on https://repo.or.cz/dnstt.git/commitdiff/d92a791b6864901f9263f7d73d97cfd30ac53b09..98bdffa1706dfc041d1e99b86c47f29d72ad3a0c
+// by dcf1
+func (r *uTLSHTTPRoundTripperImpl) dialTLS(ctx context.Context, addr string) (*utls.UConn, error) {
+	config := r.config.Clone()
+
+	host, _, err := net.SplitHostPort(addr)
+	if err != nil {
+		return nil, err
+	}
+	config.ServerName = host
+
+	dialer := &net.Dialer{}
+	conn, err := dialer.DialContext(ctx, "tcp", addr)
+	if err != nil {
+		return nil, err
+	}
+	uconn := utls.UClient(conn, config, r.clientHelloID)
+	if (net.ParseIP(config.ServerName) != nil) || r.removeSNI {
+		err := uconn.RemoveSNIExtension()
+		if err != nil {
+			uconn.Close()
+			return nil, err
+		}
+	}
+
+	err = uconn.Handshake()
+	if err != nil {
+		return nil, err
+	}
+	return uconn, nil
+}
+
+func (r *uTLSHTTPRoundTripperImpl) init() {
+	r.httpsH2Transport = &http2.Transport{
+		DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
+			return r.dialOrGetTLSWithExpectedALPN(context.Background(), addr, true)
+		},
+	}
+	r.httpsH1Transport = &http.Transport{
+		DialTLSContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
+			return r.dialOrGetTLSWithExpectedALPN(ctx, addr, false)
+		},
+	}
+}
diff --git a/common/utls/roundtripper_test.go b/common/utls/roundtripper_test.go
new file mode 100644
index 0000000..b0209ff
--- /dev/null
+++ b/common/utls/roundtripper_test.go
@@ -0,0 +1,153 @@
+package utls
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	utls "github.com/refraction-networking/utls"
+	"golang.org/x/net/http2"
+	"math/big"
+	"net/http"
+	"testing"
+	"time"
+)
+
+import . "github.com/smartystreets/goconvey/convey"
+
+import stdcontext "context"
+
+func TestRoundTripper(t *testing.T) {
+	var selfSignedCert []byte
+	var selfSignedPrivateKey *rsa.PrivateKey
+	httpServerContext, cancel := stdcontext.WithCancel(stdcontext.Background())
+	Convey("[Test]Set up http servers", t, func(c C) {
+		c.Convey("[Test]Generate Self-Signed Cert", func(c C) {
+			// Ported from https://gist.github.com/samuel/8b500ddd3f6118d052b5e6bc16bc4c09
+			priv, err := rsa.GenerateKey(rand.Reader, 4096)
+			c.So(err, ShouldBeNil)
+			template := x509.Certificate{
+				SerialNumber: big.NewInt(1),
+				Subject: pkix.Name{
+					CommonName: "Testing Certificate",
+				},
+				NotBefore: time.Now(),
+				NotAfter:  time.Now().Add(time.Hour * 24 * 180),
+
+				KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+				ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+				BasicConstraintsValid: true,
+			}
+			derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
+			c.So(err, ShouldBeNil)
+			selfSignedPrivateKey = priv
+			selfSignedCert = derBytes
+		})
+		c.Convey("[Test]Setup http2 server", func(c C) {
+			listener, err := tls.Listen("tcp", "127.0.0.1:23802", &tls.Config{
+				NextProtos: []string{http2.NextProtoTLS},
+				Certificates: []tls.Certificate{
+					tls.Certificate{Certificate: [][]byte{selfSignedCert}, PrivateKey: selfSignedPrivateKey},
+				},
+			})
+			c.So(err, ShouldBeNil)
+			s := http.Server{}
+			go s.Serve(listener)
+			go func() {
+				<-httpServerContext.Done()
+				s.Close()
+			}()
+		})
+		c.Convey("[Test]Setup http1 server", func(c C) {
+			listener, err := tls.Listen("tcp", "127.0.0.1:23801", &tls.Config{
+				NextProtos: []string{"http/1.1"},
+				Certificates: []tls.Certificate{
+					tls.Certificate{Certificate: [][]byte{selfSignedCert}, PrivateKey: selfSignedPrivateKey},
+				},
+			})
+			c.So(err, ShouldBeNil)
+			s := http.Server{}
+			go s.Serve(listener)
+			go func() {
+				<-httpServerContext.Done()
+				s.Close()
+			}()
+		})
+	})
+	for _, v := range []struct {
+		id   utls.ClientHelloID
+		name string
+	}{
+		{
+			id:   utls.HelloChrome_58,
+			name: "HelloChrome_58",
+		},
+		{
+			id:   utls.HelloChrome_62,
+			name: "HelloChrome_62",
+		},
+		{
+			id:   utls.HelloChrome_70,
+			name: "HelloChrome_70",
+		},
+		{
+			id:   utls.HelloChrome_72,
+			name: "HelloChrome_72",
+		},
+		{
+			id:   utls.HelloChrome_83,
+			name: "HelloChrome_83",
+		},
+		{
+			id:   utls.HelloFirefox_55,
+			name: "HelloFirefox_55",
+		},
+		{
+			id:   utls.HelloFirefox_55,
+			name: "HelloFirefox_55",
+		},
+		{
+			id:   utls.HelloFirefox_63,
+			name: "HelloFirefox_63",
+		},
+		{
+			id:   utls.HelloFirefox_65,
+			name: "HelloFirefox_65",
+		},
+		{
+			id:   utls.HelloIOS_11_1,
+			name: "HelloIOS_11_1",
+		},
+		{
+			id:   utls.HelloIOS_12_1,
+			name: "HelloIOS_12_1",
+		},
+	} {
+		t.Run("Testing fingerprint for "+v.name, func(t *testing.T) {
+			rtter := NewUTLSHTTPRoundTripper(v.id, &utls.Config{
+				InsecureSkipVerify: true,
+			}, http.DefaultTransport)
+
+			Convey("HTTP 1.1 Test", t, func(c C) {
+				{
+					req, err := http.NewRequest("GET", "https://127.0.0.1:23801/", nil)
+					So(err, ShouldBeNil)
+					_, err = rtter.RoundTrip(req)
+					So(err, ShouldBeNil)
+				}
+			})
+
+			Convey("HTTP 2 Test", t, func(c C) {
+				{
+					req, err := http.NewRequest("GET", "https://127.0.0.1:23802/", nil)
+					So(err, ShouldBeNil)
+					_, err = rtter.RoundTrip(req)
+					So(err, ShouldBeNil)
+				}
+			})
+		})
+	}
+
+	cancel()
+}
diff --git a/go.mod b/go.mod
index 03541eb..705c05a 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
 	github.com/pion/webrtc/v3 v3.0.15
 	github.com/prometheus/client_golang v1.10.0
 	github.com/prometheus/client_model v0.2.0
+	github.com/refraction-networking/utls v1.0.0 // indirect
 	github.com/smartystreets/goconvey v1.6.4
 	github.com/stretchr/testify v1.7.0 // indirect
 	github.com/xtaci/kcp-go/v5 v5.6.1
diff --git a/go.sum b/go.sum
index ecf91a3..c2fa108 100644
--- a/go.sum
+++ b/go.sum
@@ -302,6 +302,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/refraction-networking/utls v1.0.0 h1:6XQHSjDmeBCF9sPq8p2zMVGq7Ud3rTD2q88Fw8Tz1tA=
+github.com/refraction-networking/utls v1.0.0/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

From 44478606615c3ff848d9d9749a17fd89430aa6d9 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 9 Feb 2022 15:38:27 +0000
Subject: [PATCH 309/385] Add repeated test for utls roundtripper

---
 common/utls/roundtripper_test.go | 34 +++++++++++++++++---------------
 1 file changed, 18 insertions(+), 16 deletions(-)

diff --git a/common/utls/roundtripper_test.go b/common/utls/roundtripper_test.go
index b0209ff..90c09bd 100644
--- a/common/utls/roundtripper_test.go
+++ b/common/utls/roundtripper_test.go
@@ -129,23 +129,25 @@ func TestRoundTripper(t *testing.T) {
 				InsecureSkipVerify: true,
 			}, http.DefaultTransport)
 
-			Convey("HTTP 1.1 Test", t, func(c C) {
-				{
-					req, err := http.NewRequest("GET", "https://127.0.0.1:23801/", nil)
-					So(err, ShouldBeNil)
-					_, err = rtter.RoundTrip(req)
-					So(err, ShouldBeNil)
-				}
-			})
+			for count := 0; count <= 10; count++ {
+				Convey("HTTP 1.1 Test", t, func(c C) {
+					{
+						req, err := http.NewRequest("GET", "https://127.0.0.1:23801/", nil)
+						So(err, ShouldBeNil)
+						_, err = rtter.RoundTrip(req)
+						So(err, ShouldBeNil)
+					}
+				})
 
-			Convey("HTTP 2 Test", t, func(c C) {
-				{
-					req, err := http.NewRequest("GET", "https://127.0.0.1:23802/", nil)
-					So(err, ShouldBeNil)
-					_, err = rtter.RoundTrip(req)
-					So(err, ShouldBeNil)
-				}
-			})
+				Convey("HTTP 2 Test", t, func(c C) {
+					{
+						req, err := http.NewRequest("GET", "https://127.0.0.1:23802/", nil)
+						So(err, ShouldBeNil)
+						_, err = rtter.RoundTrip(req)
+						So(err, ShouldBeNil)
+					}
+				})
+			}
 		})
 	}
 

From c1b0f763efb57316469d689d7addf53566685f78 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 9 Feb 2022 15:43:53 +0000
Subject: [PATCH 310/385] Add reformat for utls roundtripper

---
 common/utls/roundtripper_test.go | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/common/utls/roundtripper_test.go b/common/utls/roundtripper_test.go
index 90c09bd..0962b4a 100644
--- a/common/utls/roundtripper_test.go
+++ b/common/utls/roundtripper_test.go
@@ -6,18 +6,19 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
-	utls "github.com/refraction-networking/utls"
-	"golang.org/x/net/http2"
 	"math/big"
 	"net/http"
 	"testing"
 	"time"
+
+	stdcontext "context"
+
+	utls "github.com/refraction-networking/utls"
+	"golang.org/x/net/http2"
+
+	. "github.com/smartystreets/goconvey/convey"
 )
 
-import . "github.com/smartystreets/goconvey/convey"
-
-import stdcontext "context"
-
 func TestRoundTripper(t *testing.T) {
 	var selfSignedCert []byte
 	var selfSignedPrivateKey *rsa.PrivateKey

From c1c3596cf8bbc87b180e6d916da9515e27609969 Mon Sep 17 00:00:00 2001
From: Max Bittman 
Date: Thu, 10 Feb 2022 11:46:58 +0000
Subject: [PATCH 311/385] Add name to utls client hello id

---
 common/utls/client_hello_id.go | 38 ++++++++++++++++++++++++++++++++++
 1 file changed, 38 insertions(+)
 create mode 100644 common/utls/client_hello_id.go

diff --git a/common/utls/client_hello_id.go b/common/utls/client_hello_id.go
new file mode 100644
index 0000000..e423cf3
--- /dev/null
+++ b/common/utls/client_hello_id.go
@@ -0,0 +1,38 @@
+package utls
+
+import (
+	"errors"
+	utls "github.com/refraction-networking/utls"
+	"strings"
+)
+
+// ported from https://github.com/max-b/snowflake/commit/9dded063cb74c6941a16ad90b9dd0e06e618e55e
+var clientHelloIDMap = map[string]utls.ClientHelloID{
+	// No HelloCustom: not useful for external configuration.
+	// No HelloRandomized: doesn't negotiate consistent ALPN.
+	"hellorandomizedalpn":   utls.HelloRandomizedALPN,
+	"hellorandomizednoalpn": utls.HelloRandomizedNoALPN,
+	"hellofirefox_auto":     utls.HelloFirefox_Auto,
+	"hellofirefox_55":       utls.HelloFirefox_55,
+	"hellofirefox_56":       utls.HelloFirefox_56,
+	"hellofirefox_63":       utls.HelloFirefox_63,
+	"hellofirefox_65":       utls.HelloFirefox_65,
+	"hellochrome_auto":      utls.HelloChrome_Auto,
+	"hellochrome_58":        utls.HelloChrome_58,
+	"hellochrome_62":        utls.HelloChrome_62,
+	"hellochrome_70":        utls.HelloChrome_70,
+	"hellochrome_72":        utls.HelloChrome_72,
+	"helloios_auto":         utls.HelloIOS_Auto,
+	"helloios_11_1":         utls.HelloIOS_11_1,
+	"helloios_12_1":         utls.HelloIOS_12_1,
+}
+
+var errNameNotFound = errors.New("client hello name is unrecognized")
+
+func NameToUTlsID(name string) (utls.ClientHelloID, error) {
+	normalizedName := strings.ToLower(name)
+	if id, ok := clientHelloIDMap[normalizedName]; ok {
+		return id, nil
+	}
+	return utls.ClientHelloID{}, errNameNotFound
+}

From 9af0ad119b8b0f129f015c5347fe5a3b03596ff0 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 10 Feb 2022 17:04:42 +0000
Subject: [PATCH 312/385] Add utls imitate setting to snowflake client

---
 client/lib/rendezvous.go | 27 +++++++++++++++++++++++----
 client/lib/snowflake.go  |  3 +++
 client/snowflake.go      |  2 ++
 3 files changed, 28 insertions(+), 4 deletions(-)

diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index 98cd4d6..4c7c6f9 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -5,6 +5,8 @@ package snowflake_client
 
 import (
 	"errors"
+	"fmt"
+
 	"log"
 	"net/http"
 	"sync"
@@ -14,7 +16,9 @@ import (
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/util"
+	utlsutil "git.torproject.org/pluggable-transports/snowflake.git/v2/common/utls"
 	"github.com/pion/webrtc/v3"
+	utls "github.com/refraction-networking/utls"
 )
 
 const (
@@ -51,10 +55,14 @@ func createBrokerTransport() http.RoundTripper {
 	return transport
 }
 
-// NewBrokerChannel construct a new BrokerChannel, where:
+func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (*BrokerChannel, error) {
+	return NewBrokerChannelWithUTlsClientID(broker, ampCache, front, keepLocalAddresses, "")
+}
+
+// NewBrokerChannelWithUTlsClientID construct a new BrokerChannel, where:
 // |broker| is the full URL of the facilitating program which assigns proxies
 // to clients, and |front| is the option fronting domain.
-func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (*BrokerChannel, error) {
+func NewBrokerChannelWithUTlsClientID(broker, ampCache, front string, keepLocalAddresses bool, utlsClientID string) (*BrokerChannel, error) {
 	log.Println("Rendezvous using Broker at:", broker)
 	if ampCache != "" {
 		log.Println("Through AMP cache at:", ampCache)
@@ -63,12 +71,23 @@ func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (
 		log.Println("Domain fronting using:", front)
 	}
 
+	brokerTransport := createBrokerTransport()
+
+	if utlsClientID != "" {
+		utlsClientHelloID, err := utlsutil.NameToUTlsID(utlsClientID)
+		if err != nil {
+			return nil, fmt.Errorf("unable to create broker channel: %v", err)
+		}
+		config := &utls.Config{}
+		brokerTransport = utlsutil.NewUTLSHTTPRoundTripper(utlsClientHelloID, config, brokerTransport, false)
+	}
+
 	var rendezvous RendezvousMethod
 	var err error
 	if ampCache != "" {
-		rendezvous, err = newAMPCacheRendezvous(broker, ampCache, front, createBrokerTransport())
+		rendezvous, err = newAMPCacheRendezvous(broker, ampCache, front, brokerTransport)
 	} else {
-		rendezvous, err = newHTTPRendezvous(broker, front, createBrokerTransport())
+		rendezvous, err = newHTTPRendezvous(broker, front, brokerTransport)
 	}
 	if err != nil {
 		return nil, err
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 594c62c..19442d8 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -97,6 +97,9 @@ type ClientConfig struct {
 	// Max is the maximum number of snowflake proxy peers that the client should attempt to
 	// connect to. Defaults to 1.
 	Max int
+	// UTlsClientID is the type of user application that snowflake should imitate.
+	// If an empty value is provided, it will use Go's default TLS implementation
+	UTlsClientID string
 }
 
 // NewSnowflakeClient creates a new Snowflake transport client that can spawn multiple
diff --git a/client/snowflake.go b/client/snowflake.go
index 5a00206..addedb9 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -126,6 +126,7 @@ func main() {
 	frontDomain := flag.String("front", "", "front domain")
 	ampCacheURL := flag.String("ampcache", "", "URL of AMP cache to use as a proxy for signaling")
 	logFilename := flag.String("log", "", "name of log file")
+	utlsClientHelloID := flag.String("utls-imitate", "", "type of TLS client to imitate with utls")
 	logToStateDir := flag.Bool("log-to-state-dir", false, "resolve the log file relative to tor's pt state dir")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
@@ -178,6 +179,7 @@ func main() {
 		ICEAddresses:       iceAddresses,
 		KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses,
 		Max:                *max,
+		UTlsClientID:       *utlsClientHelloID,
 	}
 
 	// Begin goptlib client process.

From ccfdcab8feb7857a3089f2a88bc2e1e6c52d5865 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 11 Feb 2022 09:57:37 +0000
Subject: [PATCH 313/385] Add uTLS remove SNI to snowflake client

---
 client/lib/rendezvous.go | 6 +++---
 client/lib/snowflake.go  | 3 +++
 client/snowflake.go      | 2 ++
 3 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index 4c7c6f9..7c27dfc 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -56,13 +56,13 @@ func createBrokerTransport() http.RoundTripper {
 }
 
 func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (*BrokerChannel, error) {
-	return NewBrokerChannelWithUTlsClientID(broker, ampCache, front, keepLocalAddresses, "")
+	return NewBrokerChannelWithUTlsClientID(broker, ampCache, front, keepLocalAddresses, "", false)
 }
 
 // NewBrokerChannelWithUTlsClientID construct a new BrokerChannel, where:
 // |broker| is the full URL of the facilitating program which assigns proxies
 // to clients, and |front| is the option fronting domain.
-func NewBrokerChannelWithUTlsClientID(broker, ampCache, front string, keepLocalAddresses bool, utlsClientID string) (*BrokerChannel, error) {
+func NewBrokerChannelWithUTlsClientID(broker, ampCache, front string, keepLocalAddresses bool, utlsClientID string, removeSNI bool) (*BrokerChannel, error) {
 	log.Println("Rendezvous using Broker at:", broker)
 	if ampCache != "" {
 		log.Println("Through AMP cache at:", ampCache)
@@ -79,7 +79,7 @@ func NewBrokerChannelWithUTlsClientID(broker, ampCache, front string, keepLocalA
 			return nil, fmt.Errorf("unable to create broker channel: %v", err)
 		}
 		config := &utls.Config{}
-		brokerTransport = utlsutil.NewUTLSHTTPRoundTripper(utlsClientHelloID, config, brokerTransport, false)
+		brokerTransport = utlsutil.NewUTLSHTTPRoundTripper(utlsClientHelloID, config, brokerTransport, removeSNI)
 	}
 
 	var rendezvous RendezvousMethod
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 19442d8..510567e 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -100,6 +100,9 @@ type ClientConfig struct {
 	// UTlsClientID is the type of user application that snowflake should imitate.
 	// If an empty value is provided, it will use Go's default TLS implementation
 	UTlsClientID string
+	// UTlsRemoveSNI is the flag to control whether SNI should be removed from Client Hello
+	// when uTLS is used.
+	UTlsRemoveSNI bool
 }
 
 // NewSnowflakeClient creates a new Snowflake transport client that can spawn multiple
diff --git a/client/snowflake.go b/client/snowflake.go
index addedb9..a693ca6 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -127,6 +127,7 @@ func main() {
 	ampCacheURL := flag.String("ampcache", "", "URL of AMP cache to use as a proxy for signaling")
 	logFilename := flag.String("log", "", "name of log file")
 	utlsClientHelloID := flag.String("utls-imitate", "", "type of TLS client to imitate with utls")
+	utlsRemoveSNI := flag.Bool("utls-nosni", false, "remove SNI from client hello(ignored if uTLS is not used)")
 	logToStateDir := flag.Bool("log-to-state-dir", false, "resolve the log file relative to tor's pt state dir")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
@@ -180,6 +181,7 @@ func main() {
 		KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses,
 		Max:                *max,
 		UTlsClientID:       *utlsClientHelloID,
+		UTlsRemoveSNI:      *utlsRemoveSNI,
 	}
 
 	// Begin goptlib client process.

From 1573502e93b7149e8a4784e62bb1adc979312940 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 11 Feb 2022 10:03:45 +0000
Subject: [PATCH 314/385] Use uTLS aware broker channel constructor

---
 client/lib/rendezvous.go | 6 +++---
 client/lib/snowflake.go  | 5 +++--
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index 7c27dfc..ee07600 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -56,13 +56,13 @@ func createBrokerTransport() http.RoundTripper {
 }
 
 func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (*BrokerChannel, error) {
-	return NewBrokerChannelWithUTlsClientID(broker, ampCache, front, keepLocalAddresses, "", false)
+	return NewBrokerChannelWithUTlsSettings(broker, ampCache, front, keepLocalAddresses, "", false)
 }
 
-// NewBrokerChannelWithUTlsClientID construct a new BrokerChannel, where:
+// NewBrokerChannelWithUTlsSettings construct a new BrokerChannel, where:
 // |broker| is the full URL of the facilitating program which assigns proxies
 // to clients, and |front| is the option fronting domain.
-func NewBrokerChannelWithUTlsClientID(broker, ampCache, front string, keepLocalAddresses bool, utlsClientID string, removeSNI bool) (*BrokerChannel, error) {
+func NewBrokerChannelWithUTlsSettings(broker, ampCache, front string, keepLocalAddresses bool, utlsClientID string, removeSNI bool) (*BrokerChannel, error) {
 	log.Println("Rendezvous using Broker at:", broker)
 	if ampCache != "" {
 		log.Println("Through AMP cache at:", ampCache)
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 510567e..e309b44 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -131,8 +131,9 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
 	}
 
 	// Rendezvous with broker using the given parameters.
-	broker, err := NewBrokerChannel(
-		config.BrokerURL, config.AmpCacheURL, config.FrontDomain, config.KeepLocalAddresses)
+	broker, err := NewBrokerChannelWithUTlsSettings(
+		config.BrokerURL, config.AmpCacheURL, config.FrontDomain,
+		config.KeepLocalAddresses, config.UTlsClientID, config.UTlsRemoveSNI)
 	if err != nil {
 		return nil, err
 	}

From f5254900320aaa03b63c281351ebb145395d6357 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 11 Feb 2022 10:18:52 +0000
Subject: [PATCH 315/385] Update utls test to match uTLS Round Tripper
 constructor

---
 common/utls/roundtripper_test.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/common/utls/roundtripper_test.go b/common/utls/roundtripper_test.go
index 0962b4a..6a91385 100644
--- a/common/utls/roundtripper_test.go
+++ b/common/utls/roundtripper_test.go
@@ -128,7 +128,7 @@ func TestRoundTripper(t *testing.T) {
 		t.Run("Testing fingerprint for "+v.name, func(t *testing.T) {
 			rtter := NewUTLSHTTPRoundTripper(v.id, &utls.Config{
 				InsecureSkipVerify: true,
-			}, http.DefaultTransport)
+			}, http.DefaultTransport, false)
 
 			for count := 0; count <= 10; count++ {
 				Convey("HTTP 1.1 Test", t, func(c C) {

From e3aeb5fe5b3cace5d482c2fa40e0b964711ab189 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 11 Feb 2022 10:23:15 +0000
Subject: [PATCH 316/385] Add line wrap to NewBrokerChannelWithUTlsSettings

---
 client/lib/rendezvous.go | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index ee07600..dcf613c 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -62,7 +62,8 @@ func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (
 // NewBrokerChannelWithUTlsSettings construct a new BrokerChannel, where:
 // |broker| is the full URL of the facilitating program which assigns proxies
 // to clients, and |front| is the option fronting domain.
-func NewBrokerChannelWithUTlsSettings(broker, ampCache, front string, keepLocalAddresses bool, utlsClientID string, removeSNI bool) (*BrokerChannel, error) {
+func NewBrokerChannelWithUTlsSettings(broker, ampCache, front string, keepLocalAddresses bool,
+	utlsClientID string, removeSNI bool) (*BrokerChannel, error) {
 	log.Println("Rendezvous using Broker at:", broker)
 	if ampCache != "" {
 		log.Println("Through AMP cache at:", ampCache)

From 8d5998b7441eb9e213b8d86052e94a27d7656495 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 11 Feb 2022 11:26:41 +0000
Subject: [PATCH 317/385] Harmonize identifiers to uTLS

---
 client/lib/rendezvous.go       | 12 ++++++------
 client/lib/snowflake.go        | 12 ++++++------
 client/snowflake.go            |  8 ++++----
 common/utls/client_hello_id.go |  2 +-
 4 files changed, 17 insertions(+), 17 deletions(-)

diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index dcf613c..1fc2a69 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -56,14 +56,14 @@ func createBrokerTransport() http.RoundTripper {
 }
 
 func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (*BrokerChannel, error) {
-	return NewBrokerChannelWithUTlsSettings(broker, ampCache, front, keepLocalAddresses, "", false)
+	return NewBrokerChannelWithUTLSSettings(broker, ampCache, front, keepLocalAddresses, "", false)
 }
 
-// NewBrokerChannelWithUTlsSettings construct a new BrokerChannel, where:
+// NewBrokerChannelWithUTLSSettings construct a new BrokerChannel, where:
 // |broker| is the full URL of the facilitating program which assigns proxies
 // to clients, and |front| is the option fronting domain.
-func NewBrokerChannelWithUTlsSettings(broker, ampCache, front string, keepLocalAddresses bool,
-	utlsClientID string, removeSNI bool) (*BrokerChannel, error) {
+func NewBrokerChannelWithUTLSSettings(broker, ampCache, front string, keepLocalAddresses bool,
+	uTLSClientID string, removeSNI bool) (*BrokerChannel, error) {
 	log.Println("Rendezvous using Broker at:", broker)
 	if ampCache != "" {
 		log.Println("Through AMP cache at:", ampCache)
@@ -74,8 +74,8 @@ func NewBrokerChannelWithUTlsSettings(broker, ampCache, front string, keepLocalA
 
 	brokerTransport := createBrokerTransport()
 
-	if utlsClientID != "" {
-		utlsClientHelloID, err := utlsutil.NameToUTlsID(utlsClientID)
+	if uTLSClientID != "" {
+		utlsClientHelloID, err := utlsutil.NameToUTLSID(uTLSClientID)
 		if err != nil {
 			return nil, fmt.Errorf("unable to create broker channel: %v", err)
 		}
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index e309b44..1c6c381 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -97,12 +97,12 @@ type ClientConfig struct {
 	// Max is the maximum number of snowflake proxy peers that the client should attempt to
 	// connect to. Defaults to 1.
 	Max int
-	// UTlsClientID is the type of user application that snowflake should imitate.
+	// UTLSClientID is the type of user application that snowflake should imitate.
 	// If an empty value is provided, it will use Go's default TLS implementation
-	UTlsClientID string
-	// UTlsRemoveSNI is the flag to control whether SNI should be removed from Client Hello
+	UTLSClientID string
+	// UTLSRemoveSNI is the flag to control whether SNI should be removed from Client Hello
 	// when uTLS is used.
-	UTlsRemoveSNI bool
+	UTLSRemoveSNI bool
 }
 
 // NewSnowflakeClient creates a new Snowflake transport client that can spawn multiple
@@ -131,9 +131,9 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
 	}
 
 	// Rendezvous with broker using the given parameters.
-	broker, err := NewBrokerChannelWithUTlsSettings(
+	broker, err := NewBrokerChannelWithUTLSSettings(
 		config.BrokerURL, config.AmpCacheURL, config.FrontDomain,
-		config.KeepLocalAddresses, config.UTlsClientID, config.UTlsRemoveSNI)
+		config.KeepLocalAddresses, config.UTLSClientID, config.UTLSRemoveSNI)
 	if err != nil {
 		return nil, err
 	}
diff --git a/client/snowflake.go b/client/snowflake.go
index a693ca6..76a5cc4 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -126,8 +126,8 @@ func main() {
 	frontDomain := flag.String("front", "", "front domain")
 	ampCacheURL := flag.String("ampcache", "", "URL of AMP cache to use as a proxy for signaling")
 	logFilename := flag.String("log", "", "name of log file")
-	utlsClientHelloID := flag.String("utls-imitate", "", "type of TLS client to imitate with utls")
-	utlsRemoveSNI := flag.Bool("utls-nosni", false, "remove SNI from client hello(ignored if uTLS is not used)")
+	uTLSClientHelloID := flag.String("utls-imitate", "", "type of TLS client to imitate with utls")
+	uTLSRemoveSNI := flag.Bool("utls-nosni", false, "remove SNI from client hello(ignored if uTLS is not used)")
 	logToStateDir := flag.Bool("log-to-state-dir", false, "resolve the log file relative to tor's pt state dir")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
@@ -180,8 +180,8 @@ func main() {
 		ICEAddresses:       iceAddresses,
 		KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses,
 		Max:                *max,
-		UTlsClientID:       *utlsClientHelloID,
-		UTlsRemoveSNI:      *utlsRemoveSNI,
+		UTLSClientID:       *uTLSClientHelloID,
+		UTLSRemoveSNI:      *uTLSRemoveSNI,
 	}
 
 	// Begin goptlib client process.
diff --git a/common/utls/client_hello_id.go b/common/utls/client_hello_id.go
index e423cf3..8a13280 100644
--- a/common/utls/client_hello_id.go
+++ b/common/utls/client_hello_id.go
@@ -29,7 +29,7 @@ var clientHelloIDMap = map[string]utls.ClientHelloID{
 
 var errNameNotFound = errors.New("client hello name is unrecognized")
 
-func NameToUTlsID(name string) (utls.ClientHelloID, error) {
+func NameToUTLSID(name string) (utls.ClientHelloID, error) {
 	normalizedName := strings.ToLower(name)
 	if id, ok := clientHelloIDMap[normalizedName]; ok {
 		return id, nil

From 3132f680122e27bb9cfb957fbb29c3cbe73935cf Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 16 Feb 2022 11:11:37 +0000
Subject: [PATCH 318/385] Add connection expire time for uTLS pendingConn

---
 common/utls/roundtripper.go | 47 +++++++++++++++++++++++++++++++++----
 1 file changed, 43 insertions(+), 4 deletions(-)

diff --git a/common/utls/roundtripper.go b/common/utls/roundtripper.go
index e2fc82b..df31ff4 100644
--- a/common/utls/roundtripper.go
+++ b/common/utls/roundtripper.go
@@ -7,6 +7,7 @@ import (
 	"net"
 	"net/http"
 	"sync"
+	"time"
 
 	utls "github.com/refraction-networking/utls"
 	"golang.org/x/net/http2"
@@ -19,7 +20,7 @@ func NewUTLSHTTPRoundTripper(clientHelloID utls.ClientHelloID, uTlsConfig *utls.
 		config:            uTlsConfig,
 		connectWithH1:     map[string]bool{},
 		backdropTransport: backdropTransport,
-		pendingConn:       map[pendingConnKey]net.Conn{},
+		pendingConn:       map[pendingConnKey]*unclaimedConnection{},
 		removeSNI:         removeSNI,
 	}
 	rtImpl.init()
@@ -38,7 +39,7 @@ type uTLSHTTPRoundTripperImpl struct {
 	backdropTransport http.RoundTripper
 
 	accessDialingConnection sync.Mutex
-	pendingConn             map[pendingConnKey]net.Conn
+	pendingConn             map[pendingConnKey]*unclaimedConnection
 
 	removeSNI bool
 }
@@ -50,6 +51,7 @@ type pendingConnKey struct {
 
 var errEAGAIN = errors.New("incorrect ALPN negotiated, try again with another ALPN")
 var errEAGAINTooMany = errors.New("incorrect ALPN negotiated")
+var errExpired = errors.New("connection have expired")
 
 func (r *uTLSHTTPRoundTripperImpl) RoundTrip(req *http.Request) (*http.Response, error) {
 	if req.URL.Scheme != "https" {
@@ -99,12 +101,15 @@ func getPendingConnectionID(dest string, alpnIsH2 bool) pendingConnKey {
 
 func (r *uTLSHTTPRoundTripperImpl) putConn(addr string, alpnIsH2 bool, conn net.Conn) {
 	connId := getPendingConnectionID(addr, alpnIsH2)
-	r.pendingConn[connId] = conn
+	r.pendingConn[connId] = NewUnclaimedConnection(conn, time.Minute)
 }
 func (r *uTLSHTTPRoundTripperImpl) getConn(addr string, alpnIsH2 bool) net.Conn {
 	connId := getPendingConnectionID(addr, alpnIsH2)
 	if conn, ok := r.pendingConn[connId]; ok {
-		return conn
+		delete(r.pendingConn, connId)
+		if claimedConnection, err := conn.claimConnection(); err == nil {
+			return claimedConnection
+		}
 	}
 	return nil
 }
@@ -189,3 +194,37 @@ func (r *uTLSHTTPRoundTripperImpl) init() {
 		},
 	}
 }
+
+func NewUnclaimedConnection(conn net.Conn, expireTime time.Duration) *unclaimedConnection {
+	c := &unclaimedConnection{
+		Conn: conn,
+	}
+	time.AfterFunc(expireTime, c.tick)
+	return c
+}
+
+type unclaimedConnection struct {
+	net.Conn
+	claimed bool
+	access  sync.Mutex
+}
+
+func (c *unclaimedConnection) claimConnection() (net.Conn, error) {
+	c.access.Lock()
+	defer c.access.Unlock()
+	if !c.claimed {
+		c.claimed = true
+		return c.Conn, nil
+	}
+	return nil, errExpired
+}
+
+func (c *unclaimedConnection) tick() {
+	c.access.Lock()
+	defer c.access.Unlock()
+	if !c.claimed {
+		c.claimed = true
+		c.Conn.Close()
+		c.Conn = nil
+	}
+}

From ab9604476ee7673cf35a3aea33e225946a1426e0 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 7 Mar 2022 16:32:47 +0000
Subject: [PATCH 319/385] Move uTLS configuration to socks5 arg

---
 client/snowflake.go | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/client/snowflake.go b/client/snowflake.go
index 76a5cc4..5856750 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -84,6 +84,17 @@ func socksAcceptLoop(ln *pt.SocksListener, config sf.ClientConfig, shutdown chan
 			if arg, ok := conn.Req.Args.Get("url"); ok {
 				config.BrokerURL = arg
 			}
+			if arg, ok := conn.Req.Args.Get("utls-nosni"); ok {
+				switch strings.ToLower(arg) {
+				case "true":
+					fallthrough
+				case "yes":
+					config.UTLSRemoveSNI = true
+				}
+			}
+			if arg, ok := conn.Req.Args.Get("utls-imitate"); ok {
+				config.UTLSClientID = arg
+			}
 			transport, err := sf.NewSnowflakeClient(config)
 			if err != nil {
 				conn.Reject()
@@ -126,8 +137,6 @@ func main() {
 	frontDomain := flag.String("front", "", "front domain")
 	ampCacheURL := flag.String("ampcache", "", "URL of AMP cache to use as a proxy for signaling")
 	logFilename := flag.String("log", "", "name of log file")
-	uTLSClientHelloID := flag.String("utls-imitate", "", "type of TLS client to imitate with utls")
-	uTLSRemoveSNI := flag.Bool("utls-nosni", false, "remove SNI from client hello(ignored if uTLS is not used)")
 	logToStateDir := flag.Bool("log-to-state-dir", false, "resolve the log file relative to tor's pt state dir")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
@@ -180,8 +189,6 @@ func main() {
 		ICEAddresses:       iceAddresses,
 		KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses,
 		Max:                *max,
-		UTLSClientID:       *uTLSClientHelloID,
-		UTLSRemoveSNI:      *uTLSRemoveSNI,
 	}
 
 	// Begin goptlib client process.

From 6e29dc676c44ea1b6fe5f13aec48aae91ff1cc3c Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 15 Mar 2022 12:27:29 +0000
Subject: [PATCH 320/385] Add document for NewUTLSHTTPRoundTripper

---
 common/utls/roundtripper.go | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/common/utls/roundtripper.go b/common/utls/roundtripper.go
index df31ff4..53b997f 100644
--- a/common/utls/roundtripper.go
+++ b/common/utls/roundtripper.go
@@ -13,6 +13,14 @@ import (
 	"golang.org/x/net/http2"
 )
 
+// NewUTLSHTTPRoundTripper creates an instance of RoundTripper that dial to remote HTTPS endpoint with
+// an alternative version of TLS implementation that attempts to imitate browsers' fingerprint.
+// clientHelloID is the clientHello that uTLS attempts to imitate
+// uTlsConfig is the TLS Configuration template
+// backdropTransport is the transport that will be used for non-https traffic
+// removeSNI indicates not to send Server Name Indication Extension
+// returns a RoundTripper: its behaviour is documented at
+// https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/76#note_2777161
 func NewUTLSHTTPRoundTripper(clientHelloID utls.ClientHelloID, uTlsConfig *utls.Config,
 	backdropTransport http.RoundTripper, removeSNI bool) http.RoundTripper {
 	rtImpl := &uTLSHTTPRoundTripperImpl{

From 6fd0f1ae5dd22bb30100353d80f681b70d879d92 Mon Sep 17 00:00:00 2001
From: Arlo Breault 
Date: Tue, 8 Mar 2022 17:49:28 -0500
Subject: [PATCH 321/385] Rename *PollRequest methods to distinguish
 client/proxy

---
 broker/http.go                   | 2 +-
 broker/ipc.go                    | 2 +-
 client/lib/rendezvous.go         | 2 +-
 client/lib/rendezvous_test.go    | 2 +-
 common/messages/client.go        | 2 +-
 common/messages/messages_test.go | 8 ++++----
 common/messages/proxy.go         | 4 ++--
 proxy/lib/snowflake.go           | 2 +-
 8 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/broker/http.go b/broker/http.go
index 9ec95d8..3b0ba1f 100644
--- a/broker/http.go
+++ b/broker/http.go
@@ -149,7 +149,7 @@ func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) {
 			Offer: string(body),
 			NAT:   r.Header.Get("Snowflake-NAT-Type"),
 		}
-		body, err = req.EncodePollRequest()
+		body, err = req.EncodeClientPollRequest()
 		if err != nil {
 			log.Printf("Error shimming the legacy request: %s", err.Error())
 			w.WriteHeader(http.StatusInternalServerError)
diff --git a/broker/ipc.go b/broker/ipc.go
index c5d66e8..b8359f6 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -75,7 +75,7 @@ func (i *IPC) Debug(_ interface{}, response *string) error {
 }
 
 func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
-	sid, proxyType, natType, clients, err := messages.DecodePollRequest(arg.Body)
+	sid, proxyType, natType, clients, err := messages.DecodeProxyPollRequest(arg.Body)
 	if err != nil {
 		return messages.ErrBadRequest
 	}
diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index 1fc2a69..0ce2744 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -125,7 +125,7 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) (
 		Offer: offerSDP,
 		NAT:   bc.natType,
 	}
-	encReq, err := req.EncodePollRequest()
+	encReq, err := req.EncodeClientPollRequest()
 	bc.lock.Unlock()
 	if err != nil {
 		return nil, err
diff --git a/client/lib/rendezvous_test.go b/client/lib/rendezvous_test.go
index 582a979..21b9f57 100644
--- a/client/lib/rendezvous_test.go
+++ b/client/lib/rendezvous_test.go
@@ -45,7 +45,7 @@ func makeEncPollReq(offer string) []byte {
 	encPollReq, err := (&messages.ClientPollRequest{
 		Offer: offer,
 		NAT:   nat.NATUnknown,
-	}).EncodePollRequest()
+	}).EncodeClientPollRequest()
 	if err != nil {
 		panic(err)
 	}
diff --git a/common/messages/client.go b/common/messages/client.go
index edb7115..5a7d73b 100644
--- a/common/messages/client.go
+++ b/common/messages/client.go
@@ -54,7 +54,7 @@ type ClientPollRequest struct {
 }
 
 // Encodes a poll message from a snowflake client
-func (req *ClientPollRequest) EncodePollRequest() ([]byte, error) {
+func (req *ClientPollRequest) EncodeClientPollRequest() ([]byte, error) {
 	body, err := json.Marshal(req)
 	if err != nil {
 		return nil, err
diff --git a/common/messages/messages_test.go b/common/messages/messages_test.go
index a38746b..0d8b450 100644
--- a/common/messages/messages_test.go
+++ b/common/messages/messages_test.go
@@ -97,7 +97,7 @@ func TestDecodeProxyPollRequest(t *testing.T) {
 				fmt.Errorf(""),
 			},
 		} {
-			sid, proxyType, natType, clients, err := DecodePollRequest([]byte(test.data))
+			sid, proxyType, natType, clients, err := DecodeProxyPollRequest([]byte(test.data))
 			So(sid, ShouldResemble, test.sid)
 			So(proxyType, ShouldResemble, test.proxyType)
 			So(natType, ShouldResemble, test.natType)
@@ -110,9 +110,9 @@ func TestDecodeProxyPollRequest(t *testing.T) {
 
 func TestEncodeProxyPollRequests(t *testing.T) {
 	Convey("Context", t, func() {
-		b, err := EncodePollRequest("ymbcCMto7KHNGYlp", "standalone", "unknown", 16)
+		b, err := EncodeProxyPollRequest("ymbcCMto7KHNGYlp", "standalone", "unknown", 16)
 		So(err, ShouldEqual, nil)
-		sid, proxyType, natType, clients, err := DecodePollRequest(b)
+		sid, proxyType, natType, clients, err := DecodeProxyPollRequest(b)
 		So(sid, ShouldEqual, "ymbcCMto7KHNGYlp")
 		So(proxyType, ShouldEqual, "standalone")
 		So(natType, ShouldEqual, "unknown")
@@ -328,7 +328,7 @@ func TestEncodeClientPollRequests(t *testing.T) {
 			NAT:   "unknown",
 			Offer: "fake",
 		}
-		b, err := req1.EncodePollRequest()
+		b, err := req1.EncodeClientPollRequest()
 		So(err, ShouldEqual, nil)
 		fmt.Println(string(b))
 		parts := bytes.SplitN(b, []byte("\n"), 2)
diff --git a/common/messages/proxy.go b/common/messages/proxy.go
index 83606d3..64f139b 100644
--- a/common/messages/proxy.go
+++ b/common/messages/proxy.go
@@ -92,7 +92,7 @@ type ProxyPollRequest struct {
 	Clients int
 }
 
-func EncodePollRequest(sid string, proxyType string, natType string, clients int) ([]byte, error) {
+func EncodeProxyPollRequest(sid string, proxyType string, natType string, clients int) ([]byte, error) {
 	return json.Marshal(ProxyPollRequest{
 		Sid:     sid,
 		Version: version,
@@ -105,7 +105,7 @@ func EncodePollRequest(sid string, proxyType string, natType string, clients int
 // Decodes a poll message from a snowflake proxy and returns the
 // sid, proxy type, nat type and clients of the proxy on success
 // and an error if it failed
-func DecodePollRequest(data []byte) (sid string, proxyType string, natType string, clients int, err error) {
+func DecodeProxyPollRequest(data []byte) (sid string, proxyType string, natType string, clients int, err error) {
 	var message ProxyPollRequest
 
 	err = json.Unmarshal(data, &message)
diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 8747f66..ae9d5bf 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -199,7 +199,7 @@ func (s *SignalingServer) pollOffer(sid string, shutdown chan struct{}) *webrtc.
 		default:
 			numClients := int((tokens.count() / 8) * 8) // Round down to 8
 			currentNATTypeLoaded := getCurrentNATType()
-			body, err := messages.EncodePollRequest(sid, "standalone", currentNATTypeLoaded, numClients)
+			body, err := messages.EncodeProxyPollRequest(sid, "standalone", currentNATTypeLoaded, numClients)
 			if err != nil {
 				log.Printf("Error encoding poll message: %s", err.Error())
 				return nil

From 829cacac5f7ecb2cc701a24061679814fc1841bc Mon Sep 17 00:00:00 2001
From: Arlo Breault 
Date: Wed, 9 Mar 2022 19:48:16 -0500
Subject: [PATCH 322/385] Parse ClientPollRequest version in
 DecodeClientPollRequest

Instead of IPC.ClientOffers.  This makes things consistent with
EncodeClientPollRequest which adds the version while serializing.
---
 broker/http.go                   |  5 ++--
 broker/ipc.go                    | 45 +++++++-------------------------
 client/lib/rendezvous.go         |  5 ++--
 client/lib/rendezvous_test.go    |  5 ++--
 common/messages/client.go        | 28 ++++++++++++++++----
 common/messages/messages_test.go | 21 +++++++--------
 6 files changed, 52 insertions(+), 57 deletions(-)

diff --git a/broker/http.go b/broker/http.go
index 3b0ba1f..7acc465 100644
--- a/broker/http.go
+++ b/broker/http.go
@@ -146,8 +146,9 @@ func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) {
 	if len(body) > 0 && body[0] == '{' {
 		isLegacy = true
 		req := messages.ClientPollRequest{
-			Offer: string(body),
-			NAT:   r.Header.Get("Snowflake-NAT-Type"),
+			Offer:   string(body),
+			NAT:     r.Header.Get("Snowflake-NAT-Type"),
+			Version: messages.ClientVersion1_0,
 		}
 		body, err = req.EncodeClientPollRequest()
 		if err != nil {
diff --git a/broker/ipc.go b/broker/ipc.go
index b8359f6..768c0b7 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"bytes"
 	"container/heap"
 	"fmt"
 	"log"
@@ -21,12 +20,6 @@ const (
 	NATUnrestricted = "unrestricted"
 )
 
-type clientVersion int
-
-const (
-	v1 clientVersion = iota
-)
-
 type IPC struct {
 	ctx *BrokerContext
 }
@@ -132,32 +125,16 @@ func sendClientResponse(resp *messages.ClientPollResponse, response *[]byte) err
 }
 
 func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
-	var version clientVersion
-
 	startTime := time.Now()
-	body := arg.Body
 
-	parts := bytes.SplitN(body, []byte("\n"), 2)
-	if len(parts) < 2 {
-		// no version number found
-		err := fmt.Errorf("unsupported message version")
-		return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response)
-	}
-	body = parts[1]
-	if string(parts[0]) == "1.0" {
-		version = v1
-	} else {
-		err := fmt.Errorf("unsupported message version")
+	req, err := messages.DecodeClientPollRequest(arg.Body)
+	if err != nil {
 		return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response)
 	}
 
 	var offer *ClientOffer
-	switch version {
-	case v1:
-		req, err := messages.DecodeClientPollRequest(body)
-		if err != nil {
-			return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response)
-		}
+	switch req.Version {
+	case messages.ClientVersion1_0:
 		offer = &ClientOffer{
 			natType: req.NAT,
 			sdp:     []byte(req.Offer),
@@ -188,8 +165,8 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 			i.ctx.metrics.clientRestrictedDeniedCount++
 		}
 		i.ctx.metrics.lock.Unlock()
-		switch version {
-		case v1:
+		switch req.Version {
+		case messages.ClientVersion1_0:
 			resp := &messages.ClientPollResponse{Error: messages.StrNoProxies}
 			return sendClientResponse(resp, response)
 		default:
@@ -204,8 +181,6 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 	i.ctx.snowflakeLock.Unlock()
 	snowflake.offerChannel <- offer
 
-	var err error
-
 	// Wait for the answer to be returned on the channel or timeout.
 	select {
 	case answer := <-snowflake.answerChannel:
@@ -213,8 +188,8 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 		i.ctx.metrics.clientProxyMatchCount++
 		i.ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "matched"}).Inc()
 		i.ctx.metrics.lock.Unlock()
-		switch version {
-		case v1:
+		switch req.Version {
+		case messages.ClientVersion1_0:
 			resp := &messages.ClientPollResponse{Answer: answer}
 			err = sendClientResponse(resp, response)
 		default:
@@ -224,8 +199,8 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 		i.ctx.metrics.clientRoundtripEstimate = time.Since(startTime) / time.Millisecond
 	case <-time.After(time.Second * ClientTimeout):
 		log.Println("Client: Timed out.")
-		switch version {
-		case v1:
+		switch req.Version {
+		case messages.ClientVersion1_0:
 			resp := &messages.ClientPollResponse{Error: messages.StrTimedOut}
 			err = sendClientResponse(resp, response)
 		default:
diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index 0ce2744..e7543ad 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -122,8 +122,9 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) (
 	// Encode the client poll request.
 	bc.lock.Lock()
 	req := &messages.ClientPollRequest{
-		Offer: offerSDP,
-		NAT:   bc.natType,
+		Offer:   offerSDP,
+		NAT:     bc.natType,
+		Version: messages.ClientVersion1_0,
 	}
 	encReq, err := req.EncodeClientPollRequest()
 	bc.lock.Unlock()
diff --git a/client/lib/rendezvous_test.go b/client/lib/rendezvous_test.go
index 21b9f57..a233e7d 100644
--- a/client/lib/rendezvous_test.go
+++ b/client/lib/rendezvous_test.go
@@ -43,8 +43,9 @@ func (t errorTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 // offer.
 func makeEncPollReq(offer string) []byte {
 	encPollReq, err := (&messages.ClientPollRequest{
-		Offer: offer,
-		NAT:   nat.NATUnknown,
+		Offer:   offer,
+		NAT:     nat.NATUnknown,
+		Version: messages.ClientVersion1_0,
 	}).EncodeClientPollRequest()
 	if err != nil {
 		panic(err)
diff --git a/common/messages/client.go b/common/messages/client.go
index 5a7d73b..2a35594 100644
--- a/common/messages/client.go
+++ b/common/messages/client.go
@@ -4,13 +4,14 @@
 package messages
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
 )
 
-const ClientVersion = "1.0"
+const ClientVersion1_0 = "1.0"
 
 /* Client--Broker protocol v1.x specification:
 
@@ -49,24 +50,41 @@ for the error.
 */
 
 type ClientPollRequest struct {
-	Offer string `json:"offer"`
-	NAT   string `json:"nat"`
+	Offer   string `json:"offer"`
+	NAT     string `json:"nat"`
+	Version string `json:"-"`
 }
 
 // Encodes a poll message from a snowflake client
 func (req *ClientPollRequest) EncodeClientPollRequest() ([]byte, error) {
+	if req.Version != ClientVersion1_0 {
+		return nil, fmt.Errorf("unsupported message version")
+	}
 	body, err := json.Marshal(req)
 	if err != nil {
 		return nil, err
 	}
-	return append([]byte(ClientVersion+"\n"), body...), nil
+	return append([]byte(req.Version+"\n"), body...), nil
 }
 
 // Decodes a poll message from a snowflake client
 func DecodeClientPollRequest(data []byte) (*ClientPollRequest, error) {
+	parts := bytes.SplitN(data, []byte("\n"), 2)
+
+	if len(parts) < 2 {
+		// no version number found
+		return nil, fmt.Errorf("unsupported message version")
+	}
+
 	var message ClientPollRequest
 
-	err := json.Unmarshal(data, &message)
+	if string(parts[0]) == ClientVersion1_0 {
+		message.Version = ClientVersion1_0
+	} else {
+		return nil, fmt.Errorf("unsupported message version")
+	}
+
+	err := json.Unmarshal(parts[1], &message)
 	if err != nil {
 		return nil, err
 	}
diff --git a/common/messages/messages_test.go b/common/messages/messages_test.go
index 0d8b450..e0aa2a8 100644
--- a/common/messages/messages_test.go
+++ b/common/messages/messages_test.go
@@ -1,7 +1,6 @@
 package messages
 
 import (
-	"bytes"
 	"encoding/json"
 	"fmt"
 	"testing"
@@ -286,14 +285,16 @@ func TestDecodeClientPollRequest(t *testing.T) {
 				//version 1.0 client message
 				"unknown",
 				"fake",
-				`{"nat":"unknown","offer":"fake"}`,
+				`1.0
+{"nat":"unknown","offer":"fake"}`,
 				nil,
 			},
 			{
 				//version 1.0 client message
 				"unknown",
 				"fake",
-				`{"offer":"fake"}`,
+				`1.0
+{"offer":"fake"}`,
 				nil,
 			},
 			{
@@ -307,16 +308,17 @@ func TestDecodeClientPollRequest(t *testing.T) {
 				//no offer
 				"",
 				"",
-				`{"nat":"unknown"}`,
+				`1.0
+{"nat":"unknown"}`,
 				fmt.Errorf(""),
 			},
 		} {
 			req, err := DecodeClientPollRequest([]byte(test.data))
+			So(err, ShouldHaveSameTypeAs, test.err)
 			if test.err == nil {
 				So(req.NAT, ShouldResemble, test.natType)
 				So(req.Offer, ShouldResemble, test.offer)
 			}
-			So(err, ShouldHaveSameTypeAs, test.err)
 		}
 
 	})
@@ -325,15 +327,12 @@ func TestDecodeClientPollRequest(t *testing.T) {
 func TestEncodeClientPollRequests(t *testing.T) {
 	Convey("Context", t, func() {
 		req1 := &ClientPollRequest{
-			NAT:   "unknown",
-			Offer: "fake",
+			NAT:     "unknown",
+			Offer:   "fake",
+			Version: ClientVersion1_0,
 		}
 		b, err := req1.EncodeClientPollRequest()
 		So(err, ShouldEqual, nil)
-		fmt.Println(string(b))
-		parts := bytes.SplitN(b, []byte("\n"), 2)
-		So(string(parts[0]), ShouldEqual, "1.0")
-		b = parts[1]
 		req2, err := DecodeClientPollRequest(b)
 		So(err, ShouldEqual, nil)
 		So(req2, ShouldResemble, req1)

From bd636a1374efb514bbc40acbd1dcaf0ecec26916 Mon Sep 17 00:00:00 2001
From: Arlo Breault 
Date: Thu, 10 Mar 2022 14:13:35 -0500
Subject: [PATCH 323/385] Introduce an unexported newBrokerChannelFromConfig

A follow-up wants to pass in a new property from the ClientConfig but it
would be an API breaking change to NewBrokerChannel.

However, it's unclear why NewBrokerChannel is exported at all.  No other
package in the repo depends on it and the known users of the library
probably wouldn't be construct them.

While this patch was being reviewed, a new constructor was added,
NewBrokerChannelWithUTLSSettings, with effectively the same issue.
Both of those exported ones are deleted here.
---
 client/lib/rendezvous.go | 38 ++++++++++++++++----------------------
 client/lib/snowflake.go  |  4 +---
 2 files changed, 17 insertions(+), 25 deletions(-)

diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index e7543ad..73c62ed 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -55,40 +55,34 @@ func createBrokerTransport() http.RoundTripper {
 	return transport
 }
 
-func NewBrokerChannel(broker, ampCache, front string, keepLocalAddresses bool) (*BrokerChannel, error) {
-	return NewBrokerChannelWithUTLSSettings(broker, ampCache, front, keepLocalAddresses, "", false)
-}
+func newBrokerChannelFromConfig(config ClientConfig) (*BrokerChannel, error) {
+	log.Println("Rendezvous using Broker at:", config.BrokerURL)
 
-// NewBrokerChannelWithUTLSSettings construct a new BrokerChannel, where:
-// |broker| is the full URL of the facilitating program which assigns proxies
-// to clients, and |front| is the option fronting domain.
-func NewBrokerChannelWithUTLSSettings(broker, ampCache, front string, keepLocalAddresses bool,
-	uTLSClientID string, removeSNI bool) (*BrokerChannel, error) {
-	log.Println("Rendezvous using Broker at:", broker)
-	if ampCache != "" {
-		log.Println("Through AMP cache at:", ampCache)
-	}
-	if front != "" {
-		log.Println("Domain fronting using:", front)
+	if config.FrontDomain != "" {
+		log.Println("Domain fronting using:", config.FrontDomain)
 	}
 
 	brokerTransport := createBrokerTransport()
 
-	if uTLSClientID != "" {
-		utlsClientHelloID, err := utlsutil.NameToUTLSID(uTLSClientID)
+	if config.UTLSClientID != "" {
+		utlsClientHelloID, err := utlsutil.NameToUTLSID(config.UTLSClientID)
 		if err != nil {
 			return nil, fmt.Errorf("unable to create broker channel: %v", err)
 		}
-		config := &utls.Config{}
-		brokerTransport = utlsutil.NewUTLSHTTPRoundTripper(utlsClientHelloID, config, brokerTransport, removeSNI)
+		utlsConfig := &utls.Config{}
+		brokerTransport = utlsutil.NewUTLSHTTPRoundTripper(utlsClientHelloID, utlsConfig, brokerTransport, config.UTLSRemoveSNI)
 	}
 
 	var rendezvous RendezvousMethod
 	var err error
-	if ampCache != "" {
-		rendezvous, err = newAMPCacheRendezvous(broker, ampCache, front, brokerTransport)
+	if config.AmpCacheURL != "" {
+		log.Println("Through AMP cache at:", config.AmpCacheURL)
+		rendezvous, err = newAMPCacheRendezvous(
+			config.BrokerURL, config.AmpCacheURL, config.FrontDomain,
+			brokerTransport)
 	} else {
-		rendezvous, err = newHTTPRendezvous(broker, front, brokerTransport)
+		rendezvous, err = newHTTPRendezvous(
+			config.BrokerURL, config.FrontDomain, brokerTransport)
 	}
 	if err != nil {
 		return nil, err
@@ -96,7 +90,7 @@ func NewBrokerChannelWithUTLSSettings(broker, ampCache, front string, keepLocalA
 
 	return &BrokerChannel{
 		Rendezvous:         rendezvous,
-		keepLocalAddresses: keepLocalAddresses,
+		keepLocalAddresses: config.KeepLocalAddresses,
 		natType:            nat.NATUnknown,
 	}, nil
 }
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 1c6c381..1b236a6 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -131,9 +131,7 @@ func NewSnowflakeClient(config ClientConfig) (*Transport, error) {
 	}
 
 	// Rendezvous with broker using the given parameters.
-	broker, err := NewBrokerChannelWithUTLSSettings(
-		config.BrokerURL, config.AmpCacheURL, config.FrontDomain,
-		config.KeepLocalAddresses, config.UTLSClientID, config.UTLSRemoveSNI)
+	broker, err := newBrokerChannelFromConfig(config)
 	if err != nil {
 		return nil, err
 	}

From b265bd3092742bb0f71acffa52c0f5b7b8216a10 Mon Sep 17 00:00:00 2001
From: meskio 
Date: Fri, 11 Mar 2022 14:32:35 +0100
Subject: [PATCH 324/385] Make easier to extend the list of known proxy types

And include iptproxy as a valid proxy type.
---
 broker/ipc.go                   | 19 +++++-----
 broker/metrics.go               | 64 +++++++++++++--------------------
 broker/snowflake-broker_test.go | 17 +++++++--
 common/messages/proxy.go        | 21 ++++++-----
 4 files changed, 57 insertions(+), 64 deletions(-)

diff --git a/broker/ipc.go b/broker/ipc.go
index 768c0b7..9b47b90 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -25,18 +25,15 @@ type IPC struct {
 }
 
 func (i *IPC) Debug(_ interface{}, response *string) error {
-	var webexts, browsers, standalones, unknowns int
+	var unknowns int
 	var natRestricted, natUnrestricted, natUnknown int
+	proxyTypes := make(map[string]int)
 
 	i.ctx.snowflakeLock.Lock()
 	s := fmt.Sprintf("current snowflakes available: %d\n", len(i.ctx.idToSnowflake))
 	for _, snowflake := range i.ctx.idToSnowflake {
-		if snowflake.proxyType == "badge" {
-			browsers++
-		} else if snowflake.proxyType == "webext" {
-			webexts++
-		} else if snowflake.proxyType == "standalone" {
-			standalones++
+		if messages.KnownProxyTypes[snowflake.proxyType] {
+			proxyTypes[snowflake.proxyType]++
 		} else {
 			unknowns++
 		}
@@ -53,10 +50,10 @@ func (i *IPC) Debug(_ interface{}, response *string) error {
 	}
 	i.ctx.snowflakeLock.Unlock()
 
-	s += fmt.Sprintf("\tstandalone proxies: %d", standalones)
-	s += fmt.Sprintf("\n\tbrowser proxies: %d", browsers)
-	s += fmt.Sprintf("\n\twebext proxies: %d", webexts)
-	s += fmt.Sprintf("\n\tunknown proxies: %d", unknowns)
+	for pType, num := range proxyTypes {
+		s += fmt.Sprintf("\t%s proxies: %d\n", pType, num)
+	}
+	s += fmt.Sprintf("\tunknown proxies: %d", unknowns)
 
 	s += fmt.Sprintf("\nNAT Types available:")
 	s += fmt.Sprintf("\n\trestricted: %d", natRestricted)
diff --git a/broker/metrics.go b/broker/metrics.go
index 8229e0f..c642045 100644
--- a/broker/metrics.go
+++ b/broker/metrics.go
@@ -14,6 +14,7 @@ import (
 	"sync"
 	"time"
 
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
 	"github.com/prometheus/client_golang/prometheus"
 	"gitlab.torproject.org/tpo/anti-censorship/geoip"
 )
@@ -24,10 +25,9 @@ const (
 )
 
 type CountryStats struct {
-	standalone map[string]bool
-	badge      map[string]bool
-	webext     map[string]bool
-	unknown    map[string]bool
+	// map[proxyType][address]bool
+	proxies map[string]map[string]bool
+	unknown map[string]bool
 
 	natRestricted   map[string]bool
 	natUnrestricted map[string]bool
@@ -96,22 +96,17 @@ func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType stri
 	var country string
 	var ok bool
 
-	if proxyType == "standalone" {
-		if m.countryStats.standalone[addr] {
-			return
-		}
-	} else if proxyType == "badge" {
-		if m.countryStats.badge[addr] {
-			return
-		}
-	} else if proxyType == "webext" {
-		if m.countryStats.webext[addr] {
-			return
-		}
-	} else {
+	addresses, ok := m.countryStats.proxies[proxyType]
+	if !ok {
 		if m.countryStats.unknown[addr] {
 			return
 		}
+		m.countryStats.unknown[addr] = true
+	} else {
+		if addresses[addr] {
+			return
+		}
+		addresses[addr] = true
 	}
 
 	ip := net.ParseIP(addr)
@@ -122,18 +117,7 @@ func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType stri
 	if !ok {
 		country = "??"
 	}
-
-	//update map of unique ips and counts
 	m.countryStats.counts[country]++
-	if proxyType == "standalone" {
-		m.countryStats.standalone[addr] = true
-	} else if proxyType == "badge" {
-		m.countryStats.badge[addr] = true
-	} else if proxyType == "webext" {
-		m.countryStats.webext[addr] = true
-	} else {
-		m.countryStats.unknown[addr] = true
-	}
 
 	m.promMetrics.ProxyTotal.With(prometheus.Labels{
 		"nat":  natType,
@@ -166,14 +150,15 @@ func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) {
 
 	m.countryStats = CountryStats{
 		counts:          make(map[string]int),
-		standalone:      make(map[string]bool),
-		badge:           make(map[string]bool),
-		webext:          make(map[string]bool),
+		proxies:         make(map[string]map[string]bool),
 		unknown:         make(map[string]bool),
 		natRestricted:   make(map[string]bool),
 		natUnrestricted: make(map[string]bool),
 		natUnknown:      make(map[string]bool),
 	}
+	for pType := range messages.KnownProxyTypes {
+		m.countryStats.proxies[pType] = make(map[string]bool)
+	}
 
 	m.logger = metricsLogger
 	m.promMetrics = initPrometheus()
@@ -197,11 +182,12 @@ func (m *Metrics) printMetrics() {
 	m.lock.Lock()
 	m.logger.Println("snowflake-stats-end", time.Now().UTC().Format("2006-01-02 15:04:05"), fmt.Sprintf("(%d s)", int(metricsResolution.Seconds())))
 	m.logger.Println("snowflake-ips", m.countryStats.Display())
-	m.logger.Println("snowflake-ips-total", len(m.countryStats.standalone)+
-		len(m.countryStats.badge)+len(m.countryStats.webext)+len(m.countryStats.unknown))
-	m.logger.Println("snowflake-ips-standalone", len(m.countryStats.standalone))
-	m.logger.Println("snowflake-ips-badge", len(m.countryStats.badge))
-	m.logger.Println("snowflake-ips-webext", len(m.countryStats.webext))
+	total := len(m.countryStats.unknown)
+	for pType, addresses := range m.countryStats.proxies {
+		m.logger.Printf("snowflake-ips-%s %d\n", pType, len(addresses))
+		total += len(addresses)
+	}
+	m.logger.Println("snowflake-ips-total", total)
 	m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount))
 	m.logger.Println("client-denied-count", binCount(m.clientDeniedCount))
 	m.logger.Println("client-restricted-denied-count", binCount(m.clientRestrictedDeniedCount))
@@ -221,9 +207,9 @@ func (m *Metrics) zeroMetrics() {
 	m.clientUnrestrictedDeniedCount = 0
 	m.clientProxyMatchCount = 0
 	m.countryStats.counts = make(map[string]int)
-	m.countryStats.standalone = make(map[string]bool)
-	m.countryStats.badge = make(map[string]bool)
-	m.countryStats.webext = make(map[string]bool)
+	for pType := range m.countryStats.proxies {
+		m.countryStats.proxies[pType] = make(map[string]bool)
+	}
 	m.countryStats.unknown = make(map[string]bool)
 	m.countryStats.natRestricted = make(map[string]bool)
 	m.countryStats.natUnrestricted = make(map[string]bool)
diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go
index 9c975eb..f7850f8 100644
--- a/broker/snowflake-broker_test.go
+++ b/broker/snowflake-broker_test.go
@@ -549,8 +549,13 @@ func TestMetrics(t *testing.T) {
 			p.offerChannel <- nil
 			<-done
 			ctx.metrics.printMetrics()
-			So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips CA=4\nsnowflake-ips-total 4\nsnowflake-ips-standalone 1\nsnowflake-ips-badge 1\nsnowflake-ips-webext 1\nsnowflake-idle-count 8\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 1\n")
 
+			metricsStr := buf.String()
+			So(metricsStr, ShouldStartWith, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips CA=4\n")
+			So(metricsStr, ShouldContainSubstring, "\nsnowflake-ips-standalone 1\n")
+			So(metricsStr, ShouldContainSubstring, "\nsnowflake-ips-badge 1\n")
+			So(metricsStr, ShouldContainSubstring, "\nsnowflake-ips-webext 1\n")
+			So(metricsStr, ShouldEndWith, "\nsnowflake-ips-total 4\nsnowflake-idle-count 8\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 1\n")
 		})
 
 		//Test addition of client failures
@@ -570,7 +575,11 @@ func TestMetrics(t *testing.T) {
 			buf.Reset()
 			ctx.metrics.zeroMetrics()
 			ctx.metrics.printMetrics()
-			So(buf.String(), ShouldContainSubstring, "snowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 0\n")
+			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips \n")
+			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-standalone 0\n")
+			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-badge 0\n")
+			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-webext 0\n")
+			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-total 0\nsnowflake-idle-count 0\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 0\n")
 		})
 		//Test addition of client matches
 		Convey("for client-proxy match", func() {
@@ -690,7 +699,9 @@ func TestMetrics(t *testing.T) {
 			<-done
 
 			ctx.metrics.printMetrics()
-			So(buf.String(), ShouldContainSubstring, "snowflake-ips CA=1\nsnowflake-ips-total 1")
+			metricsStr := buf.String()
+			So(metricsStr, ShouldContainSubstring, "snowflake-ips CA=1\n")
+			So(metricsStr, ShouldContainSubstring, "snowflake-ips-total 1\n")
 		})
 		//Test NAT types
 		Convey("proxy counts by NAT type", func() {
diff --git a/common/messages/proxy.go b/common/messages/proxy.go
index 64f139b..dcfe0ab 100644
--- a/common/messages/proxy.go
+++ b/common/messages/proxy.go
@@ -12,14 +12,17 @@ import (
 )
 
 const (
-	version = "1.2"
-
-	ProxyStandalone = "standalone"
-	ProxyWebext     = "webext"
-	ProxyBadge      = "badge"
-	ProxyUnknown    = "unknown"
+	version      = "1.2"
+	ProxyUnknown = "unknown"
 )
 
+var KnownProxyTypes = map[string]bool{
+	"standalone": true,
+	"webext":     true,
+	"badge":      true,
+	"iptproxy":   true,
+}
+
 /* Version 1.2 specification:
 
 == ProxyPollRequest ==
@@ -138,11 +141,7 @@ func DecodeProxyPollRequest(data []byte) (sid string, proxyType string, natType
 
 	// we don't reject polls with an unknown proxy type because we encourage
 	// projects that embed proxy code to include their own type
-	switch message.Type {
-	case ProxyStandalone:
-	case ProxyWebext:
-	case ProxyBadge:
-	default:
+	if !KnownProxyTypes[message.Type] {
 		message.Type = ProxyUnknown
 	}
 

From b73add155074657cb763fcf12a3f7d2e9e22316d Mon Sep 17 00:00:00 2001
From: meskio 
Date: Fri, 11 Mar 2022 16:42:05 +0100
Subject: [PATCH 325/385] Make the proxy type configurable for users of the
 library

Closes: #40104
---
 proxy/lib/proxy-go_test.go |  4 ++--
 proxy/lib/snowflake.go     | 16 +++++++++++-----
 2 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/proxy/lib/proxy-go_test.go b/proxy/lib/proxy-go_test.go
index 86616c4..f4cbfbf 100644
--- a/proxy/lib/proxy-go_test.go
+++ b/proxy/lib/proxy-go_test.go
@@ -365,7 +365,7 @@ func TestBrokerInteractions(t *testing.T) {
 				b,
 			}
 
-			sdp := broker.pollOffer(sampleOffer, nil)
+			sdp := broker.pollOffer(sampleOffer, DefaultProxyType, nil)
 			expectedSDP, _ := strconv.Unquote(sampleSDP)
 			So(sdp.SDP, ShouldResemble, expectedSDP)
 		})
@@ -379,7 +379,7 @@ func TestBrokerInteractions(t *testing.T) {
 				b,
 			}
 
-			sdp := broker.pollOffer(sampleOffer, nil)
+			sdp := broker.pollOffer(sampleOffer, DefaultProxyType, nil)
 			So(sdp, ShouldBeNil)
 		})
 		Convey("sends answer to broker", func() {
diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index ae9d5bf..17f0126 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -56,6 +56,7 @@ const DefaultNATProbeURL = "https://snowflake-broker.torproject.net:8443/probe"
 const DefaultRelayURL = "wss://snowflake.bamsoftware.com/"
 
 const DefaultSTUNURL = "stun:stun.stunprotocol.org:3478"
+const DefaultProxyType = "standalone"
 const pollInterval = 5 * time.Second
 
 const (
@@ -115,8 +116,10 @@ type SnowflakeProxy struct {
 	NATProbeURL string
 	// NATTypeMeasurementInterval is time before NAT type is retested
 	NATTypeMeasurementInterval time.Duration
-	EventDispatcher            event.SnowflakeEventDispatcher
-	shutdown                   chan struct{}
+	// ProxyType is the type reported to the broker, if not provided it "standalone" will be used
+	ProxyType       string
+	EventDispatcher event.SnowflakeEventDispatcher
+	shutdown        chan struct{}
 }
 
 // Checks whether an IP address is a remote address for the client
@@ -185,7 +188,7 @@ func (s *SignalingServer) Post(path string, payload io.Reader) ([]byte, error) {
 	return limitedRead(resp.Body, readLimit)
 }
 
-func (s *SignalingServer) pollOffer(sid string, shutdown chan struct{}) *webrtc.SessionDescription {
+func (s *SignalingServer) pollOffer(sid string, proxyType string, shutdown chan struct{}) *webrtc.SessionDescription {
 	brokerPath := s.url.ResolveReference(&url.URL{Path: "proxy"})
 
 	ticker := time.NewTicker(pollInterval)
@@ -199,7 +202,7 @@ func (s *SignalingServer) pollOffer(sid string, shutdown chan struct{}) *webrtc.
 		default:
 			numClients := int((tokens.count() / 8) * 8) // Round down to 8
 			currentNATTypeLoaded := getCurrentNATType()
-			body, err := messages.EncodeProxyPollRequest(sid, "standalone", currentNATTypeLoaded, numClients)
+			body, err := messages.EncodeProxyPollRequest(sid, proxyType, currentNATTypeLoaded, numClients)
 			if err != nil {
 				log.Printf("Error encoding poll message: %s", err.Error())
 				return nil
@@ -467,7 +470,7 @@ func (sf *SnowflakeProxy) makeNewPeerConnection(config webrtc.Configuration,
 }
 
 func (sf *SnowflakeProxy) runSession(sid string) {
-	offer := broker.pollOffer(sid, sf.shutdown)
+	offer := broker.pollOffer(sid, sf.ProxyType, sf.shutdown)
 	if offer == nil {
 		log.Printf("bad offer from broker")
 		tokens.ret()
@@ -525,6 +528,9 @@ func (sf *SnowflakeProxy) Start() error {
 	if sf.NATProbeURL == "" {
 		sf.NATProbeURL = DefaultNATProbeURL
 	}
+	if sf.ProxyType == "" {
+		sf.ProxyType = DefaultProxyType
+	}
 	if sf.EventDispatcher == nil {
 		sf.EventDispatcher = event.NewSnowflakeEventDispatcher()
 	}

From 281d917bebe19642914ec94d7d5ef911f03c8f57 Mon Sep 17 00:00:00 2001
From: Arlo Breault 
Date: Wed, 16 Mar 2022 20:26:40 -0400
Subject: [PATCH 326/385] Stop storing version in ClientPollRequest

This continues to asserts the known version while decoding.  The client
will only ever generate the latest version while encoding and if the
response needs to change, the impetus will be a new feature, set in the
deserialized request, which can be used as a distinguisher.
---
 broker/http.go                   |  5 ++--
 broker/ipc.go                    | 39 ++++++++------------------------
 client/lib/rendezvous.go         |  5 ++--
 client/lib/rendezvous_test.go    |  5 ++--
 common/messages/client.go        | 16 ++++---------
 common/messages/messages_test.go |  5 ++--
 6 files changed, 22 insertions(+), 53 deletions(-)

diff --git a/broker/http.go b/broker/http.go
index 7acc465..3b0ba1f 100644
--- a/broker/http.go
+++ b/broker/http.go
@@ -146,9 +146,8 @@ func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) {
 	if len(body) > 0 && body[0] == '{' {
 		isLegacy = true
 		req := messages.ClientPollRequest{
-			Offer:   string(body),
-			NAT:     r.Header.Get("Snowflake-NAT-Type"),
-			Version: messages.ClientVersion1_0,
+			Offer: string(body),
+			NAT:   r.Header.Get("Snowflake-NAT-Type"),
 		}
 		body, err = req.EncodeClientPollRequest()
 		if err != nil {
diff --git a/broker/ipc.go b/broker/ipc.go
index 9b47b90..5cc595b 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -129,15 +129,9 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 		return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response)
 	}
 
-	var offer *ClientOffer
-	switch req.Version {
-	case messages.ClientVersion1_0:
-		offer = &ClientOffer{
-			natType: req.NAT,
-			sdp:     []byte(req.Offer),
-		}
-	default:
-		panic("unknown version")
+	offer := &ClientOffer{
+		natType: req.NAT,
+		sdp:     []byte(req.Offer),
 	}
 
 	// Only hand out known restricted snowflakes to unrestricted clients
@@ -162,13 +156,8 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 			i.ctx.metrics.clientRestrictedDeniedCount++
 		}
 		i.ctx.metrics.lock.Unlock()
-		switch req.Version {
-		case messages.ClientVersion1_0:
-			resp := &messages.ClientPollResponse{Error: messages.StrNoProxies}
-			return sendClientResponse(resp, response)
-		default:
-			panic("unknown version")
-		}
+		resp := &messages.ClientPollResponse{Error: messages.StrNoProxies}
+		return sendClientResponse(resp, response)
 	}
 
 	// Otherwise, find the most available snowflake proxy, and pass the offer to it.
@@ -185,24 +174,14 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 		i.ctx.metrics.clientProxyMatchCount++
 		i.ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "matched"}).Inc()
 		i.ctx.metrics.lock.Unlock()
-		switch req.Version {
-		case messages.ClientVersion1_0:
-			resp := &messages.ClientPollResponse{Answer: answer}
-			err = sendClientResponse(resp, response)
-		default:
-			panic("unknown version")
-		}
+		resp := &messages.ClientPollResponse{Answer: answer}
+		err = sendClientResponse(resp, response)
 		// Initial tracking of elapsed time.
 		i.ctx.metrics.clientRoundtripEstimate = time.Since(startTime) / time.Millisecond
 	case <-time.After(time.Second * ClientTimeout):
 		log.Println("Client: Timed out.")
-		switch req.Version {
-		case messages.ClientVersion1_0:
-			resp := &messages.ClientPollResponse{Error: messages.StrTimedOut}
-			err = sendClientResponse(resp, response)
-		default:
-			panic("unknown version")
-		}
+		resp := &messages.ClientPollResponse{Error: messages.StrTimedOut}
+		err = sendClientResponse(resp, response)
 	}
 
 	i.ctx.snowflakeLock.Lock()
diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index 73c62ed..d908b77 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -116,9 +116,8 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) (
 	// Encode the client poll request.
 	bc.lock.Lock()
 	req := &messages.ClientPollRequest{
-		Offer:   offerSDP,
-		NAT:     bc.natType,
-		Version: messages.ClientVersion1_0,
+		Offer: offerSDP,
+		NAT:   bc.natType,
 	}
 	encReq, err := req.EncodeClientPollRequest()
 	bc.lock.Unlock()
diff --git a/client/lib/rendezvous_test.go b/client/lib/rendezvous_test.go
index a233e7d..21b9f57 100644
--- a/client/lib/rendezvous_test.go
+++ b/client/lib/rendezvous_test.go
@@ -43,9 +43,8 @@ func (t errorTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 // offer.
 func makeEncPollReq(offer string) []byte {
 	encPollReq, err := (&messages.ClientPollRequest{
-		Offer:   offer,
-		NAT:     nat.NATUnknown,
-		Version: messages.ClientVersion1_0,
+		Offer: offer,
+		NAT:   nat.NATUnknown,
 	}).EncodeClientPollRequest()
 	if err != nil {
 		panic(err)
diff --git a/common/messages/client.go b/common/messages/client.go
index 2a35594..b6155f7 100644
--- a/common/messages/client.go
+++ b/common/messages/client.go
@@ -11,7 +11,7 @@ import (
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
 )
 
-const ClientVersion1_0 = "1.0"
+const ClientVersion = "1.0"
 
 /* Client--Broker protocol v1.x specification:
 
@@ -50,21 +50,17 @@ for the error.
 */
 
 type ClientPollRequest struct {
-	Offer   string `json:"offer"`
-	NAT     string `json:"nat"`
-	Version string `json:"-"`
+	Offer string `json:"offer"`
+	NAT   string `json:"nat"`
 }
 
 // Encodes a poll message from a snowflake client
 func (req *ClientPollRequest) EncodeClientPollRequest() ([]byte, error) {
-	if req.Version != ClientVersion1_0 {
-		return nil, fmt.Errorf("unsupported message version")
-	}
 	body, err := json.Marshal(req)
 	if err != nil {
 		return nil, err
 	}
-	return append([]byte(req.Version+"\n"), body...), nil
+	return append([]byte(ClientVersion+"\n"), body...), nil
 }
 
 // Decodes a poll message from a snowflake client
@@ -78,9 +74,7 @@ func DecodeClientPollRequest(data []byte) (*ClientPollRequest, error) {
 
 	var message ClientPollRequest
 
-	if string(parts[0]) == ClientVersion1_0 {
-		message.Version = ClientVersion1_0
-	} else {
+	if string(parts[0]) != ClientVersion {
 		return nil, fmt.Errorf("unsupported message version")
 	}
 
diff --git a/common/messages/messages_test.go b/common/messages/messages_test.go
index e0aa2a8..ae0d4f9 100644
--- a/common/messages/messages_test.go
+++ b/common/messages/messages_test.go
@@ -327,9 +327,8 @@ func TestDecodeClientPollRequest(t *testing.T) {
 func TestEncodeClientPollRequests(t *testing.T) {
 	Convey("Context", t, func() {
 		req1 := &ClientPollRequest{
-			NAT:     "unknown",
-			Offer:   "fake",
-			Version: ClientVersion1_0,
+			NAT:   "unknown",
+			Offer: "fake",
 		}
 		b, err := req1.EncodeClientPollRequest()
 		So(err, ShouldEqual, nil)

From b563141c6abba128386bc1ad18122d5e13e09789 Mon Sep 17 00:00:00 2001
From: Arlo Breault 
Date: Tue, 8 Mar 2022 16:27:52 -0500
Subject: [PATCH 327/385] Forward bridge fingerprint

gitlab 28651
---
 broker/broker.go          |  7 ++++---
 broker/ipc.go             |  5 +++--
 client/lib/rendezvous.go  |  7 +++++--
 client/lib/snowflake.go   |  3 +++
 client/snowflake.go       |  3 +++
 client/torrc              |  2 +-
 common/messages/client.go | 25 ++++++++++++++++++++++---
 7 files changed, 41 insertions(+), 11 deletions(-)

diff --git a/broker/broker.go b/broker/broker.go
index 7a29265..6e85fbd 100644
--- a/broker/broker.go
+++ b/broker/broker.go
@@ -139,10 +139,11 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri
 	return snowflake
 }
 
-// Client offer contains an SDP and the NAT type of the client
+// Client offer contains an SDP, bridge fingerprint and the NAT type of the client
 type ClientOffer struct {
-	natType string
-	sdp     []byte
+	natType     string
+	sdp         []byte
+	fingerprint string
 }
 
 func main() {
diff --git a/broker/ipc.go b/broker/ipc.go
index 5cc595b..2ef4ccd 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -130,8 +130,9 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 	}
 
 	offer := &ClientOffer{
-		natType: req.NAT,
-		sdp:     []byte(req.Offer),
+		natType:     req.NAT,
+		sdp:         []byte(req.Offer),
+		fingerprint: req.Fingerprint,
 	}
 
 	// Only hand out known restricted snowflakes to unrestricted clients
diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go
index d908b77..38e4620 100644
--- a/client/lib/rendezvous.go
+++ b/client/lib/rendezvous.go
@@ -43,6 +43,7 @@ type BrokerChannel struct {
 	keepLocalAddresses bool
 	natType            string
 	lock               sync.Mutex
+	BridgeFingerprint  string
 }
 
 // We make a copy of DefaultTransport because we want the default Dial
@@ -92,6 +93,7 @@ func newBrokerChannelFromConfig(config ClientConfig) (*BrokerChannel, error) {
 		Rendezvous:         rendezvous,
 		keepLocalAddresses: config.KeepLocalAddresses,
 		natType:            nat.NATUnknown,
+		BridgeFingerprint:  config.BridgeFingerprint,
 	}, nil
 }
 
@@ -116,8 +118,9 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) (
 	// Encode the client poll request.
 	bc.lock.Lock()
 	req := &messages.ClientPollRequest{
-		Offer: offerSDP,
-		NAT:   bc.natType,
+		Offer:       offerSDP,
+		NAT:         bc.natType,
+		Fingerprint: bc.BridgeFingerprint,
 	}
 	encReq, err := req.EncodeClientPollRequest()
 	bc.lock.Unlock()
diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index 1b236a6..dd78c12 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -103,6 +103,9 @@ type ClientConfig struct {
 	// UTLSRemoveSNI is the flag to control whether SNI should be removed from Client Hello
 	// when uTLS is used.
 	UTLSRemoveSNI bool
+	// BridgeFingerprint is the fingerprint of the bridge that the client will eventually
+	// connect to, as specified in the Bridge line of the torrc.
+	BridgeFingerprint string
 }
 
 // NewSnowflakeClient creates a new Snowflake transport client that can spawn multiple
diff --git a/client/snowflake.go b/client/snowflake.go
index 5856750..33834ad 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -95,6 +95,9 @@ func socksAcceptLoop(ln *pt.SocksListener, config sf.ClientConfig, shutdown chan
 			if arg, ok := conn.Req.Args.Get("utls-imitate"); ok {
 				config.UTLSClientID = arg
 			}
+			if arg, ok := conn.Req.Args.Get("fingerprint"); ok {
+				config.BridgeFingerprint = arg
+			}
 			transport, err := sf.NewSnowflakeClient(config)
 			if err != nil {
 				conn.Reject()
diff --git a/client/torrc b/client/torrc
index 039653f..aee4df1 100644
--- a/client/torrc
+++ b/client/torrc
@@ -3,6 +3,6 @@ DataDirectory datadir
 
 ClientTransportPlugin snowflake exec ./client -log snowflake.log
 
-Bridge snowflake 192.0.2.3:1 url=https://snowflake-broker.torproject.net.global.prod.fastly.net/ front=cdn.sstatic.net ice=stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478
+Bridge snowflake 192.0.2.3:1 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://snowflake-broker.torproject.net.global.prod.fastly.net/ front=cdn.sstatic.net ice=stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478
 
 SocksPort auto
diff --git a/common/messages/client.go b/common/messages/client.go
index b6155f7..4d435ab 100644
--- a/common/messages/client.go
+++ b/common/messages/client.go
@@ -29,10 +29,13 @@ each encoded in JSON format
 {
   offer: 
   [nat: (unknown|restricted|unrestricted)]
+  [fingerprint: ]
 }
 
 The NAT field is optional, and if it is missing a
-value of "unknown" will be assumed.
+value of "unknown" will be assumed.  The fingerprint
+is also optional and, if absent, will be assigned the
+fingerprint of the default bridge.
 
 == ClientPollResponse ==
  :=
@@ -49,13 +52,25 @@ for the error.
 
 */
 
+// The bridge fingerprint to assume, for client poll requests that do not
+// specify a fingerprint.  Before #28651, there was only one bridge with one
+// fingerprint, which all clients expected to be connected to implicitly.
+// If a client is old enough that it does not specify a fingerprint, this is
+// the fingerprint it expects.  Clients that do set a fingerprint in the
+// SOCKS params will also be assumed to want to connect to the default bridge.
+const defaultBridgeFingerprint = "2B280B23E1107BB62ABFC40DDCC8824814F80A72"
+
 type ClientPollRequest struct {
-	Offer string `json:"offer"`
-	NAT   string `json:"nat"`
+	Offer       string `json:"offer"`
+	NAT         string `json:"nat"`
+	Fingerprint string `json:"fingerprint"`
 }
 
 // Encodes a poll message from a snowflake client
 func (req *ClientPollRequest) EncodeClientPollRequest() ([]byte, error) {
+	if req.Fingerprint == "" {
+		req.Fingerprint = defaultBridgeFingerprint
+	}
 	body, err := json.Marshal(req)
 	if err != nil {
 		return nil, err
@@ -87,6 +102,10 @@ func DecodeClientPollRequest(data []byte) (*ClientPollRequest, error) {
 		return nil, fmt.Errorf("no supplied offer")
 	}
 
+	if message.Fingerprint == "" {
+		message.Fingerprint = defaultBridgeFingerprint
+	}
+
 	switch message.NAT {
 	case "":
 		message.NAT = nat.NATUnknown

From fa2f6824d924f3317c82bc740130f354f6a1780c Mon Sep 17 00:00:00 2001
From: Arlo Breault 
Date: Thu, 17 Mar 2022 11:23:49 -0400
Subject: [PATCH 328/385] Add some test cases for client poll requests

---
 common/messages/messages_test.go | 45 ++++++++++++++++++++++++++------
 1 file changed, 37 insertions(+), 8 deletions(-)

diff --git a/common/messages/messages_test.go b/common/messages/messages_test.go
index ae0d4f9..5365aa8 100644
--- a/common/messages/messages_test.go
+++ b/common/messages/messages_test.go
@@ -326,15 +326,44 @@ func TestDecodeClientPollRequest(t *testing.T) {
 
 func TestEncodeClientPollRequests(t *testing.T) {
 	Convey("Context", t, func() {
-		req1 := &ClientPollRequest{
-			NAT:   "unknown",
-			Offer: "fake",
+		for i, test := range []struct {
+			natType     string
+			offer       string
+			fingerprint string
+			err         error
+		}{
+			{
+				"unknown",
+				"fake",
+				"",
+				nil,
+			},
+			{
+				"unknown",
+				"fake",
+				defaultBridgeFingerprint,
+				nil,
+			},
+		} {
+			req1 := &ClientPollRequest{
+				NAT:         test.natType,
+				Offer:       test.offer,
+				Fingerprint: test.fingerprint,
+			}
+			b, err := req1.EncodeClientPollRequest()
+			So(err, ShouldEqual, nil)
+			req2, err := DecodeClientPollRequest(b)
+			So(err, ShouldHaveSameTypeAs, test.err)
+			if test.err == nil {
+				So(req2.Offer, ShouldEqual, req1.Offer)
+				So(req2.NAT, ShouldEqual, req1.NAT)
+				fingerprint := test.fingerprint
+				if i == 0 {
+					fingerprint = defaultBridgeFingerprint
+				}
+				So(req2.Fingerprint, ShouldEqual, fingerprint)
+			}
 		}
-		b, err := req1.EncodeClientPollRequest()
-		So(err, ShouldEqual, nil)
-		req2, err := DecodeClientPollRequest(b)
-		So(err, ShouldEqual, nil)
-		So(req2, ShouldResemble, req1)
 	})
 }
 

From 2f89fbc2ed3e25d2b4be76edc600cca37de91864 Mon Sep 17 00:00:00 2001
From: Arlo Breault 
Date: Thu, 17 Mar 2022 10:46:37 -0400
Subject: [PATCH 329/385] Represent fingerprint internally as byte array

---
 broker/broker.go                 |  2 +-
 broker/ipc.go                    | 12 +++++++++---
 common/messages/client.go        |  4 ++++
 common/messages/messages_test.go |  6 ++++++
 4 files changed, 20 insertions(+), 4 deletions(-)

diff --git a/broker/broker.go b/broker/broker.go
index 6e85fbd..10129d7 100644
--- a/broker/broker.go
+++ b/broker/broker.go
@@ -143,7 +143,7 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri
 type ClientOffer struct {
 	natType     string
 	sdp         []byte
-	fingerprint string
+	fingerprint [20]byte
 }
 
 func main() {
diff --git a/broker/ipc.go b/broker/ipc.go
index 2ef4ccd..e11a33c 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"container/heap"
+	"encoding/hex"
 	"fmt"
 	"log"
 	"net"
@@ -130,11 +131,16 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 	}
 
 	offer := &ClientOffer{
-		natType:     req.NAT,
-		sdp:         []byte(req.Offer),
-		fingerprint: req.Fingerprint,
+		natType: req.NAT,
+		sdp:     []byte(req.Offer),
 	}
 
+	fingerprint, err := hex.DecodeString(req.Fingerprint)
+	if err != nil {
+		return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response)
+	}
+	copy(offer.fingerprint[:], fingerprint)
+
 	// Only hand out known restricted snowflakes to unrestricted clients
 	var snowflakeHeap *SnowflakeHeap
 	if offer.natType == NATUnrestricted {
diff --git a/common/messages/client.go b/common/messages/client.go
index 4d435ab..96f8ed8 100644
--- a/common/messages/client.go
+++ b/common/messages/client.go
@@ -5,6 +5,7 @@ package messages
 
 import (
 	"bytes"
+	"encoding/hex"
 	"encoding/json"
 	"fmt"
 
@@ -105,6 +106,9 @@ func DecodeClientPollRequest(data []byte) (*ClientPollRequest, error) {
 	if message.Fingerprint == "" {
 		message.Fingerprint = defaultBridgeFingerprint
 	}
+	if hex.DecodedLen(len(message.Fingerprint)) != 20 {
+		return nil, fmt.Errorf("cannot decode fingerprint")
+	}
 
 	switch message.NAT {
 	case "":
diff --git a/common/messages/messages_test.go b/common/messages/messages_test.go
index 5365aa8..dd1f4fb 100644
--- a/common/messages/messages_test.go
+++ b/common/messages/messages_test.go
@@ -344,6 +344,12 @@ func TestEncodeClientPollRequests(t *testing.T) {
 				defaultBridgeFingerprint,
 				nil,
 			},
+			{
+				"unknown",
+				"fake",
+				"123123",
+				fmt.Errorf(""),
+			},
 		} {
 			req1 := &ClientPollRequest{
 				NAT:         test.natType,

From d807e9d370e79ede725e45edf259223820bb7dc9 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Mon, 11 Apr 2022 11:38:52 -0400
Subject: [PATCH 330/385] Move tor-specific code outside of client library

---
 client/lib/lib_test.go              | 5 +++--
 client/{lib => }/pt_event_logger.go | 2 +-
 client/snowflake.go                 | 2 +-
 3 files changed, 5 insertions(+), 4 deletions(-)
 rename client/{lib => }/pt_event_logger.go (97%)

diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go
index 45e8fe2..e1b6427 100644
--- a/client/lib/lib_test.go
+++ b/client/lib/lib_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 	"time"
 
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
@@ -171,8 +172,8 @@ func TestSnowflakeClient(t *testing.T) {
 
 func TestWebRTCPeer(t *testing.T) {
 	Convey("WebRTCPeer", t, func(c C) {
-		eventsLogger := NewPTEventLogger()
-		p := &WebRTCPeer{closed: make(chan struct{}), eventsLogger: eventsLogger}
+		p := &WebRTCPeer{closed: make(chan struct{}),
+			eventsLogger: event.NewSnowflakeEventDispatcher()}
 		Convey("checks for staleness", func() {
 			go p.checkForStaleness(time.Second)
 			<-time.After(2 * time.Second)
diff --git a/client/lib/pt_event_logger.go b/client/pt_event_logger.go
similarity index 97%
rename from client/lib/pt_event_logger.go
rename to client/pt_event_logger.go
index 25883c4..788c074 100644
--- a/client/lib/pt_event_logger.go
+++ b/client/pt_event_logger.go
@@ -1,4 +1,4 @@
-package snowflake_client
+package main
 
 import (
 	"fmt"
diff --git a/client/snowflake.go b/client/snowflake.go
index 33834ad..ac66a0d 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -104,7 +104,7 @@ func socksAcceptLoop(ln *pt.SocksListener, config sf.ClientConfig, shutdown chan
 				log.Println("Failed to start snowflake transport: ", err)
 				return
 			}
-			transport.AddSnowflakeEventListener(sf.NewPTEventLogger())
+			transport.AddSnowflakeEventListener(NewPTEventLogger())
 			err = conn.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0})
 			if err != nil {
 				log.Printf("conn.Grant error: %s", err)

From aab806429fb1cec915b30a0e66a9921bf33bee74 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Mon, 11 Apr 2022 11:50:36 -0400
Subject: [PATCH 331/385] Fix gitlab CI to work with multiple client .go files

---
 .gitlab-ci.yml | 2 +-
 go.mod         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2ef29ef..ebbcf36 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -135,7 +135,7 @@ android:
 
     - cd $CI_PROJECT_DIR/client
     # gomobile builds a shared library not a CLI executable
-    - sed -i 's,^package main$,package snowflakeclient,' snowflake.go
+    - sed -i 's,^package main$,package snowflakeclient,' *.go
     - go get golang.org/x/mobile/bind
     - gomobile bind -v -target=android $REPRODUCIBLE_FLAGS .
 
diff --git a/go.mod b/go.mod
index 705c05a..432a237 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,7 @@ require (
 	github.com/pion/webrtc/v3 v3.0.15
 	github.com/prometheus/client_golang v1.10.0
 	github.com/prometheus/client_model v0.2.0
-	github.com/refraction-networking/utls v1.0.0 // indirect
+	github.com/refraction-networking/utls v1.0.0
 	github.com/smartystreets/goconvey v1.6.4
 	github.com/stretchr/testify v1.7.0 // indirect
 	github.com/xtaci/kcp-go/v5 v5.6.1

From e2838201adf1a98ee065cc65598463777afac2dc Mon Sep 17 00:00:00 2001
From: itchyonion 
Date: Wed, 23 Mar 2022 08:49:28 -0700
Subject: [PATCH 332/385] Scrub ptEvent logs

---
 client/pt_event_logger.go | 53 +++++++++++++++++++++++++++++++++------
 go.mod                    |  3 +--
 go.sum                    | 10 --------
 3 files changed, 47 insertions(+), 19 deletions(-)

diff --git a/client/pt_event_logger.go b/client/pt_event_logger.go
index 788c074..aae8716 100644
--- a/client/pt_event_logger.go
+++ b/client/pt_event_logger.go
@@ -1,10 +1,12 @@
 package main
 
 import (
+	"bytes"
 	"fmt"
-
 	pt "git.torproject.org/pluggable-transports/goptlib.git"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
+	"strings"
 )
 
 func NewPTEventLogger() event.SnowflakeEventReceiver {
@@ -14,30 +16,67 @@ func NewPTEventLogger() event.SnowflakeEventReceiver {
 type ptEventLogger struct {
 }
 
+type logSeverity int
+
+const (
+	Debug logSeverity = iota
+	Info
+	Notice
+	Warning
+	Error
+)
+
+func safePTLog(severity logSeverity, format string, a ...interface{}) {
+	var buff bytes.Buffer
+	scrubber := &safelog.LogScrubber{Output: &buff}
+
+	// make sure logString ends with exactly one "\n" so it's not stuck in scrubber.Write()'s internal buffer
+	logString := strings.TrimRight(fmt.Sprintf(format, a...), "\n") + "\n"
+	scrubber.Write([]byte(logString))
+
+	// remove newline before calling pt.Log because it adds a newline
+	msg := strings.TrimRight(buff.String(), "\n")
+
+	switch severity {
+	case Error:
+		pt.Log(pt.LogSeverityError, msg)
+	case Warning:
+		pt.Log(pt.LogSeverityWarning, msg)
+	case Notice:
+		pt.Log(pt.LogSeverityWarning, msg)
+	case Info:
+		pt.Log(pt.LogSeverityInfo, msg)
+	case Debug:
+		pt.Log(pt.LogSeverityDebug, msg)
+	default:
+		pt.Log(pt.LogSeverityNotice, msg)
+	}
+}
+
 func (p ptEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
 	switch e.(type) {
 	case event.EventOnOfferCreated:
 		e := e.(event.EventOnOfferCreated)
 		if e.Error != nil {
-			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("offer creation failure %v", e.Error.Error()))
+			safePTLog(Notice, "offer creation failure %v", e.Error.Error())
 		} else {
-			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("offer created"))
+			safePTLog(Notice, "offer created")
 		}
 
 	case event.EventOnBrokerRendezvous:
 		e := e.(event.EventOnBrokerRendezvous)
 		if e.Error != nil {
-			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("broker failure %v", e.Error.Error()))
+			safePTLog(Notice, "broker failure %v", e.Error.Error())
 		} else {
-			pt.Log(pt.LogSeverityNotice, fmt.Sprintf("broker rendezvous peer received"))
+			safePTLog(Notice, "broker rendezvous peer received")
 		}
 
 	case event.EventOnSnowflakeConnected:
-		pt.Log(pt.LogSeverityNotice, fmt.Sprintf("connected"))
+		safePTLog(Notice, "connected")
 
 	case event.EventOnSnowflakeConnectionFailed:
 		e := e.(event.EventOnSnowflakeConnectionFailed)
-		pt.Log(pt.LogSeverityNotice, fmt.Sprintf("connection failed %v", e.Error.Error()))
+		safePTLog(Notice, "trying a new proxy: %v", e.Error.Error())
 	}
 
 }
diff --git a/go.mod b/go.mod
index 432a237..d05b325 100644
--- a/go.mod
+++ b/go.mod
@@ -10,13 +10,12 @@ require (
 	github.com/pion/ice/v2 v2.0.15
 	github.com/pion/sdp/v3 v3.0.4
 	github.com/pion/stun v0.3.5
-	github.com/pion/transport v0.12.3 // indirect
 	github.com/pion/webrtc/v3 v3.0.15
 	github.com/prometheus/client_golang v1.10.0
 	github.com/prometheus/client_model v0.2.0
 	github.com/refraction-networking/utls v1.0.0
 	github.com/smartystreets/goconvey v1.6.4
-	github.com/stretchr/testify v1.7.0 // indirect
+	github.com/stretchr/testify v1.7.0
 	github.com/xtaci/kcp-go/v5 v5.6.1
 	github.com/xtaci/smux v1.5.15
 	gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01
diff --git a/go.sum b/go.sum
index c2fa108..c2258f0 100644
--- a/go.sum
+++ b/go.sum
@@ -224,7 +224,6 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
 github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
 github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
 github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI=
-github.com/pion/dtls/v2 v2.0.8 h1:reGe8rNIMfO/UAeFLqO61tl64t154Qfkr4U3Gzu1tsg=
 github.com/pion/dtls/v2 v2.0.8/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10=
 github.com/pion/dtls/v2 v2.0.12 h1:QMSvNht7FM/XDXij3Ic90SCbl5yL7kppeI4ghfF4in8=
 github.com/pion/dtls/v2 v2.0.12/go.mod h1:5Pe3QJI0Ajsx+uCfxREeewGFlKYBzLrXe9ku7Y0oRXM=
@@ -260,7 +259,6 @@ github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw
 github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
 github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
 github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
-github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
 github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
 github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
 github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
@@ -380,8 +378,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo=
-golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
 golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -421,8 +417,6 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c h1:WtYZ93XtWSO5KlOMgPZu7hXY9WhMZpprvlm5VwvAl8c=
 golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -465,9 +459,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M=
-golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -475,7 +466,6 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

From b6875c6ae91bf2154c55e41ee346a117af7306e5 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Tue, 12 Apr 2022 12:10:01 -0400
Subject: [PATCH 333/385] Bump webrtc library version

go get github.com/pion/webrtc/v3@latest
go mod tidy
---
 go.mod |  14 +++----
 go.sum | 116 ++++++++++++++++++++++++++++++---------------------------
 2 files changed, 68 insertions(+), 62 deletions(-)

diff --git a/go.mod b/go.mod
index d05b325..2afb03b 100644
--- a/go.mod
+++ b/go.mod
@@ -4,22 +4,20 @@ go 1.13
 
 require (
 	git.torproject.org/pluggable-transports/goptlib.git v1.1.0
-	github.com/google/uuid v1.2.0 // indirect
 	github.com/gorilla/websocket v1.4.1
-	github.com/pion/dtls/v2 v2.0.12 // indirect
-	github.com/pion/ice/v2 v2.0.15
+	github.com/pion/ice/v2 v2.2.3
 	github.com/pion/sdp/v3 v3.0.4
 	github.com/pion/stun v0.3.5
-	github.com/pion/webrtc/v3 v3.0.15
+	github.com/pion/webrtc/v3 v3.1.28
 	github.com/prometheus/client_golang v1.10.0
 	github.com/prometheus/client_model v0.2.0
 	github.com/refraction-networking/utls v1.0.0
 	github.com/smartystreets/goconvey v1.6.4
-	github.com/stretchr/testify v1.7.0
+	github.com/stretchr/testify v1.7.1
 	github.com/xtaci/kcp-go/v5 v5.6.1
 	github.com/xtaci/smux v1.5.15
 	gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01
-	golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
-	golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c
-	google.golang.org/protobuf v1.23.0
+	golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
+	golang.org/x/net v0.0.0-20220401154927-543a649e0bdd
+	google.golang.org/protobuf v1.26.0
 )
diff --git a/go.sum b/go.sum
index c2258f0..68c19b7 100644
--- a/go.sum
+++ b/go.sum
@@ -67,6 +67,7 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -84,8 +85,10 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -93,14 +96,14 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
-github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
@@ -195,17 +198,19 @@ github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
 github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
 github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
 github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
+github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
 github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
 github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
@@ -221,49 +226,46 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP
 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
 github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
-github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
-github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
-github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI=
-github.com/pion/dtls/v2 v2.0.8/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10=
-github.com/pion/dtls/v2 v2.0.12 h1:QMSvNht7FM/XDXij3Ic90SCbl5yL7kppeI4ghfF4in8=
-github.com/pion/dtls/v2 v2.0.12/go.mod h1:5Pe3QJI0Ajsx+uCfxREeewGFlKYBzLrXe9ku7Y0oRXM=
-github.com/pion/ice/v2 v2.0.15 h1:KZrwa2ciL9od8+TUVJiYTNsCW9J5lktBjGwW1MacEnQ=
-github.com/pion/ice/v2 v2.0.15/go.mod h1:ZIiVGevpgAxF/cXiIVmuIUtCb3Xs4gCzCbXB6+nFkSI=
-github.com/pion/interceptor v0.0.10 h1:dXFyFWRJFwmzQqyn0U8dUAbOJu+JJnMVAqxmvTu30B4=
-github.com/pion/interceptor v0.0.10/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4=
+github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
+github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
+github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio=
+github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
+github.com/pion/ice/v2 v2.2.3 h1:kBVhmtMcI1L3bWDepilO9kKpCGpLQeppCuVxVS8obhE=
+github.com/pion/ice/v2 v2.2.3/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
+github.com/pion/interceptor v0.1.10 h1:DJ2GjMGm4XGIQgMJxuEpdaExdY/6RdngT7Uh4oVmquU=
+github.com/pion/interceptor v0.1.10/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
-github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
+github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
+github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
 github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
 github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
-github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo=
 github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
-github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U=
-github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
-github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE=
-github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
+github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
+github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
+github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
+github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
+github.com/pion/rtp v1.7.12 h1:Wtrx1btLYn96vQGx35UTpgRBG/MGJmIHvrGND1m219A=
+github.com/pion/rtp v1.7.12/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
+github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
+github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
+github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
 github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
 github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
-github.com/pion/srtp/v2 v2.0.2 h1:664iGzVmaY7KYS5M0gleY0DscRo9ReDfTxQrq4UgGoU=
-github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0=
+github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
+github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
 github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
 github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
-github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
-github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
-github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
-github.com/pion/transport v0.12.1/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
 github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
-github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw=
 github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
-github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
-github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
-github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
+github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
+github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
+github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
+github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
 github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
 github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
-github.com/pion/webrtc/v3 v3.0.15 h1:g8MMJohjQoj0+pTrU329tWM6dvCieNTgnjtqv1kmEdY=
-github.com/pion/webrtc/v3 v3.0.15/go.mod h1:uUt2nRSsCnK/nfzTAfOmaeLan26ZJ0aP9iwjc/gcC2Y=
+github.com/pion/webrtc/v3 v3.1.28 h1:cNUENLrHmY3PWO9na3RGrhnSjzPLQyXRVRDREC7a5Ug=
+github.com/pion/webrtc/v3 v3.1.28/go.mod h1:MKhUmhMsy0NZuLpZkEvTg7tcn9HBHZO39Mh2+VDj67g=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -331,8 +333,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
 github.com/templexxx/cpu v0.0.7 h1:pUEZn8JBy/w5yzdYWgx+0m0xL9uk6j4K91C5kOViAzo=
 github.com/templexxx/cpu v0.0.7/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
@@ -352,6 +355,7 @@ github.com/xtaci/smux v1.5.15 h1:6hMiXswcleXj5oNfcJc+DXS8Vj36XX2LaX98udog6Kc=
 github.com/xtaci/smux v1.5.15/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01 h1:4949mHh9Vj2/okk48yG8nhP6TosFWOUfSfSr502sKGE=
 gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01/go.mod h1:K3LOI4H8fa6j+7E10ViHeGEQV10304FG4j94ypmKLjY=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -376,10 +380,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
-golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
+golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -407,19 +409,19 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c h1:WtYZ93XtWSO5KlOMgPZu7hXY9WhMZpprvlm5VwvAl8c=
 golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220401154927-543a649e0bdd h1:zYlwaUHTmxuf6H7hwO2dgwqozQmH7zf4x+/qql4oVWc=
+golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -429,6 +431,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -446,29 +449,30 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -486,8 +490,9 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200425043458-8463f397d07c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123 h1:4JSJPND/+4555t1HfXYF4UEqDqiSKCgeV0+hbA8hMs4=
 golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -516,8 +521,10 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -536,6 +543,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

From dd83b68efa63144a80bd81b9c2a550a1825fa464 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Tue, 24 May 2022 11:39:47 -0400
Subject: [PATCH 334/385] Bump version of pion/webrtc to v3.1.41

This bumps the version of pion/dtls to v2.1.5 to fix three CVEs:
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=2022-29189
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=2022-29190
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=2022-29222
---
 go.mod | 10 +++++-----
 go.sum | 37 +++++++++++++++++++------------------
 2 files changed, 24 insertions(+), 23 deletions(-)

diff --git a/go.mod b/go.mod
index 2afb03b..842648c 100644
--- a/go.mod
+++ b/go.mod
@@ -5,10 +5,10 @@ go 1.13
 require (
 	git.torproject.org/pluggable-transports/goptlib.git v1.1.0
 	github.com/gorilla/websocket v1.4.1
-	github.com/pion/ice/v2 v2.2.3
-	github.com/pion/sdp/v3 v3.0.4
+	github.com/pion/ice/v2 v2.2.6
+	github.com/pion/sdp/v3 v3.0.5
 	github.com/pion/stun v0.3.5
-	github.com/pion/webrtc/v3 v3.1.28
+	github.com/pion/webrtc/v3 v3.1.41
 	github.com/prometheus/client_golang v1.10.0
 	github.com/prometheus/client_model v0.2.0
 	github.com/refraction-networking/utls v1.0.0
@@ -17,7 +17,7 @@ require (
 	github.com/xtaci/kcp-go/v5 v5.6.1
 	github.com/xtaci/smux v1.5.15
 	gitlab.torproject.org/tpo/anti-censorship/geoip v0.0.0-20210928150955-7ce4b3d98d01
-	golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
-	golang.org/x/net v0.0.0-20220401154927-543a649e0bdd
+	golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8
+	golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
 	google.golang.org/protobuf v1.26.0
 )
diff --git a/go.sum b/go.sum
index 68c19b7..2c6f232 100644
--- a/go.sum
+++ b/go.sum
@@ -228,32 +228,30 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
 github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
-github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio=
 github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
-github.com/pion/ice/v2 v2.2.3 h1:kBVhmtMcI1L3bWDepilO9kKpCGpLQeppCuVxVS8obhE=
-github.com/pion/ice/v2 v2.2.3/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
-github.com/pion/interceptor v0.1.10 h1:DJ2GjMGm4XGIQgMJxuEpdaExdY/6RdngT7Uh4oVmquU=
-github.com/pion/interceptor v0.1.10/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
+github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
+github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
+github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
+github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
+github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
+github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
 github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
 github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
 github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
 github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
-github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
 github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
 github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
-github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/rtp v1.7.12 h1:Wtrx1btLYn96vQGx35UTpgRBG/MGJmIHvrGND1m219A=
-github.com/pion/rtp v1.7.12/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
+github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
+github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
 github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
 github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
 github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
-github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
-github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
-github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
-github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
+github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
+github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
+github.com/pion/srtp/v2 v2.0.9 h1:JJq3jClmDFBPX/F5roEb0U19jSU7eUhyDqR/NZ34EKQ=
+github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
 github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
 github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
 github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
@@ -264,8 +262,8 @@ github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
 github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
 github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
 github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
-github.com/pion/webrtc/v3 v3.1.28 h1:cNUENLrHmY3PWO9na3RGrhnSjzPLQyXRVRDREC7a5Ug=
-github.com/pion/webrtc/v3 v3.1.28/go.mod h1:MKhUmhMsy0NZuLpZkEvTg7tcn9HBHZO39Mh2+VDj67g=
+github.com/pion/webrtc/v3 v3.1.41 h1:QogLjtriu+OwerRp4r6emTg4+zDWUy5R6EqthDBy7c0=
+github.com/pion/webrtc/v3 v3.1.41/go.mod h1:sUcW9SFPEWerDqGOBmdYEMfRvbdd7rgwo4bNzfsXww4=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -380,8 +378,10 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
 golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
+golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -420,8 +420,9 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220401154927-543a649e0bdd h1:zYlwaUHTmxuf6H7hwO2dgwqozQmH7zf4x+/qql4oVWc=
 golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

From 9757784c5aa25a6148f7bdf39295b08c738b06d5 Mon Sep 17 00:00:00 2001
From: itchyonion 
Date: Wed, 30 Mar 2022 12:24:43 -0700
Subject: [PATCH 335/385] Wait some time before displaying the proxy usage log

---
 common/task/periodic.go      | 6 ++++++
 proxy/lib/pt_event_logger.go | 2 +-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/common/task/periodic.go b/common/task/periodic.go
index 37c56eb..7c1a235 100644
--- a/common/task/periodic.go
+++ b/common/task/periodic.go
@@ -95,6 +95,12 @@ func (t *Periodic) Start() error {
 	return nil
 }
 
+func (t *Periodic) WaitThenStart() {
+	time.AfterFunc(t.Interval, func() {
+		t.Start()
+	})
+}
+
 // Close implements common.Closable.
 func (t *Periodic) Close() error {
 	t.access.Lock()
diff --git a/proxy/lib/pt_event_logger.go b/proxy/lib/pt_event_logger.go
index df94b0a..cb262e4 100644
--- a/proxy/lib/pt_event_logger.go
+++ b/proxy/lib/pt_event_logger.go
@@ -13,7 +13,7 @@ func NewProxyEventLogger(logPeriod time.Duration, output io.Writer) event.Snowfl
 	logger := log.New(output, "", log.LstdFlags|log.LUTC)
 	el := &logEventLogger{logPeriod: logPeriod, logger: logger}
 	el.task = &task.Periodic{Interval: logPeriod, Execute: el.logTick}
-	el.task.Start()
+	el.task.WaitThenStart()
 	return el
 }
 

From 1d592b06e51b42ca4ed13ec219f012a2915c2b1d Mon Sep 17 00:00:00 2001
From: meskio 
Date: Fri, 20 May 2022 09:43:03 +0200
Subject: [PATCH 336/385] Implement String() method on events

To make it safe for logging safelog.Scrub function is now public.

Closes: #40141
---
 client/pt_event_logger.go | 67 +--------------------------------------
 common/event/interface.go | 36 ++++++++++++++++++++-
 common/safelog/log.go     |  4 +--
 3 files changed, 38 insertions(+), 69 deletions(-)

diff --git a/client/pt_event_logger.go b/client/pt_event_logger.go
index aae8716..483a3c6 100644
--- a/client/pt_event_logger.go
+++ b/client/pt_event_logger.go
@@ -1,12 +1,8 @@
 package main
 
 import (
-	"bytes"
-	"fmt"
 	pt "git.torproject.org/pluggable-transports/goptlib.git"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
-	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
-	"strings"
 )
 
 func NewPTEventLogger() event.SnowflakeEventReceiver {
@@ -16,67 +12,6 @@ func NewPTEventLogger() event.SnowflakeEventReceiver {
 type ptEventLogger struct {
 }
 
-type logSeverity int
-
-const (
-	Debug logSeverity = iota
-	Info
-	Notice
-	Warning
-	Error
-)
-
-func safePTLog(severity logSeverity, format string, a ...interface{}) {
-	var buff bytes.Buffer
-	scrubber := &safelog.LogScrubber{Output: &buff}
-
-	// make sure logString ends with exactly one "\n" so it's not stuck in scrubber.Write()'s internal buffer
-	logString := strings.TrimRight(fmt.Sprintf(format, a...), "\n") + "\n"
-	scrubber.Write([]byte(logString))
-
-	// remove newline before calling pt.Log because it adds a newline
-	msg := strings.TrimRight(buff.String(), "\n")
-
-	switch severity {
-	case Error:
-		pt.Log(pt.LogSeverityError, msg)
-	case Warning:
-		pt.Log(pt.LogSeverityWarning, msg)
-	case Notice:
-		pt.Log(pt.LogSeverityWarning, msg)
-	case Info:
-		pt.Log(pt.LogSeverityInfo, msg)
-	case Debug:
-		pt.Log(pt.LogSeverityDebug, msg)
-	default:
-		pt.Log(pt.LogSeverityNotice, msg)
-	}
-}
-
 func (p ptEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
-	switch e.(type) {
-	case event.EventOnOfferCreated:
-		e := e.(event.EventOnOfferCreated)
-		if e.Error != nil {
-			safePTLog(Notice, "offer creation failure %v", e.Error.Error())
-		} else {
-			safePTLog(Notice, "offer created")
-		}
-
-	case event.EventOnBrokerRendezvous:
-		e := e.(event.EventOnBrokerRendezvous)
-		if e.Error != nil {
-			safePTLog(Notice, "broker failure %v", e.Error.Error())
-		} else {
-			safePTLog(Notice, "broker rendezvous peer received")
-		}
-
-	case event.EventOnSnowflakeConnected:
-		safePTLog(Notice, "connected")
-
-	case event.EventOnSnowflakeConnectionFailed:
-		e := e.(event.EventOnSnowflakeConnectionFailed)
-		safePTLog(Notice, "trying a new proxy: %v", e.Error.Error())
-	}
-
+	pt.Log(pt.LogSeverityNotice, e.String())
 }
diff --git a/common/event/interface.go b/common/event/interface.go
index b41d7c3..968b270 100644
--- a/common/event/interface.go
+++ b/common/event/interface.go
@@ -1,6 +1,11 @@
 package event
 
-import "github.com/pion/webrtc/v3"
+import (
+	"fmt"
+
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
+	"github.com/pion/webrtc/v3"
+)
 
 type SnowflakeEvent interface {
 	IsSnowflakeEvent()
@@ -13,27 +18,56 @@ type EventOnOfferCreated struct {
 	Error                  error
 }
 
+func (e EventOnOfferCreated) String() string {
+	if e.Error != nil {
+		scrubbed := safelog.Scrub([]byte(e.Error.Error()))
+		return fmt.Sprintf("offer creation failure %s", scrubbed)
+	}
+	return "offer created"
+}
+
 type EventOnBrokerRendezvous struct {
 	SnowflakeEvent
 	WebRTCRemoteDescription *webrtc.SessionDescription
 	Error                   error
 }
 
+func (e EventOnBrokerRendezvous) String() string {
+	if e.Error != nil {
+		scrubbed := safelog.Scrub([]byte(e.Error.Error()))
+		return fmt.Sprintf("broker failure %s", scrubbed)
+	}
+	return "broker rendezvous peer received"
+}
+
 type EventOnSnowflakeConnected struct {
 	SnowflakeEvent
 }
 
+func (e EventOnSnowflakeConnected) String() string {
+	return "connected"
+}
+
 type EventOnSnowflakeConnectionFailed struct {
 	SnowflakeEvent
 	Error error
 }
 
+func (e EventOnSnowflakeConnectionFailed) String() string {
+	scrubbed := safelog.Scrub([]byte(e.Error.Error()))
+	return fmt.Sprintf("trying a new proxy: %s", scrubbed)
+}
+
 type EventOnProxyConnectionOver struct {
 	SnowflakeEvent
 	InboundTraffic  int
 	OutboundTraffic int
 }
 
+func (e EventOnProxyConnectionOver) String() string {
+	return fmt.Sprintf("Proxy connection closed (↑ %d, ↓ %d)", e.InboundTraffic, e.OutboundTraffic)
+}
+
 type SnowflakeEventReceiver interface {
 	// OnNewSnowflakeEvent notify receiver about a new event
 	// This method MUST not block
diff --git a/common/safelog/log.go b/common/safelog/log.go
index 4a135ce..6ca23ee 100644
--- a/common/safelog/log.go
+++ b/common/safelog/log.go
@@ -38,7 +38,7 @@ type LogScrubber struct {
 func (ls *LogScrubber) Lock()   { (*ls).lock.Lock() }
 func (ls *LogScrubber) Unlock() { (*ls).lock.Unlock() }
 
-func scrub(b []byte) []byte {
+func Scrub(b []byte) []byte {
 	scrubbedBytes := b
 	for _, pattern := range scrubberPatterns {
 		// this is a workaround since go does not yet support look ahead or look
@@ -62,7 +62,7 @@ func (ls *LogScrubber) Write(b []byte) (n int, err error) {
 			return
 		}
 		fullLines := ls.buffer[:i+1]
-		_, err = ls.Output.Write(scrub(fullLines))
+		_, err = ls.Output.Write(Scrub(fullLines))
 		if err != nil {
 			return
 		}

From 3473b438e518f12fc1e0945b362e8cef25756b4f Mon Sep 17 00:00:00 2001
From: meskio 
Date: Wed, 25 May 2022 17:56:12 +0200
Subject: [PATCH 337/385] Move ptEventLogger into the client/snowflake.go

Remove client/pt_event_logger.go file as is very minimal.
---
 client/pt_event_logger.go | 17 -----------------
 client/snowflake.go       | 12 ++++++++++++
 2 files changed, 12 insertions(+), 17 deletions(-)
 delete mode 100644 client/pt_event_logger.go

diff --git a/client/pt_event_logger.go b/client/pt_event_logger.go
deleted file mode 100644
index 483a3c6..0000000
--- a/client/pt_event_logger.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package main
-
-import (
-	pt "git.torproject.org/pluggable-transports/goptlib.git"
-	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
-)
-
-func NewPTEventLogger() event.SnowflakeEventReceiver {
-	return &ptEventLogger{}
-}
-
-type ptEventLogger struct {
-}
-
-func (p ptEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
-	pt.Log(pt.LogSeverityNotice, e.String())
-}
diff --git a/client/snowflake.go b/client/snowflake.go
index ac66a0d..2cb6549 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -17,6 +17,7 @@ import (
 
 	pt "git.torproject.org/pluggable-transports/goptlib.git"
 	sf "git.torproject.org/pluggable-transports/snowflake.git/v2/client/lib"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
 )
 
@@ -24,6 +25,17 @@ const (
 	DefaultSnowflakeCapacity = 1
 )
 
+type ptEventLogger struct {
+}
+
+func NewPTEventLogger() event.SnowflakeEventReceiver {
+	return &ptEventLogger{}
+}
+
+func (p ptEventLogger) OnNewSnowflakeEvent(e event.SnowflakeEvent) {
+	pt.Log(pt.LogSeverityNotice, e.String())
+}
+
 // Exchanges bytes between two ReadWriters.
 // (In this case, between a SOCKS connection and a snowflake transport conn)
 func copyLoop(socks, sfconn io.ReadWriter) {

From ae5a71e6e58e664311e3a12f9adb48ed439df4a5 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Wed, 25 May 2022 12:10:27 -0400
Subject: [PATCH 338/385] Updated ChangeLog for v2.2.0 release

---
 ChangeLog | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/ChangeLog b/ChangeLog
index 00f71bf..9ac6fae 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,19 @@
+Changes in version v2.2.0 - 2022-05-25
+
+- Issue 40099: Initialize SnowflakeListener.closed
+- Add connection failure events for proxy timeouts
+- Issue 40103: Fix proxy logging verb tense
+- Fix up and downstream metrics output for proxy
+- Issue 40095: uTLS for broker negotiation
+- Forward bridge fingerprint from client to broker (WIP, Issue 28651)
+- Issue 40104: Make it easier to configure proxy type
+- Remove version from ClientPollRequest
+- Issue 40124: Move tor-specific code out of library
+- Issue 40115: Scrub pt event logs
+- Issue 40127: Bump webrtc and dtls library versions
+- Bump version of webrtc and dtls to fix dtls CVEs
+- Issue 40141: Ensure library calls of events can be scrubbed
+
 Changes in version v2.1.0 - 2022-02-08
 
 - Issue 40098: Remove support for legacy one shot mode

From 4e7f8975273a8386e632cfa40a4fcf6f1a6a4aee Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Thu, 26 May 2022 12:51:54 -0400
Subject: [PATCH 339/385] Update snowflake CI to test with go 1.18

---
 .gitlab-ci.yml | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ebbcf36..e03c8c4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -139,13 +139,6 @@ android:
     - go get golang.org/x/mobile/bind
     - gomobile bind -v -target=android $REPRODUCIBLE_FLAGS .
 
-go-1.15:
-  image: golang:1.15-stretch
-  <<: *golang-docker-debian-template
-  <<: *test-template
-  script:
-    - *go-test
-
 go-1.16:
   image: golang:1.16-stretch
   <<: *golang-docker-debian-template
@@ -160,6 +153,13 @@ go-1.17:
   script:
     - *go-test
 
+go-1.18:
+  image: golang:1.18-stretch
+  <<: *golang-docker-debian-template
+  <<: *test-template
+  script:
+    - *go-test
+
 debian-testing:
   image: debian:testing
   <<: *debian-native-template

From 6310ca438152b5c467eee93d764d35b5b1f60f70 Mon Sep 17 00:00:00 2001
From: Cecylia Bocovich 
Date: Fri, 27 May 2022 10:01:19 -0400
Subject: [PATCH 340/385] Avoid performing two NAT probe tests at startup

After the initial NAT probe test, a full interval before starting the
recurring NAT retests.
---
 proxy/lib/snowflake.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 17f0126..c508447 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -574,7 +574,7 @@ func (sf *SnowflakeProxy) Start() error {
 	}
 
 	if sf.NATTypeMeasurementInterval != 0 {
-		NatRetestTask.Start()
+		NatRetestTask.WaitThenStart()
 		defer NatRetestTask.Close()
 	}
 

From e4c01f0595f4f9bd5e87c4f1ef83132ce88c7ee2 Mon Sep 17 00:00:00 2001
From: itchyonion 
Date: Mon, 16 May 2022 14:59:47 -0700
Subject: [PATCH 341/385] Wrap client NAT log

---
 client/lib/snowflake.go |  7 +++++--
 common/nat/nat.go       | 29 +++++++----------------------
 2 files changed, 12 insertions(+), 24 deletions(-)

diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go
index dd78c12..7231d86 100644
--- a/client/lib/snowflake.go
+++ b/client/lib/snowflake.go
@@ -237,7 +237,7 @@ func (conn *SnowflakeConn) Close() error {
 }
 
 // loop through all provided STUN servers until we exhaust the list or find
-// one that is compatable with RFC 5780
+// one that is compatible with RFC 5780
 func updateNATType(servers []webrtc.ICEServer, broker *BrokerChannel) {
 
 	var restrictedNAT bool
@@ -245,7 +245,10 @@ func updateNATType(servers []webrtc.ICEServer, broker *BrokerChannel) {
 	for _, server := range servers {
 		addr := strings.TrimPrefix(server.URLs[0], "stun:")
 		restrictedNAT, err = nat.CheckIfRestrictedNAT(addr)
-		if err == nil {
+
+		if err != nil {
+			log.Printf("Warning: NAT checking failed for server at %s: %s", addr, err)
+		} else {
 			if restrictedNAT {
 				broker.SetNATType(nat.NATRestricted)
 			} else {
diff --git a/common/nat/nat.go b/common/nat/nat.go
index 552ed45..81a82bc 100644
--- a/common/nat/nat.go
+++ b/common/nat/nat.go
@@ -49,8 +49,7 @@ func isRestrictedMapping(addrStr string) (bool, error) {
 
 	mapTestConn, err := connect(addrStr)
 	if err != nil {
-		log.Printf("Error creating STUN connection: %s", err.Error())
-		return false, err
+		return false, fmt.Errorf("Error creating STUN connection: %w", err)
 	}
 
 	defer mapTestConn.Close()
@@ -59,48 +58,34 @@ func isRestrictedMapping(addrStr string) (bool, error) {
 	message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
 
 	resp, err := mapTestConn.RoundTrip(message, mapTestConn.PrimaryAddr)
-	if err == ErrTimedOut {
-		log.Printf("Error: no response from server")
-		return false, err
-	}
 	if err != nil {
-		log.Printf("Error receiving response from server: %s", err.Error())
-		return false, err
+		return false, fmt.Errorf("Error completing roundtrip map test: %w", err)
 	}
 
 	// Decoding XOR-MAPPED-ADDRESS attribute from message.
 	if err = xorAddr1.GetFrom(resp); err != nil {
-		log.Printf("Error retrieving XOR-MAPPED-ADDRESS resonse: %s", err.Error())
-		return false, err
+		return false, fmt.Errorf("Error retrieving XOR-MAPPED-ADDRESS resonse: %w", err)
 	}
 
 	// Decoding OTHER-ADDRESS attribute from message.
 	var otherAddr stun.OtherAddress
 	if err = otherAddr.GetFrom(resp); err != nil {
-		log.Println("NAT discovery feature not supported by this server")
-		return false, err
+		return false, fmt.Errorf("NAT discovery feature not supported: %w", err)
 	}
 
 	if err = mapTestConn.AddOtherAddr(otherAddr.String()); err != nil {
-		log.Printf("Failed to resolve address %s\t", otherAddr.String())
-		return false, err
+		return false, fmt.Errorf("Error resolving address %s: %w", otherAddr.String(), err)
 	}
 
 	// Test II: Send binding request to other address
 	resp, err = mapTestConn.RoundTrip(message, mapTestConn.OtherAddr)
-	if err == ErrTimedOut {
-		log.Printf("Error: no response from server")
-		return false, err
-	}
 	if err != nil {
-		log.Printf("Error retrieving server response: %s", err.Error())
-		return false, err
+		return false, fmt.Errorf("Error retrieveing server response: %w", err)
 	}
 
 	// Decoding XOR-MAPPED-ADDRESS attribute from message.
 	if err = xorAddr2.GetFrom(resp); err != nil {
-		log.Printf("Error retrieving XOR-MAPPED-ADDRESS resonse: %s", err.Error())
-		return false, err
+		return false, fmt.Errorf("Error retrieving XOR-MAPPED-ADDRESS resonse: %w", err)
 	}
 
 	return xorAddr1.String() != xorAddr2.String(), nil

From f38c91f906af5b806f463e790eddc134961abf1f Mon Sep 17 00:00:00 2001
From: meskio 
Date: Thu, 2 Jun 2022 11:19:47 +0200
Subject: [PATCH 342/385] Don't use entropy for test

Use math/rand instead of crypto/rand, so entropy is not a blocker when
running the tests.
---
 common/amp/armor_test.go         |  2 +-
 common/utls/roundtripper_test.go | 14 +++++++++++---
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/common/amp/armor_test.go b/common/amp/armor_test.go
index 594ae65..fc7561e 100644
--- a/common/amp/armor_test.go
+++ b/common/amp/armor_test.go
@@ -1,9 +1,9 @@
 package amp
 
 import (
-	"crypto/rand"
 	"io"
 	"io/ioutil"
+	"math/rand"
 	"strings"
 	"testing"
 )
diff --git a/common/utls/roundtripper_test.go b/common/utls/roundtripper_test.go
index 6a91385..bccb799 100644
--- a/common/utls/roundtripper_test.go
+++ b/common/utls/roundtripper_test.go
@@ -1,12 +1,12 @@
 package utls
 
 import (
-	"crypto/rand"
 	"crypto/rsa"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"math/big"
+	"math/rand"
 	"net/http"
 	"testing"
 	"time"
@@ -26,7 +26,15 @@ func TestRoundTripper(t *testing.T) {
 	Convey("[Test]Set up http servers", t, func(c C) {
 		c.Convey("[Test]Generate Self-Signed Cert", func(c C) {
 			// Ported from https://gist.github.com/samuel/8b500ddd3f6118d052b5e6bc16bc4c09
-			priv, err := rsa.GenerateKey(rand.Reader, 4096)
+
+			// note that we use the insecure math/rand here because some platforms
+			// fail the test suite at build time in Debian, due to entropy starvation.
+			// since that's not a problem at test time, we do *not* use a secure
+			// mechanism for key generation.
+			//
+			// DO NOT REUSE THIS CODE IN PRODUCTION, IT IS DANGEROUS
+			insecureRandReader := rand.New(rand.NewSource(1337))
+			priv, err := rsa.GenerateKey(insecureRandReader, 4096)
 			c.So(err, ShouldBeNil)
 			template := x509.Certificate{
 				SerialNumber: big.NewInt(1),
@@ -40,7 +48,7 @@ func TestRoundTripper(t *testing.T) {
 				ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
 				BasicConstraintsValid: true,
 			}
-			derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
+			derBytes, err := x509.CreateCertificate(insecureRandReader, &template, &template, priv.Public(), priv)
 			c.So(err, ShouldBeNil)
 			selfSignedPrivateKey = priv
 			selfSignedCert = derBytes

From 3d4f294241c662872ec75a5adcf8928faec60e5e Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 28 Mar 2022 17:17:10 +0100
Subject: [PATCH 343/385] Add Bridge List Definition

---
 broker/bridge-list.go | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
 create mode 100644 broker/bridge-list.go

diff --git a/broker/bridge-list.go b/broker/bridge-list.go
new file mode 100644
index 0000000..3913a2e
--- /dev/null
+++ b/broker/bridge-list.go
@@ -0,0 +1,18 @@
+package main
+
+import "sync"
+
+type bridgeListHolder struct {
+	bridgeInfo       map[[20]byte]BridgeInfo
+	accessBridgeInfo sync.RWMutex
+}
+
+type BridgeListHolder interface {
+	GetBridgeInfo(fingerprint [20]byte) (BridgeInfo, error)
+}
+
+type BridgeInfo struct {
+	DisplayName      string `json:"displayName"`
+	WebSocketAddress string `json:"webSocketAddress"`
+	Fingerprint      string `json:"fingerprint"`
+}

From 0822c5f87b29aee159645af25d7c97c61f539315 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 29 Mar 2022 13:29:48 +0100
Subject: [PATCH 344/385] Add Bridge List Holder

---
 broker/bridge-list.go | 53 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 52 insertions(+), 1 deletion(-)

diff --git a/broker/bridge-list.go b/broker/bridge-list.go
index 3913a2e..e77db65 100644
--- a/broker/bridge-list.go
+++ b/broker/bridge-list.go
@@ -1,6 +1,20 @@
 package main
 
-import "sync"
+import (
+	"bufio"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"io"
+	"sync"
+)
+
+var ErrBridgeNotFound = errors.New("bridge not found")
+var ErrBridgeFingerprintInvalid = errors.New("bridge fingerprint invalid")
+
+func NewBridgeListHolder() BridgeListHolderFileBased {
+	return &bridgeListHolder{}
+}
 
 type bridgeListHolder struct {
 	bridgeInfo       map[[20]byte]BridgeInfo
@@ -11,8 +25,45 @@ type BridgeListHolder interface {
 	GetBridgeInfo(fingerprint [20]byte) (BridgeInfo, error)
 }
 
+type BridgeListHolderFileBased interface {
+	BridgeListHolder
+	LoadBridgeInfo(reader io.Reader) error
+}
+
 type BridgeInfo struct {
 	DisplayName      string `json:"displayName"`
 	WebSocketAddress string `json:"webSocketAddress"`
 	Fingerprint      string `json:"fingerprint"`
 }
+
+func (h *bridgeListHolder) GetBridgeInfo(fingerprint [20]byte) (BridgeInfo, error) {
+	h.accessBridgeInfo.RLock()
+	defer h.accessBridgeInfo.RUnlock()
+	if bridgeInfo, ok := h.bridgeInfo[fingerprint]; ok {
+		return bridgeInfo, nil
+	}
+	return BridgeInfo{}, ErrBridgeNotFound
+}
+
+func (h *bridgeListHolder) LoadBridgeInfo(reader io.Reader) error {
+	bridgeInfoMap := map[[20]byte]BridgeInfo{}
+	inputScanner := bufio.NewScanner(reader)
+	for inputScanner.Scan() {
+		inputLine := inputScanner.Bytes()
+		bridgeInfo := BridgeInfo{}
+		if err := json.Unmarshal(inputLine, &bridgeInfo); err != nil {
+			return err
+		}
+		var bridgeHash [20]byte
+		if n, err := hex.Decode(bridgeHash[:], []byte(bridgeInfo.Fingerprint)); err != nil {
+			return err
+		} else if n != 20 {
+			return ErrBridgeFingerprintInvalid
+		}
+		bridgeInfoMap[bridgeHash] = bridgeInfo
+	}
+	h.accessBridgeInfo.Lock()
+	defer h.accessBridgeInfo.Unlock()
+	h.bridgeInfo = bridgeInfoMap
+	return nil
+}

From 5578b4dd76339cbf76ee8fced30d08e02a424d3b Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 29 Mar 2022 14:06:05 +0100
Subject: [PATCH 345/385] Add Bridge List Holder Test

---
 broker/bridge-list_test.go | 59 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 59 insertions(+)
 create mode 100644 broker/bridge-list_test.go

diff --git a/broker/bridge-list_test.go b/broker/bridge-list_test.go
new file mode 100644
index 0000000..73da43c
--- /dev/null
+++ b/broker/bridge-list_test.go
@@ -0,0 +1,59 @@
+package main
+
+import (
+	"bytes"
+	"encoding/hex"
+	. "github.com/smartystreets/goconvey/convey"
+	"testing"
+)
+
+const DefaultBridges = `{"displayName":"default", "webSocketAddress":"wss://snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80A72"}
+`
+
+const ImaginaryBridges = `{"displayName":"default", "webSocketAddress":"wss://snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80A72"}
+{"displayName":"imaginary-1", "webSocketAddress":"wss://imaginary-1-snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80B00"}
+{"displayName":"imaginary-2", "webSocketAddress":"wss://imaginary-2-snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80B01"}
+{"displayName":"imaginary-3", "webSocketAddress":"wss://imaginary-3-snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80B02"}
+{"displayName":"imaginary-4", "webSocketAddress":"wss://imaginary-4-snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80B03"}
+{"displayName":"imaginary-5", "webSocketAddress":"wss://imaginary-5-snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80B04"}
+{"displayName":"imaginary-6", "webSocketAddress":"wss://imaginary-6-snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80B05"}
+{"displayName":"imaginary-7", "webSocketAddress":"wss://imaginary-7-snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80B06"}
+{"displayName":"imaginary-8", "webSocketAddress":"wss://imaginary-8-snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80B07"}
+{"displayName":"imaginary-9", "webSocketAddress":"wss://imaginary-9-snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80B08"}
+{"displayName":"imaginary-10", "webSocketAddress":"wss://imaginary-10-snowflake.torproject.org", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80B09"}
+`
+
+func TestBridgeLoad(t *testing.T) {
+	Convey("load default list", t, func() {
+		bridgeList := NewBridgeListHolder()
+		So(bridgeList.LoadBridgeInfo(bytes.NewReader([]byte(DefaultBridges))), ShouldBeNil)
+		{
+			bridgeFingerprint := [20]byte{}
+			{
+				n, err := hex.Decode(bridgeFingerprint[:], []byte("2B280B23E1107BB62ABFC40DDCC8824814F80A72"))
+				So(n, ShouldEqual, 20)
+				So(err, ShouldBeNil)
+			}
+			bridgeInfo, err := bridgeList.GetBridgeInfo(bridgeFingerprint)
+			So(err, ShouldBeNil)
+			So(bridgeInfo.DisplayName, ShouldEqual, "default")
+			So(bridgeInfo.WebSocketAddress, ShouldEqual, "wss://snowflake.torproject.org")
+		}
+	})
+	Convey("load imaginary list", t, func() {
+		bridgeList := NewBridgeListHolder()
+		So(bridgeList.LoadBridgeInfo(bytes.NewReader([]byte(ImaginaryBridges))), ShouldBeNil)
+		{
+			bridgeFingerprint := [20]byte{}
+			{
+				n, err := hex.Decode(bridgeFingerprint[:], []byte("2B280B23E1107BB62ABFC40DDCC8824814F80B07"))
+				So(n, ShouldEqual, 20)
+				So(err, ShouldBeNil)
+			}
+			bridgeInfo, err := bridgeList.GetBridgeInfo(bridgeFingerprint)
+			So(err, ShouldBeNil)
+			So(bridgeInfo.DisplayName, ShouldEqual, "imaginary-8")
+			So(bridgeInfo.WebSocketAddress, ShouldEqual, "wss://imaginary-8-snowflake.torproject.org")
+		}
+	})
+}

From 38f0e00e5d576bbb6ee78cc14ba07003c40d6091 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 30 Mar 2022 16:42:59 +0100
Subject: [PATCH 346/385] Add Domain Name Matcher

Design difference from original vision: Skipped FQDN step to make it more generalized
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/28651#note_2787394
---
 common/namematcher/matcher.go      | 26 ++++++++++++++
 common/namematcher/matcher_test.go | 55 ++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+)
 create mode 100644 common/namematcher/matcher.go
 create mode 100644 common/namematcher/matcher_test.go

diff --git a/common/namematcher/matcher.go b/common/namematcher/matcher.go
new file mode 100644
index 0000000..57f9c56
--- /dev/null
+++ b/common/namematcher/matcher.go
@@ -0,0 +1,26 @@
+package namematcher
+
+import "strings"
+
+func NewNameMatcher(rule string) NameMatcher {
+	return NameMatcher{suffix: strings.TrimPrefix(rule, "^"), exact: strings.HasPrefix(rule, "^")}
+}
+
+type NameMatcher struct {
+	exact  bool
+	suffix string
+}
+
+func (m *NameMatcher) IsSupersetOf(matcher NameMatcher) bool {
+	if m.exact {
+		return matcher.exact && m.suffix == matcher.suffix
+	}
+	return strings.HasSuffix(matcher.suffix, m.suffix)
+}
+
+func (m *NameMatcher) IsMember(s string) bool {
+	if m.exact {
+		return s == m.suffix
+	}
+	return strings.HasSuffix(s, m.suffix)
+}
diff --git a/common/namematcher/matcher_test.go b/common/namematcher/matcher_test.go
new file mode 100644
index 0000000..8d92614
--- /dev/null
+++ b/common/namematcher/matcher_test.go
@@ -0,0 +1,55 @@
+package namematcher
+
+import "testing"
+
+import . "github.com/smartystreets/goconvey/convey"
+
+func TestMatchMember(t *testing.T) {
+	testingVector := []struct {
+		matcher string
+		target  string
+		expects bool
+	}{
+		{matcher: "", target: "", expects: true},
+		{matcher: "^snowflake.torproject.net", target: "snowflake.torproject.net", expects: true},
+		{matcher: "^snowflake.torproject.net", target: "faketorproject.net", expects: false},
+		{matcher: "snowflake.torproject.net", target: "faketorproject.net", expects: false},
+		{matcher: "snowflake.torproject.net", target: "snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net", target: "imaginary-01-snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net", target: "imaginary-aaa-snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net", target: "imaginary-aaa-snowflake.faketorproject.net", expects: false},
+	}
+	for _, v := range testingVector {
+		t.Run(v.matcher+"<>"+v.target, func(t *testing.T) {
+			Convey("test", t, func() {
+				matcher := NewNameMatcher(v.matcher)
+				So(matcher.IsMember(v.target), ShouldEqual, v.expects)
+			})
+		})
+	}
+}
+
+func TestMatchSubset(t *testing.T) {
+	testingVector := []struct {
+		matcher string
+		target  string
+		expects bool
+	}{
+		{matcher: "", target: "", expects: true},
+		{matcher: "^snowflake.torproject.net", target: "^snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net", target: "^snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net", target: "snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net", target: "testing-snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net", target: "^testing-snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net", target: "", expects: false},
+	}
+	for _, v := range testingVector {
+		t.Run(v.matcher+"<>"+v.target, func(t *testing.T) {
+			Convey("test", t, func() {
+				matcher := NewNameMatcher(v.matcher)
+				target := NewNameMatcher(v.target)
+				So(matcher.IsSupersetOf(target), ShouldEqual, v.expects)
+			})
+		})
+	}
+}

From 613ceaf9709e7170cc95c065cbd3ea8a205a04b2 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 5 Apr 2022 15:03:31 +0100
Subject: [PATCH 347/385] Add RelayURL and AllowedRelayPattern to snowflake
 signaling

---
 common/messages/messages_test.go | 163 +++++++++++++++++--------------
 common/messages/proxy.go         |  64 +++++++++---
 2 files changed, 141 insertions(+), 86 deletions(-)

diff --git a/common/messages/messages_test.go b/common/messages/messages_test.go
index dd1f4fb..d1a5e96 100644
--- a/common/messages/messages_test.go
+++ b/common/messages/messages_test.go
@@ -17,90 +17,103 @@ func TestDecodeProxyPollRequest(t *testing.T) {
 			clients   int
 			data      string
 			err       error
+
+			acceptedRelayPattern string
 		}{
 			{
 				//Version 1.0 proxy message
-				"ymbcCMto7KHNGYlp",
-				"unknown",
-				"unknown",
-				0,
-				`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`,
-				nil,
+				sid:       "ymbcCMto7KHNGYlp",
+				proxyType: "unknown",
+				natType:   "unknown",
+				clients:   0,
+				data:      `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`,
+				err:       nil,
 			},
 			{
 				//Version 1.1 proxy message
-				"ymbcCMto7KHNGYlp",
-				"standalone",
-				"unknown",
-				0,
-				`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.1","Type":"standalone"}`,
-				nil,
+				sid:       "ymbcCMto7KHNGYlp",
+				proxyType: "standalone",
+				natType:   "unknown",
+				clients:   0,
+				data:      `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.1","Type":"standalone"}`,
+				err:       nil,
 			},
 			{
 				//Version 1.2 proxy message
-				"ymbcCMto7KHNGYlp",
-				"standalone",
-				"restricted",
-				0,
-				`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"standalone", "NAT":"restricted"}`,
-				nil,
+				sid:       "ymbcCMto7KHNGYlp",
+				proxyType: "standalone",
+				natType:   "restricted",
+				clients:   0,
+				data:      `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"standalone", "NAT":"restricted"}`,
+				err:       nil,
 			},
 			{
 				//Version 1.2 proxy message with clients
-				"ymbcCMto7KHNGYlp",
-				"standalone",
-				"restricted",
-				24,
-				`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"standalone", "NAT":"restricted","Clients":24}`,
-				nil,
+				sid:       "ymbcCMto7KHNGYlp",
+				proxyType: "standalone",
+				natType:   "restricted",
+				clients:   24,
+				data:      `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"standalone", "NAT":"restricted","Clients":24}`,
+				err:       nil,
+			},
+			{
+				//Version 1.3 proxy message with clients and proxyURL
+				sid:                  "ymbcCMto7KHNGYlp",
+				proxyType:            "standalone",
+				natType:              "restricted",
+				clients:              24,
+				acceptedRelayPattern: "snowfalke.torproject.org",
+				data:                 `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"standalone", "NAT":"restricted","Clients":24, "AcceptedRelayPattern":"snowfalke.torproject.org"}`,
+				err:                  nil,
 			},
 			{
 				//Version 0.X proxy message:
-				"",
-				"",
-				"",
-				0,
-				"",
-				&json.SyntaxError{},
+				sid:       "",
+				proxyType: "",
+				natType:   "",
+				clients:   0,
+				data:      "",
+				err:       &json.SyntaxError{},
 			},
 			{
-				"",
-				"",
-				"",
-				0,
-				`{"Sid":"ymbcCMto7KHNGYlp"}`,
-				fmt.Errorf(""),
+				sid:       "",
+				proxyType: "",
+				natType:   "",
+				clients:   0,
+				data:      `{"Sid":"ymbcCMto7KHNGYlp"}`,
+				err:       fmt.Errorf(""),
 			},
 			{
-				"",
-				"",
-				"",
-				0,
-				"{}",
-				fmt.Errorf(""),
+				sid:       "",
+				proxyType: "",
+				natType:   "",
+				clients:   0,
+				data:      "{}",
+				err:       fmt.Errorf(""),
 			},
 			{
-				"",
-				"",
-				"",
-				0,
-				`{"Version":"1.0"}`,
-				fmt.Errorf(""),
+				sid:       "",
+				proxyType: "",
+				natType:   "",
+				clients:   0,
+				data:      `{"Version":"1.0"}`,
+				err:       fmt.Errorf(""),
 			},
 			{
-				"",
-				"",
-				"",
-				0,
-				`{"Version":"2.0"}`,
-				fmt.Errorf(""),
+				sid:       "",
+				proxyType: "",
+				natType:   "",
+				clients:   0,
+				data:      `{"Version":"2.0"}`,
+				err:       fmt.Errorf(""),
 			},
 		} {
-			sid, proxyType, natType, clients, err := DecodeProxyPollRequest([]byte(test.data))
+			sid, proxyType, natType, clients, relayPattern, err := DecodeProxyPollRequestWithRelayPrefix([]byte(test.data))
 			So(sid, ShouldResemble, test.sid)
 			So(proxyType, ShouldResemble, test.proxyType)
 			So(natType, ShouldResemble, test.natType)
 			So(clients, ShouldEqual, test.clients)
+			So(relayPattern, ShouldResemble, test.acceptedRelayPattern)
 			So(err, ShouldHaveSameTypeAs, test.err)
 		}
 
@@ -123,34 +136,42 @@ func TestEncodeProxyPollRequests(t *testing.T) {
 func TestDecodeProxyPollResponse(t *testing.T) {
 	Convey("Context", t, func() {
 		for _, test := range []struct {
-			offer string
-			data  string
-			err   error
+			offer    string
+			data     string
+			relayURL string
+			err      error
 		}{
 			{
-				"fake offer",
-				`{"Status":"client match","Offer":"fake offer","NAT":"unknown"}`,
-				nil,
+				offer: "fake offer",
+				data:  `{"Status":"client match","Offer":"fake offer","NAT":"unknown"}`,
+				err:   nil,
 			},
 			{
-				"",
-				`{"Status":"no match"}`,
-				nil,
+				offer:    "fake offer",
+				data:     `{"Status":"client match","Offer":"fake offer","NAT":"unknown", "RelayURL":"wss://snowflake.torproject.org/proxy"}`,
+				relayURL: "wss://snowflake.torproject.org/proxy",
+				err:      nil,
 			},
 			{
-				"",
-				`{"Status":"client match"}`,
-				fmt.Errorf("no supplied offer"),
+				offer: "",
+				data:  `{"Status":"no match"}`,
+				err:   nil,
 			},
 			{
-				"",
-				`{"Test":"test"}`,
-				fmt.Errorf(""),
+				offer: "",
+				data:  `{"Status":"client match"}`,
+				err:   fmt.Errorf("no supplied offer"),
+			},
+			{
+				offer: "",
+				data:  `{"Test":"test"}`,
+				err:   fmt.Errorf(""),
 			},
 		} {
-			offer, _, err := DecodePollResponse([]byte(test.data))
+			offer, _, relayURL, err := DecodePollResponseWithRelayURL([]byte(test.data))
 			So(err, ShouldHaveSameTypeAs, test.err)
 			So(offer, ShouldResemble, test.offer)
+			So(relayURL, ShouldResemble, test.relayURL)
 		}
 
 	})
diff --git a/common/messages/proxy.go b/common/messages/proxy.go
index dcfe0ab..d18a7c3 100644
--- a/common/messages/proxy.go
+++ b/common/messages/proxy.go
@@ -5,6 +5,7 @@ package messages
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"strings"
 
@@ -23,6 +24,8 @@ var KnownProxyTypes = map[string]bool{
 	"iptproxy":   true,
 }
 
+var ErrExtraInfo = errors.New("client sent extra info")
+
 /* Version 1.2 specification:
 
 == ProxyPollRequest ==
@@ -93,22 +96,39 @@ type ProxyPollRequest struct {
 	Type    string
 	NAT     string
 	Clients int
+
+	AcceptedRelayPattern string
 }
 
 func EncodeProxyPollRequest(sid string, proxyType string, natType string, clients int) ([]byte, error) {
+	return EncodeProxyPollRequestWithRelayPrefix(sid, proxyType, natType, clients, "")
+}
+
+func EncodeProxyPollRequestWithRelayPrefix(sid string, proxyType string, natType string, clients int, relayPattern string) ([]byte, error) {
 	return json.Marshal(ProxyPollRequest{
-		Sid:     sid,
-		Version: version,
-		Type:    proxyType,
-		NAT:     natType,
-		Clients: clients,
+		Sid:                  sid,
+		Version:              version,
+		Type:                 proxyType,
+		NAT:                  natType,
+		Clients:              clients,
+		AcceptedRelayPattern: relayPattern,
 	})
 }
 
+func DecodeProxyPollRequest(data []byte) (sid string, proxyType string, natType string, clients int, err error) {
+	var relayPrefix string
+	sid, proxyType, natType, clients, relayPrefix, err = DecodeProxyPollRequestWithRelayPrefix(data)
+	if relayPrefix != "" {
+		return "", "", "", 0, ErrExtraInfo
+	}
+	return
+}
+
 // Decodes a poll message from a snowflake proxy and returns the
 // sid, proxy type, nat type and clients of the proxy on success
 // and an error if it failed
-func DecodeProxyPollRequest(data []byte) (sid string, proxyType string, natType string, clients int, err error) {
+func DecodeProxyPollRequestWithRelayPrefix(data []byte) (
+	sid string, proxyType string, natType string, clients int, relayPrefix string, err error) {
 	var message ProxyPollRequest
 
 	err = json.Unmarshal(data, &message)
@@ -145,21 +165,28 @@ func DecodeProxyPollRequest(data []byte) (sid string, proxyType string, natType
 		message.Type = ProxyUnknown
 	}
 
-	return message.Sid, message.Type, message.NAT, message.Clients, nil
+	return message.Sid, message.Type, message.NAT, message.Clients, message.AcceptedRelayPattern, nil
 }
 
 type ProxyPollResponse struct {
 	Status string
 	Offer  string
 	NAT    string
+
+	RelayURL string
 }
 
 func EncodePollResponse(offer string, success bool, natType string) ([]byte, error) {
+	return EncodePollResponseWithRelayURL(offer, success, natType, "")
+}
+
+func EncodePollResponseWithRelayURL(offer string, success bool, natType, relayURL string) ([]byte, error) {
 	if success {
 		return json.Marshal(ProxyPollResponse{
-			Status: "client match",
-			Offer:  offer,
-			NAT:    natType,
+			Status:   "client match",
+			Offer:    offer,
+			NAT:      natType,
+			RelayURL: relayURL,
 		})
 
 	}
@@ -167,23 +194,30 @@ func EncodePollResponse(offer string, success bool, natType string) ([]byte, err
 		Status: "no match",
 	})
 }
+func DecodePollResponse(data []byte) (string, string, error) {
+	offer, natType, relayURL, err := DecodePollResponseWithRelayURL(data)
+	if relayURL != "" {
+		return "", "", ErrExtraInfo
+	}
+	return offer, natType, err
+}
 
 // Decodes a poll response from the broker and returns an offer and the client's NAT type
 // If there is a client match, the returned offer string will be non-empty
-func DecodePollResponse(data []byte) (string, string, error) {
+func DecodePollResponseWithRelayURL(data []byte) (string, string, string, error) {
 	var message ProxyPollResponse
 
 	err := json.Unmarshal(data, &message)
 	if err != nil {
-		return "", "", err
+		return "", "", "", err
 	}
 	if message.Status == "" {
-		return "", "", fmt.Errorf("received invalid data")
+		return "", "", "", fmt.Errorf("received invalid data")
 	}
 
 	if message.Status == "client match" {
 		if message.Offer == "" {
-			return "", "", fmt.Errorf("no supplied offer")
+			return "", "", "", fmt.Errorf("no supplied offer")
 		}
 	} else {
 		message.Offer = ""
@@ -194,7 +228,7 @@ func DecodePollResponse(data []byte) (string, string, error) {
 		natType = "unknown"
 	}
 
-	return message.Offer, natType, nil
+	return message.Offer, natType, message.RelayURL, nil
 }
 
 type ProxyAnswerRequest struct {

From 863a8296e85ae467aa3855ab85f6f990f9cb40e5 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 7 Apr 2022 21:32:55 +0100
Subject: [PATCH 348/385] Add RelayURL support in proxy

---
 proxy/lib/proxy-go_test.go |  4 ++--
 proxy/lib/snowflake.go     | 43 +++++++++++++++++++++++++++-----------
 2 files changed, 33 insertions(+), 14 deletions(-)

diff --git a/proxy/lib/proxy-go_test.go b/proxy/lib/proxy-go_test.go
index f4cbfbf..b5ff86c 100644
--- a/proxy/lib/proxy-go_test.go
+++ b/proxy/lib/proxy-go_test.go
@@ -365,7 +365,7 @@ func TestBrokerInteractions(t *testing.T) {
 				b,
 			}
 
-			sdp := broker.pollOffer(sampleOffer, DefaultProxyType, nil)
+			sdp, _ := broker.pollOffer(sampleOffer, DefaultProxyType, "", nil)
 			expectedSDP, _ := strconv.Unquote(sampleSDP)
 			So(sdp.SDP, ShouldResemble, expectedSDP)
 		})
@@ -379,7 +379,7 @@ func TestBrokerInteractions(t *testing.T) {
 				b,
 			}
 
-			sdp := broker.pollOffer(sampleOffer, DefaultProxyType, nil)
+			sdp, _ := broker.pollOffer(sampleOffer, DefaultProxyType, "", nil)
 			So(sdp, ShouldBeNil)
 		})
 		Convey("sends answer to broker", func() {
diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index c508447..83e4cd9 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -112,6 +112,12 @@ type SnowflakeProxy struct {
 	KeepLocalAddresses bool
 	// RelayURL is the URL of the Snowflake server that all traffic will be relayed to
 	RelayURL string
+	// RelayDomainNamePattern is the pattern specify allowed domain name for relay
+	// If the pattern starts with ^ then an exact match is required.
+	// The rest of pattern is the suffix of domain name.
+	// 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.
+	RelayDomainNamePattern string
 	// NATProbeURL is the URL of the probe service we use for NAT checks
 	NATProbeURL string
 	// NATTypeMeasurementInterval is time before NAT type is retested
@@ -188,7 +194,7 @@ func (s *SignalingServer) Post(path string, payload io.Reader) ([]byte, error) {
 	return limitedRead(resp.Body, readLimit)
 }
 
-func (s *SignalingServer) pollOffer(sid string, proxyType string, shutdown chan struct{}) *webrtc.SessionDescription {
+func (s *SignalingServer) pollOffer(sid string, proxyType string, acceptedRelayPattern string, shutdown chan struct{}) (*webrtc.SessionDescription, string) {
 	brokerPath := s.url.ResolveReference(&url.URL{Path: "proxy"})
 
 	ticker := time.NewTicker(pollInterval)
@@ -198,38 +204,38 @@ func (s *SignalingServer) pollOffer(sid string, proxyType string, shutdown chan
 	for ; true; <-ticker.C {
 		select {
 		case <-shutdown:
-			return nil
+			return nil, ""
 		default:
 			numClients := int((tokens.count() / 8) * 8) // Round down to 8
 			currentNATTypeLoaded := getCurrentNATType()
 			body, err := messages.EncodeProxyPollRequest(sid, proxyType, currentNATTypeLoaded, numClients)
 			if err != nil {
 				log.Printf("Error encoding poll message: %s", err.Error())
-				return nil
+				return nil, ""
 			}
 			resp, err := s.Post(brokerPath.String(), bytes.NewBuffer(body))
 			if err != nil {
 				log.Printf("error polling broker: %s", err.Error())
 			}
 
-			offer, _, err := messages.DecodePollResponse(resp)
+			offer, _, relayURL, err := messages.DecodePollResponseWithRelayURL(resp)
 			if err != nil {
 				log.Printf("Error reading broker response: %s", err.Error())
 				log.Printf("body: %s", resp)
-				return nil
+				return nil, ""
 			}
 			if offer != "" {
 				offer, err := util.DeserializeSessionDescription(offer)
 				if err != nil {
 					log.Printf("Error processing session description: %s", err.Error())
-					return nil
+					return nil, ""
 				}
-				return offer
+				return offer, relayURL
 
 			}
 		}
 	}
-	return nil
+	return nil, ""
 }
 
 func (s *SignalingServer) sendAnswer(sid string, pc *webrtc.PeerConnection) error {
@@ -295,11 +301,14 @@ func copyLoop(c1 io.ReadWriteCloser, c2 io.ReadWriteCloser, shutdown chan struct
 // conn.RemoteAddr() inside this function, as a workaround for a hang that
 // otherwise occurs inside of conn.pc.RemoteDescription() (called by
 // RemoteAddr). https://bugs.torproject.org/18628#comment:8
-func (sf *SnowflakeProxy) datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) {
+func (sf *SnowflakeProxy) datachannelHandler(conn *webRTCConn, remoteAddr net.Addr, relayURL string) {
 	defer conn.Close()
 	defer tokens.ret()
 
-	u, err := url.Parse(sf.RelayURL)
+	if relayURL == "" {
+		relayURL = sf.RelayURL
+	}
+	u, err := url.Parse(relayURL)
 	if err != nil {
 		log.Fatalf("invalid relay url: %s", err)
 	}
@@ -326,6 +335,15 @@ func (sf *SnowflakeProxy) datachannelHandler(conn *webRTCConn, remoteAddr net.Ad
 	log.Printf("datachannelHandler ends")
 }
 
+type dataChannelHandlerWithRelayURL struct {
+	RelayURL string
+	sf       *SnowflakeProxy
+}
+
+func (d dataChannelHandlerWithRelayURL) datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) {
+	d.sf.datachannelHandler(conn, remoteAddr, d.RelayURL)
+}
+
 // Create a PeerConnection from an SDP offer. Blocks until the gathering of ICE
 // candidates is complete and the answer is available in LocalDescription.
 // Installs an OnDataChannel callback that creates a webRTCConn and passes it to
@@ -470,14 +488,15 @@ func (sf *SnowflakeProxy) makeNewPeerConnection(config webrtc.Configuration,
 }
 
 func (sf *SnowflakeProxy) runSession(sid string) {
-	offer := broker.pollOffer(sid, sf.ProxyType, sf.shutdown)
+	offer, relayURL := broker.pollOffer(sid, sf.ProxyType, sf.RelayDomainNamePattern, sf.shutdown)
 	if offer == nil {
 		log.Printf("bad offer from broker")
 		tokens.ret()
 		return
 	}
 	dataChan := make(chan struct{})
-	pc, err := sf.makePeerConnectionFromOffer(offer, config, dataChan, sf.datachannelHandler)
+	dataChannelAdaptor := dataChannelHandlerWithRelayURL{RelayURL: relayURL, sf: sf}
+	pc, err := sf.makePeerConnectionFromOffer(offer, config, dataChan, dataChannelAdaptor.datachannelHandler)
 	if err != nil {
 		log.Printf("error making WebRTC connection: %s", err)
 		tokens.ret()

From d5a87c3c02ea673d397e3cb8f945f2f0f0e05a76 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 8 Apr 2022 15:14:38 +0100
Subject: [PATCH 349/385] Guard Proxy Relay URL Acceptance with Pattern Check

---
 proxy/lib/snowflake.go | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 83e4cd9..b2a2be1 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -30,6 +30,7 @@ import (
 	"crypto/rand"
 	"encoding/base64"
 	"fmt"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/namematcher"
 	"io"
 	"io/ioutil"
 	"log"
@@ -494,6 +495,12 @@ func (sf *SnowflakeProxy) runSession(sid string) {
 		tokens.ret()
 		return
 	}
+	matcher := namematcher.NewNameMatcher(sf.RelayDomainNamePattern)
+	if relayURL != "" && !matcher.IsMember(relayURL) {
+		log.Printf("bad offer from broker: rejected Relay URL")
+		tokens.ret()
+		return
+	}
 	dataChan := make(chan struct{})
 	dataChannelAdaptor := dataChannelHandlerWithRelayURL{RelayURL: relayURL, sf: sf}
 	pc, err := sf.makePeerConnectionFromOffer(offer, config, dataChan, dataChannelAdaptor.datachannelHandler)

From 5d7a3766d6f8af0a3adc24b19aaa30747b49c847 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 11 Apr 2022 14:24:49 +0100
Subject: [PATCH 350/385] Add Relay Info Forwarding for Snowflake

---
 broker/broker.go | 15 +++++++++++++++
 broker/ipc.go    | 12 +++++++++++-
 2 files changed, 26 insertions(+), 1 deletion(-)

diff --git a/broker/broker.go b/broker/broker.go
index 10129d7..692cea4 100644
--- a/broker/broker.go
+++ b/broker/broker.go
@@ -36,6 +36,13 @@ type BrokerContext struct {
 	snowflakeLock sync.Mutex
 	proxyPolls    chan *ProxyPoll
 	metrics       *Metrics
+
+	bridgeList          BridgeListHolderFileBased
+	allowedRelayPattern string
+}
+
+func (ctx *BrokerContext) GetBridgeInfo(fingerprint [20]byte) (BridgeInfo, error) {
+	return ctx.bridgeList.GetBridgeInfo(fingerprint)
 }
 
 func NewBrokerContext(metricsLogger *log.Logger) *BrokerContext {
@@ -139,6 +146,14 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri
 	return snowflake
 }
 
+func (ctx *BrokerContext) InstallBridgeListProfile(reader io.Reader, relayPattern string) error {
+	if err := ctx.bridgeList.LoadBridgeInfo(reader); err != nil {
+		return err
+	}
+	ctx.allowedRelayPattern = relayPattern
+	return nil
+}
+
 // Client offer contains an SDP, bridge fingerprint and the NAT type of the client
 type ClientOffer struct {
 	natType     string
diff --git a/broker/ipc.go b/broker/ipc.go
index e11a33c..e559c2a 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -102,7 +102,13 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
 	}
 
 	i.ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "matched"}).Inc()
-	b, err = messages.EncodePollResponse(string(offer.sdp), true, offer.natType)
+	var relayURL string
+	if info, err := i.ctx.bridgeList.GetBridgeInfo(offer.fingerprint); err != nil {
+		return err
+	} else {
+		relayURL = info.WebSocketAddress
+	}
+	b, err = messages.EncodePollResponseWithRelayURL(string(offer.sdp), true, offer.natType, relayURL)
 	if err != nil {
 		return messages.ErrInternal
 	}
@@ -141,6 +147,10 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 	}
 	copy(offer.fingerprint[:], fingerprint)
 
+	if _, err := i.ctx.GetBridgeInfo(offer.fingerprint); err != nil {
+		return err
+	}
+
 	// Only hand out known restricted snowflakes to unrestricted clients
 	var snowflakeHeap *SnowflakeHeap
 	if offer.natType == NATUnrestricted {

From c7549d886eb84ef0fb31bbdced6de3bb00818a4e Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 11 Apr 2022 16:29:08 +0100
Subject: [PATCH 351/385] Update default snowflake server address

Change snowflake broker test for updated address

Amend DefaultBridges Value

Add Default Fingerprint Info for Snowflake
---
 broker/broker.go                |  8 ++++++++
 broker/ipc.go                   |  3 ++-
 broker/snowflake-broker_test.go | 13 +++++++++----
 3 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/broker/broker.go b/broker/broker.go
index 692cea4..d9e8dea 100644
--- a/broker/broker.go
+++ b/broker/broker.go
@@ -6,6 +6,7 @@ SessionDescriptions in order to negotiate a WebRTC connection.
 package main
 
 import (
+	"bytes"
 	"container/heap"
 	"crypto/tls"
 	"flag"
@@ -60,12 +61,19 @@ func NewBrokerContext(metricsLogger *log.Logger) *BrokerContext {
 		panic("Failed to create metrics")
 	}
 
+	bridgeListHolder := NewBridgeListHolder()
+
+	const DefaultBridges = `{"displayName":"default", "webSocketAddress":"wss://snowflake.torproject.net/", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80A72"}
+`
+	bridgeListHolder.LoadBridgeInfo(bytes.NewReader([]byte(DefaultBridges)))
+
 	return &BrokerContext{
 		snowflakes:           snowflakes,
 		restrictedSnowflakes: rSnowflakes,
 		idToSnowflake:        make(map[string]*Snowflake),
 		proxyPolls:           make(chan *ProxyPoll),
 		metrics:              metrics,
+		bridgeList:           bridgeListHolder,
 	}
 }
 
diff --git a/broker/ipc.go b/broker/ipc.go
index e559c2a..780a9a5 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -66,7 +66,8 @@ func (i *IPC) Debug(_ interface{}, response *string) error {
 }
 
 func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
-	sid, proxyType, natType, clients, err := messages.DecodeProxyPollRequest(arg.Body)
+	sid, proxyType, natType, clients, relayPattern, err := messages.DecodeProxyPollRequestWithRelayPrefix(arg.Body)
+	_ = relayPattern
 	if err != nil {
 		return messages.ErrBadRequest
 	}
diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go
index f7850f8..fdd1114 100644
--- a/broker/snowflake-broker_test.go
+++ b/broker/snowflake-broker_test.go
@@ -3,6 +3,7 @@ package main
 import (
 	"bytes"
 	"container/heap"
+	"encoding/hex"
 	"io"
 	"io/ioutil"
 	"log"
@@ -36,6 +37,10 @@ func decodeAMPArmorToString(r io.Reader) (string, error) {
 
 func TestBroker(t *testing.T) {
 
+	defaultBridgeValue, _ := hex.DecodeString("2B280B23E1107BB62ABFC40DDCC8824814F80A72")
+	var defaultBridge [20]byte
+	copy(defaultBridge[:], defaultBridgeValue)
+
 	Convey("Context", t, func() {
 		ctx := NewBrokerContext(NullLogger())
 		i := &IPC{ctx}
@@ -253,10 +258,10 @@ func TestBroker(t *testing.T) {
 				// Pass a fake client offer to this proxy
 				p := <-ctx.proxyPolls
 				So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp")
-				p.offerChannel <- &ClientOffer{sdp: []byte("fake offer")}
+				p.offerChannel <- &ClientOffer{sdp: []byte("fake offer"), fingerprint: defaultBridge}
 				<-done
 				So(w.Code, ShouldEqual, http.StatusOK)
-				So(w.Body.String(), ShouldEqual, `{"Status":"client match","Offer":"fake offer","NAT":""}`)
+				So(w.Body.String(), ShouldEqual, `{"Status":"client match","Offer":"fake offer","NAT":"","RelayURL":"wss://snowflake.torproject.net/"}`)
 			})
 
 			Convey("return empty 200 OK when no client offer is available.", func() {
@@ -269,7 +274,7 @@ func TestBroker(t *testing.T) {
 				// nil means timeout
 				p.offerChannel <- nil
 				<-done
-				So(w.Body.String(), ShouldEqual, `{"Status":"no match","Offer":"","NAT":""}`)
+				So(w.Body.String(), ShouldEqual, `{"Status":"no match","Offer":"","NAT":"","RelayURL":""}`)
 				So(w.Code, ShouldEqual, http.StatusOK)
 			})
 		})
@@ -412,7 +417,7 @@ func TestBroker(t *testing.T) {
 
 			<-polled
 			So(wP.Code, ShouldEqual, http.StatusOK)
-			So(wP.Body.String(), ShouldResemble, `{"Status":"client match","Offer":"fake","NAT":"unknown"}`)
+			So(wP.Body.String(), ShouldResemble, `{"Status":"client match","Offer":"fake","NAT":"unknown","RelayURL":"wss://snowflake.torproject.net/"}`)
 			So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil)
 			// Follow up with the answer request afterwards
 			wA := httptest.NewRecorder()

From 50c0d64e108a56dc42623f2c430cda790ed887c6 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 11 Apr 2022 16:30:45 +0100
Subject: [PATCH 352/385] Add Detailed Error Output for proxyPolls,
 proxyAnswers

---
 broker/http.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/broker/http.go b/broker/http.go
index 3b0ba1f..2f81f1d 100644
--- a/broker/http.go
+++ b/broker/http.go
@@ -94,7 +94,7 @@ For snowflake proxies to request a client from the Broker.
 func proxyPolls(i *IPC, w http.ResponseWriter, r *http.Request) {
 	body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit))
 	if err != nil {
-		log.Println("Invalid data.")
+		log.Println("Invalid data.", err.Error())
 		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
@@ -204,7 +204,7 @@ which the broker will pass back to the original client.
 func proxyAnswers(i *IPC, w http.ResponseWriter, r *http.Request) {
 	body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit))
 	if err != nil {
-		log.Println("Invalid data.")
+		log.Println("Invalid data.", err.Error())
 		w.WriteHeader(http.StatusBadRequest)
 		return
 	}

From c961b07459cd6fcc9ba40631e41a2108baf554d7 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 13 Apr 2022 13:42:06 +0100
Subject: [PATCH 353/385] Add Detailed Error Output for datachannelHandler

---
 proxy/lib/snowflake.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index b2a2be1..7e06c0f 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -326,7 +326,7 @@ func (sf *SnowflakeProxy) datachannelHandler(conn *webRTCConn, remoteAddr net.Ad
 
 	ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
 	if err != nil {
-		log.Printf("error dialing relay: %s", err)
+		log.Printf("error dialing relay: %s = %s", u.String(), err)
 		return
 	}
 	wsConn := websocketconn.New(ws)

From 02c6f764c9f94cbbc7ef482d6db43fcb0e794996 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 13 Apr 2022 14:19:27 +0100
Subject: [PATCH 354/385] Add support for specifying bridge list file

---
 broker/broker.go | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/broker/broker.go b/broker/broker.go
index d9e8dea..476bc81 100644
--- a/broker/broker.go
+++ b/broker/broker.go
@@ -176,6 +176,7 @@ func main() {
 	var addr string
 	var geoipDatabase string
 	var geoip6Database string
+	var bridgeListFilePath, allowedRelayPattern string
 	var disableTLS bool
 	var certFilename, keyFilename string
 	var disableGeoip bool
@@ -190,6 +191,8 @@ func main() {
 	flag.StringVar(&addr, "addr", ":443", "address to listen on")
 	flag.StringVar(&geoipDatabase, "geoipdb", "/usr/share/tor/geoip", "path to correctly formatted geoip database mapping IPv4 address ranges to country codes")
 	flag.StringVar(&geoip6Database, "geoip6db", "/usr/share/tor/geoip6", "path to correctly formatted geoip database mapping IPv6 address ranges to country codes")
+	flag.StringVar(&bridgeListFilePath, "bridge-list-path", "", "file path for bridgeListFile")
+	flag.StringVar(&allowedRelayPattern, "allowed-relay-pattern", "", "allowed pattern for relay host name")
 	flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS")
 	flag.BoolVar(&disableGeoip, "disable-geoip", false, "don't use geoip for stats collection")
 	flag.StringVar(&metricsFilename, "metrics-log", "", "path to metrics logging output")
@@ -222,6 +225,17 @@ func main() {
 
 	ctx := NewBrokerContext(metricsLogger)
 
+	if bridgeListFilePath != "" {
+		bridgeListFile, err := os.Open(bridgeListFilePath)
+		if err != nil {
+			log.Fatal(err.Error())
+		}
+		err = ctx.InstallBridgeListProfile(bridgeListFile, allowedRelayPattern)
+		if err != nil {
+			log.Fatal(err.Error())
+		}
+	}
+
 	if !disableGeoip {
 		err = ctx.metrics.LoadGeoipDatabases(geoipDatabase, geoip6Database)
 		if err != nil {

From b09a2e09b3e8abadac5f5b96662864eec4ebd597 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 13 Apr 2022 16:20:52 +0100
Subject: [PATCH 355/385] Add Relay URL Check in Snowflake Proxy

---
 proxy/lib/snowflake.go | 9 ++++++++-
 proxy/main.go          | 5 +++++
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 7e06c0f..7dbc976 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -119,6 +119,7 @@ type SnowflakeProxy struct {
 	// 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.
 	RelayDomainNamePattern string
+	AllowNonTLSRelay       bool
 	// NATProbeURL is the URL of the probe service we use for NAT checks
 	NATProbeURL string
 	// NATTypeMeasurementInterval is time before NAT type is retested
@@ -496,7 +497,13 @@ func (sf *SnowflakeProxy) runSession(sid string) {
 		return
 	}
 	matcher := namematcher.NewNameMatcher(sf.RelayDomainNamePattern)
-	if relayURL != "" && !matcher.IsMember(relayURL) {
+	parsedRelayURL, err := url.Parse(relayURL)
+	if err != nil {
+		log.Printf("bad offer from broker: bad Relay URL %v", err.Error())
+		tokens.ret()
+		return
+	}
+	if relayURL != "" && (!matcher.IsMember(parsedRelayURL.Hostname()) || (!sf.AllowNonTLSRelay && parsedRelayURL.Scheme != "wss")) {
 		log.Printf("bad offer from broker: rejected Relay URL")
 		tokens.ret()
 		return
diff --git a/proxy/main.go b/proxy/main.go
index 7d025ea..305d0b0 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -21,6 +21,8 @@ func main() {
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	relayURL := flag.String("relay", sf.DefaultRelayURL, "websocket relay URL")
+	allowedRelayHostNamePattern := flag.String("allowed-relay-hostname-pattern", "", "a pattern to specify allowed hostname pattern for relay URL.")
+	allowNonTLSRelay := flag.Bool("allow-non-tls-relay", false, "allow relay without tls encryption")
 	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\". ")
 	SummaryInterval := flag.Duration("summary-interval", time.Hour,
@@ -40,6 +42,9 @@ func main() {
 
 		NATTypeMeasurementInterval: *NATTypeMeasurementInterval,
 		EventDispatcher:            eventLogger,
+
+		RelayDomainNamePattern: *allowedRelayHostNamePattern,
+		AllowNonTLSRelay:       *allowNonTLSRelay,
 	}
 
 	var logOutput io.Writer = os.Stderr

From 2ebdc89c42dfb1331dd172282b4c2192bfbb4acc Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 13 Apr 2022 17:51:17 +0100
Subject: [PATCH 356/385] Add Allowed Relay Hostname Pattern Indication

---
 broker/ipc.go                    |  3 ++-
 common/messages/messages_test.go |  2 +-
 common/messages/proxy.go         | 16 ++++++++++------
 proxy/lib/snowflake.go           |  2 +-
 4 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/broker/ipc.go b/broker/ipc.go
index 780a9a5..fbaed48 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -66,8 +66,9 @@ func (i *IPC) Debug(_ interface{}, response *string) error {
 }
 
 func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
-	sid, proxyType, natType, clients, relayPattern, err := messages.DecodeProxyPollRequestWithRelayPrefix(arg.Body)
+	sid, proxyType, natType, clients, relayPattern, relayPatternSupported, err := messages.DecodeProxyPollRequestWithRelayPrefix(arg.Body)
 	_ = relayPattern
+	_ = relayPatternSupported
 	if err != nil {
 		return messages.ErrBadRequest
 	}
diff --git a/common/messages/messages_test.go b/common/messages/messages_test.go
index d1a5e96..017e959 100644
--- a/common/messages/messages_test.go
+++ b/common/messages/messages_test.go
@@ -108,7 +108,7 @@ func TestDecodeProxyPollRequest(t *testing.T) {
 				err:       fmt.Errorf(""),
 			},
 		} {
-			sid, proxyType, natType, clients, relayPattern, err := DecodeProxyPollRequestWithRelayPrefix([]byte(test.data))
+			sid, proxyType, natType, clients, relayPattern, _, err := DecodeProxyPollRequestWithRelayPrefix([]byte(test.data))
 			So(sid, ShouldResemble, test.sid)
 			So(proxyType, ShouldResemble, test.proxyType)
 			So(natType, ShouldResemble, test.natType)
diff --git a/common/messages/proxy.go b/common/messages/proxy.go
index d18a7c3..19cf6a3 100644
--- a/common/messages/proxy.go
+++ b/common/messages/proxy.go
@@ -97,7 +97,7 @@ type ProxyPollRequest struct {
 	NAT     string
 	Clients int
 
-	AcceptedRelayPattern string
+	AcceptedRelayPattern *string
 }
 
 func EncodeProxyPollRequest(sid string, proxyType string, natType string, clients int) ([]byte, error) {
@@ -111,13 +111,13 @@ func EncodeProxyPollRequestWithRelayPrefix(sid string, proxyType string, natType
 		Type:                 proxyType,
 		NAT:                  natType,
 		Clients:              clients,
-		AcceptedRelayPattern: relayPattern,
+		AcceptedRelayPattern: &relayPattern,
 	})
 }
 
 func DecodeProxyPollRequest(data []byte) (sid string, proxyType string, natType string, clients int, err error) {
 	var relayPrefix string
-	sid, proxyType, natType, clients, relayPrefix, err = DecodeProxyPollRequestWithRelayPrefix(data)
+	sid, proxyType, natType, clients, relayPrefix, _, err = DecodeProxyPollRequestWithRelayPrefix(data)
 	if relayPrefix != "" {
 		return "", "", "", 0, ErrExtraInfo
 	}
@@ -128,7 +128,7 @@ func DecodeProxyPollRequest(data []byte) (sid string, proxyType string, natType
 // sid, proxy type, nat type and clients of the proxy on success
 // and an error if it failed
 func DecodeProxyPollRequestWithRelayPrefix(data []byte) (
-	sid string, proxyType string, natType string, clients int, relayPrefix string, err error) {
+	sid string, proxyType string, natType string, clients int, relayPrefix string, relayPrefixAware bool, err error) {
 	var message ProxyPollRequest
 
 	err = json.Unmarshal(data, &message)
@@ -164,8 +164,12 @@ func DecodeProxyPollRequestWithRelayPrefix(data []byte) (
 	if !KnownProxyTypes[message.Type] {
 		message.Type = ProxyUnknown
 	}
-
-	return message.Sid, message.Type, message.NAT, message.Clients, message.AcceptedRelayPattern, nil
+	var acceptedRelayPattern = ""
+	if message.AcceptedRelayPattern != nil {
+		acceptedRelayPattern = *message.AcceptedRelayPattern
+	}
+	return message.Sid, message.Type, message.NAT, message.Clients,
+		acceptedRelayPattern, message.AcceptedRelayPattern != nil, nil
 }
 
 type ProxyPollResponse struct {
diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 7dbc976..a60b5ab 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -210,7 +210,7 @@ func (s *SignalingServer) pollOffer(sid string, proxyType string, acceptedRelayP
 		default:
 			numClients := int((tokens.count() / 8) * 8) // Round down to 8
 			currentNATTypeLoaded := getCurrentNATType()
-			body, err := messages.EncodeProxyPollRequest(sid, proxyType, currentNATTypeLoaded, numClients)
+			body, err := messages.EncodeProxyPollRequestWithRelayPrefix(sid, proxyType, currentNATTypeLoaded, numClients, acceptedRelayPattern)
 			if err != nil {
 				log.Printf("Error encoding poll message: %s", err.Error())
 				return nil, ""

From b18a9431b26af8c4e3c908f6e34c2340fc4911bc Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 14 Apr 2022 11:15:35 +0100
Subject: [PATCH 357/385] Add Broker Allowed Relay Pattern Indication Rejection
 for Proxy

---
 broker/broker.go | 23 ++++++++++++++++++-----
 broker/ipc.go    |  6 ++++--
 2 files changed, 22 insertions(+), 7 deletions(-)

diff --git a/broker/broker.go b/broker/broker.go
index 476bc81..8ca0120 100644
--- a/broker/broker.go
+++ b/broker/broker.go
@@ -20,6 +20,7 @@ import (
 	"syscall"
 	"time"
 
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/namematcher"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -38,8 +39,9 @@ type BrokerContext struct {
 	proxyPolls    chan *ProxyPoll
 	metrics       *Metrics
 
-	bridgeList          BridgeListHolderFileBased
-	allowedRelayPattern string
+	bridgeList                     BridgeListHolderFileBased
+	allowedRelayPattern            string
+	presumedPatternForLegacyClient string
 }
 
 func (ctx *BrokerContext) GetBridgeInfo(fingerprint [20]byte) (BridgeInfo, error) {
@@ -154,14 +156,24 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri
 	return snowflake
 }
 
-func (ctx *BrokerContext) InstallBridgeListProfile(reader io.Reader, relayPattern string) error {
+func (ctx *BrokerContext) InstallBridgeListProfile(reader io.Reader, relayPattern, presumedPatternForLegacyClient string) error {
 	if err := ctx.bridgeList.LoadBridgeInfo(reader); err != nil {
 		return err
 	}
 	ctx.allowedRelayPattern = relayPattern
+	ctx.presumedPatternForLegacyClient = presumedPatternForLegacyClient
 	return nil
 }
 
+func (ctx *BrokerContext) CheckProxyRelayPattern(pattern string, nonSupported bool) bool {
+	if nonSupported {
+		pattern = ctx.presumedPatternForLegacyClient
+	}
+	proxyPattern := namematcher.NewNameMatcher(pattern)
+	brokerPattern := namematcher.NewNameMatcher(ctx.allowedRelayPattern)
+	return proxyPattern.IsSupersetOf(brokerPattern)
+}
+
 // Client offer contains an SDP, bridge fingerprint and the NAT type of the client
 type ClientOffer struct {
 	natType     string
@@ -176,7 +188,7 @@ func main() {
 	var addr string
 	var geoipDatabase string
 	var geoip6Database string
-	var bridgeListFilePath, allowedRelayPattern string
+	var bridgeListFilePath, allowedRelayPattern, presumedPatternForLegacyClient string
 	var disableTLS bool
 	var certFilename, keyFilename string
 	var disableGeoip bool
@@ -193,6 +205,7 @@ func main() {
 	flag.StringVar(&geoip6Database, "geoip6db", "/usr/share/tor/geoip6", "path to correctly formatted geoip database mapping IPv6 address ranges to country codes")
 	flag.StringVar(&bridgeListFilePath, "bridge-list-path", "", "file path for bridgeListFile")
 	flag.StringVar(&allowedRelayPattern, "allowed-relay-pattern", "", "allowed pattern for relay host name")
+	flag.StringVar(&presumedPatternForLegacyClient, "default-relay-pattern", "", "presumed pattern for legacy client")
 	flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS")
 	flag.BoolVar(&disableGeoip, "disable-geoip", false, "don't use geoip for stats collection")
 	flag.StringVar(&metricsFilename, "metrics-log", "", "path to metrics logging output")
@@ -230,7 +243,7 @@ func main() {
 		if err != nil {
 			log.Fatal(err.Error())
 		}
-		err = ctx.InstallBridgeListProfile(bridgeListFile, allowedRelayPattern)
+		err = ctx.InstallBridgeListProfile(bridgeListFile, allowedRelayPattern, presumedPatternForLegacyClient)
 		if err != nil {
 			log.Fatal(err.Error())
 		}
diff --git a/broker/ipc.go b/broker/ipc.go
index fbaed48..97f26ef 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -67,12 +67,14 @@ func (i *IPC) Debug(_ interface{}, response *string) error {
 
 func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
 	sid, proxyType, natType, clients, relayPattern, relayPatternSupported, err := messages.DecodeProxyPollRequestWithRelayPrefix(arg.Body)
-	_ = relayPattern
-	_ = relayPatternSupported
 	if err != nil {
 		return messages.ErrBadRequest
 	}
 
+	if !i.ctx.CheckProxyRelayPattern(relayPattern, !relayPatternSupported) {
+		return fmt.Errorf("bad request: rejected relay pattern from proxy = %v", messages.ErrBadRequest)
+	}
+
 	// Log geoip stats
 	remoteIP, _, err := net.SplitHostPort(arg.RemoteAddr)
 	if err != nil {

From 3ebb5a4186581784a2d2ada6a5ce1c703030c3bb Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 21 Apr 2022 12:00:15 +0100
Subject: [PATCH 358/385] Show relay URL when connecting to relay

---
 proxy/lib/snowflake.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index a60b5ab..2770aa4 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -331,7 +331,7 @@ func (sf *SnowflakeProxy) datachannelHandler(conn *webRTCConn, remoteAddr net.Ad
 		return
 	}
 	wsConn := websocketconn.New(ws)
-	log.Printf("connected to relay")
+	log.Printf("connected to relay: %v", relayURL)
 	defer wsConn.Close()
 	copyLoop(conn, wsConn, sf.shutdown)
 	log.Printf("datachannelHandler ends")

From 6e8fbe54eeebc0ffbf84d4dd82e3e9a87d7729c4 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 29 Apr 2022 17:12:59 +0100
Subject: [PATCH 359/385] Rejection reason feedback

---
 broker/ipc.go            | 10 ++++++++--
 common/messages/proxy.go | 12 ++++++++----
 2 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/broker/ipc.go b/broker/ipc.go
index 97f26ef..5a93585 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -72,7 +72,13 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
 	}
 
 	if !i.ctx.CheckProxyRelayPattern(relayPattern, !relayPatternSupported) {
-		return fmt.Errorf("bad request: rejected relay pattern from proxy = %v", messages.ErrBadRequest)
+		log.Printf("bad request: rejected relay pattern from proxy = %v", messages.ErrBadRequest)
+		b, err := messages.EncodePollResponseWithRelayURL("", false, "", "", "incorrect relay pattern")
+		*response = b
+		if err != nil {
+			return messages.ErrInternal
+		}
+		return nil
 	}
 
 	// Log geoip stats
@@ -112,7 +118,7 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
 	} else {
 		relayURL = info.WebSocketAddress
 	}
-	b, err = messages.EncodePollResponseWithRelayURL(string(offer.sdp), true, offer.natType, relayURL)
+	b, err = messages.EncodePollResponseWithRelayURL(string(offer.sdp), true, offer.natType, relayURL, "")
 	if err != nil {
 		return messages.ErrInternal
 	}
diff --git a/common/messages/proxy.go b/common/messages/proxy.go
index 19cf6a3..6ea2c8a 100644
--- a/common/messages/proxy.go
+++ b/common/messages/proxy.go
@@ -181,10 +181,10 @@ type ProxyPollResponse struct {
 }
 
 func EncodePollResponse(offer string, success bool, natType string) ([]byte, error) {
-	return EncodePollResponseWithRelayURL(offer, success, natType, "")
+	return EncodePollResponseWithRelayURL(offer, success, natType, "", "no match")
 }
 
-func EncodePollResponseWithRelayURL(offer string, success bool, natType, relayURL string) ([]byte, error) {
+func EncodePollResponseWithRelayURL(offer string, success bool, natType, relayURL, failReason string) ([]byte, error) {
 	if success {
 		return json.Marshal(ProxyPollResponse{
 			Status:   "client match",
@@ -195,7 +195,7 @@ func EncodePollResponseWithRelayURL(offer string, success bool, natType, relayUR
 
 	}
 	return json.Marshal(ProxyPollResponse{
-		Status: "no match",
+		Status: failReason,
 	})
 }
 func DecodePollResponse(data []byte) (string, string, error) {
@@ -219,12 +219,16 @@ func DecodePollResponseWithRelayURL(data []byte) (string, string, string, error)
 		return "", "", "", fmt.Errorf("received invalid data")
 	}
 
+	err = nil
 	if message.Status == "client match" {
 		if message.Offer == "" {
 			return "", "", "", fmt.Errorf("no supplied offer")
 		}
 	} else {
 		message.Offer = ""
+		if message.Status != "no match" {
+			err = errors.New(message.Status)
+		}
 	}
 
 	natType := message.NAT
@@ -232,7 +236,7 @@ func DecodePollResponseWithRelayURL(data []byte) (string, string, string, error)
 		natType = "unknown"
 	}
 
-	return message.Offer, natType, message.RelayURL, nil
+	return message.Offer, natType, message.RelayURL, err
 }
 
 type ProxyAnswerRequest struct {

From 1b48ee14f47f6b5ac4b061c2bd50aaf58b2fff4f Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 2 May 2022 13:24:39 +0100
Subject: [PATCH 360/385] Add test for proxy poll with Relay URL

---
 common/messages/messages_test.go | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/common/messages/messages_test.go b/common/messages/messages_test.go
index 017e959..2fc76df 100644
--- a/common/messages/messages_test.go
+++ b/common/messages/messages_test.go
@@ -194,6 +194,34 @@ func TestEncodeProxyPollResponse(t *testing.T) {
 		So(err, ShouldEqual, nil)
 	})
 }
+
+func TestEncodeProxyPollResponseWithProxyURL(t *testing.T) {
+	Convey("Context", t, func() {
+		b, err := EncodePollResponseWithRelayURL("fake offer", true, "restricted", "wss://test/", "")
+		So(err, ShouldBeNil)
+		offer, natType, err := DecodePollResponse(b)
+		So(err, ShouldNotBeNil)
+
+		offer, natType, relay, err := DecodePollResponseWithRelayURL(b)
+		So(offer, ShouldEqual, "fake offer")
+		So(natType, ShouldEqual, "restricted")
+		So(relay, ShouldEqual, "wss://test/")
+		So(err, ShouldBeNil)
+
+		b, err = EncodePollResponse("", false, "unknown")
+		So(err, ShouldBeNil)
+		offer, natType, relay, err = DecodePollResponseWithRelayURL(b)
+		So(offer, ShouldEqual, "")
+		So(natType, ShouldEqual, "unknown")
+		So(err, ShouldBeNil)
+
+		b, err = EncodePollResponseWithRelayURL("fake offer", false, "restricted", "wss://test/", "test error reason")
+		So(err, ShouldBeNil)
+		offer, natType, relay, err = DecodePollResponseWithRelayURL(b)
+		So(err, ShouldNotBeNil)
+		So(err.Error(), ShouldContainSubstring, "test error reason")
+	})
+}
 func TestDecodeProxyAnswerRequest(t *testing.T) {
 	Convey("Context", t, func() {
 		for _, test := range []struct {

From b391d986799459a843e1596c0ffe60c46e2e4d25 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 2 May 2022 14:03:32 +0100
Subject: [PATCH 361/385] Add Proxy Relay URL Support Counting Metrics Output

---
 broker/metrics.go               | 28 ++++++++++++++++++++++++++++
 broker/snowflake-broker_test.go |  4 ++--
 2 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/broker/metrics.go b/broker/metrics.go
index c642045..5d95cb4 100644
--- a/broker/metrics.go
+++ b/broker/metrics.go
@@ -49,6 +49,9 @@ type Metrics struct {
 	clientUnrestrictedDeniedCount uint
 	clientProxyMatchCount         uint
 
+	proxyPollWithRelayURLExtension    uint
+	proxyPollWithoutRelayURLExtension uint
+
 	// synchronization for access to snowflake metrics
 	lock sync.Mutex
 
@@ -189,6 +192,8 @@ func (m *Metrics) printMetrics() {
 	}
 	m.logger.Println("snowflake-ips-total", total)
 	m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount))
+	m.logger.Println("snowflake-proxy-poll-with-relay-url-count", binCount(m.proxyPollWithRelayURLExtension))
+	m.logger.Println("snowflake-proxy-poll-without-relay-url-count", binCount(m.proxyPollWithoutRelayURLExtension))
 	m.logger.Println("client-denied-count", binCount(m.clientDeniedCount))
 	m.logger.Println("client-restricted-denied-count", binCount(m.clientRestrictedDeniedCount))
 	m.logger.Println("client-unrestricted-denied-count", binCount(m.clientUnrestrictedDeniedCount))
@@ -227,6 +232,9 @@ type PromMetrics struct {
 	ProxyPollTotal   *RoundedCounterVec
 	ClientPollTotal  *RoundedCounterVec
 	AvailableProxies *prometheus.GaugeVec
+
+	ProxyPollWithRelayURLExtensionTotal    *RoundedCounterVec
+	ProxyPollWithoutRelayURLExtensionTotal *RoundedCounterVec
 }
 
 // Initialize metrics for prometheus exporter
@@ -262,6 +270,24 @@ func initPrometheus() *PromMetrics {
 		[]string{"nat", "status"},
 	)
 
+	promMetrics.ProxyPollWithRelayURLExtensionTotal = NewRoundedCounterVec(
+		prometheus.CounterOpts{
+			Namespace: prometheusNamespace,
+			Name:      "rounded_proxy_poll_with_relay_url_extension_total",
+			Help:      "The number of snowflake proxy polls with Relay URL Extension, rounded up to a multiple of 8",
+		},
+		[]string{"nat", "status"},
+	)
+
+	promMetrics.ProxyPollWithoutRelayURLExtensionTotal = NewRoundedCounterVec(
+		prometheus.CounterOpts{
+			Namespace: prometheusNamespace,
+			Name:      "rounded_proxy_poll_without_relay_url_extension_total",
+			Help:      "The number of snowflake proxy polls without Relay URL Extension, rounded up to a multiple of 8",
+		},
+		[]string{"nat", "status"},
+	)
+
 	promMetrics.ClientPollTotal = NewRoundedCounterVec(
 		prometheus.CounterOpts{
 			Namespace: prometheusNamespace,
@@ -275,6 +301,8 @@ func initPrometheus() *PromMetrics {
 	promMetrics.registry.MustRegister(
 		promMetrics.ClientPollTotal, promMetrics.ProxyPollTotal,
 		promMetrics.ProxyTotal, promMetrics.AvailableProxies,
+		promMetrics.ProxyPollWithRelayURLExtensionTotal,
+		promMetrics.ProxyPollWithoutRelayURLExtensionTotal,
 	)
 
 	return promMetrics
diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go
index fdd1114..6a3ba62 100644
--- a/broker/snowflake-broker_test.go
+++ b/broker/snowflake-broker_test.go
@@ -560,7 +560,7 @@ func TestMetrics(t *testing.T) {
 			So(metricsStr, ShouldContainSubstring, "\nsnowflake-ips-standalone 1\n")
 			So(metricsStr, ShouldContainSubstring, "\nsnowflake-ips-badge 1\n")
 			So(metricsStr, ShouldContainSubstring, "\nsnowflake-ips-webext 1\n")
-			So(metricsStr, ShouldEndWith, "\nsnowflake-ips-total 4\nsnowflake-idle-count 8\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 1\n")
+			So(metricsStr, ShouldEndWith, "\nsnowflake-ips-total 4\nsnowflake-idle-count 8\nsnowflake-proxy-poll-with-relay-url-count 0\nsnowflake-proxy-poll-without-relay-url-count 8\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 1\n")
 		})
 
 		//Test addition of client failures
@@ -584,7 +584,7 @@ func TestMetrics(t *testing.T) {
 			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-standalone 0\n")
 			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-badge 0\n")
 			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-webext 0\n")
-			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-total 0\nsnowflake-idle-count 0\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 0\n")
+			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-total 0\nsnowflake-idle-count 0\nsnowflake-proxy-poll-with-relay-url-count 0\nsnowflake-proxy-poll-without-relay-url-count 0\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 0\n")
 		})
 		//Test addition of client matches
 		Convey("for client-proxy match", func() {

From 7caab017850fdf7cb79e93b60ab9bd6baa28b027 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 2 May 2022 14:15:41 +0100
Subject: [PATCH 362/385] Fixed desynchronized comment and behavior for log
 interval

In 64ce7dff1b38ecda027d67c8ba54d8290755afa0, the log interval is modified while the comment is left unchanged.
---
 broker/metrics.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/broker/metrics.go b/broker/metrics.go
index 5d95cb4..fbf3452 100644
--- a/broker/metrics.go
+++ b/broker/metrics.go
@@ -166,7 +166,7 @@ func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) {
 	m.logger = metricsLogger
 	m.promMetrics = initPrometheus()
 
-	// Write to log file every hour with updated metrics
+	// Write to log file every day with updated metrics
 	go m.logMetrics()
 
 	return m, nil

From b78eb74e42e58827ac579e05049f4d9f5cd6f23a Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 2 May 2022 16:13:43 +0100
Subject: [PATCH 363/385] Add Proxy Relay URL Rejection Metrics

---
 broker/metrics.go | 21 +++++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/broker/metrics.go b/broker/metrics.go
index fbf3452..d9f8123 100644
--- a/broker/metrics.go
+++ b/broker/metrics.go
@@ -49,8 +49,9 @@ type Metrics struct {
 	clientUnrestrictedDeniedCount uint
 	clientProxyMatchCount         uint
 
-	proxyPollWithRelayURLExtension    uint
-	proxyPollWithoutRelayURLExtension uint
+	proxyPollWithRelayURLExtension         uint
+	proxyPollWithoutRelayURLExtension      uint
+	proxyPollRejectedWithRelayURLExtension uint
 
 	// synchronization for access to snowflake metrics
 	lock sync.Mutex
@@ -194,6 +195,7 @@ func (m *Metrics) printMetrics() {
 	m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount))
 	m.logger.Println("snowflake-proxy-poll-with-relay-url-count", binCount(m.proxyPollWithRelayURLExtension))
 	m.logger.Println("snowflake-proxy-poll-without-relay-url-count", binCount(m.proxyPollWithoutRelayURLExtension))
+	m.logger.Println("snowflake-proxy-rejected-for-relay-url-count", binCount(m.proxyPollRejectedWithRelayURLExtension))
 	m.logger.Println("client-denied-count", binCount(m.clientDeniedCount))
 	m.logger.Println("client-restricted-denied-count", binCount(m.clientRestrictedDeniedCount))
 	m.logger.Println("client-unrestricted-denied-count", binCount(m.clientUnrestrictedDeniedCount))
@@ -235,6 +237,8 @@ type PromMetrics struct {
 
 	ProxyPollWithRelayURLExtensionTotal    *RoundedCounterVec
 	ProxyPollWithoutRelayURLExtensionTotal *RoundedCounterVec
+
+	ProxyPollRejectedForRelayURLExtensionTotal *RoundedCounterVec
 }
 
 // Initialize metrics for prometheus exporter
@@ -276,7 +280,7 @@ func initPrometheus() *PromMetrics {
 			Name:      "rounded_proxy_poll_with_relay_url_extension_total",
 			Help:      "The number of snowflake proxy polls with Relay URL Extension, rounded up to a multiple of 8",
 		},
-		[]string{"nat", "status"},
+		[]string{"nat"},
 	)
 
 	promMetrics.ProxyPollWithoutRelayURLExtensionTotal = NewRoundedCounterVec(
@@ -285,7 +289,16 @@ func initPrometheus() *PromMetrics {
 			Name:      "rounded_proxy_poll_without_relay_url_extension_total",
 			Help:      "The number of snowflake proxy polls without Relay URL Extension, rounded up to a multiple of 8",
 		},
-		[]string{"nat", "status"},
+		[]string{"nat"},
+	)
+
+	promMetrics.ProxyPollRejectedForRelayURLExtensionTotal = NewRoundedCounterVec(
+		prometheus.CounterOpts{
+			Namespace: prometheusNamespace,
+			Name:      "rounded_proxy_poll_rejected_relay_url_extension_total",
+			Help:      "The number of snowflake proxy polls rejected by Relay URL Extension, rounded up to a multiple of 8",
+		},
+		[]string{"nat"},
 	)
 
 	promMetrics.ClientPollTotal = NewRoundedCounterVec(

From dd61e2be0f65aed72b0740aa22debab7246ebc48 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 2 May 2022 16:19:27 +0100
Subject: [PATCH 364/385] Add Proxy Relay URL Metrics Collection

---
 broker/ipc.go                   | 17 +++++++++++++++++
 broker/metrics.go               |  1 +
 broker/snowflake-broker_test.go |  4 ++--
 3 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/broker/ipc.go b/broker/ipc.go
index 5a93585..c86d1a7 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -71,7 +71,24 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
 		return messages.ErrBadRequest
 	}
 
+	if !relayPatternSupported {
+		i.ctx.metrics.lock.Lock()
+		i.ctx.metrics.proxyPollWithoutRelayURLExtension++
+		i.ctx.metrics.promMetrics.ProxyPollWithoutRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType}).Inc()
+		i.ctx.metrics.lock.Unlock()
+	} else {
+		i.ctx.metrics.lock.Lock()
+		i.ctx.metrics.proxyPollWithRelayURLExtension++
+		i.ctx.metrics.promMetrics.ProxyPollWithRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType}).Inc()
+		i.ctx.metrics.lock.Unlock()
+	}
+
 	if !i.ctx.CheckProxyRelayPattern(relayPattern, !relayPatternSupported) {
+		i.ctx.metrics.lock.Lock()
+		i.ctx.metrics.proxyPollRejectedWithRelayURLExtension++
+		i.ctx.metrics.promMetrics.ProxyPollRejectedForRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType}).Inc()
+		i.ctx.metrics.lock.Unlock()
+
 		log.Printf("bad request: rejected relay pattern from proxy = %v", messages.ErrBadRequest)
 		b, err := messages.EncodePollResponseWithRelayURL("", false, "", "", "incorrect relay pattern")
 		*response = b
diff --git a/broker/metrics.go b/broker/metrics.go
index d9f8123..eecc137 100644
--- a/broker/metrics.go
+++ b/broker/metrics.go
@@ -316,6 +316,7 @@ func initPrometheus() *PromMetrics {
 		promMetrics.ProxyTotal, promMetrics.AvailableProxies,
 		promMetrics.ProxyPollWithRelayURLExtensionTotal,
 		promMetrics.ProxyPollWithoutRelayURLExtensionTotal,
+		promMetrics.ProxyPollRejectedForRelayURLExtensionTotal,
 	)
 
 	return promMetrics
diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go
index 6a3ba62..aee8578 100644
--- a/broker/snowflake-broker_test.go
+++ b/broker/snowflake-broker_test.go
@@ -560,7 +560,7 @@ func TestMetrics(t *testing.T) {
 			So(metricsStr, ShouldContainSubstring, "\nsnowflake-ips-standalone 1\n")
 			So(metricsStr, ShouldContainSubstring, "\nsnowflake-ips-badge 1\n")
 			So(metricsStr, ShouldContainSubstring, "\nsnowflake-ips-webext 1\n")
-			So(metricsStr, ShouldEndWith, "\nsnowflake-ips-total 4\nsnowflake-idle-count 8\nsnowflake-proxy-poll-with-relay-url-count 0\nsnowflake-proxy-poll-without-relay-url-count 8\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 1\n")
+			So(metricsStr, ShouldEndWith, "\nsnowflake-ips-total 4\nsnowflake-idle-count 8\nsnowflake-proxy-poll-with-relay-url-count 0\nsnowflake-proxy-poll-without-relay-url-count 8\nsnowflake-proxy-rejected-for-relay-url-count 0\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 1\n")
 		})
 
 		//Test addition of client failures
@@ -584,7 +584,7 @@ func TestMetrics(t *testing.T) {
 			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-standalone 0\n")
 			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-badge 0\n")
 			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-webext 0\n")
-			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-total 0\nsnowflake-idle-count 0\nsnowflake-proxy-poll-with-relay-url-count 0\nsnowflake-proxy-poll-without-relay-url-count 0\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 0\n")
+			So(buf.String(), ShouldContainSubstring, "\nsnowflake-ips-total 0\nsnowflake-idle-count 0\nsnowflake-proxy-poll-with-relay-url-count 0\nsnowflake-proxy-poll-without-relay-url-count 0\nsnowflake-proxy-rejected-for-relay-url-count 0\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 0\n")
 		})
 		//Test addition of client matches
 		Convey("for client-proxy match", func() {

From f789dce6d2b5e6e7d02eef6b168c31ed2ddd149e Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 17 May 2022 15:53:15 +0100
Subject: [PATCH 365/385] Represent Bridge Fingerprint As String

---
 broker/bridge-list.go                   | 22 +++++++++---------
 broker/bridge-list_test.go              |  9 ++++++--
 broker/broker.go                        |  5 +++--
 broker/ipc.go                           | 17 +++++++++++---
 broker/snowflake-broker_test.go         |  2 +-
 common/bridgefingerprint/fingerprint.go | 30 +++++++++++++++++++++++++
 common/messages/client.go               |  5 +++--
 7 files changed, 69 insertions(+), 21 deletions(-)
 create mode 100644 common/bridgefingerprint/fingerprint.go

diff --git a/broker/bridge-list.go b/broker/bridge-list.go
index e77db65..ca2c041 100644
--- a/broker/bridge-list.go
+++ b/broker/bridge-list.go
@@ -2,27 +2,26 @@ package main
 
 import (
 	"bufio"
-	"encoding/hex"
 	"encoding/json"
 	"errors"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
 	"io"
 	"sync"
 )
 
 var ErrBridgeNotFound = errors.New("bridge not found")
-var ErrBridgeFingerprintInvalid = errors.New("bridge fingerprint invalid")
 
 func NewBridgeListHolder() BridgeListHolderFileBased {
 	return &bridgeListHolder{}
 }
 
 type bridgeListHolder struct {
-	bridgeInfo       map[[20]byte]BridgeInfo
+	bridgeInfo       map[bridgefingerprint.Fingerprint]BridgeInfo
 	accessBridgeInfo sync.RWMutex
 }
 
 type BridgeListHolder interface {
-	GetBridgeInfo(fingerprint [20]byte) (BridgeInfo, error)
+	GetBridgeInfo(bridgefingerprint.Fingerprint) (BridgeInfo, error)
 }
 
 type BridgeListHolderFileBased interface {
@@ -36,7 +35,7 @@ type BridgeInfo struct {
 	Fingerprint      string `json:"fingerprint"`
 }
 
-func (h *bridgeListHolder) GetBridgeInfo(fingerprint [20]byte) (BridgeInfo, error) {
+func (h *bridgeListHolder) GetBridgeInfo(fingerprint bridgefingerprint.Fingerprint) (BridgeInfo, error) {
 	h.accessBridgeInfo.RLock()
 	defer h.accessBridgeInfo.RUnlock()
 	if bridgeInfo, ok := h.bridgeInfo[fingerprint]; ok {
@@ -46,7 +45,7 @@ func (h *bridgeListHolder) GetBridgeInfo(fingerprint [20]byte) (BridgeInfo, erro
 }
 
 func (h *bridgeListHolder) LoadBridgeInfo(reader io.Reader) error {
-	bridgeInfoMap := map[[20]byte]BridgeInfo{}
+	bridgeInfoMap := map[bridgefingerprint.Fingerprint]BridgeInfo{}
 	inputScanner := bufio.NewScanner(reader)
 	for inputScanner.Scan() {
 		inputLine := inputScanner.Bytes()
@@ -54,13 +53,14 @@ func (h *bridgeListHolder) LoadBridgeInfo(reader io.Reader) error {
 		if err := json.Unmarshal(inputLine, &bridgeInfo); err != nil {
 			return err
 		}
-		var bridgeHash [20]byte
-		if n, err := hex.Decode(bridgeHash[:], []byte(bridgeInfo.Fingerprint)); err != nil {
+
+		var bridgeFingerprint bridgefingerprint.Fingerprint
+		var err error
+		if bridgeFingerprint, err = bridgefingerprint.FingerprintFromHexString(bridgeInfo.Fingerprint); err != nil {
 			return err
-		} else if n != 20 {
-			return ErrBridgeFingerprintInvalid
 		}
-		bridgeInfoMap[bridgeHash] = bridgeInfo
+
+		bridgeInfoMap[bridgeFingerprint] = bridgeInfo
 	}
 	h.accessBridgeInfo.Lock()
 	defer h.accessBridgeInfo.Unlock()
diff --git a/broker/bridge-list_test.go b/broker/bridge-list_test.go
index 73da43c..4b53821 100644
--- a/broker/bridge-list_test.go
+++ b/broker/bridge-list_test.go
@@ -3,6 +3,7 @@ package main
 import (
 	"bytes"
 	"encoding/hex"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
 	. "github.com/smartystreets/goconvey/convey"
 	"testing"
 )
@@ -34,7 +35,9 @@ func TestBridgeLoad(t *testing.T) {
 				So(n, ShouldEqual, 20)
 				So(err, ShouldBeNil)
 			}
-			bridgeInfo, err := bridgeList.GetBridgeInfo(bridgeFingerprint)
+			Fingerprint, err := bridgefingerprint.FingerprintFromBytes(bridgeFingerprint[:])
+			So(err, ShouldBeNil)
+			bridgeInfo, err := bridgeList.GetBridgeInfo(Fingerprint)
 			So(err, ShouldBeNil)
 			So(bridgeInfo.DisplayName, ShouldEqual, "default")
 			So(bridgeInfo.WebSocketAddress, ShouldEqual, "wss://snowflake.torproject.org")
@@ -50,7 +53,9 @@ func TestBridgeLoad(t *testing.T) {
 				So(n, ShouldEqual, 20)
 				So(err, ShouldBeNil)
 			}
-			bridgeInfo, err := bridgeList.GetBridgeInfo(bridgeFingerprint)
+			Fingerprint, err := bridgefingerprint.FingerprintFromBytes(bridgeFingerprint[:])
+			So(err, ShouldBeNil)
+			bridgeInfo, err := bridgeList.GetBridgeInfo(Fingerprint)
 			So(err, ShouldBeNil)
 			So(bridgeInfo.DisplayName, ShouldEqual, "imaginary-8")
 			So(bridgeInfo.WebSocketAddress, ShouldEqual, "wss://imaginary-8-snowflake.torproject.org")
diff --git a/broker/broker.go b/broker/broker.go
index 8ca0120..9162370 100644
--- a/broker/broker.go
+++ b/broker/broker.go
@@ -10,6 +10,7 @@ import (
 	"container/heap"
 	"crypto/tls"
 	"flag"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
 	"io"
 	"log"
 	"net/http"
@@ -44,7 +45,7 @@ type BrokerContext struct {
 	presumedPatternForLegacyClient string
 }
 
-func (ctx *BrokerContext) GetBridgeInfo(fingerprint [20]byte) (BridgeInfo, error) {
+func (ctx *BrokerContext) GetBridgeInfo(fingerprint bridgefingerprint.Fingerprint) (BridgeInfo, error) {
 	return ctx.bridgeList.GetBridgeInfo(fingerprint)
 }
 
@@ -178,7 +179,7 @@ func (ctx *BrokerContext) CheckProxyRelayPattern(pattern string, nonSupported bo
 type ClientOffer struct {
 	natType     string
 	sdp         []byte
-	fingerprint [20]byte
+	fingerprint []byte
 }
 
 func main() {
diff --git a/broker/ipc.go b/broker/ipc.go
index c86d1a7..f5d4747 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -4,6 +4,7 @@ import (
 	"container/heap"
 	"encoding/hex"
 	"fmt"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
 	"log"
 	"net"
 	"time"
@@ -130,7 +131,11 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
 
 	i.ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "matched"}).Inc()
 	var relayURL string
-	if info, err := i.ctx.bridgeList.GetBridgeInfo(offer.fingerprint); err != nil {
+	bridgeFingerprint, err := bridgefingerprint.FingerprintFromBytes(offer.fingerprint)
+	if err != nil {
+		return messages.ErrBadRequest
+	}
+	if info, err := i.ctx.bridgeList.GetBridgeInfo(bridgeFingerprint); err != nil {
 		return err
 	} else {
 		relayURL = info.WebSocketAddress
@@ -172,12 +177,18 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 	if err != nil {
 		return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response)
 	}
-	copy(offer.fingerprint[:], fingerprint)
 
-	if _, err := i.ctx.GetBridgeInfo(offer.fingerprint); err != nil {
+	BridgeFingerprint, err := bridgefingerprint.FingerprintFromBytes(fingerprint)
+	if err != nil {
+		return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response)
+	}
+
+	if _, err := i.ctx.GetBridgeInfo(BridgeFingerprint); err != nil {
 		return err
 	}
 
+	offer.fingerprint = BridgeFingerprint.ToBytes()
+
 	// Only hand out known restricted snowflakes to unrestricted clients
 	var snowflakeHeap *SnowflakeHeap
 	if offer.natType == NATUnrestricted {
diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go
index aee8578..a72f3ac 100644
--- a/broker/snowflake-broker_test.go
+++ b/broker/snowflake-broker_test.go
@@ -258,7 +258,7 @@ func TestBroker(t *testing.T) {
 				// Pass a fake client offer to this proxy
 				p := <-ctx.proxyPolls
 				So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp")
-				p.offerChannel <- &ClientOffer{sdp: []byte("fake offer"), fingerprint: defaultBridge}
+				p.offerChannel <- &ClientOffer{sdp: []byte("fake offer"), fingerprint: defaultBridge[:]}
 				<-done
 				So(w.Code, ShouldEqual, http.StatusOK)
 				So(w.Body.String(), ShouldEqual, `{"Status":"client match","Offer":"fake offer","NAT":"","RelayURL":"wss://snowflake.torproject.net/"}`)
diff --git a/common/bridgefingerprint/fingerprint.go b/common/bridgefingerprint/fingerprint.go
new file mode 100644
index 0000000..1a89773
--- /dev/null
+++ b/common/bridgefingerprint/fingerprint.go
@@ -0,0 +1,30 @@
+package bridgefingerprint
+
+import (
+	"encoding/hex"
+	"errors"
+)
+
+type Fingerprint string
+
+var ErrBridgeFingerprintInvalid = errors.New("bridge fingerprint invalid")
+
+func FingerprintFromBytes(bytes []byte) (Fingerprint, error) {
+	n := len(bytes)
+	if n != 20 && n != 32 {
+		return Fingerprint(""), ErrBridgeFingerprintInvalid
+	}
+	return Fingerprint(bytes), nil
+}
+
+func FingerprintFromHexString(hexString string) (Fingerprint, error) {
+	decoded, err := hex.DecodeString(hexString)
+	if err != nil {
+		return "", err
+	}
+	return FingerprintFromBytes(decoded)
+}
+
+func (f Fingerprint) ToBytes() []byte {
+	return []byte(f)
+}
diff --git a/common/messages/client.go b/common/messages/client.go
index 96f8ed8..af63e08 100644
--- a/common/messages/client.go
+++ b/common/messages/client.go
@@ -5,9 +5,9 @@ package messages
 
 import (
 	"bytes"
-	"encoding/hex"
 	"encoding/json"
 	"fmt"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
 
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
 )
@@ -106,7 +106,8 @@ func DecodeClientPollRequest(data []byte) (*ClientPollRequest, error) {
 	if message.Fingerprint == "" {
 		message.Fingerprint = defaultBridgeFingerprint
 	}
-	if hex.DecodedLen(len(message.Fingerprint)) != 20 {
+
+	if _, err := bridgefingerprint.FingerprintFromHexString(message.Fingerprint); err != nil {
 		return nil, fmt.Errorf("cannot decode fingerprint")
 	}
 

From c5e5b45b062098389c22710745ae2f7372d299e4 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 17 May 2022 17:44:37 +0100
Subject: [PATCH 366/385] Update message protocol version to 1.3 for RelayURL

---
 common/messages/proxy.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/common/messages/proxy.go b/common/messages/proxy.go
index 6ea2c8a..aa55c22 100644
--- a/common/messages/proxy.go
+++ b/common/messages/proxy.go
@@ -13,7 +13,7 @@ import (
 )
 
 const (
-	version      = "1.2"
+	version      = "1.3"
 	ProxyUnknown = "unknown"
 )
 

From 8ab45651d094de98ff48c5900e6de50a74a0f867 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 18 May 2022 12:19:21 +0100
Subject: [PATCH 367/385] Disallow unknown bridge list file field

---
 broker/bridge-list.go | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/broker/bridge-list.go b/broker/bridge-list.go
index ca2c041..8a80f6a 100644
--- a/broker/bridge-list.go
+++ b/broker/bridge-list.go
@@ -2,11 +2,13 @@ package main
 
 import (
 	"bufio"
+	"bytes"
 	"encoding/json"
 	"errors"
-	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
 	"io"
 	"sync"
+
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
 )
 
 var ErrBridgeNotFound = errors.New("bridge not found")
@@ -50,7 +52,9 @@ func (h *bridgeListHolder) LoadBridgeInfo(reader io.Reader) error {
 	for inputScanner.Scan() {
 		inputLine := inputScanner.Bytes()
 		bridgeInfo := BridgeInfo{}
-		if err := json.Unmarshal(inputLine, &bridgeInfo); err != nil {
+		decoder := json.NewDecoder(bytes.NewReader(inputLine))
+		decoder.DisallowUnknownFields()
+		if err := decoder.Decode(&bridgeInfo); err != nil {
 			return err
 		}
 

From 8ba89179f1f862a4957ed28a88b6a08167b653e9 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 18 May 2022 14:11:35 +0100
Subject: [PATCH 368/385] Add document for LoadBridgeInfo input

---
 broker/bridge-list.go | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/broker/bridge-list.go b/broker/bridge-list.go
index 8a80f6a..4433a12 100644
--- a/broker/bridge-list.go
+++ b/broker/bridge-list.go
@@ -1,3 +1,24 @@
+/* (*BridgeListHolderFileBased).LoadBridgeInfo loads a Snowflake Server bridge info description file,
+   its format is as follows:
+
+   This file should be in newline-delimited JSON format(https://jsonlines.org/).
+   For each line, the format of json data should be in the format of:
+   {"displayName":"default", "webSocketAddress":"wss://snowflake.torproject.net/", "fingerprint":"2B280B23E1107BB62ABFC40DDCC8824814F80A72"}
+
+   displayName:string is the name of this bridge. This value is not currently used programmatically.
+
+   webSocketAddress:string is the WebSocket URL of this bridge.
+   This will be the address proxy used to connect to this snowflake server.
+
+   fingerprint:string is the identifier of the bridge.
+   This will be used by a client to identify the bridge it wishes to connect to.
+
+   The existence of ANY other fields is NOT permitted.
+
+   The file will be considered invalid if there is at least one invalid json record.
+   In this case, an error will be returned, and none of the records will be loaded.
+*/
+
 package main
 
 import (

From a4bbb728e611dbf5bd8e2021e8b1e654923e5c1d Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 18 May 2022 15:52:46 +0100
Subject: [PATCH 369/385] Fix not zero metrics for 1.3 values

---
 broker/metrics.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/broker/metrics.go b/broker/metrics.go
index eecc137..639d505 100644
--- a/broker/metrics.go
+++ b/broker/metrics.go
@@ -212,6 +212,9 @@ func (m *Metrics) zeroMetrics() {
 	m.clientDeniedCount = 0
 	m.clientRestrictedDeniedCount = 0
 	m.clientUnrestrictedDeniedCount = 0
+	m.proxyPollRejectedWithRelayURLExtension = 0
+	m.proxyPollWithRelayURLExtension = 0
+	m.proxyPollWithoutRelayURLExtension = 0
 	m.clientProxyMatchCount = 0
 	m.countryStats.counts = make(map[string]int)
 	for pType := range m.countryStats.proxies {

From 0ae4d821f0c8440a3d1d9771f16912ef897754be Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 18 May 2022 16:19:38 +0100
Subject: [PATCH 370/385] Move ErrExtraInfo to ipc.go

---
 common/messages/ipc.go   | 1 +
 common/messages/proxy.go | 2 --
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/common/messages/ipc.go b/common/messages/ipc.go
index 13e096f..3250742 100644
--- a/common/messages/ipc.go
+++ b/common/messages/ipc.go
@@ -12,6 +12,7 @@ type Arg struct {
 var (
 	ErrBadRequest = errors.New("bad request")
 	ErrInternal   = errors.New("internal error")
+	ErrExtraInfo  = errors.New("client sent extra info")
 
 	StrTimedOut  = "timed out waiting for answer!"
 	StrNoProxies = "no snowflake proxies currently available"
diff --git a/common/messages/proxy.go b/common/messages/proxy.go
index aa55c22..41af4bf 100644
--- a/common/messages/proxy.go
+++ b/common/messages/proxy.go
@@ -24,8 +24,6 @@ var KnownProxyTypes = map[string]bool{
 	"iptproxy":   true,
 }
 
-var ErrExtraInfo = errors.New("client sent extra info")
-
 /* Version 1.2 specification:
 
 == ProxyPollRequest ==

From e5b799d618fc9a1669626c6ca1a6e759192640b2 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 18 May 2022 16:49:19 +0100
Subject: [PATCH 371/385] Update documents for broker messages

---
 common/messages/proxy.go | 12 +++++++-----
 doc/broker-spec.txt      | 28 ++++++++++++++++++++++++----
 2 files changed, 31 insertions(+), 9 deletions(-)

diff --git a/common/messages/proxy.go b/common/messages/proxy.go
index 41af4bf..b135c34 100644
--- a/common/messages/proxy.go
+++ b/common/messages/proxy.go
@@ -24,15 +24,16 @@ var KnownProxyTypes = map[string]bool{
 	"iptproxy":   true,
 }
 
-/* Version 1.2 specification:
+/* Version 1.3 specification:
 
 == ProxyPollRequest ==
 {
   Sid: [generated session id of proxy],
-  Version: 1.2,
+  Version: 1.3,
   Type: ["badge"|"webext"|"standalone"],
   NAT: ["unknown"|"restricted"|"unrestricted"],
-  Clients: [number of current clients, rounded down to multiples of 8]
+  Clients: [number of current clients, rounded down to multiples of 8],
+  AcceptedRelayPattern: [a pattern representing accepted set of relay domains]
 }
 
 == ProxyPollResponse ==
@@ -44,7 +45,8 @@ HTTP 200 OK
     type: offer,
     sdp: [WebRTC SDP]
   },
-  NAT: ["unknown"|"restricted"|"unrestricted"]
+  NAT: ["unknown"|"restricted"|"unrestricted"],
+  RelayURL: [the WebSocket URL proxy should connect to relay Snowflake traffic]
 }
 
 2) If a client is not matched:
@@ -60,7 +62,7 @@ HTTP 400 BadRequest
 == ProxyAnswerRequest ==
 {
   Sid: [generated session id of proxy],
-  Version: 1.2,
+  Version: 1.3,
   Answer:
   {
     type: answer,
diff --git a/doc/broker-spec.txt b/doc/broker-spec.txt
index f25be79..b138605 100644
--- a/doc/broker-spec.txt
+++ b/doc/broker-spec.txt
@@ -100,6 +100,24 @@ Metrics data from the Snowflake broker can be retrieved by sending an HTTP GET r
         A count of the total number of unique IP addresses of snowflake
         proxies that have an unknown NAT type.
 
+   "snowflake-proxy-poll-with-relay-url-count" NUM NL
+        [At most once.]
+
+        A count of snowflake proxy polls with relay url extension present.
+        This means this proxy understands relay url, and is sending its
+        allowed prefix.
+   "snowflake-proxy-poll-without-relay-url-count" NUM NL
+        [At most once.]
+
+        A count of snowflake proxy polls with relay url extension absent.
+        This means this proxy is not yet updated.
+   "snowflake-proxy-rejected-for-relay-url-count" NUM NL
+        [At most once.]
+
+        A count of snowflake proxy polls with relay url extension rejected
+        based on broker's relay url extension policy.
+        This means an incompatible allowed relay pattern is included in the
+        proxy poll message.
 2. Broker messaging specification and endpoints
 
 The broker facilitates the connection of snowflake clients and snowflake proxies
@@ -177,10 +195,11 @@ POST /proxy HTTP
 
 {
   Sid: [generated session id of proxy],
-  Version: 1.1,
+  Version: 1.3,
   Type: ["badge"|"webext"|"standalone"|"mobile"],
   NAT: ["unknown"|"restricted"|"unrestricted"],
-  Clients: [number of current clients, rounded down to multiples of 8]
+  Clients: [number of current clients, rounded down to multiples of 8],
+  AcceptedRelayPattern: [a pattern representing accepted set of relay domains]
 }
 ```
 
@@ -195,7 +214,8 @@ HTTP 200 OK
   {
     type: offer,
     sdp: [WebRTC SDP]
-  }
+  },
+  RelayURL: [the WebSocket URL proxy should connect to relay Snowflake traffic]
 }
 ```
 
@@ -220,7 +240,7 @@ POST /answer HTTP
 
 {
   Sid: [generated session id of proxy],
-  Version: 1.1,
+  Version: 1.3,
   Answer:
   {
     type: answer,

From ddf72025d199db0ebd265f4b0ccc11dc243d88f9 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 1 Jun 2022 14:39:53 +0100
Subject: [PATCH 372/385] Restrict Allowed Relay to Tor Pool by default

---
 proxy/main.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxy/main.go b/proxy/main.go
index 305d0b0..63ed5c7 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -21,7 +21,7 @@ func main() {
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	relayURL := flag.String("relay", sf.DefaultRelayURL, "websocket relay URL")
-	allowedRelayHostNamePattern := flag.String("allowed-relay-hostname-pattern", "", "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.")
 	allowNonTLSRelay := flag.Bool("allow-non-tls-relay", false, "allow relay without tls encryption")
 	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\". ")

From 97dea533da7b6b3b2b1dfbffe7dca3a8350fab0b Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 15 Jun 2022 13:20:58 +0100
Subject: [PATCH 373/385] Update Relay Pattern format to include dollar sign

---
 common/namematcher/matcher.go      |  5 +++++
 common/namematcher/matcher_test.go | 26 +++++++++++++-------------
 proxy/lib/snowflake.go             |  6 +++++-
 proxy/main.go                      |  2 +-
 4 files changed, 24 insertions(+), 15 deletions(-)

diff --git a/common/namematcher/matcher.go b/common/namematcher/matcher.go
index 57f9c56..afcdbff 100644
--- a/common/namematcher/matcher.go
+++ b/common/namematcher/matcher.go
@@ -3,9 +3,14 @@ package namematcher
 import "strings"
 
 func NewNameMatcher(rule string) NameMatcher {
+	rule = strings.TrimSuffix(rule, "$")
 	return NameMatcher{suffix: strings.TrimPrefix(rule, "^"), exact: strings.HasPrefix(rule, "^")}
 }
 
+func IsValidRule(rule string) bool {
+	return strings.HasSuffix(rule, "$")
+}
+
 type NameMatcher struct {
 	exact  bool
 	suffix string
diff --git a/common/namematcher/matcher_test.go b/common/namematcher/matcher_test.go
index 8d92614..08d089c 100644
--- a/common/namematcher/matcher_test.go
+++ b/common/namematcher/matcher_test.go
@@ -11,13 +11,13 @@ func TestMatchMember(t *testing.T) {
 		expects bool
 	}{
 		{matcher: "", target: "", expects: true},
-		{matcher: "^snowflake.torproject.net", target: "snowflake.torproject.net", expects: true},
-		{matcher: "^snowflake.torproject.net", target: "faketorproject.net", expects: false},
-		{matcher: "snowflake.torproject.net", target: "faketorproject.net", expects: false},
-		{matcher: "snowflake.torproject.net", target: "snowflake.torproject.net", expects: true},
-		{matcher: "snowflake.torproject.net", target: "imaginary-01-snowflake.torproject.net", expects: true},
-		{matcher: "snowflake.torproject.net", target: "imaginary-aaa-snowflake.torproject.net", expects: true},
-		{matcher: "snowflake.torproject.net", target: "imaginary-aaa-snowflake.faketorproject.net", expects: false},
+		{matcher: "^snowflake.torproject.net$", target: "snowflake.torproject.net", expects: true},
+		{matcher: "^snowflake.torproject.net$", target: "faketorproject.net", expects: false},
+		{matcher: "snowflake.torproject.net$", target: "faketorproject.net", expects: false},
+		{matcher: "snowflake.torproject.net$", target: "snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net$", target: "imaginary-01-snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net$", target: "imaginary-aaa-snowflake.torproject.net", expects: true},
+		{matcher: "snowflake.torproject.net$", target: "imaginary-aaa-snowflake.faketorproject.net", expects: false},
 	}
 	for _, v := range testingVector {
 		t.Run(v.matcher+"<>"+v.target, func(t *testing.T) {
@@ -36,12 +36,12 @@ func TestMatchSubset(t *testing.T) {
 		expects bool
 	}{
 		{matcher: "", target: "", expects: true},
-		{matcher: "^snowflake.torproject.net", target: "^snowflake.torproject.net", expects: true},
-		{matcher: "snowflake.torproject.net", target: "^snowflake.torproject.net", expects: true},
-		{matcher: "snowflake.torproject.net", target: "snowflake.torproject.net", expects: true},
-		{matcher: "snowflake.torproject.net", target: "testing-snowflake.torproject.net", expects: true},
-		{matcher: "snowflake.torproject.net", target: "^testing-snowflake.torproject.net", expects: true},
-		{matcher: "snowflake.torproject.net", target: "", expects: false},
+		{matcher: "^snowflake.torproject.net$", target: "^snowflake.torproject.net$", expects: true},
+		{matcher: "snowflake.torproject.net$", target: "^snowflake.torproject.net$", expects: true},
+		{matcher: "snowflake.torproject.net$", target: "snowflake.torproject.net$", expects: true},
+		{matcher: "snowflake.torproject.net$", target: "testing-snowflake.torproject.net$", expects: true},
+		{matcher: "snowflake.torproject.net$", target: "^testing-snowflake.torproject.net$", expects: true},
+		{matcher: "snowflake.torproject.net$", target: "", expects: false},
 	}
 	for _, v := range testingVector {
 		t.Run(v.matcher+"<>"+v.target, func(t *testing.T) {
diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 2770aa4..34f8abe 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -30,7 +30,6 @@ import (
 	"crypto/rand"
 	"encoding/base64"
 	"fmt"
-	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/namematcher"
 	"io"
 	"io/ioutil"
 	"log"
@@ -43,6 +42,7 @@ import (
 
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/namematcher"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/task"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/util"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/websocketconn"
@@ -582,6 +582,10 @@ func (sf *SnowflakeProxy) Start() error {
 		return fmt.Errorf("invalid relay url: %s", err)
 	}
 
+	if !namematcher.IsValidRule(sf.RelayDomainNamePattern) {
+		return fmt.Errorf("invalid relay domain name pattern")
+	}
+
 	config = webrtc.Configuration{
 		ICEServers: []webrtc.ICEServer{
 			{
diff --git a/proxy/main.go b/proxy/main.go
index 63ed5c7..c42852e 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -21,7 +21,7 @@ func main() {
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
 	keepLocalAddresses := flag.Bool("keep-local-addresses", false, "keep local LAN address ICE candidates")
 	relayURL := flag.String("relay", sf.DefaultRelayURL, "websocket relay URL")
-	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.")
 	allowNonTLSRelay := flag.Bool("allow-non-tls-relay", false, "allow relay without tls encryption")
 	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\". ")

From 211254fa9849a1ae705a482ba984d0d415730560 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 27 May 2022 16:20:47 +0100
Subject: [PATCH 374/385] Add distinct IP counter

---
 common/ipsetsink/sink.go                    | 52 ++++++++++++++++
 common/ipsetsink/sink_test.go               | 47 ++++++++++++++
 common/ipsetsink/sinkcluster/common.go      |  9 +++
 common/ipsetsink/sinkcluster/reader.go      | 60 ++++++++++++++++++
 common/ipsetsink/sinkcluster/writer.go      | 68 +++++++++++++++++++++
 common/ipsetsink/sinkcluster/writer_test.go | 33 ++++++++++
 distinctcounter/counter.go                  | 37 +++++++++++
 go.mod                                      |  1 +
 8 files changed, 307 insertions(+)
 create mode 100644 common/ipsetsink/sink.go
 create mode 100644 common/ipsetsink/sink_test.go
 create mode 100644 common/ipsetsink/sinkcluster/common.go
 create mode 100644 common/ipsetsink/sinkcluster/reader.go
 create mode 100644 common/ipsetsink/sinkcluster/writer.go
 create mode 100644 common/ipsetsink/sinkcluster/writer_test.go
 create mode 100644 distinctcounter/counter.go

diff --git a/common/ipsetsink/sink.go b/common/ipsetsink/sink.go
new file mode 100644
index 0000000..b62f786
--- /dev/null
+++ b/common/ipsetsink/sink.go
@@ -0,0 +1,52 @@
+package ipsetsink
+
+import (
+	"crypto/hmac"
+	"hash"
+	"hash/crc64"
+
+	"github.com/clarkduvall/hyperloglog"
+	"golang.org/x/crypto/sha3"
+)
+
+func NewIPSetSink(maskingKey string) *IPSetSink {
+	countDistinct, _ := hyperloglog.NewPlus(18)
+	return &IPSetSink{
+		ipMaskingKey:  maskingKey,
+		countDistinct: countDistinct,
+	}
+}
+
+type IPSetSink struct {
+	ipMaskingKey  string
+	countDistinct *hyperloglog.HyperLogLogPlus
+}
+
+func (s *IPSetSink) maskIPAddress(ipAddress string) []byte {
+	hmacIPMasker := hmac.New(func() hash.Hash {
+		return sha3.New256()
+	}, []byte(s.ipMaskingKey))
+	hmacIPMasker.Write([]byte(ipAddress))
+	return hmacIPMasker.Sum(nil)
+}
+
+func (s *IPSetSink) AddIPToSet(ipAddress string) {
+	s.countDistinct.Add(crc64FromBytes{hashValue(s.maskIPAddress(ipAddress))})
+}
+
+func (s *IPSetSink) Dump() ([]byte, error) {
+	return s.countDistinct.GobEncode()
+}
+
+func (s *IPSetSink) Reset() {
+	s.countDistinct.Clear()
+}
+
+type hashValue []byte
+type crc64FromBytes struct {
+	hashValue
+}
+
+func (c crc64FromBytes) Sum64() uint64 {
+	return crc64.Checksum(c.hashValue, crc64.MakeTable(crc64.ECMA))
+}
diff --git a/common/ipsetsink/sink_test.go b/common/ipsetsink/sink_test.go
new file mode 100644
index 0000000..00ae965
--- /dev/null
+++ b/common/ipsetsink/sink_test.go
@@ -0,0 +1,47 @@
+package ipsetsink
+
+import (
+	"fmt"
+	"github.com/clarkduvall/hyperloglog"
+	"testing"
+)
+import . "github.com/smartystreets/goconvey/convey"
+
+func TestSinkInit(t *testing.T) {
+	Convey("Context", t, func() {
+		sink := NewIPSetSink("demo")
+		sink.AddIPToSet("test1")
+		sink.AddIPToSet("test2")
+		data, err := sink.Dump()
+		So(err, ShouldBeNil)
+		structure, err := hyperloglog.NewPlus(18)
+		So(err, ShouldBeNil)
+		err = structure.GobDecode(data)
+		So(err, ShouldBeNil)
+		count := structure.Count()
+		So(count, ShouldBeBetweenOrEqual, 1, 3)
+	})
+}
+
+func TestSinkCounting(t *testing.T) {
+	Convey("Context", t, func() {
+		for itemCount := 300; itemCount <= 10000; itemCount += 200 {
+			sink := NewIPSetSink("demo")
+			for i := 0; i <= itemCount; i++ {
+				sink.AddIPToSet(fmt.Sprintf("demo%v", i))
+			}
+			for i := 0; i <= itemCount; i++ {
+				sink.AddIPToSet(fmt.Sprintf("demo%v", i))
+			}
+			data, err := sink.Dump()
+			So(err, ShouldBeNil)
+			structure, err := hyperloglog.NewPlus(18)
+			So(err, ShouldBeNil)
+			err = structure.GobDecode(data)
+			So(err, ShouldBeNil)
+			count := structure.Count()
+			So((float64(count)/float64(itemCount))-1.0, ShouldAlmostEqual, 0, 0.01)
+		}
+
+	})
+}
diff --git a/common/ipsetsink/sinkcluster/common.go b/common/ipsetsink/sinkcluster/common.go
new file mode 100644
index 0000000..501c753
--- /dev/null
+++ b/common/ipsetsink/sinkcluster/common.go
@@ -0,0 +1,9 @@
+package sinkcluster
+
+import "time"
+
+type SinkEntry struct {
+	RecordingStart time.Time `json:"recordingStart"`
+	RecordingEnd   time.Time `json:"recordingEnd"`
+	Recorded       []byte    `json:"recorded"`
+}
diff --git a/common/ipsetsink/sinkcluster/reader.go b/common/ipsetsink/sinkcluster/reader.go
new file mode 100644
index 0000000..3f7a08f
--- /dev/null
+++ b/common/ipsetsink/sinkcluster/reader.go
@@ -0,0 +1,60 @@
+package sinkcluster
+
+import (
+	"bufio"
+	"encoding/json"
+	"github.com/clarkduvall/hyperloglog"
+	"io"
+	"time"
+)
+
+func NewClusterCounter(from time.Time, to time.Time) *ClusterCounter {
+	return &ClusterCounter{from: from, to: to}
+}
+
+type ClusterCounter struct {
+	from time.Time
+	to   time.Time
+}
+
+type ClusterCountResult struct {
+	Sum           uint64
+	ChunkIncluded int64
+}
+
+func (c ClusterCounter) Count(reader io.Reader) (*ClusterCountResult, error) {
+	result := ClusterCountResult{}
+	counter, err := hyperloglog.NewPlus(18)
+	if err != nil {
+		return nil, err
+	}
+	inputScanner := bufio.NewScanner(reader)
+	for inputScanner.Scan() {
+		inputLine := inputScanner.Bytes()
+		sinkInfo := SinkEntry{}
+		if err := json.Unmarshal(inputLine, &sinkInfo); err != nil {
+			return nil, err
+		}
+
+		if (sinkInfo.RecordingStart.Before(c.from) && !sinkInfo.RecordingStart.Equal(c.from)) ||
+			sinkInfo.RecordingEnd.After(c.to) {
+			continue
+		}
+
+		restoredCounter, err := hyperloglog.NewPlus(18)
+		if err != nil {
+			return nil, err
+		}
+		err = restoredCounter.GobDecode(sinkInfo.Recorded)
+		if err != nil {
+			return nil, err
+		}
+		result.ChunkIncluded++
+		err = counter.Merge(restoredCounter)
+		if err != nil {
+			return nil, err
+		}
+	}
+	result.Sum = counter.Count()
+	return &result, nil
+}
diff --git a/common/ipsetsink/sinkcluster/writer.go b/common/ipsetsink/sinkcluster/writer.go
new file mode 100644
index 0000000..1c409e8
--- /dev/null
+++ b/common/ipsetsink/sinkcluster/writer.go
@@ -0,0 +1,68 @@
+package sinkcluster
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"log"
+	"time"
+
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/ipsetsink"
+)
+
+func NewClusterWriter(writer WriteSyncer, writeInterval time.Duration, sink *ipsetsink.IPSetSink) *ClusterWriter {
+	c := &ClusterWriter{
+		writer:        writer,
+		lastWriteTime: time.Now(),
+		writeInterval: writeInterval,
+		current:       sink,
+	}
+	return c
+}
+
+type ClusterWriter struct {
+	writer        WriteSyncer
+	lastWriteTime time.Time
+	writeInterval time.Duration
+	current       *ipsetsink.IPSetSink
+}
+
+type WriteSyncer interface {
+	Sync() error
+	io.Writer
+}
+
+func (c *ClusterWriter) WriteIPSetToDisk() {
+	currentTime := time.Now()
+	data, err := c.current.Dump()
+	if err != nil {
+		log.Println("unable able to write ipset to file:", err)
+		return
+	}
+	entry := &SinkEntry{
+		RecordingStart: c.lastWriteTime,
+		RecordingEnd:   currentTime,
+		Recorded:       data,
+	}
+	jsonData, err := json.Marshal(entry)
+	if err != nil {
+		log.Println("unable able to write ipset to file:", err)
+		return
+	}
+	jsonData = append(jsonData, byte('\n'))
+	_, err = io.Copy(c.writer, bytes.NewReader(jsonData))
+	if err != nil {
+		log.Println("unable able to write ipset to file:", err)
+		return
+	}
+	c.writer.Sync()
+	c.lastWriteTime = currentTime
+	c.current.Reset()
+}
+
+func (c *ClusterWriter) AddIPToSet(ipAddress string) {
+	if c.lastWriteTime.Add(c.writeInterval).Before(time.Now()) {
+		c.WriteIPSetToDisk()
+	}
+	c.current.AddIPToSet(ipAddress)
+}
diff --git a/common/ipsetsink/sinkcluster/writer_test.go b/common/ipsetsink/sinkcluster/writer_test.go
new file mode 100644
index 0000000..5319cb2
--- /dev/null
+++ b/common/ipsetsink/sinkcluster/writer_test.go
@@ -0,0 +1,33 @@
+package sinkcluster
+
+import (
+	"bytes"
+	"io"
+	"testing"
+	"time"
+
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/ipsetsink"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+type writerStub struct {
+	io.Writer
+}
+
+func (w writerStub) Sync() error {
+	return nil
+}
+
+func TestSinkWriter(t *testing.T) {
+
+	Convey("Context", t, func() {
+		buffer := bytes.NewBuffer(nil)
+		writerStubInst := &writerStub{buffer}
+		sink := ipsetsink.NewIPSetSink("demo")
+		clusterWriter := NewClusterWriter(writerStubInst, time.Minute, sink)
+		clusterWriter.AddIPToSet("1")
+		clusterWriter.WriteIPSetToDisk()
+		So(buffer.Bytes(), ShouldNotBeNil)
+	})
+}
diff --git a/distinctcounter/counter.go b/distinctcounter/counter.go
new file mode 100644
index 0000000..c128465
--- /dev/null
+++ b/distinctcounter/counter.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"time"
+
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/ipsetsink/sinkcluster"
+)
+
+func main() {
+	inputFile := flag.String("in", "", "")
+	start := flag.String("start", "", "")
+	end := flag.String("end", "", "")
+	flag.Parse()
+	startTime, err := time.Parse(time.UnixDate, *start)
+	if err != nil {
+		log.Fatal("unable to parse start time:", err)
+	}
+	endTime, err := time.Parse(time.UnixDate, *end)
+	if err != nil {
+		log.Fatal("unable to parse end time:", err)
+	}
+	fd, err := os.Open(*inputFile)
+	if err != nil {
+		log.Fatal("unable to open input file:", err)
+	}
+	counter := sinkcluster.NewClusterCounter(startTime, endTime)
+	result, err := counter.Count(fd)
+	if err != nil {
+		log.Fatal("unable to count:", err)
+	}
+	fmt.Printf("sum = %v\n", result.Sum)
+	fmt.Printf("chunkIncluded = %v\n", result.ChunkIncluded)
+}
diff --git a/go.mod b/go.mod
index 842648c..c782967 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.13
 
 require (
 	git.torproject.org/pluggable-transports/goptlib.git v1.1.0
+	github.com/clarkduvall/hyperloglog v0.0.0-20171127014514-a0107a5d8004 // indirect
 	github.com/gorilla/websocket v1.4.1
 	github.com/pion/ice/v2 v2.2.6
 	github.com/pion/sdp/v3 v3.0.5

From fa7d1e2bb77b92452785ce3d03c81a90efc9891e Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 27 May 2022 16:21:28 +0100
Subject: [PATCH 375/385] Add distinct IP counter to metrics

---
 broker/metrics.go | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/broker/metrics.go b/broker/metrics.go
index 639d505..cd1ca37 100644
--- a/broker/metrics.go
+++ b/broker/metrics.go
@@ -14,6 +14,7 @@ import (
 	"sync"
 	"time"
 
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/ipsetsink/sinkcluster"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
 	"github.com/prometheus/client_golang/prometheus"
 	"gitlab.torproject.org/tpo/anti-censorship/geoip"
@@ -41,6 +42,8 @@ type Metrics struct {
 	logger  *log.Logger
 	geoipdb *geoip.Geoip
 
+	distinctIPWriter *sinkcluster.ClusterWriter
+
 	countryStats                  CountryStats
 	clientRoundtripEstimate       time.Duration
 	proxyIdleCount                uint
@@ -324,3 +327,13 @@ func initPrometheus() *PromMetrics {
 
 	return promMetrics
 }
+
+func (m *Metrics) RecordIPAddress(ip string) {
+	if m.distinctIPWriter != nil {
+		m.distinctIPWriter.AddIPToSet(ip)
+	}
+}
+
+func (m *Metrics) SetIPAddressRecorder(recorder *sinkcluster.ClusterWriter) {
+	m.distinctIPWriter = recorder
+}

From 2541b13166b9e502c5314536c3c2777fd84db45b Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 27 May 2022 17:37:23 +0100
Subject: [PATCH 376/385] Add distinct IP counter to broker

---
 broker/broker.go | 17 +++++++++++++++++
 broker/ipc.go    |  1 +
 2 files changed, 18 insertions(+)

diff --git a/broker/broker.go b/broker/broker.go
index 9162370..2bf4614 100644
--- a/broker/broker.go
+++ b/broker/broker.go
@@ -11,6 +11,8 @@ import (
 	"crypto/tls"
 	"flag"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/ipsetsink"
+	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/ipsetsink/sinkcluster"
 	"io"
 	"log"
 	"net/http"
@@ -194,6 +196,8 @@ func main() {
 	var certFilename, keyFilename string
 	var disableGeoip bool
 	var metricsFilename string
+	var ipCountFilename, ipCountMaskingKey string
+	var ipCountInterval time.Duration
 	var unsafeLogging bool
 
 	flag.StringVar(&acmeEmail, "acme-email", "", "optional contact email for Let's Encrypt notifications")
@@ -210,6 +214,9 @@ func main() {
 	flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS")
 	flag.BoolVar(&disableGeoip, "disable-geoip", false, "don't use geoip for stats collection")
 	flag.StringVar(&metricsFilename, "metrics-log", "", "path to metrics logging output")
+	flag.StringVar(&ipCountFilename, "ip-count-log", "", "path to ip count logging output")
+	flag.StringVar(&ipCountMaskingKey, "ip-count-mask", "", "masking key for ip count logging")
+	flag.DurationVar(&ipCountInterval, "ip-count-interval", time.Hour, "time interval between each chunk")
 	flag.BoolVar(&unsafeLogging, "unsafe-logging", false, "prevent logs from being scrubbed")
 	flag.Parse()
 
@@ -257,6 +264,16 @@ func main() {
 		}
 	}
 
+	if ipCountFilename != "" {
+		ipCountFile, err := os.OpenFile(ipCountFilename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+
+		if err != nil {
+			log.Fatal(err.Error())
+		}
+		ipSetSink := ipsetsink.NewIPSetSink(ipCountMaskingKey)
+		ctx.metrics.distinctIPWriter = sinkcluster.NewClusterWriter(ipCountFile, ipCountInterval, ipSetSink)
+	}
+
 	go ctx.Broker()
 
 	i := &IPC{ctx}
diff --git a/broker/ipc.go b/broker/ipc.go
index f5d4747..30de180 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -106,6 +106,7 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
 	} else {
 		i.ctx.metrics.lock.Lock()
 		i.ctx.metrics.UpdateCountryStats(remoteIP, proxyType, natType)
+		i.ctx.metrics.RecordIPAddress(remoteIP)
 		i.ctx.metrics.lock.Unlock()
 	}
 

From be40b623a40d388450722250cc21343f03ea5b46 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 30 May 2022 14:43:04 +0100
Subject: [PATCH 377/385] Add go sum for hyperloglog

---
 go.sum | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/go.sum b/go.sum
index 2c6f232..e610825 100644
--- a/go.sum
+++ b/go.sum
@@ -32,6 +32,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/clarkduvall/hyperloglog v0.0.0-20171127014514-a0107a5d8004 h1:mK6JroY6bLiPS3s6QCYOSjRyErFc2iHNkhhmRfF0nHo=
+github.com/clarkduvall/hyperloglog v0.0.0-20171127014514-a0107a5d8004/go.mod h1:drodPoQNro6QBO6TJ/MpMZbz8Bn2eSDtRN6jpG4VGw8=
 github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=

From af1134362aff7ddbd2103b1b2fd284abe9a03782 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Mon, 30 May 2022 16:34:15 +0100
Subject: [PATCH 378/385] Update distinct counter interface

---
 distinctcounter/counter.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/distinctcounter/counter.go b/distinctcounter/counter.go
index c128465..67110b4 100644
--- a/distinctcounter/counter.go
+++ b/distinctcounter/counter.go
@@ -12,14 +12,14 @@ import (
 
 func main() {
 	inputFile := flag.String("in", "", "")
-	start := flag.String("start", "", "")
-	end := flag.String("end", "", "")
+	start := flag.String("from", "", "")
+	end := flag.String("to", "", "")
 	flag.Parse()
-	startTime, err := time.Parse(time.UnixDate, *start)
+	startTime, err := time.Parse(time.RFC3339, *start)
 	if err != nil {
 		log.Fatal("unable to parse start time:", err)
 	}
-	endTime, err := time.Parse(time.UnixDate, *end)
+	endTime, err := time.Parse(time.RFC3339, *end)
 	if err != nil {
 		log.Fatal("unable to parse end time:", err)
 	}

From b18e6fcfe41e15b213e6793b035106492b311a42 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Tue, 31 May 2022 14:02:04 +0100
Subject: [PATCH 379/385] Add document for Distinct IP file

---
 common/ipsetsink/sinkcluster/common.go | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/common/ipsetsink/sinkcluster/common.go b/common/ipsetsink/sinkcluster/common.go
index 501c753..4360f70 100644
--- a/common/ipsetsink/sinkcluster/common.go
+++ b/common/ipsetsink/sinkcluster/common.go
@@ -1,5 +1,20 @@
 package sinkcluster
 
+/* ClusterWriter, and (ClusterCountResult).Count output a streamed IP set journal file to remember distinct IP address
+
+   its format is as follows:
+
+   This file should be in newline-delimited JSON format(https://jsonlines.org/).
+   For each line, the format of json data should be in the format of:
+   {"recordingStart":"2022-05-30T14:38:44.678610091Z","recordingEnd":"2022-05-30T14:39:48.157630926Z","recorded":""}
+
+	recordingStart:datetime is the time this chunk of recording start.
+
+	recordingEnd:datetime is the time this chunk of recording end.
+
+	recorded is the checkpoint data generated by hyperloglog.
+*/
+
 import "time"
 
 type SinkEntry struct {

From 35e9ab8c0b3168b5eaa4f6538b8e9208eb38c508 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Wed, 15 Jun 2022 15:32:58 +0100
Subject: [PATCH 380/385] Use truncated hash instead crc64 for counted hash

---
 common/ipsetsink/sink.go | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/common/ipsetsink/sink.go b/common/ipsetsink/sink.go
index b62f786..168d061 100644
--- a/common/ipsetsink/sink.go
+++ b/common/ipsetsink/sink.go
@@ -1,9 +1,10 @@
 package ipsetsink
 
 import (
+	"bytes"
 	"crypto/hmac"
+	"encoding/binary"
 	"hash"
-	"hash/crc64"
 
 	"github.com/clarkduvall/hyperloglog"
 	"golang.org/x/crypto/sha3"
@@ -31,7 +32,7 @@ func (s *IPSetSink) maskIPAddress(ipAddress string) []byte {
 }
 
 func (s *IPSetSink) AddIPToSet(ipAddress string) {
-	s.countDistinct.Add(crc64FromBytes{hashValue(s.maskIPAddress(ipAddress))})
+	s.countDistinct.Add(truncatedHash64FromBytes{hashValue(s.maskIPAddress(ipAddress))})
 }
 
 func (s *IPSetSink) Dump() ([]byte, error) {
@@ -43,10 +44,12 @@ func (s *IPSetSink) Reset() {
 }
 
 type hashValue []byte
-type crc64FromBytes struct {
+type truncatedHash64FromBytes struct {
 	hashValue
 }
 
-func (c crc64FromBytes) Sum64() uint64 {
-	return crc64.Checksum(c.hashValue, crc64.MakeTable(crc64.ECMA))
+func (c truncatedHash64FromBytes) Sum64() uint64 {
+	var value uint64
+	binary.Read(bytes.NewReader(c.hashValue), binary.BigEndian, &value)
+	return value
 }

From c983c13a84554d0ba1ffcdd054491090c0eafc54 Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Thu, 23 Jun 2022 11:37:16 +0100
Subject: [PATCH 381/385] Updated ChangeLog for v2.3.0 release

---
 ChangeLog | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/ChangeLog b/ChangeLog
index 9ac6fae..59c5c89 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,10 @@
+Changes in version v2.3.0 - 2022-06-23
+- Issue 40146: Avoid performing two NAT probe tests at startup
+- Issue 40134: Log messages from client NAT check failures are confusing
+- Issue 34075: Implement metrics to measure snowflake churn
+- Issue 28651: Prepare all pieces of the snowflake pipeline for a second snowflake bridge
+- Issue 40129: Distributed Snowflake Server Support
+
 Changes in version v2.2.0 - 2022-05-25
 
 - Issue 40099: Initialize SnowflakeListener.closed

From 03b2b56f879879bb379cff8d7352ace1102d8811 Mon Sep 17 00:00:00 2001
From: itchyonion 
Date: Thu, 26 May 2022 23:26:38 -0700
Subject: [PATCH 382/385] Fix broker race condition

---
 broker/broker.go |  2 +-
 broker/ipc.go    | 41 ++++++++++++++++++++++-------------------
 2 files changed, 23 insertions(+), 20 deletions(-)

diff --git a/broker/broker.go b/broker/broker.go
index 2bf4614..c356de9 100644
--- a/broker/broker.go
+++ b/broker/broker.go
@@ -154,8 +154,8 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri
 		heap.Push(ctx.restrictedSnowflakes, snowflake)
 	}
 	ctx.metrics.promMetrics.AvailableProxies.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc()
-	ctx.snowflakeLock.Unlock()
 	ctx.idToSnowflake[id] = snowflake
+	ctx.snowflakeLock.Unlock()
 	return snowflake
 }
 
diff --git a/broker/ipc.go b/broker/ipc.go
index 30de180..6b8971f 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -190,19 +190,10 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 
 	offer.fingerprint = BridgeFingerprint.ToBytes()
 
-	// Only hand out known restricted snowflakes to unrestricted clients
-	var snowflakeHeap *SnowflakeHeap
-	if offer.natType == NATUnrestricted {
-		snowflakeHeap = i.ctx.restrictedSnowflakes
+	snowflake := i.matchSnowflake(offer.natType)
+	if snowflake != nil {
+		snowflake.offerChannel <- offer
 	} else {
-		snowflakeHeap = i.ctx.snowflakes
-	}
-
-	// Immediately fail if there are no snowflakes available.
-	i.ctx.snowflakeLock.Lock()
-	numSnowflakes := snowflakeHeap.Len()
-	i.ctx.snowflakeLock.Unlock()
-	if numSnowflakes <= 0 {
 		i.ctx.metrics.lock.Lock()
 		i.ctx.metrics.clientDeniedCount++
 		i.ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "denied"}).Inc()
@@ -216,13 +207,6 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 		return sendClientResponse(resp, response)
 	}
 
-	// Otherwise, find the most available snowflake proxy, and pass the offer to it.
-	// Delete must be deferred in order to correctly process answer request later.
-	i.ctx.snowflakeLock.Lock()
-	snowflake := heap.Pop(snowflakeHeap).(*Snowflake)
-	i.ctx.snowflakeLock.Unlock()
-	snowflake.offerChannel <- offer
-
 	// Wait for the answer to be returned on the channel or timeout.
 	select {
 	case answer := <-snowflake.answerChannel:
@@ -248,6 +232,25 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error {
 	return err
 }
 
+func (i *IPC) matchSnowflake(natType string) *Snowflake {
+	// Only hand out known restricted snowflakes to unrestricted clients
+	var snowflakeHeap *SnowflakeHeap
+	if natType == NATUnrestricted {
+		snowflakeHeap = i.ctx.restrictedSnowflakes
+	} else {
+		snowflakeHeap = i.ctx.snowflakes
+	}
+
+	i.ctx.snowflakeLock.Lock()
+	defer i.ctx.snowflakeLock.Unlock()
+
+	if snowflakeHeap.Len() > 0 {
+		return heap.Pop(snowflakeHeap).(*Snowflake)
+	} else {
+		return nil
+	}
+}
+
 func (i *IPC) ProxyAnswers(arg messages.Arg, response *[]byte) error {
 	answer, id, err := messages.DecodeAnswerRequest(arg.Body)
 	if err != nil || answer == "" {

From 36f03dfd4483922b3e7400dedc71df9cf2f30b6b Mon Sep 17 00:00:00 2001
From: Shelikhoo 
Date: Fri, 23 Sep 2022 12:44:02 +0100
Subject: [PATCH 383/385] Record proxy type for proxy relay stats

---
 broker/ipc.go     | 6 +++---
 broker/metrics.go | 6 +++---
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/broker/ipc.go b/broker/ipc.go
index 6b8971f..14db4d8 100644
--- a/broker/ipc.go
+++ b/broker/ipc.go
@@ -75,19 +75,19 @@ func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error {
 	if !relayPatternSupported {
 		i.ctx.metrics.lock.Lock()
 		i.ctx.metrics.proxyPollWithoutRelayURLExtension++
-		i.ctx.metrics.promMetrics.ProxyPollWithoutRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType}).Inc()
+		i.ctx.metrics.promMetrics.ProxyPollWithoutRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc()
 		i.ctx.metrics.lock.Unlock()
 	} else {
 		i.ctx.metrics.lock.Lock()
 		i.ctx.metrics.proxyPollWithRelayURLExtension++
-		i.ctx.metrics.promMetrics.ProxyPollWithRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType}).Inc()
+		i.ctx.metrics.promMetrics.ProxyPollWithRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc()
 		i.ctx.metrics.lock.Unlock()
 	}
 
 	if !i.ctx.CheckProxyRelayPattern(relayPattern, !relayPatternSupported) {
 		i.ctx.metrics.lock.Lock()
 		i.ctx.metrics.proxyPollRejectedWithRelayURLExtension++
-		i.ctx.metrics.promMetrics.ProxyPollRejectedForRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType}).Inc()
+		i.ctx.metrics.promMetrics.ProxyPollRejectedForRelayURLExtensionTotal.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc()
 		i.ctx.metrics.lock.Unlock()
 
 		log.Printf("bad request: rejected relay pattern from proxy = %v", messages.ErrBadRequest)
diff --git a/broker/metrics.go b/broker/metrics.go
index cd1ca37..77b7675 100644
--- a/broker/metrics.go
+++ b/broker/metrics.go
@@ -286,7 +286,7 @@ func initPrometheus() *PromMetrics {
 			Name:      "rounded_proxy_poll_with_relay_url_extension_total",
 			Help:      "The number of snowflake proxy polls with Relay URL Extension, rounded up to a multiple of 8",
 		},
-		[]string{"nat"},
+		[]string{"nat", "type"},
 	)
 
 	promMetrics.ProxyPollWithoutRelayURLExtensionTotal = NewRoundedCounterVec(
@@ -295,7 +295,7 @@ func initPrometheus() *PromMetrics {
 			Name:      "rounded_proxy_poll_without_relay_url_extension_total",
 			Help:      "The number of snowflake proxy polls without Relay URL Extension, rounded up to a multiple of 8",
 		},
-		[]string{"nat"},
+		[]string{"nat", "type"},
 	)
 
 	promMetrics.ProxyPollRejectedForRelayURLExtensionTotal = NewRoundedCounterVec(
@@ -304,7 +304,7 @@ func initPrometheus() *PromMetrics {
 			Name:      "rounded_proxy_poll_rejected_relay_url_extension_total",
 			Help:      "The number of snowflake proxy polls rejected by Relay URL Extension, rounded up to a multiple of 8",
 		},
-		[]string{"nat"},
+		[]string{"nat", "type"},
 	)
 
 	promMetrics.ClientPollTotal = NewRoundedCounterVec(

From a8829d49b7bd315db08713abd4bf7396466d8b59 Mon Sep 17 00:00:00 2001
From: Daniel Golle 
Date: Sun, 25 Sep 2022 01:50:31 +0100
Subject: [PATCH 384/385] Fix proxy command line help output

---
 proxy/main.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/proxy/main.go b/proxy/main.go
index c42852e..563b3de 100644
--- a/proxy/main.go
+++ b/proxy/main.go
@@ -15,7 +15,7 @@ import (
 
 func main() {
 	capacity := flag.Uint("capacity", 0, "maximum concurrent clients")
-	stunURL := flag.String("stun", sf.DefaultSTUNURL, "broker URL")
+	stunURL := flag.String("stun", sf.DefaultSTUNURL, "STUN URL")
 	logFilename := flag.String("log", "", "log filename")
 	rawBrokerURL := flag.String("broker", sf.DefaultBrokerURL, "broker URL")
 	unsafeLogging := flag.Bool("unsafe-logging", false, "prevent logs from being scrubbed")
@@ -26,7 +26,7 @@ func main() {
 	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\". ")
 	SummaryInterval := flag.Duration("summary-interval", time.Hour,
-		"the time interval to output summary, 0s disables retest. Valid time units are \"s\", \"m\", \"h\". ")
+		"the time interval to output summary, 0s disables summaries. Valid time units are \"s\", \"m\", \"h\". ")
 	verboseLogging := flag.Bool("verbose", false, "increase log verbosity")
 
 	flag.Parse()

From 9ce1de4eee4e23c918c7c5e96666ff5c6ddc654e Mon Sep 17 00:00:00 2001
From: Tommaso Gragnato 
Date: Sun, 14 Aug 2022 14:34:57 +0200
Subject: [PATCH 385/385] Use Pion's Setting Engine to reduce Multicast DNS
 noise

https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40123

The purpose of the patch is to prevent Pion from opening the mDNS port,
thus preventing snowflake from directly leaking .local candidates.

What this doesn't prevent is the resolution of .local candidates
once they are passed on to the system DNS.
---
 client/lib/webrtc.go   |  6 +++++-
 proxy/lib/snowflake.go | 11 +++++++++--
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go
index d5264a9..01990e0 100644
--- a/client/lib/webrtc.go
+++ b/client/lib/webrtc.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
+	"github.com/pion/ice/v2"
 	"github.com/pion/webrtc/v3"
 )
 
@@ -189,7 +190,10 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel
 // after ICE candidate gathering is complete..
 func (c *WebRTCPeer) preparePeerConnection(config *webrtc.Configuration) error {
 	var err error
-	c.pc, err = webrtc.NewPeerConnection(*config)
+	s := webrtc.SettingEngine{}
+	s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
+	api := webrtc.NewAPI(webrtc.WithSettingEngine(s))
+	c.pc, err = api.NewPeerConnection(*config)
 	if err != nil {
 		log.Printf("NewPeerConnection ERROR: %s", err)
 		return err
diff --git a/proxy/lib/snowflake.go b/proxy/lib/snowflake.go
index 34f8abe..f9bcddb 100644
--- a/proxy/lib/snowflake.go
+++ b/proxy/lib/snowflake.go
@@ -47,6 +47,7 @@ import (
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/util"
 	"git.torproject.org/pluggable-transports/snowflake.git/v2/common/websocketconn"
 	"github.com/gorilla/websocket"
+	"github.com/pion/ice/v2"
 	"github.com/pion/webrtc/v3"
 )
 
@@ -355,7 +356,10 @@ func (sf *SnowflakeProxy) makePeerConnectionFromOffer(sdp *webrtc.SessionDescrip
 	dataChan chan struct{},
 	handler func(conn *webRTCConn, remoteAddr net.Addr)) (*webrtc.PeerConnection, error) {
 
-	pc, err := webrtc.NewPeerConnection(config)
+	s := webrtc.SettingEngine{}
+	s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
+	api := webrtc.NewAPI(webrtc.WithSettingEngine(s))
+	pc, err := api.NewPeerConnection(config)
 	if err != nil {
 		return nil, fmt.Errorf("accept: NewPeerConnection: %s", err)
 	}
@@ -442,7 +446,10 @@ func (sf *SnowflakeProxy) makePeerConnectionFromOffer(sdp *webrtc.SessionDescrip
 func (sf *SnowflakeProxy) makeNewPeerConnection(config webrtc.Configuration,
 	dataChan chan struct{}) (*webrtc.PeerConnection, error) {
 
-	pc, err := webrtc.NewPeerConnection(config)
+	s := webrtc.SettingEngine{}
+	s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
+	api := webrtc.NewAPI(webrtc.WithSettingEngine(s))
+	pc, err := api.NewPeerConnection(config)
 	if err != nil {
 		return nil, fmt.Errorf("accept: NewPeerConnection: %s", err)
 	}

Oujla;U?NIzft`lM0m9O22CQZ?{_A+gCbjRf^=4aoZoVej?rWbK>$-D@}^I zQh-sya;qxy^iN9rXDlT(U~ooiQfJpdJ8P6(rX{{0&CQsdH4kzqyd z`n43e3$qI#Pu8Gk-3n>Q_-m168z1~5FHj)@(&XUd_dACl8yod0u)XkmO2BNKgv>a6+_CaPSAXei z1OB3-?tp&LJI?I~K`AZ0z6`c2&bsPGj<$MD+htjVvv;6}Ys2a^hJ1wxX2?TMQv!>T z$L*R4kO*;)%4`OmfYc=v?AdAgpon=TFfLuzE09qsGm|5fKq?Bzf>YTmPZmE>yx@<} z7;(dHX}v!G7}?;Z^AQI3;31*#SItZg(WQq1to}hSlr95vWhG){maY{u9D5mVt7>F| zB^MC3jPPT+hF|m9*r4Z3;2Uw>&pAd6(CtF`CN#P913w8<-9ssxQ}3;NdXaIuEzucA ztDK!Ktv`1=5W@rKH3hB&EA>!>1C~f9A|}2|AMB=UJ(ofcJNRO>`hCz6Ugu_ePKwyf z;I4iEJjldC%f6{DOgv)=vEiOBKKc6~{kMiUfMm;aS_XW*WQHKWiI9XRf@Y)^wxRkB z&r;L%RYfmdfIc()hRrlF8nCh3ons38CKC$&kyw%fV1`_DAfzVRE~?budK=@ok~2YyH$IW)-~YquwvVBUM;6{VH1!C#?gZ<~#tF zTcpKr0B!t&vXOEdfN+Um_W82DO85w#PIt$5n`#2ct9f}p3ia6ri5N)wN0H4U^;s3h z#y@1|!}z+qErzSKe*^Gxn~5r$iLWnKU$c;|IX(PqNsj7lTGKzV;iMS|^~tjQ`8h+~ zvvD%*_u>U>rRT0{m$z^GKb_gCFC6}kgbdaFp4{3@SRtqjuaa6G?F-(2SbLEEHPB0j z(Fj=itn#PL&~|&J_5x@HKU%f@v3dq8(t%D`I+Jm`j7GY%Ktn0L6AEYM7WcXASu{At zD(3{O!jjd~>FK51lwA>$cp=U!Gh)r^)!az2Syl9ypj%jCbbOU%vkbyv_Xh5jIwzBy z(?W*~42F7Ax-mwas}`Ugz^X`ll<9D#YsH>VZkk%Zb{2X`K-ob&@mv*FPWt#c?Vyan z31vpP6if;p{G4S|MOg=~e-|WELD%d(6-^1oL_p7STgt$;{kXPzL)?{-^WWSnnB6RE zzNfQqN9jWRt%U5Lw(W1Ptb7oupE{oer^y9AVPeD+yFS{0crQ>NGKQ}j%!&8eKa`u4 zF4-1zwq3rD-JP$?iOUkKT4Ui^jdFool9xmRYoZj`fn9Gizg`E^c#6-t?{Zl#r*?d! z5LWBe?Jw{d+JbK(hX1uToa8FU&_IItHB5tyB3mh+33{tLJ-t*QD+!hRc%-vG{>Tm(_+voP%{h-rsRTH&z zPvfup%^Y>b`HOG5RoP7)$%z<6f%?U3A~(KMpg`v%oc}yW-@q@-e-!*4G|V({-l@3) z(SG^u(aOw$uH4n$!1;OZ#Cqq6{$cc{Y5cMxt_gLh;rsml7`$~%JpXtiBH{@xf8GR< z`ujAI)`g#8rcAh)Q90R)S_+sfdGYbyt~F5xR2#e7KopZZZrWYCab!=p!h`fPG%8_{{8kufm&tvMAmMi-1BZQc8vAxMVTTK>NGVt)@Jt45-A6+H0eQj;CTLMA2Ztu0D1VT^G9vp|Y*Qa)zQXRw+3 zYZTNeK-JISr>ei<_sinIWI>xY1B!2L)TjtVeXfxAPjRKQ(v#7jb}zp&P{rMSr4S{f z^l}rmlwB8BWd3<;%Yw}oEmcFveo16g%?8s&pk-xw%8A~;j<@P3i$9I;JGK?~*^u z{eiM6DF85t%&7*9u2y~f7uWz0&8^`^QoG1U!cBl8=zn3^@yzJ(2luD90|A`r4Wj~z zU7e2`*Mp?}%__9di3D0j#QOQa_XAy8>6hkXrtr}Y8to;hr|o77ez^U>;F#_{yQI|V zL9*Y@XX9>w<5VFJFkJ3hWMWL9F?Z6i7FD4cG3t!%;XCwKZ?6W0 zUjbStQ-ZfO=+i!K*4N&u7(=YLpbluqa^}9vQpNi1hcMo+ONVcAKPWrVOvv3kxEJ@B zwuAdd?KL&lrb#zd)6C=OLo}Te%N0)%AS?a#c(8VGS`~0_{uf%l8Znye&^G-0ET+Ng zD2Ql7t(tRl={|1Pq1zJPX2T`L%cl1p7$>i0>Fqcupo6uzot)e%deVu(>f(&&K%JyM zT3}$uG$|kO$o5bd{JofIo$EbSDbvT0d$bnfTR4AZg3Z%RzZR1C>Ik>;<-x3%%EURK z2O{6F5Q2a!@Yr!GzOxE?zEGCj|KJgmxm6FiGTnFa8&2jN0_r!7`=WoQ zz)ghfz!BKrlY~3a%FLfc97=7r+N!!k>aO30_dQDe#lLh?yA$w9dgb^lQpQHKuFmtE zpF3bT{EXD%KP1$qc~vH0Z~G@J-ra2q-S?7tD3azz|qwv7?NFJSfab zLEW*eoyHv0@ZU6z1Y2eQY8TV=5cXpmc)|RF$ob5qY2*GZFYcf1|8D)X3pz zvhmo+54a~xBK!$b4x`zCkywBoV7{YQ`@moEA&L#o38J*sRh(D=*8!uRG>nSm=DDD% z49UWZgG%-IHG-M}qCcb}$zJYOwGuI0cVvEuRkXBu%jx|qv4Ybseu0OR{wJq+QqK8C ze)CGNc#Ah)5`2R(e=2>d(=QZ}D@t?bJd?W-Y`+|)0v~yYCD+BF#*|>N-fzPxEzi6+ z?%55?lxhJDhwfhgn&ZL^$`#mhS#iex$2rutZo?2&=N{hPi7R>v?(pomWG3bc_F5fL zHx+0M;2lv8`oMrT4qDWk8=TrWZqGx}0rTO4(by^qglUoY$-znjBI)<9z~9wJ z&DX#6dx0D)#W~3P)s3lrz#5Rp!BT?Y(?f2L0hMbuf#kfvq>n;|;vD+z4FNa!C#B%a z66rTY1`a!Q10I`(VpPwY{v6+Y6L<9e*bMOVc)JexL#~68F35vS(RU{XQ`xj&U{d ztfV)LC;vXRd+c6`_llpQ1qC!F+;dwe38@@EwQZF_1Gbb&*HuDn%+)Ed@HD`R?xbMd zzpsvNxj)?uc6e%&$-*Q|>a@`YaMCN64oM;=wWy;9VF|46+^#0F;HCje`gmji6Jp-70S1#X!KYP6s#ItzM% zzAOP(?%x2j{DCokLL#|FrKoTDFy7(d^SubVO~Zb_r@9SL<-^=$=Le_8KE_8c5uapS z04M9M*5To_ebkv)8F%#{qA6YH9`Q-3-;)-$nFzoF%}CwpMtfyQf|UF8l0vG(L_Oe% z>6SuX%F()h@)uE{f*sA%)a@T%?|@=I{ie^Kb_!uGgLZ6)H0UNB&QcB6>K@;8BCF9< z$gCK7ZH(50{6EINI;_d|?SHgL3`IIdDCmQR`LC^SaLKbADp;vR}LAuRbN|AgrjYJp7Qb zXOCAta~TlYrcL<$#Zkdy2kX(TJf#f6n^}B(4)7198S(4C68JH;;L(K^ZY)DnQ56K< zXVYhAwe^LUa2o{KgB9&R~$NAfbZLLdq!%5or3sTZp${XucnV>@Bunb`Bo!8#`r)NdPN z%Qmg%6mB5CgiW@!w6fg0J!8S?;ABd#gSMb$d`JJ9HgG7~?Rc=Gk>FN}0m4}!rWce! z(?4Jk0Jn@@J!eQ831bo-V+t3s{I;T@t=n2Z-JG>wVXe(I0dTJOxbfEdVZ_1U_ve!d z`&n;GtcKq@4&5f^0OMl^o`R%(;-3LF9F0w?2tIABi zQ1b%}%pYYR)3QG*qARwhZX#7tiDjL+k(2t~47kch)jZcIgk1j?V2mEy`KV-Oaf9Kr zj-$!|5G=}NeZ=f@j($DRH5>J=8BF_mzDAR!Y&fDRqKm7$o{|wWY+$QP6*OH(!%|fj z8M+>@pWmFFv%10*NIog0{{;fMI)Q$Y=pA0dEwXa?tz_OSVp56}RE`hvxi04YYx3t3 z8i}DlDqabf?7b_9Mr<3-yB>)8k9T*GQ7tvQ#)H;KB1vv1ikLog>#H8RN_065YupbL z@c(g|Ax9;PQ>D*+4-oM-Jb?J=IHgtUyY%0$l{t-h+B32Gyiv{{ z|3t^o*~)$qY`Oo#6*=rNZk>3ITZ}a~p;(FPNZ0+xffL!j(soQ#o?e&q9eD4muC^P7 zX~Yum+9S8dOpDN%gr($1fLKVtpOD50Jh)~`cP>YYN}{fgA{o$KxeL!pb_8j>b$ou} z-JQj~_obv!9t~f`y9D}^n4Y5!2_dqISJu6eXFWhL=w@?D#gNRjkEL&P%WqZXdA~it zFxzR!7QIfcB7zp=&K{0~HJO@4|LEa2_u5HaJOr!Izvz=f?c`OzF~CPTjvLvg=^5fk zu%LYtosk0Tf&L2eyS<+NbGMOYmGK>uU4!Z)%my{n0+an3QyZ10*Rk{`nFTd zGs`AAY~Cl@Z{SygF8u-Ty*|?BSy1fdS5DOvw|WjfMQ>88_zNE1)f#_F+Rv>Re+-X> zOrFavBod{nL0A;o?fIBp`zQQ`;u;2jwJvLvH5l5V$JI3;KfyKTfzjf-mYl9_@(skX z%MepMr?CwaTFUI6;%-q$Q~`m_AD+wY24DiLf-?U5LJhNOhy4wag|y25SN?sIDSmyw zEFwzBdhB_S&PG{Z&+Xk%UI-*Ii#x^gHu!OfY{f9uf;80Yu(!v|xEduZg$&opSH{mx z-AGiE*mtAtPT0#Z#4qIi_9D;9O5R_5IC!=H4t`(-RF?eL(Z2=YD_aWz-cFb)=lVQ2?b1W=&V6 z`MgZ5JtbxOLB$2L!`{k|u8i|QjdVM@WGn5o6{CSjoOe74=UKd)vje;||e{(=l>mwxTn<7IW(G`?j<}klI zllRjyUOMBK=cuUxhz>=kKX~sP@Wz5kuUp5@ujZi5P^>;6g;(*rf@%|FWk#WNk0`e& z`tS6Tc*t?rO}Y~|#wm91xbWSw>!m0oS(eqH;`T8o*Y!!q>02tT zA2g`cx@|UGs$9iBBHd_%!eI~P$YW0z+jCOZe}y;7qlPs}!c98eP&;@uM* zrOd_Kqy`-K3{lcE4^n^UB?;J~zAKh3Br?H(dsX*uA$bJ4_*Jcdd#`73o$7JEYD&I8 zqSGjlWiD*}s=H;%74D{zNeG5SQnbv%)Dfd47@6oK+Vu3&51H(P_Fgu z58{5R;(&5+euv?A?l-zYGuHL4eCeC8^DB~2qiV|iM?uM!Bv^RB`v*i98*PwZ-bHw1 z2#0G01IeUiE**;6zF(<~B$-6tNK4Qz(*;F}x`5&Xsk=cheQO?;b6<*Msy-fS_?+U> zQ0;_ie&^544hp^xR1}B%x8MEK+!=~sBEuOA*mDz9bh@NF=g&}RsUe8MLiTI4dQb@S?0}|kiLOoMkNV}7;ho)FVKPMiSw*L23=AQ^L3|?VepTRNci2(qw_5HZB*sK$#>Q&Vl+Q@ zfi%W)8$*U)+75ZRD#MZmpo(Cu>G1LH5K9wrzKS@kJP&C*SnW?`CMH7$o{R&@CvGCLr*P^ZPaQjd$qWIt_}h_gp^-)LTJJu*nq}>S(q{msDsTeVlgAf@EkNS3t*?InEH`#TVHbKHWpT z-UPb;E;VvclzJHR-cw47GXKa9yP|jWs1Hn)&ZT0(4pF8-xu{Ef+@ngLCP6CAg1N88 z3*K<5)RJcOs8Q>4%QUkr^{G*U%p#>G0w2DnJRc>MrqQ66k++yLPAV$vg->wGrv?)C(RX#(4+U1Tlt2{acWMW`=V9K}*9eo_Bw9TGp5T#i3DkXloPBQ2i zAi|ylSZPXRLORD5$l|_{qbzpC6&dg5ARE*5zk)P5e!7$@mHkDAg&-c~^@|w23tT&& z(b{I2*4Bm)L(gTlhM7HIF9=XoOMIx118ec;!y6^FammR%b4B5~EDJk9tMV zBL_czGV~qb4=r|a(jeTQBRW310Mzkhm)!*S*h@nqWD1=9>40mtTJmy73$=#HSA?ZBVJyRN>$W^^H&m-I7`#73luU)X9Vr%MWHRc*#=mT+JioK@ZkfHGd*OJU z#kRyaa!53k`~^&<#4{KC{@IungcUdERoeyE2lznIRWxzKQgB0*x%_yGu z_?A4@r9@#TuA!)XDUykht&g|O*l!eDutg_R$@Bprh;`JrYU%|No|#4<&&1nrRDZg# zX@q-Enp;uKGG1m`0BmEF`9stiw8C3JKxme1r{Lur%-IeiL1ji-y$5TBUkU)oVcHX& z1pM143&A^_V#(fOx0;=HeeZU73j!Uxa>pTbIa7Sb(&@l$>4U8W`<1^t_chu)#nWS2 zJFa^e^))HQxcxvW~Dt>;z{JLjLSIZ z!vx8zsm*`x2hCC+w;1RscMTZyj)mKjt~!P(?|v;d2sRB+8KTM#-coPws2UqWrp7`C zLcTZ#wqZn(acncJE3J%DfOO$G8{&ZVU%mVt0F+=RZS3}c%e|eMlXDPO`73bJMT4X8 z$h2lH3YtbtYAnh#+===E_YmBA+zEI3ixPbU8QBvdlv}v!o<%c@@3O5>+){sUIdwe$ z{@{7v{Y^h~9}?Klpy$aWiExcAnj{&cVHMx4Q5J9IO(8q_YFNoenp^OIzZpZd%q~ zd938Px2m&yEeg=gm&$nV0o!W$MTU627F0dMj4`O-L86Fo%xa!Z48cKWy}C%Yrow2Rf+@v~%>CQu#P>Gp#DirttO4Uw)ZNNh zaqYRa%tSKiS*bFzF+?l~gf$sFh^Jnb1iOr(Q{X|#ji1?R`j_TDPsF?sUgU)-&u0QL z>vnuI%bYqDqSWvHz{Cpvl={%$$G1vEEET6HtR}TH;$8q0lEMnP}nMo$#s9;eMwvfWUT$SZ7m3=Id+jR9O z_;U*Li}xJU^zPH{0ZP+nmsF{)A!JzvXvN3<__Vpr(yj<k_zfq? zy>Sh3;dbj_uoqWs!$DK}M-hZIAoRA>ak>b{*gzm1qugA}KvX#(<;W6hEMT#M8b_vL z-kftDylw#lM76|hIhKX^UU%8-lTZ$QeJ{AIkF)$XEIrNU#}ckoID(cg=YiR2^{u1# zQr0>>#rjC*Zmm}Pe}jT6A_czBQKq3i+{sLXiu2x4JS3~yvF*cE+gx*~AJ$L?Y$DJg zRGtPIgO$$6_n#Rk>Ed(x12YIchWab-%0d(^k=|BnYB3Q8{VQaG!; z1?g_=baDUa$6xBV@sw{mGdw5#@hSnKiYuWm4^(QkdZ{SN&Y>86FSPH{2kUE&qL(6q zeZP{xi5XCUl>AOWXwLM_l$fyTZhs*OUbVLoDJ~1~sUkPL>+iqY>)jx!oAE?PsXPup zUZgA!o)-Als1(SnvnWs6-(4-M*H*#15z|v_(e6ptSu(09Q5nf=qg+%Ymw=St_^|1D z`9;k^?@KJzGAKieM|3&1O%@bMXut2)d#{1&1KE+$j)gwbwXHm1cquYyv(iu0<)oaV z%$KLDRTkxRVasaZqq_AQ+1w_h=f4V{0Qg8Tt~o)pnw&FmdMBOA|4Omm#iP)jfayth zM4UxsD_PEYsM*P#=D&?b6!d^4yzb7W;-6|Hthje-Kb-~ET8AF*lo1{@-s4ZfG|B)p zyE*&02b;0dr?c2HV%`}$#+~xAbG6jTFH10uivQ$Id~(bA{`+f6SaVV>MOYIQ);!s? zd^5G*Q^A|&RIn)AWP?FzfBt3X^}}qsVZ`E!xzLOMtdE7Me3#uFR@X$gy^g9Ul=tM_ z?!4w1krNJu^7`zH1>;IxMEbdsUPxb;%kaBQTS3%U)^2WAWvNl(M(x zHLaNR}>?>r;fhwaIm+4Nl2OLqdC2qeQ20WE6Y zNWgl)jQQ_N9DtiR&(uMCbwL0IT|QS? znJqMZymxDRhlzs6nj-$_YSiQG*#A7q;&9~%o@Emao41{eZsM|>v#Flp&cU^>QQ3j# z2T#0xRj;Z1va)AT;n)fh<^kD{JzxSw1z4`9UZ@%cQ&h-wu=g*i+x}9YD8pGhDvF^D z>Apk1Mx&&8fUMk2%{=*qebV`&sqnT`@(xj8_@Ntk_qT!o(#l1=wA^pW zNv}6V8UUhmqfHSS?kSYYP&VcsLP<+N&+=wDUGzMFb{>Ef@gAnjO52IOF={REgD z0pqxlysBV^=;(G6pCP5IrAum39OW9zW|J}H2?WU~ z0bn7MbdwzV%692r&!hELxM5K=AJBM06q*AT^0*sCQL^yUBt{Sz0p$acr!f(~t1k^01g35BQs+Xn!GrF9mtY{X| zt?aWklaa-idl$aYzI-rpRr`8$xCkp3!g|+{yNc_C9rt7EBN$Tsw+`C>xmYk^7p}Nh z5~h0^^TeUV1&~3TVg_sh1iUy@Z=Qt%ajzo}Gq(Fi#BKR2KHDKpyX#9-30XUuIz#KZ z#EHW$&z{qh6!QrI8dHns4@h_lVvR7WeGSrkvJsiwHPO;TS8&%YZ+6%&RuW>?Vd%)( zrBT1K0bY9f=$a1ORMNRlJH|jWkp~hlT~E>C>>Xl-f%X;t%!h zOGmFOCr(#cyC(-WlKp&@Ri&F2GXisa^6U&Zv!p&<+O;O;d>3PymZr?5b`V>=T9!4~ zNuv0BMQ1~~E9{C9mp3qf*)v#k8yAh`SZ7zf>iuhGobursK=PO$#9!_NH{EuJJQ3{d zYm>7D-C({s8$!>;PTp?@pjCc*xc~+41Q=lfb2GdVtEI7uf1U7kpG-j>d9WN3NKX9S zWZMJ>{RE8C>yP86t4GR@9#UmYeZPOgkg3&6@n(V)o40k`sR?~tAqslCrit&etO`68akgOUcH`i!-5@17^xuD__49=%t&@Vw9;Li!OD-9vK^ zMtEls7qEgRmgU65==b^bJc_K_rDp`s=YI#$%b^{Ea1mARi zB9gkX8}Dn>nfGc~mp^3dUNfZJ%9ss-9h;lkH%ECQqOUEsd2&`8b!5TttGOZcPor3> zrH97mU<&X#$-tc~PN+QdL1cFWPFFPL%6pf<$5OqN*AUTGls8#(btu8jA*?sy4)5oh zKWZel4QIRfjD0CA0JtW^G4>g<+cW#G1|rE6td`owh7S3-4?Aoh?vLc)3InWMUCgk$ z95Yy-{a0%ZgwPNu(SO115hcu`J0|sQMOhXJyhG|A@)BQ9(mE#V%#OsUdX;?4roAF? z=tZXW@KBxaP8URS!dOnK#PvEHIXi9Neb0Lx@}=9loLS}=m4>=Y#pUq`7N8=jUv26+ ztT%zOJ-+i{*vt~XL-Obh$W|T2D|8Y7hjG)>2RF?6!8OX0zjvPo{J7p zDy_?HCN&=RByZ!6-YXxXrq9o2rugJOPN6Om@6Q+eDeqF;lmlXY2ia^>#s4x(iEN~| z;~osnYe;{q?fy_IV@hBdfLW0AY%Gnz=EGL8(|ZNp@ri@YE~HrCUs~?=x}dmAx&_CU zqD69ReeJoQl3{lz(gVkc{HnIVUO;tb$RmYfFP<`ciGMtn<*j%EC^zO7u@ch`yGYB6 z)B9{m_f=Br(ImR#$k0TjG&Rpi;uW|0XTa=5myNuC>DiAUUeJV4LDfga3(g&js*s60 zgvUlro9u6wSm;qh2uk1aU;aQF;ueuth<0*y2gDaS=gdLtLVswj;@E~JP%H+~j3J5k z($q~;O2w5_c9z}Mecocr(8h+jv7tbnt&`^8eCE()fT#ft1VT8UnX&eG)B6Wg{X4Xg ze*883QP7OlJbm5MDUBRs(CUHUG{3OVfYMHyUqgJ~yp+qCE_S!P+kYbJN5JIAGK?c6 zRhP_p9oX}V!+zF)PjAj_c^`gif&cOYzn$)^p<7MZ;V$l}rzpuVRrsSKJ3crPZQMvX zH%$PP9PXKPhvvg6(Rpbh)KxJ*OnALtr1nATREm=w^le@Tr!27Y=^=6tny> zQ^(<0r5xh1;uxYm2zJ9Q+eFo{eA30aV6(DZj_+1*u>@c&5ROi9_R6c+X27FLqF+Svx-;V91NJaVXwJWBG$t_yAW=vG{Z0tS!eA+@IdQlrdRkEt}Q_?J1>$fj%?O? z(XF1(>D=%Q4KmL`BmKTJT0VaW@?VJ6vqc3?{unjR5z~Krl26jctKUY_aO>qN-^LEu z9e2%$A$KaYTkXPIrDx;Iqe>5fqd?Jz=l2GfUY49>UJ^#sU2LWjCAtkOx2*P|jMMi4__4(B`!ZSP# z4E6+}k7A4|hf4_hp6*D}POaU=nDQCBDD)q6yCNjSgkcm?5OH~?5!8zKY$SzHr$m2J z1>u1SZVrExq6C5Gk{H{Ik@y)CM&H$&XyLc8o|~pMLq+w;A=gYs$A1OwMC(mT5g3x- zpZBjZ%w$3iIyJirpF40(z)A3$%7+816Emk?B*(rM}4_G}pa^XWs_VOf0d9a?5)~1)BI`6-88@dvB zK-gbg@6V=}S(?tG4VO#m_dnS-^(n!KjKy}IX<8=L);gOiX5oPJl z$knm?6y%)^q5M25X%ZRrhm29FTS}p)AEyugxIcYHMtaP`JS3osC$~ewWiu zbARQ%rbN^W=Y+POA>o6o@I8zyAyhPgaS-aY9}+!^lYmMg`|{f5DZhEyLq+_No*aKP7_i zg`_F#=Joy?^}j<;uk*p?v7CgWzP+efT@}h2MUpqUx6>Y0w7=>ZYgkV_2c?Vs!kDb< zUWe%wY)E#o2+Opg?9PL!H9kaA*kuik2ogX(=cKkcL#G?07k(Im#(14uNKh1SXe7^C z_en^7V_4fv_Sxix%qle(-Ub+ujPalz5wBC+6_C!52yh!??x*zWVBTu8Y$K55E$@(s zJw3ZREna&&0h%FD@q=mWLI&ldl#}Ef-~f7xL6=#5e^2_lXswWLD&ZX7(V@ub3A#$` zvsS;>W}D#72nCC4%Pq#a(UQDO5h@$7bur!FOK;oat+;M({g*La<|ujDfW80e^NFac zgslglg33PMUUxrpi+lw(l?fJAx5$#IOW=i9pGC-?P}{i&%)g{;l5Xp=)mPdvkJ*G2 zsGH=FB>2km(vjNXf`qeuIy;Xjs-SMdw&lu=pji@bMHF0R0_g!jhr|k@IrfG7cAnoa zfvMymyuHIq-tTIFGe*Cnw1;I@vaPkN?%fx~JU)?8qOH=pkvddYg+uw+q08~sD5=@u zO_I3Mc2>+AMDeRM&TW;E%^mCRg!mo9lz$ueq`r)gI=jPeG*L}2v%K0k~u-=-|vX&6@$Qz-n=uvf3lw)m|=^wlvJMNwK~-p{R?xtAnlSxA}p0fOdp`(V2FZ+QL-YgHDt*0wC&9oLTJIhQ1SI}D!26QG_++)NS)1( zd2@-g6*~#(6OXjKAg_;GbGnkx&%Ijya`U*0qQ;m38gK{3YiJ~wviF0JF&tEZE5lUc z-u|yeaB7hv6t6TNE^OJg2F>b*7t%@iMCwf-PX|21LGx`G{r~9I^0GC`U7W@t@#QLM zneB8HFP=E?U~r>*r$BQtJhU6+U^5fB$eQC|RsCf+S+0W0eS9uDTv_ID`D&F1VMUj!AQ zWj6>@!OnlMq8!qG0iBJ$?h3iQ9}BFinw+k8NQ%agZ+0T!L5C#RmF;TVPl~xEMyBtC zo3Oi+L4j8a$kMzZ`dX-ncx z`FL%kT)~P<%K70z=w*Ip!Z9vSHmf(EsHwPOQRr9k`-}Y}6%Lq5;5j{-r6?7at)gg7 z!MenIbGgOv>84JPzMNvT{pC@u1{sn7xxS8%6)5RbV2(q^sjs895We{Y<r;A5p zH!$7XZ$#^LENI@l^%<Xioe@z=ifi(b4eP~OWNf(=v6>OnhWnz#j*nBx7j9jj z3$l3ikXBK^027UwUCas+J~=cW@7>^j`-9}tf_kLzYhkJ&XycBcYgdeJ^rg|{iiMYt zwP$2k$oK%am9n`x*zKj`&_2V|EW&`4Kt)A)zON@@BfPI}O9w&hhh8Pab|1csHe$Z@ zlJ+4>M$aP~v#P&(PIvX}D>)>`-TGkah#?1JwLX+bJg!7bKa(C$f7o<6-z)b#zrYfl=mEDxGx>{ z%C&brtPEot5_F$Syuu0LKHc@7wF+15RSqyDbJ%%6Sx}qzZOMRQuPZeYc08B^sf#A5z5QW zza`ygSu|-_L{jgMS{}|%F7ryw1RV>DG0iMnQqrjYEy76ymd?+epPlSHsL)G+n=DPOnkd8>v-%GikW;-=(`;Y}gaNvqROWaydVjL4 zRR-!d&loG)83BVC*{Ka#MC}%jh7eXIZRb}c0juyiAJgl9e9}QQIgVWZXJN1qdXf`XhnSg4Z|NqLMfm>lV+_LadI6f2WM#ii17U=65&j z)a}{JznUH*33Fc8K@+Ia4aQzIF;#~O;QDtXw|JnP-op4IQ8^N>k*w*tcBpW~?;GL)iQVQte*h+oK?p#8Z2;sKR8~FkrINF` z+J{q0-6R%}@$yK>ay3=tcser!5UfZdz0=qBy#;IisQ#DHb714$)ltGKBP>-{Oab7w zBX?gmK#O9x{-e)QhdP>FVV@cBn?}rPJ`Mqlqz=@s{r?#) z75!wB2rhRWh_{%I{QW+Ds^M_r*80zK`;Jt^%1NA-7yZE#u@d|1a5=>9Tf1#!#)Uu9oyPjl zRRhkAY_rzU7%p!UHlB%#Td$tz8{Ti=+)H*WxQDCH);;0UBYRrP1jZ#@_+z^PutKj} zroTn5l%1P2;N4jg{&ChHO!|z>?q-7~$9A{@ zOsQBI@pflGqmg(+pBnHMkh>3NgRbq5-SfOTzdmT#0gUq@Gh>g`_Tsz5GYer`fQWWr zG`MR2+~%Rt6XA3{+BIKclKD&?#r#=`G2l_k6{?GcODU|B1zeFsU-uS&=SBpf<(a8( z4IF;dz$8n1UAe1t(u7e)oIJ_Zgd^x54Qgj7gmtN^&m^r`*dhY5z`VJ*)+Zs!uModY zm>2P6y`jRIZh#>=VDKRMlsJ@5h@f>ivr|v5c9rc(N(0xTiwH|Cg4YvcderQFe*Gw0 zeCeMPgRv3wzwB=lwG6caKEft0Wis0|PkldtIbtl(LR8k|@_afj|5mSeqm{mRs+9$h zk(KPJJ)hJ#E4rKl*D#do-K&~bOUYL^tnRt!Y~Vbj(K`95rNl*IlW}%2B6iBnKJot3 zhkyGU0F`+N)X#pk{k}JcYTRH{)!TS8Y{<(CK!RyG7VL~R|yAzM<(eAV2 z1pl24Vfz!o#A)8jZnrMo&7||j&&SNRH}!i(q@azC!GfFPD1Q>)0-Fy0FT0{D_I{1T zQdX`#_fk;p$aacfhi(-6Wtt&+GpT5U%;Vp2l(#m~eo)(XU5^ zj0k0i1&(Z&`en*P8=f&neol5k)Ao{&#+q9Y2Qp!TeBLy?j6Bkz7lwv)bbHS-qG($h z2{im?xR-5_6_)LlLg+;!T$C5w5^`XC;|F$n0r7|`{_5iu!BjXxkKXteg<7-H!8;CA!;VR;TY+uZ0dV( zwC}Cid#^U_r1M@q)fZUOEFSU4`LbQ=W~Q4eu~WgyLSc!3beG4k!J=ah`t0M_V@{=@hV6tWP>V zw8!$3J?wQbh700VxUMF5pfrE3wIvnRXv_q?)l$-(*y7z=+uw?FjMA|HJv(=&Hs-12 z<^XQ8QbS*>ybmRxGir?yOOiXWY_@ecDl*9cqqbH!9I&hpzS7U#$%KTf;__XnJs^u=ZEY zHtAh`Yce|kK&wc6xn9`VkSARWsNouwhNJ)Gq-mbh)AWuiEf#k1yZ`uSv_^16cDx{W z5_77@<|>%`lFRQupQqYWd}e?a5nxJkkw$v~BT{*G{T+>xsxnR{g z$RgpygHSnqqr(xfQJkrRuw1e!%#W1V4gUne6wvw~-(F#jZLoX|9+067*qbwQfOYh1 z8*jwTis8jyC?%Y{!o*N6ol4AC==qfM| zAnhwn#On-#`EYIL-F)}b&SsI~eAh=CL!aQ^ds=cvz=S;#z}@x1?tmWJFp`=JUaU&> zF(Q)H65r>V8Ae~lzhld}?N+*96oszACxr)vOZ)vaB*g1fbsRj64$A&4OC8Y7&#V1| z{A1RnRkTm+$Zn_-MYU@`@4?pAOQqtgKAj}5Bcep$CldvDd$pmi)j?;hB(nv3cd?cJ z9Iqhs=|ol^E04nlXQKpe0)vb)q3=Dfd?2{tzj_HbX{V5!7E}>BI0muhj)Q>W<|p7^ z;f1a4X#2DF&XX*}^eOcx&n#n&Gkc~Kz1JqPKBu3UL@nfNJU-=aIF<5#;IH`0IaDuO zHRh$wx#_lz)%M9BHz`k~o8E;4wg)c1&K^Dp7oW_g$ZQyKC zEZ`*tTwr0dw2}W{Uvq z=iG3QTGccafKuL%7EjZ`tDUVqQX6yYk9*SET8RTpE^QH>1Ge2Ut@?R}#o;X4fM3!1 z`M%Ap|F84i)*O&AuAWPg@PAd81_{2T3 zKw6~nx8oC-p6cB3sZ=n=Cmp`ayGI`Gh`3TME9h$HXv@)1{54c90*1XJrh^L${N{*o zu(Sit>*Bk3hX!bgAMicS!2K86=J6eXB4Hm$qA0|eVl;D@=ByrBY}58P&w(2S)EcPo zK&k<|N9vcFs$jl*#IJj{5)UmT8<1P79Q8o#$4MrR>ZVMXh!k#zM^3;a(@&(s+hhd* z`+n?iZGC^KUm!x5Qyb~fOeEEHl$5DtnL_OJEc0Ce00kgQVNp2mpQU8J+zXPq$06hV z5TR4Ta#Z8FejhzNLEs5kh7!eV|2ZF4tY&pxOg`ASu!?a^)(6pst8sXvW1N*doUt!9 z6>>5)xuTi58(aV%Nas3Qx71Yp+DCxEa7OMyf^j^Nl}=`GU9gL4eyhA)S3a$HnAs2)!Va@~!f$78{V zTRE>^DC#Y!Xk{QnRV6^)_pUFH&&f;(6ch|(itce#614K>`6uFc1~-dlT?71H=Q z(#>&WKo`v}0mT0-1xr`7u{3>#B~A(x_gG`rW3N39SpsY?BZM{Y>iw740k2cq7p3Ql z{4(F2B{Mp}Azo*N_$Fe-S| zt;u_d@O0$lgBKVAJinF;793WO;|7CwgXq01QagD9ZU8rRVXsGAjji?sQPt0{$Q|>K zuAvM&wNSAH6$ctfbRssWlgRsO;Q!-~YTdG(b;M$chyXu-;F-&3Jmm`*?h3~-9N{@#>HE(uT+2Po8KDSH>5w>ee(n_4@4Ln zpSmq%No$9H@Rz|Q&v3q*z_tfwl#4ga11~siykaDLAo$kcZ ze&xFS1PpxT&o^>-QI8%ljCqCkei-MK*ehpXy?yj-WpZpRo(tQD&4X1iy_W91B*-Ge z>ar5F25CNY+H&ok?3kP5#htLvJgpTDkS~`dBzG)amO1TDx-65_m#plMNwG3G)$(jM zc1VPAA`j=o1Z@xUD}75CD}9}NsM?;7P978s^^HvKyzMLSv|koHO%7a77a>g(5*W7- z&ZbU&0o=v>S|ST;+RL}J%8%BBob;nBeibaUyv4QyxEz^V1iHS4I8w(T{%)|UCTVza z`O8$zc4i`aVNmgB>EeTu8)wCEcfS@J|0A!3PNmw>W7o^1YIbPy30b;-d*lDS|9`!- zqM8`6%R$J+PtkqTeDzjB%D&h5prSuz3Lb0ClSK&a*Twfw*RV(*Y!B$a@I5ZWos{IF z>l?%$!<`~{l^-WNN3ex-n(_7+m~tLkBr2yR9pr`?Y0JY2CBhmzO$ zzLez&ob!fRIL?p5PX#I7#^*HZ8yA!w*O+wheO$3TC2Z_)c!Q~eQ0N%BeJiS?t#fot zzMy`|*1P0_SHZ1uW)T6fEnRD8v9SMx|3O6IP6jw+q)Eem+Sr{1x99+%1gqVJeKSe?n(1CM z0lx#bwusTHEoKwtkGed?^TXB+yITa~~yd`t1LH&K?aBjUcgim0m zDGR3-zKC{)jGbQBXjofHgiLrkW8k=X=ByNyHtpr{`ZabTZc<}f>HH@tMVb8N2aZ5U zDAxoV*1PWkugq2LINmZHa_7na;felv%(DsnVy}q@(J(o+i5Os4eJ5Gw3cowPugVF7 z*QL5eKEaoW z80M)Mc$2V|JTZAB{;bcTamfYgfq16}%X9ish=jZLd~kV;EO-5hO=d~++UeFjy#V(V zA9C!)t}VUmPfxt<%#+FwJgT7Sefs~cQ1kDYU%>ASG?b_v@mJmzo$ku5aC<0#+Z-_+ z^{J7)Q%;cWDUT{`r7P*9N;Jqru=k~>w<)|6{ly+e=d24?oB?PhvUlV}N{uk%d2;1D z;V{k7?JO4 z`j91xRd8`RS8H3jchG13?dyct0>Ys~&Fkgu zwYEoD_)9+Iv$h+#Z+)NfvD`hpW2y+Bv_LL01c?AS6>Apd6&eHzM<9*$<_YNNll zs%6TwAuY^zi1NXaH-?j)fYEWWzkD5iOvz-(3Z+#El*y@ zQ|$OUm4|PwEU8CKo2h6)LrVhpfYa$l`_;tZ)h;6>agh#Evs8^utd)yLEBlM%`;mu0 zK7iUBEl`WJTOFSNYc1aRQ;Wajff5Gxo>l$SOf8010uoy#gGjwSFFSm-fx#P-zLgne zRlwY8=ykZUa)nQX3;AMCD3IuIyRdU+vh8EHGFq#A`Fmu9c*EkT!TgOumv5%~(QDWx zV-v!=L6di-z3vgby;=`cU9g19R5X-HN$uqqKDv}J-`h3584&g^?~^2V(MJ2bU8#aN z&7Mm9*5Q3Q(l|PCf%N2CA`KN|ixbH#HA^sL#U0Mqm3u6fj%GiKlky7@$8jA8e>t9d zDhA?!ge{Da8R4Bj@-XKUmmI}Qpf*+Ei2t=V=Ty#@t#05COKx``z*OjPQut?B<#t#8 z?DV`Cj(@Kl2R4W-PU|{;;UOfRc=mZ3vwP2Vo_>(YfRnI2I@(*v?s8puVw=VC37#kf zy8%%+-F0f`SrpSQk?b|IH*>gr_Ln|#u`jS^hBfeTDHCdRAK`}T(G8!!^}+L1$3v_k zYQ-^^=_BNe$MLglAJZ4ohoB#UJ%y%7aSj|t^C9xUrN?x4@vf-Gj^Xl1>R61^!q+sV z_qE5z@9L#Qu@RbYbK7q;*7zI@D)|akx+s*PH@lq{ZjlG~L+L6B?Elj#bH1jHqs~uN z%TvI^l(-d_qqrz%oXz6WP7hCgp2}Uqx*5!WRaVYYNId*)7$nAB7vMQ3V~?Q;)K%#X?UIRtiL{}eM` zi^>X9*DlEWG@*7eaYknntul5Hvr}EWb5DS(WB7a#{v+Ec%rZMR)~XPu?FX-Lym`=m zKb$41fOXOMsNHhLqyp!wbr!j&82r!$!*!j0eoQ*11vHVTIGFG%I?pfG>$Ef%5-Qu-OedeeeW1n zXjLjDx-Bzp=$;7<+fI2b&K&LuuQod-L6}=>sE-lWiW9;nb+Fclsil&?K6!F;5P7v_?Q>!5ROTty+ zg!1*=?+z{m5(`Dp&1Z_YSqKl%rmpO^GgV28G%6>EcFV%08P>+VS5`B{5Q`g#u5R0i z!Q^+})4z=RY=;SHTP})H`O7O*hjL-Poirm62b^*xIM<($|7!!>851?hH&x4|8QE6N z>*VUt6(ZovTL zhB1pVZgEtE+cfpFVnqIF$6myZMG@yc>9XZcWRJ_n)tD`lOIhCy#frx~d4M5A|q&wvz%ryAyubt_D#yntC|b4EvtYP@L~Ouq0afacXi6AT%G3mwi6o z5$a1u!w<`mIIYdjm_pXu{{Qz*5IBA0)g~VB#OE7x$q}Vsr3r!9 zJ-qQ*+GsDLbWVu*#_}^;A!xepjL$*kQh|(iR~A}0KzuQK8R(T{hDADJow`Sp8j@aX zhr8WsJbd0vbh@-*Vt;#`@1a*jzRt)Ssq{dc$io_oH8PF-yuP=AS11H%%Elm4!F zC;Nk1#g4Xq<%+L`0#1Bi7ZY0zamMb6P9E$uu^R{PR7Z*}eijF0<&R|s{AR#%To&wN zzzUMeIRER~@*j5{;EON1&pZ3(m&ZNR8$Idu>NVa%$_-44oR2@uUNZ<448I%sJIm!} zfjHN6r1PR|`z`K9189JFsc_(=xm={>j-!*~uaXX#g1pBw#|tjPYZiJt#+ZX-W0Oy= z6&BYPolXw(vkaXgDz#5i5?)Q0;bPPnnF`-H%8YkAcwMca?4 ztSy)JmU504Upx2`zkSO_WU+Fm_P8Sq_G77j%|HPzyEg$C^~Lpoj9->v!tI<8p6*4>-3=IpaGqDwVk0;95TLW6n93u*LVab3daMJk0SD z@Yru~J3qn%Epsz<-S%%^IXE3-52cYZ8?@Y5FcWi0-i>k;9d2O8J?wBgO1G~3NF}bt!N)i&-D7P_V_?KmkSEWH zci6$-_M|2UNj=BSU+D=c@GRc^alxjc9|g^1KpVm=;s_>gU1-4AL-iGzJpB_bV85d^r-`Z z)0#*!TLx2;(cT$9K6_ExTgB=M>aBijVdgS%U&+FP$Rlhq(Ko?a^ohOXPlo_eThyyr z!B}_0DI4v^qZOGe>{y$GBGysdZTSjG0?>}SM6`jn9kFJs5$30hV1gO9nhMM#}DCEje#F7{4N@~ORE)2ec0_x$}tuLxS(;K(c#$6G!WY1BJEsYX6N zo%>eee%5@WO6P@yz-EKs`V97`GkWBJcRukxeaBzDw@&;ypo)_2fK6+3AC{M#WX9Va z15ao7C+VZjxD_;@{x09seRyyK+dm%;`Nz*(@cT1sBdIP*;UR6;-#bUIUP4cPd7-bD zdC%O7r~I>^L7-c@@F|5X~IJ;<5XOB=LD zR7_7f7?Eyj*s|HLN!1N$qyt2>U)1X8|Na*(D$3LgFV*9=0IjD7VwPf~ZlBWGA+Ra*UhWi~KGZ>6g^wP=)^0?j<7iTLceOpi%OZD1HO+n#R5CC z&na*RW;K%2^H;qD%Z-bZY3`lf$qGk&5tFBFJHhuIM`p^q)FRpODY$h0!BYw(9R$|* z%^+jFAhLNQ-#^9@@{yyLMl5}|0hP#aTS2kHKSC$z-_S|emZBy`3rCkI8mNXuWZsU& z|7x00Nnr%io{XhqdNjNl2@wN~uzB5qXXXnB9N1_0Iyfwk+8Eyf{4vZwt`56jbL~&J zpSlo3KC^6_j!B9gtTP#V4Imn{8!nXe!dQBU{vhe<`^}f+lQi(E?RQnQ`9!bH`G4d( zJYf84D(SY(4|HH{IkS4^bfIptbD*Q8!C&p|y1j*{tlN&PTlkWo;9(v7!RrVtt<244 zE&;fx@P#l$nD!KsY3oZ8qhX?Z#$M=ORXh^4nk-yTR;n0X6=j7^Yr;Z92J)amAuD+% zhm7UNmUfetmezsc?@OOGOy-xSfZYyDU;Q{=qcw~twrdoI{4-|8v^4K&jUw}g@(Ia-9oFLa<#Doi71Gio;r>YH2G;#jm`l^mJ{d1B@Q<#y`@Il| z*7QpP=Qnx0mAv`JnNRA@hdW~<{mC#n7gTbM^BbhsqCcMJeB^V@z?zrMdg=De@vw<& zyMjKPzFK{gG-+iFd0cXf)(^8yv&__y-6T?N@2NXLvt=?+=-qlCXH{T#$?fFk#InHb8<4DF|{eCl; zIX(5kR@I)T$GtA6?i>%2REMDw1v?rBM;q<8`YRu7snEwTRz<^I%y~?D94%Sv5QH{! z6mEp-zZsuX%XtOkQU59>Jzp(;co5{y`gnb+8Ttm`4;so5x=juH%tkM%&<_Aem24hb zDdsb^46Hc|4f>NJ>V`tDYg~-8f&Mi?`ew@bhDV?Iw&TQOw0ZGV3iejpe z>(R*dB@xJyNHjV$18p#Z;L?n?BUHDNb^i%EBuj0b6y-e$2io;Qc>|!@f(ScIPfsxRh)DS<72lvNm;OC0mi_)Keq=f@}z#>y!1 zD<@5ddrw06{g7Jv5L`ZXlqkkP@mf*F#z4aQ?I=-WOyr*)mGSKj=A5Tlo|(<>JAOM# zw{W+OEGoxINiQcctsZog`ikR^B;;<-i1Ds=K0r2WP;UH!ck4zaV|mjf>9REMgD!T3 zlc(33*5fY?y}FJw=#%C@oO8FayXEp)4Hv=IQSWM%dY~aXBp=`V(+ci`K41OATV?uQ z|Ihy1ep!ims`{0j>#3Yay;_#qL%(#JlJXJDUNs!2N9XBGJuPe9NzeAi!c4FCR*@;l zav2*`zSp1dsLD$5t5_c~pDS5f==2_VP0L2aHmjuqb1=8S+|AMzZG?GE_%douJMUv& z@#L57*s2c+4tahEmHUr8WYEry`i*;EnFd_!T|E{amhw9b6Ng!S&H9ET?@b>Bm-`1( z^UyQkB+L|s`GHq5Q0U$1RHT)}Yy6Ow9Qm`s zWpzPS^EXfz(bsxf`)j9G)onqOU*$et-3#?x+uP>F{hL2O`$IXSNknAnFO%9I-uj( zXd8yCXcRX9xk&v5-sZ#k2b4+?`(1@p00E_|2Qgs_jXbP6mNYIAi`CV#cBmUZw>&vx zJ82Iv8-?TuIaz>gwC5%Cr@;VrE`))ei02Wvqx9QnI`o8rRJ=WhsaI&*a7}~ANbM{t zp25u|_q&!h>7i23eF*rn6QgBuc}nX}Td|h%ncMmqL09+rCM@MCC^tIt2YJgaE_xQ6 z2;BjMN{5Z3cH(PGEB7R?2%oTP`OfI~Iu?x5tI}Al+vEjjJ(LavH}mp%IDj>aQ$7c) z8CY7IjoV5R&(8|kK*6qSmzN~kg#46-pB$D{!>bh+Rh-0+`ucpX2q>2%Lbcc|q@Wvh zs=R0$aZc&cBiOFW=n9Q~E`R+|ooFsu$w9bE5)9V^bCQRZ#-^;sroMji#hDW5{+28r z6~!<0BMK)1mwM*E0Mq>H>hp(vF|;LHzZF}F*+BI*HL=Yk5?B^OpXJx^S!-hH#LM^o zi$5NhO!|FKewc1ap_4w=KGclS`6D?uDgIJ&bbaR$b74;5FZ~)K^#WY7NfY8}{QkNk zjpx~)=5QQkh`6FOoBIYLI5o`7Yp<)ghQLmru2_gK&JBi?`!j|r)E(~ujlI^)*Hk<3J%}_fm+#vg<#E3(nmXaRuMZoTsvz06Q&EUwuEe< zuOaBc>o6e&seoPDiCbFga0jzgZuOuz*O3cLUtpXsM)%ifBmpfNfFcaN*RyN9_oRUJ zXyKBQ;xE+deH8pURw9S7F}t@O#Xg0cSwafw!qFT?of5Au8pQbs3e(RyU8QtZEMjpM zu{g0Y0(33M@=V!SZ961Met=+~3~b;&F#|5hx{Kvl|6rRTz00U>d#jpdU6=Y#ID zpk{Sdynd2zdAweNyDO6c9TjT?3(=eKsd(l42f>+3ac)R?9Q0-*qY`ZmP z*2R(nH;u7kEAjx3;98_U-K(ppON^LK9{2@o9~|Z54Gx07DiCc*N%Em} z@&;S}>YoCB++iI<9xzy#Btd6t(I1Ae^pLE`wK7n*==Bj1_+#Ocgm7uI0aKj5B(7H` zw<;@~ghhljYC`=xb2l#DXb8trI5!~z9v3ORulLl>a#ItDTT8;AUW>CV<3@Iva)EhX zWQmPY#Wd5K=k_&@$;Qz`U;dD6tWhKT&oWN#u8`T z=6>oOf)wIdMz{bJ2gEstoBOJC$M_&VqAe4`kcKdp%3<7c?xjbyLzwy9fUZn+9gGQF51voaE}6g5J#k{=9GR)PBk7gc>CVR zM<-iN^5^n=l1pj4kvEp08(edjT^@fobIv`l3pPeY&pd->?A3stOkxRoxi0&R1VNpx z$*%nzI(}>l^#}F>8hyQ&ocKqx7slPP^4DpN9qkTUS&x_j_gfcqCJtiz!=r zQp;hE#TQtwNcK_ThXMjZMs(H1C$0;e{J(VTVVWFJ)$;6b^Re0ibT>yJDrj@ucX;%4tLDxmefu{lm!Ib{YG{MAx_$P3|Jw< zO->CfqrIrL?@;@|=mtt1Bx2bTxomwqbbRT2io;nC65ub1o&`o^N5j~f$mt0@zZw?Y ze+hY7iilj25Y*M%MKsz`oAV<^@sXnfW%5^9!Jk17w!VaYe1Tjk5)1&_w?mr7-a`~) z9<3=x5Yi$E6KO&w46*(}g7ZOC9K5Tx%2z^UF7@HxE1L8MO=mv753LC(DdoA)zu7Ee zqT@4MpC+5VRE96TU?8aEr=}Z0;M0Wka76~8T#LqQfU*2W`+*Ck@sLCQv(sm#wZM_4 zHbab?=*oTATsSQ9S?7+v!O<1omaCW$fGRmt$SAeLkEP41TKFHdlP#_0$(K9WftQ)7 z5$Tp;6%iM7ry&(S);2@le!vS=<-V2N}vjhKK6WnWFWMo$~U=-qFbu? zpVk3yvqAC;t6`x>rb+>=n9ybea4g;N%ZYh6QrFLrcYys4m!TJ*Sz#l}WE>SCv=AJ1 z!M(^Y;f9=pOH1;ruMRFvDkXO3SvO^)u|v}~8g7CG=QpX=4mIBKy%81J@w{o{E=y#| ztEH(;87@1o%Yw36k0idUX)GWU12;>aAL976MDs3>FXhByh($KHfPb}yD(0Y#Dtm2IA^UZd_FHCC^fcv)BF06< zi^2x-#BtO#V7tTAw-oZt;N=!1FIng>%oRG|RveTvj6WbvOf!nvIAAfCaDt1abH!t- zyMZy|$TJD^j!FQqD@yJiIHm|xAU|Frj4~LD(fNelBZM2QZhR%&5rtq7;&XMA z_JW%HYj>L{C`F>+Y{?rrlgeEw2e5+7oT&b?4J*dO=k@<6(I%ED6x%XZ<=-)NC+xEEoH;Yb2$A zPsq3JG$9`+LPnjy5EWg+GxK(XJU zs+8pF#w2$%Px2OlqAQ%So-_dQ0I*}V8c>6*_gmi`4O_`W3>33PIMBUw#|T0mO2`tJ zwy%XGuj%uMFs_#bol{5;Ti>{Hlmr{U!8HT}tqT7qsMh5gm{JOKNI2b0GS^Pf1|xJI z7gDX{ub!;`UB<9pV-eIIBx)@b70F8^ttU#rn@9-}8y6np)wAfG!}a;~ z8?)ylyKt;vL0A1SmNqcg8e|a|mZ~!--1a(9N zKskQZFfL3;LGtU0b`@HBaAg{P|7twFl3u%KWps=-Dz3y-Q7TdTME>#$6xw=eb+m6% zq1|9@@!+LQ8(`QH@#Yfikz7SR<$Pr$9M3C1Qi-Z}}z=j^UYa;CfcBT`I=3Jt|C zMXru#6Cqr8p?5HY5R8(>E!#un0%r@``Rtppm7Cn^5u+vHw`9(r*%HWk5dRAw@$gqN zeec>V@F<0Wuut?0r~r~pmmcmictRR{R|uHE!>gOe$$;%iNW<~bYhD>B?&z9QjguN_ z1vf6sd!pbjahm7Cwim461o-ewV5bCYDO%8@WT-gC%|Sr?1?ffX^hhi=83?x8DFXY{ zlIo3W`;BNMADgh3vlDCWNASl{M>AcAJ$Pyog*a^DCAsvB`kkK^` z_vZd`hYa!AKc5TxZg{)=dlrD0zG6d2g+oC1C*Ai2{^v%Rc^BA(((D|yX<7Hq>j;j( z>|u>A$0e(h;MW;n^(x1|Tz+}I8(?k z;U+T=N-JmY(PnQ`R8Vq0CJf~MShdt<-qgeKh6)3WeC1@(`$xA1I`3h5F3$fIAN^yW zOO_YwUOnp9Oi|+DN*5zD?jmq^Ja33)UXdooTx)oq)z{UvI{7&In(@ekHx*eEF|XUj zYIKB&09aV;=2vC81}GK=_}it-0^p7md1g#0eg!eo962)452p2tCr9Qd*NaR8r~y|^ zcQ(OBvsGTcxbG~CgE2JWPt^Syj+jWvZVrFPBLhW!ZRC7e%K&wX`vkQ9q#h<{M}}(j z_S>l;U#Kn_>|>d}(kRsIh^^2$w^zW12xleQ?m7DiH@^J#=g>%^HSD&N~HYWnG(1mD^-Zt z;_T%pNq+b(l{uT&?KV(^n7Ml_3l4S&fZ^B^YMoD)F0_%t(%TOv)6*fO)CC>(h0TTf zI=SjtXuf*;bNgLc2-z~hSK>XV@Ai)i7q^^r&8ht0T=sqd0jxxrTi=_9wI{rV@rje^ zr%0c9p5)KvIv_*ZOjBV9aHC4*3D4Yf zh!%US>dnZX*@>?#6bZXMb3C3jm`Sy*T>=W(XzZm!)uZ+2m5g4{K9m|sGb%5BKAlww zKM_W>oja+2@Q`CW&VB=z^kjCuMr?>3z?ti?J~ZGl>a_M~B2`)%Zo{hU&T- zok+0|yBlS2jdBS)bxHhp6V+_49Jdx{SK|yiuQoNX!ImmfE!101H?R|1W+TUD6|i&h zSP$f%kIKR~TB%3`{3vnv{otZd;@!lS+N-fWa3$r}j%v;KhWvel4>MbCi@lT~q??%>Z}g+Gyn|=8&?`R&d|ew>w6kegjt*`H zPqmyF^7l;B{MezhQe`GpG~1>mO`ILUaHEn1OUW14v>2hq+t$STP($w)!XKG?g!fEO z=P501R(kLlL*M>c{#L66O47gf0YenIB)aCcTWu=Gsa1@5Q#CMBQ8E$9v=OvsMXgY_ zf%2E{v_(?6ty<8}RaqV1EhbfeGz!)MNrE3x+l0-hd8o3ktuec0YsUYOHTpHG#7q%& zaca5h&U47=7Kxx8qn{e!jF!U9Ai%D?kz}MlQWZt}wzxSZeA)mQeu*Eqg}j*6S47FfZHUNR`*gg1T4A;#N_aW zp(GQyYkjN8t~FApBYFl=UR_F#lwC2IE}iH85)qSl(LsUvN~qX95nA9bbtv7p8gapq zgLz5(+v_82?o4QR_!uIY`?`)nI$)QTl-dx@BI1rh3E60L zzpYvlBVzfY{6|A0vX_#3FqI71(kKR=|3 z{Wd@td)Lmpe5DA>{xbX=H1jvVr!V05Z18z(Re7%b#R6cnOfE9bSsL)W+%Z}GR!edF z9FsMm&Q_YeUlU|*+I|5a5~<_FVN$%L4XS%f+F>2Uf)oiD*xrWY-Jr^8lnsFH*#7#| zG|&?9n-6ABLe|r0SHJZL$xSf=cXxMooMLX%dxVgzPOU%b}Dq|Fz7oz_V?-Wd1lR7DJ7iDfbuhzMzU-NF-=m4PS z0qdQRcP1)#+W!ocS<`+}t5De-rCYLB>-@E;%xIN7WvP_bl#b@8)m-2*AbZj%S=kEN zox+VOjP$?Jda!%8lV{?3Rjnu)q82GW?rXUicQ#6EOYczFp~Ni4;sFv;F+b$ducXnL z@qoqkx8!ap*)FfqqFVYh`_y|zLhY!M#9;NE3GNqBjm9EhK8Lcn-ef{OvrcRjZV-xG zN4(RbBvH{@1xxb=zk#ea3x=!{96mV>M&jVEBF^DKAPf$*6F(Ev!A_-YqV)!GfrHJL{h`*f!CixLU5?~05#HKtcLOU za_z*NA}+$8UdX%$TfN6Ex-jRvighAi4*yTvwC%B_;crOvjF%BQ95n;^0V1{~`!qtd z7{2TJBaStS3Rg?(AWfi>OL&Mi3orYWe=twqOB~FW^GiTY1iZiuIh63a+i>>~IJ(FJ+g?raIc@Xu7irX?hWJ>9d}&#hDuj!0%gT#+_$fYuWwm z)YL|IKg$NwSzvBxL$fRWKtJ}Yrs|&1M?Do@oZC{_K5N-dN!v?A*^ghKGo*`SKYR}; z&6)`WxHI~9>0Ee#Ve_(m!&#~-oEo!ue%DlmLQ0IjD;+4fq15W;X)pY&wt-|y@Q!SS zt4U&JZcgeg%B~foKhM{0y%lx3#HgpLQ}5`k6cOM4^!wB8Qgj<$EX3c070&?3RBPHI zSpD(=)f9D^Mc&+)#oo89$t&;nblB+I{WF-B`JV2S)g~^j7&PG6h68!cA zbatBCaexkYF9IE_9Sd@BaU5LzVC6_+U@4$fBm>}2CeZu}QiViS&WOX51@_XN*>ukP zr4O#76SblQfxXfzMPXJ-=dZs@7&ThT8|E6jUSVm4Fqu%W)>purdL0up5d!fUkpo++ z_-A@b!D28jHtRi<9J~0t$BZK3fjcDA#365mK(I&W{$<{b`o2g3+xssQ8~F6B1ES)h z@jZ@J`A(&4PQwxL05h6?$$v1kP`jb({*gH=Bk1Ivx=qrhmHW;vTMeqlBI3XJU3GT# zGQKn45Vt)2sYhCeR8ddSoEE#H+weBTv#)ZU=$z)~1)WPrvI$cDd&UM~2>?CZ>|B9m zGOiz-&C6%w8N9^bxd^R>@I7wK&CNQp&43@b%*EV#6}?9rvWRPb+OndT<4%Zl_j;0f9N6AYmxs zJ#3(uPjj%mG%;y`q<0jrW}&nYq|-f^>|vn~v)c1LX@D()9G9Si z>N>Pchs}!G;g7jNu?qDK?sY|1w4`wl81EPglaW+RwRuF>cKUbt8gAycd0 ziC*nAQ@YSbn>%bd=cq0qPup!(+_sK=II_Dch5iy7o{T*5Fta&0J36r$=`KHPh5WD5 zcj|ZPTj3G?hI!2Yw}y8j5<3kf+?>7t8ddB*pU|E&W7AzB{E=3#-`b@g{v|YbyYDqb zF;f#M@Denw_v=Zlv|gLsNLM-U&1ub-KJC`mw8b3eP_T!8<_j6G+DLg)+ADfT;DrDf zQ^I}-*qPZ5*dsoIEW@lo6|kH78k;&Ew`LDCwxbqX9jYRS zv-uG)xBoeBF<{|iwDj)mNS^e3zguJw$mpXavFpvb20VUjPSJ`fw zDKBk;dC2{w5WtbdCKi<2FZKrId<*!CR=CUxHac6RuF(15vUR4J2f7rW#P*WxJumwt zLEO^IpW4Ea(u-0$DVodK;5N7fLNg`w#J^NZqt^KvG9BRehLd+cc=g!peda|EC8fjH zYZlJqUdty&J%qtlO?x8LP9(#2%zryzK7X${aVif>Da5Rfk~Onw39E?6@yB9JhUd>D z{zVTUQV2cX{TJ@4=`d)|c)+`eLHEc(gcgRo<33X7zHIfWzi2P4R09?A7~4@x+jBvl zpjor!F@TlKhT84)=2J|Dhi=OS>y|1sQ7&CoE1RIOB9AH184Vg>f5=l_NDWqYc~#0B zclmw}AHWdTaVP|}89|-2lTuWMQvUeT=~K|HQWhx@x@20IHf%!2beO$`s)hj9ut`pXvl6{uvA3SWbb>1(T zGd?>v5LJkU8@sPg=7X0*&cYk)+5N9!sDk~xL3{6n*Vi)#)KONoML7n2X|U0e2CrKG zl*z;81a#BPR~TZI$o$isI#{7;%{D6u__AN!?d9z1M+$H>N-GOt*5X;U=vw6u> zb??mV$YG0LBd4Nt&s(QPr}tFk4YY60Dvn#n?0XzW+@Ux(4L##wBs>s1+mhgQA*y0D zU5S&e#MyM+(m?sn9%j-Z5BIwk=ba9H3UZ=V#(EEkbtcu%RfEtQo3hR_u;bI7rnir{DbxJgYpM9 zyiB1azF)KUOYRNRVJ#TQwI`|5L7Bf zL_FSeclKU5?I)D)S?YrOeZ4l;+Vb!0A@U6;!cda4(ZWK~3T72%6LP>!Dw}&=tqiDJ z3PmLT0=71Ne{gc4#K;gdxo6R+<>?PPuZv!Dd!1i*9W)!1Z$ml^{D)k}5$an1hM%9& zwbhjy_MUT71HB9Qs7CMG){Ge6TNp>}q$RFUZ=h3&MW zWvu3ZTTV}i)g=AyGGzDZl`=N}PHfc3{Z4E!LjHeCY(;Cz*>(&Vmjb~LB9EZsF>DlNy$J=#i_|jC$ z^!9Y60LRASq_T>nFI-zG_2+B{|0K-v%Fr#U*d81CET!<{Rr|7OR9B1#Z4(%g{qAkb zp26w!F&PGGBkxL_!|s0}LkiVvE9q}DV=gm4{0RLkZM)Ro1Ds`G8SB!9DN9yeGSV*m z0O{GpZe3&JGxloN!Zxd@V##7r2GKng+yk-Xv(H9jyI9Yf8FCcCS0Ok6f(cMZZBDFdg6khD^Z#x-&xGaNvWX-p@ z=q6-;R7DW`aQzBc&90OJMt+tg+GWtS-`ka9#;W5|M;V04M7y=`GwY2hc(pQq^^Y|* zhS%GbXD=IV-DmLzF(cGM3lBZ|bu7y8d288eMk-q~b;~l?MkND>qgrplz!5-v^2M<4IwGtT#`c;5E z_wf5GR(}!hHy;@+YN8bQm$v8dmh(mjM-9y3%bgk!{-Lb>#HOPu1Jeb%P;28u6LK2& zk2$boi+O|9@Kw$QN)htf@ujN1pSR5!m)}??{fJNSQDNs-oVjZmwosR})m-+umh%7i zu#3gpu~J2Y@ADSETOI@F1K3;+tEFe2yJ(QUWjp(dg3%=S%zHIPjmILefOw=Ejol}o z@+6JcGqV;8So65nu9jbwlTb@r(WCq=Mq77OjcF0MCWsju$R$fx;(Z=q3Q&-U;hwUM zu8)o!il$kjMV`;O?&CRmF3u+k=nwyn1 zK{1%=)Rdy$0P^ORxS@G^%ht7$to)>1%6g`z#bg&*DkDX0LvslY0cJL%Q8p3fX#s{J z4!-6DfPazw1A1&$(LbW;-Zftr&Fsjo+VETNT}Gdt^$7ml2%H+)9Y|Al_+*NO2jmxJ z;dU8f>tAVapz4YO1EsO5eiXL~#eI}^2&ga6Zg*JsQ*c+m$(*cA*Mc?2ak#ct@or$( z+V$qwjue#?^4W;5Da}A*={FF#D!UyWJ`!9T{Y+0yK_&3GD#9cw5y*%qt}w!hhqOT86l*GP%KcM!@;5x7ZVeH78|6IPugdGjfg@=Jp>$xZFs?pC4oh*I8_GInwE#$ypZ0I=Q@#DbWzq|8mpU*uDbf(K z$-XQkN^3F=jF~*$h4t5Nmwm)x$2CJcaoPyVAFYZPtQxMZze?*qn4=?;YI3!RF(qr4 z+0+ITUk!vmlr}1(4+&g2)7TCRz(fS}g`I84!rBuZgj|VfEgIZi?FKsSnIfPj82f@cv+0HG%Maff~kIW(W}jVYxMx^nr2UeW_b>DLYYt$JbSR0)g%{|o!ucDffEN?v62ENnx@R@5Dted62YZ$p2fH4R3xT+d>O9_FB`Fo>6g8%ut)T8*2)FhJ zX}Jbm_82D6{|GB2Z|K&zL4)QEazz{-ys<=Ec};W=k&`oqi73*L*p^K@#exDZ4{nMK z3Rp>v`2KX=o@e;O>=oFZ62D0;&`&2j@kZcOl&CkfYqqZUsKiQj+W==J@_B?%K0aDL z3Bq)Ch&s||P^FmNxk%_ntP0+hsS&!STy7~nG=SD?%eDRsST)eHL=>0V`HD)pQ=}bO zZq2v8;33(_URj+izLDTIp7!&B*E)B4{qWmJ+v(+CQ<8KZZ1#0{%QyzDR_Qy1X)sxY zGA~IszggF^Xe*k$ysqf0Aj~QzTKzYxVe}i?|4}4w@Z>>dux65cY=GI&*Z{uY{RWv0n;Ic)a_q+|IMnr0>VvM4zccp&Ofm_>+ z%$u|g?iRpp4RM?esaB$8Imzy9#Sq`iZYB@9@)usUtYF>+JmVq`Ri9H4DcL99U`XAM z17N!N;%>O};q%3SHc21bZUlPia6#cv3i-?)_~3K>iI{+SLjav;OeLai$H3 z`MAJK!c(%jKZ8Qh!QpxfCMY#Wc>}T|vmNe%v4ubTx#^9V-`bOk=xvJyfT$RN{8on? z%|702u%;#SY6U88ys&20#1AI~HaABnHP4-L7QiPtzW$w5Dd=I-HCt)o%u*MC;JEJn zW{20h8AUr3+k&2*rL6s^H;JP19!ab}iX5sXvnY4dMQ<*8t*sr^GxZ2Rn-?u65MMnI zWPaH)PT#jJc)v5DC}7WwrZ|B8IwyY7H6)~Y#GhtX?jCB)$Zaw7ixT_8vOjj;!&MCJ zu7DHxJj2;V-7n|=qD z*bGlu@`Z|NugkE@rWzIo=`jBHJK+@@^E0vm|2d{l!kxVN$aNkVz%GSXB1Rj(ltzcc40N5QJG?|JwJr) zwWao8!^LR3W2L77VfO3lhKrnAd=MbuX06A;Z-EVb?$oX32bHb)pCd-^haC6tjq0h$ zILLVX0X!CZOjl}|1rf4>CkdB!&_ZU&v(t-KuJR8h8MSdCV%yD6OvGoNX;OarB7=z- z1GJbgQ&CP;l1I;99#VCLmeFpXd!f2X647i_Hf{Py8ATgkb$wH$&?9y<>h#$JeRNRd ziAPkpdJ)Tw|A$9Tb{hItJz%|)pd-%PPI+S_j&#ODBb*ET*5H1@ovixcdYS}YHumWw zjhUw>Os~}KFD~*!Pv-)1QhTOk-Gejc2C_r7=3{G-q+pGowo-yR<+ABBeX4aW{)@@n z%H-kV0P_xpxXKhpDMQW6fWX-bihg@lV=St`KWZ^K7O;YF+@YUMwf8ke!E zR&HI=;+7{C0~~ahh&!cHiWHrv{4!E8QW(CZijJ;)SRd6@8UFCK9A9gK98QWqCpnk5 zZZx2M9Rd(a)%0p5)9`cd#Q$K}rRwJZ+K}jy84Y>r?w7JS8Yf(zoOsc;6;yT6_^$a$ z2YH1^7fznIUk>hJrb93~M-K|#NQ6QZfILy06P8+X9DDoL7X;fOKCJV4M^I)_RpZUL zcFX+FnH2eCmtvhULso5W^#{G_z*v828PKLOnfR)FH_MW|0`YO;<3?L8?2acPe*|>a z7b3tEkJz}(TtgV)+Wl(Q8@)gu2+}l;J|FVqf$8bJmzQl4e`25n=QW zr_To`g+NDrU@%FJv1b!Q_Q3Ny@Ebd>1s$H7#$Azp8 zWfApQZ9>lsT_7YD@rhXp!ZA93EbDZ+3&HxPQNXAXP#M{%Fgmj~-Pk?~&Y)pe0YxuQ z@lLe>p*{ZWLz)78Pd4|#9wNK}qZA2NOQOAB+2rv$+S#cQh9`lJXV%is?m2tM2rh!4 zJWc2Mz3OqJpM61>2a##4*p}5RjqYP+i6EtM#E&(U&W(u6z#-t?Eru{M`y0E_rKCM=j8mUexlG9I z*DK<5Y7BP!huIJn0ezEACL3W35Iq@{Ea zGH*NNP{>>xo>ye@@`q-V`LUWenN0o-V~J~#q@m-Hr;vJ%lxp@OCWQ{KYGiD&{JdK5 zLJ=GGXBeSs2JI}K>hgDvIQro4tYFChD%Y&`#!f*gBI_Vh7TIbcf9M>L=}Ua==LJ5~ zmh9rlig&tP4vZwKw}3!&oBb2gkX}$RSV@;~2xV2=c)?Q-#Gkn)@B}ALBm09=YwtHV zX78s=mE&8{%Bg;RXSScIAiA!!>tek7N@1+{0}~FH4fP~`46t>vxvQBo7M5{GV6w?j zlD)R4oEnb{a|lhQ3|`N-`+mN0dqAaO}#KhMAW8Vj~w#hj-EJ&8(Gu zQl7Qg8I?!AgU0jh?PkNcJ9mXX`EJ%==qAz{biB_ak>PQl^k+8T<5qVEtIq64?+*Cm zusp-JH}(er>n+uw3CDA|*xbiE4)b!;qRv!>9~!ZWG9}nJ{0P zJeOEwZD$-r)A9Wb6%f2WqZX7^=~$pGhOX~nJPPP1+8p8L$deLvnUohn=dv|P$iOQ z(-wfFyHCE$dFw3W&;=y4H2|OHYuIt|;TS4l)6@n}%Rb$k!%LwqGw~o~z$vJ4aqV8_ z0l92G?|*>0l}E-`1uyuG;b(EbDx~)nE952)fz{DYRtrzBrYDfcm0mI;mJA z$wG%k-Kf>$(Y&jvJ?+Z1FOv1IZ8=-1s4y|&`uR}ZzXrf76C5P8J_EjQw?S4-8)_^$|6 zh|_PkebHwtn_=xJz!V*DG{A_iQ3TA+B-o`3brFJ9lVamC+)nUc?f!9n8Op} zAC;so`#*OsaCCLyCCHeCCl)yo(z%Y5G}Qagej0{O5r>A??W*>&R0>Yj>VGzfO9IX`qWAzr#$bphWsGHu< zfN^;hGwzuMrgKxLt#hm_=Uy>qT@nhbX2NepRCG$JXhBHMj3W|R@P;Y{BHE|4s4j2-}?ss@bf1Fnc0rJgEwZ2)%!BBv&D&0A8TrKhfa<$qE)f=FBTOl>sbtNSW4& zT2-Tj8TYx7??u3Uq)?mP=Qv-Uq%DL_X!tMJ10H((=&zI$7&4FX&0;*|qQ{0O#W-R% z7%`hvK+YM-NXvRG__j@`l3oK-jcl-tXb>pd?Le&rM>GeIC`pAQ&D`NeU)zlKSp0p| zQi#*Jjif_SzSXng;%vAFE>IJ?BQ~J^mPJ{UgzCsqAf;1Z-+eFDW-8NO^MGQJojjS{ zu>Gs%b4Fz&&lY_BGKUvenhVD<`(>|p+Kez+IoFFq<7&IIcK<&m%9jR!8E7=C$g`># z<&>HxwiH(Dxqw`;rLrmUNYhlopmp_?L`qt;iPWuK?DF{rn}l)%yYq|A8JGdgMkmGO)yAK_fZtQZ-RZAa{5%6FEzOG(@k#uz!s^5yz(&K!CFU8> z*C6vowR6T%j4ORRPeOS05MI#g-6^wY=zuDd2Bzs%Oo_CScg?QBOCZvG*4UAi|5lQ% zHyHD$0k_bJ&y=dph4tD(Yh{ezOQls}H=Qi(2^qOlbEm;4Q<+Z$h9+hns@-qip_8-P z_D5TC+Gh~XpFRDmIfT8Em@D%!Dg4(gEP+XJC^ZJAi>=#mn(&q07uXpn-e>t~nd8=6e{=VMDGit+M!^2gM&4j>ekb7`u}I9JzIkv+i$PK z{mfwM;aq-4+HcqBxEX5W_1V?8Za0(Bs|s>ep~SY-^KM%G}OnX2p?}?&a z&c$d%!%>Dpd}tl~p(T74vZB1iNqQt|2CZHwb+ywq4R-~qEL2Z&Y3)p#;LLr(urN7+&SmpOQ^rGMq%M{FiXNN({OQ+Y}FOXys!L)W^8+C&$M z*elcSAgJr=;0QnN`mWVR`zlLq=?Zr5d|HkUkc_F}cp{cxb22=WRjgko3es_mjKYkb z4Z;0^an-(C;yYvK@z9-jj-MZW5}%{{CbGi$3&4qNDiQtYUmm*m6WOrssuQ##7S5;s znoRGmep7%MF02}S%VhcF=!BcPCme6Z!THSocoktP`XSfCgJc2>6$N~-=YctAnSy_w zYC)0w-5nR#qP>lza0jZy2c-G*R`r7lGjx8$;^^&two!J5U|gxS4ZW=YG+_p z$m6c-G0r-7Q7SmzJp1neI5VY+` z-N(|7Aji0D@mJ~>(F+bWv9lUibn_xP?F-D9(wQxH&d6Ts{CYbBWm=*sUc0{BXy=}z zh1(fOmF=zFO_O~cX;3jfyz=i>zz? zr58$4HEs<^EN8H_IEk@AC1T5W+7I?mZ3%ITYQ?*M!fh!I z3fA6tRz7B1$?-1V8h+i_en}h; zGq;zWmFSdI5Gsl15cW$u>l*LkJXW7*YSj-iG@ypjDGgPW!UJuicP?HfD1C9SWaCy^45 ziM1zh-8SVSYN&6vaQ3d~Ne*Z$%7w6qr6W^GL9+F6+mu#hoGSh_fCFfRxWUkWMrW>F zl8Yq&(R0AqtXMs6Gp%7ZY|33l)yHOkc~xJz(K5v;0MzKhatZq#)9YzpI!>@xvJ|;> zwlb)wY&Rvit{lnL(h4u z==!;&E1Jh%_e`zp`m!OxnsIOa>Pe%xW|+3Cn(n6@#Rp_(nG9s4AGg<2iM%{bwEiB; zM1c5dxb!hktu#;Co|LS7(M;RD_goz@uzAQ5Czbm5FJEqo7=N4a-;%KYe3a#9BRI*A zLyhj6Y4G~W^}RRyDt*JHIYqmpx93#EvAvzYhJJEeJPw6nggRJ8J^c}cg#e>Qj;0-_ z3%Uzn?o+Mwaz_QLj601sy5xqJfD#It)V7z7g6_K4o~$)fxm!E2>h6)o%Dd}4hP|j~ zua#?mAqL;w5x{X`cdN%oynH(NuJ0^{t!RIdF?OlDpqZ#^@(MiWWdaZh;cpJ1T#6eg zfqxpbS@71OMD>sX;EE0*kL$n(H#()#U;t8+v(nem@>gN(Up~=)m-)}tElF71atnSR zb%*ggLoF>6H95VUZNjX|%ZvrjB5?27-9*9r#ZYKiF+0y1=2L>2UOk0Q&v6v3c6hZC zwWRtla2W^KPMrFlfnbR_y3WJ8yahOPUFy_5mzmYv=Ws;kA&EQM_r5jM7`ag$y{=F% z6N8xB`pULP0hdSAa=pH31StXzvNoT#YgM6Mm7hz;ee!h4T9*Udtf(c=$YBNA1mAo? zpj%~UEL_vfBv;GK>EU~JSS?-{lR_4ZL4N)9zorP0&F`4OaZ69}`-)}M;(svC>9lLP z##x^sAXNrEb8JTT^y(2*W!fGc`v=!?MSpH<7Q|?l3LoI@tXy&mC~Gw}3fG;X=}%GM zxKHxWPkdWc-l$ z;Yl?wpIP>Y_S|77n{9By9&-v=flb`0P7S_ehBopa-5f9~{T`YGT%fhy)qkyC|MDzP z{h+Qvd{fQFwc`zpIk`4LkCtnkxQm(!$mgMY#{0BBq~Ep`lHo zX3LAu`g!*K;|hrK=5k{dSHZpvZNr0Uo+9pES{$<* zVj7`MX-FKY*C&*{k{VSyee_FkhNZ#ec`oinR(hLlRmEdGtj+a?vbb{munqgp#TZm9 zV7EnJg2A}!6Bm-ahut=Oy1()FxRW=eK1w_CcD-*WR_u%ieS_qzO5fS{3}y||R#|Y_ z#9)=Fzx1sDZaCkesooPX(N7r_DfCrCd7j@LS+8f~H98^Mu7uK+)ia+X;OcZrozk?P?hcsiBg_8y{?J}0m$`;@)RSFvzRpWG7sL*`MMXtoK=E2Lpd5F) zd=$-K{mXyy>m4u7jhytO_cJti6>ab}U!Ujm<=BUZS5nBBADi}>z=RFda;RpaMrHM(+ar4al-m2lDY1A|!O+ zP}X=A51~X3x1GH#;(;mK(>2{rOu20)oGp=dKek#9?83~oo8DI(_1>SYSyk)*Pkl> zYi=eV=)pVs4%7r{ws#)-ANB5*zx{{ay_}Z}=-pr0kNWz!eH+W_c-P0i+1Rj~&SGjV zKAQr-uc&iD-t1$9(0UK8`fC#v4cjK(z5PRPHZ=Od=8b@5$Z^v*y=yoP-m&MTvuCMY zd*mIK-Olp%+PAzJ?+*?|zg1b^@@TiBjrCq7n9~dZ>)h(XhL|}Wko(fl!vQS9afh!| zdsEB>1}a%cY?w(#0?is@Co#0%1Rz^=#iieu9Jqhzn*P(XUGQ5|`oI5w%Lw$`uBm#b zurWJ7Uu5cHBCl^TO0%&CdJCgpnVi~+wVik8W$PoOrk-fVg>spQ3Hw;RXIb7{fRhu< zPbNN#SH*^xx+VDU2KVlP6Hh>NqOm(Q$*c(g@E>D_X8)db^q{H1r}4uU$d_LYPq`oR zK6-Mgg!0oj`74H)W0$M7^p(-;0J0HHi<}y}$WT8-768m0o`1H|o3Q)<1a#({F6(={ zTC)oVQx8BVW*gM&C?n5cF4cmm%<5#oP2ShB^8be~`BzD>{I!^nT7~WD=>^HVE$V=v ziz09!fL%8zil2A#UbsK=SbxmM9s>+Y9+x$^x#y>k1AuVjRFIE!p>p@EMvhX-_3sn&+O*;l2?d@7t0Qv>-MxFk z+{vO#z;FOEq+8wp{yd|v{sSHY<5Jv`SQM>!M?H|rA=p!>i>bkMccLe^A~iv{$K{*{RLRRKOL zHT(h6{?Rb%2ivP14xEUUiVQ(;MtFa|DAjm8Yxl~w7R8gi>6K4ko2MhP#!X!+C~Z>L zj*{eGs`SQ0NK+gm+`BevTAD=Y#Io(_4&qLHE_pj05i`I_W?$+L1B&yu<)sl0mij9~@iAZ5Z`tvLtAEFxC0 z-{AC4*u61Eoe1DH^e@B&=-6e&TR0L?4Uxw(OZ%2scJ$S9Q&0iZadI`|=&|Y1O-_HP ztnWJ1=cmh!O=C7s`?A-6QCp{b4D<@O`nH2;thw_p>fbDYUM@VPD17}L*L;+KGg1`o z1Ue^j;qOm5^sds4YCM~pE{Mjo<&dgbH}fz$tKA08*6E!j?<)V%yor`XRX|b(46C+# zDW3SNX6N5^sH7?-N&!?DlKx~0K_#s3`BWyp!e66Z_69DZwtINVE_2&de|09z9Bik~ zOa);e+l^qDvERn+g~_mD(*hJjhC}fcQid)@WG>Y&`od7XdA`o3mHO5}h4{spaWBV` zRwwd{<-J@Yrx&_?GvpWl;@!%JbgO)_+q&_86dTmoIg9QtuCqV#h~SJsGZnI$6lARJ zV-_8|jg9LN+%9FqLXI|63{QCKe-5g)4iQ1zUYN`!fY7LYX?9Te!s#Nmf~HdL zc;AGkS?0I&8eYF2tte?ab;1e;)tRcdC-(sUu860;oWd8a=O;(sTQCu7yHJp$PhGA! zc2>&ZTmsUPg4}y?9dn%=1SToN|6?)qAFXw)j0Atv(M=kA z?vz=1s}hbdtJ3*TIC{q}TB@9{Wto4ve_`YN)eh%&R31prA(k;PQs`alLKV#P{nCcF z5iY-m*w>U(w5WX}zw~ljlCKmA)5_xg7jOAyjnj@EtQr6G=I*XvU_dln!30Ne4in2+ zKcN3{^}|8C)7yyG2|=qmklQYB9Tvmt$u|V0`SvkN2&;-WGX{Y(T{(GK+MuA-1XP}H zhQhE{!?YO+VS{P5a%Fv`RllR(vp()`EnQ8LnVpqesou&mV(exx=>rX(~WkL%0tV58%rO3^FU7Z9 z@)j+^L#E_rb9`T9mgxWr`2Pf~6NmQ7br`3a9*N6n%KgSnYUrJeWhk|}A48z+GwaTO z=r=4KCS|gA`DTJZg@lxv-8JQ*7cuo+{a8RU$Hqlm2wQHIoAd7yY_zIgDiU884s-|yK6}hbl)9z@iPL@~;Pxc-p}{h0)fZ;B?5YIa=M zl2%l7)mPc|5T#L@z+uQZr5WFBQ^Y!`WHF2dc@Wcz_{d6>uyGO9a?Ga7!Bu-4|CYyb zE8LT%acnvJht;Vu2PNNXEt2dt6t491x>3YMYB`GC3N{g!kkS=MW7)W3s!|%-c2)-; z8~4oCWYMcG9j_AG`#6+yJ}92Q{JYTq>@%VPmz75%>6H7Ap6{2T!}l9Y_F|kFuv-Tn!E!dA%DP)riYGUH&2|Jd%J+&oB@_9+I929P8F| zWm`<|%<32?1>7IkAhYV+2}tVqYLgR6U`r;U{iU9ha2#V-rj5n(SPQi7t3+NePBSE^ zECHd^4Eg2Q$JpYN0dB(@WX~?;{-zHIxph3RSoE(gs?z>vrG0Bv9Regsv?x^jBv$}@Cy40B-Ge;TUA5G;{b4RR=JA-OGTV2nFz&d6lZ)DNzgL2 z-erS8l`+?1aXX|D4T6d*m`ZbEUc#AYJyiou75@ylwhFpGSi2 zlBo;M>>O4VNRTw8%FHTxwOQ>)`slYlrmmifbn;ftL;`Wxs3+0sNr;2B3{Fd}>qP(^ z@Bfh@h5JTCy-N?QPlnshA}KkL99L(-r0xJ?YV-!}$tx$C5A;Jx!`p6l)poHa$*}>9 zGgsPaP`vJ^-G*sSi0q=(CDaBlv_q^Odr$Pty)&#r+(G}ap7EhtZ^Mh~`Z> z+5~+~Z1R8adl8KNLKRWUo-aF~Nt7M^L#T z(Ip^ymi|$Ad}caBLZz=7U)6hdcXl(0Kc7n%&SLdzULB2~8vN}!keei5F3&i@$){q> z!|7dB+42Yc@Lx#wGsp*v_Z7;J_m`NVX_?5UOa9voXfGQymEN1WCXjeD(|Nga5uCQ( zrM=%8O6*eBy05u>`s_4;ey@ z9|zn@5=tu~PF%n3?qSgyB*uz6FBX!`?$Evg5jg;=6F^14g3Gz%Y4UUlin$H84j>sU zg5?0V5#6tI?O8&$OwNVKlJt2JGON()4N&Z(SYRw`IFB@)CubAU*DZ(T`-*4zcwI&4Pu^+Iw~F20Nka@M-U^`6cLl;Hm`H>B{Z{T7vKdI`mGM_bX{&XNkfR;n{qk4xooqB2>-!A71Tk{M*l84wAGNAU{IaC}gbc zG)j7Q!gqFO$`?QH&@(lUl1}e!oR{=e0yd7Hq~6sCa21Xdo)Vn<7k)F-HK6iFo|Zi4 zjpf^M(rDeS$1H3{FE}cSSq|L(uXSn&IsSakIdWYlIyX#%FOtQk=k$ano#!D=KK%JFE;;D5aozJY??gRZI z_8iwF@W#e%dZ@fW?xfDLrT6Ue?2hr{LU&1+hu-~85%7`^3$N&!_e^<(0HorP>=N6) z=_%r)yK$Wsch|7}y@jLUB6b!Q5K+5+%vxRUYJqT{LJX=Udg~V*knwwp%>ZtY9=a+d zi&y4HMJ0qF>DfHXnaqee1JoG655v^70`D?eIK2@PCie>6mSFv7Htk=p%W4J5TC5&K zBL6`bwX9C9ojok8J;)~-b5Qz`JI(_a?yzI$fkD=6oe!GlPM#jk0VD6#f5UU)8Trw~ zcaNx*OG~Jy0=w-Y4k|nsSaYk*1G&E1=gUYM zTN`$-=Uvsuv)iN&oF74*4$U!5-~CvB7<1q&?a7~!~9JP8^qz3yaSShN}d!we>_8ND5d(E z`R@R)sF#gLE*CxeGmFE^yx)rJDq$F6Bp;-F%T@Kp{<-PIHJ{Y{Zs9hX0I zCqN_t#R|1N9nfFEs4v4d!d$bUYjLnAzz)`Tz!vy|8*Z2+|7{cdLso)Gg@}GmF)D_A zy!CGxNK6`IJVqh|iOr%K&q2&gh9xo(EnV$1a+h7h-9;nysd=zA?@--#urk>*Ij01! z2!RaSTC=I=Y`*5*$TV~DZo3*g@?>XhLqwTPqkHGl#IVG|a3|2#YVFx{JEbgPX;jAl~VywUXj14afd5u@^^Q;bZ8xN)Jh+ zHdSd}Hl1Ub&Mhbd1eF16tQfc$A0R9-jq|IjQFL|$#%2y)1KA_`4EznEBu33X;+?R5 zfF@d7RnuQJoAceJhIKNsl;1M=R!{>RFMQ!-e8d1_InP>xW?EfH!?Jx%^8)Y>*8bYV z5|iYxMEfY`I=w1^qw*=**wJ`FVr^2EU=(oyj6omKKn;uOqJFH=>K6vp$=4)Qgk6;v z54RH1a+@SV$<-y`UJvX-4t9v{Nqb|*7RCU)C?6)X>KrjVRYm4kfiNrcqCtLT&9g3!K6bf7)N4Y&^{}j& zgRzsRs`mXFIaUi2E!3`A_^=8$Ak6vQ1EMK2VGjTJQZE`v4lSZ9&WHRc?e{Sn;`I)& z3w>Z}< z4rwG~l;?s6-a6>k>^?>x4>z}Id4n;Sq_mb9&;^t`)%kh0)O?GYGtZBJ1ha@KLUWrTZYy`M|2apxKNImKO`M#OxYUQ2t0eeAD6zR=h%- zMxz>4LuRrC60-B0ec8S}k}H${n^}3EQlZen7*@vv>Uyx68Y_X5^@n>$>qO=rH>F1t z(ax@4!w$NMt5cIZA0?%17x95F3D(E~461D_@cxwb2K%xUmTXWXiYxGq@S6wk>G{7H z2%E?-N)$3NgG8e>k~x7X<-bEDp2-pxU4aYLffm2kjAtpn<0~mcKhA@E@{Ct1I!sU! zOv^PqZWszfEGMt4t9V$GJ+KVZ0kMOGCTg>JNvD1)#6+dTY=@!U?8zFyRkpZNu{;Ghby~(r*M8^p3@-{6c$Y$WYDez~X(i2!j5Fd=m1&J0AX;*W%PFvEu7k z2Z1))1#VbO!b8$l8NA_Ic$=>Y7m7Eyr-Ll!F5 z#lXKm_@@=9hQv4`c|jGcp{EQ#^RL(Z%nF@o`ZkM;tCi^8XL8=f&oTv`GTu3`j5)M~ z-7L(88yx6QxUrURlU0m~2hh^O;*zAPjJ+Ix3-ANb+Vl^cWogFy#utt>`DtiBT{06y zi9cptcn$=ZL9=;ZwkXWqoGZ{*i1}UULS}}nF*7H>q7Iq^L%EDTJ9(MMC(*=}a3b2J zNpF*(5a+t|TDXIyeV!*4(Z&7ePp=vZ^G%VQae#MpA5N1jbQkGFRC1h48ExwBcyz~aC;+xUI622V#hwGXlJe!Pjel0 zaHVep7X;6Vs+zhHwcc^jBhHdvmi zR+eiV+=O|pYPwADobJCmO%zR2P^(qgmH9asY2O!x5%s@Tu0KnH%>B?3{mV$GmVI2c zU+Z!gen0hu@jKIhN%Rx~z@#Nn{1tbR23eYs47wFRaiy`mykUt#&3h9MI?)(r2jrK~ zre zYtxO85PFug>?_$fK-BY*D2YeD&&D+3{LSsXB)LD`v0fkvKH*Jps8+qo$fjvYEJM;dXiXT^K8}fP}_vtB8(xICI0e<1!Gs75lWA-=T|7bx9KLib` zdimP6rkwMH~F<&SpwFeI+T@_E)oF zXTI4pY47_v>Z|&fX7nq8P|UjHtPY=;)iz6;D7}kRlvg${56cj6Sd%5w z#Bgp)iEIb;lF*I2r_3yvPn+!WtsR7j1(Vzb7(1H=bI>5UcA0PdSnb)P;;-Zv33+9v zCRNwR*0yPP3gK+j%?e*uyG-(Tw14VrV#bX_7t(MON#x8cjj1MHG4&rn_q<7^-j$6+ z#7#(!VxLVP^oc2gpt7Fg!Md*42ShT_ixm5y6>3RYIr!CkZ?-*@21hPy--u8V0+pUE zP1-A4eM76v4X{^IlWumZrhL2NrdbJJem1)Vq1diye?xH3Y(XkGDerXVLL_*`eN!es zls>FFbc0yaU|2IFf`MNEPgGQv?DuNBZwsg(k3!O z+APJJeSXPQp1z)_Qo`)Z< zw*7*yg~=|RZHx`Kqv1M(3%Nu=GPyA6Gt`4`b&s7*FIfA8*aT$-hSl+$fg5wg{f{Br zQTK)eB=nk!C^|m^J5MkZee>|buK$*@)lFgI@f>I6TggG{e%i6J0e`<^326OXYe7Fd z4?`jLk$-1LQ0EtsZ7==`Q2SG+HznEg7zUrM)C%Npy%i#v7-iLDH(nbVT)10aUgF(- z9QrL4HX4XoLO%7%%bw_Wu$ep;`764TF(CL(b@V0SRec@M4F8XcY!s(HFHNj#+^w9% zT2N4GY_@1gK9=E{uU|+d+XQ^EIZKE-L#1X(-F2K<-+4Pcp}tE!jAK#K|M~3oioF|O1)}!y^bec&%M5Tf zKb1a~ov%19!L=-Z4AE^V+rBLI3(aQcKl#SmuKTb086TyH{k?F`i^+zy0rG4hVK3~Q8V9(@c^b3SH% zpcowx+hjZ+sB2c?QUefhq8=zcaug@mS(&+<0{&R#Ryz+a1ONvs=nmN^>#Rj(PCh~E zRqPUn8&udU`Xa8jUN%|=!ihkG?(OWw3O}1lNET{2TJtt9Z*xZ%Og0@%P(wcLo8U$P zL5=O1MJW#}zl^RWJ|!)ZkaGhK!^8U-!b0B0Mx;mq= zW;eS>w&1QYC+5SG1Ruz`?V(nT=~hL^5p1;^XSOVel~fkwQ`^c~K+KM;UWF^q1&vP7 zNHMr1ES_v_qZF<6%dXzr8w+KhWtkQzbUo-Ss~G8tMWE~w3UOP{xFeeNK)P@UpFImo>t?kUq5EanbXy6o|zsp&{kY(5_X(cjzHkNgmTvdR8nb$d23 zyQvE}h{7qD=HwxTd*cY7@`^^~7kv8c*X#xPa6IotaPW>cnY z2FHQeUGb}nO(u|?F_5{k;Va959TvGPh3=G^4Y`>WGI*|l2M2Nw`BVJlPS@sDcIN!v zdE&NJ`g?c~KzfAjSQRcgEA$gc737ZYjl#IkBgUYcCoc0Y#fN_vy>QfaYMlFh#qC@n z23q~GyNI2lI5%|&B>V=d;qN01PRa4hP_1RgM!kbLXBxahsy+Vioutp24&gAc*ka{6 zy<3}v{x-rsfR055g>MHn5r)}z<`HR`+#OEU23j`h$Z8m-jjab)=XfFHIEh_>2xtKc z(ZAqTGo2De8lc#K;fp+fyQFpp5~}rExNdjm8w_Pav!nU)(tFvJSch zeX&y;4%}sFLkMjzyV?7f8W2`lzU*lt`|i$ohq=?AqpUHU55Vza?wj9yDF#Vu01~4| zU4%5>Yr5UnTriQu^!TAZZ{@^*9PAq83V11C3-^^&d`R4Jp`26Qb(%qXX`fUa>zbcA zf39kN=sbxNp1^*sUYzk^CL4iytY?qjH%dHHo?)52lW3`LRAII4%S25NQqRXs!fo(1 z+dqARXj>DV;#4{E3CB!G%QnK`CCmA=025}1dU4`oXoTX;EVRpc!>gpH*>itbd=HCL zVAwy&DVxu$QQH)@nhVrdMtiiVR@>;)_@U{A)JEgmW`pyAuk(Kw{t!(YkQQsfunz zRu*j-zLz-#ycEqLneh;B=c^3QZIJltr>i5`5fv^+Mnf`J@7JeXbXjr7i)!w!tW7{n zp|M8Mvk!@+RZrE%kg)l7_J4T&*Ip08R=^+#Q)#dQ&I|7snN>QG!F79me$$eVQZhT& zXpu8!3n4iKGs;cbEOv@*2aqGwh$O$z%7a8XN6uHqd3GjErxfYyIOQM_iGWgv+@J)y zW)jSp&L)yn>c`94X}l${)mD9j&%1iu`peUG@6@g@m+d$?45-2TK7CXus4ppHMXQff zo`KdME9Hf+WeM&7m`9JEoc8(PGU*h8_|Y9+j|il?qL+hImxeplR`1{=l;^&<;^S8G zfW6ZANUyU2OShjR)8T64=Tn)8I4Ln%|xJzltPk>jZ_kHCO$ z;koykI+k-`HU>tURbG&JyNNKvNC=v5!T}auHLQy8I+;1m<|tHC)J!B3n3>NOP5A_e z6lZ}o7t!jO$vV!@Ix)b)8Wjq(`vHDZs1K-$e}c+2q4U|up5Oy%SyeqXOW%tQEDt-XVm}Wbe46<}j1)>Hb$S5)3%&nuZ$qfM z`Py81M)ETFyuf%Nq-XU_QL1xo#-C?ZnNzEsj44CBwbJ>RssIz|2vWb_#7~?VZXtqY z2ebLmktJ`UnoBZ9sG5$^k{BkPAm(Tr>>?r;8f>C$A}ZqbFC2w)TkT^^oE>(M)i#_+ z)8B=Yn>qR8bmQ&gZk)Uw_GF$iZomH%Sz|-(Nc?JG)eZioI5!TSs83?{#Wl{bp1+Y_ zpP`~r7(uzE3EJQ!2V}UUF1%8YtycyB)ZYD5l}3qc0BWF;h1M8eW6s@PxubqW!BYOw zzUzq-=S)LjbY!C{AKkHHui3TmS);fr+^y!f(NJq9n#?Sb;sS{k=%G`5Ng~nA&6h@{ z8ApS{`(?hnS8RVZcw~WJ#qRg$cFiw)nr*5%x8;z0pyz6+$%>wY4i@~hbEllA84z0) zp}Hd7YAZ9#uUdYScSIni`f4i9&;OD+-_~@@Ru0BZ22@j$ER2j*RED+pPqfPNxi3m; zjgh5FA>sR90a!561XdI_3||R3KJ*y=w*9QkEN745=lQqkBn>1!j{LL!^Gu--?Gc#S z5l&tWhX|1(Ld>GldRIc_g^o_)3Y5$Dzag9vj{-_03@0AqCTQonkjYAQgbMiIl|?xc zOSDHh{!+?ZXYg3l*3xk~?=!NOburt%QFlWc*~vJI z-7uAJgJwEBG=XZPIXIn(qG_5w^STw>`K#{0iZ6Qg9CM!=usjBZHNCW%am0q`a~d55 z-&IB&uWW^Z>~LeJy&kn!OZA~_XA6BC2q$SP>kL@k#=jf%bxf0~zUV-=7K$?gl}`+F z4+ADbm(eCh9=>>7dpT$48%$&=NrhK(+Z~PgmuEohfV}Q2Z*K@{c*O_ny_v;xZ2JVN9m9E&XTGfrpuGpj-m~o^{HjlSS)ZA{D`@s{p24S_=%J_l<0{E!?N#MP z>Uq?J8?;n8jKWpZZc^M0$1wC=L{vnpm| zM{Ig!CYOL~0Ij_Ua9zXt{CU$oQ^_rjNccr!}pbub7UBSYk&!Z zuQc%U)^~K1Qy$M=P3Sd!gPAa895-e7VRPDO<$M0#f_O*@g}pwA?{9%7u|Ke*zfZF) z_~IeP{I}gYyXV)!XLXH&-9pL`j0#qz`l2U|>*r9HBaZ`w? zzXF@pY5;i{*|J%(oPOLiVJ7Ap?Vs?uX|L}*Z`%ijR~3HA{VHJ>8yioZw=qn~H+ck1 zqWcI7C*SYiS93;AL8aHH4VWlrh52oGrjHj$NV$!z0~2%Kf)Dh!?o0G!1-|`)`~1m&+7H}0#;KR$ih%iRZ6j9DIB%bx3V;ID=LYx49S0+$m7s(tedb28pp9vwf-7SiMbOlrQPYyd<;j)4^$Xz%(qIi+oilTFa=a;)pZhjA@eZ}(q;7gBxG$zdn$ z%c9gz$)6wM48KI6rV8w+9WbcBYZl=pWdb(1@CuQUhb?ik<`MCr02|3U_^_9U{ zoc=p^%a5+-otqO>!yc8xqRBdQ7b0~*kk)_$)skvw6veOu&Jf2u70Pu)#Nc9<0b&<=&zKMwH#)z2! zaI4M1VhIK9R(z0kt7^zVf#S1eW~A24@Hccjo*TZcFLNH4`&-V#ZZ|P^6he+qqM>Gz z?cX{}nvY5rz%e_UxpZ#PeX;(LZ6kD!EqPJ)67*gj%Yj@lovAhT!k!=1_COshbaeK| znu<1j%Jt~zgLf+_oAO;xOtYui%WGp003RuTc<@$Dhe}u4Ds$r|?k^ zp;>(pC7G8sE(2>VP{FP6&(wie*4t_qF%9K1 zMp~PiG;5Iy1}MNtwvq}%lg_9`mI+hbl{4`L>9fS}k>VM*k`j?8By-Sezi&U`dHL2$ z+7gww;_`LQy!nM5z8E5B|etc(`uG68+tBHYbAkBfMVIl4VH^FUU@TTP)SI zR#mrtw$cEm4FFMbHvMPQc#Y&*ms$dR!T(&3V#np9zYU)6M<|SHZ6D0NbyGb|!U*!4 zD2sTW_!RmDXh!&3*Kbue>v$%PVj1 zCw+JQaDFa6e@K<^r7=3#p0irL!Zp9izkKo-_8aoxqW#a%F8=oRRyDLj$&(2Nd+w#W zebW2m{~2g4nQX|btu-i4V(jW7Adv>@(#*1w6dzfG2T~e~iQ^67%gOFD*}s{@`5YX7 zLjPXXmi`Pp_p3|TST59fzY~%ZdYZ$vBder&t|-~`P`YtB6Pi+&0%F_ZGsu7jWs_bm z$yspT;0cTgSG1uU0?o=p8$b%saw&6Hkyb+6>)s7@0}ez5_Q|)3v_oUedu(l~>x&39 zFd2Daoxa9gm)I&m!FlAEKsrHtcDbI!hgqmVw53AAF>MTzs$;YG_Uy(9b+BUL{c)#o7$Z z(OgXBbQ(MPG)YGZRO;DM#M@DnyCS>-h@HZbUaYC9kpp8^d)dD6sIlWjk?ug_(%!_X zn%O6|n8U%Bjzm4Y=BIq;6=&VY8_T*?vMaK9gO=K|s0JU>__>@z&GhP*OARQR^K@(S z$CP-23dLz+j6rv--_71DyKXs)^xS9>kwQY_GFsP$T}mY?QF1I{hlnsduXGI1B z9+;euS&K378SFQ|YT_Jd#b^nTIXqAjnGfrWzy1#vrqz0h5nBm+0I06#S`+}a{^+)H zjrPm&IQ7-a1_jc%V`+Frox}*Xm^hQYJHV8u^Nu&x|FtSU^QNAby*pdly@K0f{ef0q ziJ*u3rMjIrM0391fm%*e^6V;#aQl~(o^E&1h*sIv|8CUN7J(fqZ<~&9b{9n>TmNwu2zr0!V$NZ*=bt`+-%TQy1KLY|Li8TvQQ{ySkH^Cn=E}!)@ zVz;DY);&+X-XbKK=|yM)k& z-qvR~&D(gUIAtm9)Ce&9FC?cf7+*Fz;52XMJhxvYkuB7&iL4ob_%~xWxzDJ`=CsG_ zE%#{h8(eR%9)z(DMj*2m*BEQ!?ZbdFuXlbE+%M>n650o=wamLVdgbpw-l-p3*1U3` zgCF}D-JFbE-deWI!W&3P_A&n=*>gW8mDe`3B?dM839_FHJY@VYhC6g^WyhviJ=E<+613TFt6bPikIEsoYr zx5hBW!wY%+sJkDf?pn86P9DHau{CXiMZIJBuIpYZX@amdt6n2isi#;Prs-{TE+JJ1 za8iM>uVL(52+_Fr158#F2=!M47Xufm3fJ>b#X8^+@hO2KsvgyCl~3$;Eg#S3)T%9i zEb4wgCq-RloxaZwsA)=Xk!zm9<{khDYpzeYk^y{*_qTbR(xJ6P8_})KTMaM%J|*9` zV6|gSRCXP(WU-2mb(=T^jGaqY{<3T3QBLjD{(+ID1X3H5doa*Vr!bB?ymFv`J!Y@v zj-kM~XA#?4^)i2l38}Ezw&IENFctA~eM$|lVeAdA`wF;!Z--C&yQ@{zoHP$&qS!HO zR^!sb>jRd!f*Wo{Op89_!JjUqbtCG>vz{i^IDL$BTI&UMwFG#v)k`BO17S{UA`A1Q_#NUMvNuaY&Px73R*|E9CMzAe=K z^xFJq5zr+9M#L2J1K0h0Rx9>G{ZxIsGsHND+wn%DmLO2myX5ZFeBKPIuI229ie+v~ zJp2S)a*%UovJJ<%T`9KK;foYVuS8vXfeU<$VNfwiz-{)d0tn)B>Otn`}YtagZi zK~GD}hQb-@g5SAY_fwn?X8wA>U?|?y$=ThoT6M4m+0v{=wl@hdVB;|Z*LC6oaIK}X zA=lVNh`bYkD!v~_a^{TKRXHGVc{e9C?Ju`2`wp45051aiC9LS#(#wtU8yw%Gya&qb zGihvft`1eokMU_F@418Qsp|U`)L~k6YETBHDIRjff_N32R&s^jJU?9okgMih6%7M>Jj5m~e+PrQIP;T{AMw~re&SG8FZ6eRx&g^$R;Z=nCxebJQHiYA^FV0l8f zE>+_zPAjiak9edje{TIYEc8SEgHi0kMrURfO9Og&kXj(S!g*E}$`-691l7#cPuADF zvoxO%iE4AIm$*~iWrT4&nBEqYveZV*O__OpKq}#w{%@t}yKE+->-RG6eXH$aZyxw% z?44o~mefY$)#LCO`MPRe;y3(h6{plIPB1Obsy+SXs1}?F3-B5*LhsBWV$ht?1I%jP z^rDh@5?|BiY*6^hZ#}?U+VI1A@^3QQe|^>ize{cn{Q-&ah+eqAScYI-D?B^2moc?e zSFUvUo#F#xH)DPT!fh#f`gwfD3n=JQ&T{}_mN4`S^^p8Eb{%L}0ZJW`>j@Al3e+D){#^RbP9i0$?~BKK3~Kv0J)|8iXJ>d>cBg zP}zVC9v7&GtEYD-+t5SXlymJ-rfK|knU_g|7RC6AhHJL$Euzo#r{)<0tHUJk*dLU9(qQv=+-Q~ajB)hi4JdK%pn?O*hfFmF^&(^T5>HiOP z?;X`-x-V?oX4G*M5e21=qB4q*VU#KfDkv}tj5SJ&f{Gd-gd!~owh<5k5j6@3GN244 zM0!o4(rYLtp+^V_5PBexgp}`&bN1P@&)zfc_x;ZK)_4BYwOq?5&vTbwyRP4{tut}x z0DqH8&+$GJ{hx_(LGLk%O_rnZ$cq`V`=21$I|?G*b;D;9n5D(*7mJLEFsLol*1ud( zA9Ot62Hmp9BP7@c`B=Lj-O@knh)66Dc{38PSkI5l#6Z!Y7RgWrTKg;lX zF*VSWU3|YT%Br@E0J&AoY|pD(U%T#DU+vlsBFihJ*0!VR@USydmFWCR6m(d`EyyOm zF`J+ndvqeLv=*=FlhtX)b=Fx`2>Uy-bSjy{KAy!Cebk}-h6|bVq%l|83r}{Oc4Is@ z>b^5f=>83@=6v`Sa=jPVB)7~YVE!^6F`fYlRG^q4v1qYPa8$gG6I8adqLP4gu@-Uh zX+cn#p@&%>QtF01-&0prGV29~m!hUc!j$QPotR-q`6QNOn=123q;Ev%Xq7IR1BCkwg+g?ek1BLt4o7%A;AQX{wAY- zEW;6L&#$@JkzkSP&f31`Ow8X{0LLGo`YJ{9zQ4(y6Z*>)djdD4lD4CN1*zg4K1xRa zG@te#>)Tjee%0C_Oss53RF@0pwF^@yMR4cF2xOHu*Dy)F!vqKUV5SecoQ9;s5hMEn zndD)1x-P@0vN_f+rifg6neZBCntfKn;m`XFt`vpP(*fC z&GeRFdz5g+x0H@hk2Iz#JW&Q1(%#BnRCG?lvcp7kB5r*F9bicD$Eg_u=aY`{bQMVA zc{XDtgGgTOuMRMzFBZZZDG9;{*|;+Bkr%;SU?I_xlTH;_fHQdE$F2v&)gSLIM831@ z`j;CQ_{%<@&xg^ld}t{|K2-+d=JHS^gz`i&!)9n|{^gz88p1szrhqIqrrA%>Gb|Re zfq`I?%6avEN0~`$g5buDYlPztRGkKgmzOm>JdiEC`qbH$aKItWMvz7GiA(yN?qRbF zJIe?1IfbRmT#YZywt&H}L!O;3T{|LH#%*WvCR%$Hq@Gq%?(~C6YlTxuXynt1cixMwL86mNWSCwC*}QgVFSY>jyc0>VOUipTSsCEXLB@aM?DTOwq-0FFXGie4 zyBn_Q{;<4wK^z+4Mvpdnqa!5n$MRJm8LBqppz{)|9!JWxi=h+n83EgX06-*m>{N{> zIM!S^Zgmss?lfMaW%?j~DbC67$kK)XljDfomnylV=XB2~cfEs3ibtQivByJv737Av zkb@2rj;&3J@GSSui`?$}h}WSurvj;UmfY~JDnP@!1!uF(3DCr#;I%0?5=xGRxVf&M z6OY|M=HA#0&nflfCGA@t=zB-+t-4pA8v~t~zOLEmlnczu{3Q%4eAEhCCGrpHQ2(Kp z_WkOVT2FDksx^g!99S7KpNqR0%!^~3*Td1A$33|PqHH1tT&XR~w5!>q>qrOnBWhoN+1TzvhM~KhK<{ltY1`bVPB{t zWSV*Y56>2C=H&+O8UFXz=I0Led=mH6u}Y2iTAO}vQ7?%CE$W2oJk!Wd=SjDMCP0YN zM^DVtj6q@$BD)I9MkPf=7I?O}MLE=kwOj$eJ!v-{W)PF&iS2fjIRXSJHGvsouMFCt zpj`p`eRl*dKOG&ga(PFsFefn|ZL}j7ed~!|G?=&2j$Ja^MX@W2647;tNfWg8&e=V= z1^_`y^Rz(5@J7;f+@OUMA!cPxR7Zp}e~vxf7}q-mY-k4&OFiqbUl$)hRo-b^$k;74 zIOy0g9W7RWVYuD1RNdCwO!}YP5 zu~EmTFMZ-1aFcS6LpKl&!X6{;%{Ip7*ZF0+J3G6JZj949>l6}L=6r5l7{>{VH5AaZ zv{Wwifxc(u;<~YJc6<3LJKTU$bd|-M9a{;aD+Gzpf8?ly$e(&GB19=vfad41;rUku zBiS%$CJ}~+9*qj_G9pVna2${18abNuv)@gsZ2+p+0=tiY_B%L-t;)5!rd^;x4xLYv z3`*0zRQe0C^nvM^vQx2i zllASDA1;~m;;yB}o!)qBlBFdJ`j2+fCo4T71FKdJ4bhA3s3u3su^1s%%U2 zpJ{8^t)O^X;)1@eZ$z1xbU;MA&ClO!=~?TH8&+$L zw0fy)YW_G78&>(+0Bc4v@9zHi*jW0=E=q0RRRWlrvEyY+!RU%!bT%Z`#81fDl8vGe z08@hCr{?-B9D!7+BbxR45Vb8R3s*pFgp_!Nl-u~9%&Rv-YU{P#X>v$uQ^L9Ny>g?$ zosI6yD!5Z<7f32SbU+_WoN7HnO2z!>lED}CCzB^czH~U^6?$p!eOK(M6XP~i_`Ze= zQ5~5u_NHZ#4x)>Sh2MneA6LBWICf1dz6Zor-;S^PQ{Hss8sIf2ed}xQk+NoV)BU9~ z`SqkcGg*EY`*9F-0kZb;cfuaIRE-esEf_)P_MCf|cfU!=Lwy>Lm=m{j+dz90EV1ba z`V;nVh@$|$6xn>9or^Owjbh*n68vjXIv==N_3j6 z^LT;u9nZ_)&_+P-94)4G)0?yn`K0`^W$?P_Z zc8cob@WqDYSt2r;TNvZ%ySb^jCz>G=+bcKd@6{`gZd$ucZNnkM6~Ab`+i`8I_Z7L6 zeh!ANu1w%*zqSbJs~jhGs}2Y^i($z@I8$3hAWv zftQHq>Xv9Wp!s3e6Kn{H04}!JZCUPZk)$Nxve-?<8 zUS0KwVG^03drZ!e`Su02#fI$tZr70|ZR4g`ul1=Kb>YM+(hHqZk~>sGD+B~4MPoZ6 z(Sf(dw1!-dw7M`b z9iPnJ7bbj=7rriPh+%o_s#2X>`)!Rin9(lJV>!5|LtJ>6yMdK`-fe50`){|TcpH0K zG@i^M^S9Imy7bOIcNy@V-~KB8uGLLqxhLE5V$=H!tf~^sKRqwVy+p<=?oY*zRBraQ z95P$G4!H+)~A0i+X0x*-xEmLhd*_!oy_ne|Gj^QewqH}h?7 zp3Hl(C1~9@=dtU&PkW`4MJ+3JmW|aUcsR|4JlE8`q(f6do_Ju98&ASo9m%%Elgg6y zvHX_kNL6ebPuI1zcJk!I^9G82!uq4lO%U)hH|_oBX{~?A5uM%f*4fl|QJI%d82mdQ zwSDfrMJi7sm$CT&*Z{f}ny#;MWtu#sxAyYIY zYyyW(yd0DN&6ghTJD;(}xY#ad#9P4jQmXc;Ofi^RuayyJVh@dkRQJ#7xQ%MR7-wH> zq9$p*JU0BgN+B0bqaqwe4f#%q&i*wj1(^$G!r4noPFO#^Stp_vrZLluc$=vZKxOu-j7;(7?vYIlcOow0pXP3{v zrKz=6faIR#hTn_nLUR<-{u=9B*8HQIAY=9I?;G+6)j#ae$3056NJV<9Q(uH(nB2Q6 z!+%vw7cJ;%X_c4S2p`vqd$ke5W^e;vG$0)t_C{3!yn)OX3*i;*C2-s)9REf+Xfl#fL;f_Il;mJ)sVbeOyyn zf8OX+Gk4u)TsjIWh`Y7MrX040?d{;(K`@A`@tkM31fHHm>a{uw4nP6{!Aa0yH+leO zdaQ6=a@_uf{pUNMF!=)lc&Uo_B-_dFaYA+b zHWq*8arUP%-X$G$cyyxn_(NM~SImx=eY20ba|W@)odB)L=o11Bx@z`GO;~~{mgN;x zTc(;#wbmxUVeTK49h|&Eg0oQ==aks406l-P{WIl&&kJ(dbtNy8L~(5k|1!Tsxa^X` zvczYxZAZl&Bk<%2aY54s(y{}Pb1-ShYN=sTGBD8sQq8 zf0m*u9!nbDZGVkf42rolkf)2O6aR6i`G+&{^Sx+F3kI#wbh z;t0Mil2&iV31>!Cx8f0H4~1vL*eV2veaxUESiVzOk#T3T4oDOCsvs7)YWP@v`_nHy zk>(YqiRUJHbkrNtF)0#T9B=hrevZ`Mk8H_RyFTZy`@NSvaSPt!qWQbC4^nj*j?9p1 z9bc;>om<>1=?4@PW*y1%-Nl35=PP>>&`6}dH9u;kGx-4|7o?=gV+?6r;=ogFswTkJ zxXFcAe@rg_5q@_?b-Cd3KgcNl%TVlk={F zh_c5wio@m>PU@2brQXTi{AKdi#T7OFZZiQPVi)l)MDV#EyOLfKLI8&=6BdIe zXgPHC3~xs!3#x_$>LY^)>}=%u{9KtyvO^i$R<%jDwzB99#r1>{D?Ys@c{66uZ|dVi z(Ttjs?NO+9qpi0v(G9A26i%2vLZRdLe<(@vkE4s+3Q$?3H%0AnzF9iS(a#H>rWvRy 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 diff --git a/proxy/static/tor-logo@2x.png b/proxy/static/tor-logo@2x.png deleted file mode 100644 index 5a459dec76185d8ad65044216c08397b8e126d6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10042 zcmV-AC&k!_P)6aw@6?aj=a_ujmD`)+pnFy??8 zw!f2?9+`_?qE=e}^*;Ne??hbJN)s@X1ScZad=ng{^;_TMDLnVUw%d8!8os@f+GI!> z5z`IUa#X@|<;s<@xogk;N|<>Vg?MVVg`zI_2X(cGPnsY}~jpC#JBu(`D-A<@r=FJp1on zX;$C_o`GOO#3CH;mr^k_N1!TmEvuBh1{rx2m505e;T}gv*1_4ibzh7|E+*YM96Wb!HBt%6b z^$+s#Q^G95797j~N&;f2jBAF?=q;jxh>V`>RK&G>5Wg?O5t*&zCMU!Ph+$MzlqN$Z zolT>HRI$X2_{N&T_}?!>h75T`hQv&S$=78_MU0F(Zwe2;&uN4o85tR-WTFh~G6XY@ znZ`QP~zUb8=jRam^zW| zLV;(O;X_IJwG^e~FNcAss?3#yGLl)q)6=ss2}O1DyaiLZfByV=>Hhuu7XMx|QmO^4 z=0|y(?^rz#RIfZvV(iR1bDHI)VY_umT^v=1IUuz+a*N(h+4LJ9`8YgZFN zhL*J)e(&9ZEbPWDHs|LhGE4+kuU;*JhJ1&Gq}P?vt2i6;`4FDblP6K!AJA_U_d9iN z%@)jGL7zVhL3;P@J<$SaF8~5q{kNI(%9t@@?#!7pNuPW=fd7ldABfDMO4SNdcz8IO zorETd=D(aLYX9&)jd^~rc6>lkv*wA>fWtOvG*cS=sFc6(@8Vl-lun7~j0VO&*q=o! z3cac8FtvPWIUYv0yoRkI8l_O?VcovAIkJ^_T}`-&1>=8BBQJ>(ka#;oNDCPU1o?a# z8!Ppqgv4l2FisMijFqJCDFd(E+0u)Q66I@GQT9QV0#vM6?rOoRIi&vUn>GWK_cK4MR;m@Vsx8PEU_kiQ*IJOG95Ba<*{I)2-!euO{OL z2h6$0$J3=tS3-3AuV25;yRBAGEx>w(vR6n8<^n((ymj})e{BRtb3J`Y|~^e!>{u&{c6j1O+@RG-jSA;v$`V%eFm$gs4PPgzqDR z>=Y+WDd6nhy$F^)dsZ1l{hPe`ycu;d=rt>yk=`Qmk!90Ysgkpeuu?!mB>|n~92WBI z*@WTKeBPC1dHy_hLJm7C;INQyR!Nv^Q+8{sVo<=A!|QHi3rCsmu#hmBDa7c;^_x~- z?dM}AuzdN@I9E8bNAw&GO=$`;hlRwS;bZz!&`?U9y4Be~|NJX>%6{E?)!DUc2_KYE zqed}wfpf|Ti3x~mK*N`-Ai*CR*RceYKa ztdp@smW_a9%-mU7^$YR|K|3%uz%PUkl=R=!su+9z^nk02sn!g?kc-aBM~@HxI>K~m z)Xq~6F$vQi<8NhGmkpNJ2TGgWKd9+wNs1kxCrdr|TMzd2m6*g^jqV=qS$=A{a5QNhYbJ6dU_9$D=l%B=J=Xt}d=`%=8OBtw*0!X)oPh zJ63wJgDG3FP$K5Rrv{JXLj5yp#b1*-<$gtX> zCI0ae{I00S@z~z;5_B2<1X{RHiRmNN7q5P zISvT;NSA3`BLCuA;|+_PT(edcIk{Mg{N*VE<;H@1i=5!Z02H=IC=G0>16j-l`$9I- z6OzH$>U^!pRKvIEQzmfK4`s`i`2Z9pojARNTjPbxhh?iof0M7M-M)Q0w@fcDuedb; z`YdgxLjEfnWiSM?$y^|STaqghirY%c>(R5btTgeL-nn}%hwVGCf&00=bG=ZOaMGkn zT&bWZ3$n|B4{uK^`PmyboFNz|i6SwQQGt|~D_0H+qsJJ#A|HpedqASjr4iqEc}pT)A?2P5Aa5t5ooJ-fhd{_aB~>mM>qfTWZP+ zdCt-*tS+H`;wmJU!((4t90X4JYvUvGk(Ls%R!pTRp;+>RHZGFWCQSTH4mWMqkcWw% zEk`(L*talOrY$;+^v}(C*cAsurznyN6ufPPtc*ubLiz3yrQ$@mUHg_Yo`8`*{NuhH ze(%9q!-`zIcrpK*n-^L`QMN#uE4}P9OLHk4jMuAQL#CX7)r(>xmw=IxkJ(Q@&g1D~ zE>%X34}AUYNOttd-z;4^54K|Ydh=w>c-O97HQ~+zridby!ng8G*h98t@mfrnWMG2^ zf1pI+;Tb>8=l;#G+uT2S;tcoq??1|YTR&I^U<)Nt=_#SeJ0034C~C?NfpP(k9QjKQ zD+A-dna(~M^gaiDG++$(+qC0@;jCO)%gtv>S`o%Z6bkBYiWIm>>N~5BBq6DuT(WGY zT>pu7VulRqWtgZFhYanlg3VtzmB-g_SSX!2w?{g3bThB}^eVQ=lP7D|k0gs8os~4r zI+A!9a?VsFWk|{_T%>@kq)L^_@o<5H`D7R=(|jHMf<@DKdQ~h9rpMDWowRP-VU^H&Lh4c^6#@c#WD>czDXR zu~NrQt)=wo(`y#FZ{NP04~E+sQeM`aCb2#+^(j(WI4F;=3G2|Ym9%WdEQyrr?VU@y zc;zrxoag~fndUFms$GpMY3R_Qa*-cBdL)%8Q-;IAD-t_U4b}fL1LFZrMH-rL5(P$H zV-apF;x_zaC{Wzq0vm{0$!I*c!%~Sakd`opriIAZdf@(uHE=t%ll7q}6DW&EXtk4yicCKF+umb>JVPz#2>Y)P2EL#)MSs6m@sq>GG-jdXW) zt?6&_kL|yq$h*G0g2nK|+1@^jS1o z_D6s64L(d8xujTzrT1)aK%4FL4YP6*VyamC14SBchT$*U57ril6=;vqElKa?>RBM_ zMKqtMF)`DbfVSJpIY~Emv2n8tJBg6-ub@pF&y27KZU9!J#bOavdaSnSDJ#)rx(@}X zh!RQeRAG^j!^4OohXna~$4F7f(L>IQtpr%bCpHwAUIez?mZC=SQhjWmvZ%dWiDUw@ z5^3jS$|{k>d8rhMzjX-`TItf6(&tgeQ!4z@D01NTb~((IgqgvC)7xygWe!h?_)DY6 z*vFCIs1#%}{XNSsM01K&g_lf`RINySfM{53Ux^YW(xIuNn!O5avn^E(PNZr@;;-9f z)tWg~aSTp$V7uQlUOE)Fx7Fm9PIF8!nT$_68&u$_!MSTo$5^#DE5*<#`K7Bzq|8|} zK8v@~H4xfxBizYX+q#e&k~82zpyHdyD#k%Qs1D{8=7Jv>m4pS@{N z-e;guf9TXq<^jix&LP{6N7sSQViTd7m(|!3UB+>Vd#q9;0ZJ@*|Na9RB_bk%)v8s? z1Z(Uo1Ct;C<*dX*%d7R2D021{0h>NCwb$8XY5)PcmS+%YeUPIu*diqZ6jDBoTEy#FH5pWd!0Qwh>ht~Ctkn#ms~GW8*=%|03QGQ z)bHHCdFO;`l`d^w$Ib>`Z(@$Sr9Jr)F3NSp-m;nDr;a)jJ(!74o;P?yixSpI9aGtW(S)QOJ{!;0Pbh=@#&30EK^qtH6}(C~g&#hA#BdTQ zzSvLMk%Jdm^X74jPR;S?FHh*Ox-8~tB91g8$`{FJdsd=TD+M4Cz@D}1MOKlc01 z)hZ?8gTns%?;#ohx?2KMdk>(FR z2+Q{F53-PT;`cLIbaV_`u}WR<5h7XFp@V;Nn!wo;SwuwKkri~(v<0Jh0MSgFHjTc@ zldRx4`nRSx)Duq*{IkgIPj*FS%9LS#`t)9`a+Nn!W$WH`5c_oGKvtwEpZiy((fuG0 zmc>BH!oB-;KOSt=x~TwZ`hy3@)u~YG1PuhkRhxpAMtf-bcoCj^0-o%OjEsEJYvpS7 z!is=di`b&YKMI*F{E*;X9M+;`BRNgrH*MCybmYhp8LkzecB={CaxrI~S8;AOnZQi;>B&qxldJShd^O1Gy-cg1xS-gf_y>gwMz~)XG z;5Fiqzxpp=gvcOHvMTb6FTRNTZ^jGPxJf;B`O-BW+qz{Z6ElTM*{4mN!*E{?2jScc zws+5A9;VUqV@FT0KmR<*3KuSH^RG=r=w|-PiIdR!${K) z$gjH|o-@)?M3JcU*FyMn1@l#eyI@Mp{5odV4M`!-{j~gMMqC$1# zyIm3vOQQIf{vS)tn*C2|-=P(+AF-IqLJr)X%hSO7v7QVde0l3gWx-I8{D|+$nXMc@ zEoI-qjcm^!hxq-(ix#fpsB`B}=50la&=teJem*Q$?i_6Uwm(?5Y}xq35#xVm&6;a0f14N(0*)2mPrqt3DnxxM_RRJwnUrY$qJh{e~bHcT0Cuf zrw#Fht8_DF%n(*cU+7Pa!>F*XEb;uR>!^DVeaq&(z_TuSg7-B6L;uY5nnX1lg zvMyYm{_IQKHA5q+bk4*%PZ!U|Q++pG7dUCjmkkmMp;@T9c#7bW;RvTyN0VhV^u7lT z>?Gf&99Oel`YO;ba!QI^YKnf2wWKA5zln4 z&8IdGme(4Iw6x^YNj0rdpiOD1Q1+GuIv|o3Fictj?Q}<<da~ua#ama+p$!!Q9Bi>*wDd4_Q;E)vQ}d7v9pVtOWoM#(ngF#W+!@lg7@N= zzEHK4{Log4!afY28Ev+uuA) zOJA9iCvicg7i_kHES|Z#x;32XyY&EL+YF$meU#S*nFQ~+-v*y<=q=aNV~rmDBAgc5 zQxE4=OiPg!XlX&w1qu}K!fKFFSX)%lqCByZ#bzvl14E2qZY~vG8>7&O+J9}3YJC`v zUA=lW8a?Fquom`D7@1gdbT?B@&)E-eM%C70L_KS7zq;uITZf#|N`DQ52B6f}Em(g|FYyKzR-~F6YbEcKKL#ib5YY8zTyXf*FM zi-&h{JK=r2(jkYYxa|llq9wUy{_wxw?Hz?;cbm2+*0FV|~6v2bBT)Q0Y8mNKiQ#^bo z!Y=B>As+$xFL-F(pziJuvutPr`!dR(=v6Z~Q6~(Wa&zCi7s15-DUnyyljw|eL=ib` zBj>v-pyx6o{blu0hDMN`Hto@A7KvRzoF6<6fT!-r1aRzkV7diQa=8NbOXyq*x;q|5 zEy3sQNZP!jd^T-f7S_9WZ?2E;zWYveekAnH%Q;t)q~(SJ&@|`AcpmY>B+XAJrG#RP`4N|$fe5@ZV(m*dFA>s>DJwo5_aKZsxZ#cRicDX589G_ zoheUJ@PgFE@tlhnKdHUcae8S|C^9K|V!Pg8oaOcu8%0kQDxBW~R~;}qc~v-Zgbsbe z>5}!@b>0l8tDkz^V|lztks|Ew-Mei6{{7U^@j!=%4*R3CmBV11?Kn~j{h+KKc(4+Q z9m8W}XQWdleQl6B>V$Fh!4nB%#1cj+AY0Zfo(q4O&d;FUv1<*>lP8yonocSD59b&N zhZIN{Hp`-4PB_|sMN)*PLVSFc_@xvdE~Vqu))X&^qStOT-jw^AMW3ZnL5JNP)(hX1Qgofiul zoHWMPuiK)6S9`Mp3l3RnDqFTJ!!0qYH1kM}8Z{VB0;hBE7<%ZG?2Yga`tzXHq;cOw zIT)ii7%WT_6DJbCk_L&+LjkB(j~vLy0v`S zzP(2n4$$ESsYO-4K~0=Ml$Fhzxlol{qgEyM^XzFaa32(-wx%ryd2uhw6kco2pFf`; zV-I8c9`F}v=eDv%uamwKX-U=;(8*Ub>6J4`)G7<2qoZl#RDn^WKjMb?&G=E8$}tDd zoUwqFEnS~&*|LLwp6Ph31`YX)mpLkr)13(9sP-hhZ@;GFZxURmx0|_^DN`yYzUE6v zuo3FnvnM}s@8gd@CQKJ}gf_u2kJ#JBghA&8eM1}BC1QfZ>v-n{pKhqmeusmWrHmOfiVn~ds{M_=NaQxm{v4f7K_eIuc;@19dnIZ}%&3&X zrF6GY;4IXsfMHlYiJM*~@YsNUqZnP-)UaV~_RrZrREJ9Iz3>&wgBaa3Nbc0Bv-}e) z@bO1rjT+ZulYa;lAY7bRi4`kW^!d1P<7AjUfkK4}VQ49W6)#?#$h=VdpNCNvGRVWQ z@fzf(hqfJwg;61xJGu}yE1nPWJWulmi5Zd1P{iLbGFLh?|K~ZA^pwVi4gHFZ`t(~i zD{un8{Kd=5Qx{*E_}S?1*t=cY%An%K3(MCP2_P9H^}biGNVwCaL&uhGbLY;*wMWqc z%`P8pNPe{kU6cUl0X_%q{e0V%p?%2IYs&~@^TBh)K2j%OMVMUOxc5(crQ zM^8dkpG)ysCjaR1V5wroveMoA|5(LQ;w!_a&+L|3wrngteEgpUZO2Zn=>l*IINFrb zh0BLI95*5HPwu>VbcM9z?#!7p;?qq%@R_z=Cvvj;q)`W?t8YsVWqO*fAu+5|<-++( zd@N@4n8Cbfsw}(+(|Nsf=PYB?%eJRRq}xpT_Z`V{ieZt}g0V99K^7ZWa@l$N_l&_Rv%`jdv?DxD90TDjk zsczAR6|rS-nl?yW49HvhI3HTI078@=7h|#MxWr}J^l<{D>C-j8HLA5>zbsnKKKo)Q zTe571#XPnqp#nI5!f5tG{7qy6-1!|pK8{IkN^6g7HWz)?miK$oTs^2y^U-hO#vc1?@W z^Tmr-afOxk$Op}ylq=JKb$;8IVL^l-vFX!iefzdOye+!pcUr}j;4q$v&vpb0yK##H zGviZFVZ|M1w30l5z`@Y<>+-G)`QTFrStUDJnl?yib3bYHsaFc4gQ%|qJQvV7e+Ha%ZxJiB1zC%knnGBQKsI)LbxZ)*>u-~`u;n9m1udwXd zvhd3W3E&Ka3CM(pZD0r@6|(g z7cN|8bZMAT*Rpw6w(+-LaO-bQ3liL3#D1JUPli)X9U0$*2npWJue3jOfEH&nY?-ZZ zk`WyU96NT5Em#m&`=Z=)sr(MGNl}tCZIGl{Osx%s7fA~pYT+trYU3%LUz=vU4Htr& zJcX-Mr#cH-FITPttm)6g_$93tvUVwOp+foDzWD1`iNTHQVJvIbES%T=eMcB>hGN@r zFB!S!&1pxeV-8E`lsD~#HE7VFzhj(ImMYB~#w@sHS(ay+?q_O8J^}1l9W#iE6_eGg5bSt=t$)AuFU@W z>og_FZpu`Dc3}T8Hh1;sRH>n{9(U)g`|Y21!~+ z&y&tD%0X>P8q%p(+mE&N>!~u9ao>E(sMFD@eQ&<{n+D8Be)0{Y8_=z}y!pJ@)~!3O zC9{b859FaGYA#SyfClQzmMewruvhq%qBPV)+hgr@9Xobpw4xmAA*t=Tmdb~fKN0rw zNz?dHN~&61p`~mr&ngJRyo84@M(EbPqiO*JZSJpHwSx5L-`k{+?JFhBQS;kBX|1Dj zQ3Ztww?M&s(*1{LEz&6c{SV%eX!93^Cr|O0KJ4=zzl!z5sU7^Z3zA{_^yyHWaW``0 zNO=(j=}0}3XtY4m?E9ObV$OX0+L^wru-&rk>}vS zgAG|C+QJgH%_EF87Xjb^Nv?x6uS3GWbNXHNG+=sEr588iWFZ1vW%9AAS|2l@ixwPS| QcK`qY07*qoM6N<$g6a{mVgLXD diff --git a/proxy/ui.js b/proxy/ui.js deleted file mode 100644 index f667bef..0000000 --- a/proxy/ui.js +++ /dev/null @@ -1,17 +0,0 @@ -/* -All of Snowflake's DOM manipulation and inputs. -*/ - -class UI { - - setStatus() {} - - setActive(connected) { - return this.active = connected; - } - - log() {} - -} - -UI.prototype.active = false; diff --git a/proxy/util.js b/proxy/util.js deleted file mode 100644 index 42843d7..0000000 --- a/proxy/util.js +++ /dev/null @@ -1,216 +0,0 @@ -/* exported Util, Params, DummyRateLimit */ - -/* -A JavaScript WebRTC snowflake proxy - -Contains helpers for parsing query strings and other utilities. -*/ - -class Util { - - static genSnowflakeID() { - return Math.random().toString(36).substring(2); - } - - static hasWebRTC() { - return typeof PeerConnection === 'function'; - } - - static hasCookies() { - return navigator.cookieEnabled; - } - -} - - -class Parse { - - // Parse a cookie data string (usually document.cookie). The return type is an - // object mapping cookies names to values. Returns null on error. - // http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-8747038 - static cookie(cookies) { - var i, j, len, name, result, string, strings, value; - result = {}; - strings = []; - if (cookies) { - strings = cookies.split(';'); - } - for (i = 0, len = strings.length; i < len; i++) { - string = strings[i]; - j = string.indexOf('='); - if (-1 === j) { - return null; - } - name = decodeURIComponent(string.substr(0, j).trim()); - value = decodeURIComponent(string.substr(j + 1).trim()); - if (!(name in result)) { - result[name] = value; - } - } - return result; - } - - // Parse an address in the form 'host:port'. Returns an Object with keys 'host' - // (String) and 'port' (int). Returns null on error. - static address(spec) { - var host, m, port; - m = null; - if (!m) { - // IPv6 syntax. - m = spec.match(/^\[([\0-9a-fA-F:.]+)\]:([0-9]+)$/); - } - if (!m) { - // IPv4 syntax. - m = spec.match(/^([0-9.]+):([0-9]+)$/); - } - if (!m) { - // TODO: Domain match - return null; - } - host = m[1]; - port = parseInt(m[2], 10); - if (isNaN(port) || port < 0 || port > 65535) { - return null; - } - return { - host: host, - port: port - }; - } - - // Parse a count of bytes. A suffix of 'k', 'm', or 'g' (or uppercase) - // does what you would think. Returns null on error. - static byteCount(spec) { - let matches = spec.match(/^(\d+(?:\.\d*)?)(\w*)$/); - if (matches === null) { - return null; - } - let count = Number(matches[1]); - if (isNaN(count)) { - return null; - } - const UNITS = new Map([ - ['', 1], - ['k', 1024], - ['m', 1024*1024], - ['g', 1024*1024*1024], - ]); - let unit = matches[2].toLowerCase(); - if (!UNITS.has(unit)) { - return null; - } - let multiplier = UNITS.get(unit); - return count * multiplier; - } - - // Parse a connection-address out of the "c=" Connection Data field of a - // session description. Return undefined if none is found. - // https://tools.ietf.org/html/rfc4566#section-5.7 - static ipFromSDP(sdp) { - var i, len, m, pattern, ref; - ref = [/^c=IN IP4 ([\d.]+)(?:(?:\/\d+)?\/\d+)?(:? |$)/m, /^c=IN IP6 ([0-9A-Fa-f:.]+)(?:\/\d+)?(:? |$)/m]; - for (i = 0, len = ref.length; i < len; i++) { - pattern = ref[i]; - m = pattern.exec(sdp); - if (m != null) { - return m[1]; - } - } - } - -} - - -class Params { - - static getBool(query, param, defaultValue) { - if (!query.has(param)) { - return defaultValue; - } - var val; - val = query.get(param); - if ('true' === val || '1' === val || '' === val) { - return true; - } - if ('false' === val || '0' === val) { - return false; - } - return null; - } - - // Get an object value and parse it as a byte count. Example byte counts are - // '100' and '1.3m'. Returns |defaultValue| if param is not a key. Return null - // on a parsing error. - static getByteCount(query, param, defaultValue) { - if (!query.has(param)) { - return defaultValue; - } - return Parse.byteCount(query.get(param)); - } - -} - - -class BucketRateLimit { - - constructor(capacity, time) { - this.capacity = capacity; - this.time = time; - } - - age() { - var delta, now; - now = new Date(); - delta = (now - this.lastUpdate) / 1000.0; - this.lastUpdate = now; - this.amount -= delta * this.capacity / this.time; - if (this.amount < 0.0) { - return this.amount = 0.0; - } - } - - update(n) { - this.age(); - this.amount += n; - return this.amount <= this.capacity; - } - - // How many seconds in the future will the limit expire? - when() { - this.age(); - return (this.amount - this.capacity) / (this.capacity / this.time); - } - - isLimited() { - this.age(); - return this.amount > this.capacity; - } - -} - -BucketRateLimit.prototype.amount = 0.0; - -BucketRateLimit.prototype.lastUpdate = new Date(); - - -// A rate limiter that never limits. -class DummyRateLimit { - - constructor(capacity, time) { - this.capacity = capacity; - this.time = time; - } - - update() { - return true; - } - - when() { - return 0.0; - } - - isLimited() { - return false; - } - -} diff --git a/proxy/webext/embed.js b/proxy/webext/embed.js deleted file mode 100644 index eae482f..0000000 --- a/proxy/webext/embed.js +++ /dev/null @@ -1,48 +0,0 @@ -/* global chrome, Popup */ - -// Fill i18n in HTML -window.onload = () => { - Popup.fill(document.body, (m) => { - return chrome.i18n.getMessage(m); - }); -}; - -const port = chrome.runtime.connect({ - name: "popup" -}); - -port.onMessage.addListener((m) => { - const { active, enabled, total, missingFeature } = m; - const popup = new Popup(); - - if (missingFeature) { - popup.setEnabled(false); - popup.setActive(false); - popup.setStatusText(chrome.i18n.getMessage('popupStatusOff')); - popup.setStatusDesc(chrome.i18n.getMessage(missingFeature), true); - popup.hideButton(); - return; - } - - const clients = active ? 1 : 0; - - if (enabled) { - popup.setChecked(true); - if (clients > 0) { - popup.setStatusText(chrome.i18n.getMessage('popupStatusOn', String(clients))); - } else { - popup.setStatusText(chrome.i18n.getMessage('popupStatusReady')); - } - popup.setStatusDesc((total > 0) ? chrome.i18n.getMessage('popupDescOn', String(total)) : ''); - } else { - popup.setChecked(false); - popup.setStatusText(chrome.i18n.getMessage('popupStatusOff')); - popup.setStatusDesc(""); - } - popup.setEnabled(enabled); - popup.setActive(active); -}); - -document.addEventListener('change', (event) => { - port.postMessage({ enabled: event.target.checked }); -}) diff --git a/proxy/webext/manifest.json b/proxy/webext/manifest.json deleted file mode 100644 index c3ccfa7..0000000 --- a/proxy/webext/manifest.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "manifest_version": 2, - "name": "Snowflake", - "version": "0.2.2", - "description": "__MSG_appDesc__", - "default_locale": "en_US", - "background": { - "scripts": [ - "snowflake.js" - ], - "persistent": true - }, - "browser_action": { - "default_icon": { - "48": "assets/toolbar-on-48.png", - "96": "assets/toolbar-on-96.png" - }, - "default_title": "Snowflake", - "default_popup": "embed.html" - }, - "permissions": [ - "storage" - ] -} \ No newline at end of file diff --git a/proxy/websocket.js b/proxy/websocket.js deleted file mode 100644 index da7ba94..0000000 --- a/proxy/websocket.js +++ /dev/null @@ -1,78 +0,0 @@ -/* -Only websocket-specific stuff. -*/ - -class WS { - - // Build an escaped URL string from unescaped components. Only scheme and host - // are required. See RFC 3986, section 3. - static buildUrl(scheme, host, port, path, params) { - var parts; - parts = []; - parts.push(encodeURIComponent(scheme)); - parts.push('://'); - // If it contains a colon but no square brackets, treat it as IPv6. - if (host.match(/:/) && !host.match(/[[\]]/)) { - parts.push('['); - parts.push(host); - parts.push(']'); - } else { - parts.push(encodeURIComponent(host)); - } - if (void 0 !== port && this.DEFAULT_PORTS[scheme] !== port) { - parts.push(':'); - parts.push(encodeURIComponent(port.toString())); - } - if (void 0 !== path && '' !== path) { - if (!path.match(/^\//)) { - path = '/' + path; - } - path = path.replace(/[^/]+/, function(m) { - return encodeURIComponent(m); - }); - parts.push(path); - } - if (void 0 !== params) { - parts.push('?'); - parts.push(new URLSearchParams(params).toString()); - } - return parts.join(''); - } - - static makeWebsocket(addr, params) { - var url, ws, wsProtocol; - wsProtocol = this.WSS_ENABLED ? 'wss' : 'ws'; - url = this.buildUrl(wsProtocol, addr.host, addr.port, '/', params); - ws = new WebSocket(url); - /* - 'User agents can use this as a hint for how to handle incoming binary data: - if the attribute is set to 'blob', it is safe to spool it to disk, and if it - is set to 'arraybuffer', it is likely more efficient to keep the data in - memory.' - */ - ws.binaryType = 'arraybuffer'; - return ws; - } - - static probeWebsocket(addr) { - return new Promise((resolve, reject) => { - const ws = WS.makeWebsocket(addr); - ws.onopen = () => { - resolve(); - ws.close(); - }; - ws.onerror = () => { - reject(); - ws.close(); - }; - }); - } - -} - -WS.WSS_ENABLED = true; - -WS.DEFAULT_PORTS = { - http: 80, - https: 443 -}; From da01bf232385cd1ae57e818d955990ced478b5df Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 19 Mar 2020 12:21:04 -0400 Subject: [PATCH 086/385] Remove web proxy instructions from README.md --- README.md | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/README.md b/README.md index 05fb5f7..7145248 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,6 @@ Client: - [pion/webrtc](https://github.com/pion/webrtc) - Go 1.10+ -Proxy: -- JavaScript - --- #### More Info @@ -66,32 +63,6 @@ ClientTransportPlugin snowflake exec ./client --meek ``` -#### Building - -This describes how to build the in-browser snowflake. For the client, see Usage, -above. - -The client will only work if there are browser snowflakes available. -To run your own: - -``` -cd proxy/ -npm run build -``` - -Then, start a local http server in the `proxy/build/` in any way you like. -For instance: - -``` -cd build/ -python -m http.server -``` - -Then, open a browser tab to `http://127.0.0.1:8000/embed.html` to view -the debug-console of the snowflake., -So long as that tab is open, you are an ephemeral Tor bridge. - - #### Test Environment There is a Docker-based test environment at https://github.com/cohosh/snowbox. From 3ff04c3c65bef4f10ed33c037f77a95c3e550b78 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 19 Mar 2020 12:59:49 -0400 Subject: [PATCH 087/385] Update .travis.yml for proxy/ code removal --- .travis.yml | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 56d612c..941df43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,32 +4,10 @@ dist: xenial go_import_path: git.torproject.org/pluggable-transports/snowflake.git -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-5 - - gcc-5 - go: - 1.13.x -env: - - TRAVIS_NODE_VERSION="8" CC="gcc-5" CXX="g++-5" - -before_install: - - nvm install $TRAVIS_NODE_VERSION - -install: - - pushd proxy - - npm install - - popd - script: - test -z "$(go fmt ./...)" - go vet ./... - go test -v -race ./... - - cd proxy - - npm run lint - - npm test From 20180dcb041929c1ddd37106e6e74ff8c847f2ee Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 16 Apr 2020 10:02:11 -0400 Subject: [PATCH 088/385] Rename proxy-go/ directory to proxy/ Now that the web proxies are in a different repository, no need to distinguish the two. --- README.md | 4 ++-- {proxy-go => proxy}/README.md | 2 +- {proxy-go => proxy}/proxy-go_test.go | 0 {proxy-go => proxy}/snowflake.go | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename {proxy-go => proxy}/README.md (80%) rename {proxy-go => proxy}/proxy-go_test.go (100%) rename {proxy-go => proxy}/snowflake.go (100%) diff --git a/README.md b/README.md index 7145248..d9be45b 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,9 @@ abundance of ephemeral and short-lived (and special!) volunteer proxies... ##### -- Testing with Standalone Proxy -- ``` -cd proxy-go +cd proxy go build -./proxy-go +./proxy ``` More documentation on the way. diff --git a/proxy-go/README.md b/proxy/README.md similarity index 80% rename from proxy-go/README.md rename to proxy/README.md index 264fc4f..381e3e5 100644 --- a/proxy-go/README.md +++ b/proxy/README.md @@ -1,3 +1,3 @@ This is a standalone (not browser-based) version of the Snowflake proxy. -Usage: ./proxy-go +Usage: ./proxy diff --git a/proxy-go/proxy-go_test.go b/proxy/proxy-go_test.go similarity index 100% rename from proxy-go/proxy-go_test.go rename to proxy/proxy-go_test.go diff --git a/proxy-go/snowflake.go b/proxy/snowflake.go similarity index 100% rename from proxy-go/snowflake.go rename to proxy/snowflake.go From e9b218a65cf731d0cf50a8655602ea82e67c128e Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 22 Apr 2020 11:09:32 -0400 Subject: [PATCH 089/385] Clean up .gitignore --- .gitignore | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 2d31939..9f36c7c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,20 +6,7 @@ datadir/ broker/broker client/client -server-webrtc/server-webrtc server/server -proxy-go/proxy-go +proxy/proxy snowflake.log -proxy/test -proxy/build -proxy/node_modules -proxy/snowflake-library.js -proxy/spec/support -proxy/webext/snowflake.js -proxy/webext/popup.js -proxy/webext/embed.html -proxy/webext/embed.css -proxy/webext/assets/ -proxy/webext/_locales/ ignore/ -npm-debug.log From ee2fb42d33ea105995adfc84d2be47a9d6dfc97f Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 30 Jan 2020 23:49:41 -0700 Subject: [PATCH 090/385] Immediately and unconditionally grant new SOCKS connections. --- client/lib/snowflake.go | 10 +--------- client/snowflake.go | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 2065f73..199f8a4 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -16,22 +16,14 @@ const ( // Given an accepted SOCKS connection, establish a WebRTC connection to the // remote peer and exchange traffic. -func Handler(socks SocksConnector, snowflakes SnowflakeCollector) error { +func Handler(socks net.Conn, snowflakes SnowflakeCollector) error { // Obtain an available WebRTC remote. May block. snowflake := snowflakes.Pop() if nil == snowflake { - if err := socks.Reject(); err != nil { - log.Printf("socks.Reject returned error: %v", err) - } - return errors.New("handler: Received invalid Snowflake") } defer snowflake.Close() log.Println("---- Handler: snowflake assigned ----") - err := socks.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0}) - if err != nil { - return err - } go func() { // When WebRTC resets, close the SOCKS connection too. diff --git a/client/snowflake.go b/client/snowflake.go index af8447c..dda59ae 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -59,9 +59,17 @@ func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { log.Printf("SOCKS accepted: %v", conn.Req) go func() { defer conn.Close() + + err := conn.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + log.Printf("conn.Grant error: %s", err) + return + } + err = sf.Handler(conn, snowflakes) if err != nil { log.Printf("handler error: %s", err) + return } }() } From 904af9cb8aa6aa25e094da1c025c7afed55d46ea Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 21 Feb 2020 14:47:34 -0700 Subject: [PATCH 091/385] Let copyLoop exit when either direction finishes. Formerly we waiting until *both* directions finished. What this meant in practice is that when the remote connection ended, copyLoop would become useless but would continue blocking its caller until something else finally closed the socks connection. --- client/lib/snowflake.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 199f8a4..409ce14 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -5,7 +5,6 @@ import ( "io" "log" "net" - "sync" "time" ) @@ -41,20 +40,19 @@ func Handler(socks net.Conn, snowflakes SnowflakeCollector) error { // Exchanges bytes between two ReadWriters. // (In this case, between a SOCKS and WebRTC connection.) func copyLoop(socks, webRTC io.ReadWriter) { - var wg sync.WaitGroup - wg.Add(2) + done := make(chan struct{}, 2) go func() { if _, err := io.Copy(socks, webRTC); err != nil { log.Printf("copying WebRTC to SOCKS resulted in error: %v", err) } - wg.Done() + done <- struct{}{} }() go func() { if _, err := io.Copy(webRTC, socks); err != nil { log.Printf("copying SOCKS to WebRTC resulted in error: %v", err) } - wg.Done() + done <- struct{}{} }() - wg.Wait() + <-done log.Println("copy loop ended") } From 222ab3d85a4113088db3e3b742411806922c028c Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 28 Jan 2020 02:29:34 -0700 Subject: [PATCH 092/385] Import Turbo Tunnel support code. Copied and slightly modified from https://gitweb.torproject.org/pluggable-transports/meek.git/log/?h=turbotunnel&id=7eb94209f857fc71c2155907b0462cc587fc76cc https://github.com/net4people/bbs/issues/21 RedialPacketConn is adapted from clientPacketConn in https://dip.torproject.org/dcf/obfs4/blob/c64a61c6da3bf1c2f98221bb1e1af8a358f22b87/obfs4proxy/turbotunnel_client.go https://github.com/net4people/bbs/issues/14#issuecomment-544747519 --- common/encapsulation/encapsulation.go | 194 ++++++++++++ common/encapsulation/encapsulation_test.go | 330 +++++++++++++++++++++ common/turbotunnel/clientid.go | 28 ++ common/turbotunnel/clientmap.go | 144 +++++++++ common/turbotunnel/consts.go | 13 + common/turbotunnel/queuepacketconn.go | 137 +++++++++ common/turbotunnel/redialpacketconn.go | 204 +++++++++++++ 7 files changed, 1050 insertions(+) create mode 100644 common/encapsulation/encapsulation.go create mode 100644 common/encapsulation/encapsulation_test.go create mode 100644 common/turbotunnel/clientid.go create mode 100644 common/turbotunnel/clientmap.go create mode 100644 common/turbotunnel/consts.go create mode 100644 common/turbotunnel/queuepacketconn.go create mode 100644 common/turbotunnel/redialpacketconn.go diff --git a/common/encapsulation/encapsulation.go b/common/encapsulation/encapsulation.go new file mode 100644 index 0000000..bfe9b5b --- /dev/null +++ b/common/encapsulation/encapsulation.go @@ -0,0 +1,194 @@ +// Package encapsulation implements a way of encoding variable-size chunks of +// data and padding into a byte stream. +// +// Each chunk of data or padding starts with a variable-size length prefix. One +// bit ("d") in the first byte of the prefix indicates whether the chunk +// represents data or padding (1=data, 0=padding). Another bit ("c" for +// "continuation") is the indicates whether there are more bytes in the length +// prefix. The remaining 6 bits ("x") encode part of the length value. +// dcxxxxxx +// If the continuation bit is set, then the next byte is also part of the length +// prefix. It lacks the "d" bit, has its own "c" bit, and 7 value-carrying bits +// ("y"). +// cyyyyyyy +// The length is decoded by concatenating value-carrying bits, from left to +// right, of all value-carrying bits, up to and including the first byte whose +// "c" bit is 0. Although in principle this encoding would allow for length +// prefixes of any size, length prefixes are arbitrarily limited to 3 bytes and +// any attempt to read or write a longer one is an error. These are therefore +// the only valid formats: +// 00xxxxxx xxxxxx₂ bytes of padding +// 10xxxxxx xxxxxx₂ bytes of data +// 01xxxxxx 0yyyyyyy xxxxxxyyyyyyy₂ bytes of padding +// 11xxxxxx 0yyyyyyy xxxxxxyyyyyyy₂ bytes of data +// 01xxxxxx 1yyyyyyy 0zzzzzzz xxxxxxyyyyyyyzzzzzzz₂ bytes of padding +// 11xxxxxx 1yyyyyyy 0zzzzzzz xxxxxxyyyyyyyzzzzzzz₂ bytes of data +// The maximum encodable length is 11111111111111111111₂ = 0xfffff = 1048575. +// There is no requirement to use a length prefix of minimum size; i.e. 00000100 +// and 01000000 00000100 are both valid encodings of the value 4. +// +// After the length prefix follow that many bytes of padding or data. There are +// no restrictions on the value of bytes comprising padding. +// +// The idea for this encapsulation is sketched here: +// https://github.com/net4people/bbs/issues/9#issuecomment-524095186 +package encapsulation + +import ( + "errors" + "io" + "io/ioutil" +) + +// ErrTooLong is the error returned when an encoded length prefix is longer than +// 3 bytes, or when ReadData receives an input whose length is too large to +// encode in a 3-byte length prefix. +var ErrTooLong = errors.New("length prefix is too long") + +// ReadData returns a new slice with the contents of the next available data +// chunk, skipping over any padding chunks that may come first. The returned +// error value is nil if and only if a data chunk was present and was read in +// its entirety. The returned error is io.EOF only if r ended before the first +// byte of a length prefix. If r ended in the middle of a length prefix or +// data/padding, the returned error is io.ErrUnexpectedEOF. +func ReadData(r io.Reader) ([]byte, error) { + for { + var b [1]byte + _, err := r.Read(b[:]) + if err != nil { + // This is the only place we may return a real io.EOF. + return nil, err + } + isData := (b[0] & 0x80) != 0 + moreLength := (b[0] & 0x40) != 0 + n := int(b[0] & 0x3f) + for i := 0; moreLength; i++ { + if i >= 2 { + return nil, ErrTooLong + } + _, err := r.Read(b[:]) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return nil, err + } + moreLength = (b[0] & 0x80) != 0 + n = (n << 7) | int(b[0]&0x7f) + } + if isData { + p := make([]byte, n) + _, err := io.ReadFull(r, p) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return nil, err + } + return p, err + } else { + _, err := io.CopyN(ioutil.Discard, r, int64(n)) + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return nil, err + } + } + } +} + +// dataPrefixForLength returns a length prefix for the given length, with the +// "d" bit set to 1. +func dataPrefixForLength(n int) ([]byte, error) { + switch { + case (n>>0)&0x3f == (n >> 0): + return []byte{0x80 | byte((n>>0)&0x3f)}, nil + case (n>>7)&0x3f == (n >> 7): + return []byte{0xc0 | byte((n>>7)&0x3f), byte((n >> 0) & 0x7f)}, nil + case (n>>14)&0x3f == (n >> 14): + return []byte{0xc0 | byte((n>>14)&0x3f), 0x80 | byte((n>>7)&0x7f), byte((n >> 0) & 0x7f)}, nil + default: + return nil, ErrTooLong + } +} + +// WriteData encodes a data chunk into w. It returns the total number of bytes +// written; i.e., including the length prefix. The error is ErrTooLong if the +// length of data cannot fit into a length prefix. +func WriteData(w io.Writer, data []byte) (int, error) { + prefix, err := dataPrefixForLength(len(data)) + if err != nil { + return 0, err + } + total := 0 + n, err := w.Write(prefix) + total += n + if err != nil { + return total, err + } + n, err = w.Write(data) + total += n + return total, err +} + +var paddingBuffer = make([]byte, 1024) + +// WritePadding encodes padding chunks, whose total size (including their own +// length prefixes) is n. Returns the total number of bytes written to w, which +// will be exactly n unless there was an error. The error cannot be ErrTooLong +// because this function will write multiple padding chunks if necessary to +// reach the requested size. Panics if n is negative. +func WritePadding(w io.Writer, n int) (int, error) { + if n < 0 { + panic("negative length") + } + total := 0 + for n > 0 { + p := len(paddingBuffer) + if p > n { + p = n + } + n -= p + var prefix []byte + switch { + case ((p-1)>>0)&0x3f == ((p - 1) >> 0): + p = p - 1 + prefix = []byte{byte((p >> 0) & 0x3f)} + case ((p-2)>>7)&0x3f == ((p - 2) >> 7): + p = p - 2 + prefix = []byte{0x40 | byte((p>>7)&0x3f), byte((p >> 0) & 0x7f)} + case ((p-3)>>14)&0x3f == ((p - 3) >> 14): + p = p - 3 + prefix = []byte{0x40 | byte((p>>14)&0x3f), 0x80 | byte((p>>7)&0x3f), byte((p >> 0) & 0x7f)} + } + nn, err := w.Write(prefix) + total += nn + if err != nil { + return total, err + } + nn, err = w.Write(paddingBuffer[:p]) + total += nn + if err != nil { + return total, err + } + } + return total, nil +} + +// MaxDataForSize returns the length of the longest slice that can pe passed to +// WriteData, whose total encoded size (including length prefix) is no larger +// than n. Call this to find out if a chunk of data will fit into a length +// budget. Panics if n == 0. +func MaxDataForSize(n int) int { + if n == 0 { + panic("zero length") + } + prefix, err := dataPrefixForLength(n) + if err == ErrTooLong { + return (1 << (6 + 7 + 7)) - 1 - 3 + } else if err != nil { + panic(err) + } + return n - len(prefix) +} diff --git a/common/encapsulation/encapsulation_test.go b/common/encapsulation/encapsulation_test.go new file mode 100644 index 0000000..333abb4 --- /dev/null +++ b/common/encapsulation/encapsulation_test.go @@ -0,0 +1,330 @@ +package encapsulation + +import ( + "bytes" + "io" + "math/rand" + "testing" +) + +// Return a byte slice with non-trivial contents. +func pseudorandomBuffer(n int) []byte { + source := rand.NewSource(0) + p := make([]byte, n) + for i := 0; i < len(p); i++ { + p[i] = byte(source.Int63() & 0xff) + } + return p +} + +func mustWriteData(w io.Writer, p []byte) int { + n, err := WriteData(w, p) + if err != nil { + panic(err) + } + return n +} + +func mustWritePadding(w io.Writer, n int) int { + n, err := WritePadding(w, n) + if err != nil { + panic(err) + } + return n +} + +// Test that ReadData(WriteData()) recovers the original data. +func TestRoundtrip(t *testing.T) { + // Test above and below interesting thresholds. + for _, i := range []int{ + 0x00, 0x01, + 0x3e, 0x3f, 0x40, 0x41, + 0xfe, 0xff, 0x100, 0x101, + 0x1ffe, 0x1fff, 0x2000, 0x2001, + 0xfffe, 0xffff, 0x10000, 0x10001, + 0xffffe, 0xfffff, + } { + original := pseudorandomBuffer(i) + var enc bytes.Buffer + n, err := WriteData(&enc, original) + if err != nil { + t.Fatalf("size %d, WriteData returned error %v", i, err) + } + if enc.Len() != n { + t.Fatalf("size %d, returned length was %d, written length was %d", + i, n, enc.Len()) + } + inverse, err := ReadData(&enc) + if err != nil { + t.Fatalf("size %d, ReadData returned error %v", i, err) + } + if !bytes.Equal(inverse, original) { + t.Fatalf("size %d, got <%x>, expected <%x>", i, inverse, original) + } + } +} + +// Test that WritePadding writes exactly as much as requested. +func TestPaddingLength(t *testing.T) { + // Test above and below interesting thresholds. WritePadding also gets + // values above 0xfffff, the maximum value of a single length prefix. + for _, i := range []int{ + 0x00, 0x01, + 0x3f, 0x40, 0x41, 0x42, + 0xff, 0x100, 0x101, 0x102, + 0x2000, 0x2001, 0x2002, 0x2003, + 0x10000, 0x10001, 0x10002, 0x10003, + 0x100001, 0x100002, 0x100003, 0x100004, + } { + var enc bytes.Buffer + n, err := WritePadding(&enc, i) + if err != nil { + t.Fatalf("size %d, WritePadding returned error %v", i, err) + } + if n != i { + t.Fatalf("requested %d bytes, returned %d", i, n) + } + if enc.Len() != n { + t.Fatalf("requested %d bytes, wrote %d bytes", i, enc.Len()) + } + } +} + +// Test that ReadData skips over padding. +func TestSkipPadding(t *testing.T) { + var data = [][]byte{{}, {}, []byte("hello"), {}, []byte("world")} + var enc bytes.Buffer + mustWritePadding(&enc, 10) + mustWritePadding(&enc, 100) + mustWriteData(&enc, data[0]) + mustWriteData(&enc, data[1]) + mustWritePadding(&enc, 10) + mustWriteData(&enc, data[2]) + mustWriteData(&enc, data[3]) + mustWritePadding(&enc, 10) + mustWriteData(&enc, data[4]) + mustWritePadding(&enc, 10) + mustWritePadding(&enc, 10) + for i, expected := range data { + actual, err := ReadData(&enc) + if err != nil { + t.Fatalf("slice %d, got error %v, expected %v", i, err, nil) + } + if !bytes.Equal(actual, expected) { + t.Fatalf("slice %d, got <%x>, expected <%x>", i, actual, expected) + } + } + p, err := ReadData(&enc) + if p != nil || err != io.EOF { + t.Fatalf("got (<%x>, %v), expected (%v, %v)", p, err, nil, io.EOF) + } +} + +// Test that EOF before a length prefix returns io.EOF. +func TestEOF(t *testing.T) { + p, err := ReadData(bytes.NewReader(nil)) + if p != nil || err != io.EOF { + t.Fatalf("got (<%x>, %v), expected (%v, %v)", p, err, nil, io.EOF) + } +} + +// Test that an EOF while reading a length prefix, or while reading the +// subsequent data/padding, returns io.ErrUnexpectedEOF. +func TestUnexpectedEOF(t *testing.T) { + for _, test := range [][]byte{ + {0x40}, // expecting a second length byte + {0xc0}, // expecting a second length byte + {0x41, 0x80}, // expecting a third length byte + {0xc1, 0x80}, // expecting a third length byte + {0x02}, // expecting 2 bytes of padding + {0x82}, // expecting 2 bytes of data + {0x02, 'X'}, // expecting 1 byte of padding + {0x82, 'X'}, // expecting 1 byte of data + {0x41, 0x00}, // expecting 128 bytes of padding + {0xc1, 0x00}, // expecting 128 bytes of data + {0x41, 0x00, 'X'}, // expecting 127 bytes of padding + {0xc1, 0x00, 'X'}, // expecting 127 bytes of data + {0x41, 0x80, 0x00}, // expecting 32768 bytes of padding + {0xc1, 0x80, 0x00}, // expecting 32768 bytes of data + {0x41, 0x80, 0x00, 'X'}, // expecting 32767 bytes of padding + {0xc1, 0x80, 0x00, 'X'}, // expecting 32767 bytes of data + } { + p, err := ReadData(bytes.NewReader(test)) + if p != nil || err != io.ErrUnexpectedEOF { + t.Fatalf("<%x> got (<%x>, %v), expected (%v, %v)", test, p, err, nil, io.ErrUnexpectedEOF) + } + } +} + +// Test that length encodings that are longer than they could be are still +// interpreted. +func TestNonMinimalLengthEncoding(t *testing.T) { + for _, test := range []struct { + enc []byte + expected []byte + }{ + {[]byte{0x81, 'X'}, []byte("X")}, + {[]byte{0xc0, 0x01, 'X'}, []byte("X")}, + {[]byte{0xc0, 0x80, 0x01, 'X'}, []byte("X")}, + } { + p, err := ReadData(bytes.NewReader(test.enc)) + if err != nil { + t.Fatalf("<%x> got error %v, expected %v", test.enc, err, nil) + } + if !bytes.Equal(p, test.expected) { + t.Fatalf("<%x> got <%x>, expected <%x>", test.enc, p, test.expected) + } + } +} + +// Test that ReadData only reads up to 3 bytes of length prefix. +func TestReadLimits(t *testing.T) { + // Test the maximum length that's possible with 3 bytes of length + // prefix. + maxLength := (0x3f << 14) | (0x7f << 7) | 0x7f + data := bytes.Repeat([]byte{'X'}, maxLength) + prefix := []byte{0xff, 0xff, 0x7f} // encodes 0xfffff + p, err := ReadData(bytes.NewReader(append(prefix, data...))) + if err != nil { + t.Fatalf("got error %v, expected %v", err, nil) + } + if !bytes.Equal(p, data) { + t.Fatalf("got %d bytes unequal to %d bytes", len(p), len(data)) + } + // Test a 4-byte prefix. + prefix = []byte{0xc0, 0xc0, 0x80, 0x80} // encodes 0x100000 + data = bytes.Repeat([]byte{'X'}, maxLength+1) + p, err = ReadData(bytes.NewReader(append(prefix, data...))) + if p != nil || err != ErrTooLong { + t.Fatalf("got (<%x>, %v), expected (%v, %v)", p, err, nil, ErrTooLong) + } + // Test that 4 bytes don't work, even when they encode an integer that + // would fix in 3 bytes. + prefix = []byte{0xc0, 0x80, 0x80, 0x80} // encodes 0x0 + data = []byte{} + p, err = ReadData(bytes.NewReader(append(prefix, data...))) + if p != nil || err != ErrTooLong { + t.Fatalf("got (<%x>, %v), expected (%v, %v)", p, err, nil, ErrTooLong) + } + + // Do the same tests with padding lengths. + data = []byte("hello") + prefix = []byte{0x7f, 0xff, 0x7f} // encodes 0xfffff + padding := bytes.Repeat([]byte{'X'}, maxLength) + enc := bytes.NewBuffer(append(prefix, padding...)) + mustWriteData(enc, data) + p, err = ReadData(enc) + if err != nil { + t.Fatalf("got error %v, expected %v", err, nil) + } + if !bytes.Equal(p, data) { + t.Fatalf("got <%x>, expected <%x>", p, data) + } + prefix = []byte{0x40, 0xc0, 0x80, 0x80} // encodes 0x100000 + padding = bytes.Repeat([]byte{'X'}, maxLength+1) + enc = bytes.NewBuffer(append(prefix, padding...)) + mustWriteData(enc, data) + p, err = ReadData(enc) + if p != nil || err != ErrTooLong { + t.Fatalf("got (<%x>, %v), expected (%v, %v)", p, err, nil, ErrTooLong) + } + prefix = []byte{0x40, 0x80, 0x80, 0x80} // encodes 0x0 + padding = []byte{} + enc = bytes.NewBuffer(append(prefix, padding...)) + mustWriteData(enc, data) + p, err = ReadData(enc) + if p != nil || err != ErrTooLong { + t.Fatalf("got (<%x>, %v), expected (%v, %v)", p, err, nil, ErrTooLong) + } +} + +// Test that WriteData and WritePadding only accept lengths that can be encoded +// in up to 3 bytes of length prefix. +func TestWriteLimits(t *testing.T) { + maxLength := (0x3f << 14) | (0x7f << 7) | 0x7f + var enc bytes.Buffer + n, err := WriteData(&enc, bytes.Repeat([]byte{'X'}, maxLength)) + if n != maxLength+3 || err != nil { + t.Fatalf("got (%d, %v), expected (%d, %v)", n, err, maxLength, nil) + } + enc.Reset() + n, err = WriteData(&enc, bytes.Repeat([]byte{'X'}, maxLength+1)) + if n != 0 || err != ErrTooLong { + t.Fatalf("got (%d, %v), expected (%d, %v)", n, err, 0, ErrTooLong) + } + + // Padding gets an extra 3 bytes because the prefix is counted as part + // of the length. + enc.Reset() + n, err = WritePadding(&enc, maxLength+3) + if n != maxLength+3 || err != nil { + t.Fatalf("got (%d, %v), expected (%d, %v)", n, err, maxLength+3, nil) + } + // Writing a too-long padding is okay because WritePadding will break it + // into smaller chunks. + enc.Reset() + n, err = WritePadding(&enc, maxLength+4) + if n != maxLength+4 || err != nil { + t.Fatalf("got (%d, %v), expected (%d, %v)", n, err, maxLength+4, nil) + } +} + +// Test that WritePadding panics when given a negative length. +func TestNegativeLength(t *testing.T) { + for _, n := range []int{-1, ^0} { + var enc bytes.Buffer + panicked, nn, err := testNegativeLengthSub(t, &enc, n) + if !panicked { + t.Fatalf("WritePadding(%d) returned (%d, %v) instead of panicking", n, nn, err) + } + } +} + +// Calls WritePadding(w, n) and augments the return value with a flag indicating +// whether the call panicked. +func testNegativeLengthSub(t *testing.T, w io.Writer, n int) (panicked bool, nn int, err error) { + defer func() { + if r := recover(); r != nil { + panicked = true + } + }() + t.Helper() + nn, err = WritePadding(w, n) + return false, n, err +} + +// Test that MaxDataForSize panics when given a 0 length. +func TestMaxDataForSizeZero(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("didn't panic") + } + }() + MaxDataForSize(0) +} + +// Test thresholds of available sizes for MaxDataForSize. +func TestMaxDataForSize(t *testing.T) { + for _, test := range []struct { + size int + expected int + }{ + {0x01, 0x00}, + {0x02, 0x01}, + {0x3f, 0x3e}, + {0x40, 0x3e}, + {0x41, 0x3f}, + {0x1fff, 0x1ffd}, + {0x2000, 0x1ffd}, + {0x2001, 0x1ffe}, + {0xfffff, 0xffffc}, + {0x100000, 0xffffc}, + {0x100001, 0xffffc}, + {0x7fffffff, 0xffffc}, + } { + max := MaxDataForSize(test.size) + if max != test.expected { + t.Fatalf("size %d, got %d, expected %d", test.size, max, test.expected) + } + } +} diff --git a/common/turbotunnel/clientid.go b/common/turbotunnel/clientid.go new file mode 100644 index 0000000..17257e1 --- /dev/null +++ b/common/turbotunnel/clientid.go @@ -0,0 +1,28 @@ +package turbotunnel + +import ( + "crypto/rand" + "encoding/hex" +) + +// ClientID is an abstract identifier that binds together all the communications +// belonging to a single client session, even though those communications may +// arrive from multiple IP addresses or over multiple lower-level connections. +// It plays the same role that an (IP address, port number) tuple plays in a +// net.UDPConn: it's the return address pertaining to a long-lived abstract +// client session. The client attaches its ClientID to each of its +// communications, enabling the server to disambiguate requests among its many +// clients. ClientID implements the net.Addr interface. +type ClientID [8]byte + +func NewClientID() ClientID { + var id ClientID + _, err := rand.Read(id[:]) + if err != nil { + panic(err) + } + return id +} + +func (id ClientID) Network() string { return "clientid" } +func (id ClientID) String() string { return hex.EncodeToString(id[:]) } diff --git a/common/turbotunnel/clientmap.go b/common/turbotunnel/clientmap.go new file mode 100644 index 0000000..fa12915 --- /dev/null +++ b/common/turbotunnel/clientmap.go @@ -0,0 +1,144 @@ +package turbotunnel + +import ( + "container/heap" + "net" + "sync" + "time" +) + +// clientRecord is a record of a recently seen client, with the time it was last +// seen and a send queue. +type clientRecord struct { + Addr net.Addr + LastSeen time.Time + SendQueue chan []byte +} + +// ClientMap manages a mapping of live clients (keyed by address, which will be +// a ClientID) to their respective send queues. ClientMap's functions are safe +// to call from multiple goroutines. +type ClientMap struct { + // We use an inner structure to avoid exposing public heap.Interface + // functions to users of clientMap. + inner clientMapInner + // Synchronizes access to inner. + lock sync.Mutex +} + +// NewClientMap creates a ClientMap that expires clients after a timeout. +// +// The timeout does not have to be kept in sync with QUIC's internal idle +// timeout. If a client is removed from the client map while the QUIC session is +// still live, the worst that can happen is a loss of whatever packets were in +// the send queue at the time. If QUIC later decides to send more packets to the +// same client, we'll instantiate a new send queue, and if the client ever +// connects again with the proper client ID, we'll deliver them. +func NewClientMap(timeout time.Duration) *ClientMap { + m := &ClientMap{ + inner: clientMapInner{ + byAge: make([]*clientRecord, 0), + byAddr: make(map[net.Addr]int), + }, + } + go func() { + for { + time.Sleep(timeout / 2) + now := time.Now() + m.lock.Lock() + m.inner.removeExpired(now, timeout) + m.lock.Unlock() + } + }() + return m +} + +// SendQueue returns the send queue corresponding to addr, creating it if +// necessary. +func (m *ClientMap) SendQueue(addr net.Addr) chan []byte { + m.lock.Lock() + defer m.lock.Unlock() + return m.inner.SendQueue(addr, time.Now()) +} + +// clientMapInner is the inner type of ClientMap, implementing heap.Interface. +// byAge is the backing store, a heap ordered by LastSeen time, to facilitate +// expiring old client records. byAddr is a map from addresses (i.e., ClientIDs) +// to heap indices, to allow looking up by address. Unlike ClientMap, +// clientMapInner requires external synchonization. +type clientMapInner struct { + byAge []*clientRecord + byAddr map[net.Addr]int +} + +// removeExpired removes all client records whose LastSeen timestamp is more +// than timeout in the past. +func (inner *clientMapInner) removeExpired(now time.Time, timeout time.Duration) { + for len(inner.byAge) > 0 && now.Sub(inner.byAge[0].LastSeen) >= timeout { + heap.Pop(inner) + } +} + +// SendQueue finds the existing client record corresponding to addr, or creates +// a new one if none exists yet. It updates the client record's LastSeen time +// and returns its SendQueue. +func (inner *clientMapInner) SendQueue(addr net.Addr, now time.Time) chan []byte { + var record *clientRecord + i, ok := inner.byAddr[addr] + if ok { + // Found one, update its LastSeen. + record = inner.byAge[i] + record.LastSeen = now + heap.Fix(inner, i) + } else { + // Not found, create a new one. + record = &clientRecord{ + Addr: addr, + LastSeen: now, + SendQueue: make(chan []byte, queueSize), + } + heap.Push(inner, record) + } + return record.SendQueue +} + +// heap.Interface for clientMapInner. + +func (inner *clientMapInner) Len() int { + if len(inner.byAge) != len(inner.byAddr) { + panic("inconsistent clientMap") + } + return len(inner.byAge) +} + +func (inner *clientMapInner) Less(i, j int) bool { + return inner.byAge[i].LastSeen.Before(inner.byAge[j].LastSeen) +} + +func (inner *clientMapInner) Swap(i, j int) { + inner.byAge[i], inner.byAge[j] = inner.byAge[j], inner.byAge[i] + inner.byAddr[inner.byAge[i].Addr] = i + inner.byAddr[inner.byAge[j].Addr] = j +} + +func (inner *clientMapInner) Push(x interface{}) { + record := x.(*clientRecord) + if _, ok := inner.byAddr[record.Addr]; ok { + panic("duplicate address in clientMap") + } + // Insert into byAddr map. + inner.byAddr[record.Addr] = len(inner.byAge) + // Insert into byAge slice. + inner.byAge = append(inner.byAge, record) +} + +func (inner *clientMapInner) Pop() interface{} { + n := len(inner.byAddr) + // Remove from byAge slice. + record := inner.byAge[n-1] + inner.byAge[n-1] = nil + inner.byAge = inner.byAge[:n-1] + // Remove from byAddr map. + delete(inner.byAddr, record.Addr) + return record +} diff --git a/common/turbotunnel/consts.go b/common/turbotunnel/consts.go new file mode 100644 index 0000000..4699d1d --- /dev/null +++ b/common/turbotunnel/consts.go @@ -0,0 +1,13 @@ +// Package turbotunnel provides support for overlaying a virtual net.PacketConn +// on some other network carrier. +// +// https://github.com/net4people/bbs/issues/9 +package turbotunnel + +import "errors" + +// The size of receive and send queues. +const queueSize = 32 + +var errClosedPacketConn = errors.New("operation on closed connection") +var errNotImplemented = errors.New("not implemented") diff --git a/common/turbotunnel/queuepacketconn.go b/common/turbotunnel/queuepacketconn.go new file mode 100644 index 0000000..14a9833 --- /dev/null +++ b/common/turbotunnel/queuepacketconn.go @@ -0,0 +1,137 @@ +package turbotunnel + +import ( + "net" + "sync" + "sync/atomic" + "time" +) + +// taggedPacket is a combination of a []byte and a net.Addr, encapsulating the +// return type of PacketConn.ReadFrom. +type taggedPacket struct { + P []byte + Addr net.Addr +} + +// QueuePacketConn implements net.PacketConn by storing queues of packets. There +// is one incoming queue (where packets are additionally tagged by the source +// address of the client that sent them). There are many outgoing queues, one +// for each client address that has been recently seen. The QueueIncoming method +// inserts a packet into the incoming queue, to eventually be returned by +// ReadFrom. WriteTo inserts a packet into an address-specific outgoing queue, +// which can later by accessed through the OutgoingQueue method. +type QueuePacketConn struct { + clients *ClientMap + localAddr net.Addr + recvQueue chan taggedPacket + closeOnce sync.Once + closed chan struct{} + // What error to return when the QueuePacketConn is closed. + err atomic.Value +} + +// NewQueuePacketConn makes a new QueuePacketConn, set to track recent clients +// for at least a duration of timeout. +func NewQueuePacketConn(localAddr net.Addr, timeout time.Duration) *QueuePacketConn { + return &QueuePacketConn{ + clients: NewClientMap(timeout), + localAddr: localAddr, + recvQueue: make(chan taggedPacket, queueSize), + closed: make(chan struct{}), + } +} + +// QueueIncoming queues and incoming packet and its source address, to be +// returned in a future call to ReadFrom. +func (c *QueuePacketConn) QueueIncoming(p []byte, addr net.Addr) { + select { + case <-c.closed: + // If we're closed, silently drop it. + return + default: + } + // Copy the slice so that the caller may reuse it. + buf := make([]byte, len(p)) + copy(buf, p) + select { + case c.recvQueue <- taggedPacket{buf, addr}: + default: + // Drop the incoming packet if the receive queue is full. + } +} + +// OutgoingQueue returns the queue of outgoing packets corresponding to addr, +// creating it if necessary. The contents of the queue will be packets that are +// written to the address in question using WriteTo. +func (c *QueuePacketConn) OutgoingQueue(addr net.Addr) <-chan []byte { + return c.clients.SendQueue(addr) +} + +// ReadFrom returns a packet and address previously stored by QueueIncoming. +func (c *QueuePacketConn) ReadFrom(p []byte) (int, net.Addr, error) { + select { + case <-c.closed: + return 0, nil, &net.OpError{Op: "read", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)} + default: + } + select { + case <-c.closed: + return 0, nil, &net.OpError{Op: "read", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)} + case packet := <-c.recvQueue: + return copy(p, packet.P), packet.Addr, nil + } +} + +// WriteTo queues an outgoing packet for the given address. The queue can later +// be retrieved using the OutgoingQueue method. +func (c *QueuePacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { + select { + case <-c.closed: + return 0, &net.OpError{Op: "write", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)} + default: + } + // Copy the slice so that the caller may reuse it. + buf := make([]byte, len(p)) + copy(buf, p) + select { + case c.clients.SendQueue(addr) <- buf: + return len(buf), nil + default: + // Drop the outgoing packet if the send queue is full. + return len(buf), nil + } +} + +// closeWithError unblocks pending operations and makes future operations fail +// with the given error. If err is nil, it becomes errClosedPacketConn. +func (c *QueuePacketConn) closeWithError(err error) error { + var newlyClosed bool + c.closeOnce.Do(func() { + newlyClosed = true + // Store the error to be returned by future PacketConn + // operations. + if err == nil { + err = errClosedPacketConn + } + c.err.Store(err) + close(c.closed) + }) + if !newlyClosed { + return &net.OpError{Op: "close", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)} + } + return nil +} + +// Close unblocks pending operations and makes future operations fail with a +// "closed connection" error. +func (c *QueuePacketConn) Close() error { + return c.closeWithError(nil) +} + +// LocalAddr returns the localAddr value that was passed to NewQueuePacketConn. +func (c *QueuePacketConn) LocalAddr() net.Addr { return c.localAddr } + +func (c *QueuePacketConn) SetDeadline(t time.Time) error { return errNotImplemented } +func (c *QueuePacketConn) SetReadDeadline(t time.Time) error { return errNotImplemented } +func (c *QueuePacketConn) SetWriteDeadline(t time.Time) error { return errNotImplemented } diff --git a/common/turbotunnel/redialpacketconn.go b/common/turbotunnel/redialpacketconn.go new file mode 100644 index 0000000..cf6a8c9 --- /dev/null +++ b/common/turbotunnel/redialpacketconn.go @@ -0,0 +1,204 @@ +package turbotunnel + +import ( + "context" + "errors" + "net" + "sync" + "sync/atomic" + "time" +) + +// RedialPacketConn implements a long-lived net.PacketConn atop a sequence of +// other, transient net.PacketConns. RedialPacketConn creates a new +// net.PacketConn by calling a provided dialContext function. Whenever the +// net.PacketConn experiences a ReadFrom or WriteTo error, RedialPacketConn +// calls the dialContext function again and starts sending and receiving packets +// on the new net.PacketConn. RedialPacketConn's own ReadFrom and WriteTo +// methods return an error only when the dialContext function returns an error. +// +// RedialPacketConn uses static local and remote addresses that are independent +// of those of any dialed net.PacketConn. +type RedialPacketConn struct { + localAddr net.Addr + remoteAddr net.Addr + dialContext func(context.Context) (net.PacketConn, error) + recvQueue chan []byte + sendQueue chan []byte + closed chan struct{} + closeOnce sync.Once + // The first dial error, which causes the clientPacketConn to be + // closed and is returned from future read/write operations. Compare to + // the rerr and werr in io.Pipe. + err atomic.Value +} + +// NewQueuePacketConn makes a new RedialPacketConn, with the given static local +// and remote addresses, and dialContext function. +func NewRedialPacketConn( + localAddr, remoteAddr net.Addr, + dialContext func(context.Context) (net.PacketConn, error), +) *RedialPacketConn { + c := &RedialPacketConn{ + localAddr: localAddr, + remoteAddr: remoteAddr, + dialContext: dialContext, + recvQueue: make(chan []byte, queueSize), + sendQueue: make(chan []byte, queueSize), + closed: make(chan struct{}), + err: atomic.Value{}, + } + go c.dialLoop() + return c +} + +// dialLoop repeatedly calls c.dialContext and passes the resulting +// net.PacketConn to c.exchange. It returns only when c is closed or dialContext +// returns an error. +func (c *RedialPacketConn) dialLoop() { + ctx, cancel := context.WithCancel(context.Background()) + for { + select { + case <-c.closed: + cancel() + return + default: + } + conn, err := c.dialContext(ctx) + if err != nil { + c.closeWithError(err) + cancel() + return + } + c.exchange(conn) + conn.Close() + } +} + +// exchange calls ReadFrom on the given net.PacketConn and places the resulting +// packets in the receive queue, and takes packets from the send queue and calls +// WriteTo on them, making the current net.PacketConn active. +func (c *RedialPacketConn) exchange(conn net.PacketConn) { + readErrCh := make(chan error) + writeErrCh := make(chan error) + + go func() { + defer close(readErrCh) + for { + select { + case <-c.closed: + return + case <-writeErrCh: + return + default: + } + + var buf [1500]byte + n, _, err := conn.ReadFrom(buf[:]) + if err != nil { + readErrCh <- err + return + } + p := make([]byte, n) + copy(p, buf[:]) + select { + case c.recvQueue <- p: + default: // OK to drop packets. + } + } + }() + + go func() { + defer close(writeErrCh) + for { + select { + case <-c.closed: + return + case <-readErrCh: + return + case p := <-c.sendQueue: + _, err := conn.WriteTo(p, c.remoteAddr) + if err != nil { + writeErrCh <- err + return + } + } + } + }() + + select { + case <-readErrCh: + case <-writeErrCh: + } +} + +// ReadFrom reads a packet from the currently active net.PacketConn. The +// packet's original remote address is replaced with the RedialPacketConn's own +// remote address. +func (c *RedialPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { + select { + case <-c.closed: + return 0, nil, &net.OpError{Op: "read", Net: c.LocalAddr().Network(), Source: c.LocalAddr(), Addr: c.remoteAddr, Err: c.err.Load().(error)} + default: + } + select { + case <-c.closed: + return 0, nil, &net.OpError{Op: "read", Net: c.LocalAddr().Network(), Source: c.LocalAddr(), Addr: c.remoteAddr, Err: c.err.Load().(error)} + case buf := <-c.recvQueue: + return copy(p, buf), c.remoteAddr, nil + } +} + +// WriteTo writes a packet to the currently active net.PacketConn. The addr +// argument is ignored and instead replaced with the RedialPacketConn's own +// remote address. +func (c *RedialPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { + // addr is ignored. + select { + case <-c.closed: + return 0, &net.OpError{Op: "write", Net: c.LocalAddr().Network(), Source: c.LocalAddr(), Addr: c.remoteAddr, Err: c.err.Load().(error)} + default: + } + buf := make([]byte, len(p)) + copy(buf, p) + select { + case c.sendQueue <- buf: + return len(buf), nil + default: + // Drop the outgoing packet if the send queue is full. + return len(buf), nil + } +} + +// closeWithError unblocks pending operations and makes future operations fail +// with the given error. If err is nil, it becomes errClosedPacketConn. +func (c *RedialPacketConn) closeWithError(err error) error { + var once bool + c.closeOnce.Do(func() { + // Store the error to be returned by future read/write + // operations. + if err == nil { + err = errors.New("operation on closed connection") + } + c.err.Store(err) + close(c.closed) + once = true + }) + if !once { + return &net.OpError{Op: "close", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)} + } + return nil +} + +// Close unblocks pending operations and makes future operations fail with a +// "closed connection" error. +func (c *RedialPacketConn) Close() error { + return c.closeWithError(nil) +} + +// LocalAddr returns the localAddr value that was passed to NewRedialPacketConn. +func (c *RedialPacketConn) LocalAddr() net.Addr { return c.localAddr } + +func (c *RedialPacketConn) SetDeadline(t time.Time) error { return errNotImplemented } +func (c *RedialPacketConn) SetReadDeadline(t time.Time) error { return errNotImplemented } +func (c *RedialPacketConn) SetWriteDeadline(t time.Time) error { return errNotImplemented } From 70126177fbdf5b1fa4977f2fc26f624641708098 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 28 Jan 2020 02:32:02 -0700 Subject: [PATCH 093/385] Turbo Tunnel client and server. The client opts into turbotunnel mode by sending a magic token at the beginning of each WebSocket connection (before sending even the ClientID). The token is just a random byte string I generated. The server peeks at the token and, if it matches, uses turbotunnel mode. Otherwise, it unreads the token and continues in the old one-session-per-WebSocket mode. --- client/lib/snowflake.go | 100 ++++++++++++--- client/lib/turbotunnel.go | 68 +++++++++++ common/turbotunnel/consts.go | 4 + go.mod | 4 +- go.sum | 21 +++- server/server.go | 231 ++++++++++++++++++++++++++++++++++- 6 files changed, 399 insertions(+), 29 deletions(-) create mode 100644 client/lib/turbotunnel.go diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 409ce14..4b7dd4d 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -1,11 +1,16 @@ package lib import ( + "context" "errors" "io" "log" "net" "time" + + "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" + "github.com/xtaci/kcp-go/v5" + "github.com/xtaci/smux" ) const ( @@ -13,43 +18,98 @@ const ( SnowflakeTimeout = 30 * time.Second ) +type dummyAddr struct{} + +func (addr dummyAddr) Network() string { return "dummy" } +func (addr dummyAddr) String() string { return "dummy" } + // Given an accepted SOCKS connection, establish a WebRTC connection to the // remote peer and exchange traffic. func Handler(socks net.Conn, snowflakes SnowflakeCollector) error { - // Obtain an available WebRTC remote. May block. - snowflake := snowflakes.Pop() - if nil == snowflake { - return errors.New("handler: Received invalid Snowflake") + clientID := turbotunnel.NewClientID() + + // We build a persistent KCP session on a sequence of ephemeral WebRTC + // connections. This dialContext tells RedialPacketConn how to get a new + // WebRTC connection when the previous one dies. Inside each WebRTC + // connection, we use EncapsulationPacketConn to encode packets into a + // stream. + dialContext := func(ctx context.Context) (net.PacketConn, error) { + log.Printf("redialing on same connection") + // Obtain an available WebRTC remote. May block. + conn := snowflakes.Pop() + if conn == nil { + return nil, errors.New("handler: Received invalid Snowflake") + } + log.Println("---- Handler: snowflake assigned ----") + // Send the magic Turbo Tunnel token. + _, err := conn.Write(turbotunnel.Token[:]) + if err != nil { + return nil, err + } + // Send ClientID prefix. + _, err = conn.Write(clientID[:]) + if err != nil { + return nil, err + } + return NewEncapsulationPacketConn(dummyAddr{}, dummyAddr{}, conn), nil } - defer snowflake.Close() - log.Println("---- Handler: snowflake assigned ----") + pconn := turbotunnel.NewRedialPacketConn(dummyAddr{}, dummyAddr{}, dialContext) + defer pconn.Close() - go func() { - // When WebRTC resets, close the SOCKS connection too. - snowflake.WaitForReset() - socks.Close() - }() + // conn is built on the underlying RedialPacketConn—when one WebRTC + // connection dies, another one will be found to take its place. The + // sequence of packets across multiple WebRTC connections drives the KCP + // engine. + conn, err := kcp.NewConn2(dummyAddr{}, nil, 0, 0, pconn) + if err != nil { + return err + } + defer conn.Close() + // Permit coalescing the payloads of consecutive sends. + conn.SetStreamMode(true) + // Disable the dynamic congestion window (limit only by the + // maximum of local and remote static windows). + conn.SetNoDelay( + 0, // default nodelay + 0, // default interval + 0, // default resend + 1, // nc=1 => congestion window off + ) + // On the KCP connection we overlay an smux session and stream. + smuxConfig := smux.DefaultConfig() + smuxConfig.Version = 2 + smuxConfig.KeepAliveTimeout = 10 * time.Minute + sess, err := smux.Client(conn, smuxConfig) + if err != nil { + return err + } + defer sess.Close() + stream, err := sess.OpenStream() + if err != nil { + return err + } + defer stream.Close() - // Begin exchanging data. Either WebRTC or localhost SOCKS will close first. - // In eithercase, this closes the handler and induces a new handler. - copyLoop(socks, snowflake) - log.Println("---- Handler: closed ---") + // Begin exchanging data. + log.Printf("---- Handler: begin stream %v ---", stream.ID()) + copyLoop(socks, stream) + log.Printf("---- Handler: closed stream %v ---", stream.ID()) return nil } // Exchanges bytes between two ReadWriters. -// (In this case, between a SOCKS and WebRTC connection.) -func copyLoop(socks, webRTC io.ReadWriter) { +// (In this case, between a SOCKS connection and smux stream.) +func copyLoop(socks, stream io.ReadWriter) { done := make(chan struct{}, 2) go func() { - if _, err := io.Copy(socks, webRTC); err != nil { + if _, err := io.Copy(socks, stream); err != nil { log.Printf("copying WebRTC to SOCKS resulted in error: %v", err) } done <- struct{}{} }() go func() { - if _, err := io.Copy(webRTC, socks); err != nil { - log.Printf("copying SOCKS to WebRTC resulted in error: %v", err) + if _, err := io.Copy(stream, socks); err != nil { + log.Printf("copying SOCKS to stream resulted in error: %v", err) } done <- struct{}{} }() diff --git a/client/lib/turbotunnel.go b/client/lib/turbotunnel.go new file mode 100644 index 0000000..aad2e6a --- /dev/null +++ b/client/lib/turbotunnel.go @@ -0,0 +1,68 @@ +package lib + +import ( + "bufio" + "errors" + "io" + "net" + "time" + + "git.torproject.org/pluggable-transports/snowflake.git/common/encapsulation" +) + +var errNotImplemented = errors.New("not implemented") + +// EncapsulationPacketConn implements the net.PacketConn interface over an +// io.ReadWriteCloser stream, using the encapsulation package to represent +// packets in a stream. +type EncapsulationPacketConn struct { + io.ReadWriteCloser + localAddr net.Addr + remoteAddr net.Addr + bw *bufio.Writer +} + +// NewEncapsulationPacketConn makes +func NewEncapsulationPacketConn( + localAddr, remoteAddr net.Addr, + conn io.ReadWriteCloser, +) *EncapsulationPacketConn { + return &EncapsulationPacketConn{ + ReadWriteCloser: conn, + localAddr: localAddr, + remoteAddr: remoteAddr, + bw: bufio.NewWriter(conn), + } +} + +// ReadFrom reads an encapsulated packet from the stream. +func (c *EncapsulationPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { + data, err := encapsulation.ReadData(c.ReadWriteCloser) + if err != nil { + return 0, c.remoteAddr, err + } + return copy(p, data), c.remoteAddr, nil +} + +// WriteTo writes an encapsulated packet to the stream. +func (c *EncapsulationPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { + // addr is ignored. + _, err := encapsulation.WriteData(c.bw, p) + if err == nil { + err = c.bw.Flush() + } + if err != nil { + return 0, err + } + return len(p), nil +} + +// LocalAddr returns the localAddr value that was passed to +// NewEncapsulationPacketConn. +func (c *EncapsulationPacketConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *EncapsulationPacketConn) SetDeadline(t time.Time) error { return errNotImplemented } +func (c *EncapsulationPacketConn) SetReadDeadline(t time.Time) error { return errNotImplemented } +func (c *EncapsulationPacketConn) SetWriteDeadline(t time.Time) error { return errNotImplemented } diff --git a/common/turbotunnel/consts.go b/common/turbotunnel/consts.go index 4699d1d..80f70af 100644 --- a/common/turbotunnel/consts.go +++ b/common/turbotunnel/consts.go @@ -6,6 +6,10 @@ package turbotunnel import "errors" +// This magic prefix is how a client opts into turbo tunnel mode. It is just a +// randomly generated byte string. +var Token = [8]byte{0x12, 0x93, 0x60, 0x5d, 0x27, 0x81, 0x75, 0xf5} + // The size of receive and send queues. const queueSize = 32 diff --git a/go.mod b/go.mod index 4366d6a..6502651 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,5 @@ module git.torproject.org/pluggable-transports/snowflake.git -go 1.13 - require ( git.torproject.org/pluggable-transports/goptlib.git v1.1.0 github.com/golang/protobuf v1.3.1 // indirect @@ -9,6 +7,8 @@ require ( github.com/pion/sdp/v2 v2.3.4 github.com/pion/webrtc/v2 v2.2.2 github.com/smartystreets/goconvey v1.6.4 + github.com/xtaci/kcp-go/v5 v5.5.12 + github.com/xtaci/smux v1.5.12 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa golang.org/x/text v0.3.2 // indirect diff --git a/go.sum b/go.sum index 3708fc0..6768e02 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,10 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/cpuid v1.2.2 h1:1xAgYebNnsb9LKCdLOvFWtAxGU/33mjJtyOVbmUa0Us= +github.com/klauspost/cpuid v1.2.2/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY= +github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -82,14 +86,28 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY= +github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/xorsimd v0.4.1 h1:iUZcywbOYDRAZUasAs2eSCUW8eobuZDy0I9FJiORkVg= +github.com/templexxx/xorsimd v0.4.1/go.mod h1:W+ffZz8jJMH2SXwuKu9WhygqBMbFnp14G2fqEr8qaNo= +github.com/tjfoc/gmsm v1.0.1 h1:R11HlqhXkDospckjZEihx9SW/2VW0RgdwrykyWMFOQU= +github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc= +github.com/xtaci/kcp-go/v5 v5.5.12 h1:iALGyvti/oBbl1TbVoUpHEUHCorDEb3tEKl1CPY3KXM= +github.com/xtaci/kcp-go/v5 v5.5.12/go.mod h1:H0T/EJ+lPNytnFYsKLH0JHUtiwZjG3KXlTM6c+Q4YUo= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= +github.com/xtaci/smux v1.5.12 h1:n9OGjdqQuVZXLh46+L4IR5tR2wvuUFwRABnN/V55bIY= +github.com/xtaci/smux v1.5.12/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U= golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= @@ -99,7 +117,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/server/server.go b/server/server.go index c03e41c..028a24d 100644 --- a/server/server.go +++ b/server/server.go @@ -3,6 +3,8 @@ package main import ( + "bufio" + "bytes" "crypto/tls" "flag" "fmt" @@ -20,9 +22,13 @@ import ( "time" pt "git.torproject.org/pluggable-transports/goptlib.git" + "git.torproject.org/pluggable-transports/snowflake.git/common/encapsulation" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" + "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" "github.com/gorilla/websocket" + "github.com/xtaci/kcp-go/v5" + "github.com/xtaci/smux" "golang.org/x/crypto/acme/autocert" "golang.org/x/net/http2" ) @@ -30,6 +36,13 @@ import ( const ptMethodName = "snowflake" const requestTimeout = 10 * time.Second +// How long to remember outgoing packets for a client, when we don't currently +// have an active WebSocket connection corresponding to that client. Because a +// client session may span multiple WebSocket connections, we keep packets we +// aren't able to send immediately in memory, for a little while but not +// indefinitely. +const clientMapTimeout = 1 * time.Minute + // How long to wait for ListenAndServe or ListenAndServeTLS to return an error // before deciding that it's not going to return. const listenAndServeErrorTimeout = 100 * time.Millisecond @@ -49,8 +62,8 @@ additional HTTP listener on port 80 to work with ACME. flag.PrintDefaults() } -// Copy from WebSocket to socket and vice versa. -func proxy(local *net.TCPConn, conn *websocketconn.Conn) { +// Copy from one stream to another. +func proxy(local *net.TCPConn, conn net.Conn) { var wg sync.WaitGroup wg.Add(2) @@ -101,7 +114,23 @@ var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } -type HTTPHandler struct{} +// overrideReadConn is a net.Conn with an overridden Read method. Compare to +// recordingConn at +// https://dave.cheney.net/2015/05/22/struct-composition-with-go. +type overrideReadConn struct { + net.Conn + io.Reader +} + +func (conn *overrideReadConn) Read(p []byte) (int, error) { + return conn.Reader.Read(p) +} + +type HTTPHandler struct { + // pconn is the adapter layer between stream-oriented WebSocket + // connections and the packet-oriented KCP layer. + pconn *turbotunnel.QueuePacketConn +} func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ws, err := upgrader.Upgrade(w, r, nil) @@ -116,15 +145,182 @@ func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Pass the address of client as the remote address of incoming connection clientIPParam := r.URL.Query().Get("client_ip") addr := clientAddr(clientIPParam) + + var token [len(turbotunnel.Token)]byte + _, err = io.ReadFull(conn, token[:]) + if err != nil { + // Don't bother logging EOF: that happens with an unused + // connection, which clients make frequently as they maintain a + // pool of proxies. + if err != io.EOF { + log.Printf("reading token: %v", err) + } + return + } + + switch { + case bytes.Equal(token[:], turbotunnel.Token[:]): + err = turbotunnelMode(conn, addr, handler.pconn) + default: + // We didn't find a matching token, which means that we are + // dealing with a client that doesn't know about such things. + // "Unread" the token by constructing a new Reader and pass it + // to the old one-session-per-WebSocket mode. + conn2 := &overrideReadConn{Conn: conn, Reader: io.MultiReader(bytes.NewReader(token[:]), conn)} + err = oneshotMode(conn2, addr) + } + if err != nil { + log.Println(err) + return + } +} + +// oneshotMode handles clients that did not send turbotunnel.Token at the start +// of their stream. These clients use the WebSocket as a raw pipe, and expect +// their session to begin and end when this single WebSocket does. +func oneshotMode(conn net.Conn, addr string) error { statsChannel <- addr != "" or, err := pt.DialOr(&ptInfo, addr, ptMethodName) if err != nil { - log.Printf("failed to connect to ORPort: %s", err) - return + return fmt.Errorf("failed to connect to ORPort: %s", err) } defer or.Close() proxy(or, conn) + + return nil +} + +// turbotunnelMode handles clients that sent turbotunnel.Token at the start of +// their stream. These clients expect to send and receive encapsulated packets, +// with a long-lived session identified by ClientID. +func turbotunnelMode(conn net.Conn, addr string, pconn *turbotunnel.QueuePacketConn) error { + // Read the ClientID prefix. Every packet encapsulated in this WebSocket + // connection pertains to the same ClientID. + var clientID turbotunnel.ClientID + _, err := io.ReadFull(conn, clientID[:]) + if err != nil { + return fmt.Errorf("reading ClientID: %v", err) + } + + // TODO: ClientID-to-client_ip address mapping + // Peek at the first read packet to get the KCP conv ID. + + errCh := make(chan error) + + // The remainder of the WebSocket stream consists of encapsulated + // packets. We read them one by one and feed them into the + // QueuePacketConn on which kcp.ServeConn was set up, which eventually + // leads to KCP-level sessions in the acceptSessions function. + go func() { + for { + p, err := encapsulation.ReadData(conn) + if err != nil { + errCh <- err + break + } + pconn.QueueIncoming(p, clientID) + } + }() + + // At the same time, grab packets addressed to this ClientID and + // encapsulate them into the downstream. + go func() { + // Buffer encapsulation.WriteData operations to keep length + // prefixes in the same send as the data that follows. + bw := bufio.NewWriter(conn) + for p := range pconn.OutgoingQueue(clientID) { + _, err := encapsulation.WriteData(bw, p) + if err == nil { + err = bw.Flush() + } + if err != nil { + errCh <- err + break + } + } + }() + + // Wait until one of the above loops terminates. The closing of the + // WebSocket connection will terminate the other one. + <-errCh + + return nil +} + +// handleStream bidirectionally connects a client stream with the ORPort. +func handleStream(stream net.Conn) error { + // TODO: This is where we need to provide the client IP address. + statsChannel <- false + or, err := pt.DialOr(&ptInfo, "", ptMethodName) + if err != nil { + return fmt.Errorf("connecting to ORPort: %v", err) + } + defer or.Close() + + proxy(or, stream) + + return nil +} + +// acceptStreams layers an smux.Session on the KCP connection and awaits streams +// on it. Passes each stream to handleStream. +func acceptStreams(conn *kcp.UDPSession) error { + smuxConfig := smux.DefaultConfig() + smuxConfig.Version = 2 + smuxConfig.KeepAliveTimeout = 10 * time.Minute + sess, err := smux.Server(conn, smuxConfig) + if err != nil { + return err + } + for { + stream, err := sess.AcceptStream() + if err != nil { + if err, ok := err.(net.Error); ok && err.Temporary() { + continue + } + return err + } + go func() { + defer stream.Close() + err := handleStream(stream) + if err != nil { + log.Printf("handleStream: %v", err) + } + }() + } +} + +// acceptSessions listens for incoming KCP connections and passes them to +// acceptStreams. It is handler.ServeHTTP that provides the network interface +// that drives this function. +func acceptSessions(ln *kcp.Listener) error { + for { + conn, err := ln.AcceptKCP() + if err != nil { + if err, ok := err.(net.Error); ok && err.Temporary() { + continue + } + return err + } + // Permit coalescing the payloads of consecutive sends. + conn.SetStreamMode(true) + // Disable the dynamic congestion window (limit only by the + // maximum of local and remote static windows). + conn.SetNoDelay( + 0, // default nodelay + 0, // default interval + 0, // default resend + 1, // nc=1 => congestion window off + ) + go func() { + defer conn.Close() + err := acceptStreams(conn) + if err != nil { + log.Printf("acceptStreams: %v", err) + } + }() + } } func initServer(addr *net.TCPAddr, @@ -140,7 +336,12 @@ func initServer(addr *net.TCPAddr, return nil, fmt.Errorf("cannot listen on port %d; configure a port using ServerTransportListenAddr", addr.Port) } - var handler HTTPHandler + handler := HTTPHandler{ + // pconn is shared among all connections to this server. It + // overlays packet-based client sessions on top of ephemeral + // WebSocket connections. + pconn: turbotunnel.NewQueuePacketConn(addr, clientMapTimeout), + } server := &http.Server{ Addr: addr.String(), Handler: &handler, @@ -176,6 +377,24 @@ func initServer(addr *net.TCPAddr, break } + // Start a KCP engine, set up to read and write its packets over the + // WebSocket connections that arrive at the web server. + // handler.ServeHTTP is responsible for encapsulation/decapsulation of + // packets on behalf of KCP. KCP takes those packets and turns them into + // sessions which appear in the acceptSessions function. + ln, err := kcp.ServeConn(nil, 0, 0, handler.pconn) + if err != nil { + server.Close() + return server, err + } + go func() { + defer ln.Close() + err := acceptSessions(ln) + if err != nil { + log.Printf("acceptSessions: %v", err) + } + }() + return server, err } From 0790954020b550f5d5351d9e54b108c9d357fa50 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 4 Feb 2020 22:27:58 -0700 Subject: [PATCH 094/385] USERADDR support for turbotunnel sessions. The difficulty here is that the whole point of turbotunnel sessions is that they are not necessarily tied to a single WebSocket connection, nor even a single client IP address. We use a heuristic: whenever a WebSocket connection starts that has a new ClientID, we store a mapping from that ClientID to the IP address attached to the WebSocket connection in a lookup table. Later, when enough packets have arrived to establish a turbotunnel session, we recover the ClientID associated with the session (which kcp-go has stored in the RemoteAddr field), and look it up in the table to get an IP address. We introduce a new data type, clientIDMap, to store the clientID-to-IP mapping during the short time between when a WebSocket connection starts and handleSession receives a fully fledged KCP session. --- server/server.go | 47 ++++++++++++--- server/turbotunnel.go | 85 ++++++++++++++++++++++++++ server/turbotunnel_test.go | 119 +++++++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 server/turbotunnel.go create mode 100644 server/turbotunnel_test.go diff --git a/server/server.go b/server/server.go index 028a24d..1a53de7 100644 --- a/server/server.go +++ b/server/server.go @@ -43,6 +43,11 @@ const requestTimeout = 10 * time.Second // indefinitely. const clientMapTimeout = 1 * time.Minute +// How big to make the map of ClientIDs to IP addresses. The map is used in +// turbotunnelMode to store a reasonable IP address for a client session that +// may outlive any single WebSocket connection. +const clientIDAddrMapCapacity = 1024 + // How long to wait for ListenAndServe or ListenAndServeTLS to return an error // before deciding that it's not going to return. const listenAndServeErrorTimeout = 100 * time.Millisecond @@ -114,6 +119,15 @@ var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } +// clientIDAddrMap stores short-term mappings from ClientIDs to IP addresses. +// When we call pt.DialOr, tor wants us to provide a USERADDR string that +// represents the remote IP address of the client (for metrics purposes, etc.). +// This data structure bridges the gap between ServeHTTP, which knows about IP +// addresses, and handleStream, which is what calls pt.DialOr. The common piece +// of information linking both ends of the chain is the ClientID, which is +// attached to the WebSocket connection and every session. +var clientIDAddrMap = newClientIDMap(clientIDAddrMapCapacity) + // overrideReadConn is a net.Conn with an overridden Read method. Compare to // recordingConn at // https://dave.cheney.net/2015/05/22/struct-composition-with-go. @@ -203,8 +217,16 @@ func turbotunnelMode(conn net.Conn, addr string, pconn *turbotunnel.QueuePacketC return fmt.Errorf("reading ClientID: %v", err) } - // TODO: ClientID-to-client_ip address mapping - // Peek at the first read packet to get the KCP conv ID. + // Store a a short-term mapping from the ClientID to the client IP + // address attached to this WebSocket connection. tor will want us to + // provide a client IP address when we call pt.DialOr. But a KCP session + // does not necessarily correspond to any single IP address--it's + // composed of packets that are carried in possibly multiple WebSocket + // streams. We apply the heuristic that the IP address of the most + // recent WebSocket connection that has had to do with a session, at the + // time the session is established, is the IP address that should be + // credited for the entire KCP session. + clientIDAddrMap.Set(clientID, addr) errCh := make(chan error) @@ -249,10 +271,9 @@ func turbotunnelMode(conn net.Conn, addr string, pconn *turbotunnel.QueuePacketC } // handleStream bidirectionally connects a client stream with the ORPort. -func handleStream(stream net.Conn) error { - // TODO: This is where we need to provide the client IP address. - statsChannel <- false - or, err := pt.DialOr(&ptInfo, "", ptMethodName) +func handleStream(stream net.Conn, addr string) error { + statsChannel <- addr != "" + or, err := pt.DialOr(&ptInfo, addr, ptMethodName) if err != nil { return fmt.Errorf("connecting to ORPort: %v", err) } @@ -266,6 +287,17 @@ func handleStream(stream net.Conn) error { // acceptStreams layers an smux.Session on the KCP connection and awaits streams // on it. Passes each stream to handleStream. func acceptStreams(conn *kcp.UDPSession) error { + // Look up the IP address associated with this KCP session, via the + // ClientID that is returned by the session's RemoteAddr method. + addr, ok := clientIDAddrMap.Get(conn.RemoteAddr().(turbotunnel.ClientID)) + if !ok { + // This means that the map is tending to run over capacity, not + // just that there was not client_ip on the incoming connection. + // We store "" in the map in the absence of client_ip. This log + // message means you should increase clientIDAddrMapCapacity. + log.Printf("no address in clientID-to-IP map (capacity %d)", clientIDAddrMapCapacity) + } + smuxConfig := smux.DefaultConfig() smuxConfig.Version = 2 smuxConfig.KeepAliveTimeout = 10 * time.Minute @@ -273,6 +305,7 @@ func acceptStreams(conn *kcp.UDPSession) error { if err != nil { return err } + for { stream, err := sess.AcceptStream() if err != nil { @@ -283,7 +316,7 @@ func acceptStreams(conn *kcp.UDPSession) error { } go func() { defer stream.Close() - err := handleStream(stream) + err := handleStream(stream, addr) if err != nil { log.Printf("handleStream: %v", err) } diff --git a/server/turbotunnel.go b/server/turbotunnel.go new file mode 100644 index 0000000..1d00897 --- /dev/null +++ b/server/turbotunnel.go @@ -0,0 +1,85 @@ +package main + +import ( + "sync" + + "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" +) + +// clientIDMap is a fixed-capacity mapping from ClientIDs to address strings. +// Adding a new entry using the Set method causes the oldest existing entry to +// be forgotten. +// +// This data type is meant to be used to remember the IP address associated with +// a ClientID, during the short period of time between when a WebSocket +// connection with that ClientID began, and when a KCP session is established. +// +// The design requirements of this type are that it needs to remember a mapping +// for only a short time, and old entries should expire so as not to consume +// unbounded memory. It is not a critical error if an entry is forgotten before +// it is needed; better to forget entries than to use too much memory. +type clientIDMap struct { + lock sync.Mutex + // entries is a circular buffer of (ClientID, addr) pairs. + entries []struct { + clientID turbotunnel.ClientID + addr string + } + // oldest is the index of the oldest member of the entries buffer, the + // one that will be overwritten at the next call to Set. + oldest int + // current points to the index of the most recent entry corresponding to + // each ClientID. + current map[turbotunnel.ClientID]int +} + +// newClientIDMap makes a new clientIDMap with the given capacity. +func newClientIDMap(capacity int) *clientIDMap { + return &clientIDMap{ + entries: make([]struct { + clientID turbotunnel.ClientID + addr string + }, capacity), + oldest: 0, + current: make(map[turbotunnel.ClientID]int), + } +} + +// Set adds a mapping from clientID to addr, replacing any previous mapping for +// clientID. It may also cause the clientIDMap to forget at most one other +// mapping, the oldest one. +func (m *clientIDMap) Set(clientID turbotunnel.ClientID, addr string) { + m.lock.Lock() + defer m.lock.Unlock() + if len(m.entries) == 0 { + // The invariant m.oldest < len(m.entries) does not hold in this + // special case. + return + } + // m.oldest is the index of the entry we're about to overwrite. If it's + // the current entry for any ClientID, we need to delete that clientID + // from the current map (that ClientID is now forgotten). + if i, ok := m.current[m.entries[m.oldest].clientID]; ok && i == m.oldest { + delete(m.current, m.entries[m.oldest].clientID) + } + // Overwrite the oldest entry. + m.entries[m.oldest].clientID = clientID + m.entries[m.oldest].addr = addr + // Add the overwritten entry to the quick-lookup map. + m.current[clientID] = m.oldest + // What was the oldest entry is now the newest. + m.oldest = (m.oldest + 1) % len(m.entries) +} + +// Get returns a previously stored mapping. The second return value indicates +// whether clientID was actually present in the map. If it is false, then the +// returned address string will be "". +func (m *clientIDMap) Get(clientID turbotunnel.ClientID) (string, bool) { + m.lock.Lock() + defer m.lock.Unlock() + if i, ok := m.current[clientID]; ok { + return m.entries[i].addr, true + } else { + return "", false + } +} diff --git a/server/turbotunnel_test.go b/server/turbotunnel_test.go new file mode 100644 index 0000000..c4bf02b --- /dev/null +++ b/server/turbotunnel_test.go @@ -0,0 +1,119 @@ +package main + +import ( + "encoding/binary" + "testing" + + "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" +) + +func TestClientIDMap(t *testing.T) { + // Convert a uint64 into a ClientID. + id := func(n uint64) turbotunnel.ClientID { + var clientID turbotunnel.ClientID + binary.PutUvarint(clientID[:], n) + return clientID + } + + // Does m.Get(key) and checks that the output matches what is expected. + expectGet := func(m *clientIDMap, clientID turbotunnel.ClientID, expectedAddr string, expectedOK bool) { + t.Helper() + addr, ok := m.Get(clientID) + if addr != expectedAddr || ok != expectedOK { + t.Errorf("expected (%+q, %v), got (%+q, %v)", expectedAddr, expectedOK, addr, ok) + } + } + + // Checks that the len of m.current is as expected. + expectSize := func(m *clientIDMap, expectedLen int) { + t.Helper() + if len(m.current) != expectedLen { + t.Errorf("expected map len %d, got %d %+v", expectedLen, len(m.current), m.current) + } + } + + // Zero-capacity map can't remember anything. + { + m := newClientIDMap(0) + expectSize(m, 0) + expectGet(m, id(0), "", false) + expectGet(m, id(1234), "", false) + + m.Set(id(0), "A") + expectSize(m, 0) + expectGet(m, id(0), "", false) + expectGet(m, id(1234), "", false) + + m.Set(id(1234), "A") + expectSize(m, 0) + expectGet(m, id(0), "", false) + expectGet(m, id(1234), "", false) + } + + { + m := newClientIDMap(1) + expectSize(m, 0) + expectGet(m, id(0), "", false) + expectGet(m, id(1), "", false) + + m.Set(id(0), "A") + expectSize(m, 1) + expectGet(m, id(0), "A", true) + expectGet(m, id(1), "", false) + + m.Set(id(1), "B") // forgets the (0, "A") entry + expectSize(m, 1) + expectGet(m, id(0), "", false) + expectGet(m, id(1), "B", true) + + m.Set(id(1), "C") // forgets the (1, "B") entry + expectSize(m, 1) + expectGet(m, id(0), "", false) + expectGet(m, id(1), "C", true) + } + + { + m := newClientIDMap(5) + m.Set(id(0), "A") + m.Set(id(1), "B") + m.Set(id(2), "C") + m.Set(id(0), "D") // shadows the (0, "D") entry + m.Set(id(3), "E") + expectSize(m, 4) + expectGet(m, id(0), "D", true) + expectGet(m, id(1), "B", true) + expectGet(m, id(2), "C", true) + expectGet(m, id(3), "E", true) + expectGet(m, id(4), "", false) + + m.Set(id(4), "F") // forgets the (0, "A") entry but should preserve (0, "D") + expectSize(m, 5) + expectGet(m, id(0), "D", true) + expectGet(m, id(1), "B", true) + expectGet(m, id(2), "C", true) + expectGet(m, id(3), "E", true) + expectGet(m, id(4), "F", true) + + m.Set(id(5), "G") // forgets the (1, "B") entry + m.Set(id(0), "H") // forgets the (2, "C") entry and shadows (0, "D") + expectSize(m, 4) + expectGet(m, id(0), "H", true) + expectGet(m, id(1), "", false) + expectGet(m, id(2), "", false) + expectGet(m, id(3), "E", true) + expectGet(m, id(4), "F", true) + expectGet(m, id(5), "G", true) + + m.Set(id(0), "I") // forgets the (0, "D") entry and shadows (0, "H") + m.Set(id(0), "J") // forgets the (3, "E") entry and shadows (0, "I") + m.Set(id(0), "K") // forgets the (4, "F") entry and shadows (0, "J") + m.Set(id(0), "L") // forgets the (5, "G") entry and shadows (0, "K") + expectSize(m, 1) + expectGet(m, id(0), "L", true) + expectGet(m, id(1), "", false) + expectGet(m, id(2), "", false) + expectGet(m, id(3), "", false) + expectGet(m, id(4), "", false) + expectGet(m, id(5), "", false) + } +} From 2022496d3b6fc76b7725135758c37d7d49546d3d Mon Sep 17 00:00:00 2001 From: David Fifield Date: Wed, 18 Mar 2020 18:00:44 -0600 Subject: [PATCH 095/385] Use a global RedialPacketConn and smux.Session. This allows multiple SOCKS connections to share the available proxies, and in particular prevents a SOCKS connection from being starved of a proxy when the maximum proxy capacity is less then the number of the number of SOCKS connections. This is option 4 from https://bugs.torproject.org/33519. --- client/lib/snowflake.go | 78 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 4b7dd4d..27991b2 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -6,6 +6,7 @@ import ( "io" "log" "net" + "sync" "time" "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" @@ -23,9 +24,10 @@ type dummyAddr struct{} func (addr dummyAddr) Network() string { return "dummy" } func (addr dummyAddr) String() string { return "dummy" } -// Given an accepted SOCKS connection, establish a WebRTC connection to the -// remote peer and exchange traffic. -func Handler(socks net.Conn, snowflakes SnowflakeCollector) error { +// newSession returns a new smux.Session and the net.PacketConn it is running +// over. The net.PacketConn successively connects through Snowflake proxies +// pulled from snowflakes. +func newSession(snowflakes SnowflakeCollector) (net.PacketConn, *smux.Session, error) { clientID := turbotunnel.NewClientID() // We build a persistent KCP session on a sequence of ephemeral WebRTC @@ -54,7 +56,6 @@ func Handler(socks net.Conn, snowflakes SnowflakeCollector) error { return NewEncapsulationPacketConn(dummyAddr{}, dummyAddr{}, conn), nil } pconn := turbotunnel.NewRedialPacketConn(dummyAddr{}, dummyAddr{}, dialContext) - defer pconn.Close() // conn is built on the underlying RedialPacketConn—when one WebRTC // connection dies, another one will be found to take its place. The @@ -62,9 +63,9 @@ func Handler(socks net.Conn, snowflakes SnowflakeCollector) error { // engine. conn, err := kcp.NewConn2(dummyAddr{}, nil, 0, 0, pconn) if err != nil { - return err + pconn.Close() + return nil, nil, err } - defer conn.Close() // Permit coalescing the payloads of consecutive sends. conn.SetStreamMode(true) // Disable the dynamic congestion window (limit only by the @@ -80,10 +81,73 @@ func Handler(socks net.Conn, snowflakes SnowflakeCollector) error { smuxConfig.Version = 2 smuxConfig.KeepAliveTimeout = 10 * time.Minute sess, err := smux.Client(conn, smuxConfig) + if err != nil { + conn.Close() + pconn.Close() + return nil, nil, err + } + + return pconn, sess, err +} + +// sessionManager_ maintains a single global smux.Session that is shared among +// incoming SOCKS connections. +type sessionManager_ struct { + mutex sync.Mutex + sess *smux.Session +} + +// Get creates and returns a new global smux.Session if none exists yet. If one +// already exists, it returns the existing one. It monitors the returned session +// and if it ever fails, sets things up so the next call to Get will create a +// new session. +func (manager *sessionManager_) Get(snowflakes SnowflakeCollector) (*smux.Session, error) { + manager.mutex.Lock() + defer manager.mutex.Unlock() + + if manager.sess == nil { + log.Printf("starting a new session") + pconn, sess, err := newSession(snowflakes) + if err != nil { + return nil, err + } + manager.sess = sess + go func() { + // If the session dies, set it to be recreated. + for { + <-time.After(5 * time.Second) + if sess.IsClosed() { + break + } + } + log.Printf("discarding finished session") + // Close the underlying to force any ongoing WebRTC + // connection to close as well, and relinquish the + // SnowflakeCollector. + pconn.Close() + manager.mutex.Lock() + manager.sess = nil + manager.mutex.Unlock() + }() + } else { + log.Printf("reusing the existing session") + } + + return manager.sess, nil +} + +var sessionManager = sessionManager_{} + +// Given an accepted SOCKS connection, establish a WebRTC connection to the +// remote peer and exchange traffic. +func Handler(socks net.Conn, snowflakes SnowflakeCollector) error { + // Return the global smux.Session. + sess, err := sessionManager.Get(snowflakes) if err != nil { return err } - defer sess.Close() + + // On the smux session we overlay a stream. stream, err := sess.OpenStream() if err != nil { return err From 2f52217d2f62e61a05ea265257d2229410f79732 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 17:08:49 -0600 Subject: [PATCH 096/385] Restore `go 1.13` to go.mod, lost in the turbotunnel merge. --- go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.mod b/go.mod index 6502651..07c49a2 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ module git.torproject.org/pluggable-transports/snowflake.git +go 1.13 + require ( git.torproject.org/pluggable-transports/goptlib.git v1.1.0 github.com/golang/protobuf v1.3.1 // indirect From 65ecb798ca8842a431214c2aa5133620e576c5f3 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 20:36:55 -0600 Subject: [PATCH 097/385] Update a comment (no signal pipe anymore). --- client/lib/webrtc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 5aa7aec..3e20549 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -304,8 +304,8 @@ func (c *WebRTCPeer) sendOfferToBroker() { c.answerChannel <- answer } -// Block until an SDP offer is available, send it to either -// the Broker or signal pipe, then await for the SDP answer. +// exchangeSDP blocks until an SDP offer is available, sends it to the Broker, +// then awaits the SDP answer. func (c *WebRTCPeer) exchangeSDP() error { select { case <-c.offerChannel: From d376d7036bdee70797dbda50af34d5930d45dadd Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 21:08:01 -0600 Subject: [PATCH 098/385] Make WebRTCPeer and Peers not inherit the methods of BytesLogger. You would have been able to do, for example, snowflake.(*WebRTCPeer).AddInbound(...). --- client/lib/peers.go | 2 +- client/lib/webrtc.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lib/peers.go b/client/lib/peers.go index d385971..f70905e 100644 --- a/client/lib/peers.go +++ b/client/lib/peers.go @@ -20,7 +20,7 @@ import ( // version of Snowflake) type Peers struct { Tongue - BytesLogger + BytesLogger BytesLogger snowflakeChan chan Snowflake activePeers *list.List diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 3e20549..589bbfa 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -40,7 +40,7 @@ type WebRTCPeer struct { lock sync.Mutex // Synchronization for DataChannel destruction once sync.Once // Synchronization for PeerConnection destruction - BytesLogger + BytesLogger BytesLogger } // Construct a WebRTC PeerConnection. From 9a4e3e7bd97ba87255453a005b2a3474d1914621 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 21:12:45 -0600 Subject: [PATCH 099/385] Remove unused BytesSyncLogger.IsLogging. --- client/lib/util.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/client/lib/util.go b/client/lib/util.go index cacf1d7..8ac0213 100644 --- a/client/lib/util.go +++ b/client/lib/util.go @@ -31,11 +31,9 @@ type BytesSyncLogger struct { Inbound int OutEvents int InEvents int - IsLogging bool } func (b *BytesSyncLogger) Log() { - b.IsLogging = true var amount int output := func() { log.Printf("Traffic Bytes (in|out): %d | %d -- (%d OnMessages, %d Sends)", @@ -71,15 +69,9 @@ func (b *BytesSyncLogger) Log() { } func (b *BytesSyncLogger) AddOutbound(amount int) { - if !b.IsLogging { - return - } b.OutboundChan <- amount } func (b *BytesSyncLogger) AddInbound(amount int) { - if !b.IsLogging { - return - } b.InboundChan <- amount } From 2853fc93628bf997903de33cd7aebd1420e5975f Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 21:20:16 -0600 Subject: [PATCH 100/385] Make BytesSyncLogger's implementation details internal. Provide NewBytesSyncLogger that returns an opaque data structure. Automatically start up the logging loop goroutine in NewBytesSyncLogger. --- client/lib/util.go | 52 ++++++++++++++++++++++++--------------------- client/snowflake.go | 10 +-------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/client/lib/util.go b/client/lib/util.go index 8ac0213..44df031 100644 --- a/client/lib/util.go +++ b/client/lib/util.go @@ -10,7 +10,6 @@ const ( ) type BytesLogger interface { - Log() AddOutbound(int) AddInbound(int) } @@ -18,50 +17,55 @@ type BytesLogger interface { // Default BytesLogger does nothing. type BytesNullLogger struct{} -func (b BytesNullLogger) Log() {} func (b BytesNullLogger) AddOutbound(amount int) {} func (b BytesNullLogger) AddInbound(amount int) {} // BytesSyncLogger uses channels to safely log from multiple sources with output // occuring at reasonable intervals. type BytesSyncLogger struct { - OutboundChan chan int - InboundChan chan int - Outbound int - Inbound int - OutEvents int - InEvents int + outboundChan chan int + inboundChan chan int } -func (b *BytesSyncLogger) Log() { - var amount int +// NewBytesSyncLogger returns a new BytesSyncLogger and starts it loggin. +func NewBytesSyncLogger() *BytesSyncLogger { + b := &BytesSyncLogger{ + outboundChan: make(chan int, 5), + inboundChan: make(chan int, 5), + } + go b.log() + return b +} + +func (b *BytesSyncLogger) log() { + var outbound, inbound, outEvents, inEvents int output := func() { log.Printf("Traffic Bytes (in|out): %d | %d -- (%d OnMessages, %d Sends)", - b.Inbound, b.Outbound, b.InEvents, b.OutEvents) - b.Outbound = 0 - b.OutEvents = 0 - b.Inbound = 0 - b.InEvents = 0 + inbound, outbound, inEvents, outEvents) + outbound = 0 + outEvents = 0 + inbound = 0 + inEvents = 0 } last := time.Now() for { select { - case amount = <-b.OutboundChan: - b.Outbound += amount - b.OutEvents++ + case amount := <-b.outboundChan: + outbound += amount + outEvents++ if time.Since(last) > time.Second*LogTimeInterval { last = time.Now() output() } - case amount = <-b.InboundChan: - b.Inbound += amount - b.InEvents++ + case amount := <-b.inboundChan: + inbound += amount + inEvents++ if time.Since(last) > time.Second*LogTimeInterval { last = time.Now() output() } case <-time.After(time.Second * LogTimeInterval): - if b.InEvents > 0 || b.OutEvents > 0 { + if inEvents > 0 || outEvents > 0 { output() } } @@ -69,9 +73,9 @@ func (b *BytesSyncLogger) Log() { } func (b *BytesSyncLogger) AddOutbound(amount int) { - b.OutboundChan <- amount + b.outboundChan <- amount } func (b *BytesSyncLogger) AddInbound(amount int) { - b.InboundChan <- amount + b.inboundChan <- amount } diff --git a/client/snowflake.go b/client/snowflake.go index dda59ae..d66225d 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -161,15 +161,7 @@ func main() { snowflakes.Tongue = sf.NewWebRTCDialer(broker, iceServers) // Use a real logger to periodically output how much traffic is happening. - snowflakes.BytesLogger = &sf.BytesSyncLogger{ - InboundChan: make(chan int, 5), - OutboundChan: make(chan int, 5), - Inbound: 0, - Outbound: 0, - InEvents: 0, - OutEvents: 0, - } - go snowflakes.BytesLogger.Log() + snowflakes.BytesLogger = sf.NewBytesSyncLogger() go ConnectLoop(snowflakes) From 73173cb6987dbf26fdb1036e4b7710c200f87141 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 21:26:19 -0600 Subject: [PATCH 101/385] Simplify BytesSyncLogger. --- client/lib/util.go | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/client/lib/util.go b/client/lib/util.go index 44df031..0eb8ddd 100644 --- a/client/lib/util.go +++ b/client/lib/util.go @@ -6,7 +6,7 @@ import ( ) const ( - LogTimeInterval = 5 + LogTimeInterval = 5 * time.Second ) type BytesLogger interface { @@ -39,35 +39,24 @@ func NewBytesSyncLogger() *BytesSyncLogger { func (b *BytesSyncLogger) log() { var outbound, inbound, outEvents, inEvents int - output := func() { - log.Printf("Traffic Bytes (in|out): %d | %d -- (%d OnMessages, %d Sends)", - inbound, outbound, inEvents, outEvents) - outbound = 0 - outEvents = 0 - inbound = 0 - inEvents = 0 - } - last := time.Now() + ticker := time.NewTicker(LogTimeInterval) for { select { + case <-ticker.C: + if outEvents > 0 || inEvents > 0 { + log.Printf("Traffic Bytes (in|out): %d | %d -- (%d OnMessages, %d Sends)", + inbound, outbound, inEvents, outEvents) + } + outbound = 0 + outEvents = 0 + inbound = 0 + inEvents = 0 case amount := <-b.outboundChan: outbound += amount outEvents++ - if time.Since(last) > time.Second*LogTimeInterval { - last = time.Now() - output() - } case amount := <-b.inboundChan: inbound += amount inEvents++ - if time.Since(last) > time.Second*LogTimeInterval { - last = time.Now() - output() - } - case <-time.After(time.Second * LogTimeInterval): - if inEvents > 0 || outEvents > 0 { - output() - } } } } From 6c2e3adc41c2c6d1ed794adac019a5a6eb069536 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 22:22:34 -0600 Subject: [PATCH 102/385] Disable trickle ICE. https://bugs.torproject.org/33984 OnICEGatheringStateChange is no longer called when candidate gathering is complete. SetLocalDescription kicks off the gathering process. https://bugs.torproject.org/28942#comment:28 https://bugs.torproject.org/33157#comment:2 --- client/lib/webrtc.go | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 589bbfa..e2a755f 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -165,10 +165,7 @@ func (c *WebRTCPeer) preparePeerConnection() error { c.pc = nil } - s := webrtc.SettingEngine{} - s.SetTrickle(true) - api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) - pc, err := api.NewPeerConnection(*c.config) + pc, err := webrtc.NewPeerConnection(*c.config) if err != nil { log.Printf("NewPeerConnection ERROR: %s", err) return err @@ -178,22 +175,11 @@ func (c *WebRTCPeer) preparePeerConnection() error { pc.OnICECandidate(func(candidate *webrtc.ICECandidate) { if candidate == nil { log.Printf("WebRTC: Done gathering candidates") + c.offerChannel <- pc.LocalDescription() } else { log.Printf("WebRTC: Got ICE candidate: %s", candidate.String()) } }) - pc.OnICEGatheringStateChange(func(state webrtc.ICEGathererState) { - if state == webrtc.ICEGathererStateComplete { - log.Println("WebRTC: ICEGatheringStateComplete") - c.offerChannel <- pc.LocalDescription() - } - }) - // This callback is not expected, as the Client initiates the creation - // of the data channel, not the remote peer. - pc.OnDataChannel(func(channel *webrtc.DataChannel) { - log.Println("OnDataChannel") - panic("Unexpected OnDataChannel!") - }) c.pc = pc go func() { offer, err := pc.CreateOffer(nil) @@ -226,9 +212,6 @@ func (c *WebRTCPeer) establishDataChannel() error { Ordered: &ordered, } dc, err := c.pc.CreateDataChannel(c.id, dataChannelOptions) - // Triggers "OnNegotiationNeeded" on the PeerConnection, which will prepare - // an SDP offer while other goroutines operating on this struct handle the - // signaling. Eventually fires "OnOpen". if err != nil { log.Printf("CreateDataChannel ERROR: %s", err) return err From 17c0d0ff82ccc0fad077ceb8ff8b20d580c56a24 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 18:31:48 -0600 Subject: [PATCH 103/385] Remove unused Resetter interface. WaitForReset is not used since 70126177fbdf5b1fa4977f2fc26f624641708098. --- client/lib/interfaces.go | 6 ------ client/lib/webrtc.go | 16 +--------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/client/lib/interfaces.go b/client/lib/interfaces.go index f6e8240..57171f8 100644 --- a/client/lib/interfaces.go +++ b/client/lib/interfaces.go @@ -9,16 +9,10 @@ type Connector interface { Connect() error } -type Resetter interface { - Reset() - WaitForReset() -} - // Interface for a single remote WebRTC peer. // In the Client context, "Snowflake" refers to the remote browser proxy. type Snowflake interface { io.ReadWriteCloser - Resetter Connector } diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index e2a755f..719ea74 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -15,7 +15,7 @@ import ( // Remote WebRTC peer. // Implements the |Snowflake| interface, which includes -// |io.ReadWriter|, |Resetter|, and |Connector|. +// |io.ReadWriter| and |Connector|. // // Handles preparation of go-webrtc PeerConnection. Only ever has // one DataChannel. @@ -33,7 +33,6 @@ type WebRTCPeer struct { writePipe *io.PipeWriter lastReceive time.Time buffer bytes.Buffer - reset chan struct{} closed bool @@ -61,7 +60,6 @@ func NewWebRTCPeer(config *webrtc.Configuration, // Error channel is mostly for reporting during the initial SDP offer // creation & local description setting, which happens asynchronously. connection.errorChannel = make(chan error, 1) - connection.reset = make(chan struct{}, 1) // Override with something that's not NullLogger to have real logging. connection.BytesLogger = &BytesNullLogger{} @@ -98,23 +96,11 @@ func (c *WebRTCPeer) Close() error { c.once.Do(func() { c.closed = true c.cleanup() - c.Reset() log.Printf("WebRTC: Closing") }) return nil } -// As part of |Resetter| -func (c *WebRTCPeer) Reset() { - if nil == c.reset { - return - } - c.reset <- struct{}{} -} - -// As part of |Resetter| -func (c *WebRTCPeer) WaitForReset() { <-c.reset } - // Prevent long-lived broken remotes. // Should also update the DataChannel in underlying go-webrtc's to make Closes // more immediate / responsive. From 3520f4e8b96e05dfb30874ec2174457684269db6 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 18:14:27 -0600 Subject: [PATCH 104/385] Simplify Peers.Pop. --- client/lib/peers.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/lib/peers.go b/client/lib/peers.go index f70905e..2649638 100644 --- a/client/lib/peers.go +++ b/client/lib/peers.go @@ -62,24 +62,24 @@ func (p *Peers) Collect() (Snowflake, error) { return connection, nil } -// As part of |SnowflakeCollector| interface. +// Pop blocks until an available, valid snowflake appears. Returns nil after End +// has been called. +// +// Part of |SnowflakeCollector| interface. func (p *Peers) Pop() Snowflake { - // Blocks until an available, valid snowflake appears. - var snowflake Snowflake - var ok bool - for snowflake == nil { - snowflake, ok = <-p.snowflakeChan + for { + snowflake, ok := <-p.snowflakeChan if !ok { return nil } conn := snowflake.(*WebRTCPeer) if conn.closed { - snowflake = nil + continue } + // Set to use the same rate-limited traffic logger to keep consistency. + conn.BytesLogger = p.BytesLogger + return conn } - // Set to use the same rate-limited traffic logger to keep consistency. - snowflake.(*WebRTCPeer).BytesLogger = p.BytesLogger - return snowflake } // As part of |SnowflakeCollector| interface. From 51bb49fa6f4ac7e01b19fd3136411a54a67a4ff6 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 18:52:32 -0600 Subject: [PATCH 105/385] Move pc.CreateOffer/pc.SetLocalDescription out of a goroutine. This allows us to remove the internal errorChannel. --- client/lib/webrtc.go | 47 +++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 719ea74..3fe8410 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -28,7 +28,6 @@ type WebRTCPeer struct { offerChannel chan *webrtc.SessionDescription answerChannel chan *webrtc.SessionDescription - errorChannel chan error recvPipe *io.PipeReader writePipe *io.PipeWriter lastReceive time.Time @@ -57,9 +56,6 @@ func NewWebRTCPeer(config *webrtc.Configuration, connection.broker = broker connection.offerChannel = make(chan *webrtc.SessionDescription, 1) connection.answerChannel = make(chan *webrtc.SessionDescription, 1) - // Error channel is mostly for reporting during the initial SDP offer - // creation & local description setting, which happens asynchronously. - connection.errorChannel = make(chan error, 1) // Override with something that's not NullLogger to have real logging. connection.BytesLogger = &BytesNullLogger{} @@ -167,21 +163,23 @@ func (c *WebRTCPeer) preparePeerConnection() error { } }) c.pc = pc - go func() { - offer, err := pc.CreateOffer(nil) - // TODO: Potentially timeout and retry if ICE isn't working. - if err != nil { - c.errorChannel <- err - return - } - log.Println("WebRTC: Created offer") - err = pc.SetLocalDescription(offer) - if err != nil { - c.errorChannel <- err - return - } - log.Println("WebRTC: Set local description") - }() + + offer, err := pc.CreateOffer(nil) + // TODO: Potentially timeout and retry if ICE isn't working. + if err != nil { + log.Println("Failed to prepare offer", err) + c.Close() + return err + } + log.Println("WebRTC: Created offer") + err = pc.SetLocalDescription(offer) + if err != nil { + log.Println("Failed to prepare offer", err) + c.Close() + return err + } + log.Println("WebRTC: Set local description") + log.Println("WebRTC: PeerConnection created.") return nil } @@ -276,13 +274,7 @@ func (c *WebRTCPeer) sendOfferToBroker() { // exchangeSDP blocks until an SDP offer is available, sends it to the Broker, // then awaits the SDP answer. func (c *WebRTCPeer) exchangeSDP() error { - select { - case <-c.offerChannel: - case err := <-c.errorChannel: - log.Println("Failed to prepare offer", err) - c.Close() - return err - } + <-c.offerChannel // Keep trying the same offer until a valid answer arrives. var ok bool var answer *webrtc.SessionDescription @@ -312,9 +304,6 @@ func (c *WebRTCPeer) cleanup() { if nil != c.answerChannel { close(c.answerChannel) } - if nil != c.errorChannel { - close(c.errorChannel) - } // Close this side of the SOCKS pipe. if nil != c.writePipe { c.writePipe.Close() From d9b076c32eed106a382a19bda973a9616cc44501 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 21:01:19 -0600 Subject: [PATCH 106/385] Don't do a separate check for a short write. A short write will result in a non-nil error. It's an io.PipeWriter anyway, which blocks until all the data has been read or the read end is closed, in which case it returns io.ErrClosedPipe if not some other error. --- client/lib/webrtc.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 3fe8410..389e02b 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -239,8 +239,8 @@ func (c *WebRTCPeer) establishDataChannel() error { if len(msg.Data) <= 0 { log.Println("0 length message---") } - c.BytesLogger.AddInbound(len(msg.Data)) n, err := c.writePipe.Write(msg.Data) + c.BytesLogger.AddInbound(n) if err != nil { // TODO: Maybe shouldn't actually close. log.Println("Error writing to SOCKS pipe") @@ -248,10 +248,6 @@ func (c *WebRTCPeer) establishDataChannel() error { log.Printf("c.writePipe.CloseWithError returned error: %v", inerr) } } - if n != len(msg.Data) { - log.Println("Error: short write") - panic("short write") - } c.lastReceive = time.Now() }) log.Println("WebRTC: DataChannel created.") From 76732155e7d730573b3ced62209e4e1e4ead511c Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 24 Apr 2020 14:21:08 -0600 Subject: [PATCH 107/385] Remove `Snowflake` interface, use `*WebRTCPeer` directly. The other interfaces in client/lib/interfaces.go exist for the purpose of running tests, but not Snowflake. Existing code would not have worked with other types anyway, because it does unchecked .(*WebRTCPeer) conversions. --- client/lib/interfaces.go | 13 +++---------- client/lib/lib_test.go | 8 ++++---- client/lib/peers.go | 17 +++++++---------- client/lib/rendezvous.go | 2 +- client/lib/webrtc.go | 3 --- 5 files changed, 15 insertions(+), 28 deletions(-) diff --git a/client/lib/interfaces.go b/client/lib/interfaces.go index 57171f8..e551c4d 100644 --- a/client/lib/interfaces.go +++ b/client/lib/interfaces.go @@ -9,16 +9,9 @@ type Connector interface { Connect() error } -// Interface for a single remote WebRTC peer. -// In the Client context, "Snowflake" refers to the remote browser proxy. -type Snowflake interface { - io.ReadWriteCloser - Connector -} - // Interface for catching Snowflakes. (aka the remote dialer) type Tongue interface { - Catch() (Snowflake, error) + Catch() (*WebRTCPeer, error) } // Interface for collecting some number of Snowflakes, for passing along @@ -26,10 +19,10 @@ type Tongue interface { type SnowflakeCollector interface { // Add a Snowflake to the collection. // Implementation should decide how to connect and maintain the webRTCConn. - Collect() (Snowflake, error) + Collect() (*WebRTCPeer, error) // Remove and return the most available Snowflake from the collection. - Pop() Snowflake + Pop() *WebRTCPeer // Signal when the collector has stopped collecting. Melted() <-chan struct{} diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 1cdc2c6..50a211d 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -52,7 +52,7 @@ func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { type FakeDialer struct{} -func (w FakeDialer) Catch() (Snowflake, error) { +func (w FakeDialer) Catch() (*WebRTCPeer, error) { fmt.Println("Caught a dummy snowflake.") return &WebRTCPeer{}, nil } @@ -70,9 +70,9 @@ func (f FakeSocksConn) Grant(addr *net.TCPAddr) error { return nil } type FakePeers struct{ toRelease *WebRTCPeer } -func (f FakePeers) Collect() (Snowflake, error) { return &WebRTCPeer{}, nil } -func (f FakePeers) Pop() Snowflake { return nil } -func (f FakePeers) Melted() <-chan struct{} { return nil } +func (f FakePeers) Collect() (*WebRTCPeer, error) { return &WebRTCPeer{}, nil } +func (f FakePeers) Pop() *WebRTCPeer { return nil } +func (f FakePeers) Melted() <-chan struct{} { return nil } const sampleSDP = `"v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\na=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 8.8.8.8 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"` diff --git a/client/lib/peers.go b/client/lib/peers.go index 2649638..f766a66 100644 --- a/client/lib/peers.go +++ b/client/lib/peers.go @@ -22,7 +22,7 @@ type Peers struct { Tongue BytesLogger BytesLogger - snowflakeChan chan Snowflake + snowflakeChan chan *WebRTCPeer activePeers *list.List capacity int @@ -33,14 +33,14 @@ type Peers struct { func NewPeers(max int) *Peers { p := &Peers{capacity: max} // Use buffered go channel to pass snowflakes onwards to the SOCKS handler. - p.snowflakeChan = make(chan Snowflake, max) + p.snowflakeChan = make(chan *WebRTCPeer, max) p.activePeers = list.New() p.melt = make(chan struct{}) return p } // As part of |SnowflakeCollector| interface. -func (p *Peers) Collect() (Snowflake, error) { +func (p *Peers) Collect() (*WebRTCPeer, error) { cnt := p.Count() s := fmt.Sprintf("Currently at [%d/%d]", cnt, p.capacity) if cnt >= p.capacity { @@ -64,21 +64,18 @@ func (p *Peers) Collect() (Snowflake, error) { // Pop blocks until an available, valid snowflake appears. Returns nil after End // has been called. -// -// Part of |SnowflakeCollector| interface. -func (p *Peers) Pop() Snowflake { +func (p *Peers) Pop() *WebRTCPeer { for { snowflake, ok := <-p.snowflakeChan if !ok { return nil } - conn := snowflake.(*WebRTCPeer) - if conn.closed { + if snowflake.closed { continue } // Set to use the same rate-limited traffic logger to keep consistency. - conn.BytesLogger = p.BytesLogger - return conn + snowflake.BytesLogger = p.BytesLogger + return snowflake } } diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index c82fc9e..f236c47 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -147,7 +147,7 @@ func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer) *WebR } // Initialize a WebRTC Connection by signaling through the broker. -func (w WebRTCDialer) Catch() (Snowflake, error) { +func (w WebRTCDialer) Catch() (*WebRTCPeer, error) { // TODO: [#25591] Fetch ICE server information from Broker. // TODO: [#25596] Consider TURN servers here too. connection := NewWebRTCPeer(w.webrtcConfig, w.BrokerChannel) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 389e02b..19fd5c9 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -14,8 +14,6 @@ import ( ) // Remote WebRTC peer. -// Implements the |Snowflake| interface, which includes -// |io.ReadWriter| and |Connector|. // // Handles preparation of go-webrtc PeerConnection. Only ever has // one DataChannel. @@ -87,7 +85,6 @@ func (c *WebRTCPeer) Write(b []byte) (int, error) { return len(b), nil } -// As part of |Snowflake| func (c *WebRTCPeer) Close() error { c.once.Do(func() { c.closed = true From b48fb781ee15cf033efc61496746a295dc0d63c7 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Mon, 27 Apr 2020 18:45:10 -0600 Subject: [PATCH 108/385] Have util.{Serialize,Deserialize}SessionDescription return an error https://bugs.torproject.org/33897#comment:4 --- client/lib/lib_test.go | 11 ++++++++--- client/lib/rendezvous.go | 9 ++++++--- common/util/util.go | 33 ++++++++++++--------------------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 50a211d..41d9cf9 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -231,7 +231,8 @@ func TestSnowflakeClient(t *testing.T) { So(err, ShouldBeNil) c.offerChannel <- nil - answer := util.DeserializeSessionDescription(sampleAnswer) + answer, err := util.DeserializeSessionDescription(sampleAnswer) + So(err, ShouldBeNil) So(answer, ShouldNotBeNil) c.answerChannel <- answer err = c.exchangeSDP() @@ -256,7 +257,8 @@ func TestSnowflakeClient(t *testing.T) { ctx.So(err, ShouldBeNil) wg.Done() }() - answer := util.DeserializeSessionDescription(sampleAnswer) + answer, err := util.DeserializeSessionDescription(sampleAnswer) + So(err, ShouldBeNil) c.answerChannel <- answer wg.Wait() }) @@ -286,7 +288,10 @@ func TestSnowflakeClient(t *testing.T) { http.StatusOK, []byte(`{"type":"answer","sdp":"fake"}`), } - fakeOffer := util.DeserializeSessionDescription(`{"type":"offer","sdp":"test"}`) + fakeOffer, err := util.DeserializeSessionDescription(`{"type":"offer","sdp":"test"}`) + if err != nil { + panic(err) + } Convey("Construct BrokerChannel with no front domain", func() { b, err := NewBrokerChannel("test.broker", "", transport, false) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index f236c47..85e25d2 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -96,7 +96,11 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( SDP: util.StripLocalAddresses(offer.SDP), } } - data := bytes.NewReader([]byte(util.SerializeSessionDescription(offer))) + offerSDP, err := util.SerializeSessionDescription(offer) + if err != nil { + return nil, err + } + data := bytes.NewReader([]byte(offerSDP)) // Suffix with broker's client registration handler. clientURL := bc.url.ResolveReference(&url.URL{Path: "client"}) request, err := http.NewRequest("POST", clientURL.String(), data) @@ -119,8 +123,7 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( if nil != err { return nil, err } - answer := util.DeserializeSessionDescription(string(body)) - return answer, nil + return util.DeserializeSessionDescription(string(body)) case http.StatusServiceUnavailable: return nil, errors.New(BrokerError503) case http.StatusBadRequest: diff --git a/common/util/util.go b/common/util/util.go index fa62fd7..ac254fa 100644 --- a/common/util/util.go +++ b/common/util/util.go @@ -2,43 +2,38 @@ package util import ( "encoding/json" - "log" + "errors" "net" "github.com/pion/sdp/v2" "github.com/pion/webrtc/v2" ) -func SerializeSessionDescription(desc *webrtc.SessionDescription) string { +func SerializeSessionDescription(desc *webrtc.SessionDescription) (string, error) { bytes, err := json.Marshal(*desc) - if nil != err { - log.Println(err) - return "" + if err != nil { + return "", err } - return string(bytes) + return string(bytes), nil } -func DeserializeSessionDescription(msg string) *webrtc.SessionDescription { +func DeserializeSessionDescription(msg string) (*webrtc.SessionDescription, error) { var parsed map[string]interface{} err := json.Unmarshal([]byte(msg), &parsed) - if nil != err { - log.Println(err) - return nil + if err != nil { + return nil, err } if _, ok := parsed["type"]; !ok { - log.Println("Cannot deserialize SessionDescription without type field.") - return nil + return nil, errors.New("cannot deserialize SessionDescription without type field") } if _, ok := parsed["sdp"]; !ok { - log.Println("Cannot deserialize SessionDescription without sdp field.") - return nil + return nil, errors.New("cannot deserialize SessionDescription without sdp field") } var stype webrtc.SDPType switch parsed["type"].(string) { default: - log.Println("Unknown SDP type") - return nil + return nil, errors.New("Unknown SDP type") case "offer": stype = webrtc.SDPTypeOffer case "pranswer": @@ -49,14 +44,10 @@ func DeserializeSessionDescription(msg string) *webrtc.SessionDescription { stype = webrtc.SDPTypeRollback } - if err != nil { - log.Println(err) - return nil - } return &webrtc.SessionDescription{ Type: stype, SDP: parsed["sdp"].(string), - } + }, nil } // Stolen from https://github.com/golang/go/pull/30278 From 32207d6f06c9254b3229f8e3161b80fd7f5df645 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 24 Apr 2020 00:05:06 -0600 Subject: [PATCH 109/385] Eliminate separate WebRTCPeer.Connect method. Do it as a side effect of NewWebRTCPeer. Remove WebRTCPeer tests as they currently require invasively modifying internal fields at different stages of construction. --- client/lib/interfaces.go | 4 -- client/lib/lib_test.go | 100 --------------------------------------- client/lib/rendezvous.go | 4 +- client/lib/webrtc.go | 13 +++-- 4 files changed, 10 insertions(+), 111 deletions(-) diff --git a/client/lib/interfaces.go b/client/lib/interfaces.go index e551c4d..fa0bfbe 100644 --- a/client/lib/interfaces.go +++ b/client/lib/interfaces.go @@ -5,10 +5,6 @@ import ( "net" ) -type Connector interface { - Connect() error -} - // Interface for catching Snowflakes. (aka the remote dialer) type Tongue interface { Catch() (*WebRTCPeer, error) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 41d9cf9..ebcf284 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -6,35 +6,12 @@ import ( "io/ioutil" "net" "net/http" - "sync" "testing" "git.torproject.org/pluggable-transports/snowflake.git/common/util" - "github.com/pion/webrtc/v2" . "github.com/smartystreets/goconvey/convey" ) -type MockDataChannel struct { - destination bytes.Buffer - done chan bool -} - -func (m *MockDataChannel) Send(data []byte) error { - m.destination.Write(data) - m.done <- true - return nil -} - -func (*MockDataChannel) Close() error { return nil } - -type MockResponse struct{} - -func (m *MockResponse) Read(p []byte) (int, error) { - p = []byte(`{"type":"answer","sdp":"fake"}`) - return 0, nil -} -func (m *MockResponse) Close() error { return nil } - type MockTransport struct { statusOverride int body []byte @@ -74,10 +51,6 @@ func (f FakePeers) Collect() (*WebRTCPeer, error) { return &WebRTCPeer{}, nil } func (f FakePeers) Pop() *WebRTCPeer { return nil } func (f FakePeers) Melted() <-chan struct{} { return nil } -const sampleSDP = `"v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\na=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 8.8.8.8 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"` - -const sampleAnswer = `{"type":"answer","sdp":` + sampleSDP + `}` - func TestSnowflakeClient(t *testing.T) { Convey("Peers", t, func() { @@ -191,79 +164,6 @@ func TestSnowflakeClient(t *testing.T) { Handler(socks, snowflakes) So(socks.rejected, ShouldEqual, true) }) - - Convey("WebRTC Connection", func() { - c := NewWebRTCPeer(nil, nil) - So(c.buffer.Bytes(), ShouldEqual, nil) - - Convey("Can construct a WebRTCConn", func() { - s := NewWebRTCPeer(nil, nil) - So(s, ShouldNotBeNil) - So(s.offerChannel, ShouldNotBeNil) - So(s.answerChannel, ShouldNotBeNil) - s.Close() - }) - - Convey("Write buffers when datachannel is nil", func() { - c.Write([]byte("test")) - c.transport = nil - So(c.buffer.Bytes(), ShouldResemble, []byte("test")) - }) - - Convey("Write sends to datachannel when not nil", func() { - mock := new(MockDataChannel) - c.transport = mock - mock.done = make(chan bool, 1) - c.Write([]byte("test")) - <-mock.done - So(c.buffer.Bytes(), ShouldEqual, nil) - So(mock.destination.Bytes(), ShouldResemble, []byte("test")) - }) - - Convey("Exchange SDP sets remote description", func() { - c.offerChannel = make(chan *webrtc.SessionDescription, 1) - c.answerChannel = make(chan *webrtc.SessionDescription, 1) - - c.config = &webrtc.Configuration{} - c.pc, _ = webrtc.NewPeerConnection(*c.config) - offer, _ := c.pc.CreateOffer(nil) - err := c.pc.SetLocalDescription(offer) - So(err, ShouldBeNil) - - c.offerChannel <- nil - answer, err := util.DeserializeSessionDescription(sampleAnswer) - So(err, ShouldBeNil) - So(answer, ShouldNotBeNil) - c.answerChannel <- answer - err = c.exchangeSDP() - So(err, ShouldBeNil) - }) - - Convey("Exchange SDP keeps trying on nil answer", func(ctx C) { - var wg sync.WaitGroup - wg.Add(1) - - c.offerChannel = make(chan *webrtc.SessionDescription, 1) - c.answerChannel = make(chan *webrtc.SessionDescription, 1) - c.config = &webrtc.Configuration{} - c.pc, _ = webrtc.NewPeerConnection(*c.config) - offer, _ := c.pc.CreateOffer(nil) - c.pc.SetLocalDescription(offer) - - c.offerChannel <- nil - c.answerChannel <- nil - go func() { - err := c.exchangeSDP() - ctx.So(err, ShouldBeNil) - wg.Done() - }() - answer, err := util.DeserializeSessionDescription(sampleAnswer) - So(err, ShouldBeNil) - c.answerChannel <- answer - wg.Wait() - }) - - }) }) Convey("Dialers", t, func() { diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 85e25d2..ca15d35 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -153,7 +153,5 @@ func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer) *WebR func (w WebRTCDialer) Catch() (*WebRTCPeer, error) { // TODO: [#25591] Fetch ICE server information from Broker. // TODO: [#25596] Consider TURN servers here too. - connection := NewWebRTCPeer(w.webrtcConfig, w.BrokerChannel) - err := connection.Connect() - return connection, err + return NewWebRTCPeer(w.webrtcConfig, w.BrokerChannel) } diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 19fd5c9..91b32e9 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -41,7 +41,7 @@ type WebRTCPeer struct { // Construct a WebRTC PeerConnection. func NewWebRTCPeer(config *webrtc.Configuration, - broker *BrokerChannel) *WebRTCPeer { + broker *BrokerChannel) (*WebRTCPeer, error) { connection := new(WebRTCPeer) { var buf [8]byte @@ -60,7 +60,13 @@ func NewWebRTCPeer(config *webrtc.Configuration, // Pipes remain the same even when DataChannel gets switched. connection.recvPipe, connection.writePipe = io.Pipe() - return connection + + err := connection.connect() + if err != nil { + connection.Close() + return nil, err + } + return connection, nil } // Read bytes from local SOCKS. @@ -113,8 +119,7 @@ func (c *WebRTCPeer) checkForStaleness() { } } -// As part of |Connector| interface. -func (c *WebRTCPeer) Connect() error { +func (c *WebRTCPeer) connect() error { log.Println(c.id, " connecting...") // TODO: When go-webrtc is more stable, it's possible that a new // PeerConnection won't need to be re-prepared each time. From 8caa737700d282dc7b174b4df3e514cc02bb0386 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 24 Apr 2020 14:41:19 -0600 Subject: [PATCH 110/385] Remove SnowflakeDataChannel interface. Use *webrtc.DataChannel directly. --- client/lib/interfaces.go | 7 ------- client/lib/webrtc.go | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/client/lib/interfaces.go b/client/lib/interfaces.go index fa0bfbe..71426d6 100644 --- a/client/lib/interfaces.go +++ b/client/lib/interfaces.go @@ -1,7 +1,6 @@ package lib import ( - "io" "net" ) @@ -30,9 +29,3 @@ type SocksConnector interface { Reject() error net.Conn } - -// Interface for the Snowflake's transport. (Typically just webrtc.DataChannel) -type SnowflakeDataChannel interface { - io.Closer - Send([]byte) error -} diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 91b32e9..cba2574 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -21,7 +21,7 @@ type WebRTCPeer struct { id string config *webrtc.Configuration pc *webrtc.PeerConnection - transport SnowflakeDataChannel // Holds the WebRTC DataChannel. + transport *webrtc.DataChannel broker *BrokerChannel offerChannel chan *webrtc.SessionDescription @@ -321,7 +321,7 @@ func (c *WebRTCPeer) cleanup() { if c.pc == nil { panic("DataChannel w/o PeerConnection, not good.") } - dataChannel.(*webrtc.DataChannel).Close() + dataChannel.Close() } else { c.lock.Unlock() } From 5787d5b8b0a78a27a12385fbeea7bb12f5b11fb1 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 19:05:29 -0600 Subject: [PATCH 111/385] Simplify WebRTCPeer.exchangeSDP. No need to run sendOfferToBroker in a goroutine. --- client/lib/webrtc.go | 45 ++++++++++++++------------------------------ 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index cba2574..e4ac8e0 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -24,12 +24,11 @@ type WebRTCPeer struct { transport *webrtc.DataChannel broker *BrokerChannel - offerChannel chan *webrtc.SessionDescription - answerChannel chan *webrtc.SessionDescription - recvPipe *io.PipeReader - writePipe *io.PipeWriter - lastReceive time.Time - buffer bytes.Buffer + offerChannel chan *webrtc.SessionDescription + recvPipe *io.PipeReader + writePipe *io.PipeWriter + lastReceive time.Time + buffer bytes.Buffer closed bool @@ -53,7 +52,6 @@ func NewWebRTCPeer(config *webrtc.Configuration, connection.config = config connection.broker = broker connection.offerChannel = make(chan *webrtc.SessionDescription, 1) - connection.answerChannel = make(chan *webrtc.SessionDescription, 1) // Override with something that's not NullLogger to have real logging. connection.BytesLogger = &BytesNullLogger{} @@ -256,34 +254,22 @@ func (c *WebRTCPeer) establishDataChannel() error { return nil } -func (c *WebRTCPeer) sendOfferToBroker() { - if nil == c.broker { - return - } - offer := c.pc.LocalDescription() - answer, err := c.broker.Negotiate(offer) - if nil != err || nil == answer { - log.Printf("BrokerChannel Error: %s", err) - answer = nil - } - c.answerChannel <- answer -} - // exchangeSDP blocks until an SDP offer is available, sends it to the Broker, // then awaits the SDP answer. func (c *WebRTCPeer) exchangeSDP() error { <-c.offerChannel // Keep trying the same offer until a valid answer arrives. - var ok bool var answer *webrtc.SessionDescription - for nil == answer { - go c.sendOfferToBroker() - answer, ok = <-c.answerChannel // Blocks... - if !ok || nil == answer { - log.Printf("Failed to retrieve answer. Retrying in %v", ReconnectTimeout) - <-time.After(ReconnectTimeout) - answer = nil + for { + var err error + // Send offer to broker (blocks). + answer, err = c.broker.Negotiate(c.pc.LocalDescription()) + if err == nil { + break } + log.Printf("BrokerChannel Error: %s", err) + log.Printf("Failed to retrieve answer. Retrying in %v", ReconnectTimeout) + <-time.After(ReconnectTimeout) } log.Printf("Received Answer.\n") err := c.pc.SetRemoteDescription(*answer) @@ -299,9 +285,6 @@ func (c *WebRTCPeer) cleanup() { if nil != c.offerChannel { close(c.offerChannel) } - if nil != c.answerChannel { - close(c.answerChannel) - } // Close this side of the SOCKS pipe. if nil != c.writePipe { c.writePipe.Close() From 81d14ad33a21ac487cefb2912a32a72a7cc8ee39 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 20:13:16 -0600 Subject: [PATCH 112/385] Make WebRTCPeer.preparePeerConnection block. Formerly, preparePeerConnection set up a callback that sent into a channel, and exchangeSDP waited until it could receive from the channel. We can move the channel entirely into preparePeerConnection (having it not return until the callback has been called) and that way remove some shared state. --- client/lib/webrtc.go | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index e4ac8e0..d489096 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -24,11 +24,10 @@ type WebRTCPeer struct { transport *webrtc.DataChannel broker *BrokerChannel - offerChannel chan *webrtc.SessionDescription - recvPipe *io.PipeReader - writePipe *io.PipeWriter - lastReceive time.Time - buffer bytes.Buffer + recvPipe *io.PipeReader + writePipe *io.PipeWriter + lastReceive time.Time + buffer bytes.Buffer closed bool @@ -51,7 +50,6 @@ func NewWebRTCPeer(config *webrtc.Configuration, } connection.config = config connection.broker = broker - connection.offerChannel = make(chan *webrtc.SessionDescription, 1) // Override with something that's not NullLogger to have real logging. connection.BytesLogger = &BytesNullLogger{} @@ -153,11 +151,12 @@ func (c *WebRTCPeer) preparePeerConnection() error { return err } // Prepare PeerConnection callbacks. + offerChannel := make(chan struct{}) // Allow candidates to accumulate until ICEGatheringStateComplete. pc.OnICECandidate(func(candidate *webrtc.ICECandidate) { if candidate == nil { log.Printf("WebRTC: Done gathering candidates") - c.offerChannel <- pc.LocalDescription() + close(offerChannel) } else { log.Printf("WebRTC: Got ICE candidate: %s", candidate.String()) } @@ -180,6 +179,7 @@ func (c *WebRTCPeer) preparePeerConnection() error { } log.Println("WebRTC: Set local description") + <-offerChannel // Wait for ICE candidate gathering to complete. log.Println("WebRTC: PeerConnection created.") return nil } @@ -254,10 +254,9 @@ func (c *WebRTCPeer) establishDataChannel() error { return nil } -// exchangeSDP blocks until an SDP offer is available, sends it to the Broker, -// then awaits the SDP answer. +// exchangeSDP sends the local SDP offer to the Broker and awaits the SDP +// answer. func (c *WebRTCPeer) exchangeSDP() error { - <-c.offerChannel // Keep trying the same offer until a valid answer arrives. var answer *webrtc.SessionDescription for { @@ -282,9 +281,6 @@ func (c *WebRTCPeer) exchangeSDP() error { // Close all channels and transports func (c *WebRTCPeer) cleanup() { - if nil != c.offerChannel { - close(c.offerChannel) - } // Close this side of the SOCKS pipe. if nil != c.writePipe { c.writePipe.Close() From 8295c87fbe4a3fb0d9e6c5fb04f6f0a52573faaf Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 20:57:50 -0600 Subject: [PATCH 113/385] Make preparePeerConnection a standalone function. --- client/lib/webrtc.go | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index d489096..f47bb2e 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -19,7 +19,6 @@ import ( // one DataChannel. type WebRTCPeer struct { id string - config *webrtc.Configuration pc *webrtc.PeerConnection transport *webrtc.DataChannel broker *BrokerChannel @@ -48,7 +47,6 @@ func NewWebRTCPeer(config *webrtc.Configuration, } connection.id = "snowflake-" + hex.EncodeToString(buf[:]) } - connection.config = config connection.broker = broker // Override with something that's not NullLogger to have real logging. @@ -57,7 +55,7 @@ func NewWebRTCPeer(config *webrtc.Configuration, // Pipes remain the same even when DataChannel gets switched. connection.recvPipe, connection.writePipe = io.Pipe() - err := connection.connect() + err := connection.connect(config) if err != nil { connection.Close() return nil, err @@ -115,11 +113,12 @@ func (c *WebRTCPeer) checkForStaleness() { } } -func (c *WebRTCPeer) connect() error { +func (c *WebRTCPeer) connect(config *webrtc.Configuration) error { log.Println(c.id, " connecting...") // TODO: When go-webrtc is more stable, it's possible that a new // PeerConnection won't need to be re-prepared each time. - err := c.preparePeerConnection() + var err error + c.pc, err = preparePeerConnection(config) if err != nil { return err } @@ -136,19 +135,13 @@ func (c *WebRTCPeer) connect() error { return nil } -// Create and prepare callbacks on a new WebRTC PeerConnection. -func (c *WebRTCPeer) preparePeerConnection() error { - if nil != c.pc { - if err := c.pc.Close(); err != nil { - log.Printf("c.pc.Close returned error: %v", err) - } - c.pc = nil - } - - pc, err := webrtc.NewPeerConnection(*c.config) +// preparePeerConnection creates a new WebRTC PeerConnection and returns it +// after ICE candidate gathering is complete.. +func preparePeerConnection(config *webrtc.Configuration) (*webrtc.PeerConnection, error) { + pc, err := webrtc.NewPeerConnection(*config) if err != nil { log.Printf("NewPeerConnection ERROR: %s", err) - return err + return nil, err } // Prepare PeerConnection callbacks. offerChannel := make(chan struct{}) @@ -161,27 +154,26 @@ func (c *WebRTCPeer) preparePeerConnection() error { log.Printf("WebRTC: Got ICE candidate: %s", candidate.String()) } }) - c.pc = pc offer, err := pc.CreateOffer(nil) // TODO: Potentially timeout and retry if ICE isn't working. if err != nil { log.Println("Failed to prepare offer", err) - c.Close() - return err + pc.Close() + return nil, err } log.Println("WebRTC: Created offer") err = pc.SetLocalDescription(offer) if err != nil { log.Println("Failed to prepare offer", err) - c.Close() - return err + pc.Close() + return nil, err } log.Println("WebRTC: Set local description") <-offerChannel // Wait for ICE candidate gathering to complete. log.Println("WebRTC: PeerConnection created.") - return nil + return pc, nil } // Create a WebRTC DataChannel locally. From 85277274fd3020819fe5c556b8164e7671bb799b Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 23 Apr 2020 22:33:38 -0600 Subject: [PATCH 114/385] Make exchangeSDP into a standalone function. --- client/lib/webrtc.go | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index f47bb2e..7ceeaff 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -21,7 +21,6 @@ type WebRTCPeer struct { id string pc *webrtc.PeerConnection transport *webrtc.DataChannel - broker *BrokerChannel recvPipe *io.PipeReader writePipe *io.PipeWriter @@ -47,7 +46,6 @@ func NewWebRTCPeer(config *webrtc.Configuration, } connection.id = "snowflake-" + hex.EncodeToString(buf[:]) } - connection.broker = broker // Override with something that's not NullLogger to have real logging. connection.BytesLogger = &BytesNullLogger{} @@ -55,7 +53,7 @@ func NewWebRTCPeer(config *webrtc.Configuration, // Pipes remain the same even when DataChannel gets switched. connection.recvPipe, connection.writePipe = io.Pipe() - err := connection.connect(config) + err := connection.connect(config, broker) if err != nil { connection.Close() return nil, err @@ -113,7 +111,7 @@ func (c *WebRTCPeer) checkForStaleness() { } } -func (c *WebRTCPeer) connect(config *webrtc.Configuration) error { +func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel) error { log.Println(c.id, " connecting...") // TODO: When go-webrtc is more stable, it's possible that a new // PeerConnection won't need to be re-prepared each time. @@ -127,8 +125,11 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration) error { // nolint: golint return errors.New("WebRTC: Could not establish DataChannel") } - err = c.exchangeSDP() - if err != nil { + answer := exchangeSDP(broker, c.pc.LocalDescription()) + log.Printf("Received Answer.\n") + err = c.pc.SetRemoteDescription(*answer) + if nil != err { + log.Println("WebRTC: Unable to SetRemoteDescription:", err) return err } go c.checkForStaleness() @@ -246,29 +247,20 @@ func (c *WebRTCPeer) establishDataChannel() error { return nil } -// exchangeSDP sends the local SDP offer to the Broker and awaits the SDP -// answer. -func (c *WebRTCPeer) exchangeSDP() error { +// exchangeSDP sends the local SDP offer to the Broker, awaits the SDP answer, +// and returns the answer. +func exchangeSDP(broker *BrokerChannel, offer *webrtc.SessionDescription) *webrtc.SessionDescription { // Keep trying the same offer until a valid answer arrives. - var answer *webrtc.SessionDescription for { - var err error // Send offer to broker (blocks). - answer, err = c.broker.Negotiate(c.pc.LocalDescription()) + answer, err := broker.Negotiate(offer) if err == nil { - break + return answer } log.Printf("BrokerChannel Error: %s", err) log.Printf("Failed to retrieve answer. Retrying in %v", ReconnectTimeout) <-time.After(ReconnectTimeout) } - log.Printf("Received Answer.\n") - err := c.pc.SetRemoteDescription(*answer) - if nil != err { - log.Println("WebRTC: Unable to SetRemoteDescription:", err) - return err - } - return nil } // Close all channels and transports From e8c41650ae44e300aabe57e361c445808635fb4f Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 24 Apr 2020 11:44:40 -0600 Subject: [PATCH 115/385] Move establishDataChannel to after exchangeSDP. --- client/lib/webrtc.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 7ceeaff..b4c0aad 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -120,11 +120,6 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel if err != nil { return err } - err = c.establishDataChannel() - if err != nil { - // nolint: golint - return errors.New("WebRTC: Could not establish DataChannel") - } answer := exchangeSDP(broker, c.pc.LocalDescription()) log.Printf("Received Answer.\n") err = c.pc.SetRemoteDescription(*answer) @@ -132,6 +127,11 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel log.Println("WebRTC: Unable to SetRemoteDescription:", err) return err } + err = c.establishDataChannel() + if err != nil { + // nolint: golint + return errors.New("WebRTC: Could not establish DataChannel") + } go c.checkForStaleness() return nil } From 047d3214bfb46de07e5d9f223e4fb1ba24584c8a Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 24 Apr 2020 13:30:13 -0600 Subject: [PATCH 116/385] Wait for data channel OnOpen before returning from NewWebRTCPeer. Now callers cannot call Write without there being a DataChannel to write to. This lets us remove the internal buffer and checks for transport == nil. Don't set internal fields like writePipe, transport, and pc to nil when closing; just close them and let them return errors if further calls are made on them. There's now a constant DataChannelTimeout that's separate from SnowflakeTimeout (the latter is what checkForStaleness uses). Now we can set DataChannel timeout to a lower value, to quickly dispose of unconnectable proxies, while still keeping the threshold for detecting the failure of a once-working proxy at 30 seconds. https://bugs.torproject.org/33897 --- client/lib/snowflake.go | 2 + client/lib/webrtc.go | 96 +++++++++++------------------------------ 2 files changed, 26 insertions(+), 72 deletions(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 27991b2..0076b79 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -17,6 +17,8 @@ import ( const ( ReconnectTimeout = 10 * time.Second SnowflakeTimeout = 30 * time.Second + // How long to wait for the OnOpen callback on a DataChannel. + DataChannelTimeout = 30 * time.Second ) type dummyAddr struct{} diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index b4c0aad..edc8ab4 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -1,7 +1,6 @@ package lib import ( - "bytes" "crypto/rand" "encoding/hex" "errors" @@ -25,12 +24,10 @@ type WebRTCPeer struct { recvPipe *io.PipeReader writePipe *io.PipeWriter lastReceive time.Time - buffer bytes.Buffer closed bool - lock sync.Mutex // Synchronization for DataChannel destruction - once sync.Once // Synchronization for PeerConnection destruction + once sync.Once // Synchronization for PeerConnection destruction BytesLogger BytesLogger } @@ -70,16 +67,11 @@ func (c *WebRTCPeer) Read(b []byte) (int, error) { // Writes bytes out to remote WebRTC. // As part of |io.ReadWriter| func (c *WebRTCPeer) Write(b []byte) (int, error) { - c.lock.Lock() - defer c.lock.Unlock() - c.BytesLogger.AddOutbound(len(b)) - // TODO: Buffering could be improved / separated out of WebRTCPeer. - if nil == c.transport { - log.Printf("Buffered %d bytes --> WebRTC", len(b)) - c.buffer.Write(b) - } else { - c.transport.Send(b) + err := c.transport.Send(b) + if err != nil { + return 0, err } + c.BytesLogger.AddOutbound(len(b)) return len(b), nil } @@ -127,8 +119,9 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel log.Println("WebRTC: Unable to SetRemoteDescription:", err) return err } - err = c.establishDataChannel() + c.transport, err = c.establishDataChannel() if err != nil { + log.Printf("establishDataChannel: %v", err) // nolint: golint return errors.New("WebRTC: Could not establish DataChannel") } @@ -177,13 +170,9 @@ func preparePeerConnection(config *webrtc.Configuration) (*webrtc.PeerConnection return pc, nil } -// Create a WebRTC DataChannel locally. -func (c *WebRTCPeer) establishDataChannel() error { - c.lock.Lock() - defer c.lock.Unlock() - if c.transport != nil { - panic("Unexpected datachannel already exists!") - } +// Create a WebRTC DataChannel locally. Blocks until the data channel is open, +// or a timeout or error occurs. +func (c *WebRTCPeer) establishDataChannel() (*webrtc.DataChannel, error) { ordered := true dataChannelOptions := &webrtc.DataChannelInit{ Ordered: &ordered, @@ -191,41 +180,15 @@ func (c *WebRTCPeer) establishDataChannel() error { dc, err := c.pc.CreateDataChannel(c.id, dataChannelOptions) if err != nil { log.Printf("CreateDataChannel ERROR: %s", err) - return err + return nil, err } + openChannel := make(chan struct{}) dc.OnOpen(func() { - c.lock.Lock() - defer c.lock.Unlock() log.Println("WebRTC: DataChannel.OnOpen") - if nil != c.transport { - panic("WebRTC: transport already exists.") - } - // Flush buffered outgoing SOCKS data if necessary. - if c.buffer.Len() > 0 { - dc.Send(c.buffer.Bytes()) - log.Println("Flushed", c.buffer.Len(), "bytes.") - c.buffer.Reset() - } - // Then enable the datachannel. - c.transport = dc + close(openChannel) }) dc.OnClose(func() { - c.lock.Lock() - // Future writes will go to the buffer until a new DataChannel is available. - if nil == c.transport { - // Closed locally, as part of a reset. - log.Println("WebRTC: DataChannel.OnClose [locally]") - c.lock.Unlock() - return - } - // Closed remotely, need to reset everything. - // Disable the DataChannel as a write destination. - log.Println("WebRTC: DataChannel.OnClose [remotely]") - c.transport = nil - dc.Close() - // Unlock before Close'ing, since it calls cleanup and asks for the - // lock to check if the transport needs to be be deleted. - c.lock.Unlock() + log.Println("WebRTC: DataChannel.OnClose") c.Close() }) dc.OnMessage(func(msg webrtc.DataChannelMessage) { @@ -244,7 +207,14 @@ func (c *WebRTCPeer) establishDataChannel() error { c.lastReceive = time.Now() }) log.Println("WebRTC: DataChannel created.") - return nil + + select { + case <-openChannel: + return dc, nil + case <-time.After(DataChannelTimeout): + dc.Close() + return nil, errors.New("timeout waiting for DataChannel.OnOpen") + } } // exchangeSDP sends the local SDP offer to the Broker, awaits the SDP answer, @@ -266,27 +236,10 @@ func exchangeSDP(broker *BrokerChannel, offer *webrtc.SessionDescription) *webrt // Close all channels and transports func (c *WebRTCPeer) cleanup() { // Close this side of the SOCKS pipe. - if nil != c.writePipe { - c.writePipe.Close() - c.writePipe = nil - } - c.lock.Lock() + c.writePipe.Close() if nil != c.transport { log.Printf("WebRTC: closing DataChannel") - dataChannel := c.transport - // Setting transport to nil *before* dc Close indicates to OnClose that - // this was locally triggered. - c.transport = nil - // Release the lock before calling DeleteDataChannel (which in turn - // calls Close on the dataChannel), but after nil'ing out the transport, - // since otherwise we'll end up in the onClose handler in a deadlock. - c.lock.Unlock() - if c.pc == nil { - panic("DataChannel w/o PeerConnection, not good.") - } - dataChannel.Close() - } else { - c.lock.Unlock() + c.transport.Close() } if nil != c.pc { log.Printf("WebRTC: closing PeerConnection") @@ -294,6 +247,5 @@ func (c *WebRTCPeer) cleanup() { if nil != err { log.Printf("Error closing peerconnection...") } - c.pc = nil } } From 1d2df3cd719cc6074880ed7f6a39f9eae535dee1 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 28 Apr 2020 12:55:58 -0400 Subject: [PATCH 117/385] Update calls to session description utils in proxy --- proxy/snowflake.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 422cf7e..4877e6f 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -200,7 +200,13 @@ func (b *Broker) pollOffer(sid string) *webrtc.SessionDescription { return nil } if offer != "" { - return util.DeserializeSessionDescription(offer) + offer, err := util.DeserializeSessionDescription(offer) + if err != nil { + log.Printf("Error processing session description: %s", err.Error()) + return nil + } + return offer + } } } @@ -217,7 +223,10 @@ func (b *Broker) sendAnswer(sid string, pc *webrtc.PeerConnection) error { SDP: util.StripLocalAddresses(ld.SDP), } } - answer := string([]byte(util.SerializeSessionDescription(ld))) + answer, err := util.SerializeSessionDescription(ld) + if err != nil { + return err + } body, err := messages.EncodeAnswerRequest(answer, sid) if err != nil { return err From 5e8f9ac538002ef9d7ec195ccbcd51401755c657 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 28 Apr 2020 13:01:32 -0400 Subject: [PATCH 118/385] Update proxy tests to check serialization errors --- proxy/proxy-go_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/proxy/proxy-go_test.go b/proxy/proxy-go_test.go index bed00f2..03d7307 100644 --- a/proxy/proxy-go_test.go +++ b/proxy/proxy-go_test.go @@ -198,7 +198,7 @@ func TestSessionDescriptions(t *testing.T) { }, }, } { - desc := util.DeserializeSessionDescription(test.msg) + desc, _ := util.DeserializeSessionDescription(test.msg) So(desc, ShouldResemble, test.ret) } }) @@ -215,8 +215,9 @@ func TestSessionDescriptions(t *testing.T) { `{"type":"offer","sdp":"test"}`, }, } { - msg := util.SerializeSessionDescription(test.desc) + msg, err := util.SerializeSessionDescription(test.desc) So(msg, ShouldResemble, test.ret) + So(err, ShouldBeNil) } }) } @@ -240,7 +241,7 @@ func TestBrokerInteractions(t *testing.T) { }, } pc, _ := webrtc.NewPeerConnection(config) - offer := util.DeserializeSessionDescription(sampleOffer) + offer, _ := util.DeserializeSessionDescription(sampleOffer) pc.SetRemoteDescription(*offer) answer, _ := pc.CreateAnswer(nil) pc.SetLocalDescription(answer) From 72cfb96edeb7c9a3c93d38539bc31a51e30dbe8d Mon Sep 17 00:00:00 2001 From: David Fifield Date: Tue, 28 Apr 2020 11:33:09 -0600 Subject: [PATCH 119/385] Restore check for nil writePipe in WebRTCPeer.Close. I removed this check in 047d3214bfb46de07e5d9f223e4fb1ba24584c8a because NewWebRTCPeer always initializes writePipe, and it is never reset to nil. However tests used &WebRTCPeer{} which bypasses NewWebRTCPeer and leaves writePipe set to nil. https://bugs.torproject.org/34049#comment:3 https://bugs.torproject.org/34050 --- client/lib/webrtc.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index edc8ab4..23cb3e1 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -236,7 +236,9 @@ func exchangeSDP(broker *BrokerChannel, offer *webrtc.SessionDescription) *webrt // Close all channels and transports func (c *WebRTCPeer) cleanup() { // Close this side of the SOCKS pipe. - c.writePipe.Close() + if c.writePipe != nil { // c.writePipe can be nil in tests. + c.writePipe.Close() + } if nil != c.transport { log.Printf("WebRTC: closing DataChannel") c.transport.Close() From c8293a5de315ddc5e212e0c894ebdc82a46ce1e1 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 1 May 2020 10:30:04 -0600 Subject: [PATCH 120/385] Format the establishDataChannel error log message like other log messages. It was sticking out in the context of other log messages. 2020/04/30 22:39:10 WebRTC: DataChannel created. 2020/04/30 22:39:20 establishDataChannel: timeout waiting for DataChannel.OnOpen 2020/04/30 22:39:20 WebRTC: closing PeerConnection 2020/04/30 22:39:20 WebRTC: Closing 2020/04/30 22:39:20 WebRTC: WebRTC: Could not establish DataChannel Retrying in 10s... --- client/lib/webrtc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 23cb3e1..af5a45a 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -121,7 +121,7 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel } c.transport, err = c.establishDataChannel() if err != nil { - log.Printf("establishDataChannel: %v", err) + log.Printf("WebRTC: establishing data channel: %v", err) // nolint: golint return errors.New("WebRTC: Could not establish DataChannel") } From 7043a055f9fb0680281ecffd7d458a43f2ce65b5 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 1 May 2020 10:49:40 -0600 Subject: [PATCH 121/385] Reduce DataChannelTimeout from 30s to 10s. https://bugs.torproject.org/34042 --- client/lib/snowflake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 0076b79..91f7ecb 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -18,7 +18,7 @@ const ( ReconnectTimeout = 10 * time.Second SnowflakeTimeout = 30 * time.Second // How long to wait for the OnOpen callback on a DataChannel. - DataChannelTimeout = 30 * time.Second + DataChannelTimeout = 10 * time.Second ) type dummyAddr struct{} From bbf11a97e4728ca41ecb7c34117ec407de4988ec Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 7 May 2020 09:40:49 -0400 Subject: [PATCH 122/385] Reduce SnowflakeTimeout to 20 seconds The underlying smux layer sends a keep-alive ping every 10 seconds. This modification will allow for one dropped/delayed ping before discarding the snowflake --- client/lib/snowflake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 91f7ecb..b355c3e 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -16,7 +16,7 @@ import ( const ( ReconnectTimeout = 10 * time.Second - SnowflakeTimeout = 30 * time.Second + SnowflakeTimeout = 20 * time.Second // How long to wait for the OnOpen callback on a DataChannel. DataChannelTimeout = 10 * time.Second ) From 1448c3885f2bdfde9d7dfc801e730cfe5301a224 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 12 May 2020 16:08:15 -0400 Subject: [PATCH 123/385] Update documentation to include broker spec Add broker messaging specification with endpoints for clients and proxies. --- doc/broker-spec.txt | 113 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/doc/broker-spec.txt b/doc/broker-spec.txt index eba3347..c3177e0 100644 --- a/doc/broker-spec.txt +++ b/doc/broker-spec.txt @@ -67,3 +67,116 @@ Metrics data from the Snowflake broker can be retrieved by sending an HTTP GET r A count of the number of times a client successfully received a proxy from the broker, rounded up to the nearest multiple of 8. + +2. Broker messaging specification and endpoints + +The broker facilitates the connection of snowflake clients and snowflake proxies +through the exchange of WebRTC SDP information with its endpoints. + +2.1. Client interactions with the broker + +Clients interact with the broker by making a POST request to `/client` with the +offer SDP in the request body: +``` +POST /client HTTP + +[offer SDP] +``` +If the broker is behind a domain-fronted connection, this request is accompanied +with the necessary HOST information. + +If the client is matched up with a proxy, they receive a 200 OK response with +the proxy's answer SDP in the request body: +``` +HTTP 200 OK + +[answer SDP] +``` + +If no proxies were available, they receive a 503 status code: +``` +HTTP 503 Service Unavailable +``` + + +2.2 Proxy interactions with the broker + +Proxies poll the broker with a proxy poll request to `/proxy`: + +``` +POST /proxy HTTP + +{ + Sid: [generated session id of proxy], + Version: 1.1, + Type: ["badge"|"webext"|"standalone"|"mobile"] +} +``` + +If the request is well-formed, they receive a 200 OK response. + +If a client is matched: +``` +HTTP 200 OK + +{ + Status: "client match", + { + type: offer, + sdp: [WebRTC SDP] + } +} +``` + +If a client is not matched: +``` +HTTP 200 OK + +{ + Status: "no match" +} +``` + +If the request is malformed: +``` +HTTP 400 BadRequest +``` + +If they are matched with a client, they provide their SDP answer with a POST +request to `/answer`: +``` +POST /answer HTTP + +{ + Sid: [generated session id of proxy], + Version: 1.1, + Answer: + { + type: answer, + sdp: [WebRTC SDP] + } +} +``` + +If the request is well-formed, they receive a 200 OK response. + +If the client retrieved the answer: +``` +HTTP 200 OK + +{ + Status: "success" +} +``` + +If the client left: +``` +HTTP 200 OK + +{ + Status: "client gone" +} + +3) If the request is malformed: +HTTP 400 BadRequest +``` From bf924445e36a1990ad1da89d99faeff17db9e42f Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 10 Jun 2020 17:16:12 -0400 Subject: [PATCH 124/385] Implement NAT discovery (RFC 5780) at the client Snowflake clients will now attempt NAT discovery using the provided STUN servers and report their NAT type to the Snowflake broker for matching. The three possibilities for NAT types are: - unknown (the client was unable to determine their NAT type), - restricted (the client has a restrictive NAT and can only be paired with unrestricted NATs) - unrestricted (the client can be paired with any other NAT). --- client/lib/rendezvous.go | 16 +++ client/snowflake.go | 26 ++++ common/nat/nat.go | 251 +++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 5 files changed, 296 insertions(+) create mode 100644 common/nat/nat.go diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index ca15d35..2702d4e 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -16,7 +16,9 @@ import ( "log" "net/http" "net/url" + "sync" + "git.torproject.org/pluggable-transports/snowflake.git/common/nat" "git.torproject.org/pluggable-transports/snowflake.git/common/util" "github.com/pion/webrtc/v2" ) @@ -36,6 +38,8 @@ type BrokerChannel struct { url *url.URL transport http.RoundTripper // Used to make all requests. keepLocalAddresses bool + NATType string + lock sync.Mutex } // We make a copy of DefaultTransport because we want the default Dial @@ -66,6 +70,7 @@ func NewBrokerChannel(broker string, front string, transport http.RoundTripper, bc.transport = transport bc.keepLocalAddresses = keepLocalAddresses + bc.NATType = nat.NATUnknown return bc, nil } @@ -110,6 +115,10 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( if "" != bc.Host { // Set true host if necessary. request.Host = bc.Host } + // include NAT-TYPE + bc.lock.Lock() + request.Header.Set("Snowflake-NAT-TYPE", bc.NATType) + bc.lock.Unlock() resp, err := bc.transport.RoundTrip(request) if nil != err { return nil, err @@ -133,6 +142,13 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( } } +func (bc *BrokerChannel) SetNATType(NATType string) { + bc.lock.Lock() + bc.NATType = NATType + bc.lock.Unlock() + log.Printf("NAT Type: %s", NATType) +} + // Implements the |Tongue| interface to catch snowflakes, using BrokerChannel. type WebRTCDialer struct { *BrokerChannel diff --git a/client/snowflake.go b/client/snowflake.go index d66225d..02bbf1e 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -16,6 +16,7 @@ import ( pt "git.torproject.org/pluggable-transports/goptlib.git" sf "git.torproject.org/pluggable-transports/snowflake.git/client/lib" + "git.torproject.org/pluggable-transports/snowflake.git/common/nat" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" "github.com/pion/webrtc/v2" ) @@ -158,6 +159,8 @@ func main() { if err != nil { log.Fatalf("parsing broker URL: %v", err) } + go updateNATType(iceServers, broker) + snowflakes.Tongue = sf.NewWebRTCDialer(broker, iceServers) // Use a real logger to periodically output how much traffic is happening. @@ -219,3 +222,26 @@ func main() { snowflakes.End() log.Println("snowflake is done.") } + +// loop through all provided STUN servers until we exhaust the list or find +// one that is compatable with RFC 5780 +func updateNATType(servers []webrtc.ICEServer, broker *sf.BrokerChannel) { + + var restrictedNAT bool + var err error + for _, server := range servers { + addr := strings.TrimPrefix(server.URLs[0], "stun:") + restrictedNAT, err = nat.CheckIfRestrictedNAT(addr) + if err == nil { + if restrictedNAT { + broker.SetNATType(nat.NATRestricted) + } else { + broker.SetNATType(nat.NATUnrestricted) + } + break + } + } + if err != nil { + broker.SetNATType(nat.NATUnknown) + } +} diff --git a/common/nat/nat.go b/common/nat/nat.go new file mode 100644 index 0000000..c9f16ad --- /dev/null +++ b/common/nat/nat.go @@ -0,0 +1,251 @@ +/* +The majority of this code is taken from a utility I wrote for pion/stun +https://github.com/pion/stun/blob/master/cmd/stun-nat-behaviour/main.go + +Copyright 2018 Pion LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package nat + +import ( + "errors" + "fmt" + "github.com/pion/stun" + "log" + "net" + "time" +) + +var ErrTimedOut = errors.New("timed out waiting for response") + +const ( + NATUnknown = "unknown" + NATRestricted = "restricted" + NATUnrestricted = "unrestricted" +) + +// This function checks the NAT mapping and filtering +// behaviour and returns true if the NAT is restrictive +// (address-dependent mapping and/or port-dependent filtering) +// and false if the NAT is unrestrictive (meaning it +// will work with most other NATs), +func CheckIfRestrictedNAT(server string) (bool, error) { + result, err := isRestrictedMapping(server) + if err != nil { + return false, err + } + if !result { + // if the mapping is unrestrictive, we still need to check whether + // the filtering is restrictive + result, err = isRestrictedFiltering(server) + } + return result, err +} + +// Performs two tests from RFC 5780 to determine whether the mapping type +// of the client's NAT is address-independent or address-dependent +// Returns true if the mapping is address-dependent and false otherwise +func isRestrictedMapping(addrStr string) (bool, error) { + var xorAddr1 stun.XORMappedAddress + var xorAddr2 stun.XORMappedAddress + + mapTestConn, err := connect(addrStr) + if err != nil { + log.Printf("Error creating STUN connection: %s", err.Error()) + return false, err + } + + defer mapTestConn.Close() + + // Test I: Regular binding request + message := stun.MustBuild(stun.TransactionID, stun.BindingRequest) + + resp, err := mapTestConn.RoundTrip(message, mapTestConn.PrimaryAddr) + if err == ErrTimedOut { + log.Printf("Error: no response from server") + return false, err + } + if err != nil { + log.Printf("Error receiving response from server: %s", err.Error()) + return false, err + } + + // Decoding XOR-MAPPED-ADDRESS attribute from message. + if err = xorAddr1.GetFrom(resp); err != nil { + log.Printf("Error retrieving XOR-MAPPED-ADDRESS resonse: %s", err.Error()) + return false, err + } + + // Decoding OTHER-ADDRESS attribute from message. + var otherAddr stun.OtherAddress + if err = otherAddr.GetFrom(resp); err != nil { + log.Println("NAT discovery feature not supported by this server") + return false, err + } + + if err = mapTestConn.AddOtherAddr(otherAddr.String()); err != nil { + log.Printf("Failed to resolve address %s\t", otherAddr.String()) + return false, err + } + + // Test II: Send binding request to other address + resp, err = mapTestConn.RoundTrip(message, mapTestConn.OtherAddr) + if err == ErrTimedOut { + log.Printf("Error: no response from server") + return false, err + } + if err != nil { + log.Printf("Error retrieving server response: %s", err.Error()) + return false, err + } + + // Decoding XOR-MAPPED-ADDRESS attribute from message. + if err = xorAddr2.GetFrom(resp); err != nil { + log.Printf("Error retrieving XOR-MAPPED-ADDRESS resonse: %s", err.Error()) + return false, err + } + + return xorAddr1.String() != xorAddr2.String(), nil + +} + +// Performs two tests from RFC 5780 to determine whether the filtering type +// of the client's NAT is port-dependent. +// Returns true if the filtering is port-dependent and false otherwise +func isRestrictedFiltering(addrStr string) (bool, error) { + var xorAddr stun.XORMappedAddress + + mapTestConn, err := connect(addrStr) + if err != nil { + log.Printf("Error creating STUN connection: %s", err.Error()) + return false, err + } + + defer mapTestConn.Close() + + // Test I: Regular binding request + message := stun.MustBuild(stun.TransactionID, stun.BindingRequest) + + resp, err := mapTestConn.RoundTrip(message, mapTestConn.PrimaryAddr) + if err == ErrTimedOut { + log.Printf("Error: no response from server") + return false, err + } + if err != nil { + log.Printf("Error: %s", err.Error()) + return false, err + } + + // Decoding XOR-MAPPED-ADDRESS attribute from message. + if err = xorAddr.GetFrom(resp); err != nil { + log.Printf("Error retrieving XOR-MAPPED-ADDRESS from resonse: %s", err.Error()) + return false, err + } + + // Test III: Request port change + message.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x02}) + + _, err = mapTestConn.RoundTrip(message, mapTestConn.PrimaryAddr) + if err != ErrTimedOut && err != nil { + // something else went wrong + log.Printf("Error reading response from server: %s", err.Error()) + return false, err + } + + return err == ErrTimedOut, nil +} + +// Given an address string, returns a StunServerConn +func connect(addrStr string) (*StunServerConn, error) { + // Creating a "connection" to STUN server. + addr, err := net.ResolveUDPAddr("udp4", addrStr) + if err != nil { + log.Printf("Error resolving address: %s\n", err.Error()) + return nil, err + } + + c, err := net.ListenUDP("udp4", nil) + if err != nil { + return nil, err + } + + mChan := listen(c) + + return &StunServerConn{ + conn: c, + PrimaryAddr: addr, + messageChan: mChan, + }, nil +} + +type StunServerConn struct { + conn net.PacketConn + PrimaryAddr *net.UDPAddr + OtherAddr *net.UDPAddr + messageChan chan *stun.Message +} + +func (c *StunServerConn) Close() { + c.conn.Close() +} + +func (c *StunServerConn) RoundTrip(msg *stun.Message, addr net.Addr) (*stun.Message, error) { + _, err := c.conn.WriteTo(msg.Raw, addr) + if err != nil { + return nil, err + } + + // Wait for response or timeout + select { + case m, ok := <-c.messageChan: + if !ok { + return nil, fmt.Errorf("error reading from messageChan") + } + return m, nil + case <-time.After(10 * time.Second): + return nil, ErrTimedOut + } +} + +func (c *StunServerConn) AddOtherAddr(addrStr string) error { + addr2, err := net.ResolveUDPAddr("udp4", addrStr) + if err != nil { + return err + } + c.OtherAddr = addr2 + return nil +} + +// taken from https://github.com/pion/stun/blob/master/cmd/stun-traversal/main.go +func listen(conn *net.UDPConn) chan *stun.Message { + messages := make(chan *stun.Message) + go func() { + for { + buf := make([]byte, 1024) + + n, _, err := conn.ReadFromUDP(buf) + if err != nil { + close(messages) + return + } + buf = buf[:n] + + m := new(stun.Message) + m.Raw = buf + err = m.Decode() + if err != nil { + close(messages) + return + } + + messages <- m + } + }() + return messages +} diff --git a/go.mod b/go.mod index 07c49a2..2ba1b2d 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/golang/protobuf v1.3.1 // indirect github.com/gorilla/websocket v1.4.1 github.com/pion/sdp/v2 v2.3.4 + github.com/pion/stun v0.3.5 github.com/pion/webrtc/v2 v2.2.2 github.com/smartystreets/goconvey v1.6.4 github.com/xtaci/kcp-go/v5 v5.5.12 diff --git a/go.sum b/go.sum index 6768e02..9ccfb30 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/pion/srtp v1.2.7 h1:UYyLs5MXwbFtXWduBA5+RUWhaEBX7GmetXDZSKP+uPM= github.com/pion/srtp v1.2.7/go.mod h1:KIgLSadhg/ioogO/LqIkRjZrwuJo0c9RvKIaGQj4Yew= github.com/pion/stun v0.3.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw= github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= +github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= +github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk= github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= From f6cf9a453b0cae189e5ad6b25d57241fe418de91 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 16 Jun 2020 17:10:56 -0400 Subject: [PATCH 125/385] Implement NAT discover for go standalone proxies --- broker/broker.go | 2 +- common/messages/proxy.go | 30 ++++++++++++++++++----------- common/messages/proxy_test.go | 26 +++++++++++++++++++++---- proxy/snowflake.go | 36 ++++++++++++++++++++++++++++++++++- 4 files changed, 77 insertions(+), 17 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index d9ef111..2d3cd4b 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -170,7 +170,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { return } - sid, proxyType, err := messages.DecodePollRequest(body) + sid, proxyType, _, err := messages.DecodePollRequest(body) if err != nil { w.WriteHeader(http.StatusBadRequest) return diff --git a/common/messages/proxy.go b/common/messages/proxy.go index 89dd43c..923189b 100644 --- a/common/messages/proxy.go +++ b/common/messages/proxy.go @@ -9,15 +9,16 @@ import ( "strings" ) -const version = "1.1" +const version = "1.2" -/* Version 1.1 specification: +/* Version 1.2 specification: == ProxyPollRequest == { Sid: [generated session id of proxy], - Version: 1.1, + Version: 1.2, Type: ["badge"|"webext"|"standalone"] + NAT: ["unknown"|"restricted"|"unrestricted"] } == ProxyPollResponse == @@ -44,7 +45,7 @@ HTTP 400 BadRequest == ProxyAnswerRequest == { Sid: [generated session id of proxy], - Version: 1.1, + Version: 1.2, Answer: { type: answer, @@ -76,37 +77,44 @@ type ProxyPollRequest struct { Sid string Version string Type string + NAT string } -func EncodePollRequest(sid string, proxyType string) ([]byte, error) { +func EncodePollRequest(sid string, proxyType string, natType string) ([]byte, error) { return json.Marshal(ProxyPollRequest{ Sid: sid, Version: version, Type: proxyType, + NAT: natType, }) } // Decodes a poll message from a snowflake proxy and returns the // sid and proxy type of the proxy on success and an error if it failed -func DecodePollRequest(data []byte) (string, string, error) { +func DecodePollRequest(data []byte) (string, string, string, error) { var message ProxyPollRequest err := json.Unmarshal(data, &message) if err != nil { - return "", "", err + return "", "", "", err } majorVersion := strings.Split(message.Version, ".")[0] if majorVersion != "1" { - return "", "", fmt.Errorf("using unknown version") + return "", "", "", fmt.Errorf("using unknown version") } // Version 1.x requires an Sid if message.Sid == "" { - return "", "", fmt.Errorf("no supplied session id") + return "", "", "", fmt.Errorf("no supplied session id") } - return message.Sid, message.Type, nil + natType := message.NAT + if natType == "" { + natType = "unknown" + } + + return message.Sid, message.Type, natType, nil } type ProxyPollResponse struct { @@ -159,7 +167,7 @@ type ProxyAnswerRequest struct { func EncodeAnswerRequest(answer string, sid string) ([]byte, error) { return json.Marshal(ProxyAnswerRequest{ - Version: "1.1", + Version: version, Sid: sid, Answer: answer, }) diff --git a/common/messages/proxy_test.go b/common/messages/proxy_test.go index 1570d4f..3aa67fb 100644 --- a/common/messages/proxy_test.go +++ b/common/messages/proxy_test.go @@ -13,6 +13,7 @@ func TestDecodeProxyPollRequest(t *testing.T) { for _, test := range []struct { sid string proxyType string + natType string data string err error }{ @@ -20,6 +21,7 @@ func TestDecodeProxyPollRequest(t *testing.T) { //Version 1.0 proxy message "ymbcCMto7KHNGYlp", "", + "unknown", `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`, nil, }, @@ -27,44 +29,59 @@ func TestDecodeProxyPollRequest(t *testing.T) { //Version 1.1 proxy message "ymbcCMto7KHNGYlp", "standalone", + "unknown", `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.1","Type":"standalone"}`, nil, }, + { + //Version 1.2 proxy message + "ymbcCMto7KHNGYlp", + "standalone", + "restricted", + `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"standalone", "NAT":"restricted"}`, + nil, + }, { //Version 0.X proxy message: "", "", - "ymbcCMto7KHNGYlp", + "", + "", &json.SyntaxError{}, }, { + "", "", "", `{"Sid":"ymbcCMto7KHNGYlp"}`, fmt.Errorf(""), }, { + "", "", "", "{}", fmt.Errorf(""), }, { + "", "", "", `{"Version":"1.0"}`, fmt.Errorf(""), }, { + "", "", "", `{"Version":"2.0"}`, fmt.Errorf(""), }, } { - sid, proxyType, err := DecodePollRequest([]byte(test.data)) + sid, proxyType, natType, err := DecodePollRequest([]byte(test.data)) So(sid, ShouldResemble, test.sid) So(proxyType, ShouldResemble, test.proxyType) + So(natType, ShouldResemble, test.natType) So(err, ShouldHaveSameTypeAs, test.err) } @@ -73,11 +90,12 @@ func TestDecodeProxyPollRequest(t *testing.T) { func TestEncodeProxyPollRequests(t *testing.T) { Convey("Context", t, func() { - b, err := EncodePollRequest("ymbcCMto7KHNGYlp", "standalone") + b, err := EncodePollRequest("ymbcCMto7KHNGYlp", "standalone", "unknown") So(err, ShouldEqual, nil) - sid, proxyType, err := DecodePollRequest(b) + sid, proxyType, natType, err := DecodePollRequest(b) So(sid, ShouldEqual, "ymbcCMto7KHNGYlp") So(proxyType, ShouldEqual, "standalone") + So(natType, ShouldEqual, "unknown") So(err, ShouldEqual, nil) }) } diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 4877e6f..ac67748 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -19,6 +19,7 @@ import ( "time" "git.torproject.org/pluggable-transports/snowflake.git/common/messages" + "git.torproject.org/pluggable-transports/snowflake.git/common/nat" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" "git.torproject.org/pluggable-transports/snowflake.git/common/util" "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" @@ -30,6 +31,11 @@ const defaultBrokerURL = "https://snowflake-broker.bamsoftware.com/" const defaultRelayURL = "wss://snowflake.bamsoftware.com/" const defaultSTUNURL = "stun:stun.l.google.com:19302" const pollInterval = 5 * time.Second +const ( + NATUnknown = "unknown" + NATRestricted = "restricted" + NATUnrestricted = "unrestricted" +) //amount of time after sending an SDP answer before the proxy assumes the //client is not going to connect @@ -40,6 +46,8 @@ const readLimit = 100000 //Maximum number of bytes to be read from an HTTP reque var broker *Broker var relayURL string +var currentNATType = NATUnknown + const ( sessionIDLength = 16 ) @@ -174,7 +182,7 @@ func (b *Broker) pollOffer(sid string) *webrtc.SessionDescription { timeOfNextPoll = now } - body, err := messages.EncodePollRequest(sid, "standalone") + body, err := messages.EncodePollRequest(sid, "standalone", currentNATType) if err != nil { log.Printf("Error encoding poll message: %s", err.Error()) return nil @@ -485,9 +493,35 @@ func main() { tokens <- true } + // determine NAT type before polling + updateNATType(config.ICEServers) + log.Printf("NAT type: %s", currentNATType) + for { getToken() sessionID := genSessionID() runSession(sessionID) } } + +// use provided STUN server(s) to determine NAT type +func updateNATType(servers []webrtc.ICEServer) { + + var restrictedNAT bool + var err error + for _, server := range servers { + addr := strings.TrimPrefix(server.URLs[0], "stun:") + restrictedNAT, err = nat.CheckIfRestrictedNAT(addr) + if err == nil { + if restrictedNAT { + currentNATType = NATRestricted + } else { + currentNATType = NATUnrestricted + } + break + } + } + if err != nil { + currentNATType = NATUnknown + } +} From 0052c0e10cfd8e270a57d85711064e8d9e064bf5 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 16 Jun 2020 17:49:39 -0400 Subject: [PATCH 126/385] Add a new heap at the broker for restricted flakes Now when proxies poll, they provide their NAT type to the broker. This introduces a new snowflake heap of just restricted snowflakes that the broker can pull from if the client has a known, unrestricted NAT. All other clients will pull from a heap of snowflakes with unrestricted or unknown NAT topologies. --- broker/broker.go | 67 ++++++++++++++++++++++++--------- broker/snowflake-broker_test.go | 14 +++---- 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index 2d3cd4b..9297980 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -31,12 +31,18 @@ const ( ClientTimeout = 10 ProxyTimeout = 10 readLimit = 100000 //Maximum number of bytes to be read from an HTTP request + + NATUnknown = "unknown" + NATRestricted = "restricted" + NATUnrestricted = "unrestricted" ) type BrokerContext struct { - snowflakes *SnowflakeHeap - // Map keeping track of snowflakeIDs required to match SDP answers from - // the second http POST. + snowflakes *SnowflakeHeap + restrictedSnowflakes *SnowflakeHeap + // Maps keeping track of snowflakeIDs required to match SDP answers from + // the second http POST. Restricted snowflakes can only be matched up with + // clients behind an unrestricted NAT. idToSnowflake map[string]*Snowflake // Synchronization for the snowflake map and heap snowflakeLock sync.Mutex @@ -47,6 +53,8 @@ type BrokerContext struct { func NewBrokerContext(metricsLogger *log.Logger) *BrokerContext { snowflakes := new(SnowflakeHeap) heap.Init(snowflakes) + rSnowflakes := new(SnowflakeHeap) + heap.Init(rSnowflakes) metrics, err := NewMetrics(metricsLogger) if err != nil { @@ -58,10 +66,11 @@ func NewBrokerContext(metricsLogger *log.Logger) *BrokerContext { } return &BrokerContext{ - snowflakes: snowflakes, - idToSnowflake: make(map[string]*Snowflake), - proxyPolls: make(chan *ProxyPoll), - metrics: metrics, + snowflakes: snowflakes, + restrictedSnowflakes: rSnowflakes, + idToSnowflake: make(map[string]*Snowflake), + proxyPolls: make(chan *ProxyPoll), + metrics: metrics, } } @@ -79,7 +88,7 @@ type MetricsHandler struct { func (sh SnowflakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Session-ID") + w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Session-ID, Snowflake-NAT-Type") // Return early if it's CORS preflight. if "OPTIONS" == r.Method { return @@ -101,15 +110,17 @@ func (mh MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { type ProxyPoll struct { id string proxyType string + natType string offerChannel chan []byte } // Registers a Snowflake and waits for some Client to send an offer, // as part of the polling logic of the proxy handler. -func (ctx *BrokerContext) RequestOffer(id string, proxyType string) []byte { +func (ctx *BrokerContext) RequestOffer(id string, proxyType string, natType string) []byte { request := new(ProxyPoll) request.id = id request.proxyType = proxyType + request.natType = natType request.offerChannel = make(chan []byte) ctx.proxyPolls <- request // Block until an offer is available, or timeout which sends a nil offer. @@ -122,7 +133,7 @@ func (ctx *BrokerContext) RequestOffer(id string, proxyType string) []byte { // client offer or nil on timeout / none are available. func (ctx *BrokerContext) Broker() { for request := range ctx.proxyPolls { - snowflake := ctx.AddSnowflake(request.id, request.proxyType) + snowflake := ctx.AddSnowflake(request.id, request.proxyType, request.natType) // Wait for a client to avail an offer to the snowflake. go func(request *ProxyPoll) { select { @@ -133,7 +144,11 @@ func (ctx *BrokerContext) Broker() { ctx.snowflakeLock.Lock() defer ctx.snowflakeLock.Unlock() if snowflake.index != -1 { - heap.Remove(ctx.snowflakes, snowflake.index) + if request.natType == NATRestricted { + heap.Remove(ctx.restrictedSnowflakes, snowflake.index) + } else { + heap.Remove(ctx.snowflakes, snowflake.index) + } delete(ctx.idToSnowflake, snowflake.id) close(request.offerChannel) } @@ -145,7 +160,7 @@ func (ctx *BrokerContext) Broker() { // Create and add a Snowflake to the heap. // Required to keep track of proxies between providing them // with an offer and awaiting their second POST with an answer. -func (ctx *BrokerContext) AddSnowflake(id string, proxyType string) *Snowflake { +func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType string) *Snowflake { snowflake := new(Snowflake) snowflake.id = id snowflake.clients = 0 @@ -153,7 +168,11 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string) *Snowflake { snowflake.offerChannel = make(chan []byte) snowflake.answerChannel = make(chan []byte) ctx.snowflakeLock.Lock() - heap.Push(ctx.snowflakes, snowflake) + if natType == NATRestricted { + heap.Push(ctx.restrictedSnowflakes, snowflake) + } else { + heap.Push(ctx.snowflakes, snowflake) + } ctx.snowflakeLock.Unlock() ctx.idToSnowflake[id] = snowflake return snowflake @@ -170,7 +189,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { return } - sid, proxyType, _, err := messages.DecodePollRequest(body) + sid, proxyType, natType, err := messages.DecodePollRequest(body) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -187,7 +206,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { } // Wait for a client to avail an offer to the snowflake, or timeout if nil. - offer := ctx.RequestOffer(sid, proxyType) + offer := ctx.RequestOffer(sid, proxyType, natType) var b []byte if nil == offer { ctx.metrics.lock.Lock() @@ -226,9 +245,23 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) return } + + natType := r.Header.Get("Snowflake-NAT-Type") + if natType == "" { + natType = NATUnknown + } + + // Only hand out known restricted snowflakes to unrestricted clients + var snowflakeHeap *SnowflakeHeap + if natType == NATUnrestricted { + snowflakeHeap = ctx.restrictedSnowflakes + } else { + snowflakeHeap = ctx.snowflakes + } + // Immediately fail if there are no snowflakes available. ctx.snowflakeLock.Lock() - numSnowflakes := ctx.snowflakes.Len() + numSnowflakes := snowflakeHeap.Len() ctx.snowflakeLock.Unlock() if numSnowflakes <= 0 { ctx.metrics.lock.Lock() @@ -240,7 +273,7 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { // Otherwise, find the most available snowflake proxy, and pass the offer to it. // Delete must be deferred in order to correctly process answer request later. ctx.snowflakeLock.Lock() - snowflake := heap.Pop(ctx.snowflakes).(*Snowflake) + snowflake := heap.Pop(snowflakeHeap).(*Snowflake) ctx.snowflakeLock.Unlock() snowflake.offerChannel <- offer diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 18b83dd..91383a1 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -29,7 +29,7 @@ func TestBroker(t *testing.T) { Convey("Adds Snowflake", func() { So(ctx.snowflakes.Len(), ShouldEqual, 0) So(len(ctx.idToSnowflake), ShouldEqual, 0) - ctx.AddSnowflake("foo", "") + ctx.AddSnowflake("foo", "", NATUnknown) So(ctx.snowflakes.Len(), ShouldEqual, 1) So(len(ctx.idToSnowflake), ShouldEqual, 1) }) @@ -55,7 +55,7 @@ func TestBroker(t *testing.T) { Convey("Request an offer from the Snowflake Heap", func() { done := make(chan []byte) go func() { - offer := ctx.RequestOffer("test", "") + offer := ctx.RequestOffer("test", "", NATUnknown) done <- offer }() request := <-ctx.proxyPolls @@ -79,7 +79,7 @@ func TestBroker(t *testing.T) { Convey("with a proxy answer if available.", func() { done := make(chan bool) // Prepare a fake proxy to respond with. - snowflake := ctx.AddSnowflake("fake", "") + snowflake := ctx.AddSnowflake("fake", "", NATUnknown) go func() { clientOffers(ctx, w, r) done <- true @@ -97,7 +97,7 @@ func TestBroker(t *testing.T) { return } done := make(chan bool) - snowflake := ctx.AddSnowflake("fake", "") + snowflake := ctx.AddSnowflake("fake", "", NATUnknown) go func() { clientOffers(ctx, w, r) // Takes a few seconds here... @@ -147,7 +147,7 @@ func TestBroker(t *testing.T) { }) Convey("Responds to proxy answers...", func() { - s := ctx.AddSnowflake("test", "") + s := ctx.AddSnowflake("test", "", NATUnknown) w := httptest.NewRecorder() data := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"test","Answer":"test"}`)) @@ -260,7 +260,7 @@ func TestBroker(t *testing.T) { // Manually do the Broker goroutine action here for full control. p := <-ctx.proxyPolls So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") - s := ctx.AddSnowflake(p.id, "") + s := ctx.AddSnowflake(p.id, "", NATUnknown) go func() { offer := <-s.offerChannel p.offerChannel <- offer @@ -537,7 +537,7 @@ func TestMetrics(t *testing.T) { So(err, ShouldBeNil) // Prepare a fake proxy to respond with. - snowflake := ctx.AddSnowflake("fake", "") + snowflake := ctx.AddSnowflake("fake", "", NATUnknown) go func() { clientOffers(ctx, w, r) done <- true From 046dab865f18eb12a473b1a1c1d7aa15cf5883c7 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 22 Jun 2020 14:04:29 -0400 Subject: [PATCH 127/385] Have broker pass client NAT type to proxy This will allow browser-based proxies that are unable to determine their NAT type to conservatively label themselves as restricted NATs if they fail to work with clients that have restricted NATs. --- broker/broker.go | 31 ++++++++++++++++++++----------- broker/snowflake-broker_test.go | 26 +++++++++++++------------- broker/snowflake-heap.go | 2 +- common/messages/proxy.go | 24 ++++++++++++++++-------- common/messages/proxy_test.go | 16 +++++++++------- proxy/proxy-go_test.go | 2 +- proxy/snowflake.go | 2 +- 7 files changed, 61 insertions(+), 42 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index 9297980..99f6c69 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -111,17 +111,17 @@ type ProxyPoll struct { id string proxyType string natType string - offerChannel chan []byte + offerChannel chan *ClientOffer } // Registers a Snowflake and waits for some Client to send an offer, // as part of the polling logic of the proxy handler. -func (ctx *BrokerContext) RequestOffer(id string, proxyType string, natType string) []byte { +func (ctx *BrokerContext) RequestOffer(id string, proxyType string, natType string) *ClientOffer { request := new(ProxyPoll) request.id = id request.proxyType = proxyType request.natType = natType - request.offerChannel = make(chan []byte) + request.offerChannel = make(chan *ClientOffer) ctx.proxyPolls <- request // Block until an offer is available, or timeout which sends a nil offer. offer := <-request.offerChannel @@ -165,7 +165,7 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri snowflake.id = id snowflake.clients = 0 snowflake.proxyType = proxyType - snowflake.offerChannel = make(chan []byte) + snowflake.offerChannel = make(chan *ClientOffer) snowflake.answerChannel = make(chan []byte) ctx.snowflakeLock.Lock() if natType == NATRestricted { @@ -213,7 +213,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { ctx.metrics.proxyIdleCount++ ctx.metrics.lock.Unlock() - b, err = messages.EncodePollResponse("", false) + b, err = messages.EncodePollResponse("", false, "") if err != nil { w.WriteHeader(http.StatusInternalServerError) return @@ -222,7 +222,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { w.Write(b) return } - b, err = messages.EncodePollResponse(string(offer), true) + b, err = messages.EncodePollResponse(string(offer.sdp), true, offer.natType) if err != nil { w.WriteHeader(http.StatusInternalServerError) return @@ -232,28 +232,37 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { } } +// Client offer contains an SDP and the NAT type of the client +type ClientOffer struct { + natType string + sdp []byte +} + /* Expects a WebRTC SDP offer in the Request to give to an assigned snowflake proxy, which responds with the SDP answer to be sent in the HTTP response back to the client. */ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { + var err error + startTime := time.Now() - offer, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) + offer := &ClientOffer{} + offer.sdp, err = ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) if nil != err { log.Println("Invalid data.") w.WriteHeader(http.StatusBadRequest) return } - natType := r.Header.Get("Snowflake-NAT-Type") - if natType == "" { - natType = NATUnknown + offer.natType = r.Header.Get("Snowflake-NAT-Type") + if offer.natType == "" { + offer.natType = NATUnknown } // Only hand out known restricted snowflakes to unrestricted clients var snowflakeHeap *SnowflakeHeap - if natType == NATUnrestricted { + if offer.natType == NATUnrestricted { snowflakeHeap = ctx.restrictedSnowflakes } else { snowflakeHeap = ctx.snowflakes diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 91383a1..d03dca7 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -37,7 +37,7 @@ func TestBroker(t *testing.T) { Convey("Broker goroutine matches clients with proxies", func() { p := new(ProxyPoll) p.id = "test" - p.offerChannel = make(chan []byte) + p.offerChannel = make(chan *ClientOffer) go func(ctx *BrokerContext) { ctx.proxyPolls <- p close(ctx.proxyPolls) @@ -45,23 +45,23 @@ func TestBroker(t *testing.T) { ctx.Broker() So(ctx.snowflakes.Len(), ShouldEqual, 1) snowflake := heap.Pop(ctx.snowflakes).(*Snowflake) - snowflake.offerChannel <- []byte("test offer") + snowflake.offerChannel <- &ClientOffer{sdp: []byte("test offer")} offer := <-p.offerChannel So(ctx.idToSnowflake["test"], ShouldNotBeNil) - So(offer, ShouldResemble, []byte("test offer")) + So(offer.sdp, ShouldResemble, []byte("test offer")) So(ctx.snowflakes.Len(), ShouldEqual, 0) }) Convey("Request an offer from the Snowflake Heap", func() { - done := make(chan []byte) + done := make(chan *ClientOffer) go func() { offer := ctx.RequestOffer("test", "", NATUnknown) done <- offer }() request := <-ctx.proxyPolls - request.offerChannel <- []byte("test offer") + request.offerChannel <- &ClientOffer{sdp: []byte("test offer")} offer := <-done - So(offer, ShouldResemble, []byte("test offer")) + So(offer.sdp, ShouldResemble, []byte("test offer")) }) Convey("Responds to client offers...", func() { @@ -85,7 +85,7 @@ func TestBroker(t *testing.T) { done <- true }() offer := <-snowflake.offerChannel - So(offer, ShouldResemble, []byte("test")) + So(offer.sdp, ShouldResemble, []byte("test")) snowflake.answerChannel <- []byte("fake answer") <-done So(w.Body.String(), ShouldEqual, "fake answer") @@ -104,7 +104,7 @@ func TestBroker(t *testing.T) { done <- true }() offer := <-snowflake.offerChannel - So(offer, ShouldResemble, []byte("test")) + So(offer.sdp, ShouldResemble, []byte("test")) <-done So(w.Code, ShouldEqual, http.StatusGatewayTimeout) }) @@ -125,10 +125,10 @@ func TestBroker(t *testing.T) { // Pass a fake client offer to this proxy p := <-ctx.proxyPolls So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") - p.offerChannel <- []byte("fake offer") + p.offerChannel <- &ClientOffer{sdp: []byte("fake offer")} <-done So(w.Code, ShouldEqual, http.StatusOK) - So(w.Body.String(), ShouldEqual, `{"Status":"client match","Offer":"fake offer"}`) + So(w.Body.String(), ShouldEqual, `{"Status":"client match","Offer":"fake offer","NAT":""}`) }) Convey("return empty 200 OK when no client offer is available.", func() { @@ -141,7 +141,7 @@ func TestBroker(t *testing.T) { // nil means timeout p.offerChannel <- nil <-done - So(w.Body.String(), ShouldEqual, `{"Status":"no match","Offer":""}`) + So(w.Body.String(), ShouldEqual, `{"Status":"no match","Offer":"","NAT":""}`) So(w.Code, ShouldEqual, http.StatusOK) }) }) @@ -279,7 +279,7 @@ func TestBroker(t *testing.T) { <-polled So(wP.Code, ShouldEqual, http.StatusOK) - So(wP.Body.String(), ShouldResemble, `{"Status":"client match","Offer":"fake offer"}`) + So(wP.Body.String(), ShouldResemble, `{"Status":"client match","Offer":"fake offer","NAT":"unknown"}`) So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil) // Follow up with the answer request afterwards wA := httptest.NewRecorder() @@ -543,7 +543,7 @@ func TestMetrics(t *testing.T) { done <- true }() offer := <-snowflake.offerChannel - So(offer, ShouldResemble, []byte("test")) + So(offer.sdp, ShouldResemble, []byte("test")) snowflake.answerChannel <- []byte("fake answer") <-done diff --git a/broker/snowflake-heap.go b/broker/snowflake-heap.go index 19a64b2..12fe557 100644 --- a/broker/snowflake-heap.go +++ b/broker/snowflake-heap.go @@ -11,7 +11,7 @@ over the offer and answer channels. type Snowflake struct { id string proxyType string - offerChannel chan []byte + offerChannel chan *ClientOffer answerChannel chan []byte clients int index int diff --git a/common/messages/proxy.go b/common/messages/proxy.go index 923189b..2d9e58d 100644 --- a/common/messages/proxy.go +++ b/common/messages/proxy.go @@ -29,7 +29,8 @@ HTTP 200 OK { type: offer, sdp: [WebRTC SDP] - } + }, + NAT: ["unknown"|"restricted"|"unrestricted"] } 2) If a client is not matched: @@ -120,13 +121,15 @@ func DecodePollRequest(data []byte) (string, string, string, error) { type ProxyPollResponse struct { Status string Offer string + NAT string } -func EncodePollResponse(offer string, success bool) ([]byte, error) { +func EncodePollResponse(offer string, success bool, natType string) ([]byte, error) { if success { return json.Marshal(ProxyPollResponse{ Status: "client match", Offer: offer, + NAT: natType, }) } @@ -135,28 +138,33 @@ func EncodePollResponse(offer string, success bool) ([]byte, error) { }) } -// Decodes a poll response from the broker and returns an offer +// Decodes a poll response from the broker and returns an offer and the client's NAT type // If there is a client match, the returned offer string will be non-empty -func DecodePollResponse(data []byte) (string, error) { +func DecodePollResponse(data []byte) (string, string, error) { var message ProxyPollResponse err := json.Unmarshal(data, &message) if err != nil { - return "", err + return "", "", err } if message.Status == "" { - return "", fmt.Errorf("received invalid data") + return "", "", fmt.Errorf("received invalid data") } if message.Status == "client match" { if message.Offer == "" { - return "", fmt.Errorf("no supplied offer") + return "", "", fmt.Errorf("no supplied offer") } } else { message.Offer = "" } - return message.Offer, nil + natType := message.NAT + if natType == "" { + natType = "unknown" + } + + return message.Offer, natType, nil } type ProxyAnswerRequest struct { diff --git a/common/messages/proxy_test.go b/common/messages/proxy_test.go index 3aa67fb..f4191e1 100644 --- a/common/messages/proxy_test.go +++ b/common/messages/proxy_test.go @@ -109,7 +109,7 @@ func TestDecodeProxyPollResponse(t *testing.T) { }{ { "fake offer", - `{"Status":"client match","Offer":"fake offer"}`, + `{"Status":"client match","Offer":"fake offer","NAT":"unknown"}`, nil, }, { @@ -128,9 +128,9 @@ func TestDecodeProxyPollResponse(t *testing.T) { fmt.Errorf(""), }, } { - offer, err := DecodePollResponse([]byte(test.data)) - So(offer, ShouldResemble, test.offer) + offer, _, err := DecodePollResponse([]byte(test.data)) So(err, ShouldHaveSameTypeAs, test.err) + So(offer, ShouldResemble, test.offer) } }) @@ -138,16 +138,18 @@ func TestDecodeProxyPollResponse(t *testing.T) { func TestEncodeProxyPollResponse(t *testing.T) { Convey("Context", t, func() { - b, err := EncodePollResponse("fake offer", true) + b, err := EncodePollResponse("fake offer", true, "restricted") So(err, ShouldEqual, nil) - offer, err := DecodePollResponse(b) + offer, natType, err := DecodePollResponse(b) So(offer, ShouldEqual, "fake offer") + So(natType, ShouldEqual, "restricted") So(err, ShouldEqual, nil) - b, err = EncodePollResponse("", false) + b, err = EncodePollResponse("", false, "unknown") So(err, ShouldEqual, nil) - offer, err = DecodePollResponse(b) + offer, natType, err = DecodePollResponse(b) So(offer, ShouldEqual, "") + So(natType, ShouldEqual, "unknown") So(err, ShouldEqual, nil) }) } diff --git a/proxy/proxy-go_test.go b/proxy/proxy-go_test.go index 03d7307..168ca25 100644 --- a/proxy/proxy-go_test.go +++ b/proxy/proxy-go_test.go @@ -249,7 +249,7 @@ func TestBrokerInteractions(t *testing.T) { Convey("polls broker correctly", func() { var err error - b, err := messages.EncodePollResponse(sampleOffer, true) + b, err := messages.EncodePollResponse(sampleOffer, true, "unknown") So(err, ShouldEqual, nil) broker.transport = &MockTransport{ http.StatusOK, diff --git a/proxy/snowflake.go b/proxy/snowflake.go index ac67748..a1886c2 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -201,7 +201,7 @@ func (b *Broker) pollOffer(sid string) *webrtc.SessionDescription { log.Printf("error reading broker response: %s", err) } else { - offer, err := messages.DecodePollResponse(body) + offer, _, err := messages.DecodePollResponse(body) if err != nil { log.Printf("error reading broker response: %s", err.Error()) log.Printf("body: %s", body) From 8c875f0ba7775519b13118458d350dcae478af6a Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 9 Jul 2020 09:55:41 -0400 Subject: [PATCH 129/385] Use STUN server compatable with RFC 5780 in proxy --- proxy/snowflake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/snowflake.go b/proxy/snowflake.go index a1886c2..464fbb0 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -29,7 +29,7 @@ import ( const defaultBrokerURL = "https://snowflake-broker.bamsoftware.com/" const defaultRelayURL = "wss://snowflake.bamsoftware.com/" -const defaultSTUNURL = "stun:stun.l.google.com:19302" +const defaultSTUNURL = "stun:stun.stunprotocol.org:3478" const pollInterval = 5 * time.Second const ( NATUnknown = "unknown" From d44fc238150c64a7a5045e756d1e9e2dbe0a3e5a Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 30 Jun 2020 21:47:48 +0200 Subject: [PATCH 130/385] update .gitlab-ci.yml --- .gitlab-ci.yml | 158 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 134 insertions(+), 24 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b7fd956..18902e6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,29 +1,139 @@ -image: golang:1.10-stretch -cache: - paths: - - .gradle/wrapper - - .gradle/caches - -before_script: - # Create symbolic links under $GOPATH, this is needed for local build - - export src=$GOPATH/src - - mkdir -p $src/git.torproject.org/pluggable-transports - - mkdir -p $src/gitlab.com/$CI_PROJECT_NAMESPACE - - ln -s $CI_PROJECT_DIR $src/git.torproject.org/pluggable-transports/snowflake.git - - ln -s $CI_PROJECT_DIR $src/gitlab.com/$CI_PROJECT_PATH - -build: - script: +# Set things up to use the OS-native packages for Go. Anything that +# is downloaded by go during the `go fmt` stage is not coming from the +# Debian/Ubuntu repo. So those would need to be packaged for this to +# make it into Debian and/or Ubuntu. +.debian-native-template: &debian-native-template + variables: + DEBIAN_FRONTEND: noninteractive + GOPATH: /usr/share/gocode + before_script: - apt-get -qy update - - apt-get -qy install libx11-dev - - cd $src/gitlab.com/$CI_PROJECT_PATH/client - - go get ./... - - go build ./... + - apt-get -qy install --no-install-recommends + build-essential + ca-certificates + git + golang + golang-github-cheekybits-genny-dev + golang-github-jtolds-gls-dev + golang-github-klauspost-reedsolomon-dev + golang-github-lucas-clemente-quic-go-dev + golang-github-smartystreets-assertions-dev + golang-github-smartystreets-goconvey-dev + golang-github-tjfoc-gmsm-dev + golang-github-xtaci-kcp-dev + golang-github-xtaci-smux-dev + golang-golang-x-crypto-dev + golang-golang-x-net-dev + golang-golang-x-sys-dev + golang-golang-x-text-dev + golang-golang-x-xerrors-dev + lbzip2 + +# use Go installed as part of the official, Debian-based Docker images +.golang-docker-debian-template: &golang-docker-debian-template + variables: + DEBIAN_FRONTEND: noninteractive + before_script: + - apt-get -qy update + - apt-get -qy install --no-install-recommends + ca-certificates + git + lbzip2 + +.test-template: &test-template + artifacts: + name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}" + paths: + - client/*.aar + - client/*.jar + - client/client + expire_in: 1 day + when: on_success + after_script: + - echo "Download debug artifacts from https://gitlab.com/${CI_PROJECT_PATH}/-/jobs" + script: + - test -z "$(go fmt ./...)" - go vet ./... - go test -v -race ./... -after_script: - # this file changes every time but should not be cached - - rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock - - rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/ + - cd $CI_PROJECT_DIR/client/ + - go get + - go build + + # build for Android if this is the right job + - test "$CI_JOB_NAME" = "android" || exit 0 + - export GRADLE_USER_HOME=$PWD/.gradle + # This build was setup before go.mod was a thing, go back to the old days! + # 920f6791f3ec8e7467c43ee0cefffe63200bed2b broke the gomobile build. + # https://dev.to/maelvls/why-is-go111module-everywhere-and-everything-about-go-modules-24k + - export GO111MODULE=off + - go version + - go env + + - go get golang.org/x/mobile/cmd/gomobile + - go get golang.org/x/mobile/cmd/gobind + - go install golang.org/x/mobile/cmd/gomobile + - go install golang.org/x/mobile/cmd/gobind + - echo y | $ANDROID_HOME/tools/bin/sdkmanager 'ndk-bundle' > /dev/null + - gomobile init + + # Create symbolic links under $GOPATH, this is needed for local build + - export src=$GOPATH/src + - mkdir -p $src/git.torproject.org/pluggable-transports + - mkdir -p $src/github.com/keroserene + - mkdir -p $src/gitlab.com/$CI_PROJECT_NAMESPACE + - ln -s $CI_PROJECT_DIR $src/git.torproject.org/pluggable-transports/snowflake + - ln -s $CI_PROJECT_DIR $src/github.com/keroserene/snowflake + - ln -s $CI_PROJECT_DIR $src/gitlab.com/$CI_PROJECT_PATH + + - git -C $CI_PROJECT_DIR reset --hard + - git -C $CI_PROJECT_DIR clean -fdx + - cd $CI_PROJECT_DIR/client + # gomobile builds a shared library not a CLI executable + - sed -i 's,^package main$,package snowflakeclient,' snowflake.go client_test.go + - gomobile bind -v -target=android git.torproject.org/pluggable-transports/snowflake/client + + +# -- jobs ------------------------------------------------------------ + +android: + image: registry.gitlab.com/fdroid/ci-images-client + variables: + GOPATH: "/go" + cache: + paths: + - .gradle/wrapper + - .gradle/caches + before_script: + - apt-get -qy update + - apt-get -qy install --no-install-recommends + build-essential + gnupg + wget + - cd /usr/local + - export gotarball="go1.13.12.linux-amd64.tar.gz" + - wget -q https://dl.google.com/go/${gotarball} + - wget -q https://dl.google.com/go/${gotarball}.asc + - curl https://dl.google.com/linux/linux_signing_key.pub | gpg --import + - gpg --verify ${gotarball}.asc + - echo "9cacc6653563771b458c13056265aa0c21b8a23ca9408278484e4efde4160618 ${gotarball}" | sha256sum -c + - tar -xzf ${gotarball} + - export PATH="/usr/local/go/bin:$GOPATH/bin:$PATH" # putting this in 'variables:' cause weird runner errors + - cd $CI_PROJECT_DIR + <<: *test-template + +go-1.13: + image: golang:1.13-stretch + <<: *golang-docker-debian-template + <<: *test-template + +go-1.14: + image: golang:1.14-stretch + <<: *golang-docker-debian-template + <<: *test-template + +debian-testing: + image: debian:testing + <<: *debian-native-template + <<: *test-template From c1fa4efe4b6e289758224a5a4c8bcaa3d7067449 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 8 Jul 2020 10:49:41 -0400 Subject: [PATCH 131/385] Refactor android script to be in android job --- .gitlab-ci.yml | 81 +++++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 18902e6..04a58fc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,6 +41,15 @@ git lbzip2 +.go_test: &go-test + - test -z "$(go fmt ./...)" + - go vet ./... + - go test -v -race ./... + + - cd $CI_PROJECT_DIR/client/ + - go get + - go build + .test-template: &test-template artifacts: name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}" @@ -52,17 +61,35 @@ when: on_success after_script: - echo "Download debug artifacts from https://gitlab.com/${CI_PROJECT_PATH}/-/jobs" + +# -- jobs ------------------------------------------------------------ + +android: + image: registry.gitlab.com/fdroid/ci-images-client + variables: + GOPATH: "/go" + cache: + paths: + - .gradle/wrapper + - .gradle/caches + before_script: + - apt-get -qy update + - apt-get -qy install --no-install-recommends + build-essential + gnupg + wget + - cd /usr/local + - export gotarball="go1.13.12.linux-amd64.tar.gz" + - wget -q https://dl.google.com/go/${gotarball} + - wget -q https://dl.google.com/go/${gotarball}.asc + - curl https://dl.google.com/linux/linux_signing_key.pub | gpg --import + - gpg --verify ${gotarball}.asc + - echo "9cacc6653563771b458c13056265aa0c21b8a23ca9408278484e4efde4160618 ${gotarball}" | sha256sum -c + - tar -xzf ${gotarball} + - export PATH="/usr/local/go/bin:$GOPATH/bin:$PATH" # putting this in 'variables:' cause weird runner errors + - cd $CI_PROJECT_DIR script: - - test -z "$(go fmt ./...)" - - go vet ./... - - go test -v -race ./... - - - cd $CI_PROJECT_DIR/client/ - - go get - - go build - - # build for Android if this is the right job - - test "$CI_JOB_NAME" = "android" || exit 0 + - *go-test - export GRADLE_USER_HOME=$PWD/.gradle # This build was setup before go.mod was a thing, go back to the old days! # 920f6791f3ec8e7467c43ee0cefffe63200bed2b broke the gomobile build. @@ -93,47 +120,25 @@ # gomobile builds a shared library not a CLI executable - sed -i 's,^package main$,package snowflakeclient,' snowflake.go client_test.go - gomobile bind -v -target=android git.torproject.org/pluggable-transports/snowflake/client - - -# -- jobs ------------------------------------------------------------ - -android: - image: registry.gitlab.com/fdroid/ci-images-client - variables: - GOPATH: "/go" - cache: - paths: - - .gradle/wrapper - - .gradle/caches - before_script: - - apt-get -qy update - - apt-get -qy install --no-install-recommends - build-essential - gnupg - wget - - cd /usr/local - - export gotarball="go1.13.12.linux-amd64.tar.gz" - - wget -q https://dl.google.com/go/${gotarball} - - wget -q https://dl.google.com/go/${gotarball}.asc - - curl https://dl.google.com/linux/linux_signing_key.pub | gpg --import - - gpg --verify ${gotarball}.asc - - echo "9cacc6653563771b458c13056265aa0c21b8a23ca9408278484e4efde4160618 ${gotarball}" | sha256sum -c - - tar -xzf ${gotarball} - - export PATH="/usr/local/go/bin:$GOPATH/bin:$PATH" # putting this in 'variables:' cause weird runner errors - - cd $CI_PROJECT_DIR <<: *test-template go-1.13: image: golang:1.13-stretch <<: *golang-docker-debian-template <<: *test-template + script: + - *go-test go-1.14: image: golang:1.14-stretch <<: *golang-docker-debian-template <<: *test-template + script: + - *go-test debian-testing: image: debian:testing <<: *debian-native-template <<: *test-template + script: + - *go-test From eaac9f5b6be90d2ba1d63f630c727090a4a4701e Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 8 Jul 2020 16:13:05 -0400 Subject: [PATCH 132/385] Use go modules to build android library This commit removes the symlinks and turns go modules back on to run gomobile bind locally on the project. --- .gitlab-ci.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 04a58fc..2ac0aa0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -91,10 +91,6 @@ android: script: - *go-test - export GRADLE_USER_HOME=$PWD/.gradle - # This build was setup before go.mod was a thing, go back to the old days! - # 920f6791f3ec8e7467c43ee0cefffe63200bed2b broke the gomobile build. - # https://dev.to/maelvls/why-is-go111module-everywhere-and-everything-about-go-modules-24k - - export GO111MODULE=off - go version - go env @@ -105,21 +101,12 @@ android: - echo y | $ANDROID_HOME/tools/bin/sdkmanager 'ndk-bundle' > /dev/null - gomobile init - # Create symbolic links under $GOPATH, this is needed for local build - - export src=$GOPATH/src - - mkdir -p $src/git.torproject.org/pluggable-transports - - mkdir -p $src/github.com/keroserene - - mkdir -p $src/gitlab.com/$CI_PROJECT_NAMESPACE - - ln -s $CI_PROJECT_DIR $src/git.torproject.org/pluggable-transports/snowflake - - ln -s $CI_PROJECT_DIR $src/github.com/keroserene/snowflake - - ln -s $CI_PROJECT_DIR $src/gitlab.com/$CI_PROJECT_PATH - - git -C $CI_PROJECT_DIR reset --hard - git -C $CI_PROJECT_DIR clean -fdx - cd $CI_PROJECT_DIR/client # gomobile builds a shared library not a CLI executable - sed -i 's,^package main$,package snowflakeclient,' snowflake.go client_test.go - - gomobile bind -v -target=android git.torproject.org/pluggable-transports/snowflake/client + - gomobile bind -v -target=android . <<: *test-template go-1.13: From 92520f681d77127fb7dd6e578080f1351ae885a8 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 23 Jul 2020 09:28:21 -0400 Subject: [PATCH 133/385] Choose a random subset from given STUN servers Only chooses a subset as long as we have over 2 STUN servers to choose from. --- client/client_test.go | 4 ++-- client/snowflake.go | 9 +++++++++ client/torrc | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/client/client_test.go b/client/client_test.go index aeaf979..84e9cc1 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -49,8 +49,8 @@ func TestICEServerParser(t *testing.T) { So(len(servers), ShouldEqual, test.length) - for i, server := range servers { - So(server.URLs, ShouldResemble, test.urls[i]) + for _, server := range servers { + So(test.urls, ShouldContain, server.URLs) } } diff --git a/client/snowflake.go b/client/snowflake.go index 02bbf1e..c05431b 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "log" + "math/rand" "net" "os" "os/signal" @@ -77,6 +78,7 @@ func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { } // s is a comma-separated list of ICE server URLs. +// chooses a random subset of servers from inputs func parseIceServers(s string) []webrtc.ICEServer { var servers []webrtc.ICEServer s = strings.TrimSpace(s) @@ -90,6 +92,13 @@ func parseIceServers(s string) []webrtc.ICEServer { URLs: []string{url}, }) } + rand.Seed(time.Now().Unix()) + rand.Shuffle(len(servers), func(i, j int) { + servers[i], servers[j] = servers[j], servers[i] + }) + if len(servers) > 2 { + servers = servers[:len(servers)/2] + } return servers } diff --git a/client/torrc b/client/torrc index 9e3946e..813d22d 100644 --- a/client/torrc +++ b/client/torrc @@ -4,7 +4,7 @@ DataDirectory datadir ClientTransportPlugin snowflake exec ./client \ -url https://snowflake-broker.azureedge.net/ \ -front ajax.aspnetcdn.com \ --ice stun:stun.l.google.com:19302 \ +-ice stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 \ -max 3 Bridge snowflake 192.0.2.3:1 From 82031289a3362ff3fa199628276d76e840491431 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 24 Jul 2020 11:38:58 -0400 Subject: [PATCH 134/385] Refactor subsetting of ice servers into main This moves the subsetting of ice servers out of the parseIceServers function and into main. --- client/snowflake.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/snowflake.go b/client/snowflake.go index c05431b..55bc48e 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -78,7 +78,6 @@ func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { } // s is a comma-separated list of ICE server URLs. -// chooses a random subset of servers from inputs func parseIceServers(s string) []webrtc.ICEServer { var servers []webrtc.ICEServer s = strings.TrimSpace(s) @@ -92,13 +91,6 @@ func parseIceServers(s string) []webrtc.ICEServer { URLs: []string{url}, }) } - rand.Seed(time.Now().Unix()) - rand.Shuffle(len(servers), func(i, j int) { - servers[i], servers[j] = servers[j], servers[i] - }) - if len(servers) > 2 { - servers = servers[:len(servers)/2] - } return servers } @@ -153,6 +145,14 @@ func main() { log.Println("\n\n\n --- Starting Snowflake Client ---") iceServers := parseIceServers(*iceServersCommas) + // chooses a random subset of servers from inputs + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(iceServers), func(i, j int) { + iceServers[i], iceServers[j] = iceServers[j], iceServers[i] + }) + if len(iceServers) > 2 { + iceServers = iceServers[:(len(iceServers)+1)/2] + } log.Printf("Using ICE servers:") for _, server := range iceServers { log.Printf("url: %v", strings.Join(server.URLs, " ")) From d5ae7562ac65f07d1c2e4137534217644f391612 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 30 Jul 2020 16:34:34 -0400 Subject: [PATCH 135/385] Add response header timeouts to broker transports The client and proxy use the net/http default transport to make round trip connecitons to the broker. These by default don't time out and can wait indefinitely for the broker to respond if the broker hangs and doesn't terminate the connection. --- client/lib/rendezvous.go | 2 ++ proxy/snowflake.go | 1 + 2 files changed, 3 insertions(+) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 2702d4e..37ade35 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -17,6 +17,7 @@ import ( "net/http" "net/url" "sync" + "time" "git.torproject.org/pluggable-transports/snowflake.git/common/nat" "git.torproject.org/pluggable-transports/snowflake.git/common/util" @@ -48,6 +49,7 @@ type BrokerChannel struct { func CreateBrokerTransport() http.RoundTripper { transport := http.DefaultTransport.(*http.Transport) transport.Proxy = nil + transport.ResponseHeaderTimeout = 15 * time.Second return transport } diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 464fbb0..b880b36 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -481,6 +481,7 @@ func main() { } broker.transport = http.DefaultTransport.(*http.Transport) + broker.transport.(*http.Transport).ResponseHeaderTimeout = 15 * time.Second config = webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { From 3c3317503eb8e83bbf5bebff411bbd722e60ee2f Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 19 Aug 2020 11:37:43 -0400 Subject: [PATCH 136/385] Update broker stats to include info on NAT types As we now partition proxies by NAT type, our stats are more useful if they capture how many proxies of each type we have, and information on whether we have enough proxies of the right NAT type for our clients. This change adds proxy counts by NAT type and binned counts of denied clients by NAT type. --- broker/broker.go | 23 ++++++++- broker/metrics.go | 53 +++++++++++++++----- broker/snowflake-broker_test.go | 88 ++++++++++++++++++++++++++++++--- broker/snowflake-heap.go | 1 + doc/broker-spec.txt | 34 ++++++++++++- 5 files changed, 177 insertions(+), 22 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index 99f6c69..983f95d 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -165,6 +165,7 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri snowflake.id = id snowflake.clients = 0 snowflake.proxyType = proxyType + snowflake.natType = natType snowflake.offerChannel = make(chan *ClientOffer) snowflake.answerChannel = make(chan []byte) ctx.snowflakeLock.Lock() @@ -201,7 +202,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { log.Println("Error processing proxy IP: ", err.Error()) } else { ctx.metrics.lock.Lock() - ctx.metrics.UpdateCountryStats(remoteIP, proxyType) + ctx.metrics.UpdateCountryStats(remoteIP, proxyType, natType) ctx.metrics.lock.Unlock() } @@ -275,6 +276,11 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { if numSnowflakes <= 0 { ctx.metrics.lock.Lock() ctx.metrics.clientDeniedCount++ + if offer.natType == NATUnrestricted { + ctx.metrics.clientUnrestrictedDeniedCount++ + } else { + ctx.metrics.clientRestrictedDeniedCount++ + } ctx.metrics.lock.Unlock() w.WriteHeader(http.StatusServiceUnavailable) return @@ -357,6 +363,7 @@ func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { func debugHandler(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { var webexts, browsers, standalones, unknowns int + var natRestricted, natUnrestricted, natUnknown int ctx.snowflakeLock.Lock() s := fmt.Sprintf("current snowflakes available: %d\n", len(ctx.idToSnowflake)) for _, snowflake := range ctx.idToSnowflake { @@ -370,12 +377,26 @@ func debugHandler(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { unknowns++ } + switch snowflake.natType { + case NATRestricted: + natRestricted++ + case NATUnrestricted: + natUnrestricted++ + default: + natUnknown++ + } + } ctx.snowflakeLock.Unlock() s += fmt.Sprintf("\tstandalone proxies: %d", standalones) s += fmt.Sprintf("\n\tbrowser proxies: %d", browsers) s += fmt.Sprintf("\n\twebext proxies: %d", webexts) s += fmt.Sprintf("\n\tunknown proxies: %d", unknowns) + + s += fmt.Sprintf("\nNAT Types available:") + s += fmt.Sprintf("\n\trestricted: %d", natRestricted) + s += fmt.Sprintf("\n\tunrestricted: %d", natUnrestricted) + s += fmt.Sprintf("\n\tunknown: %d", natUnknown) if _, err := w.Write([]byte(s)); err != nil { log.Printf("writing proxy information returned error: %v ", err) } diff --git a/broker/metrics.go b/broker/metrics.go index ea4d220..d1beae2 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -25,7 +25,12 @@ type CountryStats struct { badge map[string]bool webext map[string]bool unknown map[string]bool - counts map[string]int + + natRestricted map[string]bool + natUnrestricted map[string]bool + natUnknown map[string]bool + + counts map[string]int } // Implements Observable @@ -34,11 +39,13 @@ type Metrics struct { tablev4 *GeoIPv4Table tablev6 *GeoIPv6Table - countryStats CountryStats - clientRoundtripEstimate time.Duration - proxyIdleCount uint - clientDeniedCount uint - clientProxyMatchCount uint + countryStats CountryStats + clientRoundtripEstimate time.Duration + proxyIdleCount uint + clientDeniedCount uint + clientRestrictedDeniedCount uint + clientUnrestrictedDeniedCount uint + clientProxyMatchCount uint //synchronization for access to snowflake metrics lock sync.Mutex @@ -58,7 +65,7 @@ func (s CountryStats) Display() string { return output } -func (m *Metrics) UpdateCountryStats(addr string, proxyType string) { +func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType string) { var country string var ok bool @@ -111,6 +118,15 @@ func (m *Metrics) UpdateCountryStats(addr string, proxyType string) { m.countryStats.unknown[addr] = true } + switch natType { + case NATRestricted: + m.countryStats.natRestricted[addr] = true + case NATUnrestricted: + m.countryStats.natUnrestricted[addr] = true + default: + m.countryStats.natUnknown[addr] = true + } + } func (m *Metrics) LoadGeoipDatabases(geoipDB string, geoip6DB string) error { @@ -139,11 +155,14 @@ func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) { m := new(Metrics) m.countryStats = CountryStats{ - counts: make(map[string]int), - standalone: make(map[string]bool), - badge: make(map[string]bool), - webext: make(map[string]bool), - unknown: make(map[string]bool), + counts: make(map[string]int), + standalone: make(map[string]bool), + badge: make(map[string]bool), + webext: make(map[string]bool), + unknown: make(map[string]bool), + natRestricted: make(map[string]bool), + natUnrestricted: make(map[string]bool), + natUnknown: make(map[string]bool), } m.logger = metricsLogger @@ -174,7 +193,12 @@ func (m *Metrics) printMetrics() { m.logger.Println("snowflake-ips-webext", len(m.countryStats.webext)) m.logger.Println("snowflake-idle-count", binCount(m.proxyIdleCount)) m.logger.Println("client-denied-count", binCount(m.clientDeniedCount)) + m.logger.Println("client-restricted-denied-count", binCount(m.clientRestrictedDeniedCount)) + m.logger.Println("client-unrestricted-denied-count", binCount(m.clientUnrestrictedDeniedCount)) m.logger.Println("client-snowflake-match-count", binCount(m.clientProxyMatchCount)) + m.logger.Println("snowflake-ips-nat-restricted", len(m.countryStats.natRestricted)) + m.logger.Println("snowflake-ips-nat-unrestricted", len(m.countryStats.natUnrestricted)) + m.logger.Println("snowflake-ips-nat-unknown", len(m.countryStats.natUnknown)) m.lock.Unlock() } @@ -182,12 +206,17 @@ func (m *Metrics) printMetrics() { func (m *Metrics) zeroMetrics() { m.proxyIdleCount = 0 m.clientDeniedCount = 0 + m.clientRestrictedDeniedCount = 0 + m.clientUnrestrictedDeniedCount = 0 m.clientProxyMatchCount = 0 m.countryStats.counts = make(map[string]int) m.countryStats.standalone = make(map[string]bool) m.countryStats.badge = make(map[string]bool) m.countryStats.webext = make(map[string]bool) m.countryStats.unknown = make(map[string]bool) + m.countryStats.natRestricted = make(map[string]bool) + m.countryStats.natUnrestricted = make(map[string]bool) + m.countryStats.natUnknown = make(map[string]bool) } // Rounds up a count to the nearest multiple of 8. diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index d03dca7..4e8e5f0 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -437,7 +437,7 @@ func TestGeoip(t *testing.T) { if err := ctx.metrics.LoadGeoipDatabases("invalid_filename", "invalid_filename6"); err != nil { log.Printf("loading geo ip databases returned error: %v", err) } - ctx.metrics.UpdateCountryStats("127.0.0.1", "") + ctx.metrics.UpdateCountryStats("127.0.0.1", "", NATUnknown) So(ctx.metrics.tablev4, ShouldEqual, nil) }) @@ -507,7 +507,7 @@ func TestMetrics(t *testing.T) { p.offerChannel <- nil <-done ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips CA=4\nsnowflake-ips-total 4\nsnowflake-ips-standalone 1\nsnowflake-ips-badge 1\nsnowflake-ips-webext 1\nsnowflake-idle-count 8\nclient-denied-count 0\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips CA=4\nsnowflake-ips-total 4\nsnowflake-ips-standalone 1\nsnowflake-ips-badge 1\nsnowflake-ips-webext 1\nsnowflake-idle-count 8\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 1\n") }) @@ -521,13 +521,13 @@ func TestMetrics(t *testing.T) { clientOffers(ctx, w, r) ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 8\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0") // Test reset buf.Reset() ctx.metrics.zeroMetrics() ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 0\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldContainSubstring, "snowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0\nsnowflake-ips-nat-restricted 0\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 0\n") }) //Test addition of client matches Convey("for client-proxy match", func() { @@ -548,7 +548,7 @@ func TestMetrics(t *testing.T) { <-done ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 0\nclient-snowflake-match-count 8\n") + So(buf.String(), ShouldContainSubstring, "client-denied-count 0\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 8") }) //Test rounding boundary Convey("binning boundary", func() { @@ -567,12 +567,12 @@ func TestMetrics(t *testing.T) { clientOffers(ctx, w, r) ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 8\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\n") clientOffers(ctx, w, r) buf.Reset() ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips \nsnowflake-ips-total 0\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 0\nclient-denied-count 16\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldContainSubstring, "client-denied-count 16\nclient-restricted-denied-count 16\nclient-unrestricted-denied-count 0\n") }) //Test unique ip @@ -605,7 +605,79 @@ func TestMetrics(t *testing.T) { <-done ctx.metrics.printMetrics() - So(buf.String(), ShouldResemble, "snowflake-stats-end "+time.Now().UTC().Format("2006-01-02 15:04:05")+" (86400 s)\nsnowflake-ips CA=1\nsnowflake-ips-total 1\nsnowflake-ips-standalone 0\nsnowflake-ips-badge 0\nsnowflake-ips-webext 0\nsnowflake-idle-count 8\nclient-denied-count 0\nclient-snowflake-match-count 0\n") + So(buf.String(), ShouldContainSubstring, "snowflake-ips CA=1\nsnowflake-ips-total 1") + }) + //Test NAT types + Convey("proxy counts by NAT type", func() { + w := httptest.NewRecorder() + data := bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"unknown","NAT":"restricted"}`)) + r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) + r.RemoteAddr = "129.97.208.23:8888" //CA geoip + So(err, ShouldBeNil) + go func(ctx *BrokerContext) { + proxyPolls(ctx, w, r) + done <- true + }(ctx) + p := <-ctx.proxyPolls //manually unblock poll + p.offerChannel <- nil + <-done + + ctx.metrics.printMetrics() + So(buf.String(), ShouldContainSubstring, "snowflake-ips-nat-restricted 1\nsnowflake-ips-nat-unrestricted 0\nsnowflake-ips-nat-unknown 0") + + data = bytes.NewReader([]byte(`{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"unknown","NAT":"unrestricted"}`)) + r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) + if err != nil { + log.Printf("unable to get NewRequest with error: %v", err) + } + r.RemoteAddr = "129.97.208.24:8888" //CA geoip + go func(ctx *BrokerContext) { + proxyPolls(ctx, w, r) + done <- true + }(ctx) + p = <-ctx.proxyPolls //manually unblock poll + p.offerChannel <- nil + <-done + + ctx.metrics.printMetrics() + So(buf.String(), ShouldContainSubstring, "snowflake-ips-nat-restricted 1\nsnowflake-ips-nat-unrestricted 1\nsnowflake-ips-nat-unknown 0") + }) + //Test client failures by NAT type + Convey("client failures by NAT type", func() { + w := httptest.NewRecorder() + data := bytes.NewReader([]byte("test")) + r, err := http.NewRequest("POST", "snowflake.broker/client", data) + r.Header.Set("Snowflake-NAT-TYPE", "restricted") + So(err, ShouldBeNil) + + clientOffers(ctx, w, r) + + ctx.metrics.printMetrics() + So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0") + + buf.Reset() + ctx.metrics.zeroMetrics() + + r, err = http.NewRequest("POST", "snowflake.broker/client", data) + r.Header.Set("Snowflake-NAT-TYPE", "unrestricted") + So(err, ShouldBeNil) + + clientOffers(ctx, w, r) + + ctx.metrics.printMetrics() + So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 8\nclient-snowflake-match-count 0") + + buf.Reset() + ctx.metrics.zeroMetrics() + + r, err = http.NewRequest("POST", "snowflake.broker/client", data) + r.Header.Set("Snowflake-NAT-TYPE", "unknown") + So(err, ShouldBeNil) + + clientOffers(ctx, w, r) + + ctx.metrics.printMetrics() + So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0") }) }) } diff --git a/broker/snowflake-heap.go b/broker/snowflake-heap.go index 12fe557..16dd264 100644 --- a/broker/snowflake-heap.go +++ b/broker/snowflake-heap.go @@ -11,6 +11,7 @@ over the offer and answer channels. type Snowflake struct { id string proxyType string + natType string offerChannel chan *ClientOffer answerChannel chan []byte clients int diff --git a/doc/broker-spec.txt b/doc/broker-spec.txt index c3177e0..9e4b8ae 100644 --- a/doc/broker-spec.txt +++ b/doc/broker-spec.txt @@ -8,7 +8,7 @@ The Snowflake broker is used to hand out Snowflake proxies to clients using the This document specifies how the Snowflake broker interacts with other parts of the Tor ecosystem, starting with the metrics CollecTor module and to be expanded upon later. -1. Metrics Reporting (version 1.0) +1. Metrics Reporting (version 1.1) Metrics data from the Snowflake broker can be retrieved by sending an HTTP GET request to https://[Snowflake broker URL]/metrics and consists of the following items: @@ -62,12 +62,44 @@ Metrics data from the Snowflake broker can be retrieved by sending an HTTP GET r from the broker but no proxies were available, rounded up to the nearest multiple of 8. + "client-restricted-denied-count" NUM NL + [At most once.] + + A count of the number of times a client with a restricted or + unknown NAT type has requested a proxy from the broker but no + proxies were available, rounded up to the nearest multiple of 8. + + "client-unrestricted-denied-count" NUM NL + [At most once.] + + A count of the number of times a client with an unrestricted NAT + type has requested a proxy from the broker but no proxies were + available, rounded up to the nearest multiple of 8. + "client-snowflake-match-count" NUM NL [At most once.] A count of the number of times a client successfully received a proxy from the broker, rounded up to the nearest multiple of 8. + "snowflake-ips-nat-restricted" NUM NL + [At most once.] + + A count of the total number of unique IP addresses of snowflake + proxies that have a restricted NAT type. + + "snowflake-ips-nat-unrestricted" NUM NL + [At most once.] + + A count of the total number of unique IP addresses of snowflake + proxies that have an unrestricted NAT type. + + "snowflake-ips-nat-unknown" NUM NL + [At most once.] + + A count of the total number of unique IP addresses of snowflake + proxies that have an unknown NAT type. + 2. Broker messaging specification and endpoints The broker facilitates the connection of snowflake clients and snowflake proxies From 1364d7d45bbec9de605a266a84ea60cdfa6676db Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 11 Aug 2020 13:22:16 -0400 Subject: [PATCH 137/385] Move snowflake ConnectLoop inside SOCKS Handler Bug #21314: maintains a separate snowflake connect loop per SOCKS connection. This way, if Tor decides to stop using Snowflake, Snowflake will stop using the client's network. --- client/lib/lib_test.go | 6 +++--- client/lib/snowflake.go | 34 +++++++++++++++++++++++++++++++++- client/snowflake.go | 38 +++++--------------------------------- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index ebcf284..a93943f 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -157,11 +157,11 @@ func TestSnowflakeClient(t *testing.T) { SkipConvey("Handler Grants correctly", func() { socks := &FakeSocksConn{} - snowflakes := &FakePeers{} + broker := &BrokerChannel{Host: "test"} + d := NewWebRTCDialer(broker, nil) So(socks.rejected, ShouldEqual, false) - snowflakes.toRelease = nil - Handler(socks, snowflakes) + Handler(socks, d) So(socks.rejected, ShouldEqual, true) }) }) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index b355c3e..d08a7bc 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -142,7 +142,17 @@ var sessionManager = sessionManager_{} // Given an accepted SOCKS connection, establish a WebRTC connection to the // remote peer and exchange traffic. -func Handler(socks net.Conn, snowflakes SnowflakeCollector) error { +func Handler(socks net.Conn, tongue Tongue) error { + // Prepare to collect remote WebRTC peers. + snowflakes := NewPeers(1) + snowflakes.Tongue = tongue + + // Use a real logger to periodically output how much traffic is happening. + snowflakes.BytesLogger = NewBytesSyncLogger() + + log.Printf("---- Handler: begin collecting snowflakes ---") + go connectLoop(snowflakes) + // Return the global smux.Session. sess, err := sessionManager.Get(snowflakes) if err != nil { @@ -160,9 +170,31 @@ func Handler(socks net.Conn, snowflakes SnowflakeCollector) error { log.Printf("---- Handler: begin stream %v ---", stream.ID()) copyLoop(socks, stream) log.Printf("---- Handler: closed stream %v ---", stream.ID()) + snowflakes.End() + log.Printf("---- Handler: end collecting snowflakes ---") return nil } +// Maintain |SnowflakeCapacity| number of available WebRTC connections, to +// transfer to the Tor SOCKS handler when needed. +func connectLoop(snowflakes SnowflakeCollector) { + for { + // Check if ending is necessary. + _, err := snowflakes.Collect() + if err != nil { + log.Printf("WebRTC: %v Retrying in %v...", + err, ReconnectTimeout) + } + select { + case <-time.After(ReconnectTimeout): + continue + case <-snowflakes.Melted(): + log.Println("ConnectLoop: stopped.") + return + } + } +} + // Exchanges bytes between two ReadWriters. // (In this case, between a SOCKS connection and smux stream.) func copyLoop(socks, stream io.ReadWriter) { diff --git a/client/snowflake.go b/client/snowflake.go index 55bc48e..a7f5208 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -26,28 +26,8 @@ const ( DefaultSnowflakeCapacity = 1 ) -// Maintain |SnowflakeCapacity| number of available WebRTC connections, to -// transfer to the Tor SOCKS handler when needed. -func ConnectLoop(snowflakes sf.SnowflakeCollector) { - for { - // Check if ending is necessary. - _, err := snowflakes.Collect() - if err != nil { - log.Printf("WebRTC: %v Retrying in %v...", - err, sf.ReconnectTimeout) - } - select { - case <-time.After(sf.ReconnectTimeout): - continue - case <-snowflakes.Melted(): - log.Println("ConnectLoop: stopped.") - return - } - } -} - // Accept local SOCKS connections and pass them to the handler. -func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { +func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue) { defer ln.Close() for { conn, err := ln.AcceptSocks() @@ -68,7 +48,7 @@ func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) { return } - err = sf.Handler(conn, snowflakes) + err = sf.Handler(conn, tongue) if err != nil { log.Printf("handler error: %s", err) return @@ -158,9 +138,6 @@ func main() { log.Printf("url: %v", strings.Join(server.URLs, " ")) } - // Prepare to collect remote WebRTC peers. - snowflakes := sf.NewPeers(*max) - // Use potentially domain-fronting broker to rendezvous. broker, err := sf.NewBrokerChannel( *brokerURL, *frontDomain, sf.CreateBrokerTransport(), @@ -170,12 +147,8 @@ func main() { } go updateNATType(iceServers, broker) - snowflakes.Tongue = sf.NewWebRTCDialer(broker, iceServers) - - // Use a real logger to periodically output how much traffic is happening. - snowflakes.BytesLogger = sf.NewBytesSyncLogger() - - go ConnectLoop(snowflakes) + // Create a new WebRTCDialer to use as the |Tongue| to catch snowflakes + dialer := sf.NewWebRTCDialer(broker, iceServers) // Begin goptlib client process. ptInfo, err := pt.ClientSetup(nil) @@ -197,7 +170,7 @@ func main() { break } log.Printf("Started SOCKS listener at %v.", ln.Addr()) - go socksAcceptLoop(ln, snowflakes) + go socksAcceptLoop(ln, dialer) pt.Cmethod(methodName, ln.Version(), ln.Addr()) listeners = append(listeners, ln) default: @@ -228,7 +201,6 @@ func main() { for _, ln := range listeners { ln.Close() } - snowflakes.End() log.Println("snowflake is done.") } From cc55481faf7bb886ef3cae99110800189abb0992 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 11 Aug 2020 13:57:51 -0400 Subject: [PATCH 138/385] Set max number of snowflakes in the Tongue --- client/lib/interfaces.go | 3 +++ client/lib/lib_test.go | 37 ++++++++++++++++++++----------------- client/lib/peers.go | 26 +++++++++++++++----------- client/lib/rendezvous.go | 10 +++++++++- client/lib/snowflake.go | 6 ++++-- client/snowflake.go | 2 +- 6 files changed, 52 insertions(+), 32 deletions(-) diff --git a/client/lib/interfaces.go b/client/lib/interfaces.go index 71426d6..5378f4a 100644 --- a/client/lib/interfaces.go +++ b/client/lib/interfaces.go @@ -7,6 +7,9 @@ import ( // Interface for catching Snowflakes. (aka the remote dialer) type Tongue interface { Catch() (*WebRTCPeer, error) + + // Get the maximum number of snowflakes + GetMax() int } // Interface for collecting some number of Snowflakes, for passing along diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index a93943f..5537a52 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -27,13 +27,19 @@ func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { return r, nil } -type FakeDialer struct{} +type FakeDialer struct { + max int +} func (w FakeDialer) Catch() (*WebRTCPeer, error) { fmt.Println("Caught a dummy snowflake.") return &WebRTCPeer{}, nil } +func (w FakeDialer) GetMax() int { + return w.max +} + type FakeSocksConn struct { net.Conn rejected bool @@ -55,19 +61,19 @@ func TestSnowflakeClient(t *testing.T) { Convey("Peers", t, func() { Convey("Can construct", func() { - p := NewPeers(1) - So(p.capacity, ShouldEqual, 1) + d := &FakeDialer{max: 1} + p, _ := NewPeers(d) + So(p.Tongue.GetMax(), ShouldEqual, 1) So(p.snowflakeChan, ShouldNotBeNil) So(cap(p.snowflakeChan), ShouldEqual, 1) }) Convey("Collecting a Snowflake requires a Tongue.", func() { - p := NewPeers(1) - _, err := p.Collect() + p, err := NewPeers(nil) So(err, ShouldNotBeNil) - So(p.Count(), ShouldEqual, 0) // Set the dialer so that collection is possible. - p.Tongue = FakeDialer{} + d := &FakeDialer{max: 1} + p, err = NewPeers(d) _, err = p.Collect() So(err, ShouldBeNil) So(p.Count(), ShouldEqual, 1) @@ -77,8 +83,7 @@ func TestSnowflakeClient(t *testing.T) { Convey("Collection continues until capacity.", func() { c := 5 - p := NewPeers(c) - p.Tongue = FakeDialer{} + p, _ := NewPeers(FakeDialer{max: c}) // Fill up to capacity. for i := 0; i < c; i++ { fmt.Println("Adding snowflake ", i) @@ -104,8 +109,7 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("Count correctly purges peers marked for deletion.", func() { - p := NewPeers(4) - p.Tongue = FakeDialer{} + p, _ := NewPeers(FakeDialer{max: 5}) p.Collect() p.Collect() p.Collect() @@ -121,7 +125,7 @@ func TestSnowflakeClient(t *testing.T) { Convey("End Closes all peers.", func() { cnt := 5 - p := NewPeers(cnt) + p, _ := NewPeers(FakeDialer{max: cnt}) for i := 0; i < cnt; i++ { p.activePeers.PushBack(&WebRTCPeer{}) } @@ -132,8 +136,7 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("Pop skips over closed peers.", func() { - p := NewPeers(4) - p.Tongue = FakeDialer{} + p, _ := NewPeers(FakeDialer{max: 4}) wc1, _ := p.Collect() wc2, _ := p.Collect() wc3, _ := p.Collect() @@ -158,7 +161,7 @@ func TestSnowflakeClient(t *testing.T) { SkipConvey("Handler Grants correctly", func() { socks := &FakeSocksConn{} broker := &BrokerChannel{Host: "test"} - d := NewWebRTCDialer(broker, nil) + d := NewWebRTCDialer(broker, nil, 1) So(socks.rejected, ShouldEqual, false) Handler(socks, d) @@ -169,14 +172,14 @@ func TestSnowflakeClient(t *testing.T) { Convey("Dialers", t, func() { Convey("Can construct WebRTCDialer.", func() { broker := &BrokerChannel{Host: "test"} - d := NewWebRTCDialer(broker, nil) + d := NewWebRTCDialer(broker, nil, 1) So(d, ShouldNotBeNil) So(d.BrokerChannel, ShouldNotBeNil) So(d.BrokerChannel.Host, ShouldEqual, "test") }) SkipConvey("WebRTCDialer can Catch a snowflake.", func() { broker := &BrokerChannel{Host: "test"} - d := NewWebRTCDialer(broker, nil) + d := NewWebRTCDialer(broker, nil, 1) conn, err := d.Catch() So(conn, ShouldBeNil) So(err, ShouldNotBeNil) diff --git a/client/lib/peers.go b/client/lib/peers.go index f766a66..d864fc8 100644 --- a/client/lib/peers.go +++ b/client/lib/peers.go @@ -24,33 +24,37 @@ type Peers struct { snowflakeChan chan *WebRTCPeer activePeers *list.List - capacity int melt chan struct{} } // Construct a fresh container of remote peers. -func NewPeers(max int) *Peers { - p := &Peers{capacity: max} +func NewPeers(tongue Tongue) (*Peers, error) { + p := &Peers{} // Use buffered go channel to pass snowflakes onwards to the SOCKS handler. - p.snowflakeChan = make(chan *WebRTCPeer, max) + if tongue == nil { + return nil, errors.New("missing Tongue to catch Snowflakes with") + } + p.snowflakeChan = make(chan *WebRTCPeer, tongue.GetMax()) p.activePeers = list.New() p.melt = make(chan struct{}) - return p + p.Tongue = tongue + return p, nil } // As part of |SnowflakeCollector| interface. func (p *Peers) Collect() (*WebRTCPeer, error) { - cnt := p.Count() - s := fmt.Sprintf("Currently at [%d/%d]", cnt, p.capacity) - if cnt >= p.capacity { - return nil, fmt.Errorf("At capacity [%d/%d]", cnt, p.capacity) - } - log.Println("WebRTC: Collecting a new Snowflake.", s) // Engage the Snowflake Catching interface, which must be available. if nil == p.Tongue { return nil, errors.New("missing Tongue to catch Snowflakes with") } + cnt := p.Count() + capacity := p.Tongue.GetMax() + s := fmt.Sprintf("Currently at [%d/%d]", cnt, capacity) + if cnt >= capacity { + return nil, fmt.Errorf("At capacity [%d/%d]", cnt, capacity) + } + log.Println("WebRTC: Collecting a new Snowflake.", s) // BUG: some broker conflict here. connection, err := p.Tongue.Catch() if nil != err { diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 37ade35..10853a5 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -155,15 +155,18 @@ func (bc *BrokerChannel) SetNATType(NATType string) { type WebRTCDialer struct { *BrokerChannel webrtcConfig *webrtc.Configuration + max int } -func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer) *WebRTCDialer { +func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int) *WebRTCDialer { config := webrtc.Configuration{ ICEServers: iceServers, } + return &WebRTCDialer{ BrokerChannel: broker, webrtcConfig: &config, + max: max, } } @@ -173,3 +176,8 @@ func (w WebRTCDialer) Catch() (*WebRTCPeer, error) { // TODO: [#25596] Consider TURN servers here too. return NewWebRTCPeer(w.webrtcConfig, w.BrokerChannel) } + +// Returns the maximum number of snowflakes to collect +func (w WebRTCDialer) GetMax() int { + return w.max +} diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index d08a7bc..0ba5667 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -144,8 +144,10 @@ var sessionManager = sessionManager_{} // remote peer and exchange traffic. func Handler(socks net.Conn, tongue Tongue) error { // Prepare to collect remote WebRTC peers. - snowflakes := NewPeers(1) - snowflakes.Tongue = tongue + snowflakes, err := NewPeers(tongue) + if err != nil { + return err + } // Use a real logger to periodically output how much traffic is happening. snowflakes.BytesLogger = NewBytesSyncLogger() diff --git a/client/snowflake.go b/client/snowflake.go index a7f5208..a1b97fa 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -148,7 +148,7 @@ func main() { go updateNATType(iceServers, broker) // Create a new WebRTCDialer to use as the |Tongue| to catch snowflakes - dialer := sf.NewWebRTCDialer(broker, iceServers) + dialer := sf.NewWebRTCDialer(broker, iceServers, *max) // Begin goptlib client process. ptInfo, err := pt.ClientSetup(nil) From 8467c01e9e88523fcdef22fed8efadbd07484966 Mon Sep 17 00:00:00 2001 From: Peter Gerber Date: Mon, 21 Sep 2020 15:53:24 +0000 Subject: [PATCH 139/385] Consider more IPs to be local --- common/util/util.go | 6 +++++- common/util/util_test.go | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/common/util/util.go b/common/util/util.go index ac254fa..b317e0b 100644 --- a/common/util/util.go +++ b/common/util/util.go @@ -56,7 +56,11 @@ func IsLocal(ip net.IP) bool { // Local IPv4 addresses are defined in https://tools.ietf.org/html/rfc1918 return ip4[0] == 10 || (ip4[0] == 172 && ip4[1]&0xf0 == 16) || - (ip4[0] == 192 && ip4[1] == 168) + (ip4[0] == 192 && ip4[1] == 168) || + // Carrier-Grade NAT as per https://tools.ietf.org/htm/rfc6598 + (ip4[0] == 100 && ip4[1]&0xc0 == 64) || + // Dynamic Configuration as per https://tools.ietf.org/htm/rfc3927 + (ip4[0] == 169 && ip4[1] == 254) } // Local IPv6 addresses are defined in https://tools.ietf.org/html/rfc4193 return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc diff --git a/common/util/util_test.go b/common/util/util_test.go index 271619a..9d52f62 100644 --- a/common/util/util_test.go +++ b/common/util/util_test.go @@ -14,6 +14,8 @@ func TestUtil(t *testing.T) { offer := offerStart + goodCandidate + "a=candidate:3769337065 1 udp 2122260223 192.168.0.100 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLocal IPv4 + "a=candidate:3769337065 1 udp 2122260223 100.127.50.5 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLocal IPv4 + "a=candidate:3769337065 1 udp 2122260223 169.254.250.88 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLocal IPv4 "a=candidate:3769337065 1 udp 2122260223 fdf8:f53b:82e4::53 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsLocal IPv6 "a=candidate:3769337065 1 udp 2122260223 0.0.0.0 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsUnspecified IPv4 "a=candidate:3769337065 1 udp 2122260223 :: 56688 typ host generation 0 network-id 1 network-cost 50\r\n" + // IsUnspecified IPv6 From d7aa9b835645bc52c29ba13cdab461fe0d0e4e66 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 30 Sep 2020 10:10:29 -0400 Subject: [PATCH 140/385] Extract remote address from ICE candidates Parse the received ICE candidates as well as the Connection Data field for a non-local IP address to pass to the bridge. This fixes bug #33157. --- proxy/proxy-go_test.go | 126 ++++++++++++++++++++++++++++++++++++++--- proxy/snowflake.go | 47 ++++++++++++--- 2 files changed, 157 insertions(+), 16 deletions(-) diff --git a/proxy/proxy-go_test.go b/proxy/proxy-go_test.go index 168ca25..1218289 100644 --- a/proxy/proxy-go_test.go +++ b/proxy/proxy-go_test.go @@ -64,6 +64,51 @@ m=audio 49170 RTP/AVP 0 m=video 51372 RTP/AVP 99 a=rtpmap:99 h263-1998/90000 `, net.ParseIP("224.2.17.12")}, + // local addresses only + {`v=0 +o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5 +s=SDP Seminar +i=A Seminar on the session description protocol +u=http://www.example.com/seminars/sdp.pdf +e=j.doe@example.com (Jane Doe) +c=IN IP4 10.47.16.5/127 +t=2873397496 2873404696 +a=recvonly +m=audio 49170 RTP/AVP 0 +m=video 51372 RTP/AVP 99 +a=rtpmap:99 h263-1998/90000 +`, nil}, + // Remote IP in candidate attribute only + {`v=0 +o=- 4358805017720277108 2 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE data +a=msid-semantic: WMS +m=application 56688 DTLS/SCTP 5000 +c=IN IP4 0.0.0.0 +a=candidate:3769337065 1 udp 2122260223 1.2.3.4 56688 typ host generation 0 network-id 1 network-cost 50 +a=ice-ufrag:aMAZ +a=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV +a=ice-options:trickle +a=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66 +a=setup:actpass +a=mid:data +a=sctpmap:5000 webrtc-datachannel 1024 +`, net.ParseIP("1.2.3.4")}, + // Unspecified address + {`v=0 +o=jdoe 2890844526 2890842807 IN IP4 0.0.0.0 +s=SDP Seminar +i=A Seminar on the session description protocol +u=http://www.example.com/seminars/sdp.pdf +e=j.doe@example.com (Jane Doe) +t=2873397496 2873404696 +a=recvonly +m=audio 49170 RTP/AVP 0 +m=video 51372 RTP/AVP 99 +a=rtpmap:99 h263-1998/90000 +`, nil}, // Missing c= line {`v=0 o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5 @@ -78,22 +123,64 @@ m=video 51372 RTP/AVP 99 a=rtpmap:99 h263-1998/90000 `, nil}, // Single line, IP address only - {`c=IN IP4 224.2.1.1 + {`v=0 +o=- 4358805017720277108 2 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE data +a=msid-semantic: WMS +m=application 56688 DTLS/SCTP 5000 +c=IN IP4 224.2.1.1 `, net.ParseIP("224.2.1.1")}, // Same, with TTL - {`c=IN IP4 224.2.1.1/127 + {`v=0 +o=- 4358805017720277108 2 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE data +a=msid-semantic: WMS +m=application 56688 DTLS/SCTP 5000 +c=IN IP4 224.2.1.1/127 `, net.ParseIP("224.2.1.1")}, // Same, with TTL and multicast addresses - {`c=IN IP4 224.2.1.1/127/3 + {`v=0 +o=- 4358805017720277108 2 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE data +a=msid-semantic: WMS +m=application 56688 DTLS/SCTP 5000 +c=IN IP4 224.2.1.1/127/3 `, net.ParseIP("224.2.1.1")}, // IPv6, address only - {`c=IN IP6 FF15::101 + {`v=0 +o=- 4358805017720277108 2 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE data +a=msid-semantic: WMS +m=application 56688 DTLS/SCTP 5000 +c=IN IP6 FF15::101 `, net.ParseIP("ff15::101")}, // Same, with multicast addresses - {`c=IN IP6 FF15::101/3 + {`v=0 +o=- 4358805017720277108 2 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE data +a=msid-semantic: WMS +m=application 56688 DTLS/SCTP 5000 +c=IN IP6 FF15::101/3 `, net.ParseIP("ff15::101")}, // Multiple c= lines - {`c=IN IP4 1.2.3.4 + {`v=0 +o=- 4358805017720277108 2 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE data +a=msid-semantic: WMS +m=application 56688 DTLS/SCTP 5000 +c=IN IP4 1.2.3.4 c=IN IP4 5.6.7.8 `, net.ParseIP("1.2.3.4")}, // Modified from SDP sent by snowflake-client. @@ -116,13 +203,34 @@ a=mid:data a=sctpmap:5000 webrtc-datachannel 1024 `, net.ParseIP("1.2.3.4")}, // Improper character within IPv4 - {`c=IN IP4 224.2z.1.1 + {`v=0 +o=- 4358805017720277108 2 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE data +a=msid-semantic: WMS +m=application 56688 DTLS/SCTP 5000 +c=IN IP4 224.2z.1.1 `, nil}, // Improper character within IPv6 - {`c=IN IP6 ff15:g::101 + {`v=0 +o=- 4358805017720277108 2 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE data +a=msid-semantic: WMS +m=application 56688 DTLS/SCTP 5000 +c=IN IP6 ff15:g::101 `, nil}, // Bogus "IP7" addrtype - {`c=IN IP7 1.2.3.4 + {`v=0 +o=- 4358805017720277108 2 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE data +a=msid-semantic: WMS +m=application 56688 DTLS/SCTP 5000 +c=IN IP7 1.2.3.4 `, nil}, } diff --git a/proxy/snowflake.go b/proxy/snowflake.go index b880b36..ac85527 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -24,6 +24,7 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/util" "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" "github.com/gorilla/websocket" + "github.com/pion/sdp/v2" "github.com/pion/webrtc/v2" ) @@ -65,15 +66,47 @@ var remoteIPPatterns = []*regexp.Regexp{ regexp.MustCompile(`(?m)^c=IN IP6 ([0-9A-Fa-f:.]+)(?:\/\d+)?(:? |\r?\n)`), } -// https://tools.ietf.org/html/rfc4566#section-5.7 -func remoteIPFromSDP(sdp string) net.IP { - for _, pattern := range remoteIPPatterns { - m := pattern.FindStringSubmatch(sdp) - if m != nil { - // Ignore parsing errors, ParseIP returns nil. - return net.ParseIP(m[1]) +// Checks whether an IP address is a remote address for the client +func isRemoteAddress(ip net.IP) bool { + return !(util.IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback()) +} + +func remoteIPFromSDP(str string) net.IP { + // Look for remote IP in "a=candidate" attribute fields + // https://tools.ietf.org/html/rfc5245#section-15.1 + var desc sdp.SessionDescription + err := desc.Unmarshal([]byte(str)) + if err != nil { + log.Println("Error parsing SDP: ", err.Error()) + return nil + } + for _, m := range desc.MediaDescriptions { + for _, a := range m.Attributes { + if a.IsICECandidate() { + ice, err := a.ToICECandidate() + if err == nil { + ip := net.ParseIP(ice.Address) + if ip != nil && isRemoteAddress(ip) { + return ip + } + } + } } } + // Finally look for remote IP in "c=" Connection Data field + // https://tools.ietf.org/html/rfc4566#section-5.7 + for _, pattern := range remoteIPPatterns { + m := pattern.FindStringSubmatch(str) + if m != nil { + // Ignore parsing errors, ParseIP returns nil. + ip := net.ParseIP(m[1]) + if ip != nil && isRemoteAddress(ip) { + return ip + } + + } + } + return nil } From 6baa3c4d5f70fd50223dc41febf67576267d039b Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 15 Oct 2020 14:47:51 -0400 Subject: [PATCH 141/385] Add synchronization to prevent post-melt collects This fixes a race condition in which snowflakes.End() is called while snowflakes.Collect() is in progress resulting in a write to a closed channel. We now wait for all in-progress collections to finish and add an extra check before proceeding with a collection. --- client/lib/peers.go | 15 +++++++++++++-- client/lib/snowflake.go | 1 - 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/client/lib/peers.go b/client/lib/peers.go index d864fc8..d02eed3 100644 --- a/client/lib/peers.go +++ b/client/lib/peers.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log" + "sync" ) // Container which keeps track of multiple WebRTC remote peers. @@ -25,7 +26,10 @@ type Peers struct { snowflakeChan chan *WebRTCPeer activePeers *list.List - melt chan struct{} + melt chan struct{} + melted bool + + collection sync.WaitGroup } // Construct a fresh container of remote peers. @@ -45,6 +49,11 @@ func NewPeers(tongue Tongue) (*Peers, error) { // As part of |SnowflakeCollector| interface. func (p *Peers) Collect() (*WebRTCPeer, error) { // Engage the Snowflake Catching interface, which must be available. + p.collection.Add(1) + defer p.collection.Done() + if p.melted { + return nil, fmt.Errorf("Snowflakes have melted") + } if nil == p.Tongue { return nil, errors.New("missing Tongue to catch Snowflakes with") } @@ -110,8 +119,10 @@ func (p *Peers) purgeClosedPeers() { // Close all Peers contained here. func (p *Peers) End() { - close(p.snowflakeChan) close(p.melt) + p.melted = true + p.collection.Wait() + close(p.snowflakeChan) cnt := p.Count() for e := p.activePeers.Front(); e != nil; { next := e.Next() diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 0ba5667..e888160 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -181,7 +181,6 @@ func Handler(socks net.Conn, tongue Tongue) error { // transfer to the Tor SOCKS handler when needed. func connectLoop(snowflakes SnowflakeCollector) { for { - // Check if ending is necessary. _, err := snowflakes.Collect() if err != nil { log.Printf("WebRTC: %v Retrying in %v...", From 912bcae24eb71bc52c6f28b908e3c7678781e1a2 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Thu, 22 Oct 2020 23:01:45 -0600 Subject: [PATCH 142/385] Don't log io.ErrClosedPipe in proxy. We expect one of these at the end of just about every proxy session, as the Conns in both directions are closed as soon as the copy loop finishes in one direction. Closes #40016. --- proxy/snowflake.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proxy/snowflake.go b/proxy/snowflake.go index ac85527..96851ae 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -300,7 +300,9 @@ func CopyLoop(c1 io.ReadWriteCloser, c2 io.ReadWriteCloser) { var wg sync.WaitGroup copyer := func(dst io.ReadWriteCloser, src io.ReadWriteCloser) { defer wg.Done() - if _, err := io.Copy(dst, src); err != nil { + // Ignore io.ErrClosedPipe because it is likely caused by the + // termination of copyer in the other direction. + if _, err := io.Copy(dst, src); err != nil && err != io.ErrClosedPipe { log.Printf("io.Copy inside CopyLoop generated an error: %v", err) } dst.Close() From 7a0428e3b11ba437f27d09b1a9ad0fa820e54d24 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 13 Oct 2020 11:06:14 -0400 Subject: [PATCH 143/385] Refactor proxy to reuse signaling code Simplify proxy interactions with the broker signaling server and prepare for the introduction of an additional signaling server. --- proxy/proxy-go_test.go | 5 +- proxy/snowflake.go | 114 +++++++++++++++++++++-------------------- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/proxy/proxy-go_test.go b/proxy/proxy-go_test.go index 1218289..e2fb82e 100644 --- a/proxy/proxy-go_test.go +++ b/proxy/proxy-go_test.go @@ -337,7 +337,7 @@ func TestBrokerInteractions(t *testing.T) { const sampleAnswer = `{"type":"answer","sdp":` + sampleSDP + `}` Convey("Proxy connections to broker", t, func() { - broker := new(Broker) + broker := new(SignalingServer) broker.url, _ = url.Parse("localhost") //Mock peerConnection @@ -417,7 +417,8 @@ func TestBrokerInteractions(t *testing.T) { } err = broker.sendAnswer("test", pc) So(err, ShouldNotEqual, nil) - So(err.Error(), ShouldResemble, "broker returned 410") + So(err.Error(), ShouldResemble, + "error sending answer to broker: remote returned status code 410") //Error if we can't parse broker message broker.transport = &MockTransport{ diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 96851ae..276ebed 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -44,7 +44,7 @@ const dataChannelTimeout = 20 * time.Second const readLimit = 100000 //Maximum number of bytes to be read from an HTTP request -var broker *Broker +var broker *SignalingServer var relayURL string var currentNATType = NATUnknown @@ -110,12 +110,6 @@ func remoteIPFromSDP(str string) net.IP { return nil } -type Broker struct { - url *url.URL - transport http.RoundTripper - keepLocalAddresses bool -} - type webRTCConn struct { dc *webrtc.DataChannel pc *webrtc.PeerConnection @@ -200,8 +194,33 @@ func limitedRead(r io.Reader, limit int64) ([]byte, error) { return p, err } -func (b *Broker) pollOffer(sid string) *webrtc.SessionDescription { - brokerPath := b.url.ResolveReference(&url.URL{Path: "proxy"}) +type SignalingServer struct { + url *url.URL + transport http.RoundTripper + keepLocalAddresses bool +} + +func (s *SignalingServer) Post(path string, payload io.Reader) ([]byte, error) { + + req, err := http.NewRequest("POST", path, payload) + if err != nil { + return nil, err + } + resp, err := s.transport.RoundTrip(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("remote returned status code %d", resp.StatusCode) + } + + defer resp.Body.Close() + return limitedRead(resp.Body, readLimit) +} + +func (s *SignalingServer) pollOffer(sid string) *webrtc.SessionDescription { + brokerPath := s.url.ResolveReference(&url.URL{Path: "proxy"}) timeOfNextPoll := time.Now() for { // Sleep until we're scheduled to poll again. @@ -220,45 +239,33 @@ func (b *Broker) pollOffer(sid string) *webrtc.SessionDescription { log.Printf("Error encoding poll message: %s", err.Error()) return nil } - req, _ := http.NewRequest("POST", brokerPath.String(), bytes.NewBuffer(body)) - resp, err := b.transport.RoundTrip(req) + resp, err := s.Post(brokerPath.String(), bytes.NewBuffer(body)) if err != nil { - log.Printf("error polling broker: %s", err) - } else { - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - log.Printf("broker returns: %d", resp.StatusCode) - } else { - body, err := limitedRead(resp.Body, readLimit) - if err != nil { - log.Printf("error reading broker response: %s", err) - } else { + log.Printf("error polling broker: %s", err.Error()) + } - offer, _, err := messages.DecodePollResponse(body) - if err != nil { - log.Printf("error reading broker response: %s", err.Error()) - log.Printf("body: %s", body) - return nil - } - if offer != "" { - offer, err := util.DeserializeSessionDescription(offer) - if err != nil { - log.Printf("Error processing session description: %s", err.Error()) - return nil - } - return offer - - } - } + offer, _, err := messages.DecodePollResponse(resp) + if err != nil { + log.Printf("Error reading broker response: %s", err.Error()) + log.Printf("body: %s", resp) + return nil + } + if offer != "" { + offer, err := util.DeserializeSessionDescription(offer) + if err != nil { + log.Printf("Error processing session description: %s", err.Error()) + return nil } + return offer + } } } -func (b *Broker) sendAnswer(sid string, pc *webrtc.PeerConnection) error { - brokerPath := b.url.ResolveReference(&url.URL{Path: "answer"}) +func (s *SignalingServer) sendAnswer(sid string, pc *webrtc.PeerConnection) error { + brokerPath := s.url.ResolveReference(&url.URL{Path: "answer"}) ld := pc.LocalDescription() - if !b.keepLocalAddresses { + if !s.keepLocalAddresses { ld = &webrtc.SessionDescription{ Type: ld.Type, SDP: util.StripLocalAddresses(ld.SDP), @@ -272,20 +279,12 @@ func (b *Broker) sendAnswer(sid string, pc *webrtc.PeerConnection) error { if err != nil { return err } - req, _ := http.NewRequest("POST", brokerPath.String(), bytes.NewBuffer(body)) - resp, err := b.transport.RoundTrip(req) + resp, err := s.Post(brokerPath.String(), bytes.NewBuffer(body)) if err != nil { - return err - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("broker returned %d", resp.StatusCode) + return fmt.Errorf("error sending answer to broker: %s", err.Error()) } - body, err = limitedRead(resp.Body, readLimit) - if err != nil { - return fmt.Errorf("error reading broker response: %s", err) - } - success, err := messages.DecodeAnswerResponse(body) + success, err := messages.DecodeAnswerResponse(resp) if err != nil { return err } @@ -327,7 +326,6 @@ func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) { log.Fatalf("invalid relay url: %s", err) } - // Retrieve client IP address if remoteAddr != nil { // Encode client IP address in relay URL q := u.Query() @@ -354,7 +352,11 @@ func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) { // candidates is complete and the answer is available in LocalDescription. // Installs an OnDataChannel callback that creates a webRTCConn and passes it to // datachannelHandler. -func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, config webrtc.Configuration, dataChan chan struct{}) (*webrtc.PeerConnection, error) { +func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, + config webrtc.Configuration, + dataChan chan struct{}, + handler func(conn *webRTCConn, remoteAddr net.Addr)) (*webrtc.PeerConnection, error) { + pc, err := webrtc.NewPeerConnection(config) if err != nil { return nil, fmt.Errorf("accept: NewPeerConnection: %s", err) @@ -390,7 +392,7 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, config webrtc.C } }) - go datachannelHandler(conn, conn.RemoteAddr()) + go handler(conn, conn.RemoteAddr()) }) err = pc.SetRemoteDescription(*sdp) @@ -433,7 +435,7 @@ func runSession(sid string) { return } dataChan := make(chan struct{}) - pc, err := makePeerConnectionFromOffer(offer, config, dataChan) + pc, err := makePeerConnectionFromOffer(offer, config, dataChan, datachannelHandler) if err != nil { log.Printf("error making WebRTC connection: %s", err) retToken() @@ -500,7 +502,7 @@ func main() { log.Println("starting") var err error - broker = new(Broker) + broker = new(SignalingServer) broker.keepLocalAddresses = keepLocalAddresses broker.url, err = url.Parse(rawBrokerURL) if err != nil { From f368c871095dae3aa990e7d46a1d6612af9909b9 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 13 Oct 2020 17:18:50 -0400 Subject: [PATCH 144/385] Add a remote service to test NAT compatability Add a remote probetest service that will allow proxies to test their compatability with symmetric NATs. --- .gitignore | 1 + probetest/probetest.go | 226 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 probetest/probetest.go diff --git a/.gitignore b/.gitignore index 9f36c7c..002f95e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ broker/broker client/client server/server proxy/proxy +probetest/probetest snowflake.log ignore/ diff --git a/probetest/probetest.go b/probetest/probetest.go new file mode 100644 index 0000000..af08e32 --- /dev/null +++ b/probetest/probetest.go @@ -0,0 +1,226 @@ +/* +Probe test server to check the reachability of Snowflake proxies from +clients with symmetric NATs. + +The probe server receives an offer from a proxy, returns an answer, and then +attempts to establish a datachannel connection to that proxy. The proxy will +self-determine whether the connection opened successfully. +*/ +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "time" + + "git.torproject.org/pluggable-transports/snowflake.git/common/messages" + "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" + "git.torproject.org/pluggable-transports/snowflake.git/common/util" + + "github.com/pion/webrtc/v2" + "golang.org/x/crypto/acme/autocert" +) + +const ( + readLimit = 100000 //Maximum number of bytes to be read from an HTTP request + dataChannelTimeout = 20 * time.Second //time after which we assume proxy data channel will not open + stunUrl = "stun:stun.l.google.com:19302" //default STUN URL +) + +// Create a PeerConnection from an SDP offer. Blocks until the gathering of ICE +// candidates is complete and the answer is available in LocalDescription. +func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, + dataChan chan struct{}) (*webrtc.PeerConnection, error) { + + config := webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{stunUrl}, + }, + }, + } + pc, err := webrtc.NewPeerConnection(config) + if err != nil { + return nil, fmt.Errorf("accept: NewPeerConnection: %s", err) + } + pc.OnDataChannel(func(dc *webrtc.DataChannel) { + close(dataChan) + }) + + err = pc.SetRemoteDescription(*sdp) + if err != nil { + if inerr := pc.Close(); inerr != nil { + log.Printf("unable to call pc.Close after pc.SetRemoteDescription with error: %v", inerr) + } + return nil, fmt.Errorf("accept: SetRemoteDescription: %s", err) + } + + answer, err := pc.CreateAnswer(nil) + if err != nil { + if inerr := pc.Close(); inerr != nil { + log.Printf("ICE gathering has generated an error when calling pc.Close: %v", inerr) + } + return nil, err + } + + err = pc.SetLocalDescription(answer) + if err != nil { + if err = pc.Close(); err != nil { + log.Printf("pc.Close after setting local description returned : %v", err) + } + return nil, err + } + + return pc, nil +} + +func probeHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + resp, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) + if nil != err { + log.Println("Invalid data.") + w.WriteHeader(http.StatusBadRequest) + return + } + + offer, _, err := messages.DecodePollResponse(resp) + if err != nil { + log.Printf("Error reading offer: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + if offer == "" { + log.Printf("Error processing session description: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + sdp, err := util.DeserializeSessionDescription(offer) + if err != nil { + log.Printf("Error processing session description: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + + dataChan := make(chan struct{}) + pc, err := makePeerConnectionFromOffer(sdp, dataChan) + if err != nil { + log.Printf("Error making WebRTC connection: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + sdp = &webrtc.SessionDescription{ + Type: pc.LocalDescription().Type, + SDP: util.StripLocalAddresses(pc.LocalDescription().SDP), + } + answer, err := util.SerializeSessionDescription(sdp) + if err != nil { + log.Printf("Error making WebRTC connection: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + body, err := messages.EncodeAnswerRequest(answer, "") + if err != nil { + log.Printf("Error making WebRTC connection: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(body) + // Set a timeout on peerconnection. If the connection state has not + // advanced to PeerConnectionStateConnected in this time, + // destroy the peer connection and return the token. + select { + case <-dataChan: + if err := pc.Close(); err != nil { + log.Printf("Error calling pc.Close: %v", err) + } + case <-time.After(dataChannelTimeout): + if err := pc.Close(); err != nil { + log.Printf("Error calling pc.Close: %v", err) + } + } + return + +} + +func main() { + var acmeEmail string + var acmeHostnamesCommas string + var acmeCertCacheDir string + var addr string + var disableTLS bool + var certFilename, keyFilename string + var unsafeLogging bool + + flag.StringVar(&acmeEmail, "acme-email", "", "optional contact email for Let's Encrypt notifications") + flag.StringVar(&acmeHostnamesCommas, "acme-hostnames", "", "comma-separated hostnames for TLS certificate") + flag.StringVar(&acmeCertCacheDir, "acme-cert-cache", "acme-cert-cache", "directory in which certificates should be cached") + flag.StringVar(&certFilename, "cert", "", "TLS certificate file") + flag.StringVar(&keyFilename, "key", "", "TLS private key file") + flag.StringVar(&addr, "addr", ":8443", "address to listen on") + flag.BoolVar(&disableTLS, "disable-tls", false, "don't use HTTPS") + flag.BoolVar(&unsafeLogging, "unsafe-logging", false, "prevent logs from being scrubbed") + flag.Parse() + + var logOutput io.Writer = os.Stderr + if unsafeLogging { + log.SetOutput(logOutput) + } else { + // Scrub log output just in case an address ends up there + log.SetOutput(&safelog.LogScrubber{Output: logOutput}) + } + + log.SetFlags(log.LstdFlags | log.LUTC) + + http.HandleFunc("/probe", probeHandler) + + server := http.Server{ + Addr: addr, + } + + var err error + if acmeHostnamesCommas != "" { + acmeHostnames := strings.Split(acmeHostnamesCommas, ",") + log.Printf("ACME hostnames: %q", acmeHostnames) + + var cache autocert.Cache + if err = os.MkdirAll(acmeCertCacheDir, 0700); err != nil { + log.Printf("Warning: Couldn't create cache directory %q (reason: %s) so we're *not* using our certificate cache.", acmeCertCacheDir, err) + } else { + cache = autocert.DirCache(acmeCertCacheDir) + } + + certManager := autocert.Manager{ + Cache: cache, + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(acmeHostnames...), + Email: acmeEmail, + } + // start certificate manager handler + go func() { + log.Printf("Starting HTTP-01 listener") + log.Fatal(http.ListenAndServe(":80", certManager.HTTPHandler(nil))) + }() + + server.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate} + err = server.ListenAndServeTLS("", "") + } else if certFilename != "" && keyFilename != "" { + err = server.ListenAndServeTLS(certFilename, keyFilename) + } else if disableTLS { + err = server.ListenAndServe() + } else { + log.Fatal("the --cert and --key, --acme-hostnames, or --disable-tls option is required") + } + + if err != nil { + log.Println(err) + } +} From a4f10d9d6eaa8806adc5eefaf7ac46d4050340d1 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 14 Oct 2020 15:49:01 -0400 Subject: [PATCH 145/385] Add Dockerfile and README for deploying probetest The easiest way to set up the probe server behind a symmetric NAT is to deploy it as a Docker container and alter the iptables rules for the Docker network subnet that the container runs in. --- probetest/Dockerfile | 3 +++ probetest/README.md | 44 ++++++++++++++++++++++++++++++++++++ probetest/docker-compose.yml | 11 +++++++++ 3 files changed, 58 insertions(+) create mode 100644 probetest/Dockerfile create mode 100644 probetest/README.md create mode 100644 probetest/docker-compose.yml diff --git a/probetest/Dockerfile b/probetest/Dockerfile new file mode 100644 index 0000000..966ab28 --- /dev/null +++ b/probetest/Dockerfile @@ -0,0 +1,3 @@ +FROM golang:1.13 + +COPY probetest /go/bin diff --git a/probetest/README.md b/probetest/README.md new file mode 100644 index 0000000..8af42f5 --- /dev/null +++ b/probetest/README.md @@ -0,0 +1,44 @@ +This is code for a remote probe test component of Snowflake. + +### Overview + +This is a probe test server to allow proxies to test their compatability +with Snowflake. Right now the only type of test implemented is a +compatability check for clients with symmetric NATs. + +### Running your own + +The server uses TLS by default. +There is a `--disable-tls` option for testing purposes, +but you should use TLS in production. + +To build the probe server, run +```go build``` + +To deploy the probe server, first set the necessary env variables with +``` +export HOSTNAMES=${YOUR HOSTNAMES} +export EMAIL=${YOUR EMAIL} +``` +then run ```docker-compose up``` + +Setting up a symmetric NAT configuration requires a few extra steps. After +upping the docker container, run +```docker inspect snowflake-probetest``` +to find the subnet used by the probetest container. Then run +```sudo iptables -L -t nat``` to find the POSTROUTING rules for the subnet. +It should look something like this: +``` +Chain POSTROUTING (policy ACCEPT) +target prot opt source destination +MASQUERADE all -- 172.19.0.0/16 anywhere +``` +to modify this rule, execute the command +```sudo iptables -t nat -R POSTROUTING $RULE_NUM -s 172.19.0.0/16 -j MASQUERADE --random``` +where RULE_NUM is the numbered rule corresponding to your docker container's subnet masquerade rule. +Afterwards, you should see the rule changed to be: +``` +Chain POSTROUTING (policy ACCEPT) +target prot opt source destination +MASQUERADE all -- 172.19.0.0/16 anywhere random +``` diff --git a/probetest/docker-compose.yml b/probetest/docker-compose.yml new file mode 100644 index 0000000..9283383 --- /dev/null +++ b/probetest/docker-compose.yml @@ -0,0 +1,11 @@ + version: "3.8" + + services: + snowflake-probetest: + build: . + container_name: snowflake-probetest + ports: + - "8443:8443" + volumes: + - /home/snowflake-broker/acme-cert-cache:/go/bin/acme-cert-cache + entrypoint: [ "probetest" , "-addr", ":8443" , "-acme-hostnames", $HOSTNAMES, "-acme-email", $EMAIL, "-acme-cert-cache", "/go/bin/acme-cert-cache"] From b5ce2598586d729b0906d3936706dc48e82e1455 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 5 Nov 2020 12:34:24 -0500 Subject: [PATCH 146/385] Fixed a bug that forced datachannel timeout The probetest answer response was not being sent until the select call received a datachannel timeout causing all attempted connections to fail. --- probetest/probetest.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/probetest/probetest.go b/probetest/probetest.go index af08e32..1d2d6ef 100644 --- a/probetest/probetest.go +++ b/probetest/probetest.go @@ -137,16 +137,15 @@ func probeHandler(w http.ResponseWriter, r *http.Request) { // Set a timeout on peerconnection. If the connection state has not // advanced to PeerConnectionStateConnected in this time, // destroy the peer connection and return the token. - select { - case <-dataChan: + go func() { + select { + case <-dataChan: + case <-time.After(dataChannelTimeout): + } if err := pc.Close(); err != nil { log.Printf("Error calling pc.Close: %v", err) } - case <-time.After(dataChannelTimeout): - if err := pc.Close(); err != nil { - log.Printf("Error calling pc.Close: %v", err) - } - } + }() return } From 4663599382e4db8167fcc23a1a890e24ebca517a Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 5 Nov 2020 16:48:00 -0500 Subject: [PATCH 147/385] Make probetest wait for a datachannel to open --- probetest/probetest.go | 7 +- proxy/snowflake.go | 205 +++++++++++++++++++++++++++++++---------- 2 files changed, 162 insertions(+), 50 deletions(-) diff --git a/probetest/probetest.go b/probetest/probetest.go index 1d2d6ef..70032da 100644 --- a/probetest/probetest.go +++ b/probetest/probetest.go @@ -51,7 +51,12 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, return nil, fmt.Errorf("accept: NewPeerConnection: %s", err) } pc.OnDataChannel(func(dc *webrtc.DataChannel) { - close(dataChan) + dc.OnOpen(func() { + close(dataChan) + }) + dc.OnClose(func() { + dc.Close() + }) }) err = pc.SetRemoteDescription(*sdp) diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 276ebed..0df0d17 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -352,7 +352,7 @@ func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) { // candidates is complete and the answer is available in LocalDescription. // Installs an OnDataChannel callback that creates a webRTCConn and passes it to // datachannelHandler. -func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, +func makePeerConnection(offering bool, sdp *webrtc.SessionDescription, config webrtc.Configuration, dataChan chan struct{}, handler func(conn *webRTCConn, remoteAddr net.Addr)) (*webrtc.PeerConnection, error) { @@ -361,67 +361,99 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, if err != nil { return nil, fmt.Errorf("accept: NewPeerConnection: %s", err) } - pc.OnDataChannel(func(dc *webrtc.DataChannel) { - log.Println("OnDataChannel") - close(dataChan) - pr, pw := io.Pipe() - conn := &webRTCConn{pc: pc, dc: dc, pr: pr} + if offering { + offer, err := pc.CreateOffer(nil) + // TODO: Potentially timeout and retry if ICE isn't working. + if err != nil { + log.Println("Failed to prepare offer", err) + pc.Close() + return nil, err + } + log.Println("WebRTC: Created offer") + err = pc.SetLocalDescription(offer) + if err != nil { + log.Println("Failed to prepare offer", err) + pc.Close() + return nil, err + } + log.Println("WebRTC: Set local description") + dc, err := pc.CreateDataChannel("test", &webrtc.DataChannelInit{}) + if err != nil { + log.Printf("CreateDataChannel ERROR: %s", err) + return nil, err + } dc.OnOpen(func() { - log.Println("OnOpen channel") + log.Println("WebRTC: DataChannel.OnOpen") + close(dataChan) }) dc.OnClose(func() { - conn.lock.Lock() - defer conn.lock.Unlock() - log.Println("OnClose channel") - conn.dc = nil + log.Println("WebRTC: DataChannel.OnClose") dc.Close() - pw.Close() }) - dc.OnMessage(func(msg webrtc.DataChannelMessage) { - var n int - n, err = pw.Write(msg.Data) - if err != nil { - if inerr := pw.CloseWithError(err); inerr != nil { - log.Printf("close with error generated an error: %v", inerr) + } else { + pc.OnDataChannel(func(dc *webrtc.DataChannel) { + log.Println("OnDataChannel") + close(dataChan) + + pr, pw := io.Pipe() + conn := &webRTCConn{pc: pc, dc: dc, pr: pr} + + dc.OnOpen(func() { + log.Println("OnOpen channel") + }) + dc.OnClose(func() { + conn.lock.Lock() + defer conn.lock.Unlock() + log.Println("OnClose channel") + conn.dc = nil + dc.Close() + pw.Close() + }) + dc.OnMessage(func(msg webrtc.DataChannelMessage) { + var n int + n, err = pw.Write(msg.Data) + if err != nil { + if inerr := pw.CloseWithError(err); inerr != nil { + log.Printf("close with error generated an error: %v", inerr) + } } - } - if n != len(msg.Data) { - panic("short write") - } + if n != len(msg.Data) { + panic("short write") + } + }) + + go handler(conn, conn.RemoteAddr()) }) - - go handler(conn, conn.RemoteAddr()) - }) - - err = pc.SetRemoteDescription(*sdp) - if err != nil { - if inerr := pc.Close(); inerr != nil { - log.Printf("unable to call pc.Close after pc.SetRemoteDescription with error: %v", inerr) + err = pc.SetRemoteDescription(*sdp) + if err != nil { + if inerr := pc.Close(); inerr != nil { + log.Printf("unable to call pc.Close after pc.SetRemoteDescription with error: %v", inerr) + } + return nil, fmt.Errorf("accept: SetRemoteDescription: %s", err) } - return nil, fmt.Errorf("accept: SetRemoteDescription: %s", err) - } - log.Println("sdp offer successfully received.") + log.Println("sdp offer successfully received.") - log.Println("Generating answer...") - answer, err := pc.CreateAnswer(nil) - // blocks on ICE gathering. we need to add a timeout if needed - // not putting this in a separate go routine, because we need - // SetLocalDescription(answer) to be called before sendAnswer - if err != nil { - if inerr := pc.Close(); inerr != nil { - log.Printf("ICE gathering has generated an error when calling pc.Close: %v", inerr) + log.Println("Generating answer...") + answer, err := pc.CreateAnswer(nil) + // blocks on ICE gathering. we need to add a timeout if needed + // not putting this in a separate go routine, because we need + // SetLocalDescription(answer) to be called before sendAnswer + if err != nil { + if inerr := pc.Close(); inerr != nil { + log.Printf("ICE gathering has generated an error when calling pc.Close: %v", inerr) + } + return nil, err } - return nil, err - } - err = pc.SetLocalDescription(answer) - if err != nil { - if err = pc.Close(); err != nil { - log.Printf("pc.Close after setting local description returned : %v", err) + err = pc.SetLocalDescription(answer) + if err != nil { + if err = pc.Close(); err != nil { + log.Printf("pc.Close after setting local description returned : %v", err) + } + return nil, err } - return nil, err } return pc, nil @@ -435,7 +467,7 @@ func runSession(sid string) { return } dataChan := make(chan struct{}) - pc, err := makePeerConnectionFromOffer(offer, config, dataChan, datachannelHandler) + pc, err := makePeerConnection(false, offer, config, dataChan, datachannelHandler) if err != nil { log.Printf("error making WebRTC connection: %s", err) retToken() @@ -535,6 +567,11 @@ func main() { updateNATType(config.ICEServers) log.Printf("NAT type: %s", currentNATType) + // use probetest to determine NAT compatability + for { + testThroughput(config, "https://snowflake-broker.torproject.net:8443") + } + for { getToken() sessionID := genSessionID() @@ -542,6 +579,76 @@ func main() { } } +func testThroughput(config webrtc.Configuration, probeURL string) { + + var err error + + probe := new(SignalingServer) + probe.transport = http.DefaultTransport.(*http.Transport) + broker.transport.(*http.Transport).ResponseHeaderTimeout = 30 * time.Second + probe.url, err = url.Parse(probeURL) + if err != nil { + log.Printf("Error parsing url: %s", err.Error()) + } + probePath := probe.url.ResolveReference(&url.URL{Path: "probe"}) + + // create offer + dataChan := make(chan struct{}) + pc, err := makePeerConnection(true, nil, config, dataChan, func(conn *webRTCConn, addr net.Addr) { conn.Close() }) + if err != nil { + log.Printf("error making WebRTC connection: %s", err) + return + } + + offer := pc.LocalDescription() + sdp, err := util.SerializeSessionDescription(offer) + if err != nil { + log.Printf("Error encoding probe message: %s", err.Error()) + return + } + + // send offer + body, err := messages.EncodePollResponse(sdp, true, "") + if err != nil { + log.Printf("Error encoding probe message: %s", err.Error()) + return + } + log.Println(string(body)) + resp, err := probe.Post(probePath.String(), bytes.NewBuffer(body)) + if err != nil { + log.Printf("error polling probe: %s", err.Error()) + return + } + + sdp, _, err = messages.DecodeAnswerRequest(resp) + if err != nil { + log.Printf("Error reading probe response: %s", err.Error()) + return + } + answer, err := util.DeserializeSessionDescription(sdp) + if err != nil { + log.Printf("Error setting answer: %s", err.Error()) + return + } + err = pc.SetRemoteDescription(*answer) + if err != nil { + log.Printf("Error setting answer: %s", err.Error()) + return + } + + log.Println("Trying to open datachannel") + select { + case <-dataChan: + log.Println("Connection successful.") + case <-time.After(dataChannelTimeout): + log.Println("Timed out waiting for client to open data channel.") + if err := pc.Close(); err != nil { + log.Printf("error calling pc.Close: %v", err) + } + } + +} + // use provided STUN server(s) to determine NAT type func updateNATType(servers []webrtc.ICEServer) { From 61beb9d996527cd8cb9e4ca650f8cbf24df1503e Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 5 Nov 2020 19:28:20 -0500 Subject: [PATCH 148/385] Revert accidentally merged code Some temporary testing code for the proxy got accidentally merged into the latest changes. This commit undoes that mistake. --- proxy/snowflake.go | 209 +++++++++++---------------------------------- 1 file changed, 51 insertions(+), 158 deletions(-) diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 0df0d17..276ebed 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -352,7 +352,7 @@ func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) { // candidates is complete and the answer is available in LocalDescription. // Installs an OnDataChannel callback that creates a webRTCConn and passes it to // datachannelHandler. -func makePeerConnection(offering bool, sdp *webrtc.SessionDescription, +func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, config webrtc.Configuration, dataChan chan struct{}, handler func(conn *webRTCConn, remoteAddr net.Addr)) (*webrtc.PeerConnection, error) { @@ -361,99 +361,67 @@ func makePeerConnection(offering bool, sdp *webrtc.SessionDescription, if err != nil { return nil, fmt.Errorf("accept: NewPeerConnection: %s", err) } + pc.OnDataChannel(func(dc *webrtc.DataChannel) { + log.Println("OnDataChannel") + close(dataChan) - if offering { - offer, err := pc.CreateOffer(nil) - // TODO: Potentially timeout and retry if ICE isn't working. - if err != nil { - log.Println("Failed to prepare offer", err) - pc.Close() - return nil, err - } - log.Println("WebRTC: Created offer") - err = pc.SetLocalDescription(offer) - if err != nil { - log.Println("Failed to prepare offer", err) - pc.Close() - return nil, err - } - log.Println("WebRTC: Set local description") + pr, pw := io.Pipe() + conn := &webRTCConn{pc: pc, dc: dc, pr: pr} - dc, err := pc.CreateDataChannel("test", &webrtc.DataChannelInit{}) - if err != nil { - log.Printf("CreateDataChannel ERROR: %s", err) - return nil, err - } dc.OnOpen(func() { - log.Println("WebRTC: DataChannel.OnOpen") - close(dataChan) + log.Println("OnOpen channel") }) dc.OnClose(func() { - log.Println("WebRTC: DataChannel.OnClose") + conn.lock.Lock() + defer conn.lock.Unlock() + log.Println("OnClose channel") + conn.dc = nil dc.Close() + pw.Close() }) - } else { - pc.OnDataChannel(func(dc *webrtc.DataChannel) { - log.Println("OnDataChannel") - close(dataChan) - - pr, pw := io.Pipe() - conn := &webRTCConn{pc: pc, dc: dc, pr: pr} - - dc.OnOpen(func() { - log.Println("OnOpen channel") - }) - dc.OnClose(func() { - conn.lock.Lock() - defer conn.lock.Unlock() - log.Println("OnClose channel") - conn.dc = nil - dc.Close() - pw.Close() - }) - dc.OnMessage(func(msg webrtc.DataChannelMessage) { - var n int - n, err = pw.Write(msg.Data) - if err != nil { - if inerr := pw.CloseWithError(err); inerr != nil { - log.Printf("close with error generated an error: %v", inerr) - } + dc.OnMessage(func(msg webrtc.DataChannelMessage) { + var n int + n, err = pw.Write(msg.Data) + if err != nil { + if inerr := pw.CloseWithError(err); inerr != nil { + log.Printf("close with error generated an error: %v", inerr) } - if n != len(msg.Data) { - panic("short write") - } - }) - - go handler(conn, conn.RemoteAddr()) + } + if n != len(msg.Data) { + panic("short write") + } }) - err = pc.SetRemoteDescription(*sdp) - if err != nil { - if inerr := pc.Close(); inerr != nil { - log.Printf("unable to call pc.Close after pc.SetRemoteDescription with error: %v", inerr) - } - return nil, fmt.Errorf("accept: SetRemoteDescription: %s", err) - } - log.Println("sdp offer successfully received.") - log.Println("Generating answer...") - answer, err := pc.CreateAnswer(nil) - // blocks on ICE gathering. we need to add a timeout if needed - // not putting this in a separate go routine, because we need - // SetLocalDescription(answer) to be called before sendAnswer - if err != nil { - if inerr := pc.Close(); inerr != nil { - log.Printf("ICE gathering has generated an error when calling pc.Close: %v", inerr) - } - return nil, err - } + go handler(conn, conn.RemoteAddr()) + }) - err = pc.SetLocalDescription(answer) - if err != nil { - if err = pc.Close(); err != nil { - log.Printf("pc.Close after setting local description returned : %v", err) - } - return nil, err + err = pc.SetRemoteDescription(*sdp) + if err != nil { + if inerr := pc.Close(); inerr != nil { + log.Printf("unable to call pc.Close after pc.SetRemoteDescription with error: %v", inerr) } + return nil, fmt.Errorf("accept: SetRemoteDescription: %s", err) + } + log.Println("sdp offer successfully received.") + + log.Println("Generating answer...") + answer, err := pc.CreateAnswer(nil) + // blocks on ICE gathering. we need to add a timeout if needed + // not putting this in a separate go routine, because we need + // SetLocalDescription(answer) to be called before sendAnswer + if err != nil { + if inerr := pc.Close(); inerr != nil { + log.Printf("ICE gathering has generated an error when calling pc.Close: %v", inerr) + } + return nil, err + } + + err = pc.SetLocalDescription(answer) + if err != nil { + if err = pc.Close(); err != nil { + log.Printf("pc.Close after setting local description returned : %v", err) + } + return nil, err } return pc, nil @@ -467,7 +435,7 @@ func runSession(sid string) { return } dataChan := make(chan struct{}) - pc, err := makePeerConnection(false, offer, config, dataChan, datachannelHandler) + pc, err := makePeerConnectionFromOffer(offer, config, dataChan, datachannelHandler) if err != nil { log.Printf("error making WebRTC connection: %s", err) retToken() @@ -567,11 +535,6 @@ func main() { updateNATType(config.ICEServers) log.Printf("NAT type: %s", currentNATType) - // use probetest to determine NAT compatability - for { - testThroughput(config, "https://snowflake-broker.torproject.net:8443") - } - for { getToken() sessionID := genSessionID() @@ -579,76 +542,6 @@ func main() { } } -func testThroughput(config webrtc.Configuration, probeURL string) { - - var err error - - probe := new(SignalingServer) - probe.transport = http.DefaultTransport.(*http.Transport) - broker.transport.(*http.Transport).ResponseHeaderTimeout = 30 * time.Second - probe.url, err = url.Parse(probeURL) - if err != nil { - log.Printf("Error parsing url: %s", err.Error()) - } - probePath := probe.url.ResolveReference(&url.URL{Path: "probe"}) - - // create offer - dataChan := make(chan struct{}) - pc, err := makePeerConnection(true, nil, config, dataChan, func(conn *webRTCConn, addr net.Addr) { conn.Close() }) - if err != nil { - log.Printf("error making WebRTC connection: %s", err) - return - } - - offer := pc.LocalDescription() - sdp, err := util.SerializeSessionDescription(offer) - if err != nil { - log.Printf("Error encoding probe message: %s", err.Error()) - return - } - - // send offer - body, err := messages.EncodePollResponse(sdp, true, "") - if err != nil { - log.Printf("Error encoding probe message: %s", err.Error()) - return - } - log.Println(string(body)) - resp, err := probe.Post(probePath.String(), bytes.NewBuffer(body)) - if err != nil { - log.Printf("error polling probe: %s", err.Error()) - return - } - - sdp, _, err = messages.DecodeAnswerRequest(resp) - if err != nil { - log.Printf("Error reading probe response: %s", err.Error()) - return - } - answer, err := util.DeserializeSessionDescription(sdp) - if err != nil { - log.Printf("Error setting answer: %s", err.Error()) - return - } - err = pc.SetRemoteDescription(*answer) - if err != nil { - log.Printf("Error setting answer: %s", err.Error()) - return - } - - log.Println("Trying to open datachannel") - select { - case <-dataChan: - log.Println("Connection successful.") - case <-time.After(dataChannelTimeout): - log.Println("Timed out waiting for client to open data channel.") - if err := pc.Close(); err != nil { - log.Printf("error calling pc.Close: %v", err) - } - } - -} - // use provided STUN server(s) to determine NAT type func updateNATType(servers []webrtc.ICEServer) { From 0bed9c48b7a3f59c7141e9934a44e73e03096faf Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 18 Nov 2020 15:40:32 -0500 Subject: [PATCH 149/385] Redefine only symmetric NATs as restricted --- common/nat/nat.go | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/common/nat/nat.go b/common/nat/nat.go index c9f16ad..4f2780c 100644 --- a/common/nat/nat.go +++ b/common/nat/nat.go @@ -16,10 +16,11 @@ package nat import ( "errors" "fmt" - "github.com/pion/stun" "log" "net" "time" + + "github.com/pion/stun" ) var ErrTimedOut = errors.New("timed out waiting for response") @@ -36,16 +37,7 @@ const ( // and false if the NAT is unrestrictive (meaning it // will work with most other NATs), func CheckIfRestrictedNAT(server string) (bool, error) { - result, err := isRestrictedMapping(server) - if err != nil { - return false, err - } - if !result { - // if the mapping is unrestrictive, we still need to check whether - // the filtering is restrictive - result, err = isRestrictedFiltering(server) - } - return result, err + return isRestrictedMapping(server) } // Performs two tests from RFC 5780 to determine whether the mapping type From cf2eb5e6c0981831d713a38056226813b8ded623 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 18 Nov 2020 15:57:51 -0500 Subject: [PATCH 150/385] Add a stub sid to probetest answer This will prevent calls to DecodeAnswerRequest from returning an error even though the sid is not needed for the probetest. --- probetest/probetest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/probetest/probetest.go b/probetest/probetest.go index 70032da..d952123 100644 --- a/probetest/probetest.go +++ b/probetest/probetest.go @@ -131,7 +131,7 @@ func probeHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) return } - body, err := messages.EncodeAnswerRequest(answer, "") + body, err := messages.EncodeAnswerRequest(answer, "stub-sid") if err != nil { log.Printf("Error making WebRTC connection: %s", err) w.WriteHeader(http.StatusInternalServerError) From 00f8f85f412878c2066fcb5d3f4739e50912a925 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 18 Nov 2020 16:18:35 -0500 Subject: [PATCH 151/385] Use remote probe to determine proxy NAT type Rather than having standalone proxies determine their NAT type by conducting the NAT behaviour checks in RFC 5780, use the remote probe service instead. --- proxy/snowflake.go | 125 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 19 deletions(-) diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 276ebed..f0fa2c0 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -19,7 +19,6 @@ import ( "time" "git.torproject.org/pluggable-transports/snowflake.git/common/messages" - "git.torproject.org/pluggable-transports/snowflake.git/common/nat" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" "git.torproject.org/pluggable-transports/snowflake.git/common/util" "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" @@ -29,6 +28,7 @@ import ( ) const defaultBrokerURL = "https://snowflake-broker.bamsoftware.com/" +const defaultProbeURL = "https://snowflake-broker.torproject.net:8443/probe" const defaultRelayURL = "wss://snowflake.bamsoftware.com/" const defaultSTUNURL = "stun:stun.stunprotocol.org:3478" const pollInterval = 5 * time.Second @@ -427,6 +427,48 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, return pc, nil } +// Create a new PeerConnection. Blocks until the gathering of ICE +// candidates is complete and the answer is available in LocalDescription. +func makeNewPeerConnection(config webrtc.Configuration, + dataChan chan struct{}) (*webrtc.PeerConnection, error) { + + pc, err := webrtc.NewPeerConnection(config) + if err != nil { + return nil, fmt.Errorf("accept: NewPeerConnection: %s", err) + } + + offer, err := pc.CreateOffer(nil) + // TODO: Potentially timeout and retry if ICE isn't working. + if err != nil { + log.Println("Failed to prepare offer", err) + pc.Close() + return nil, err + } + log.Println("WebRTC: Created offer") + err = pc.SetLocalDescription(offer) + if err != nil { + log.Println("Failed to prepare offer", err) + pc.Close() + return nil, err + } + log.Println("WebRTC: Set local description") + + dc, err := pc.CreateDataChannel("test", &webrtc.DataChannelInit{}) + if err != nil { + log.Printf("CreateDataChannel ERROR: %s", err) + return nil, err + } + dc.OnOpen(func() { + log.Println("WebRTC: DataChannel.OnOpen") + close(dataChan) + }) + dc.OnClose(func() { + log.Println("WebRTC: DataChannel.OnClose") + dc.Close() + }) + return pc, nil +} + func runSession(sid string) { offer := broker.pollOffer(sid) if offer == nil { @@ -531,8 +573,8 @@ func main() { tokens <- true } - // determine NAT type before polling - updateNATType(config.ICEServers) + // use probetest to determine NAT compatability + checkNATType(config, defaultProbeURL) log.Printf("NAT type: %s", currentNATType) for { @@ -542,24 +584,69 @@ func main() { } } -// use provided STUN server(s) to determine NAT type -func updateNATType(servers []webrtc.ICEServer) { +func checkNATType(config webrtc.Configuration, probeURL string) { - var restrictedNAT bool var err error - for _, server := range servers { - addr := strings.TrimPrefix(server.URLs[0], "stun:") - restrictedNAT, err = nat.CheckIfRestrictedNAT(addr) - if err == nil { - if restrictedNAT { - currentNATType = NATRestricted - } else { - currentNATType = NATUnrestricted - } - break - } - } + + probe := new(SignalingServer) + probe.transport = http.DefaultTransport.(*http.Transport) + probe.transport.(*http.Transport).ResponseHeaderTimeout = 30 * time.Second + probe.url, err = url.Parse(probeURL) if err != nil { - currentNATType = NATUnknown + log.Printf("Error parsing url: %s", err.Error()) } + + // create offer + dataChan := make(chan struct{}) + pc, err := makeNewPeerConnection(config, dataChan) + if err != nil { + log.Printf("error making WebRTC connection: %s", err) + return + } + + offer := pc.LocalDescription() + sdp, err := util.SerializeSessionDescription(offer) + if err != nil { + log.Printf("Error encoding probe message: %s", err.Error()) + return + } + + // send offer + body, err := messages.EncodePollResponse(sdp, true, "") + if err != nil { + log.Printf("Error encoding probe message: %s", err.Error()) + return + } + resp, err := probe.Post(probe.url.String(), bytes.NewBuffer(body)) + if err != nil { + log.Printf("error polling probe: %s", err.Error()) + return + } + + sdp, _, err = messages.DecodeAnswerRequest(resp) + if err != nil { + log.Printf("Error reading probe response: %s", err.Error()) + return + } + answer, err := util.DeserializeSessionDescription(sdp) + if err != nil { + log.Printf("Error setting answer: %s", err.Error()) + return + } + err = pc.SetRemoteDescription(*answer) + if err != nil { + log.Printf("Error setting answer: %s", err.Error()) + return + } + + select { + case <-dataChan: + currentNATType = NATUnrestricted + case <-time.After(dataChannelTimeout): + currentNATType = NATRestricted + } + if err := pc.Close(); err != nil { + log.Printf("error calling pc.Close: %v", err) + } + } From ece43cbfcfc328bf0d45ee1ce5998ea295035af4 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 20 Nov 2020 01:15:16 -0500 Subject: [PATCH 152/385] Note that isRestrictedFiltering is no longer used --- common/nat/nat.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/nat/nat.go b/common/nat/nat.go index 4f2780c..552ed45 100644 --- a/common/nat/nat.go +++ b/common/nat/nat.go @@ -110,6 +110,9 @@ func isRestrictedMapping(addrStr string) (bool, error) { // Performs two tests from RFC 5780 to determine whether the filtering type // of the client's NAT is port-dependent. // Returns true if the filtering is port-dependent and false otherwise +// Note: This function is no longer used because a client's NAT type is +// determined only by their mapping type, but the functionality might +// be useful in the future and remains here. func isRestrictedFiltering(addrStr string) (bool, error) { var xorAddr stun.XORMappedAddress From 665d76c5b04c4e470d85a826ea617a2404ed4a1d Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 13 Nov 2020 15:08:00 -0500 Subject: [PATCH 153/385] Remove for loop around broker.Negotiate Instead of continuously polling the broker until the client receives a snowflake, fail back to the Connect() loop and try again to collect more peers after ReconnectTimeout. --- client/lib/webrtc.go | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index af5a45a..3a23ffc 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -112,7 +112,10 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel if err != nil { return err } - answer := exchangeSDP(broker, c.pc.LocalDescription()) + answer, err := broker.Negotiate(c.pc.LocalDescription()) + if err != nil { + return err + } log.Printf("Received Answer.\n") err = c.pc.SetRemoteDescription(*answer) if nil != err { @@ -217,22 +220,6 @@ func (c *WebRTCPeer) establishDataChannel() (*webrtc.DataChannel, error) { } } -// exchangeSDP sends the local SDP offer to the Broker, awaits the SDP answer, -// and returns the answer. -func exchangeSDP(broker *BrokerChannel, offer *webrtc.SessionDescription) *webrtc.SessionDescription { - // Keep trying the same offer until a valid answer arrives. - for { - // Send offer to broker (blocks). - answer, err := broker.Negotiate(offer) - if err == nil { - return answer - } - log.Printf("BrokerChannel Error: %s", err) - log.Printf("Failed to retrieve answer. Retrying in %v", ReconnectTimeout) - <-time.After(ReconnectTimeout) - } -} - // Close all channels and transports func (c *WebRTCPeer) cleanup() { // Close this side of the SOCKS pipe. From 5efcde518796e319231fd68c816e1ab74dd66129 Mon Sep 17 00:00:00 2001 From: Philipp Winter Date: Fri, 27 Nov 2020 11:04:00 -0800 Subject: [PATCH 154/385] Sort snowflake-ips stats by country count. We currently don't sort the snowflake-ips metrics: snowflake-ips CA=1,DE=1,AR=1,NL=1,FR=1,GB=2,US=4,CH=1 To facilitate eyeballing our metrics, this patch sorts snowflake-ips by value. If the value is identical, we sort by string, i.e.: snowflake-ips US=4,GB=2,AR=1,CA=1,CH=1,DE=1,FR=1,NL=1 This patch fixes tpo/anti-censorship/pluggable-transports/snowflake#40011 --- broker/metrics.go | 25 ++++++++++++++++++++++++- broker/snowflake-broker_test.go | 15 +++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/broker/metrics.go b/broker/metrics.go index d1beae2..c3ffa92 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -10,6 +10,7 @@ import ( "log" "math" "net" + "sort" "sync" "time" ) @@ -51,10 +52,32 @@ type Metrics struct { lock sync.Mutex } +type record struct { + cc string + count int +} +type records []record + +func (r records) Len() int { return len(r) } +func (r records) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r records) Less(i, j int) bool { + if r[i].count == r[j].count { + return r[i].cc > r[j].cc + } + return r[i].count < r[j].count +} + func (s CountryStats) Display() string { output := "" + + // Use the records struct to sort our counts map by value. + rs := records{} for cc, count := range s.counts { - output += fmt.Sprintf("%s=%d,", cc, count) + rs = append(rs, record{cc: cc, count: count}) + } + sort.Sort(sort.Reverse(rs)) + for _, r := range rs { + output += fmt.Sprintf("%s=%d,", r.cc, r.count) } // cut off trailing "," diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 4e8e5f0..7b87313 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -679,5 +679,20 @@ func TestMetrics(t *testing.T) { ctx.metrics.printMetrics() So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0") }) + Convey("for country stats order", func() { + + stats := map[string]int{ + "IT": 50, + "FR": 200, + "TZ": 100, + "CN": 250, + "RU": 150, + "CA": 1, + "BE": 1, + "PH": 1, + } + ctx.metrics.countryStats.counts = stats + So(ctx.metrics.countryStats.Display(), ShouldEqual, "CN=250,FR=200,RU=150,TZ=100,IT=50,BE=1,CA=1,PH=1") + }) }) } From 114df695ceff25d0213200a3368ed7a1bb1c7668 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 23 Nov 2020 14:02:54 -0500 Subject: [PATCH 155/385] Create new smux session for each SOCKS connection Each SOCKS connection has its own set of snowflakes and broker poll loop. Since the session manager was tied to a single set of snowflakes, this resulted in a bug where RedialPacketConn would sometimes try to pull snowflakes from a previously melted pool. The fix is to maintain separate smux sessions for each SOCKS connection, tied to its own snowflake pool. --- client/lib/snowflake.go | 57 +++++------------------------------------ 1 file changed, 6 insertions(+), 51 deletions(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index e888160..171e173 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -6,7 +6,6 @@ import ( "io" "log" "net" - "sync" "time" "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" @@ -92,54 +91,6 @@ func newSession(snowflakes SnowflakeCollector) (net.PacketConn, *smux.Session, e return pconn, sess, err } -// sessionManager_ maintains a single global smux.Session that is shared among -// incoming SOCKS connections. -type sessionManager_ struct { - mutex sync.Mutex - sess *smux.Session -} - -// Get creates and returns a new global smux.Session if none exists yet. If one -// already exists, it returns the existing one. It monitors the returned session -// and if it ever fails, sets things up so the next call to Get will create a -// new session. -func (manager *sessionManager_) Get(snowflakes SnowflakeCollector) (*smux.Session, error) { - manager.mutex.Lock() - defer manager.mutex.Unlock() - - if manager.sess == nil { - log.Printf("starting a new session") - pconn, sess, err := newSession(snowflakes) - if err != nil { - return nil, err - } - manager.sess = sess - go func() { - // If the session dies, set it to be recreated. - for { - <-time.After(5 * time.Second) - if sess.IsClosed() { - break - } - } - log.Printf("discarding finished session") - // Close the underlying to force any ongoing WebRTC - // connection to close as well, and relinquish the - // SnowflakeCollector. - pconn.Close() - manager.mutex.Lock() - manager.sess = nil - manager.mutex.Unlock() - }() - } else { - log.Printf("reusing the existing session") - } - - return manager.sess, nil -} - -var sessionManager = sessionManager_{} - // Given an accepted SOCKS connection, establish a WebRTC connection to the // remote peer and exchange traffic. func Handler(socks net.Conn, tongue Tongue) error { @@ -155,8 +106,9 @@ func Handler(socks net.Conn, tongue Tongue) error { log.Printf("---- Handler: begin collecting snowflakes ---") go connectLoop(snowflakes) - // Return the global smux.Session. - sess, err := sessionManager.Get(snowflakes) + // Create a new smux session + log.Printf("---- Handler: starting a new session ---") + pconn, sess, err := newSession(snowflakes) if err != nil { return err } @@ -174,6 +126,9 @@ func Handler(socks net.Conn, tongue Tongue) error { log.Printf("---- Handler: closed stream %v ---", stream.ID()) snowflakes.End() log.Printf("---- Handler: end collecting snowflakes ---") + pconn.Close() + sess.Close() + log.Printf("---- Handler: discarding finished session ---") return nil } From b9cc54b3b7dbd76d85613f0f478c95b193441564 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 29 Oct 2020 16:21:37 -0400 Subject: [PATCH 156/385] Send shutdown signal to shutdown open connections Normally all dangling goroutines are terminated when the main function exits. However, for projects that use a patched version of snowflake as a library, these goroutines continued running as long as the main function had not yet terminated. This commit has all open SOCKS connections close after receiving a shutdown signal. --- client/snowflake.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/client/snowflake.go b/client/snowflake.go index a1b97fa..a1a679e 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -27,7 +27,7 @@ const ( ) // Accept local SOCKS connections and pass them to the handler. -func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue) { +func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue, shutdown chan struct{}) { defer ln.Close() for { conn, err := ln.AcceptSocks() @@ -48,11 +48,23 @@ func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue) { return } - err = sf.Handler(conn, tongue) - if err != nil { - log.Printf("handler error: %s", err) + handler := make(chan struct{}) + go func() { + err = sf.Handler(conn, tongue) + if err != nil { + log.Printf("handler error: %s", err) + } + close(handler) return + + }() + select { + case <-shutdown: + log.Println("Received shutdown signal") + case <-handler: + log.Println("Handler ended") } + return }() } } @@ -160,6 +172,7 @@ func main() { os.Exit(1) } listeners := make([]net.Listener, 0) + shutdown := make(chan struct{}) for _, methodName := range ptInfo.MethodNames { switch methodName { case "snowflake": @@ -170,7 +183,7 @@ func main() { break } log.Printf("Started SOCKS listener at %v.", ln.Addr()) - go socksAcceptLoop(ln, dialer) + go socksAcceptLoop(ln, dialer, shutdown) pt.Cmethod(methodName, ln.Version(), ln.Addr()) listeners = append(listeners, ln) default: @@ -196,11 +209,13 @@ func main() { // Wait for a signal. <-sigChan + log.Println("stopping snowflake") // Signal received, shut down. for _, ln := range listeners { ln.Close() } + close(shutdown) log.Println("snowflake is done.") } From effc6675448a3a6e62d2784557d67e2c46e376d5 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 4 Dec 2020 10:50:00 -0500 Subject: [PATCH 157/385] Wait until all goroutines finish before shutdown --- client/snowflake.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/snowflake.go b/client/snowflake.go index a1a679e..55addc1 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -12,6 +12,7 @@ import ( "os/signal" "path/filepath" "strings" + "sync" "syscall" "time" @@ -27,7 +28,7 @@ const ( ) // Accept local SOCKS connections and pass them to the handler. -func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue, shutdown chan struct{}) { +func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue, shutdown chan struct{}, wg sync.WaitGroup) { defer ln.Close() for { conn, err := ln.AcceptSocks() @@ -40,6 +41,8 @@ func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue, shutdown chan struc } log.Printf("SOCKS accepted: %v", conn.Req) go func() { + wg.Add(1) + defer wg.Done() defer conn.Close() err := conn.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0}) @@ -173,6 +176,7 @@ func main() { } listeners := make([]net.Listener, 0) shutdown := make(chan struct{}) + var wg sync.WaitGroup for _, methodName := range ptInfo.MethodNames { switch methodName { case "snowflake": @@ -183,7 +187,7 @@ func main() { break } log.Printf("Started SOCKS listener at %v.", ln.Addr()) - go socksAcceptLoop(ln, dialer, shutdown) + go socksAcceptLoop(ln, dialer, shutdown, wg) pt.Cmethod(methodName, ln.Version(), ln.Addr()) listeners = append(listeners, ln) default: @@ -216,6 +220,7 @@ func main() { ln.Close() } close(shutdown) + wg.Wait() log.Println("snowflake is done.") } From 3e8947bfc9af1b299bc202d0252245c03ba20f11 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 4 Dec 2020 13:41:11 -0500 Subject: [PATCH 158/385] Avoid double delay in client from ReconnectTimeout Run the snowflake collection ReconnectTimeout timer in parallel to the negotiation with the broker. This way, if the broker takes a long time to respond the client doesn't have to wait the full timeout to respond. --- client/lib/snowflake.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 171e173..10a2c0d 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -136,13 +136,13 @@ func Handler(socks net.Conn, tongue Tongue) error { // transfer to the Tor SOCKS handler when needed. func connectLoop(snowflakes SnowflakeCollector) { for { + timer := time.After(ReconnectTimeout) _, err := snowflakes.Collect() if err != nil { - log.Printf("WebRTC: %v Retrying in %v...", - err, ReconnectTimeout) + log.Printf("WebRTC: %v Retrying...", err) } select { - case <-time.After(ReconnectTimeout): + case <-timer: continue case <-snowflakes.Melted(): log.Println("ConnectLoop: stopped.") From 8ec8a7cb635f1eaf36eadff06c653e73fe553817 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 16 Dec 2020 10:52:19 -0500 Subject: [PATCH 159/385] Pass lock to socksAcceptLoop by reference This fixes a bug where we were passing the lock by value to socksAcceptLoop. --- client/snowflake.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/snowflake.go b/client/snowflake.go index 55addc1..e293e73 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -28,7 +28,7 @@ const ( ) // Accept local SOCKS connections and pass them to the handler. -func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue, shutdown chan struct{}, wg sync.WaitGroup) { +func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue, shutdown chan struct{}, wg *sync.WaitGroup) { defer ln.Close() for { conn, err := ln.AcceptSocks() @@ -187,7 +187,7 @@ func main() { break } log.Printf("Started SOCKS listener at %v.", ln.Addr()) - go socksAcceptLoop(ln, dialer, shutdown, wg) + go socksAcceptLoop(ln, dialer, shutdown, &wg) pt.Cmethod(methodName, ln.Version(), ln.Addr()) listeners = append(listeners, ln) default: From f908576c604e812c70e4b4ef8d12b5ebd55f3166 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 16 Dec 2020 10:19:28 -0500 Subject: [PATCH 160/385] Increase the KCP maximum window size --- client/lib/snowflake.go | 3 +++ server/server.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 10a2c0d..2ed51a1 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -69,6 +69,9 @@ func newSession(snowflakes SnowflakeCollector) (net.PacketConn, *smux.Session, e } // Permit coalescing the payloads of consecutive sends. conn.SetStreamMode(true) + // Set the maximum send and receive window sizes to a high number + // Removes KCP bottlenecks: https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40026 + conn.SetWindowSize(65535, 65535) // Disable the dynamic congestion window (limit only by the // maximum of local and remote static windows). conn.SetNoDelay( diff --git a/server/server.go b/server/server.go index 1a53de7..3b263d0 100644 --- a/server/server.go +++ b/server/server.go @@ -338,6 +338,9 @@ func acceptSessions(ln *kcp.Listener) error { } // Permit coalescing the payloads of consecutive sends. conn.SetStreamMode(true) + // Set the maximum send and receive window sizes to a high number + // Removes KCP bottlenecks: https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40026 + conn.SetWindowSize(65535, 65535) // Disable the dynamic congestion window (limit only by the // maximum of local and remote static windows). conn.SetNoDelay( From 83c01565ef90a13b0cab390fd59d7d36da76ec1e Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 17 Dec 2020 12:25:11 -0500 Subject: [PATCH 161/385] Update webrtc library to v3.0.0 This update required two main changes to how we use the library. First, we had to make sure we created the datachannel on the offering peer side before creating the offer. Second, we had to make sure we wait for the gathering of all candidates to complete since trickle-ice is enabled by default. See the release notes for more details: https://github.com/pion/webrtc/wiki/Release-WebRTC@v3.0.0. --- client/lib/rendezvous.go | 3 +- client/lib/webrtc.go | 100 ++++++++++++------------------ client/snowflake.go | 2 +- common/util/util.go | 2 +- go.mod | 8 +-- go.sum | 130 +++++++++++++++++++++++++-------------- probetest/probetest.go | 10 ++- proxy/proxy-go_test.go | 2 +- proxy/snowflake.go | 53 ++++++++++------ 9 files changed, 174 insertions(+), 136 deletions(-) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 10853a5..32da081 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -21,7 +21,7 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/nat" "git.torproject.org/pluggable-transports/snowflake.git/common/util" - "github.com/pion/webrtc/v2" + "github.com/pion/webrtc/v3" ) const ( @@ -134,6 +134,7 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( if nil != err { return nil, err } + log.Printf("Received answer: %s", string(body)) return util.DeserializeSessionDescription(string(body)) case http.StatusServiceUnavailable: return nil, errors.New(BrokerError503) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 3a23ffc..af7ba6d 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "github.com/pion/webrtc/v2" + "github.com/pion/webrtc/v3" ) // Remote WebRTC peer. @@ -25,6 +25,7 @@ type WebRTCPeer struct { writePipe *io.PipeWriter lastReceive time.Time + open chan struct{} // Channel to notify when datachannel opens closed bool once sync.Once // Synchronization for PeerConnection destruction @@ -107,11 +108,7 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel log.Println(c.id, " connecting...") // TODO: When go-webrtc is more stable, it's possible that a new // PeerConnection won't need to be re-prepared each time. - var err error - c.pc, err = preparePeerConnection(config) - if err != nil { - return err - } + c.preparePeerConnection(config) answer, err := broker.Negotiate(c.pc.LocalDescription()) if err != nil { return err @@ -122,73 +119,42 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel log.Println("WebRTC: Unable to SetRemoteDescription:", err) return err } - c.transport, err = c.establishDataChannel() - if err != nil { - log.Printf("WebRTC: establishing data channel: %v", err) - // nolint: golint - return errors.New("WebRTC: Could not establish DataChannel") + + // Wait for the datachannel to open or time out + select { + case <-c.open: + case <-time.After(DataChannelTimeout): + c.transport.Close() + return errors.New("timeout waiting for DataChannel.OnOpen") } + go c.checkForStaleness() return nil } // preparePeerConnection creates a new WebRTC PeerConnection and returns it // after ICE candidate gathering is complete.. -func preparePeerConnection(config *webrtc.Configuration) (*webrtc.PeerConnection, error) { - pc, err := webrtc.NewPeerConnection(*config) +func (c *WebRTCPeer) preparePeerConnection(config *webrtc.Configuration) error { + var err error + c.pc, err = webrtc.NewPeerConnection(*config) if err != nil { log.Printf("NewPeerConnection ERROR: %s", err) - return nil, err + return err } - // Prepare PeerConnection callbacks. - offerChannel := make(chan struct{}) - // Allow candidates to accumulate until ICEGatheringStateComplete. - pc.OnICECandidate(func(candidate *webrtc.ICECandidate) { - if candidate == nil { - log.Printf("WebRTC: Done gathering candidates") - close(offerChannel) - } else { - log.Printf("WebRTC: Got ICE candidate: %s", candidate.String()) - } - }) - - offer, err := pc.CreateOffer(nil) - // TODO: Potentially timeout and retry if ICE isn't working. - if err != nil { - log.Println("Failed to prepare offer", err) - pc.Close() - return nil, err - } - log.Println("WebRTC: Created offer") - err = pc.SetLocalDescription(offer) - if err != nil { - log.Println("Failed to prepare offer", err) - pc.Close() - return nil, err - } - log.Println("WebRTC: Set local description") - - <-offerChannel // Wait for ICE candidate gathering to complete. - log.Println("WebRTC: PeerConnection created.") - return pc, nil -} - -// Create a WebRTC DataChannel locally. Blocks until the data channel is open, -// or a timeout or error occurs. -func (c *WebRTCPeer) establishDataChannel() (*webrtc.DataChannel, error) { ordered := true dataChannelOptions := &webrtc.DataChannelInit{ Ordered: &ordered, } + // We must create the data channel before creating an offer + // https://github.com/pion/webrtc/wiki/Release-WebRTC@v3.0.0 dc, err := c.pc.CreateDataChannel(c.id, dataChannelOptions) if err != nil { log.Printf("CreateDataChannel ERROR: %s", err) - return nil, err + return err } - openChannel := make(chan struct{}) dc.OnOpen(func() { log.Println("WebRTC: DataChannel.OnOpen") - close(openChannel) + close(c.open) }) dc.OnClose(func() { log.Println("WebRTC: DataChannel.OnClose") @@ -209,15 +175,31 @@ func (c *WebRTCPeer) establishDataChannel() (*webrtc.DataChannel, error) { } c.lastReceive = time.Now() }) + c.transport = dc + c.open = make(chan struct{}) log.Println("WebRTC: DataChannel created.") - select { - case <-openChannel: - return dc, nil - case <-time.After(DataChannelTimeout): - dc.Close() - return nil, errors.New("timeout waiting for DataChannel.OnOpen") + // Allow candidates to accumulate until ICEGatheringStateComplete. + done := webrtc.GatheringCompletePromise(c.pc) + offer, err := c.pc.CreateOffer(nil) + // TODO: Potentially timeout and retry if ICE isn't working. + if err != nil { + log.Println("Failed to prepare offer", err) + c.pc.Close() + return err } + log.Println("WebRTC: Created offer") + err = c.pc.SetLocalDescription(offer) + if err != nil { + log.Println("Failed to prepare offer", err) + c.pc.Close() + return err + } + log.Println("WebRTC: Set local description") + + <-done // Wait for ICE candidate gathering to complete. + log.Println("WebRTC: PeerConnection created.") + return nil } // Close all channels and transports diff --git a/client/snowflake.go b/client/snowflake.go index e293e73..d79de97 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -20,7 +20,7 @@ import ( sf "git.torproject.org/pluggable-transports/snowflake.git/client/lib" "git.torproject.org/pluggable-transports/snowflake.git/common/nat" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" - "github.com/pion/webrtc/v2" + "github.com/pion/webrtc/v3" ) const ( diff --git a/common/util/util.go b/common/util/util.go index b317e0b..3d2acc3 100644 --- a/common/util/util.go +++ b/common/util/util.go @@ -6,7 +6,7 @@ import ( "net" "github.com/pion/sdp/v2" - "github.com/pion/webrtc/v2" + "github.com/pion/webrtc/v3" ) func SerializeSessionDescription(desc *webrtc.SessionDescription) (string, error) { diff --git a/go.mod b/go.mod index 2ba1b2d..2931be7 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,13 @@ go 1.13 require ( git.torproject.org/pluggable-transports/goptlib.git v1.1.0 - github.com/golang/protobuf v1.3.1 // indirect github.com/gorilla/websocket v1.4.1 github.com/pion/sdp/v2 v2.3.4 github.com/pion/stun v0.3.5 - github.com/pion/webrtc/v2 v2.2.2 + github.com/pion/webrtc/v3 v3.0.0 github.com/smartystreets/goconvey v1.6.4 github.com/xtaci/kcp-go/v5 v5.5.12 github.com/xtaci/smux v1.5.12 - golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d - golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa - golang.org/x/text v0.3.2 // indirect + golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 + golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 ) diff --git a/go.sum b/go.sum index 9ccfb30..6214f5c 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,24 @@ 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= -github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= -github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= @@ -30,49 +36,56 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc= -github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= -github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA= -github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pion/datachannel v1.4.15 h1:DrizUL97f9evDyoskyMLrFFFmOWCfXFGiGSbxmQyYt4= -github.com/pion/datachannel v1.4.15/go.mod h1:yixWvOWPime7vRVuihP1GzZPBELQkO/ZM1mrBc2BNM8= -github.com/pion/dtls/v2 v2.0.0-rc.7 h1:LDAIQDt1pcuAIJs7Q2EZ3PSl8MseCFA2nCW0YYSYCx0= -github.com/pion/dtls/v2 v2.0.0-rc.7/go.mod h1:U199DvHpRBN0muE9+tVN4TMy1jvEhZIZ63lk4xkvVSk= -github.com/pion/ice v0.7.9 h1:RKol/0RFu3TIE8ZLIFV1A1e/QW22B6BZKvSG9sfawEM= -github.com/pion/ice v0.7.9/go.mod h1:8BCwuq/EqAKhtUb8CIw2fWjVLotWOu13XJY09H3RVxA= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= +github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= +github.com/pion/dtls/v2 v2.0.4 h1:WuUcqi6oYMu/noNTz92QrF1DaFj4eXbhQ6dzaaAwOiI= +github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI= +github.com/pion/ice/v2 v2.0.14 h1:FxXxauyykf89SWAtkQCfnHkno6G8+bhRkNguSh9zU+4= +github.com/pion/ice/v2 v2.0.14/go.mod h1:wqaUbOq5ObDNU5ox1hRsEst0rWfsKuH1zXjQFEWiZwM= +github.com/pion/interceptor v0.0.8 h1:qsVJv9RF7mPq/RUnUV5iZCzxwGizO880FuiFKkEGQaE= +github.com/pion/interceptor v0.0.8/go.mod h1:dHgEP5dtxOTf21MObuBAjJeAayPxLUAZjerGH8Xr07c= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= -github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA= -github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k= -github.com/pion/rtcp v1.2.1 h1:S3yG4KpYAiSmBVqKAfgRa5JdwBNj4zK3RLUa8JYdhak= -github.com/pion/rtcp v1.2.1/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM= -github.com/pion/rtp v1.3.0/go.mod h1:q9wPnA96pu2urCcW/sK/RiDn597bhGoAQQ+y2fDwHuY= -github.com/pion/rtp v1.3.2 h1:Yfzf1mU4Zmg7XWHitzYe2i+l+c68iO+wshzIUW44p1c= -github.com/pion/rtp v1.3.2/go.mod h1:q9wPnA96pu2urCcW/sK/RiDn597bhGoAQQ+y2fDwHuY= -github.com/pion/sctp v1.7.5 h1:ognJDlxP7dN2xMUEHEea5pqjdD78o5UAMcLoP1JIp1g= -github.com/pion/sctp v1.7.5/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.4/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= +github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo= +github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= +github.com/pion/rtp v1.6.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U= +github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= +github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE= +github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= github.com/pion/sdp/v2 v2.3.4 h1:+f3F5Xl7ynVhc9Il8Dc7BFroYJWG3PMbfWtwFlVI+kg= github.com/pion/sdp/v2 v2.3.4/go.mod h1:jccXVYW0fuK6ds2pwKr89SVBDYlCjhgMI6nucl5R5rA= -github.com/pion/srtp v1.2.7 h1:UYyLs5MXwbFtXWduBA5+RUWhaEBX7GmetXDZSKP+uPM= -github.com/pion/srtp v1.2.7/go.mod h1:KIgLSadhg/ioogO/LqIkRjZrwuJo0c9RvKIaGQj4Yew= -github.com/pion/stun v0.3.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw= -github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= +github.com/pion/sdp/v3 v3.0.3 h1:gJK9hk+JFD2NGIM1nXmqNCq1DkVaIZ9dlA3u3otnkaw= +github.com/pion/sdp/v3 v3.0.3/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= +github.com/pion/srtp/v2 v2.0.0-rc.3 h1:1fPiK1nJlNyh235tSGgBnXrPc99wK1/D707f6ntb3qY= +github.com/pion/srtp/v2 v2.0.0-rc.3/go.mod h1:S6J9oY6ahAXdU3ni4nUwhWTJuBfssFjPxoB0u41TBpY= github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= -github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk= github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= -github.com/pion/turn/v2 v2.0.3 h1:SJUUIbcPoehlyZgMyIUbBBDhI03sBx32x3JuSIBKBWA= -github.com/pion/turn/v2 v2.0.3/go.mod h1:kl1hmT3NxcLynpXVnwJgObL8C9NaCyPTeqI2DcCpSZs= -github.com/pion/webrtc/v2 v2.2.2 h1:ace9itTe8YND8m3lv5ndQurfk/DsChj+4pBzVJeBA04= -github.com/pion/webrtc/v2 v2.2.2/go.mod h1:oftEPcdfIvZVC1J0VP1OpyVCwB9tDkRXSYAszkL/2k4= +github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= +github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= +github.com/pion/transport v0.12.0 h1:UFmOBBZkTZ3LgvLRf/NGrfWdZEubcU6zkLU3PsA9YvU= +github.com/pion/transport v0.12.0/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= +github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= +github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= +github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= +github.com/pion/webrtc/v3 v3.0.0 h1:/eTiY3NbfpKj5op8cqtCZlpTv9/yumd17YRinDNOUX0= +github.com/pion/webrtc/v3 v3.0.0/go.mod h1:/xwKHOAk1Y8dspJcxMwuTtxpi8t/Gzks37iB3W6hNuM= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -84,10 +97,11 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY= github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= github.com/templexxx/xorsimd v0.4.1 h1:iUZcywbOYDRAZUasAs2eSCUW8eobuZDy0I9FJiORkVg= @@ -100,34 +114,55 @@ github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+A github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= github.com/xtaci/smux v1.5.12 h1:n9OGjdqQuVZXLh46+L4IR5tR2wvuUFwRABnN/V55bIY= github.com/xtaci/smux v1.5.12/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U= -golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 h1:3uJsdck53FDIpWwLeAXlia9p4C8j0BO2xZrqzKpL0D8= +golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -135,6 +170,9 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/probetest/probetest.go b/probetest/probetest.go index d952123..f9bc96b 100644 --- a/probetest/probetest.go +++ b/probetest/probetest.go @@ -24,7 +24,7 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" "git.torproject.org/pluggable-transports/snowflake.git/common/util" - "github.com/pion/webrtc/v2" + "github.com/pion/webrtc/v3" "golang.org/x/crypto/acme/autocert" ) @@ -58,7 +58,10 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, dc.Close() }) }) - + // As of v3.0.0, pion-webrtc uses trickle ICE by default. + // We have to wait for candidate gathering to complete + // before we send the offer + done := webrtc.GatheringCompletePromise(pc) err = pc.SetRemoteDescription(*sdp) if err != nil { if inerr := pc.Close(); inerr != nil { @@ -82,7 +85,8 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, } return nil, err } - + // Wait for ICE candidate gathering to complete + <-done return pc, nil } diff --git a/proxy/proxy-go_test.go b/proxy/proxy-go_test.go index e2fb82e..e935ad9 100644 --- a/proxy/proxy-go_test.go +++ b/proxy/proxy-go_test.go @@ -14,7 +14,7 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "git.torproject.org/pluggable-transports/snowflake.git/common/util" - "github.com/pion/webrtc/v2" + "github.com/pion/webrtc/v3" . "github.com/smartystreets/goconvey/convey" ) diff --git a/proxy/snowflake.go b/proxy/snowflake.go index f0fa2c0..78a053d 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -24,7 +24,7 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" "github.com/gorilla/websocket" "github.com/pion/sdp/v2" - "github.com/pion/webrtc/v2" + "github.com/pion/webrtc/v3" ) const defaultBrokerURL = "https://snowflake-broker.bamsoftware.com/" @@ -394,7 +394,10 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, go handler(conn, conn.RemoteAddr()) }) - + // As of v3.0.0, pion-webrtc uses trickle ICE by default. + // We have to wait for candidate gathering to complete + // before we send the offer + done := webrtc.GatheringCompletePromise(pc) err = pc.SetRemoteDescription(*sdp) if err != nil { if inerr := pc.Close(); inerr != nil { @@ -423,7 +426,8 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, } return nil, err } - + // Wait for ICE candidate gathering to complete + <-done return pc, nil } @@ -437,22 +441,8 @@ func makeNewPeerConnection(config webrtc.Configuration, return nil, fmt.Errorf("accept: NewPeerConnection: %s", err) } - offer, err := pc.CreateOffer(nil) - // TODO: Potentially timeout and retry if ICE isn't working. - if err != nil { - log.Println("Failed to prepare offer", err) - pc.Close() - return nil, err - } - log.Println("WebRTC: Created offer") - err = pc.SetLocalDescription(offer) - if err != nil { - log.Println("Failed to prepare offer", err) - pc.Close() - return nil, err - } - log.Println("WebRTC: Set local description") - + // Must create a data channel before creating an offer + // https://github.com/pion/webrtc/wiki/Release-WebRTC@v3.0.0 dc, err := pc.CreateDataChannel("test", &webrtc.DataChannelInit{}) if err != nil { log.Printf("CreateDataChannel ERROR: %s", err) @@ -466,6 +456,30 @@ func makeNewPeerConnection(config webrtc.Configuration, log.Println("WebRTC: DataChannel.OnClose") dc.Close() }) + + offer, err := pc.CreateOffer(nil) + // TODO: Potentially timeout and retry if ICE isn't working. + if err != nil { + log.Println("Failed to prepare offer", err) + pc.Close() + return nil, err + } + log.Println("WebRTC: Created offer") + + // As of v3.0.0, pion-webrtc uses trickle ICE by default. + // We have to wait for candidate gathering to complete + // before we send the offer + done := webrtc.GatheringCompletePromise(pc) + err = pc.SetLocalDescription(offer) + if err != nil { + log.Println("Failed to prepare offer", err) + pc.Close() + return nil, err + } + log.Println("WebRTC: Set local description") + + // Wait for ICE candidate gathering to complete + <-done return pc, nil } @@ -606,6 +620,7 @@ func checkNATType(config webrtc.Configuration, probeURL string) { offer := pc.LocalDescription() sdp, err := util.SerializeSessionDescription(offer) + log.Printf("Offer: %s", sdp) if err != nil { log.Printf("Error encoding probe message: %s", err.Error()) return From 1b29ad7de14fb0a6d2bf88aea38353733682cd26 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 25 Jan 2021 10:28:17 -0500 Subject: [PATCH 162/385] Bump version of pion/sdp Update our dependency on pion/sdp from v2 to v3, to match pion/webrtc v3. This requires some changes in how we parse out addresses from ice candidates. This will ease tor browser builds of snowflake since we are now only relying on one version of pion/sdp instead of two different ones. --- common/util/util.go | 9 +++++---- go.mod | 3 ++- go.sum | 2 -- proxy/snowflake.go | 7 ++++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/common/util/util.go b/common/util/util.go index 3d2acc3..00f7302 100644 --- a/common/util/util.go +++ b/common/util/util.go @@ -5,7 +5,8 @@ import ( "errors" "net" - "github.com/pion/sdp/v2" + "github.com/pion/ice/v2" + "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3" ) @@ -77,9 +78,9 @@ func StripLocalAddresses(str string) string { attrs := make([]sdp.Attribute, 0) for _, a := range m.Attributes { if a.IsICECandidate() { - ice, err := a.ToICECandidate() - if err == nil && ice.Typ == "host" { - ip := net.ParseIP(ice.Address) + c, err := ice.UnmarshalCandidate(a.Value) + if err == nil && c.Type() == ice.CandidateTypeHost { + ip := net.ParseIP(c.Address()) if ip != nil && (IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback()) { /* no append in this case */ continue diff --git a/go.mod b/go.mod index 2931be7..a7f9ad2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.13 require ( git.torproject.org/pluggable-transports/goptlib.git v1.1.0 github.com/gorilla/websocket v1.4.1 - github.com/pion/sdp/v2 v2.3.4 + github.com/pion/ice/v2 v2.0.14 + github.com/pion/sdp/v3 v3.0.3 github.com/pion/stun v0.3.5 github.com/pion/webrtc/v3 v3.0.0 github.com/smartystreets/goconvey v1.6.4 diff --git a/go.sum b/go.sum index 6214f5c..eac95e1 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,6 @@ github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE= github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= -github.com/pion/sdp/v2 v2.3.4 h1:+f3F5Xl7ynVhc9Il8Dc7BFroYJWG3PMbfWtwFlVI+kg= -github.com/pion/sdp/v2 v2.3.4/go.mod h1:jccXVYW0fuK6ds2pwKr89SVBDYlCjhgMI6nucl5R5rA= github.com/pion/sdp/v3 v3.0.3 h1:gJK9hk+JFD2NGIM1nXmqNCq1DkVaIZ9dlA3u3otnkaw= github.com/pion/sdp/v3 v3.0.3/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= github.com/pion/srtp/v2 v2.0.0-rc.3 h1:1fPiK1nJlNyh235tSGgBnXrPc99wK1/D707f6ntb3qY= diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 78a053d..1bc21ab 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -23,7 +23,8 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/util" "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" "github.com/gorilla/websocket" - "github.com/pion/sdp/v2" + "github.com/pion/ice/v2" + "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3" ) @@ -83,9 +84,9 @@ func remoteIPFromSDP(str string) net.IP { for _, m := range desc.MediaDescriptions { for _, a := range m.Attributes { if a.IsICECandidate() { - ice, err := a.ToICECandidate() + c, err := ice.UnmarshalCandidate(a.Value) if err == nil { - ip := net.ParseIP(ice.Address) + ip := net.ParseIP(c.Address()) if ip != nil && isRemoteAddress(ip) { return ip } From bae0bacbfdcd7d195e5ecff985bbff2361937170 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Fri, 15 Jan 2021 11:50:56 -0500 Subject: [PATCH 163/385] Classify proxies with unknown NATs as restricted --- broker/broker.go | 12 ++++++------ broker/snowflake-broker_test.go | 17 +++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index 983f95d..b8c7b6c 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -144,10 +144,10 @@ func (ctx *BrokerContext) Broker() { ctx.snowflakeLock.Lock() defer ctx.snowflakeLock.Unlock() if snowflake.index != -1 { - if request.natType == NATRestricted { - heap.Remove(ctx.restrictedSnowflakes, snowflake.index) - } else { + if request.natType == NATUnrestricted { heap.Remove(ctx.snowflakes, snowflake.index) + } else { + heap.Remove(ctx.restrictedSnowflakes, snowflake.index) } delete(ctx.idToSnowflake, snowflake.id) close(request.offerChannel) @@ -169,10 +169,10 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri snowflake.offerChannel = make(chan *ClientOffer) snowflake.answerChannel = make(chan []byte) ctx.snowflakeLock.Lock() - if natType == NATRestricted { - heap.Push(ctx.restrictedSnowflakes, snowflake) - } else { + if natType == NATUnrestricted { heap.Push(ctx.snowflakes, snowflake) + } else { + heap.Push(ctx.restrictedSnowflakes, snowflake) } ctx.snowflakeLock.Unlock() ctx.idToSnowflake[id] = snowflake diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 7b87313..3b59a0f 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -29,7 +29,7 @@ func TestBroker(t *testing.T) { Convey("Adds Snowflake", func() { So(ctx.snowflakes.Len(), ShouldEqual, 0) So(len(ctx.idToSnowflake), ShouldEqual, 0) - ctx.AddSnowflake("foo", "", NATUnknown) + ctx.AddSnowflake("foo", "", NATUnrestricted) So(ctx.snowflakes.Len(), ShouldEqual, 1) So(len(ctx.idToSnowflake), ShouldEqual, 1) }) @@ -37,6 +37,7 @@ func TestBroker(t *testing.T) { Convey("Broker goroutine matches clients with proxies", func() { p := new(ProxyPoll) p.id = "test" + p.natType = "unrestricted" p.offerChannel = make(chan *ClientOffer) go func(ctx *BrokerContext) { ctx.proxyPolls <- p @@ -55,7 +56,7 @@ func TestBroker(t *testing.T) { Convey("Request an offer from the Snowflake Heap", func() { done := make(chan *ClientOffer) go func() { - offer := ctx.RequestOffer("test", "", NATUnknown) + offer := ctx.RequestOffer("test", "", NATUnrestricted) done <- offer }() request := <-ctx.proxyPolls @@ -79,7 +80,7 @@ func TestBroker(t *testing.T) { Convey("with a proxy answer if available.", func() { done := make(chan bool) // Prepare a fake proxy to respond with. - snowflake := ctx.AddSnowflake("fake", "", NATUnknown) + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted) go func() { clientOffers(ctx, w, r) done <- true @@ -97,7 +98,7 @@ func TestBroker(t *testing.T) { return } done := make(chan bool) - snowflake := ctx.AddSnowflake("fake", "", NATUnknown) + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted) go func() { clientOffers(ctx, w, r) // Takes a few seconds here... @@ -147,7 +148,7 @@ func TestBroker(t *testing.T) { }) Convey("Responds to proxy answers...", func() { - s := ctx.AddSnowflake("test", "", NATUnknown) + s := ctx.AddSnowflake("test", "", NATUnrestricted) w := httptest.NewRecorder() data := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"test","Answer":"test"}`)) @@ -260,7 +261,7 @@ func TestBroker(t *testing.T) { // Manually do the Broker goroutine action here for full control. p := <-ctx.proxyPolls So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") - s := ctx.AddSnowflake(p.id, "", NATUnknown) + s := ctx.AddSnowflake(p.id, "", NATUnrestricted) go func() { offer := <-s.offerChannel p.offerChannel <- offer @@ -437,7 +438,7 @@ func TestGeoip(t *testing.T) { if err := ctx.metrics.LoadGeoipDatabases("invalid_filename", "invalid_filename6"); err != nil { log.Printf("loading geo ip databases returned error: %v", err) } - ctx.metrics.UpdateCountryStats("127.0.0.1", "", NATUnknown) + ctx.metrics.UpdateCountryStats("127.0.0.1", "", NATUnrestricted) So(ctx.metrics.tablev4, ShouldEqual, nil) }) @@ -537,7 +538,7 @@ func TestMetrics(t *testing.T) { So(err, ShouldBeNil) // Prepare a fake proxy to respond with. - snowflake := ctx.AddSnowflake("fake", "", NATUnknown) + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted) go func() { clientOffers(ctx, w, r) done <- true From 7187f1009ef7aaae6aa557fe1f724aa1df718b24 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 25 Jan 2021 13:01:37 -0500 Subject: [PATCH 164/385] Log a throughput summary for each connection This will increase transparency for people running standalone proxies and help us debug any potential issues with proxies behaving unreliably. --- proxy/snowflake.go | 6 ++++ proxy/util.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 proxy/util.go diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 1bc21ab..86ae0b2 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -118,6 +118,8 @@ type webRTCConn struct { lock sync.Mutex // Synchronization for DataChannel destruction once sync.Once // Synchronization for PeerConnection destruction + + bytesLogger BytesLogger } func (c *webRTCConn) Read(b []byte) (int, error) { @@ -125,6 +127,7 @@ func (c *webRTCConn) Read(b []byte) (int, error) { } func (c *webRTCConn) Write(b []byte) (int, error) { + c.bytesLogger.AddInbound(len(b)) c.lock.Lock() defer c.lock.Unlock() if c.dc != nil { @@ -368,6 +371,7 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, pr, pw := io.Pipe() conn := &webRTCConn{pc: pc, dc: dc, pr: pr} + conn.bytesLogger = NewBytesSyncLogger() dc.OnOpen(func() { log.Println("OnOpen channel") @@ -376,6 +380,7 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, conn.lock.Lock() defer conn.lock.Unlock() log.Println("OnClose channel") + log.Println(conn.bytesLogger.ThroughputSummary()) conn.dc = nil dc.Close() pw.Close() @@ -388,6 +393,7 @@ func makePeerConnectionFromOffer(sdp *webrtc.SessionDescription, log.Printf("close with error generated an error: %v", inerr) } } + conn.bytesLogger.AddOutbound(n) if n != len(msg.Data) { panic("short write") } diff --git a/proxy/util.go b/proxy/util.go new file mode 100644 index 0000000..d737056 --- /dev/null +++ b/proxy/util.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "time" +) + +type BytesLogger interface { + AddOutbound(int) + AddInbound(int) + ThroughputSummary() string +} + +// Default BytesLogger does nothing. +type BytesNullLogger struct{} + +func (b BytesNullLogger) AddOutbound(amount int) {} +func (b BytesNullLogger) AddInbound(amount int) {} +func (b BytesNullLogger) ThroughputSummary() string { return "" } + +// BytesSyncLogger uses channels to safely log from multiple sources with output +// occuring at reasonable intervals. +type BytesSyncLogger struct { + outboundChan, inboundChan chan int + outbound, inbound, outEvents, inEvents int + start time.Time +} + +// NewBytesSyncLogger returns a new BytesSyncLogger and starts it loggin. +func NewBytesSyncLogger() *BytesSyncLogger { + b := &BytesSyncLogger{ + outboundChan: make(chan int, 5), + inboundChan: make(chan int, 5), + } + go b.log() + b.start = time.Now() + return b +} + +func (b *BytesSyncLogger) log() { + for { + select { + case amount := <-b.outboundChan: + b.outbound += amount + b.outEvents++ + case amount := <-b.inboundChan: + b.inbound += amount + b.inEvents++ + } + } +} + +func (b *BytesSyncLogger) AddOutbound(amount int) { + b.outboundChan <- amount +} + +func (b *BytesSyncLogger) AddInbound(amount int) { + b.inboundChan <- amount +} + +func (b *BytesSyncLogger) ThroughputSummary() string { + var inUnit, outUnit string + units := []string{"B", "KB", "MB", "GB"} + + inbound := b.inbound + outbound := b.outbound + + for i, u := range units { + inUnit = u + if (inbound < 1000) || (i == len(units)-1) { + break + } + inbound = inbound / 1000 + } + for i, u := range units { + outUnit = u + if (outbound < 1000) || (i == len(units)-1) { + break + } + outbound = outbound / 1000 + } + t := time.Now() + return fmt.Sprintf("Traffic throughput (up|down): %d %s|%d %s -- (%d OnMessages, %d Sends, over %d seconds)", inbound, inUnit, outbound, outUnit, b.outEvents, b.inEvents, int(t.Sub(b.start).Seconds())) +} From 850d2f0683ede3d24a2b907161b6d88b32bed24a Mon Sep 17 00:00:00 2001 From: David Fifield Date: Fri, 5 Mar 2021 23:26:35 -0700 Subject: [PATCH 165/385] Update required Go version to 1.13 in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9be45b..6d47012 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ This should start the client plugin, bootstrapping to 100% using WebRTC. Client: - [pion/webrtc](https://github.com/pion/webrtc) -- Go 1.10+ +- Go 1.13+ --- From 720d2b8eb7be9e2a41126624083054a66017d452 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 8 Mar 2021 21:50:42 -0500 Subject: [PATCH 166/385] Don't log io.ErrClosedPipe in server These errors are triggered in three places when the OR connection times out. They don't tell us anything useful and are filling up our logs. --- server/server.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/server.go b/server/server.go index 3b263d0..b3fa04a 100644 --- a/server/server.go +++ b/server/server.go @@ -73,7 +73,7 @@ func proxy(local *net.TCPConn, conn net.Conn) { wg.Add(2) go func() { - if _, err := io.Copy(conn, local); err != nil { + if _, err := io.Copy(conn, local); err != nil && err != io.ErrClosedPipe { log.Printf("error copying ORPort to WebSocket %v", err) } if err := local.CloseRead(); err != nil { @@ -83,7 +83,7 @@ func proxy(local *net.TCPConn, conn net.Conn) { wg.Done() }() go func() { - if _, err := io.Copy(local, conn); err != nil { + if _, err := io.Copy(local, conn); err != nil && err != io.ErrClosedPipe { log.Printf("error copying WebSocket to ORPort %v", err) } if err := local.CloseWrite(); err != nil { @@ -352,7 +352,7 @@ func acceptSessions(ln *kcp.Listener) error { go func() { defer conn.Close() err := acceptStreams(conn) - if err != nil { + if err != nil && err != io.ErrClosedPipe { log.Printf("acceptStreams: %v", err) } }() From c0b6e082f2f30cbeca962937ea5a777b98cf3ebb Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 8 Mar 2021 22:16:33 -0500 Subject: [PATCH 167/385] Don't log errors from callng close on OR conns Snowflake copies data between the OR connection and the KCP stream, meaning that in most cases the copy loops will only terminate once the OR connection times out. In this case the OR connection is already closed and so calls to CloseRead and CloseWrite will generate errors. --- server/server.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server/server.go b/server/server.go index b3fa04a..620cd50 100644 --- a/server/server.go +++ b/server/server.go @@ -76,9 +76,7 @@ func proxy(local *net.TCPConn, conn net.Conn) { if _, err := io.Copy(conn, local); err != nil && err != io.ErrClosedPipe { log.Printf("error copying ORPort to WebSocket %v", err) } - if err := local.CloseRead(); err != nil { - log.Printf("error closing read after copying ORPort to WebSocket %v", err) - } + local.CloseRead() conn.Close() wg.Done() }() @@ -86,9 +84,7 @@ func proxy(local *net.TCPConn, conn net.Conn) { if _, err := io.Copy(local, conn); err != nil && err != io.ErrClosedPipe { log.Printf("error copying WebSocket to ORPort %v", err) } - if err := local.CloseWrite(); err != nil { - log.Printf("error closing write after copying WebSocket to ORPort %v", err) - } + local.CloseWrite() conn.Close() wg.Done() }() From 087a037f82d7088c253936f5450c933867b10b2d Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 18 Mar 2021 23:08:05 -0400 Subject: [PATCH 168/385] Update webrtc library to v3.0.15 This fixes a vulnerability in the library: CVE-2021-28681 --- go.mod | 13 ++++++++----- go.sum | 56 ++++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index a7f9ad2..844f523 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,17 @@ go 1.13 require ( git.torproject.org/pluggable-transports/goptlib.git v1.1.0 + github.com/google/uuid v1.2.0 // indirect github.com/gorilla/websocket v1.4.1 - github.com/pion/ice/v2 v2.0.14 - github.com/pion/sdp/v3 v3.0.3 + github.com/pion/ice/v2 v2.0.15 + github.com/pion/sdp/v3 v3.0.4 github.com/pion/stun v0.3.5 - github.com/pion/webrtc/v3 v3.0.0 + github.com/pion/transport v0.12.3 // indirect + github.com/pion/webrtc/v3 v3.0.15 github.com/smartystreets/goconvey v1.6.4 github.com/xtaci/kcp-go/v5 v5.5.12 github.com/xtaci/smux v1.5.12 - golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 - golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 + golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 + golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 + golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e // indirect ) diff --git a/go.sum b/go.sum index eac95e1..34a8c0e 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,10 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I= +github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= @@ -47,43 +49,46 @@ github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXm github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= github.com/pion/dtls/v2 v2.0.4 h1:WuUcqi6oYMu/noNTz92QrF1DaFj4eXbhQ6dzaaAwOiI= github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI= -github.com/pion/ice/v2 v2.0.14 h1:FxXxauyykf89SWAtkQCfnHkno6G8+bhRkNguSh9zU+4= -github.com/pion/ice/v2 v2.0.14/go.mod h1:wqaUbOq5ObDNU5ox1hRsEst0rWfsKuH1zXjQFEWiZwM= -github.com/pion/interceptor v0.0.8 h1:qsVJv9RF7mPq/RUnUV5iZCzxwGizO880FuiFKkEGQaE= -github.com/pion/interceptor v0.0.8/go.mod h1:dHgEP5dtxOTf21MObuBAjJeAayPxLUAZjerGH8Xr07c= +github.com/pion/dtls/v2 v2.0.8 h1:reGe8rNIMfO/UAeFLqO61tl64t154Qfkr4U3Gzu1tsg= +github.com/pion/dtls/v2 v2.0.8/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10= +github.com/pion/ice/v2 v2.0.15 h1:KZrwa2ciL9od8+TUVJiYTNsCW9J5lktBjGwW1MacEnQ= +github.com/pion/ice/v2 v2.0.15/go.mod h1:ZIiVGevpgAxF/cXiIVmuIUtCb3Xs4gCzCbXB6+nFkSI= +github.com/pion/interceptor v0.0.10 h1:dXFyFWRJFwmzQqyn0U8dUAbOJu+JJnMVAqxmvTu30B4= +github.com/pion/interceptor v0.0.10/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.4/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo= github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= -github.com/pion/rtp v1.6.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U= github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE= github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= -github.com/pion/sdp/v3 v3.0.3 h1:gJK9hk+JFD2NGIM1nXmqNCq1DkVaIZ9dlA3u3otnkaw= -github.com/pion/sdp/v3 v3.0.3/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= -github.com/pion/srtp/v2 v2.0.0-rc.3 h1:1fPiK1nJlNyh235tSGgBnXrPc99wK1/D707f6ntb3qY= -github.com/pion/srtp/v2 v2.0.0-rc.3/go.mod h1:S6J9oY6ahAXdU3ni4nUwhWTJuBfssFjPxoB0u41TBpY= +github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8= +github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= +github.com/pion/srtp/v2 v2.0.2 h1:664iGzVmaY7KYS5M0gleY0DscRo9ReDfTxQrq4UgGoU= +github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0= github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk= github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= -github.com/pion/transport v0.12.0 h1:UFmOBBZkTZ3LgvLRf/NGrfWdZEubcU6zkLU3PsA9YvU= -github.com/pion/transport v0.12.0/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/transport v0.12.1/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/transport v0.12.2 h1:WYEjhloRHt1R86LhUKjC5y+P52Y11/QqEUalvtzVoys= +github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw= +github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= -github.com/pion/webrtc/v3 v3.0.0 h1:/eTiY3NbfpKj5op8cqtCZlpTv9/yumd17YRinDNOUX0= -github.com/pion/webrtc/v3 v3.0.0/go.mod h1:/xwKHOAk1Y8dspJcxMwuTtxpi8t/Gzks37iB3W6hNuM= +github.com/pion/webrtc/v3 v3.0.15 h1:g8MMJohjQoj0+pTrU329tWM6dvCieNTgnjtqv1kmEdY= +github.com/pion/webrtc/v3 v3.0.15/go.mod h1:uUt2nRSsCnK/nfzTAfOmaeLan26ZJ0aP9iwjc/gcC2Y= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -100,6 +105,8 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY= github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= github.com/templexxx/xorsimd v0.4.1 h1:iUZcywbOYDRAZUasAs2eSCUW8eobuZDy0I9FJiORkVg= @@ -117,6 +124,10 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo= +golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -129,6 +140,11 @@ golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 h1:3uJsdck53FDIpWwLeAXlia9p4C8j0BO2xZrqzKpL0D8= golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -137,6 +153,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -144,6 +161,13 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M= +golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From 196c230ac7e6e2d6be7c00d2c3f90e11d23b21b7 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 2 Feb 2021 16:11:29 -0500 Subject: [PATCH 169/385] Update Go version for .gitlab-ci.yml --- .gitlab-ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2ac0aa0..7288b5f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -68,6 +68,7 @@ android: image: registry.gitlab.com/fdroid/ci-images-client variables: GOPATH: "/go" + ANDROID_VERSION: 29 cache: paths: - .gradle/wrapper @@ -79,12 +80,12 @@ android: gnupg wget - cd /usr/local - - export gotarball="go1.13.12.linux-amd64.tar.gz" + - export gotarball="go1.15.10.linux-amd64.tar.gz" - wget -q https://dl.google.com/go/${gotarball} - wget -q https://dl.google.com/go/${gotarball}.asc - curl https://dl.google.com/linux/linux_signing_key.pub | gpg --import - gpg --verify ${gotarball}.asc - - echo "9cacc6653563771b458c13056265aa0c21b8a23ca9408278484e4efde4160618 ${gotarball}" | sha256sum -c + - echo "4aa1267517df32f2bf1cc3d55dfc27d0c6b2c2b0989449c96dd19273ccca051d ${gotarball}" | sha256sum -c - tar -xzf ${gotarball} - export PATH="/usr/local/go/bin:$GOPATH/bin:$PATH" # putting this in 'variables:' cause weird runner errors - cd $CI_PROJECT_DIR @@ -99,6 +100,7 @@ android: - go install golang.org/x/mobile/cmd/gomobile - go install golang.org/x/mobile/cmd/gobind - echo y | $ANDROID_HOME/tools/bin/sdkmanager 'ndk-bundle' > /dev/null + - echo y | $ANDROID_HOME/tools/bin/sdkmanager "platforms;android-${ANDROID_VERSION}" > /dev/null - gomobile init - git -C $CI_PROJECT_DIR reset --hard From eff73c3016ec259918e117665833df04f1755e80 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 1 Apr 2021 11:29:52 -0400 Subject: [PATCH 170/385] Switch front domain and host to fastly --- client/torrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/torrc b/client/torrc index 813d22d..7a1dbdf 100644 --- a/client/torrc +++ b/client/torrc @@ -2,8 +2,8 @@ UseBridges 1 DataDirectory datadir ClientTransportPlugin snowflake exec ./client \ --url https://snowflake-broker.azureedge.net/ \ --front ajax.aspnetcdn.com \ +-url https://snowflake-broker.torproject.net.global.prod.fastly.net/ \ +-front cdn.sstatic.net \ -ice stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 \ -max 3 From 83ef0b6f6de83e877caf455f17732acf8eb9b232 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 30 Mar 2021 15:40:02 -0400 Subject: [PATCH 171/385] Export snowflake broker metrics for prometheus This change adds a prometheus exporter for our existing snowflake broker metrics. Current values for the metrics can be fetched by sending a GET request to /prometheus. --- broker/broker.go | 9 + broker/metrics.go | 56 +++++- broker/snowflake-broker_test.go | 10 + go.mod | 1 + go.sum | 344 ++++++++++++++++++++++++++++++++ 5 files changed, 418 insertions(+), 2 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index b8c7b6c..b29ebd4 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -24,6 +24,8 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/crypto/acme/autocert" ) @@ -212,6 +214,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { if nil == offer { ctx.metrics.lock.Lock() ctx.metrics.proxyIdleCount++ + promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "idle"}).Inc() ctx.metrics.lock.Unlock() b, err = messages.EncodePollResponse("", false, "") @@ -223,6 +226,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { w.Write(b) return } + promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "matched"}).Inc() b, err = messages.EncodePollResponse(string(offer.sdp), true, offer.natType) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -276,6 +280,7 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { if numSnowflakes <= 0 { ctx.metrics.lock.Lock() ctx.metrics.clientDeniedCount++ + promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "denied"}).Inc() if offer.natType == NATUnrestricted { ctx.metrics.clientUnrestrictedDeniedCount++ } else { @@ -297,6 +302,7 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { case answer := <-snowflake.answerChannel: ctx.metrics.lock.Lock() ctx.metrics.clientProxyMatchCount++ + promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "matched"}).Inc() ctx.metrics.lock.Unlock() if _, err := w.Write(answer); err != nil { log.Printf("unable to write answer with error: %v", err) @@ -497,6 +503,9 @@ func main() { http.Handle("/answer", SnowflakeHandler{ctx, proxyAnswers}) http.Handle("/debug", SnowflakeHandler{ctx, debugHandler}) http.Handle("/metrics", MetricsHandler{metricsFilename, metricsHandler}) + http.Handle("/prometheus", promhttp.Handler()) + + InitPrometheus() server := http.Server{ Addr: addr, diff --git a/broker/metrics.go b/broker/metrics.go index c3ffa92..6939742 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -13,13 +13,20 @@ import ( "sort" "sync" "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" ) var ( - once sync.Once + once sync.Once + promMetrics *PromMetrics ) -const metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds +const ( + PrometheusNamespace = "snowflake" + metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds +) type CountryStats struct { standalone map[string]bool @@ -140,6 +147,11 @@ func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType stri } else { m.countryStats.unknown[addr] = true } + promMetrics.ProxyTotal.With(prometheus.Labels{ + "nat": natType, + "type": proxyType, + "cc": country, + }).Inc() switch natType { case NATRestricted: @@ -246,3 +258,43 @@ func (m *Metrics) zeroMetrics() { func binCount(count uint) uint { return uint((math.Ceil(float64(count) / 8)) * 8) } + +type PromMetrics struct { + ProxyTotal *prometheus.CounterVec + ProxyPollTotal *prometheus.CounterVec + ClientPollTotal *prometheus.CounterVec +} + +//Initialize metrics for prometheus exporter +func InitPrometheus() { + + promMetrics = &PromMetrics{} + + promMetrics.ProxyTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: PrometheusNamespace, + Name: "proxy_total", + Help: "The number of unique snowflake IPs", + }, + []string{"type", "nat", "cc"}, + ) + + promMetrics.ProxyPollTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: PrometheusNamespace, + Name: "proxy_poll_total", + Help: "The number of snowflake proxy polls", + }, + []string{"nat", "status"}, + ) + + promMetrics.ClientPollTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: PrometheusNamespace, + Name: "client_poll_total", + Help: "The number of snowflake client polls", + }, + []string{"nat", "status"}, + ) + +} diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 3b59a0f..987aae8 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -9,6 +9,7 @@ import ( "net/http" "net/http/httptest" "os" + "sync" "testing" "time" @@ -21,8 +22,12 @@ func NullLogger() *log.Logger { return logger } +var promOnce sync.Once + func TestBroker(t *testing.T) { + promOnce.Do(InitPrometheus) + Convey("Context", t, func() { ctx := NewBrokerContext(NullLogger()) @@ -298,6 +303,8 @@ func TestBroker(t *testing.T) { } func TestSnowflakeHeap(t *testing.T) { + promOnce.Do(InitPrometheus) + Convey("SnowflakeHeap", t, func() { h := new(SnowflakeHeap) heap.Init(h) @@ -341,6 +348,8 @@ func TestSnowflakeHeap(t *testing.T) { } func TestGeoip(t *testing.T) { + promOnce.Do(InitPrometheus) + Convey("Geoip", t, func() { tv4 := new(GeoIPv4Table) err := GeoIPLoadFile(tv4, "test_geoip") @@ -445,6 +454,7 @@ func TestGeoip(t *testing.T) { } func TestMetrics(t *testing.T) { + promOnce.Do(InitPrometheus) Convey("Test metrics...", t, func() { done := make(chan bool) diff --git a/go.mod b/go.mod index 844f523..ab3dc96 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/pion/stun v0.3.5 github.com/pion/transport v0.12.3 // indirect github.com/pion/webrtc/v3 v3.0.15 + github.com/prometheus/client_golang v1.10.0 github.com/smartystreets/goconvey v1.6.4 github.com/xtaci/kcp-go/v5 v5.5.12 github.com/xtaci/smux v1.5.12 diff --git a/go.sum b/go.sum index 34a8c0e..8067425 100644 --- a/go.sum +++ b/go.sum @@ -1,50 +1,226 @@ +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= +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= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I= github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid v1.2.2 h1:1xAgYebNnsb9LKCdLOvFWtAxGU/33mjJtyOVbmUa0Us= github.com/klauspost/cpuid v1.2.2/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY= github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= github.com/pion/dtls/v2 v2.0.4 h1:WuUcqi6oYMu/noNTz92QrF1DaFj4eXbhQ6dzaaAwOiI= @@ -89,17 +265,68 @@ github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= github.com/pion/webrtc/v3 v3.0.15 h1:g8MMJohjQoj0+pTrU329tWM6dvCieNTgnjtqv1kmEdY= github.com/pion/webrtc/v3 v3.0.15/go.mod h1:uUt2nRSsCnK/nfzTAfOmaeLan26ZJ0aP9iwjc/gcC2Y= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.10.0 h1:/o0BDeWzLWXNZ+4q5gXltUvaMpJqckTa+jTNoB+z4cg= +github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.18.0 h1:WCVKW7aL6LEe1uryfI9dnEc2ZqNB1Fn0ok930v0iL1Y= +github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -113,13 +340,34 @@ github.com/templexxx/xorsimd v0.4.1 h1:iUZcywbOYDRAZUasAs2eSCUW8eobuZDy0I9FJiORk github.com/templexxx/xorsimd v0.4.1/go.mod h1:W+ffZz8jJMH2SXwuKu9WhygqBMbFnp14G2fqEr8qaNo= github.com/tjfoc/gmsm v1.0.1 h1:R11HlqhXkDospckjZEihx9SW/2VW0RgdwrykyWMFOQU= github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xtaci/kcp-go/v5 v5.5.12 h1:iALGyvti/oBbl1TbVoUpHEUHCorDEb3tEKl1CPY3KXM= github.com/xtaci/kcp-go/v5 v5.5.12/go.mod h1:H0T/EJ+lPNytnFYsKLH0JHUtiwZjG3KXlTM6c+Q4YUo= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= github.com/xtaci/smux v1.5.12 h1:n9OGjdqQuVZXLh46+L4IR5tR2wvuUFwRABnN/V55bIY= github.com/xtaci/smux v1.5.12/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= @@ -128,9 +376,30 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHR golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo= golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -145,24 +414,49 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxW golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M= golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -173,28 +467,78 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= From 92bd900bc57f1d56c21c5abf736deb6ce3a83837 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 31 Mar 2021 10:52:01 -0400 Subject: [PATCH 172/385] Implement binned counts for polling metrics --- broker/metrics.go | 38 +++++++++------ broker/prometheus.go | 83 +++++++++++++++++++++++++++++++++ broker/snowflake-broker_test.go | 8 ---- go.mod | 2 + 4 files changed, 108 insertions(+), 23 deletions(-) create mode 100644 broker/prometheus.go diff --git a/broker/metrics.go b/broker/metrics.go index 6939742..24ff9b0 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -20,11 +20,11 @@ import ( var ( once sync.Once - promMetrics *PromMetrics + promMetrics = initPrometheus() ) const ( - PrometheusNamespace = "snowflake" + prometheusNamespace = "snowflake" metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds ) @@ -147,6 +147,7 @@ func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType stri } else { m.countryStats.unknown[addr] = true } + promMetrics.ProxyTotal.With(prometheus.Labels{ "nat": natType, "type": proxyType, @@ -261,40 +262,47 @@ func binCount(count uint) uint { type PromMetrics struct { ProxyTotal *prometheus.CounterVec - ProxyPollTotal *prometheus.CounterVec - ClientPollTotal *prometheus.CounterVec + ProxyPollTotal *RoundedCounterVec + ClientPollTotal *RoundedCounterVec } //Initialize metrics for prometheus exporter -func InitPrometheus() { +func initPrometheus() *PromMetrics { - promMetrics = &PromMetrics{} + promMetrics := &PromMetrics{} promMetrics.ProxyTotal = promauto.NewCounterVec( prometheus.CounterOpts{ - Namespace: PrometheusNamespace, + Namespace: prometheusNamespace, Name: "proxy_total", Help: "The number of unique snowflake IPs", }, []string{"type", "nat", "cc"}, ) - promMetrics.ProxyPollTotal = promauto.NewCounterVec( + promMetrics.ProxyPollTotal = NewRoundedCounterVec( prometheus.CounterOpts{ - Namespace: PrometheusNamespace, - Name: "proxy_poll_total", - Help: "The number of snowflake proxy polls", + Namespace: prometheusNamespace, + Name: "rounded_proxy_poll_total", + Help: "The number of snowflake proxy polls, rounded up to a multiple of 8", }, []string{"nat", "status"}, ) - promMetrics.ClientPollTotal = promauto.NewCounterVec( + promMetrics.ClientPollTotal = NewRoundedCounterVec( prometheus.CounterOpts{ - Namespace: PrometheusNamespace, - Name: "client_poll_total", - Help: "The number of snowflake client polls", + Namespace: prometheusNamespace, + Name: "rounded_client_poll_total", + Help: "The number of snowflake client polls, rounded up to a multiple of 8", }, []string{"nat", "status"}, ) + // We need to register this new metric type because there is no constructor + // for it in promauto. + prometheus.DefaultRegisterer.MustRegister(promMetrics.ClientPollTotal) + prometheus.DefaultRegisterer.MustRegister(promMetrics.ProxyPollTotal) + + return promMetrics + } diff --git a/broker/prometheus.go b/broker/prometheus.go new file mode 100644 index 0000000..d7592ec --- /dev/null +++ b/broker/prometheus.go @@ -0,0 +1,83 @@ +/* +Implements some additional prometheus metrics that we need for privacy preserving +counts of users and proxies +*/ + +package main + +import ( + "sync/atomic" + + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "google.golang.org/protobuf/proto" +) + +// New Prometheus counter type that produces rounded counts of metrics +// for privacy preserving reasons +type RoundedCounter interface { + prometheus.Metric + + Inc() +} + +type roundedCounter struct { + total uint64 //reflects the true count + value uint64 //reflects the rounded count + + desc *prometheus.Desc + labelPairs []*dto.LabelPair +} + +// Implements the RoundedCounter interface +func (c *roundedCounter) Inc() { + atomic.AddUint64(&c.total, 1) + if c.total > c.value { + atomic.AddUint64(&c.value, 8) + } +} + +// Implements the prometheus.Metric interface +func (c *roundedCounter) Desc() *prometheus.Desc { + return c.desc +} + +// Implements the prometheus.Metric interface +func (c *roundedCounter) Write(m *dto.Metric) error { + m.Label = c.labelPairs + + m.Counter = &dto.Counter{Value: proto.Float64(float64(c.value))} + return nil +} + +// New prometheus vector type that will track RoundedCounter metrics +// accross multiple labels +type RoundedCounterVec struct { + *prometheus.MetricVec +} + +func NewRoundedCounterVec(opts prometheus.CounterOpts, labelNames []string) *RoundedCounterVec { + desc := prometheus.NewDesc( + prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), + opts.Help, + labelNames, + opts.ConstLabels, + ) + return &RoundedCounterVec{ + MetricVec: prometheus.NewMetricVec(desc, func(lvs ...string) prometheus.Metric { + if len(lvs) != len(labelNames) { + panic("inconsistent cardinality") + } + return &roundedCounter{desc: desc, labelPairs: prometheus.MakeLabelPairs(desc, lvs)} + }), + } +} + +// Helper function to return the underlying RoundedCounter metric from MetricVec +func (v *RoundedCounterVec) With(labels prometheus.Labels) RoundedCounter { + metric, err := v.GetMetricWith(labels) + if err != nil { + panic(err) + } + return metric.(RoundedCounter) +} diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 987aae8..b676b04 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -26,8 +26,6 @@ var promOnce sync.Once func TestBroker(t *testing.T) { - promOnce.Do(InitPrometheus) - Convey("Context", t, func() { ctx := NewBrokerContext(NullLogger()) @@ -303,8 +301,6 @@ func TestBroker(t *testing.T) { } func TestSnowflakeHeap(t *testing.T) { - promOnce.Do(InitPrometheus) - Convey("SnowflakeHeap", t, func() { h := new(SnowflakeHeap) heap.Init(h) @@ -348,8 +344,6 @@ func TestSnowflakeHeap(t *testing.T) { } func TestGeoip(t *testing.T) { - promOnce.Do(InitPrometheus) - Convey("Geoip", t, func() { tv4 := new(GeoIPv4Table) err := GeoIPLoadFile(tv4, "test_geoip") @@ -454,8 +448,6 @@ func TestGeoip(t *testing.T) { } func TestMetrics(t *testing.T) { - promOnce.Do(InitPrometheus) - Convey("Test metrics...", t, func() { done := make(chan bool) buf := new(bytes.Buffer) diff --git a/go.mod b/go.mod index ab3dc96..ed07394 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,12 @@ require ( github.com/pion/transport v0.12.3 // indirect github.com/pion/webrtc/v3 v3.0.15 github.com/prometheus/client_golang v1.10.0 + github.com/prometheus/client_model v0.2.0 github.com/smartystreets/goconvey v1.6.4 github.com/xtaci/kcp-go/v5 v5.5.12 github.com/xtaci/smux v1.5.12 golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e // indirect + google.golang.org/protobuf v1.23.0 ) From 2a310682b51b3da514d7e1927aafcdae9b9c8820 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 31 Mar 2021 17:22:31 -0400 Subject: [PATCH 173/385] Add new gauge to show currently available proxies --- broker/broker.go | 3 +++ broker/metrics.go | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index b29ebd4..77c62d8 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -151,6 +151,7 @@ func (ctx *BrokerContext) Broker() { } else { heap.Remove(ctx.restrictedSnowflakes, snowflake.index) } + promMetrics.AvailableProxies.With(prometheus.Labels{"nat": request.natType, "type": request.proxyType}).Dec() delete(ctx.idToSnowflake, snowflake.id) close(request.offerChannel) } @@ -176,6 +177,7 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri } else { heap.Push(ctx.restrictedSnowflakes, snowflake) } + promMetrics.AvailableProxies.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc() ctx.snowflakeLock.Unlock() ctx.idToSnowflake[id] = snowflake return snowflake @@ -319,6 +321,7 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { } ctx.snowflakeLock.Lock() + promMetrics.AvailableProxies.With(prometheus.Labels{"nat": snowflake.natType, "type": snowflake.proxyType}).Dec() delete(ctx.idToSnowflake, snowflake.id) ctx.snowflakeLock.Unlock() } diff --git a/broker/metrics.go b/broker/metrics.go index 24ff9b0..be8cfd9 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -261,9 +261,10 @@ func binCount(count uint) uint { } type PromMetrics struct { - ProxyTotal *prometheus.CounterVec - ProxyPollTotal *RoundedCounterVec - ClientPollTotal *RoundedCounterVec + ProxyTotal *prometheus.CounterVec + ProxyPollTotal *RoundedCounterVec + ClientPollTotal *RoundedCounterVec + AvailableProxies *prometheus.GaugeVec } //Initialize metrics for prometheus exporter @@ -280,6 +281,15 @@ func initPrometheus() *PromMetrics { []string{"type", "nat", "cc"}, ) + promMetrics.AvailableProxies = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: prometheusNamespace, + Name: "available_proxies", + Help: "The number of currently available snowflake proxies", + }, + []string{"type", "nat"}, + ) + promMetrics.ProxyPollTotal = NewRoundedCounterVec( prometheus.CounterOpts{ Namespace: prometheusNamespace, From af6e2c30e1a6aacc6e7adf9a31df0a387891cc37 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 1 Apr 2021 14:21:12 -0400 Subject: [PATCH 174/385] Replace default with custom prometheus registry The default prometheus registry exports data that may be useful for side-channel attacks. This removes all of the default metrics and makes sure we are only reporting snowflake metrics from the broker. --- broker/broker.go | 4 +--- broker/metrics.go | 15 ++++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index 77c62d8..8d7a314 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -506,9 +506,7 @@ func main() { http.Handle("/answer", SnowflakeHandler{ctx, proxyAnswers}) http.Handle("/debug", SnowflakeHandler{ctx, debugHandler}) http.Handle("/metrics", MetricsHandler{metricsFilename, metricsHandler}) - http.Handle("/prometheus", promhttp.Handler()) - - InitPrometheus() + http.Handle("/prometheus", promhttp.HandlerFor(promMetrics.registry, promhttp.HandlerOpts{})) server := http.Server{ Addr: addr, diff --git a/broker/metrics.go b/broker/metrics.go index be8cfd9..ad55bcb 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -15,7 +15,6 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" ) var ( @@ -261,6 +260,7 @@ func binCount(count uint) uint { } type PromMetrics struct { + registry *prometheus.Registry ProxyTotal *prometheus.CounterVec ProxyPollTotal *RoundedCounterVec ClientPollTotal *RoundedCounterVec @@ -272,7 +272,9 @@ func initPrometheus() *PromMetrics { promMetrics := &PromMetrics{} - promMetrics.ProxyTotal = promauto.NewCounterVec( + promMetrics.registry = prometheus.NewRegistry() + + promMetrics.ProxyTotal = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: prometheusNamespace, Name: "proxy_total", @@ -281,7 +283,7 @@ func initPrometheus() *PromMetrics { []string{"type", "nat", "cc"}, ) - promMetrics.AvailableProxies = promauto.NewGaugeVec( + promMetrics.AvailableProxies = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: prometheusNamespace, Name: "available_proxies", @@ -308,10 +310,9 @@ func initPrometheus() *PromMetrics { []string{"nat", "status"}, ) - // We need to register this new metric type because there is no constructor - // for it in promauto. - prometheus.DefaultRegisterer.MustRegister(promMetrics.ClientPollTotal) - prometheus.DefaultRegisterer.MustRegister(promMetrics.ProxyPollTotal) + // We need to register our metrics so they can be exported. + promMetrics.registry.MustRegister(promMetrics.ClientPollTotal, promMetrics.ProxyPollTotal, + promMetrics.ProxyTotal, promMetrics.AvailableProxies) return promMetrics From e87b9175dd7559fccd665cd7eb4b6edecc231950 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Sat, 20 Mar 2021 12:36:33 -0400 Subject: [PATCH 175/385] Implement snowflake client lib as PTv2.1 Go API This implements a pluggable transports v2.1 compatible Go API in the Snowflake client library, and refactors how the main Snowflake program calls it. The Go API implements the two required client side functions: a constructor that returns a Transport, and a Dial function for the Transport that returns a net.Conn. See the PT specification for more information: https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/master/releases/PTSpecV2.1/Pluggable%20Transport%20Specification%20v2.1%20-%20Go%20Transport%20API.pdf --- client/client_test.go | 59 ------------ client/lib/lib_test.go | 55 ++++++++--- client/lib/snowflake.go | 198 +++++++++++++++++++++++++++------------- client/snowflake.go | 106 +++++++-------------- 4 files changed, 211 insertions(+), 207 deletions(-) delete mode 100644 client/client_test.go diff --git a/client/client_test.go b/client/client_test.go deleted file mode 100644 index 84e9cc1..0000000 --- a/client/client_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestICEServerParser(t *testing.T) { - Convey("Test parsing of ICE servers", t, func() { - for _, test := range []struct { - input string - urls [][]string - length int - }{ - { - "", - nil, - 0, - }, - { - " ", - nil, - 0, - }, - { - "stun:stun.l.google.com:19302", - [][]string{[]string{"stun:stun.l.google.com:19302"}}, - 1, - }, - { - "stun:stun.l.google.com:19302,stun.ekiga.net", - [][]string{[]string{"stun:stun.l.google.com:19302"}, []string{"stun.ekiga.net"}}, - 2, - }, - { - "stun:stun.l.google.com:19302, stun.ekiga.net", - [][]string{[]string{"stun:stun.l.google.com:19302"}, []string{"stun.ekiga.net"}}, - 2, - }, - } { - servers := parseIceServers(test.input) - - if test.urls == nil { - So(servers, ShouldBeNil) - } else { - So(servers, ShouldNotBeNil) - } - - So(len(servers), ShouldEqual, test.length) - - for _, server := range servers { - So(test.urls, ShouldContain, server.URLs) - } - - } - - }) -} diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 5537a52..6140e0b 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -156,19 +156,6 @@ func TestSnowflakeClient(t *testing.T) { }) - Convey("Snowflake", t, func() { - - SkipConvey("Handler Grants correctly", func() { - socks := &FakeSocksConn{} - broker := &BrokerChannel{Host: "test"} - d := NewWebRTCDialer(broker, nil, 1) - - So(socks.rejected, ShouldEqual, false) - Handler(socks, d) - So(socks.rejected, ShouldEqual, true) - }) - }) - Convey("Dialers", t, func() { Convey("Can construct WebRTCDialer.", func() { broker := &BrokerChannel{Host: "test"} @@ -267,3 +254,45 @@ func TestSnowflakeClient(t *testing.T) { }) } + +func TestICEServerParser(t *testing.T) { + Convey("Test parsing of ICE servers", t, func() { + for _, test := range []struct { + input []string + urls [][]string + length int + }{ + { + []string{"stun:stun.l.google.com:19302"}, + [][]string{[]string{"stun:stun.l.google.com:19302"}}, + 1, + }, + { + []string{"stun:stun.l.google.com:19302", "stun.ekiga.net"}, + [][]string{[]string{"stun:stun.l.google.com:19302"}, []string{"stun.ekiga.net"}}, + 2, + }, + { + []string{"stun:stun.l.google.com:19302", "stun.ekiga.net"}, + [][]string{[]string{"stun:stun.l.google.com:19302"}, []string{"stun.ekiga.net"}}, + 2, + }, + } { + servers := parseIceServers(test.input) + + if test.urls == nil { + So(servers, ShouldBeNil) + } else { + So(servers, ShouldNotBeNil) + } + + So(len(servers), ShouldEqual, test.length) + + for _, server := range servers { + So(test.urls, ShouldContain, server.URLs) + } + + } + + }) +} diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 2ed51a1..6e87b81 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -3,12 +3,15 @@ package lib import ( "context" "errors" - "io" "log" + "math/rand" "net" + "strings" "time" + "git.torproject.org/pluggable-transports/snowflake.git/common/nat" "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" + "github.com/pion/webrtc/v3" "github.com/xtaci/kcp-go/v5" "github.com/xtaci/smux" ) @@ -25,6 +28,138 @@ type dummyAddr struct{} func (addr dummyAddr) Network() string { return "dummy" } func (addr dummyAddr) String() string { return "dummy" } +// Transport is a structure with methods that conform to the Go PT v2.1 API +// https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/master/releases/PTSpecV2.1/Pluggable%20Transport%20Specification%20v2.1%20-%20Go%20Transport%20API.pdf +type Transport struct { + dialer *WebRTCDialer +} + +// Create a new Snowflake transport client that can spawn multiple Snowflake connections. +// brokerURL and frontDomain are the urls for the broker host and domain fronting host +// iceAddresses are the STUN/TURN urls needed for WebRTC negotiation +// keepLocalAddresses is a flag to enable sending local network addresses (for testing purposes) +// max is the maximum number of snowflakes the client should gather for each SOCKS connection +func NewSnowflakeClient(brokerURL, frontDomain string, iceAddresses []string, keepLocalAddresses bool, max int) (*Transport, error) { + + log.Println("\n\n\n --- Starting Snowflake Client ---") + + iceServers := parseIceServers(iceAddresses) + // chooses a random subset of servers from inputs + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(iceServers), func(i, j int) { + iceServers[i], iceServers[j] = iceServers[j], iceServers[i] + }) + if len(iceServers) > 2 { + iceServers = iceServers[:(len(iceServers)+1)/2] + } + log.Printf("Using ICE servers:") + for _, server := range iceServers { + log.Printf("url: %v", strings.Join(server.URLs, " ")) + } + + // Use potentially domain-fronting broker to rendezvous. + broker, err := NewBrokerChannel( + brokerURL, frontDomain, CreateBrokerTransport(), + keepLocalAddresses) + if err != nil { + return nil, err + } + go updateNATType(iceServers, broker) + + transport := &Transport{dialer: NewWebRTCDialer(broker, iceServers, max)} + + return transport, nil +} + +// Create a new Snowflake connection. Starts the collection of snowflakes and returns a +// smux Stream. +func (t *Transport) Dial() (net.Conn, error) { + // Prepare to collect remote WebRTC peers. + snowflakes, err := NewPeers(t.dialer) + if err != nil { + return nil, err + } + + // Use a real logger to periodically output how much traffic is happening. + snowflakes.BytesLogger = NewBytesSyncLogger() + + log.Printf("---- SnowflakeConn: begin collecting snowflakes ---") + go connectLoop(snowflakes) + + // Create a new smux session + log.Printf("---- SnowflakeConn: starting a new session ---") + pconn, sess, err := newSession(snowflakes) + if err != nil { + return nil, err + } + + // On the smux session we overlay a stream. + stream, err := sess.OpenStream() + if err != nil { + return nil, err + } + + // Begin exchanging data. + log.Printf("---- SnowflakeConn: begin stream %v ---", stream.ID()) + return &SnowflakeConn{Stream: stream, sess: sess, pconn: pconn, snowflakes: snowflakes}, nil +} + +type SnowflakeConn struct { + *smux.Stream + sess *smux.Session + pconn net.PacketConn + snowflakes *Peers +} + +func (conn *SnowflakeConn) Close() error { + log.Printf("---- SnowflakeConn: closed stream %v ---", conn.ID()) + conn.Stream.Close() + log.Printf("---- SnowflakeConn: end collecting snowflakes ---") + conn.snowflakes.End() + conn.pconn.Close() + log.Printf("---- SnowflakeConn: discarding finished session ---") + conn.sess.Close() + return nil //TODO: return errors if any of the above do +} + +// loop through all provided STUN servers until we exhaust the list or find +// one that is compatable with RFC 5780 +func updateNATType(servers []webrtc.ICEServer, broker *BrokerChannel) { + + var restrictedNAT bool + var err error + for _, server := range servers { + addr := strings.TrimPrefix(server.URLs[0], "stun:") + restrictedNAT, err = nat.CheckIfRestrictedNAT(addr) + if err == nil { + if restrictedNAT { + broker.SetNATType(nat.NATRestricted) + } else { + broker.SetNATType(nat.NATUnrestricted) + } + break + } + } + if err != nil { + broker.SetNATType(nat.NATUnknown) + } +} + +// Returns a slice of webrtc.ICEServer given a slice of addresses +func parseIceServers(addresses []string) []webrtc.ICEServer { + var servers []webrtc.ICEServer + if len(addresses) == 0 { + return nil + } + for _, url := range addresses { + url = strings.TrimSpace(url) + servers = append(servers, webrtc.ICEServer{ + URLs: []string{url}, + }) + } + return servers +} + // newSession returns a new smux.Session and the net.PacketConn it is running // over. The net.PacketConn successively connects through Snowflake proxies // pulled from snowflakes. @@ -94,47 +229,6 @@ func newSession(snowflakes SnowflakeCollector) (net.PacketConn, *smux.Session, e return pconn, sess, err } -// Given an accepted SOCKS connection, establish a WebRTC connection to the -// remote peer and exchange traffic. -func Handler(socks net.Conn, tongue Tongue) error { - // Prepare to collect remote WebRTC peers. - snowflakes, err := NewPeers(tongue) - if err != nil { - return err - } - - // Use a real logger to periodically output how much traffic is happening. - snowflakes.BytesLogger = NewBytesSyncLogger() - - log.Printf("---- Handler: begin collecting snowflakes ---") - go connectLoop(snowflakes) - - // Create a new smux session - log.Printf("---- Handler: starting a new session ---") - pconn, sess, err := newSession(snowflakes) - if err != nil { - return err - } - - // On the smux session we overlay a stream. - stream, err := sess.OpenStream() - if err != nil { - return err - } - defer stream.Close() - - // Begin exchanging data. - log.Printf("---- Handler: begin stream %v ---", stream.ID()) - copyLoop(socks, stream) - log.Printf("---- Handler: closed stream %v ---", stream.ID()) - snowflakes.End() - log.Printf("---- Handler: end collecting snowflakes ---") - pconn.Close() - sess.Close() - log.Printf("---- Handler: discarding finished session ---") - return nil -} - // Maintain |SnowflakeCapacity| number of available WebRTC connections, to // transfer to the Tor SOCKS handler when needed. func connectLoop(snowflakes SnowflakeCollector) { @@ -153,23 +247,3 @@ func connectLoop(snowflakes SnowflakeCollector) { } } } - -// Exchanges bytes between two ReadWriters. -// (In this case, between a SOCKS connection and smux stream.) -func copyLoop(socks, stream io.ReadWriter) { - done := make(chan struct{}, 2) - go func() { - if _, err := io.Copy(socks, stream); err != nil { - log.Printf("copying WebRTC to SOCKS resulted in error: %v", err) - } - done <- struct{}{} - }() - go func() { - if _, err := io.Copy(stream, socks); err != nil { - log.Printf("copying SOCKS to stream resulted in error: %v", err) - } - done <- struct{}{} - }() - <-done - log.Println("copy loop ended") -} diff --git a/client/snowflake.go b/client/snowflake.go index d79de97..f19afcf 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -6,7 +6,6 @@ import ( "io" "io/ioutil" "log" - "math/rand" "net" "os" "os/signal" @@ -14,21 +13,38 @@ import ( "strings" "sync" "syscall" - "time" pt "git.torproject.org/pluggable-transports/goptlib.git" sf "git.torproject.org/pluggable-transports/snowflake.git/client/lib" - "git.torproject.org/pluggable-transports/snowflake.git/common/nat" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" - "github.com/pion/webrtc/v3" ) const ( DefaultSnowflakeCapacity = 1 ) -// Accept local SOCKS connections and pass them to the handler. -func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue, shutdown chan struct{}, wg *sync.WaitGroup) { +// Exchanges bytes between two ReadWriters. +// (In this case, between a SOCKS connection and a snowflake transport conn) +func copyLoop(socks, sfconn io.ReadWriter) { + done := make(chan struct{}, 2) + go func() { + if _, err := io.Copy(socks, sfconn); err != nil { + log.Printf("copying Snowflake to SOCKS resulted in error: %v", err) + } + done <- struct{}{} + }() + go func() { + if _, err := io.Copy(sfconn, socks); err != nil { + log.Printf("copying SOCKS to Snowflake resulted in error: %v", err) + } + done <- struct{}{} + }() + <-done + log.Println("copy loop ended") +} + +// Accept local SOCKS connections and connect to a Snowflake connection +func socksAcceptLoop(ln *pt.SocksListener, transport *sf.Transport, shutdown chan struct{}, wg *sync.WaitGroup) { defer ln.Close() for { conn, err := ln.AcceptSocks() @@ -53,10 +69,14 @@ func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue, shutdown chan struc handler := make(chan struct{}) go func() { - err = sf.Handler(conn, tongue) + // pass an empty address because the broker chooses the bridge + sconn, err := transport.Dial() if err != nil { - log.Printf("handler error: %s", err) + log.Printf("dial error: %s", err) } + // copy between the created Snowflake conn and the SOCKS conn + copyLoop(conn, sconn) + sconn.Close() close(handler) return @@ -72,23 +92,6 @@ func socksAcceptLoop(ln *pt.SocksListener, tongue sf.Tongue, shutdown chan struc } } -// s is a comma-separated list of ICE server URLs. -func parseIceServers(s string) []webrtc.ICEServer { - var servers []webrtc.ICEServer - s = strings.TrimSpace(s) - if len(s) == 0 { - return nil - } - urls := strings.Split(s, ",") - for _, url := range urls { - url = strings.TrimSpace(url) - servers = append(servers, webrtc.ICEServer{ - URLs: []string{url}, - }) - } - return servers -} - func main() { iceServersCommas := flag.String("ice", "", "comma-separated list of ICE servers") brokerURL := flag.String("url", "", "URL of signaling broker") @@ -137,33 +140,13 @@ func main() { log.SetOutput(&safelog.LogScrubber{Output: logOutput}) } - log.Println("\n\n\n --- Starting Snowflake Client ---") + iceAddresses := strings.Split(strings.TrimSpace(*iceServersCommas), ",") - iceServers := parseIceServers(*iceServersCommas) - // chooses a random subset of servers from inputs - rand.Seed(time.Now().UnixNano()) - rand.Shuffle(len(iceServers), func(i, j int) { - iceServers[i], iceServers[j] = iceServers[j], iceServers[i] - }) - if len(iceServers) > 2 { - iceServers = iceServers[:(len(iceServers)+1)/2] - } - log.Printf("Using ICE servers:") - for _, server := range iceServers { - log.Printf("url: %v", strings.Join(server.URLs, " ")) - } - - // Use potentially domain-fronting broker to rendezvous. - broker, err := sf.NewBrokerChannel( - *brokerURL, *frontDomain, sf.CreateBrokerTransport(), - *keepLocalAddresses || *oldKeepLocalAddresses) + transport, err := sf.NewSnowflakeClient(*brokerURL, *frontDomain, iceAddresses, + *keepLocalAddresses || *oldKeepLocalAddresses, *max) if err != nil { - log.Fatalf("parsing broker URL: %v", err) + log.Fatal("Failed to start snowflake transport: ", err) } - go updateNATType(iceServers, broker) - - // Create a new WebRTCDialer to use as the |Tongue| to catch snowflakes - dialer := sf.NewWebRTCDialer(broker, iceServers, *max) // Begin goptlib client process. ptInfo, err := pt.ClientSetup(nil) @@ -187,7 +170,7 @@ func main() { break } log.Printf("Started SOCKS listener at %v.", ln.Addr()) - go socksAcceptLoop(ln, dialer, shutdown, &wg) + go socksAcceptLoop(ln, transport, shutdown, &wg) pt.Cmethod(methodName, ln.Version(), ln.Addr()) listeners = append(listeners, ln) default: @@ -223,26 +206,3 @@ func main() { wg.Wait() log.Println("snowflake is done.") } - -// loop through all provided STUN servers until we exhaust the list or find -// one that is compatable with RFC 5780 -func updateNATType(servers []webrtc.ICEServer, broker *sf.BrokerChannel) { - - var restrictedNAT bool - var err error - for _, server := range servers { - addr := strings.TrimPrefix(server.URLs[0], "stun:") - restrictedNAT, err = nat.CheckIfRestrictedNAT(addr) - if err == nil { - if restrictedNAT { - broker.SetNATType(nat.NATRestricted) - } else { - broker.SetNATType(nat.NATUnrestricted) - } - break - } - } - if err != nil { - broker.SetNATType(nat.NATUnknown) - } -} From 11f0846264d4033e7a7dc7824febb6ad7140762f Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Sat, 20 Mar 2021 18:24:00 -0400 Subject: [PATCH 176/385] Implement server as a v2.1 PT Go API --- server/lib/http.go | 211 +++++++++++++ server/lib/server_test.go | 55 ++++ server/lib/snowflake.go | 242 +++++++++++++++ server/{ => lib}/turbotunnel.go | 2 +- server/{ => lib}/turbotunnel_test.go | 2 +- server/server.go | 428 +++------------------------ server/server_test.go | 153 ---------- 7 files changed, 552 insertions(+), 541 deletions(-) create mode 100644 server/lib/http.go create mode 100644 server/lib/server_test.go create mode 100644 server/lib/snowflake.go rename server/{ => lib}/turbotunnel.go (99%) rename server/{ => lib}/turbotunnel_test.go (99%) delete mode 100644 server/server_test.go diff --git a/server/lib/http.go b/server/lib/http.go new file mode 100644 index 0000000..b1c453c --- /dev/null +++ b/server/lib/http.go @@ -0,0 +1,211 @@ +package lib + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "net" + "net/http" + "time" + + "git.torproject.org/pluggable-transports/snowflake.git/common/encapsulation" + "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" + "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" + "github.com/gorilla/websocket" +) + +const requestTimeout = 10 * time.Second + +// How long to remember outgoing packets for a client, when we don't currently +// have an active WebSocket connection corresponding to that client. Because a +// client session may span multiple WebSocket connections, we keep packets we +// aren't able to send immediately in memory, for a little while but not +// indefinitely. +const clientMapTimeout = 1 * time.Minute + +// How big to make the map of ClientIDs to IP addresses. The map is used in +// turbotunnelMode to store a reasonable IP address for a client session that +// may outlive any single WebSocket connection. +const clientIDAddrMapCapacity = 1024 + +// How long to wait for ListenAndServe or ListenAndServeTLS to return an error +// before deciding that it's not going to return. +const listenAndServeErrorTimeout = 100 * time.Millisecond + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, +} + +// clientIDAddrMap stores short-term mappings from ClientIDs to IP addresses. +// When we call pt.DialOr, tor wants us to provide a USERADDR string that +// represents the remote IP address of the client (for metrics purposes, etc.). +// This data structure bridges the gap between ServeHTTP, which knows about IP +// addresses, and handleStream, which is what calls pt.DialOr. The common piece +// of information linking both ends of the chain is the ClientID, which is +// attached to the WebSocket connection and every session. +var clientIDAddrMap = newClientIDMap(clientIDAddrMapCapacity) + +// overrideReadConn is a net.Conn with an overridden Read method. Compare to +// recordingConn at +// https://dave.cheney.net/2015/05/22/struct-composition-with-go. +type overrideReadConn struct { + net.Conn + io.Reader +} + +func (conn *overrideReadConn) Read(p []byte) (int, error) { + return conn.Reader.Read(p) +} + +type HTTPHandler struct { + // pconn is the adapter layer between stream-oriented WebSocket + // connections and the packet-oriented KCP layer. + pconn *turbotunnel.QueuePacketConn + ln *SnowflakeListener +} + +func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + + conn := websocketconn.New(ws) + defer conn.Close() + + // Pass the address of client as the remote address of incoming connection + clientIPParam := r.URL.Query().Get("client_ip") + addr := clientAddr(clientIPParam) + + var token [len(turbotunnel.Token)]byte + _, err = io.ReadFull(conn, token[:]) + if err != nil { + // Don't bother logging EOF: that happens with an unused + // connection, which clients make frequently as they maintain a + // pool of proxies. + if err != io.EOF { + log.Printf("reading token: %v", err) + } + return + } + + switch { + case bytes.Equal(token[:], turbotunnel.Token[:]): + err = turbotunnelMode(conn, addr, handler.pconn) + default: + // We didn't find a matching token, which means that we are + // dealing with a client that doesn't know about such things. + // "Unread" the token by constructing a new Reader and pass it + // to the old one-session-per-WebSocket mode. + conn2 := &overrideReadConn{Conn: conn, Reader: io.MultiReader(bytes.NewReader(token[:]), conn)} + err = oneshotMode(conn2, addr, handler.ln) + } + if err != nil { + log.Println(err) + return + } +} + +// oneshotMode handles clients that did not send turbotunnel.Token at the start +// of their stream. These clients use the WebSocket as a raw pipe, and expect +// their session to begin and end when this single WebSocket does. +func oneshotMode(conn net.Conn, addr net.Addr, ln *SnowflakeListener) error { + return ln.QueueConn(&SnowflakeClientConn{Conn: conn, address: addr}) +} + +// turbotunnelMode handles clients that sent turbotunnel.Token at the start of +// their stream. These clients expect to send and receive encapsulated packets, +// with a long-lived session identified by ClientID. +func turbotunnelMode(conn net.Conn, addr net.Addr, pconn *turbotunnel.QueuePacketConn) error { + // Read the ClientID prefix. Every packet encapsulated in this WebSocket + // connection pertains to the same ClientID. + var clientID turbotunnel.ClientID + _, err := io.ReadFull(conn, clientID[:]) + if err != nil { + return fmt.Errorf("reading ClientID: %v", err) + } + + // Store a a short-term mapping from the ClientID to the client IP + // address attached to this WebSocket connection. tor will want us to + // provide a client IP address when we call pt.DialOr. But a KCP session + // does not necessarily correspond to any single IP address--it's + // composed of packets that are carried in possibly multiple WebSocket + // streams. We apply the heuristic that the IP address of the most + // recent WebSocket connection that has had to do with a session, at the + // time the session is established, is the IP address that should be + // credited for the entire KCP session. + clientIDAddrMap.Set(clientID, addr.String()) + + errCh := make(chan error) + + // The remainder of the WebSocket stream consists of encapsulated + // packets. We read them one by one and feed them into the + // QueuePacketConn on which kcp.ServeConn was set up, which eventually + // leads to KCP-level sessions in the acceptSessions function. + go func() { + for { + p, err := encapsulation.ReadData(conn) + if err != nil { + errCh <- err + break + } + pconn.QueueIncoming(p, clientID) + } + }() + + // At the same time, grab packets addressed to this ClientID and + // encapsulate them into the downstream. + go func() { + // Buffer encapsulation.WriteData operations to keep length + // prefixes in the same send as the data that follows. + bw := bufio.NewWriter(conn) + for p := range pconn.OutgoingQueue(clientID) { + _, err := encapsulation.WriteData(bw, p) + if err == nil { + err = bw.Flush() + } + if err != nil { + errCh <- err + break + } + } + }() + + // Wait until one of the above loops terminates. The closing of the + // WebSocket connection will terminate the other one. + <-errCh + + return nil +} + +type ClientMapAddr string + +func (addr ClientMapAddr) Network() string { + return "snowflake" +} + +func (addr ClientMapAddr) String() string { + return string(addr) +} + +// Return a client address +func clientAddr(clientIPParam string) net.Addr { + if clientIPParam == "" { + return ClientMapAddr("") + } + // Check if client addr is a valid IP + clientIP := net.ParseIP(clientIPParam) + if clientIP == nil { + return ClientMapAddr("") + } + // Check if client addr is 0.0.0.0 or [::]. Some proxies erroneously + // report an address of 0.0.0.0: https://bugs.torproject.org/33157. + if clientIP.IsUnspecified() { + return ClientMapAddr("") + } + // Add a stub port number. USERADDR requires a port number. + return ClientMapAddr((&net.TCPAddr{IP: clientIP, Port: 1, Zone: ""}).String()) +} diff --git a/server/lib/server_test.go b/server/lib/server_test.go new file mode 100644 index 0000000..65d31d1 --- /dev/null +++ b/server/lib/server_test.go @@ -0,0 +1,55 @@ +package lib + +import ( + "net" + "strconv" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestClientAddr(t *testing.T) { + Convey("Testing clientAddr", t, func() { + // good tests + for _, test := range []struct { + input string + expected net.IP + }{ + {"1.2.3.4", net.ParseIP("1.2.3.4")}, + {"1:2::3:4", net.ParseIP("1:2::3:4")}, + } { + useraddr := clientAddr(test.input).String() + host, port, err := net.SplitHostPort(useraddr) + if err != nil { + t.Errorf("clientAddr(%q) → SplitHostPort error %v", test.input, err) + continue + } + if !test.expected.Equal(net.ParseIP(host)) { + t.Errorf("clientAddr(%q) → host %q, not %v", test.input, host, test.expected) + } + portNo, err := strconv.Atoi(port) + if err != nil { + t.Errorf("clientAddr(%q) → port %q", test.input, port) + continue + } + if portNo == 0 { + t.Errorf("clientAddr(%q) → port %d", test.input, portNo) + } + } + + // bad tests + for _, input := range []string{ + "", + "abc", + "1.2.3.4.5", + "[12::34]", + "0.0.0.0", + "[::]", + } { + useraddr := clientAddr(input).String() + if useraddr != "" { + t.Errorf("clientAddr(%q) → %q, not %q", input, useraddr, "") + } + } + }) +} diff --git a/server/lib/snowflake.go b/server/lib/snowflake.go new file mode 100644 index 0000000..319acd8 --- /dev/null +++ b/server/lib/snowflake.go @@ -0,0 +1,242 @@ +package lib + +import ( + "crypto/tls" + "fmt" + "io" + "log" + "net" + "net/http" + "sync" + "time" + + "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" + "github.com/xtaci/kcp-go/v5" + "github.com/xtaci/smux" + "golang.org/x/net/http2" +) + +// Transport is a structure with methods that conform to the Go PT v2.1 API +// https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/master/releases/PTSpecV2.1/Pluggable%20Transport%20Specification%20v2.1%20-%20Go%20Transport%20API.pdf +type Transport struct { + getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error) +} + +func NewSnowflakeServer(getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error)) *Transport { + + return &Transport{getCertificate: getCertificate} +} + +func (t *Transport) Listen(addr net.Addr) (*SnowflakeListener, error) { + listener := &SnowflakeListener{addr: addr, queue: make(chan net.Conn, 65534)} + + handler := HTTPHandler{ + // pconn is shared among all connections to this server. It + // overlays packet-based client sessions on top of ephemeral + // WebSocket connections. + pconn: turbotunnel.NewQueuePacketConn(addr, clientMapTimeout), + } + server := &http.Server{ + Addr: addr.String(), + Handler: &handler, + ReadTimeout: requestTimeout, + } + // We need to override server.TLSConfig.GetCertificate--but first + // server.TLSConfig needs to be non-nil. If we just create our own new + // &tls.Config, it will lack the default settings that the net/http + // package sets up for things like HTTP/2. Therefore we first call + // http2.ConfigureServer for its side effect of initializing + // server.TLSConfig properly. An alternative would be to make a dummy + // net.Listener, call Serve on it, and let it return. + // https://github.com/golang/go/issues/16588#issuecomment-237386446 + err := http2.ConfigureServer(server, nil) + if err != nil { + return nil, err + } + server.TLSConfig.GetCertificate = t.getCertificate + + // Another unfortunate effect of the inseparable net/http ListenAndServe + // is that we can't check for Listen errors like "permission denied" and + // "address already in use" without potentially entering the infinite + // loop of Serve. The hack we apply here is to wait a short time, + // listenAndServeErrorTimeout, to see if an error is returned (because + // it's better if the error message goes to the tor log through + // SMETHOD-ERROR than if it only goes to the snowflake log). + errChan := make(chan error) + go func() { + if t.getCertificate == nil { + // TLS is disabled + log.Printf("listening with plain HTTP on %s", addr) + err := server.ListenAndServe() + if err != nil { + log.Printf("error in ListenAndServe: %s", err) + } + errChan <- err + } else { + log.Printf("listening with HTTPS on %s", addr) + err := server.ListenAndServeTLS("", "") + if err != nil { + log.Printf("error in ListenAndServeTLS: %s", err) + } + errChan <- err + } + }() + + select { + case err = <-errChan: + break + case <-time.After(listenAndServeErrorTimeout): + break + } + + listener.server = server + + // Start a KCP engine, set up to read and write its packets over the + // WebSocket connections that arrive at the web server. + // handler.ServeHTTP is responsible for encapsulation/decapsulation of + // packets on behalf of KCP. KCP takes those packets and turns them into + // sessions which appear in the acceptSessions function. + ln, err := kcp.ServeConn(nil, 0, 0, handler.pconn) + if err != nil { + server.Close() + return nil, err + } + go func() { + defer ln.Close() + err := listener.acceptSessions(ln) + if err != nil { + log.Printf("acceptSessions: %v", err) + } + }() + + listener.ln = ln + + return listener, nil + +} + +type SnowflakeListener struct { + addr net.Addr + queue chan net.Conn + server *http.Server + ln *kcp.Listener + closed chan struct{} + closeOnce sync.Once +} + +// Allows the caller to accept incoming Snowflake connections +// We accept connections from a queue to accommodate both incoming +// smux Streams and legacy non-turbotunnel connections +func (l *SnowflakeListener) Accept() (net.Conn, error) { + select { + case <-l.closed: + //channel has been closed, no longer accepting connections + return nil, io.ErrClosedPipe + case conn := <-l.queue: + return conn, nil + } +} + +func (l *SnowflakeListener) Addr() net.Addr { + return l.addr +} + +func (l *SnowflakeListener) Close() error { + // Close our HTTP server and our KCP listener + l.closeOnce.Do(func() { + close(l.closed) + l.server.Close() + l.ln.Close() + }) + return nil +} + +// acceptStreams layers an smux.Session on the KCP connection and awaits streams +// on it. Passes each stream to our SnowflakeListener accept queue. +func (l *SnowflakeListener) acceptStreams(conn *kcp.UDPSession) error { + // Look up the IP address associated with this KCP session, via the + // ClientID that is returned by the session's RemoteAddr method. + addr, ok := clientIDAddrMap.Get(conn.RemoteAddr().(turbotunnel.ClientID)) + if !ok { + // This means that the map is tending to run over capacity, not + // just that there was not client_ip on the incoming connection. + // We store "" in the map in the absence of client_ip. This log + // message means you should increase clientIDAddrMapCapacity. + log.Printf("no address in clientID-to-IP map (capacity %d)", clientIDAddrMapCapacity) + } + + smuxConfig := smux.DefaultConfig() + smuxConfig.Version = 2 + smuxConfig.KeepAliveTimeout = 10 * time.Minute + sess, err := smux.Server(conn, smuxConfig) + if err != nil { + return err + } + + for { + stream, err := sess.AcceptStream() + if err != nil { + if err, ok := err.(net.Error); ok && err.Temporary() { + continue + } + return err + } + l.QueueConn(&SnowflakeClientConn{Conn: stream, address: clientAddr(addr)}) + } +} + +// acceptSessions listens for incoming KCP connections and passes them to +// acceptStreams. It is handler.ServeHTTP that provides the network interface +// that drives this function. +func (l *SnowflakeListener) acceptSessions(ln *kcp.Listener) error { + for { + conn, err := ln.AcceptKCP() + if err != nil { + if err, ok := err.(net.Error); ok && err.Temporary() { + continue + } + return err + } + // Permit coalescing the payloads of consecutive sends. + conn.SetStreamMode(true) + // Set the maximum send and receive window sizes to a high number + // Removes KCP bottlenecks: https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40026 + conn.SetWindowSize(65535, 65535) + // Disable the dynamic congestion window (limit only by the + // maximum of local and remote static windows). + conn.SetNoDelay( + 0, // default nodelay + 0, // default interval + 0, // default resend + 1, // nc=1 => congestion window off + ) + go func() { + defer conn.Close() + err := l.acceptStreams(conn) + if err != nil && err != io.ErrClosedPipe { + log.Printf("acceptStreams: %v", err) + } + }() + } +} + +func (l *SnowflakeListener) QueueConn(conn net.Conn) error { + select { + case <-l.closed: + return fmt.Errorf("accepted connection on closed listener") + case l.queue <- conn: + return nil + } +} + +// A wrapper for the underlying oneshot or turbotunnel conn +// because we need to reference our mapping to determine the client +// address +type SnowflakeClientConn struct { + net.Conn + address net.Addr +} + +func (conn *SnowflakeClientConn) RemoteAddr() net.Addr { + return conn.address +} diff --git a/server/turbotunnel.go b/server/lib/turbotunnel.go similarity index 99% rename from server/turbotunnel.go rename to server/lib/turbotunnel.go index 1d00897..bb16fa3 100644 --- a/server/turbotunnel.go +++ b/server/lib/turbotunnel.go @@ -1,4 +1,4 @@ -package main +package lib import ( "sync" diff --git a/server/turbotunnel_test.go b/server/lib/turbotunnel_test.go similarity index 99% rename from server/turbotunnel_test.go rename to server/lib/turbotunnel_test.go index c4bf02b..ba4cf60 100644 --- a/server/turbotunnel_test.go +++ b/server/lib/turbotunnel_test.go @@ -1,4 +1,4 @@ -package main +package lib import ( "encoding/binary" diff --git a/server/server.go b/server/server.go index 620cd50..b61d5b4 100644 --- a/server/server.go +++ b/server/server.go @@ -3,9 +3,6 @@ package main import ( - "bufio" - "bytes" - "crypto/tls" "flag" "fmt" "io" @@ -19,38 +16,15 @@ import ( "strings" "sync" "syscall" - "time" + + "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" + "golang.org/x/crypto/acme/autocert" pt "git.torproject.org/pluggable-transports/goptlib.git" - "git.torproject.org/pluggable-transports/snowflake.git/common/encapsulation" - "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" - "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" - "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" - "github.com/gorilla/websocket" - "github.com/xtaci/kcp-go/v5" - "github.com/xtaci/smux" - "golang.org/x/crypto/acme/autocert" - "golang.org/x/net/http2" + sf "git.torproject.org/pluggable-transports/snowflake.git/server/lib" ) const ptMethodName = "snowflake" -const requestTimeout = 10 * time.Second - -// How long to remember outgoing packets for a client, when we don't currently -// have an active WebSocket connection corresponding to that client. Because a -// client session may span multiple WebSocket connections, we keep packets we -// aren't able to send immediately in memory, for a little while but not -// indefinitely. -const clientMapTimeout = 1 * time.Minute - -// How big to make the map of ClientIDs to IP addresses. The map is used in -// turbotunnelMode to store a reasonable IP address for a client session that -// may outlive any single WebSocket connection. -const clientIDAddrMapCapacity = 1024 - -// How long to wait for ListenAndServe or ListenAndServeTLS to return an error -// before deciding that it's not going to return. -const listenAndServeErrorTimeout = 100 * time.Millisecond var ptInfo pt.ServerInfo @@ -92,366 +66,30 @@ func proxy(local *net.TCPConn, conn net.Conn) { wg.Wait() } -// Return an address string suitable to pass into pt.DialOr. -func clientAddr(clientIPParam string) string { - if clientIPParam == "" { - return "" - } - // Check if client addr is a valid IP - clientIP := net.ParseIP(clientIPParam) - if clientIP == nil { - return "" - } - // Check if client addr is 0.0.0.0 or [::]. Some proxies erroneously - // report an address of 0.0.0.0: https://bugs.torproject.org/33157. - if clientIP.IsUnspecified() { - return "" - } - // Add a dummy port number. USERADDR requires a port number. - return (&net.TCPAddr{IP: clientIP, Port: 1, Zone: ""}).String() -} - -var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, -} - -// clientIDAddrMap stores short-term mappings from ClientIDs to IP addresses. -// When we call pt.DialOr, tor wants us to provide a USERADDR string that -// represents the remote IP address of the client (for metrics purposes, etc.). -// This data structure bridges the gap between ServeHTTP, which knows about IP -// addresses, and handleStream, which is what calls pt.DialOr. The common piece -// of information linking both ends of the chain is the ClientID, which is -// attached to the WebSocket connection and every session. -var clientIDAddrMap = newClientIDMap(clientIDAddrMapCapacity) - -// overrideReadConn is a net.Conn with an overridden Read method. Compare to -// recordingConn at -// https://dave.cheney.net/2015/05/22/struct-composition-with-go. -type overrideReadConn struct { - net.Conn - io.Reader -} - -func (conn *overrideReadConn) Read(p []byte) (int, error) { - return conn.Reader.Read(p) -} - -type HTTPHandler struct { - // pconn is the adapter layer between stream-oriented WebSocket - // connections and the packet-oriented KCP layer. - pconn *turbotunnel.QueuePacketConn -} - -func (handler *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ws, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Println(err) - return - } - - conn := websocketconn.New(ws) - defer conn.Close() - - // Pass the address of client as the remote address of incoming connection - clientIPParam := r.URL.Query().Get("client_ip") - addr := clientAddr(clientIPParam) - - var token [len(turbotunnel.Token)]byte - _, err = io.ReadFull(conn, token[:]) - if err != nil { - // Don't bother logging EOF: that happens with an unused - // connection, which clients make frequently as they maintain a - // pool of proxies. - if err != io.EOF { - log.Printf("reading token: %v", err) - } - return - } - - switch { - case bytes.Equal(token[:], turbotunnel.Token[:]): - err = turbotunnelMode(conn, addr, handler.pconn) - default: - // We didn't find a matching token, which means that we are - // dealing with a client that doesn't know about such things. - // "Unread" the token by constructing a new Reader and pass it - // to the old one-session-per-WebSocket mode. - conn2 := &overrideReadConn{Conn: conn, Reader: io.MultiReader(bytes.NewReader(token[:]), conn)} - err = oneshotMode(conn2, addr) - } - if err != nil { - log.Println(err) - return - } -} - -// oneshotMode handles clients that did not send turbotunnel.Token at the start -// of their stream. These clients use the WebSocket as a raw pipe, and expect -// their session to begin and end when this single WebSocket does. -func oneshotMode(conn net.Conn, addr string) error { - statsChannel <- addr != "" - or, err := pt.DialOr(&ptInfo, addr, ptMethodName) - if err != nil { - return fmt.Errorf("failed to connect to ORPort: %s", err) - } - defer or.Close() - - proxy(or, conn) - - return nil -} - -// turbotunnelMode handles clients that sent turbotunnel.Token at the start of -// their stream. These clients expect to send and receive encapsulated packets, -// with a long-lived session identified by ClientID. -func turbotunnelMode(conn net.Conn, addr string, pconn *turbotunnel.QueuePacketConn) error { - // Read the ClientID prefix. Every packet encapsulated in this WebSocket - // connection pertains to the same ClientID. - var clientID turbotunnel.ClientID - _, err := io.ReadFull(conn, clientID[:]) - if err != nil { - return fmt.Errorf("reading ClientID: %v", err) - } - - // Store a a short-term mapping from the ClientID to the client IP - // address attached to this WebSocket connection. tor will want us to - // provide a client IP address when we call pt.DialOr. But a KCP session - // does not necessarily correspond to any single IP address--it's - // composed of packets that are carried in possibly multiple WebSocket - // streams. We apply the heuristic that the IP address of the most - // recent WebSocket connection that has had to do with a session, at the - // time the session is established, is the IP address that should be - // credited for the entire KCP session. - clientIDAddrMap.Set(clientID, addr) - - errCh := make(chan error) - - // The remainder of the WebSocket stream consists of encapsulated - // packets. We read them one by one and feed them into the - // QueuePacketConn on which kcp.ServeConn was set up, which eventually - // leads to KCP-level sessions in the acceptSessions function. - go func() { - for { - p, err := encapsulation.ReadData(conn) - if err != nil { - errCh <- err - break - } - pconn.QueueIncoming(p, clientID) - } - }() - - // At the same time, grab packets addressed to this ClientID and - // encapsulate them into the downstream. - go func() { - // Buffer encapsulation.WriteData operations to keep length - // prefixes in the same send as the data that follows. - bw := bufio.NewWriter(conn) - for p := range pconn.OutgoingQueue(clientID) { - _, err := encapsulation.WriteData(bw, p) - if err == nil { - err = bw.Flush() - } - if err != nil { - errCh <- err - break - } - } - }() - - // Wait until one of the above loops terminates. The closing of the - // WebSocket connection will terminate the other one. - <-errCh - - return nil -} - -// handleStream bidirectionally connects a client stream with the ORPort. -func handleStream(stream net.Conn, addr string) error { - statsChannel <- addr != "" - or, err := pt.DialOr(&ptInfo, addr, ptMethodName) - if err != nil { - return fmt.Errorf("connecting to ORPort: %v", err) - } - defer or.Close() - - proxy(or, stream) - - return nil -} - -// acceptStreams layers an smux.Session on the KCP connection and awaits streams -// on it. Passes each stream to handleStream. -func acceptStreams(conn *kcp.UDPSession) error { - // Look up the IP address associated with this KCP session, via the - // ClientID that is returned by the session's RemoteAddr method. - addr, ok := clientIDAddrMap.Get(conn.RemoteAddr().(turbotunnel.ClientID)) - if !ok { - // This means that the map is tending to run over capacity, not - // just that there was not client_ip on the incoming connection. - // We store "" in the map in the absence of client_ip. This log - // message means you should increase clientIDAddrMapCapacity. - log.Printf("no address in clientID-to-IP map (capacity %d)", clientIDAddrMapCapacity) - } - - smuxConfig := smux.DefaultConfig() - smuxConfig.Version = 2 - smuxConfig.KeepAliveTimeout = 10 * time.Minute - sess, err := smux.Server(conn, smuxConfig) - if err != nil { - return err - } - +func acceptLoop(ln net.Listener) { for { - stream, err := sess.AcceptStream() + conn, err := ln.Accept() if err != nil { if err, ok := err.(net.Error); ok && err.Temporary() { continue } - return err + log.Printf("Snowflake accept error: %s", err) + break } - go func() { - defer stream.Close() - err := handleStream(stream, addr) - if err != nil { - log.Printf("handleStream: %v", err) - } - }() - } -} + defer conn.Close() -// acceptSessions listens for incoming KCP connections and passes them to -// acceptStreams. It is handler.ServeHTTP that provides the network interface -// that drives this function. -func acceptSessions(ln *kcp.Listener) error { - for { - conn, err := ln.AcceptKCP() + addr := conn.RemoteAddr().String() + statsChannel <- addr != "" + or, err := pt.DialOr(&ptInfo, addr, ptMethodName) if err != nil { - if err, ok := err.(net.Error); ok && err.Temporary() { - continue - } - return err + log.Printf("failed to connect to ORPort: %s", err) + continue } - // Permit coalescing the payloads of consecutive sends. - conn.SetStreamMode(true) - // Set the maximum send and receive window sizes to a high number - // Removes KCP bottlenecks: https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40026 - conn.SetWindowSize(65535, 65535) - // Disable the dynamic congestion window (limit only by the - // maximum of local and remote static windows). - conn.SetNoDelay( - 0, // default nodelay - 0, // default interval - 0, // default resend - 1, // nc=1 => congestion window off - ) - go func() { - defer conn.Close() - err := acceptStreams(conn) - if err != nil && err != io.ErrClosedPipe { - log.Printf("acceptStreams: %v", err) - } - }() + defer or.Close() + go proxy(or, conn) } } -func initServer(addr *net.TCPAddr, - getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error), - listenAndServe func(*http.Server, chan<- error)) (*http.Server, error) { - // We're not capable of listening on port 0 (i.e., an ephemeral port - // unknown in advance). The reason is that while the net/http package - // exposes ListenAndServe and ListenAndServeTLS, those functions never - // return, so there's no opportunity to find out what the port number - // is, in between the Listen and Serve steps. - // https://groups.google.com/d/msg/Golang-nuts/3F1VRCCENp8/3hcayZiwYM8J - if addr.Port == 0 { - return nil, fmt.Errorf("cannot listen on port %d; configure a port using ServerTransportListenAddr", addr.Port) - } - - handler := HTTPHandler{ - // pconn is shared among all connections to this server. It - // overlays packet-based client sessions on top of ephemeral - // WebSocket connections. - pconn: turbotunnel.NewQueuePacketConn(addr, clientMapTimeout), - } - server := &http.Server{ - Addr: addr.String(), - Handler: &handler, - ReadTimeout: requestTimeout, - } - // We need to override server.TLSConfig.GetCertificate--but first - // server.TLSConfig needs to be non-nil. If we just create our own new - // &tls.Config, it will lack the default settings that the net/http - // package sets up for things like HTTP/2. Therefore we first call - // http2.ConfigureServer for its side effect of initializing - // server.TLSConfig properly. An alternative would be to make a dummy - // net.Listener, call Serve on it, and let it return. - // https://github.com/golang/go/issues/16588#issuecomment-237386446 - err := http2.ConfigureServer(server, nil) - if err != nil { - return server, err - } - server.TLSConfig.GetCertificate = getCertificate - - // Another unfortunate effect of the inseparable net/http ListenAndServe - // is that we can't check for Listen errors like "permission denied" and - // "address already in use" without potentially entering the infinite - // loop of Serve. The hack we apply here is to wait a short time, - // listenAndServeErrorTimeout, to see if an error is returned (because - // it's better if the error message goes to the tor log through - // SMETHOD-ERROR than if it only goes to the snowflake log). - errChan := make(chan error) - go listenAndServe(server, errChan) - select { - case err = <-errChan: - break - case <-time.After(listenAndServeErrorTimeout): - break - } - - // Start a KCP engine, set up to read and write its packets over the - // WebSocket connections that arrive at the web server. - // handler.ServeHTTP is responsible for encapsulation/decapsulation of - // packets on behalf of KCP. KCP takes those packets and turns them into - // sessions which appear in the acceptSessions function. - ln, err := kcp.ServeConn(nil, 0, 0, handler.pconn) - if err != nil { - server.Close() - return server, err - } - go func() { - defer ln.Close() - err := acceptSessions(ln) - if err != nil { - log.Printf("acceptSessions: %v", err) - } - }() - - return server, err -} - -func startServer(addr *net.TCPAddr) (*http.Server, error) { - return initServer(addr, nil, func(server *http.Server, errChan chan<- error) { - log.Printf("listening with plain HTTP on %s", addr) - err := server.ListenAndServe() - if err != nil { - log.Printf("error in ListenAndServe: %s", err) - } - errChan <- err - }) -} - -func startServerTLS(addr *net.TCPAddr, getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error)) (*http.Server, error) { - return initServer(addr, getCertificate, func(server *http.Server, errChan chan<- error) { - log.Printf("listening with HTTPS on %s", addr) - err := server.ListenAndServeTLS("", "") - if err != nil { - log.Printf("error in ListenAndServeTLS: %s", err) - } - errChan <- err - }) -} - func getCertificateCacheDir() (string, error) { stateDir, err := pt.MakeStateDir() if err != nil { @@ -535,7 +173,7 @@ func main() { // https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http-challenge needHTTP01Listener := !disableTLS - servers := make([]*http.Server, 0) + listeners := make([]net.Listener, 0) for _, bindaddr := range ptInfo.Bindaddrs { if bindaddr.MethodName != ptMethodName { pt.SmethodError(bindaddr.MethodName, "no such method") @@ -560,29 +198,47 @@ func main() { go func() { log.Fatal(server.Serve(lnHTTP01)) }() - servers = append(servers, server) + listeners = append(listeners, lnHTTP01) needHTTP01Listener = false } - var server *http.Server + // We're not capable of listening on port 0 (i.e., an ephemeral port + // unknown in advance). The reason is that while the net/http package + // exposes ListenAndServe and ListenAndServeTLS, those functions never + // return, so there's no opportunity to find out what the port number + // is, in between the Listen and Serve steps. + // https://groups.google.com/d/msg/Golang-nuts/3F1VRCCENp8/3hcayZiwYM8J + if bindaddr.Addr.Port == 0 { + err := fmt.Errorf( + "cannot listen on port %d; configure a port using ServerTransportListenAddr", + bindaddr.Addr.Port) + log.Printf("error opening listener: %s", err) + pt.SmethodError(bindaddr.MethodName, err.Error()) + continue + } + + var transport *sf.Transport args := pt.Args{} if disableTLS { args.Add("tls", "no") - server, err = startServer(bindaddr.Addr) + transport = sf.NewSnowflakeServer(nil) } else { args.Add("tls", "yes") for _, hostname := range acmeHostnames { args.Add("hostname", hostname) } - server, err = startServerTLS(bindaddr.Addr, certManager.GetCertificate) + transport = sf.NewSnowflakeServer(certManager.GetCertificate) } + ln, err := transport.Listen(bindaddr.Addr) if err != nil { log.Printf("error opening listener: %s", err) pt.SmethodError(bindaddr.MethodName, err.Error()) continue } + defer ln.Close() + go acceptLoop(ln) pt.SmethodArgs(bindaddr.MethodName, bindaddr.Addr, args) - servers = append(servers, server) + listeners = append(listeners, ln) } pt.SmethodsDone() @@ -606,7 +262,7 @@ func main() { // Signal received, shut down. log.Printf("caught signal %q, exiting", sig) - for _, server := range servers { - server.Close() + for _, ln := range listeners { + ln.Close() } } diff --git a/server/server_test.go b/server/server_test.go deleted file mode 100644 index ba00d16..0000000 --- a/server/server_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "net" - "net/http" - "strconv" - "testing" - - "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" - "github.com/gorilla/websocket" - . "github.com/smartystreets/goconvey/convey" -) - -func TestClientAddr(t *testing.T) { - Convey("Testing clientAddr", t, func() { - // good tests - for _, test := range []struct { - input string - expected net.IP - }{ - {"1.2.3.4", net.ParseIP("1.2.3.4")}, - {"1:2::3:4", net.ParseIP("1:2::3:4")}, - } { - useraddr := clientAddr(test.input) - host, port, err := net.SplitHostPort(useraddr) - if err != nil { - t.Errorf("clientAddr(%q) → SplitHostPort error %v", test.input, err) - continue - } - if !test.expected.Equal(net.ParseIP(host)) { - t.Errorf("clientAddr(%q) → host %q, not %v", test.input, host, test.expected) - } - portNo, err := strconv.Atoi(port) - if err != nil { - t.Errorf("clientAddr(%q) → port %q", test.input, port) - continue - } - if portNo == 0 { - t.Errorf("clientAddr(%q) → port %d", test.input, portNo) - } - } - - // bad tests - for _, input := range []string{ - "", - "abc", - "1.2.3.4.5", - "[12::34]", - "0.0.0.0", - "[::]", - } { - useraddr := clientAddr(input) - if useraddr != "" { - t.Errorf("clientAddr(%q) → %q, not %q", input, useraddr, "") - } - } - }) -} - -type StubHandler struct{} - -func (handler *StubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ws, _ := upgrader.Upgrade(w, r, nil) - - conn := websocketconn.New(ws) - defer conn.Close() - - //dial stub OR - or, _ := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP("localhost"), Port: 8889}) - - proxy(or, conn) -} - -func Test(t *testing.T) { - Convey("Websocket server", t, func() { - //Set up the snowflake web server - ipStr, portStr, _ := net.SplitHostPort(":8888") - port, _ := strconv.ParseUint(portStr, 10, 16) - addr := &net.TCPAddr{IP: net.ParseIP(ipStr), Port: int(port)} - Convey("We don't listen on port 0", func() { - addr = &net.TCPAddr{IP: net.ParseIP(ipStr), Port: 0} - server, err := initServer(addr, nil, - func(server *http.Server, errChan chan<- error) { - return - }) - So(err, ShouldNotBeNil) - So(server, ShouldBeNil) - }) - - Convey("Plain HTTP server accepts connections", func(c C) { - server, err := startServer(addr) - So(err, ShouldBeNil) - - ws, _, err := websocket.DefaultDialer.Dial("ws://localhost:8888", nil) - wsConn := websocketconn.New(ws) - So(err, ShouldEqual, nil) - So(wsConn, ShouldNotEqual, nil) - - server.Close() - wsConn.Close() - - }) - Convey("Handler proxies data", func(c C) { - - laddr := &net.TCPAddr{IP: net.ParseIP("localhost"), Port: 8889} - - go func() { - - //stub OR - listener, err := net.ListenTCP("tcp", laddr) - c.So(err, ShouldBeNil) - conn, err := listener.Accept() - c.So(err, ShouldBeNil) - - b := make([]byte, 5) - n, err := conn.Read(b) - c.So(err, ShouldBeNil) - c.So(n, ShouldEqual, 5) - c.So(b, ShouldResemble, []byte("Hello")) - - n, err = conn.Write([]byte("world!")) - c.So(n, ShouldEqual, 6) - c.So(err, ShouldBeNil) - }() - - //overwite handler - server, err := initServer(addr, nil, - func(server *http.Server, errChan chan<- error) { - server.ListenAndServe() - }) - So(err, ShouldBeNil) - - var handler StubHandler - server.Handler = &handler - - ws, _, err := websocket.DefaultDialer.Dial("ws://localhost:8888", nil) - So(err, ShouldEqual, nil) - wsConn := websocketconn.New(ws) - So(wsConn, ShouldNotEqual, nil) - - wsConn.Write([]byte("Hello")) - b := make([]byte, 6) - n, err := wsConn.Read(b) - So(n, ShouldEqual, 6) - So(b, ShouldResemble, []byte("world!")) - - wsConn.Close() - server.Close() - - }) - - }) -} From 7c9005bed3e353c4e108355abd1ed4b35099f2ea Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 12 May 2021 09:32:07 -0400 Subject: [PATCH 177/385] Ensure turbotunnel read and write loop terminate Introduce a waitgroup and done channel to ensure that both the read and write gorouting for turbotunnel connections terminate when the connection is closed. --- common/turbotunnel/clientmap.go | 1 + server/lib/http.go | 40 +++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/common/turbotunnel/clientmap.go b/common/turbotunnel/clientmap.go index fa12915..53d0302 100644 --- a/common/turbotunnel/clientmap.go +++ b/common/turbotunnel/clientmap.go @@ -140,5 +140,6 @@ func (inner *clientMapInner) Pop() interface{} { inner.byAge = inner.byAge[:n-1] // Remove from byAddr map. delete(inner.byAddr, record.Addr) + close(record.SendQueue) return record } diff --git a/server/lib/http.go b/server/lib/http.go index b1c453c..c612422 100644 --- a/server/lib/http.go +++ b/server/lib/http.go @@ -8,6 +8,7 @@ import ( "log" "net" "net/http" + "sync" "time" "git.torproject.org/pluggable-transports/snowflake.git/common/encapsulation" @@ -139,18 +140,21 @@ func turbotunnelMode(conn net.Conn, addr net.Addr, pconn *turbotunnel.QueuePacke // credited for the entire KCP session. clientIDAddrMap.Set(clientID, addr.String()) - errCh := make(chan error) + var wg sync.WaitGroup + wg.Add(2) + done := make(chan struct{}) // The remainder of the WebSocket stream consists of encapsulated // packets. We read them one by one and feed them into the // QueuePacketConn on which kcp.ServeConn was set up, which eventually // leads to KCP-level sessions in the acceptSessions function. go func() { + defer wg.Done() + defer close(done) // Signal the write loop to finish for { p, err := encapsulation.ReadData(conn) if err != nil { - errCh <- err - break + return } pconn.QueueIncoming(p, clientID) } @@ -159,24 +163,32 @@ func turbotunnelMode(conn net.Conn, addr net.Addr, pconn *turbotunnel.QueuePacke // At the same time, grab packets addressed to this ClientID and // encapsulate them into the downstream. go func() { + defer wg.Done() + defer conn.Close() // Signal the read loop to finish + // Buffer encapsulation.WriteData operations to keep length // prefixes in the same send as the data that follows. bw := bufio.NewWriter(conn) - for p := range pconn.OutgoingQueue(clientID) { - _, err := encapsulation.WriteData(bw, p) - if err == nil { - err = bw.Flush() - } - if err != nil { - errCh <- err - break + for { + select { + case <-done: + return + case p, ok := <-pconn.OutgoingQueue(clientID): + if !ok { + return + } + _, err := encapsulation.WriteData(bw, p) + if err == nil { + err = bw.Flush() + } + if err != nil { + return + } } } }() - // Wait until one of the above loops terminates. The closing of the - // WebSocket connection will terminate the other one. - <-errCh + wg.Wait() return nil } From 0054cb2dec19e89e07b8c5a6d8b9d23589842deb Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 12 May 2021 10:40:56 -0400 Subject: [PATCH 178/385] Update .gitlab-ci.yml after refactor of client --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7288b5f..e1a391c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -107,7 +107,7 @@ android: - git -C $CI_PROJECT_DIR clean -fdx - cd $CI_PROJECT_DIR/client # gomobile builds a shared library not a CLI executable - - sed -i 's,^package main$,package snowflakeclient,' snowflake.go client_test.go + - sed -i 's,^package main$,package snowflakeclient,' snowflake.go - gomobile bind -v -target=android . <<: *test-template From 160ae2dd71879ab83226a3d4eb2b15cefdb570f4 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Tue, 18 May 2021 20:06:28 -0400 Subject: [PATCH 179/385] Make promMetrics not a global Doesn't seem like it needs to exist outside of the metrics struct. Also, the call to logMetrics is moved to the constructor. A metrics instance is only created when a BrokerContext is created, which only happens at startup. The sync of only doing that once is left for documentation purposes, since it doesn't hurt, but also seems redundant. --- broker/broker.go | 16 ++++++++-------- broker/metrics.go | 25 ++++++++++++------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/broker/broker.go b/broker/broker.go index 8d7a314..8c1159e 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -151,7 +151,7 @@ func (ctx *BrokerContext) Broker() { } else { heap.Remove(ctx.restrictedSnowflakes, snowflake.index) } - promMetrics.AvailableProxies.With(prometheus.Labels{"nat": request.natType, "type": request.proxyType}).Dec() + ctx.metrics.promMetrics.AvailableProxies.With(prometheus.Labels{"nat": request.natType, "type": request.proxyType}).Dec() delete(ctx.idToSnowflake, snowflake.id) close(request.offerChannel) } @@ -177,7 +177,7 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri } else { heap.Push(ctx.restrictedSnowflakes, snowflake) } - promMetrics.AvailableProxies.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc() + ctx.metrics.promMetrics.AvailableProxies.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc() ctx.snowflakeLock.Unlock() ctx.idToSnowflake[id] = snowflake return snowflake @@ -216,7 +216,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { if nil == offer { ctx.metrics.lock.Lock() ctx.metrics.proxyIdleCount++ - promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "idle"}).Inc() + ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "idle"}).Inc() ctx.metrics.lock.Unlock() b, err = messages.EncodePollResponse("", false, "") @@ -228,7 +228,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { w.Write(b) return } - promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "matched"}).Inc() + ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "matched"}).Inc() b, err = messages.EncodePollResponse(string(offer.sdp), true, offer.natType) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -282,7 +282,7 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { if numSnowflakes <= 0 { ctx.metrics.lock.Lock() ctx.metrics.clientDeniedCount++ - promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "denied"}).Inc() + ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "denied"}).Inc() if offer.natType == NATUnrestricted { ctx.metrics.clientUnrestrictedDeniedCount++ } else { @@ -304,7 +304,7 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { case answer := <-snowflake.answerChannel: ctx.metrics.lock.Lock() ctx.metrics.clientProxyMatchCount++ - promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "matched"}).Inc() + ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "matched"}).Inc() ctx.metrics.lock.Unlock() if _, err := w.Write(answer); err != nil { log.Printf("unable to write answer with error: %v", err) @@ -321,7 +321,7 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { } ctx.snowflakeLock.Lock() - promMetrics.AvailableProxies.With(prometheus.Labels{"nat": snowflake.natType, "type": snowflake.proxyType}).Dec() + ctx.metrics.promMetrics.AvailableProxies.With(prometheus.Labels{"nat": snowflake.natType, "type": snowflake.proxyType}).Dec() delete(ctx.idToSnowflake, snowflake.id) ctx.snowflakeLock.Unlock() } @@ -506,7 +506,7 @@ func main() { http.Handle("/answer", SnowflakeHandler{ctx, proxyAnswers}) http.Handle("/debug", SnowflakeHandler{ctx, debugHandler}) http.Handle("/metrics", MetricsHandler{metricsFilename, metricsHandler}) - http.Handle("/prometheus", promhttp.HandlerFor(promMetrics.registry, promhttp.HandlerOpts{})) + http.Handle("/prometheus", promhttp.HandlerFor(ctx.metrics.promMetrics.registry, promhttp.HandlerOpts{})) server := http.Server{ Addr: addr, diff --git a/broker/metrics.go b/broker/metrics.go index ad55bcb..a79a1c4 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -17,11 +17,6 @@ import ( "github.com/prometheus/client_golang/prometheus" ) -var ( - once sync.Once - promMetrics = initPrometheus() -) - const ( prometheusNamespace = "snowflake" metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds @@ -54,8 +49,11 @@ type Metrics struct { clientUnrestrictedDeniedCount uint clientProxyMatchCount uint - //synchronization for access to snowflake metrics + // synchronization for access to snowflake metrics lock sync.Mutex + + promMetrics *PromMetrics + once sync.Once } type record struct { @@ -147,7 +145,7 @@ func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType stri m.countryStats.unknown[addr] = true } - promMetrics.ProxyTotal.With(prometheus.Labels{ + m.promMetrics.ProxyTotal.With(prometheus.Labels{ "nat": natType, "type": proxyType, "cc": country, @@ -201,9 +199,10 @@ func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) { } m.logger = metricsLogger + m.promMetrics = initPrometheus() // Write to log file every hour with updated metrics - go once.Do(m.logMetrics) + go m.once.Do(m.logMetrics) return m, nil } @@ -267,9 +266,8 @@ type PromMetrics struct { AvailableProxies *prometheus.GaugeVec } -//Initialize metrics for prometheus exporter +// Initialize metrics for prometheus exporter func initPrometheus() *PromMetrics { - promMetrics := &PromMetrics{} promMetrics.registry = prometheus.NewRegistry() @@ -311,9 +309,10 @@ func initPrometheus() *PromMetrics { ) // We need to register our metrics so they can be exported. - promMetrics.registry.MustRegister(promMetrics.ClientPollTotal, promMetrics.ProxyPollTotal, - promMetrics.ProxyTotal, promMetrics.AvailableProxies) + promMetrics.registry.MustRegister( + promMetrics.ClientPollTotal, promMetrics.ProxyPollTotal, + promMetrics.ProxyTotal, promMetrics.AvailableProxies, + ) return promMetrics - } From 7ef49272fa8c4169a5ec13988a71011dbe14bbfb Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 20 May 2021 15:36:08 -0400 Subject: [PATCH 180/385] Remove sync.Once from around logMetrics Follow up to 160ae2d Analysis by @dcf, > I don't think the sync.Once around logMetrics is necessary anymore. Its original purpose was to inhibit logging on later file handles of metrics.log, if there were more than one opened. See 171c55a9 and #29734 (comment 2593039) "Making a singleton *Metrics variable causes problems with how Convey does tests. It shouldn't be called more than once, but for now I'm using sync.Once on the logging at least so it's explicit." Commit ba4fe1a7 changed it so that metrics.log is opened in main, used to create a *log.Logger, and that same instance of *log.Logger is passed to both NewMetrics and NewBrokerContext. It's safe to share the same *log.Logger across multiple BrokerContext. --- broker/metrics.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/broker/metrics.go b/broker/metrics.go index a79a1c4..e8a6b0c 100644 --- a/broker/metrics.go +++ b/broker/metrics.go @@ -53,7 +53,6 @@ type Metrics struct { lock sync.Mutex promMetrics *PromMetrics - once sync.Once } type record struct { @@ -202,7 +201,7 @@ func NewMetrics(metricsLogger *log.Logger) (*Metrics, error) { m.promMetrics = initPrometheus() // Write to log file every hour with updated metrics - go m.once.Do(m.logMetrics) + go m.logMetrics() return m, nil } From ef4d0a1da56e15327173923fa14a28d9ca40789c Mon Sep 17 00:00:00 2001 From: David Fifield Date: Wed, 19 May 2021 13:03:23 +0200 Subject: [PATCH 181/385] Stop timers before expiration If we don't stop them explicitly, the timers will not get garbage collected until they timeout: https://medium.com/@oboturov/golang-time-after-is-not-garbage-collected-4cbc94740082 Related to #40039 --- probetest/probetest.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/probetest/probetest.go b/probetest/probetest.go index f9bc96b..4158fa5 100644 --- a/probetest/probetest.go +++ b/probetest/probetest.go @@ -147,10 +147,14 @@ func probeHandler(w http.ResponseWriter, r *http.Request) { // advanced to PeerConnectionStateConnected in this time, // destroy the peer connection and return the token. go func() { + timer := time.NewTimer(dataChannelTimeout) + defer timer.Stop() + select { case <-dataChan: - case <-time.After(dataChannelTimeout): + case <-timer.C: } + if err := pc.Close(); err != nil { log.Printf("Error calling pc.Close: %v", err) } From 01a96c7d95bfcc6ec5b6a770c8a56ef7da6605f4 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Mon, 24 May 2021 14:36:24 -0600 Subject: [PATCH 182/385] Fix error handling around transport.Dial. The code checked for and displayed an error, but would then go on to call copyLoop on the nil Conn returned from transport.Dial. Add a return in that case, and put the cleanup operations in defer. Also remove an obsolete comment about an empty address. Obsolete because: https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/31#note_2733279 --- client/snowflake.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/client/snowflake.go b/client/snowflake.go index f19afcf..bacc389 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -69,17 +69,15 @@ func socksAcceptLoop(ln *pt.SocksListener, transport *sf.Transport, shutdown cha handler := make(chan struct{}) go func() { - // pass an empty address because the broker chooses the bridge + defer close(handler) sconn, err := transport.Dial() if err != nil { log.Printf("dial error: %s", err) + return } + defer sconn.Close() // copy between the created Snowflake conn and the SOCKS conn copyLoop(conn, sconn) - sconn.Close() - close(handler) - return - }() select { case <-shutdown: From ae7cc478fd345a1e588f8315ec980809c6806372 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Mon, 24 May 2021 15:09:29 -0600 Subject: [PATCH 183/385] Release resources in client Transport.Dial on error. Make a stack of cleanup functions to run (as with defer), but clear the stack before returning if no error occurs. Uselessly pushing the stream.Close() cleanup just before clearing the stack is an intentional safeguard, for in case additional operations are added before the return in the future. Fixes #40042. --- client/lib/snowflake.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/lib/snowflake.go b/client/lib/snowflake.go index 6e87b81..f643c0a 100644 --- a/client/lib/snowflake.go +++ b/client/lib/snowflake.go @@ -74,11 +74,21 @@ func NewSnowflakeClient(brokerURL, frontDomain string, iceAddresses []string, ke // Create a new Snowflake connection. Starts the collection of snowflakes and returns a // smux Stream. func (t *Transport) Dial() (net.Conn, error) { + // Cleanup functions to run before returning, in case of an error. + var cleanup []func() + defer func() { + // Run cleanup in reverse order, as defer does. + for i := len(cleanup) - 1; i >= 0; i-- { + cleanup[i]() + } + }() + // Prepare to collect remote WebRTC peers. snowflakes, err := NewPeers(t.dialer) if err != nil { return nil, err } + cleanup = append(cleanup, func() { snowflakes.End() }) // Use a real logger to periodically output how much traffic is happening. snowflakes.BytesLogger = NewBytesSyncLogger() @@ -92,15 +102,22 @@ func (t *Transport) Dial() (net.Conn, error) { if err != nil { return nil, err } + cleanup = append(cleanup, func() { + pconn.Close() + sess.Close() + }) // On the smux session we overlay a stream. stream, err := sess.OpenStream() if err != nil { return nil, err } - // Begin exchanging data. log.Printf("---- SnowflakeConn: begin stream %v ---", stream.ID()) + cleanup = append(cleanup, func() { stream.Close() }) + + // All good, clear the cleanup list. + cleanup = nil return &SnowflakeConn{Stream: stream, sess: sess, pconn: pconn, snowflakes: snowflakes}, nil } From 270eb218037ca78c5a09d8e8cae9187a22cee122 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 5 May 2021 15:31:39 -0400 Subject: [PATCH 184/385] Encode client-broker messages as json in HTTP body Send the client poll request and response in a json-encoded format in the HTTP request body rather than sending the data in HTTP headers. This will pave the way for using domain-fronting alternatives for the Snowflake rendezvous. --- broker/broker.go | 122 +++++++++++++--- broker/snowflake-broker_test.go | 130 +++++++++++++++--- broker/snowflake-heap.go | 2 +- client/lib/lib_test.go | 22 +-- client/lib/rendezvous.go | 36 +++-- common/messages/client.go | 107 ++++++++++++++ .../{proxy_test.go => messages_test.go} | 116 ++++++++++++++++ 7 files changed, 472 insertions(+), 63 deletions(-) create mode 100644 common/messages/client.go rename common/messages/{proxy_test.go => messages_test.go} (71%) diff --git a/broker/broker.go b/broker/broker.go index 8c1159e..906c210 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -6,6 +6,7 @@ SessionDescriptions in order to negotiate a WebRTC connection. package main import ( + "bytes" "container/heap" "crypto/tls" "flag" @@ -39,6 +40,16 @@ const ( NATUnrestricted = "unrestricted" ) +// We support two client message formats. The legacy format is for backwards +// combatability and relies heavily on HTTP headers and status codes to convey +// information. +type clientVersion int + +const ( + v0 clientVersion = iota //legacy version + v1 +) + type BrokerContext struct { snowflakes *SnowflakeHeap restrictedSnowflakes *SnowflakeHeap @@ -90,7 +101,7 @@ type MetricsHandler struct { func (sh SnowflakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Session-ID, Snowflake-NAT-Type") + w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Session-ID") // Return early if it's CORS preflight. if "OPTIONS" == r.Method { return @@ -170,7 +181,7 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri snowflake.proxyType = proxyType snowflake.natType = natType snowflake.offerChannel = make(chan *ClientOffer) - snowflake.answerChannel = make(chan []byte) + snowflake.answerChannel = make(chan string) ctx.snowflakeLock.Lock() if natType == NATUnrestricted { heap.Push(ctx.snowflakes, snowflake) @@ -245,6 +256,20 @@ type ClientOffer struct { sdp []byte } +// Sends an encoded response to the client and an +// HTTP server error if the response encoding fails +func sendClientResponse(resp *messages.ClientPollResponse, w http.ResponseWriter) { + data, err := resp.EncodePollResponse() + if err != nil { + log.Printf("error encoding answer") + w.WriteHeader(http.StatusInternalServerError) + } else { + if _, err := w.Write([]byte(data)); err != nil { + log.Printf("unable to write answer with error: %v", err) + } + } +} + /* Expects a WebRTC SDP offer in the Request to give to an assigned snowflake proxy, which responds with the SDP answer to be sent in @@ -252,19 +277,55 @@ the HTTP response back to the client. */ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { var err error + var version clientVersion startTime := time.Now() - offer := &ClientOffer{} - offer.sdp, err = ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) - if nil != err { - log.Println("Invalid data.") - w.WriteHeader(http.StatusBadRequest) + body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) + if err != nil { + log.Printf("Error reading client request: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) return } + if len(body) > 0 && body[0] == '{' { + version = v0 + } else { + parts := bytes.SplitN(body, []byte("\n"), 2) + if len(parts) < 2 { + // no version number found + err := fmt.Errorf("unsupported message version") + sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, w) + return + } + body = parts[1] + if string(parts[0]) == "1.0" { + version = v1 - offer.natType = r.Header.Get("Snowflake-NAT-Type") - if offer.natType == "" { - offer.natType = NATUnknown + } else { + err := fmt.Errorf("unsupported message version") + sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, w) + return + } + } + + var offer *ClientOffer + switch version { + case v0: + offer = &ClientOffer{ + natType: r.Header.Get("Snowflake-NAT-Type"), + sdp: body, + } + case v1: + req, err := messages.DecodeClientPollRequest(body) + if err != nil { + sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, w) + return + } + offer = &ClientOffer{ + natType: req.NAT, + sdp: []byte(req.Offer), + } + default: + panic("unknown version") } // Only hand out known restricted snowflakes to unrestricted clients @@ -289,7 +350,15 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { ctx.metrics.clientRestrictedDeniedCount++ } ctx.metrics.lock.Unlock() - w.WriteHeader(http.StatusServiceUnavailable) + switch version { + case v0: + w.WriteHeader(http.StatusServiceUnavailable) + case v1: + resp := &messages.ClientPollResponse{Error: "no snowflake proxies currently available"} + sendClientResponse(resp, w) + default: + panic("unknown version") + } return } // Otherwise, find the most available snowflake proxy, and pass the offer to it. @@ -306,17 +375,36 @@ func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { ctx.metrics.clientProxyMatchCount++ ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "matched"}).Inc() ctx.metrics.lock.Unlock() - if _, err := w.Write(answer); err != nil { - log.Printf("unable to write answer with error: %v", err) + switch version { + case v0: + if _, err := w.Write([]byte(answer)); err != nil { + log.Printf("unable to write answer with error: %v", err) + } + case v1: + resp := &messages.ClientPollResponse{Answer: answer} + sendClientResponse(resp, w) + default: + panic("unknown version") } // Initial tracking of elapsed time. ctx.metrics.clientRoundtripEstimate = time.Since(startTime) / time.Millisecond case <-time.After(time.Second * ClientTimeout): log.Println("Client: Timed out.") - w.WriteHeader(http.StatusGatewayTimeout) - if _, err := w.Write([]byte("timed out waiting for answer!")); err != nil { - log.Printf("unable to write timeout error, failed with error: %v", err) + switch version { + case v0: + w.WriteHeader(http.StatusGatewayTimeout) + if _, err := w.Write( + []byte("timed out waiting for answer!")); err != nil { + log.Printf("unable to write timeout error, failed with error: %v", + err) + } + case v1: + resp := &messages.ClientPollResponse{ + Error: "timed out waiting for answer!"} + sendClientResponse(resp, w) + default: + panic("unknown version") } } @@ -364,7 +452,7 @@ func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { w.Write(b) if success { - snowflake.answerChannel <- []byte(answer) + snowflake.answerChannel <- answer } } diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index b676b04..646fb02 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -70,10 +70,59 @@ func TestBroker(t *testing.T) { Convey("Responds to client offers...", func() { w := httptest.NewRecorder() - data := bytes.NewReader([]byte("test")) + data := bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"unknown\"}")) r, err := http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) + Convey("with error when no snowflakes are available.", func() { + clientOffers(ctx, w, r) + So(w.Code, ShouldEqual, http.StatusOK) + So(w.Body.String(), ShouldEqual, `{"error":"no snowflake proxies currently available"}`) + }) + + Convey("with a proxy answer if available.", func() { + done := make(chan bool) + // Prepare a fake proxy to respond with. + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted) + go func() { + clientOffers(ctx, w, r) + done <- true + }() + offer := <-snowflake.offerChannel + So(offer.sdp, ShouldResemble, []byte("fake")) + snowflake.answerChannel <- "fake answer" + <-done + So(w.Body.String(), ShouldEqual, `{"answer":"fake answer"}`) + So(w.Code, ShouldEqual, http.StatusOK) + }) + + Convey("Times out when no proxy responds.", func() { + if testing.Short() { + return + } + done := make(chan bool) + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted) + go func() { + clientOffers(ctx, w, r) + // Takes a few seconds here... + done <- true + }() + offer := <-snowflake.offerChannel + So(offer.sdp, ShouldResemble, []byte("fake")) + <-done + So(w.Code, ShouldEqual, http.StatusOK) + So(w.Body.String(), ShouldEqual, `{"error":"timed out waiting for answer!"}`) + }) + }) + + Convey("Responds to legacy client offers...", func() { + w := httptest.NewRecorder() + data := bytes.NewReader([]byte("{test}")) + r, err := http.NewRequest("POST", "snowflake.broker/client", data) + So(err, ShouldBeNil) + r.Header.Set("Snowflake-NAT-TYPE", "restricted") + Convey("with 503 when no snowflakes are available.", func() { clientOffers(ctx, w, r) So(w.Code, ShouldEqual, http.StatusServiceUnavailable) @@ -89,8 +138,8 @@ func TestBroker(t *testing.T) { done <- true }() offer := <-snowflake.offerChannel - So(offer.sdp, ShouldResemble, []byte("test")) - snowflake.answerChannel <- []byte("fake answer") + So(offer.sdp, ShouldResemble, []byte("{test}")) + snowflake.answerChannel <- "fake answer" <-done So(w.Body.String(), ShouldEqual, "fake answer") So(w.Code, ShouldEqual, http.StatusOK) @@ -108,10 +157,11 @@ func TestBroker(t *testing.T) { done <- true }() offer := <-snowflake.offerChannel - So(offer.sdp, ShouldResemble, []byte("test")) + So(offer.sdp, ShouldResemble, []byte("{test}")) <-done So(w.Code, ShouldEqual, http.StatusGatewayTimeout) }) + }) Convey("Responds to proxy polls...", func() { @@ -163,7 +213,7 @@ func TestBroker(t *testing.T) { }(ctx) answer := <-s.answerChannel So(w.Code, ShouldEqual, http.StatusOK) - So(answer, ShouldResemble, []byte("test")) + So(answer, ShouldResemble, "test") }) Convey("with client gone status if the proxy is not recognized", func() { @@ -272,7 +322,8 @@ func TestBroker(t *testing.T) { So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil) // Client request blocks until proxy answer arrives. - dataC := bytes.NewReader([]byte("fake offer")) + dataC := bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"unknown\"}")) wC := httptest.NewRecorder() rC, err := http.NewRequest("POST", "snowflake.broker/client", dataC) So(err, ShouldBeNil) @@ -283,7 +334,7 @@ func TestBroker(t *testing.T) { <-polled So(wP.Code, ShouldEqual, http.StatusOK) - So(wP.Body.String(), ShouldResemble, `{"Status":"client match","Offer":"fake offer","NAT":"unknown"}`) + So(wP.Body.String(), ShouldResemble, `{"Status":"client match","Offer":"fake","NAT":"unknown"}`) So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil) // Follow up with the answer request afterwards wA := httptest.NewRecorder() @@ -295,7 +346,7 @@ func TestBroker(t *testing.T) { <-done So(wC.Code, ShouldEqual, http.StatusOK) - So(wC.Body.String(), ShouldEqual, "test") + So(wC.Body.String(), ShouldEqual, `{"answer":"test"}`) }) }) } @@ -517,7 +568,8 @@ func TestMetrics(t *testing.T) { //Test addition of client failures Convey("for no proxies available", func() { w := httptest.NewRecorder() - data := bytes.NewReader([]byte("test")) + data := bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"unknown\"}")) r, err := http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) @@ -535,7 +587,8 @@ func TestMetrics(t *testing.T) { //Test addition of client matches Convey("for client-proxy match", func() { w := httptest.NewRecorder() - data := bytes.NewReader([]byte("test")) + data := bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"unknown\"}")) r, err := http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) @@ -546,8 +599,8 @@ func TestMetrics(t *testing.T) { done <- true }() offer := <-snowflake.offerChannel - So(offer.sdp, ShouldResemble, []byte("test")) - snowflake.answerChannel <- []byte("fake answer") + So(offer.sdp, ShouldResemble, []byte("fake")) + snowflake.answerChannel <- "fake answer" <-done ctx.metrics.printMetrics() @@ -556,22 +609,63 @@ func TestMetrics(t *testing.T) { //Test rounding boundary Convey("binning boundary", func() { w := httptest.NewRecorder() - data := bytes.NewReader([]byte("test")) + data := bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) r, err := http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) clientOffers(ctx, w, r) + w = httptest.NewRecorder() + data = bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) + r, err = http.NewRequest("POST", "snowflake.broker/client", data) + So(err, ShouldBeNil) clientOffers(ctx, w, r) + w = httptest.NewRecorder() + data = bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) + r, err = http.NewRequest("POST", "snowflake.broker/client", data) + So(err, ShouldBeNil) clientOffers(ctx, w, r) + w = httptest.NewRecorder() + data = bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) + r, err = http.NewRequest("POST", "snowflake.broker/client", data) + So(err, ShouldBeNil) clientOffers(ctx, w, r) + w = httptest.NewRecorder() + data = bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) + r, err = http.NewRequest("POST", "snowflake.broker/client", data) + So(err, ShouldBeNil) clientOffers(ctx, w, r) + w = httptest.NewRecorder() + data = bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) + r, err = http.NewRequest("POST", "snowflake.broker/client", data) + So(err, ShouldBeNil) clientOffers(ctx, w, r) + w = httptest.NewRecorder() + data = bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) + r, err = http.NewRequest("POST", "snowflake.broker/client", data) + So(err, ShouldBeNil) clientOffers(ctx, w, r) + w = httptest.NewRecorder() + data = bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) + r, err = http.NewRequest("POST", "snowflake.broker/client", data) + So(err, ShouldBeNil) clientOffers(ctx, w, r) ctx.metrics.printMetrics() So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\n") + w = httptest.NewRecorder() + data = bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) + r, err = http.NewRequest("POST", "snowflake.broker/client", data) + So(err, ShouldBeNil) clientOffers(ctx, w, r) buf.Reset() ctx.metrics.printMetrics() @@ -648,9 +742,9 @@ func TestMetrics(t *testing.T) { //Test client failures by NAT type Convey("client failures by NAT type", func() { w := httptest.NewRecorder() - data := bytes.NewReader([]byte("test")) + data := bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) r, err := http.NewRequest("POST", "snowflake.broker/client", data) - r.Header.Set("Snowflake-NAT-TYPE", "restricted") So(err, ShouldBeNil) clientOffers(ctx, w, r) @@ -661,8 +755,9 @@ func TestMetrics(t *testing.T) { buf.Reset() ctx.metrics.zeroMetrics() + data = bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"unrestricted\"}")) r, err = http.NewRequest("POST", "snowflake.broker/client", data) - r.Header.Set("Snowflake-NAT-TYPE", "unrestricted") So(err, ShouldBeNil) clientOffers(ctx, w, r) @@ -673,8 +768,9 @@ func TestMetrics(t *testing.T) { buf.Reset() ctx.metrics.zeroMetrics() + data = bytes.NewReader( + []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"unknown\"}")) r, err = http.NewRequest("POST", "snowflake.broker/client", data) - r.Header.Set("Snowflake-NAT-TYPE", "unknown") So(err, ShouldBeNil) clientOffers(ctx, w, r) diff --git a/broker/snowflake-heap.go b/broker/snowflake-heap.go index 16dd264..80c1f57 100644 --- a/broker/snowflake-heap.go +++ b/broker/snowflake-heap.go @@ -13,7 +13,7 @@ type Snowflake struct { proxyType string natType string offerChannel chan *ClientOffer - answerChannel chan []byte + answerChannel chan string clients int index int } diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 6140e0b..e742e06 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -176,7 +176,7 @@ func TestSnowflakeClient(t *testing.T) { Convey("Rendezvous", t, func() { transport := &MockTransport{ http.StatusOK, - []byte(`{"type":"answer","sdp":"fake"}`), + []byte(`{"answer": "{\"type\":\"answer\",\"sdp\":\"fake\"}" }`), } fakeOffer, err := util.DeserializeSessionDescription(`{"type":"offer","sdp":"test"}`) if err != nil { @@ -209,26 +209,25 @@ func TestSnowflakeClient(t *testing.T) { So(answer.SDP, ShouldResemble, "fake") }) - Convey("BrokerChannel.Negotiate fails with 503", func() { + Convey("BrokerChannel.Negotiate fails", func() { b, err := NewBrokerChannel("test.broker", "", - &MockTransport{http.StatusServiceUnavailable, []byte("\n")}, + &MockTransport{http.StatusOK, []byte(`{"error": "no snowflake proxies currently available"}`)}, false) So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldNotBeNil) So(answer, ShouldBeNil) - So(err.Error(), ShouldResemble, BrokerError503) }) - Convey("BrokerChannel.Negotiate fails with 400", func() { + Convey("BrokerChannel.Negotiate fails with unexpected error", func() { b, err := NewBrokerChannel("test.broker", "", - &MockTransport{http.StatusBadRequest, []byte("\n")}, + &MockTransport{http.StatusInternalServerError, []byte("\n")}, false) So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldNotBeNil) So(answer, ShouldBeNil) - So(err.Error(), ShouldResemble, BrokerError400) + So(err.Error(), ShouldResemble, BrokerErrorUnexpected) }) Convey("BrokerChannel.Negotiate fails with large read", func() { @@ -242,15 +241,6 @@ func TestSnowflakeClient(t *testing.T) { So(err.Error(), ShouldResemble, "unexpected EOF") }) - Convey("BrokerChannel.Negotiate fails with unexpected error", func() { - b, err := NewBrokerChannel("test.broker", "", - &MockTransport{123, []byte("")}, false) - So(err, ShouldBeNil) - answer, err := b.Negotiate(fakeOffer) - So(err, ShouldNotBeNil) - So(answer, ShouldBeNil) - So(err.Error(), ShouldResemble, BrokerErrorUnexpected) - }) }) } diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index 32da081..b89f432 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -19,14 +19,13 @@ import ( "sync" "time" + "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "git.torproject.org/pluggable-transports/snowflake.git/common/nat" "git.torproject.org/pluggable-transports/snowflake.git/common/util" "github.com/pion/webrtc/v3" ) const ( - BrokerError503 string = "No snowflake proxies currently available." - BrokerError400 string = "You sent an invalid offer in the request." BrokerErrorUnexpected string = "Unexpected error, no answer." readLimit = 100000 //Maximum number of bytes to be read from an HTTP response ) @@ -107,7 +106,20 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( if err != nil { return nil, err } - data := bytes.NewReader([]byte(offerSDP)) + + // Encode client poll request + bc.lock.Lock() + req := &messages.ClientPollRequest{ + Offer: offerSDP, + NAT: bc.NATType, + } + body, err := req.EncodePollRequest() + bc.lock.Unlock() + if err != nil { + return nil, err + } + + data := bytes.NewReader([]byte(body)) // Suffix with broker's client registration handler. clientURL := bc.url.ResolveReference(&url.URL{Path: "client"}) request, err := http.NewRequest("POST", clientURL.String(), data) @@ -117,10 +129,6 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( if "" != bc.Host { // Set true host if necessary. request.Host = bc.Host } - // include NAT-TYPE - bc.lock.Lock() - request.Header.Set("Snowflake-NAT-TYPE", bc.NATType) - bc.lock.Unlock() resp, err := bc.transport.RoundTrip(request) if nil != err { return nil, err @@ -135,11 +143,15 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( return nil, err } log.Printf("Received answer: %s", string(body)) - return util.DeserializeSessionDescription(string(body)) - case http.StatusServiceUnavailable: - return nil, errors.New(BrokerError503) - case http.StatusBadRequest: - return nil, errors.New(BrokerError400) + + resp, err := messages.DecodeClientPollResponse(body) + if err != nil { + return nil, err + } + if resp.Error != "" { + return nil, errors.New(resp.Error) + } + return util.DeserializeSessionDescription(resp.Answer) default: return nil, errors.New(BrokerErrorUnexpected) } diff --git a/common/messages/client.go b/common/messages/client.go new file mode 100644 index 0000000..1918e34 --- /dev/null +++ b/common/messages/client.go @@ -0,0 +1,107 @@ +//Package for communication with the snowflake broker + +//import "git.torproject.org/pluggable-transports/snowflake.git/common/messages" +package messages + +import ( + "encoding/json" + "fmt" +) + +const ClientVersion = "1.0" + +/* Client--Broker protocol v1.x specification: + +All messages contain the version number +followed by a new line and then the message body + := \n + := . + := | + +There are two different types of body messages, +each encoded in JSON format + +== ClientPollRequest == + := +{ + offer: + [nat: (unknown|restricted|unrestricted)] +} + +The NAT field is optional, and if it is missing a +value of "unknown" will be assumed. + +== ClientPollResponse == + := +{ + [answer: ] + [error: ] +} + +If the broker succeeded in matching the client with a proxy, +the answer field MUST contain a valid SDP answer, and the +error field MUST be empty. If the answer field is empty, the +error field MUST contain a string explaining with a reason +for the error. + +*/ + +type ClientPollRequest struct { + Offer string `json:"offer"` + NAT string `json:"nat"` +} + +// Encodes a poll message from a snowflake client +func (req *ClientPollRequest) EncodePollRequest() ([]byte, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + return append([]byte(ClientVersion+"\n"), body...), nil +} + +// Decodes a poll message from a snowflake client +func DecodeClientPollRequest(data []byte) (*ClientPollRequest, error) { + var message ClientPollRequest + + err := json.Unmarshal(data, &message) + if err != nil { + return nil, err + } + + if message.Offer == "" { + return nil, fmt.Errorf("no supplied offer") + } + + if message.NAT == "" { + message.NAT = "unknown" + } + + return &message, nil +} + +type ClientPollResponse struct { + Answer string `json:"answer,omitempty"` + Error string `json:"error,omitempty"` +} + +// Encodes a poll response for a snowflake client +func (resp *ClientPollResponse) EncodePollResponse() ([]byte, error) { + return json.Marshal(resp) +} + +// Decodes a poll response for a snowflake client +// If the Error field is empty, the Answer should be non-empty +func DecodeClientPollResponse(data []byte) (*ClientPollResponse, error) { + var message ClientPollResponse + + err := json.Unmarshal(data, &message) + if err != nil { + return nil, err + } + if message.Error == "" && message.Answer == "" { + return nil, fmt.Errorf("received empty broker response") + } + + return &message, nil +} diff --git a/common/messages/proxy_test.go b/common/messages/messages_test.go similarity index 71% rename from common/messages/proxy_test.go rename to common/messages/messages_test.go index f4191e1..3962d3b 100644 --- a/common/messages/proxy_test.go +++ b/common/messages/messages_test.go @@ -1,6 +1,7 @@ package messages import ( + "bytes" "encoding/json" "fmt" "testing" @@ -252,3 +253,118 @@ func TestEncodeProxyAnswerResponse(t *testing.T) { So(err, ShouldEqual, nil) }) } + +func TestDecodeClientPollRequest(t *testing.T) { + Convey("Context", t, func() { + for _, test := range []struct { + natType string + offer string + data string + err error + }{ + { + //version 1.0 client message + "unknown", + "fake", + `{"nat":"unknown","offer":"fake"}`, + nil, + }, + { + //version 1.0 client message + "unknown", + "fake", + `{"offer":"fake"}`, + nil, + }, + { + //unknown version + "", + "", + `{"version":"2.0"}`, + fmt.Errorf(""), + }, + { + //no offer + "", + "", + `{"nat":"unknown"}`, + fmt.Errorf(""), + }, + } { + req, err := DecodeClientPollRequest([]byte(test.data)) + if test.err == nil { + So(req.NAT, ShouldResemble, test.natType) + So(req.Offer, ShouldResemble, test.offer) + } + So(err, ShouldHaveSameTypeAs, test.err) + } + + }) +} + +func TestEncodeClientPollRequests(t *testing.T) { + Convey("Context", t, func() { + req1 := &ClientPollRequest{ + NAT: "unknown", + Offer: "fake", + } + b, err := req1.EncodePollRequest() + So(err, ShouldEqual, nil) + fmt.Println(string(b)) + parts := bytes.SplitN(b, []byte("\n"), 2) + So(string(parts[0]), ShouldEqual, "1.0") + b = parts[1] + req2, err := DecodeClientPollRequest(b) + So(err, ShouldEqual, nil) + So(req2, ShouldResemble, req1) + }) +} + +func TestDecodeClientPollResponse(t *testing.T) { + Convey("Context", t, func() { + for _, test := range []struct { + answer string + msg string + data string + }{ + { + "fake answer", + "", + `{"answer":"fake answer"}`, + }, + { + "", + "no snowflakes", + `{"error":"no snowflakes"}`, + }, + } { + resp, err := DecodeClientPollResponse([]byte(test.data)) + So(err, ShouldBeNil) + So(resp.Answer, ShouldResemble, test.answer) + So(resp.Error, ShouldResemble, test.msg) + } + + }) +} + +func TestEncodeClientPollResponse(t *testing.T) { + Convey("Context", t, func() { + resp1 := &ClientPollResponse{ + Answer: "fake answer", + } + b, err := resp1.EncodePollResponse() + So(err, ShouldEqual, nil) + resp2, err := DecodeClientPollResponse(b) + So(err, ShouldEqual, nil) + So(resp1, ShouldResemble, resp2) + + resp1 = &ClientPollResponse{ + Error: "failed", + } + b, err = resp1.EncodePollResponse() + So(err, ShouldEqual, nil) + resp2, err = DecodeClientPollResponse(b) + So(err, ShouldEqual, nil) + So(resp1, ShouldResemble, resp2) + }) +} From c5ca41f1387b2157c0c8e66a0ecaf7a36506c4fe Mon Sep 17 00:00:00 2001 From: meskio Date: Tue, 1 Jun 2021 19:25:10 +0200 Subject: [PATCH 185/385] Add man pages for proxy and client commands To be used by the debian package (#19409) --- doc/snowflake-client.1 | 46 ++++++++++++++++++++++++++++++++++++++++++ doc/snowflake-proxy.1 | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 doc/snowflake-client.1 create mode 100644 doc/snowflake-proxy.1 diff --git a/doc/snowflake-client.1 b/doc/snowflake-client.1 new file mode 100644 index 0000000..977fc70 --- /dev/null +++ b/doc/snowflake-client.1 @@ -0,0 +1,46 @@ +.TH SNOWFLAKE-CLIENT "1" "June 2021" "snowflake-client" "User Commands" +.SH NAME +snowflake-client \- WebRTC pluggable transport client for Tor +.SH DESCRIPTION +Snowflake helps users circumvent censorship by making a WebRTC +connection to volunteer proxies. These proxies relay Tor traffic to a +Snowflake bridge and then through the Tor network. +.SS "Usage of snowflake-client:" +.HP +\fB\-front\fR string +.IP +front domain +.HP +\fB\-ice\fR string +.IP +comma\-separated list of ICE servers +.HP +\fB\-keep\-local\-addresses\fR +.IP +keep local LAN address ICE candidates +.HP +\fB\-log\fR string +.IP +name of log file +.HP +\fB\-log\-to\-state\-dir\fR +.IP +resolve the log file relative to tor's pt state dir +.HP +\fB\-logToStateDir\fR +.IP +use \fB\-log\-to\-state\-dir\fR instead +.HP +\fB\-max\fR int +.IP +capacity for number of multiplexed WebRTC peers (default 1) +.HP +\fB\-unsafe\-logging\fR +.IP +prevent logs from being scrubbed +.HP +\fB\-url\fR string +.IP +URL of signaling broker +.SH "SEE ALSO" +https://snowflake.torproject.org diff --git a/doc/snowflake-proxy.1 b/doc/snowflake-proxy.1 new file mode 100644 index 0000000..ccdd9a2 --- /dev/null +++ b/doc/snowflake-proxy.1 @@ -0,0 +1,38 @@ +.TH SNOWFLAKE-PROXY "1" "June 2021" "swnoflake-proxy" "User Commands" +.SH NAME +snowflake-proxy \- WebRTC pluggable transport proxy for Tor +.SH DESCRIPTION +Snowflake helps users circumvent censorship by making a WebRTC +connection to volunteer proxies. These proxies relay Tor traffic to a +Snowflake bridge and then through the Tor network. +.SS "Usage of snowflake-proxy:" +.HP +\fB\-broker\fR string +.IP +broker URL (default "https://snowflake\-broker.bamsoftware.com/") +.HP +\fB\-capacity\fR uint +.IP +maximum concurrent clients (default 10) +.HP +\fB\-keep\-local\-addresses\fR +.IP +keep local LAN address ICE candidates +.HP +\fB\-log\fR string +.IP +log filename +.HP +\fB\-relay\fR string +.IP +websocket relay URL (default "wss://snowflake.bamsoftware.com/") +.HP +\fB\-stun\fR string +.IP +stun URL (default "stun:stun.stunprotocol.org:3478") +.HP +\fB\-unsafe\-logging\fR +.IP +prevent logs from being scrubbed +.SH "SEE ALSO" +https://snowflake.torproject.org From 8e0b5bd20a91fabbdaa156ae3b58d35d7e8d4d71 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 7 Jun 2021 10:24:19 -0400 Subject: [PATCH 186/385] Add changelog and release v1.0.0 --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 ChangeLog diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..b2d0733 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,3 @@ +Changes in version v1.0.0 - 2021-06-07 + +- Initial release. From aefabe683f3fba846707a9f3a5e11f9b4be16990 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Mon, 3 May 2021 10:23:03 +0200 Subject: [PATCH 187/385] fix(client/snowflake.go): prevent wg.Add race condition In VSCode, the staticcheck tool emits this warning: > should call wg.Add(1) before starting the goroutine to > avoid a race (SA2000)go-staticcheck To avoid this warning, just move wg.Add outside. --- client/snowflake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/snowflake.go b/client/snowflake.go index bacc389..af9c2e4 100644 --- a/client/snowflake.go +++ b/client/snowflake.go @@ -56,8 +56,8 @@ func socksAcceptLoop(ln *pt.SocksListener, transport *sf.Transport, shutdown cha break } log.Printf("SOCKS accepted: %v", conn.Req) + wg.Add(1) go func() { - wg.Add(1) defer wg.Done() defer conn.Close() From 6634f2bec9ea7c538a819619d003db4ea2947a60 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Sat, 19 Jun 2021 11:16:38 -0400 Subject: [PATCH 188/385] Store net.Addr in clientIDAddrMap This fixes a stats collection bug where we were converting client addresses between a string and net.Addr using the clientAddr function multiple times, resulting in an empty string for all addresses. --- server/lib/http.go | 2 +- server/lib/snowflake.go | 2 +- server/lib/turbotunnel.go | 15 ++++--- server/lib/turbotunnel_test.go | 80 +++++++++++++++++++--------------- 4 files changed, 55 insertions(+), 44 deletions(-) diff --git a/server/lib/http.go b/server/lib/http.go index c612422..3dff45c 100644 --- a/server/lib/http.go +++ b/server/lib/http.go @@ -138,7 +138,7 @@ func turbotunnelMode(conn net.Conn, addr net.Addr, pconn *turbotunnel.QueuePacke // recent WebSocket connection that has had to do with a session, at the // time the session is established, is the IP address that should be // credited for the entire KCP session. - clientIDAddrMap.Set(clientID, addr.String()) + clientIDAddrMap.Set(clientID, addr) var wg sync.WaitGroup wg.Add(2) diff --git a/server/lib/snowflake.go b/server/lib/snowflake.go index 319acd8..48c6d9e 100644 --- a/server/lib/snowflake.go +++ b/server/lib/snowflake.go @@ -181,7 +181,7 @@ func (l *SnowflakeListener) acceptStreams(conn *kcp.UDPSession) error { } return err } - l.QueueConn(&SnowflakeClientConn{Conn: stream, address: clientAddr(addr)}) + l.QueueConn(&SnowflakeClientConn{Conn: stream, address: addr}) } } diff --git a/server/lib/turbotunnel.go b/server/lib/turbotunnel.go index bb16fa3..741992d 100644 --- a/server/lib/turbotunnel.go +++ b/server/lib/turbotunnel.go @@ -1,12 +1,13 @@ package lib import ( + "net" "sync" "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" ) -// clientIDMap is a fixed-capacity mapping from ClientIDs to address strings. +// clientIDMap is a fixed-capacity mapping from ClientIDs to a net.Addr. // Adding a new entry using the Set method causes the oldest existing entry to // be forgotten. // @@ -23,7 +24,7 @@ type clientIDMap struct { // entries is a circular buffer of (ClientID, addr) pairs. entries []struct { clientID turbotunnel.ClientID - addr string + addr net.Addr } // oldest is the index of the oldest member of the entries buffer, the // one that will be overwritten at the next call to Set. @@ -38,7 +39,7 @@ func newClientIDMap(capacity int) *clientIDMap { return &clientIDMap{ entries: make([]struct { clientID turbotunnel.ClientID - addr string + addr net.Addr }, capacity), oldest: 0, current: make(map[turbotunnel.ClientID]int), @@ -48,7 +49,7 @@ func newClientIDMap(capacity int) *clientIDMap { // Set adds a mapping from clientID to addr, replacing any previous mapping for // clientID. It may also cause the clientIDMap to forget at most one other // mapping, the oldest one. -func (m *clientIDMap) Set(clientID turbotunnel.ClientID, addr string) { +func (m *clientIDMap) Set(clientID turbotunnel.ClientID, addr net.Addr) { m.lock.Lock() defer m.lock.Unlock() if len(m.entries) == 0 { @@ -73,13 +74,13 @@ func (m *clientIDMap) Set(clientID turbotunnel.ClientID, addr string) { // Get returns a previously stored mapping. The second return value indicates // whether clientID was actually present in the map. If it is false, then the -// returned address string will be "". -func (m *clientIDMap) Get(clientID turbotunnel.ClientID) (string, bool) { +// returned address will be nil. +func (m *clientIDMap) Get(clientID turbotunnel.ClientID) (net.Addr, bool) { m.lock.Lock() defer m.lock.Unlock() if i, ok := m.current[clientID]; ok { return m.entries[i].addr, true } else { - return "", false + return nil, false } } diff --git a/server/lib/turbotunnel_test.go b/server/lib/turbotunnel_test.go index ba4cf60..85404af 100644 --- a/server/lib/turbotunnel_test.go +++ b/server/lib/turbotunnel_test.go @@ -2,6 +2,7 @@ package lib import ( "encoding/binary" + "net" "testing" "git.torproject.org/pluggable-transports/snowflake.git/common/turbotunnel" @@ -19,7 +20,7 @@ func TestClientIDMap(t *testing.T) { expectGet := func(m *clientIDMap, clientID turbotunnel.ClientID, expectedAddr string, expectedOK bool) { t.Helper() addr, ok := m.Get(clientID) - if addr != expectedAddr || ok != expectedOK { + if (ok && addr.String() != expectedAddr) || ok != expectedOK { t.Errorf("expected (%+q, %v), got (%+q, %v)", expectedAddr, expectedOK, addr, ok) } } @@ -32,6 +33,15 @@ func TestClientIDMap(t *testing.T) { } } + // Convert a string to a net.Addr + ip := func(addr string) net.Addr { + ret, err := net.ResolveIPAddr("ip", addr) + if err != nil { + t.Errorf("received error: %s", err.Error()) + } + return ret + } + // Zero-capacity map can't remember anything. { m := newClientIDMap(0) @@ -39,12 +49,12 @@ func TestClientIDMap(t *testing.T) { expectGet(m, id(0), "", false) expectGet(m, id(1234), "", false) - m.Set(id(0), "A") + m.Set(id(0), ip("1.1.1.1")) expectSize(m, 0) expectGet(m, id(0), "", false) expectGet(m, id(1234), "", false) - m.Set(id(1234), "A") + m.Set(id(1234), ip("1.1.1.1")) expectSize(m, 0) expectGet(m, id(0), "", false) expectGet(m, id(1234), "", false) @@ -56,60 +66,60 @@ func TestClientIDMap(t *testing.T) { expectGet(m, id(0), "", false) expectGet(m, id(1), "", false) - m.Set(id(0), "A") + m.Set(id(0), ip("1.1.1.1")) expectSize(m, 1) - expectGet(m, id(0), "A", true) + expectGet(m, id(0), "1.1.1.1", true) expectGet(m, id(1), "", false) - m.Set(id(1), "B") // forgets the (0, "A") entry + m.Set(id(1), ip("1.1.1.2")) // forgets the (0, "1.1.1.1") entry expectSize(m, 1) expectGet(m, id(0), "", false) - expectGet(m, id(1), "B", true) + expectGet(m, id(1), "1.1.1.2", true) - m.Set(id(1), "C") // forgets the (1, "B") entry + m.Set(id(1), ip("1.1.1.3")) // forgets the (1, "1.1.1.2") entry expectSize(m, 1) expectGet(m, id(0), "", false) - expectGet(m, id(1), "C", true) + expectGet(m, id(1), "1.1.1.3", true) } { m := newClientIDMap(5) - m.Set(id(0), "A") - m.Set(id(1), "B") - m.Set(id(2), "C") - m.Set(id(0), "D") // shadows the (0, "D") entry - m.Set(id(3), "E") + m.Set(id(0), ip("1.1.1.1")) + m.Set(id(1), ip("1.1.1.2")) + m.Set(id(2), ip("1.1.1.3")) + m.Set(id(0), ip("1.1.1.4")) // shadows the (0, "1.1.1.1") entry + m.Set(id(3), ip("1.1.1.5")) expectSize(m, 4) - expectGet(m, id(0), "D", true) - expectGet(m, id(1), "B", true) - expectGet(m, id(2), "C", true) - expectGet(m, id(3), "E", true) + expectGet(m, id(0), "1.1.1.4", true) + expectGet(m, id(1), "1.1.1.2", true) + expectGet(m, id(2), "1.1.1.3", true) + expectGet(m, id(3), "1.1.1.5", true) expectGet(m, id(4), "", false) - m.Set(id(4), "F") // forgets the (0, "A") entry but should preserve (0, "D") + m.Set(id(4), ip("1.1.1.6")) // forgets the (0, "1.1.1.1") entry but should preserve (0, "1.1.1.4") expectSize(m, 5) - expectGet(m, id(0), "D", true) - expectGet(m, id(1), "B", true) - expectGet(m, id(2), "C", true) - expectGet(m, id(3), "E", true) - expectGet(m, id(4), "F", true) + expectGet(m, id(0), "1.1.1.4", true) + expectGet(m, id(1), "1.1.1.2", true) + expectGet(m, id(2), "1.1.1.3", true) + expectGet(m, id(3), "1.1.1.5", true) + expectGet(m, id(4), "1.1.1.6", true) - m.Set(id(5), "G") // forgets the (1, "B") entry - m.Set(id(0), "H") // forgets the (2, "C") entry and shadows (0, "D") + m.Set(id(5), ip("1.1.1.7")) // forgets the (1, "1.1.1.2") entry + m.Set(id(0), ip("1.1.1.8")) // forgets the (2, "1.1.1.3") entry and shadows (0, "1.1.1.4") expectSize(m, 4) - expectGet(m, id(0), "H", true) + expectGet(m, id(0), "1.1.1.8", true) expectGet(m, id(1), "", false) expectGet(m, id(2), "", false) - expectGet(m, id(3), "E", true) - expectGet(m, id(4), "F", true) - expectGet(m, id(5), "G", true) + expectGet(m, id(3), "1.1.1.5", true) + expectGet(m, id(4), "1.1.1.6", true) + expectGet(m, id(5), "1.1.1.7", true) - m.Set(id(0), "I") // forgets the (0, "D") entry and shadows (0, "H") - m.Set(id(0), "J") // forgets the (3, "E") entry and shadows (0, "I") - m.Set(id(0), "K") // forgets the (4, "F") entry and shadows (0, "J") - m.Set(id(0), "L") // forgets the (5, "G") entry and shadows (0, "K") + m.Set(id(0), ip("1.1.1.9")) // forgets the (0, "1.1.1.4") entry and shadows (0, "1.1.1.8") + m.Set(id(0), ip("1.1.1.10")) // forgets the (3, "1.1.1.5") entry and shadows (0, "1.1.1.9") + m.Set(id(0), ip("1.1.1.11")) // forgets the (4, "1.1.1.6") entry and shadows (0, "1.1.1.10") + m.Set(id(0), ip("1.1.1.12")) // forgets the (5, "1.1.1.7") entry and shadows (0, "1.1.1.11") expectSize(m, 1) - expectGet(m, id(0), "L", true) + expectGet(m, id(0), "1.1.1.12", true) expectGet(m, id(1), "", false) expectGet(m, id(2), "", false) expectGet(m, id(3), "", false) From e84bc81e310f9ba95b6aab6c5d3b8be4ecbae030 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Wed, 23 Jun 2021 19:39:52 -0400 Subject: [PATCH 189/385] Bump version of kcp and smux libraries --- go.mod | 4 ++-- go.sum | 46 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index ed07394..36585aa 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,8 @@ require ( github.com/prometheus/client_golang v1.10.0 github.com/prometheus/client_model v0.2.0 github.com/smartystreets/goconvey v1.6.4 - github.com/xtaci/kcp-go/v5 v5.5.12 - github.com/xtaci/smux v1.5.12 + github.com/xtaci/kcp-go/v5 v5.6.1 + github.com/xtaci/smux v1.5.15 golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e // indirect diff --git a/go.sum b/go.sum index 8067425..f0b3927 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -152,10 +153,11 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/cpuid v1.2.2 h1:1xAgYebNnsb9LKCdLOvFWtAxGU/33mjJtyOVbmUa0Us= -github.com/klauspost/cpuid v1.2.2/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY= -github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= +github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= +github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/klauspost/reedsolomon v1.9.9 h1:qCL7LZlv17xMixl55nq2/Oa1Y86nfO8EqDfv2GHND54= +github.com/klauspost/reedsolomon v1.9.9/go.mod h1:O7yFFHiQwDR6b2t63KPUpccPtNdp5ADgh1gg4fd12wo= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -181,6 +183,8 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104 h1:ULR/QWMgcgRiZLUjSSJMU+fW+RDMstRdmnDWj9Q+AsA= +github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104/go.mod h1:wqKykBG2QzQDJEzvRkcS8x6MiSJkF52hXZsXcjaB3ls= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -336,20 +340,24 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY= github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/cpu v0.0.7 h1:pUEZn8JBy/w5yzdYWgx+0m0xL9uk6j4K91C5kOViAzo= +github.com/templexxx/cpu v0.0.7/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= github.com/templexxx/xorsimd v0.4.1 h1:iUZcywbOYDRAZUasAs2eSCUW8eobuZDy0I9FJiORkVg= github.com/templexxx/xorsimd v0.4.1/go.mod h1:W+ffZz8jJMH2SXwuKu9WhygqBMbFnp14G2fqEr8qaNo= -github.com/tjfoc/gmsm v1.0.1 h1:R11HlqhXkDospckjZEihx9SW/2VW0RgdwrykyWMFOQU= -github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc= +github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xtaci/kcp-go/v5 v5.5.12 h1:iALGyvti/oBbl1TbVoUpHEUHCorDEb3tEKl1CPY3KXM= -github.com/xtaci/kcp-go/v5 v5.5.12/go.mod h1:H0T/EJ+lPNytnFYsKLH0JHUtiwZjG3KXlTM6c+Q4YUo= +github.com/xtaci/kcp-go/v5 v5.6.1 h1:Pwn0aoeNSPF9dTS7IgiPXn0HEtaIlVb6y5UKWPsx8bI= +github.com/xtaci/kcp-go/v5 v5.6.1/go.mod h1:W3kVPyNYwZ06p79dNwFWQOVFrdcBpDBsdyvK8moQrYo= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= -github.com/xtaci/smux v1.5.12 h1:n9OGjdqQuVZXLh46+L4IR5tR2wvuUFwRABnN/V55bIY= -github.com/xtaci/smux v1.5.12/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/xtaci/smux v1.5.15 h1:6hMiXswcleXj5oNfcJc+DXS8Vj36XX2LaX98udog6Kc= +github.com/xtaci/smux v1.5.15/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -362,14 +370,16 @@ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/arch v0.0.0-20190909030613-46d78d1859ac/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= @@ -384,6 +394,9 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -401,10 +414,10 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 h1:3uJsdck53FDIpWwLeAXlia9p4C8j0BO2xZrqzKpL0D8= @@ -423,6 +436,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -444,13 +458,12 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= @@ -481,7 +494,11 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200425043458-8463f397d07c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123 h1:4JSJPND/+4555t1HfXYF4UEqDqiSKCgeV0+hbA8hMs4= +golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -540,5 +557,6 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= From ed2d5df87d0be7c86f21d19d42ec90b8d2616ae2 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Mon, 14 Jun 2021 10:46:46 +0200 Subject: [PATCH 190/385] Fix datarace for WebRTCPeer.lastReceive The race condition occurs because concurrent goroutines are intermixing reads and writes of `WebRTCPeer.lastReceive`. Spotted when integrating Snowflake inside OONI in https://github.com/ooni/probe-cli/pull/373. --- client/lib/webrtc.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index af7ba6d..6a42ebd 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -21,8 +21,10 @@ type WebRTCPeer struct { pc *webrtc.PeerConnection transport *webrtc.DataChannel - recvPipe *io.PipeReader - writePipe *io.PipeWriter + recvPipe *io.PipeReader + writePipe *io.PipeWriter + + mu sync.Mutex // protects the following: lastReceive time.Time open chan struct{} // Channel to notify when datachannel opens @@ -89,12 +91,17 @@ func (c *WebRTCPeer) Close() error { // Should also update the DataChannel in underlying go-webrtc's to make Closes // more immediate / responsive. func (c *WebRTCPeer) checkForStaleness() { + c.mu.Lock() c.lastReceive = time.Now() + c.mu.Unlock() for { if c.closed { return } - if time.Since(c.lastReceive) > SnowflakeTimeout { + c.mu.Lock() + lastReceive := c.lastReceive + c.mu.Unlock() + if time.Since(lastReceive) > SnowflakeTimeout { log.Printf("WebRTC: No messages received for %v -- closing stale connection.", SnowflakeTimeout) c.Close() @@ -173,7 +180,9 @@ func (c *WebRTCPeer) preparePeerConnection(config *webrtc.Configuration) error { log.Printf("c.writePipe.CloseWithError returned error: %v", inerr) } } + c.mu.Lock() c.lastReceive = time.Now() + c.mu.Unlock() }) c.transport = dc c.open = make(chan struct{}) From ddcdfc4f0922e00c672797b0d6544371423f2989 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 17 Jun 2021 16:36:50 -0400 Subject: [PATCH 191/385] Fix datarace for WebRTCPeer.closed The race condition occurs because concurrent goroutines are intermixing reads and writes of `WebRTCPeer.closed`. Spotted when integrating Snowflake inside OONI in https://github.com/ooni/probe-cli/pull/373. --- client/lib/lib_test.go | 6 +++--- client/lib/peers.go | 4 ++-- client/lib/webrtc.go | 24 ++++++++++++++++++------ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index e742e06..55ea7b9 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -33,7 +33,7 @@ type FakeDialer struct { func (w FakeDialer) Catch() (*WebRTCPeer, error) { fmt.Println("Caught a dummy snowflake.") - return &WebRTCPeer{}, nil + return &WebRTCPeer{closed: make(chan struct{})}, nil } func (w FakeDialer) GetMax() int { @@ -97,7 +97,7 @@ func TestSnowflakeClient(t *testing.T) { So(err, ShouldNotBeNil) So(p.Count(), ShouldEqual, c) - // But popping and closing allows it to continue. + // But popping allows it to continue. s := p.Pop() s.Close() So(s, ShouldNotBeNil) @@ -127,7 +127,7 @@ func TestSnowflakeClient(t *testing.T) { cnt := 5 p, _ := NewPeers(FakeDialer{max: cnt}) for i := 0; i < cnt; i++ { - p.activePeers.PushBack(&WebRTCPeer{}) + p.activePeers.PushBack(&WebRTCPeer{closed: make(chan struct{})}) } So(p.Count(), ShouldEqual, cnt) p.End() diff --git a/client/lib/peers.go b/client/lib/peers.go index d02eed3..6fa2d29 100644 --- a/client/lib/peers.go +++ b/client/lib/peers.go @@ -83,7 +83,7 @@ func (p *Peers) Pop() *WebRTCPeer { if !ok { return nil } - if snowflake.closed { + if snowflake.Closed() { continue } // Set to use the same rate-limited traffic logger to keep consistency. @@ -110,7 +110,7 @@ func (p *Peers) purgeClosedPeers() { next := e.Next() conn := e.Value.(*WebRTCPeer) // Purge those marked for deletion. - if conn.closed { + if conn.Closed() { p.activePeers.Remove(e) } e = next diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 6a42ebd..234f53c 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -28,7 +28,7 @@ type WebRTCPeer struct { lastReceive time.Time open chan struct{} // Channel to notify when datachannel opens - closed bool + closed chan struct{} once sync.Once // Synchronization for PeerConnection destruction @@ -46,6 +46,7 @@ func NewWebRTCPeer(config *webrtc.Configuration, } connection.id = "snowflake-" + hex.EncodeToString(buf[:]) } + connection.closed = make(chan struct{}) // Override with something that's not NullLogger to have real logging. connection.BytesLogger = &BytesNullLogger{} @@ -78,9 +79,19 @@ func (c *WebRTCPeer) Write(b []byte) (int, error) { return len(b), nil } +//Returns a boolean indicated whether the peer is closed +func (c *WebRTCPeer) Closed() bool { + select { + case <-c.closed: + return true + default: + } + return false +} + func (c *WebRTCPeer) Close() error { c.once.Do(func() { - c.closed = true + close(c.closed) c.cleanup() log.Printf("WebRTC: Closing") }) @@ -95,9 +106,6 @@ func (c *WebRTCPeer) checkForStaleness() { c.lastReceive = time.Now() c.mu.Unlock() for { - if c.closed { - return - } c.mu.Lock() lastReceive := c.lastReceive c.mu.Unlock() @@ -107,7 +115,11 @@ func (c *WebRTCPeer) checkForStaleness() { c.Close() return } - <-time.After(time.Second) + select { + case <-c.closed: + return + case <-time.After(time.Second): + } } } From bb7ff6180bf2cb9553db8cbb4600092107cf9303 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 17 Jun 2021 17:42:22 -0400 Subject: [PATCH 192/385] Fix datarace for Peers.melted Using the boolean value was unnecessary since we already have a channel we can check for closure. --- client/lib/peers.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/lib/peers.go b/client/lib/peers.go index 6fa2d29..66373f1 100644 --- a/client/lib/peers.go +++ b/client/lib/peers.go @@ -26,8 +26,7 @@ type Peers struct { snowflakeChan chan *WebRTCPeer activePeers *list.List - melt chan struct{} - melted bool + melt chan struct{} collection sync.WaitGroup } @@ -51,8 +50,10 @@ func (p *Peers) Collect() (*WebRTCPeer, error) { // Engage the Snowflake Catching interface, which must be available. p.collection.Add(1) defer p.collection.Done() - if p.melted { + select { + case <-p.melt: return nil, fmt.Errorf("Snowflakes have melted") + default: } if nil == p.Tongue { return nil, errors.New("missing Tongue to catch Snowflakes with") @@ -120,7 +121,6 @@ func (p *Peers) purgeClosedPeers() { // Close all Peers contained here. func (p *Peers) End() { close(p.melt) - p.melted = true p.collection.Wait() close(p.snowflakeChan) cnt := p.Count() From 95cbe36565d65bf1512acd0faeab9a1e2580808f Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 17 Jun 2021 17:43:40 -0400 Subject: [PATCH 193/385] Add unit tests to check for webrtc peer data races --- client/lib/lib_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 55ea7b9..1eef0c0 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "testing" + "time" "git.torproject.org/pluggable-transports/snowflake.git/common/util" . "github.com/smartystreets/goconvey/convey" @@ -154,6 +155,25 @@ func TestSnowflakeClient(t *testing.T) { So(r, ShouldEqual, wc4) }) + Convey("Terminate Connect() loop", func() { + p, _ := NewPeers(FakeDialer{max: 4}) + go func() { + for { + p.Collect() + select { + case <-p.Melted(): + return + default: + } + } + }() + <-time.After(10 * time.Second) + + p.End() + <-p.Melted() + So(p.Count(), ShouldEqual, 0) + }) + }) Convey("Dialers", t, func() { @@ -245,6 +265,17 @@ func TestSnowflakeClient(t *testing.T) { } +func TestWebRTCPeer(t *testing.T) { + Convey("WebRTCPeer", t, func(c C) { + p := &WebRTCPeer{closed: make(chan struct{})} + Convey("checks for staleness", func() { + go p.checkForStaleness() + <-time.After(2 * SnowflakeTimeout) + So(p.Closed(), ShouldEqual, true) + }) + }) +} + func TestICEServerParser(t *testing.T) { Convey("Test parsing of ICE servers", t, func() { for _, test := range []struct { From e3351cb08abe6b806fa018bc8915f0f5ae045a40 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Mon, 21 Jun 2021 15:39:41 -0400 Subject: [PATCH 194/385] Fix data race for Peers.collection We used a WaitGroup to prevent a call to Peers.End from melting snowflakes while a new one is being collected. However, calls to WaitGroup.Add are in a race with WaitGroup.Wait. To fix this, we use a Mutex instead. --- client/lib/peers.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/lib/peers.go b/client/lib/peers.go index 66373f1..7fba572 100644 --- a/client/lib/peers.go +++ b/client/lib/peers.go @@ -28,7 +28,7 @@ type Peers struct { melt chan struct{} - collection sync.WaitGroup + collectLock sync.Mutex } // Construct a fresh container of remote peers. @@ -48,8 +48,8 @@ func NewPeers(tongue Tongue) (*Peers, error) { // As part of |SnowflakeCollector| interface. func (p *Peers) Collect() (*WebRTCPeer, error) { // Engage the Snowflake Catching interface, which must be available. - p.collection.Add(1) - defer p.collection.Done() + p.collectLock.Lock() + defer p.collectLock.Unlock() select { case <-p.melt: return nil, fmt.Errorf("Snowflakes have melted") @@ -121,7 +121,8 @@ func (p *Peers) purgeClosedPeers() { // Close all Peers contained here. func (p *Peers) End() { close(p.melt) - p.collection.Wait() + p.collectLock.Lock() + defer p.collectLock.Unlock() close(p.snowflakeChan) cnt := p.Count() for e := p.activePeers.Front(); e != nil; { From 10b6075eaa90d65ebb4838b24ca8db4924e572ec Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 24 Jun 2021 11:20:44 -0400 Subject: [PATCH 195/385] Refactor checkForStaleness to take time.Duration --- client/lib/lib_test.go | 4 ++-- client/lib/webrtc.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 1eef0c0..e0856a5 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -269,8 +269,8 @@ func TestWebRTCPeer(t *testing.T) { Convey("WebRTCPeer", t, func(c C) { p := &WebRTCPeer{closed: make(chan struct{})} Convey("checks for staleness", func() { - go p.checkForStaleness() - <-time.After(2 * SnowflakeTimeout) + go p.checkForStaleness(time.Second) + <-time.After(2 * time.Second) So(p.Closed(), ShouldEqual, true) }) }) diff --git a/client/lib/webrtc.go b/client/lib/webrtc.go index 234f53c..72a3d64 100644 --- a/client/lib/webrtc.go +++ b/client/lib/webrtc.go @@ -101,7 +101,7 @@ func (c *WebRTCPeer) Close() error { // Prevent long-lived broken remotes. // Should also update the DataChannel in underlying go-webrtc's to make Closes // more immediate / responsive. -func (c *WebRTCPeer) checkForStaleness() { +func (c *WebRTCPeer) checkForStaleness(timeout time.Duration) { c.mu.Lock() c.lastReceive = time.Now() c.mu.Unlock() @@ -109,9 +109,9 @@ func (c *WebRTCPeer) checkForStaleness() { c.mu.Lock() lastReceive := c.lastReceive c.mu.Unlock() - if time.Since(lastReceive) > SnowflakeTimeout { + if time.Since(lastReceive) > timeout { log.Printf("WebRTC: No messages received for %v -- closing stale connection.", - SnowflakeTimeout) + timeout) c.Close() return } @@ -147,7 +147,7 @@ func (c *WebRTCPeer) connect(config *webrtc.Configuration, broker *BrokerChannel return errors.New("timeout waiting for DataChannel.OnOpen") } - go c.checkForStaleness() + go c.checkForStaleness(SnowflakeTimeout) return nil } From 53a2365696d144921eae57c790083e502628135d Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 24 Jun 2021 09:33:19 -0400 Subject: [PATCH 196/385] Fix leak in server acceptLoop Refactor out a separate handleStream function and ensure that all connections are closed and the references are out of scope. --- server/server.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/server/server.go b/server/server.go index b61d5b4..92d819f 100644 --- a/server/server.go +++ b/server/server.go @@ -41,7 +41,7 @@ additional HTTP listener on port 80 to work with ACME. flag.PrintDefaults() } -// Copy from one stream to another. +//proxy copies data bidirectionally from one connection to another. func proxy(local *net.TCPConn, conn net.Conn) { var wg sync.WaitGroup wg.Add(2) @@ -66,6 +66,20 @@ 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 { + addr := conn.RemoteAddr().String() + statsChannel <- addr != "" + or, err := pt.DialOr(&ptInfo, addr, ptMethodName) + if err != nil { + return fmt.Errorf("failed to connect to ORPort: %s", err) + } + defer or.Close() + proxy(or, conn) + return nil +} + +//acceptLoop accepts incoming client snowflake connection and passes them to a handler function. func acceptLoop(ln net.Listener) { for { conn, err := ln.Accept() @@ -76,17 +90,13 @@ func acceptLoop(ln net.Listener) { log.Printf("Snowflake accept error: %s", err) break } - defer conn.Close() - - addr := conn.RemoteAddr().String() - statsChannel <- addr != "" - or, err := pt.DialOr(&ptInfo, addr, ptMethodName) - if err != nil { - log.Printf("failed to connect to ORPort: %s", err) - continue - } - defer or.Close() - go proxy(or, conn) + go func() { + defer conn.Close() + err := handleConn(conn) + if err != nil { + log.Printf("handleConn: %v", err) + } + }() } } From 74bdb85b300cfec1694ffdb091a0efec31326579 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 24 Jun 2021 13:46:11 -0400 Subject: [PATCH 197/385] Update example torrc file for client Remove the -max 3 option because we only use one snowflake. Add SocksPort auto because many testers have a tor process already bound to port 9050. --- client/torrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/torrc b/client/torrc index 7a1dbdf..1328adc 100644 --- a/client/torrc +++ b/client/torrc @@ -4,7 +4,7 @@ DataDirectory datadir ClientTransportPlugin snowflake exec ./client \ -url https://snowflake-broker.torproject.net.global.prod.fastly.net/ \ -front cdn.sstatic.net \ --ice stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 \ --max 3 +-ice stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 Bridge snowflake 192.0.2.3:1 +SocksPort auto From 7a1857c42fdbaf78207d9e25ea814fe1d531036c Mon Sep 17 00:00:00 2001 From: meskio Date: Fri, 25 Jun 2021 13:47:47 +0200 Subject: [PATCH 198/385] Make the proxy to report the number of clients to the broker So the assignment of proxies is based on the load. The number of clients is ronded down to 8. Existing proxies that doesn't report the number of clients will be distributed equaly to new proxies until they get 8 clients, that is okish as the existing proxies do have a maximum capacity of 10. Fixes #40048 --- broker/broker.go | 14 ++++--- broker/snowflake-broker_test.go | 18 ++++----- common/messages/messages_test.go | 26 +++++++++++-- common/messages/proxy.go | 28 ++++++++------ doc/broker-spec.txt | 4 +- proxy/proxy-go_test.go | 17 ++------- proxy/snowflake.go | 63 +++++++++++++++----------------- proxy/tokens.go | 44 ++++++++++++++++++++++ proxy/tokens_test.go | 28 ++++++++++++++ 9 files changed, 165 insertions(+), 77 deletions(-) create mode 100644 proxy/tokens.go create mode 100644 proxy/tokens_test.go diff --git a/broker/broker.go b/broker/broker.go index 906c210..fc4727d 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -124,16 +124,18 @@ type ProxyPoll struct { id string proxyType string natType string + clients int offerChannel chan *ClientOffer } // Registers a Snowflake and waits for some Client to send an offer, // as part of the polling logic of the proxy handler. -func (ctx *BrokerContext) RequestOffer(id string, proxyType string, natType string) *ClientOffer { +func (ctx *BrokerContext) RequestOffer(id string, proxyType string, natType string, clients int) *ClientOffer { request := new(ProxyPoll) request.id = id request.proxyType = proxyType request.natType = natType + request.clients = clients request.offerChannel = make(chan *ClientOffer) ctx.proxyPolls <- request // Block until an offer is available, or timeout which sends a nil offer. @@ -146,7 +148,7 @@ func (ctx *BrokerContext) RequestOffer(id string, proxyType string, natType stri // client offer or nil on timeout / none are available. func (ctx *BrokerContext) Broker() { for request := range ctx.proxyPolls { - snowflake := ctx.AddSnowflake(request.id, request.proxyType, request.natType) + snowflake := ctx.AddSnowflake(request.id, request.proxyType, request.natType, request.clients) // Wait for a client to avail an offer to the snowflake. go func(request *ProxyPoll) { select { @@ -174,10 +176,10 @@ func (ctx *BrokerContext) Broker() { // Create and add a Snowflake to the heap. // Required to keep track of proxies between providing them // with an offer and awaiting their second POST with an answer. -func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType string) *Snowflake { +func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType string, clients int) *Snowflake { snowflake := new(Snowflake) snowflake.id = id - snowflake.clients = 0 + snowflake.clients = clients snowflake.proxyType = proxyType snowflake.natType = natType snowflake.offerChannel = make(chan *ClientOffer) @@ -205,7 +207,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { return } - sid, proxyType, natType, err := messages.DecodePollRequest(body) + sid, proxyType, natType, clients, err := messages.DecodePollRequest(body) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -222,7 +224,7 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { } // Wait for a client to avail an offer to the snowflake, or timeout if nil. - offer := ctx.RequestOffer(sid, proxyType, natType) + offer := ctx.RequestOffer(sid, proxyType, natType, clients) var b []byte if nil == offer { ctx.metrics.lock.Lock() diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 646fb02..825bc6f 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -32,7 +32,7 @@ func TestBroker(t *testing.T) { Convey("Adds Snowflake", func() { So(ctx.snowflakes.Len(), ShouldEqual, 0) So(len(ctx.idToSnowflake), ShouldEqual, 0) - ctx.AddSnowflake("foo", "", NATUnrestricted) + ctx.AddSnowflake("foo", "", NATUnrestricted, 0) So(ctx.snowflakes.Len(), ShouldEqual, 1) So(len(ctx.idToSnowflake), ShouldEqual, 1) }) @@ -59,7 +59,7 @@ func TestBroker(t *testing.T) { Convey("Request an offer from the Snowflake Heap", func() { done := make(chan *ClientOffer) go func() { - offer := ctx.RequestOffer("test", "", NATUnrestricted) + offer := ctx.RequestOffer("test", "", NATUnrestricted, 0) done <- offer }() request := <-ctx.proxyPolls @@ -84,7 +84,7 @@ func TestBroker(t *testing.T) { Convey("with a proxy answer if available.", func() { done := make(chan bool) // Prepare a fake proxy to respond with. - snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted) + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) go func() { clientOffers(ctx, w, r) done <- true @@ -102,7 +102,7 @@ func TestBroker(t *testing.T) { return } done := make(chan bool) - snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted) + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) go func() { clientOffers(ctx, w, r) // Takes a few seconds here... @@ -132,7 +132,7 @@ func TestBroker(t *testing.T) { Convey("with a proxy answer if available.", func() { done := make(chan bool) // Prepare a fake proxy to respond with. - snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted) + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) go func() { clientOffers(ctx, w, r) done <- true @@ -150,7 +150,7 @@ func TestBroker(t *testing.T) { return } done := make(chan bool) - snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted) + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) go func() { clientOffers(ctx, w, r) // Takes a few seconds here... @@ -201,7 +201,7 @@ func TestBroker(t *testing.T) { }) Convey("Responds to proxy answers...", func() { - s := ctx.AddSnowflake("test", "", NATUnrestricted) + s := ctx.AddSnowflake("test", "", NATUnrestricted, 0) w := httptest.NewRecorder() data := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"test","Answer":"test"}`)) @@ -314,7 +314,7 @@ func TestBroker(t *testing.T) { // Manually do the Broker goroutine action here for full control. p := <-ctx.proxyPolls So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") - s := ctx.AddSnowflake(p.id, "", NATUnrestricted) + s := ctx.AddSnowflake(p.id, "", NATUnrestricted, 0) go func() { offer := <-s.offerChannel p.offerChannel <- offer @@ -593,7 +593,7 @@ func TestMetrics(t *testing.T) { So(err, ShouldBeNil) // Prepare a fake proxy to respond with. - snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted) + snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) go func() { clientOffers(ctx, w, r) done <- true diff --git a/common/messages/messages_test.go b/common/messages/messages_test.go index 3962d3b..abb978d 100644 --- a/common/messages/messages_test.go +++ b/common/messages/messages_test.go @@ -15,6 +15,7 @@ func TestDecodeProxyPollRequest(t *testing.T) { sid string proxyType string natType string + clients int data string err error }{ @@ -23,6 +24,7 @@ func TestDecodeProxyPollRequest(t *testing.T) { "ymbcCMto7KHNGYlp", "", "unknown", + 0, `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`, nil, }, @@ -31,6 +33,7 @@ func TestDecodeProxyPollRequest(t *testing.T) { "ymbcCMto7KHNGYlp", "standalone", "unknown", + 0, `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.1","Type":"standalone"}`, nil, }, @@ -39,14 +42,25 @@ func TestDecodeProxyPollRequest(t *testing.T) { "ymbcCMto7KHNGYlp", "standalone", "restricted", + 0, `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"standalone", "NAT":"restricted"}`, nil, }, + { + //Version 1.2 proxy message with clients + "ymbcCMto7KHNGYlp", + "standalone", + "restricted", + 24, + `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.2","Type":"standalone", "NAT":"restricted","Clients":24}`, + nil, + }, { //Version 0.X proxy message: "", "", "", + 0, "", &json.SyntaxError{}, }, @@ -54,6 +68,7 @@ func TestDecodeProxyPollRequest(t *testing.T) { "", "", "", + 0, `{"Sid":"ymbcCMto7KHNGYlp"}`, fmt.Errorf(""), }, @@ -61,6 +76,7 @@ func TestDecodeProxyPollRequest(t *testing.T) { "", "", "", + 0, "{}", fmt.Errorf(""), }, @@ -68,6 +84,7 @@ func TestDecodeProxyPollRequest(t *testing.T) { "", "", "", + 0, `{"Version":"1.0"}`, fmt.Errorf(""), }, @@ -75,14 +92,16 @@ func TestDecodeProxyPollRequest(t *testing.T) { "", "", "", + 0, `{"Version":"2.0"}`, fmt.Errorf(""), }, } { - sid, proxyType, natType, err := DecodePollRequest([]byte(test.data)) + sid, proxyType, natType, clients, err := DecodePollRequest([]byte(test.data)) So(sid, ShouldResemble, test.sid) So(proxyType, ShouldResemble, test.proxyType) So(natType, ShouldResemble, test.natType) + So(clients, ShouldEqual, test.clients) So(err, ShouldHaveSameTypeAs, test.err) } @@ -91,12 +110,13 @@ func TestDecodeProxyPollRequest(t *testing.T) { func TestEncodeProxyPollRequests(t *testing.T) { Convey("Context", t, func() { - b, err := EncodePollRequest("ymbcCMto7KHNGYlp", "standalone", "unknown") + b, err := EncodePollRequest("ymbcCMto7KHNGYlp", "standalone", "unknown", 16) So(err, ShouldEqual, nil) - sid, proxyType, natType, err := DecodePollRequest(b) + sid, proxyType, natType, clients, err := DecodePollRequest(b) So(sid, ShouldEqual, "ymbcCMto7KHNGYlp") So(proxyType, ShouldEqual, "standalone") So(natType, ShouldEqual, "unknown") + So(clients, ShouldEqual, 16) So(err, ShouldEqual, nil) }) } diff --git a/common/messages/proxy.go b/common/messages/proxy.go index 2d9e58d..366e833 100644 --- a/common/messages/proxy.go +++ b/common/messages/proxy.go @@ -17,8 +17,9 @@ const version = "1.2" { Sid: [generated session id of proxy], Version: 1.2, - Type: ["badge"|"webext"|"standalone"] - NAT: ["unknown"|"restricted"|"unrestricted"] + Type: ["badge"|"webext"|"standalone"], + NAT: ["unknown"|"restricted"|"unrestricted"], + Clients: [number of current clients, rounded down to multiples of 8] } == ProxyPollResponse == @@ -79,43 +80,48 @@ type ProxyPollRequest struct { Version string Type string NAT string + Clients int } -func EncodePollRequest(sid string, proxyType string, natType string) ([]byte, error) { +func EncodePollRequest(sid string, proxyType string, natType string, clients int) ([]byte, error) { return json.Marshal(ProxyPollRequest{ Sid: sid, Version: version, Type: proxyType, NAT: natType, + Clients: clients, }) } // Decodes a poll message from a snowflake proxy and returns the -// sid and proxy type of the proxy on success and an error if it failed -func DecodePollRequest(data []byte) (string, string, string, error) { +// sid, proxy type, nat type and clients of the proxy on success +// and an error if it failed +func DecodePollRequest(data []byte) (sid string, proxyType string, natType string, clients int, err error) { var message ProxyPollRequest - err := json.Unmarshal(data, &message) + err = json.Unmarshal(data, &message) if err != nil { - return "", "", "", err + return } majorVersion := strings.Split(message.Version, ".")[0] if majorVersion != "1" { - return "", "", "", fmt.Errorf("using unknown version") + err = fmt.Errorf("using unknown version") + return } // Version 1.x requires an Sid if message.Sid == "" { - return "", "", "", fmt.Errorf("no supplied session id") + err = fmt.Errorf("no supplied session id") + return } - natType := message.NAT + natType = message.NAT if natType == "" { natType = "unknown" } - return message.Sid, message.Type, natType, nil + return message.Sid, message.Type, natType, message.Clients, nil } type ProxyPollResponse struct { diff --git a/doc/broker-spec.txt b/doc/broker-spec.txt index 9e4b8ae..f2cd231 100644 --- a/doc/broker-spec.txt +++ b/doc/broker-spec.txt @@ -141,7 +141,9 @@ POST /proxy HTTP { Sid: [generated session id of proxy], Version: 1.1, - Type: ["badge"|"webext"|"standalone"|"mobile"] + Type: ["badge"|"webext"|"standalone"|"mobile"], + NAT: ["unknown"|"restricted"|"unrestricted"], + Clients: [number of current clients, rounded down to multiples of 8] } ``` diff --git a/proxy/proxy-go_test.go b/proxy/proxy-go_test.go index e935ad9..183b1b4 100644 --- a/proxy/proxy-go_test.go +++ b/proxy/proxy-go_test.go @@ -7,7 +7,6 @@ import ( "io/ioutil" "net" "net/http" - "net/url" "strconv" "strings" "testing" @@ -337,8 +336,9 @@ func TestBrokerInteractions(t *testing.T) { const sampleAnswer = `{"type":"answer","sdp":` + sampleSDP + `}` Convey("Proxy connections to broker", t, func() { - broker := new(SignalingServer) - broker.url, _ = url.Parse("localhost") + broker, err := newSignalingServer("localhost", false) + So(err, ShouldEqual, nil) + tokens = newTokens(0) //Mock peerConnection config = webrtc.Configuration{ @@ -469,17 +469,6 @@ func TestUtilityFuncs(t *testing.T) { So(err, ShouldEqual, io.ErrClosedPipe) }) }) - Convey("Tokens", t, func() { - tokens = make(chan bool, 2) - for i := uint(0); i < 2; i++ { - tokens <- true - } - So(len(tokens), ShouldEqual, 2) - getToken() - So(len(tokens), ShouldEqual, 1) - retToken() - So(len(tokens), ShouldEqual, 2) - }) Convey("SessionID Generation", t, func() { sid1 := genSessionID() sid2 := genSessionID() diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 86ae0b2..f7eacf8 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -55,7 +55,7 @@ const ( ) var ( - tokens chan bool + tokens *tokens_t config webrtc.Configuration client http.Client ) @@ -171,14 +171,6 @@ func (c *webRTCConn) SetWriteDeadline(t time.Time) error { return fmt.Errorf("SetWriteDeadline not implemented") } -func getToken() { - <-tokens -} - -func retToken() { - tokens <- true -} - func genSessionID() string { buf := make([]byte, sessionIDLength) _, err := rand.Read(buf) @@ -204,6 +196,21 @@ type SignalingServer struct { keepLocalAddresses bool } +func newSignalingServer(rawURL string, keepLocalAddresses bool) (*SignalingServer, error) { + var err error + s := new(SignalingServer) + s.keepLocalAddresses = keepLocalAddresses + s.url, err = url.Parse(rawURL) + if err != nil { + return nil, fmt.Errorf("invalid broker url: %s", err) + } + + s.transport = http.DefaultTransport.(*http.Transport) + s.transport.(*http.Transport).ResponseHeaderTimeout = 30 * time.Second + + return s, nil +} + func (s *SignalingServer) Post(path string, payload io.Reader) ([]byte, error) { req, err := http.NewRequest("POST", path, payload) @@ -238,7 +245,8 @@ func (s *SignalingServer) pollOffer(sid string) *webrtc.SessionDescription { timeOfNextPoll = now } - body, err := messages.EncodePollRequest(sid, "standalone", currentNATType) + numClients := int((tokens.count() / 8) * 8) // Round down to 8 + body, err := messages.EncodePollRequest(sid, "standalone", currentNATType, numClients) if err != nil { log.Printf("Error encoding poll message: %s", err.Error()) return nil @@ -323,7 +331,7 @@ func CopyLoop(c1 io.ReadWriteCloser, c2 io.ReadWriteCloser) { // RemoteAddr). https://bugs.torproject.org/18628#comment:8 func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) { defer conn.Close() - defer retToken() + defer tokens.ret() u, err := url.Parse(relayURL) if err != nil { @@ -494,14 +502,14 @@ func runSession(sid string) { offer := broker.pollOffer(sid) if offer == nil { log.Printf("bad offer from broker") - retToken() + tokens.ret() return } dataChan := make(chan struct{}) pc, err := makePeerConnectionFromOffer(offer, config, dataChan, datachannelHandler) if err != nil { log.Printf("error making WebRTC connection: %s", err) - retToken() + tokens.ret() return } err = broker.sendAnswer(sid, pc) @@ -510,7 +518,7 @@ func runSession(sid string) { if inerr := pc.Close(); inerr != nil { log.Printf("error calling pc.Close: %v", inerr) } - retToken() + tokens.ret() return } // Set a timeout on peerconnection. If the connection state has not @@ -524,7 +532,7 @@ func runSession(sid string) { if err := pc.Close(); err != nil { log.Printf("error calling pc.Close: %v", err) } - retToken() + tokens.ret() } } @@ -536,7 +544,7 @@ func main() { var unsafeLogging bool var keepLocalAddresses bool - flag.UintVar(&capacity, "capacity", 10, "maximum concurrent clients") + flag.UintVar(&capacity, "capacity", 0, "maximum concurrent clients") flag.StringVar(&rawBrokerURL, "broker", defaultBrokerURL, "broker URL") flag.StringVar(&relayURL, "relay", defaultRelayURL, "websocket relay URL") flag.StringVar(&stunURL, "stun", defaultSTUNURL, "stun URL") @@ -565,12 +573,11 @@ func main() { log.Println("starting") var err error - broker = new(SignalingServer) - broker.keepLocalAddresses = keepLocalAddresses - broker.url, err = url.Parse(rawBrokerURL) + broker, err = newSignalingServer(rawBrokerURL, keepLocalAddresses) if err != nil { - log.Fatalf("invalid broker url: %s", err) + log.Fatal(err) } + _, err = url.Parse(stunURL) if err != nil { log.Fatalf("invalid stun url: %s", err) @@ -580,8 +587,6 @@ func main() { log.Fatalf("invalid relay url: %s", err) } - broker.transport = http.DefaultTransport.(*http.Transport) - broker.transport.(*http.Transport).ResponseHeaderTimeout = 15 * time.Second config = webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { @@ -589,17 +594,14 @@ func main() { }, }, } - tokens = make(chan bool, capacity) - for i := uint(0); i < capacity; i++ { - tokens <- true - } + tokens = newTokens(capacity) // use probetest to determine NAT compatability checkNATType(config, defaultProbeURL) log.Printf("NAT type: %s", currentNATType) for { - getToken() + tokens.get() sessionID := genSessionID() runSession(sessionID) } @@ -607,12 +609,7 @@ func main() { func checkNATType(config webrtc.Configuration, probeURL string) { - var err error - - probe := new(SignalingServer) - probe.transport = http.DefaultTransport.(*http.Transport) - probe.transport.(*http.Transport).ResponseHeaderTimeout = 30 * time.Second - probe.url, err = url.Parse(probeURL) + probe, err := newSignalingServer(probeURL, false) if err != nil { log.Printf("Error parsing url: %s", err.Error()) } diff --git a/proxy/tokens.go b/proxy/tokens.go new file mode 100644 index 0000000..fedb8f7 --- /dev/null +++ b/proxy/tokens.go @@ -0,0 +1,44 @@ +package main + +import ( + "sync/atomic" +) + +type tokens_t struct { + ch chan struct{} + capacity uint + clients int64 +} + +func newTokens(capacity uint) *tokens_t { + var ch chan struct{} + if capacity != 0 { + ch = make(chan struct{}, capacity) + } + + return &tokens_t{ + ch: ch, + capacity: capacity, + clients: 0, + } +} + +func (t *tokens_t) get() { + atomic.AddInt64(&t.clients, 1) + + if t.capacity != 0 { + t.ch <- struct{}{} + } +} + +func (t *tokens_t) ret() { + atomic.AddInt64(&t.clients, -1) + + if t.capacity != 0 { + <-t.ch + } +} + +func (t tokens_t) count() int64 { + return atomic.LoadInt64(&t.clients) +} diff --git a/proxy/tokens_test.go b/proxy/tokens_test.go new file mode 100644 index 0000000..622cc05 --- /dev/null +++ b/proxy/tokens_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestTokens(t *testing.T) { + Convey("Tokens", t, func() { + tokens := newTokens(2) + So(tokens.count(), ShouldEqual, 0) + tokens.get() + So(tokens.count(), ShouldEqual, 1) + tokens.ret() + So(tokens.count(), ShouldEqual, 0) + }) + Convey("Tokens capacity 0", t, func() { + tokens := newTokens(0) + So(tokens.count(), ShouldEqual, 0) + for i := 0; i < 20; i++ { + tokens.get() + } + So(tokens.count(), ShouldEqual, 20) + tokens.ret() + So(tokens.count(), ShouldEqual, 19) + }) +} From ced539f234af4fbf460787b5f50eb78098406774 Mon Sep 17 00:00:00 2001 From: meskio Date: Fri, 25 Jun 2021 18:37:31 +0200 Subject: [PATCH 199/385] Refactor webRTCConn to its own file --- proxy/snowflake.go | 109 --------------------------------------- proxy/webrtcconn.go | 121 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 109 deletions(-) create mode 100644 proxy/webrtcconn.go diff --git a/proxy/snowflake.go b/proxy/snowflake.go index f7eacf8..12d97d3 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -13,7 +13,6 @@ import ( "net/http" "net/url" "os" - "regexp" "strings" "sync" "time" @@ -23,8 +22,6 @@ import ( "git.torproject.org/pluggable-transports/snowflake.git/common/util" "git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn" "github.com/gorilla/websocket" - "github.com/pion/ice/v2" - "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3" ) @@ -60,117 +57,11 @@ var ( client http.Client ) -var remoteIPPatterns = []*regexp.Regexp{ - /* IPv4 */ - regexp.MustCompile(`(?m)^c=IN IP4 ([\d.]+)(?:(?:\/\d+)?\/\d+)?(:? |\r?\n)`), - /* IPv6 */ - regexp.MustCompile(`(?m)^c=IN IP6 ([0-9A-Fa-f:.]+)(?:\/\d+)?(:? |\r?\n)`), -} - // Checks whether an IP address is a remote address for the client func isRemoteAddress(ip net.IP) bool { return !(util.IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback()) } -func remoteIPFromSDP(str string) net.IP { - // Look for remote IP in "a=candidate" attribute fields - // https://tools.ietf.org/html/rfc5245#section-15.1 - var desc sdp.SessionDescription - err := desc.Unmarshal([]byte(str)) - if err != nil { - log.Println("Error parsing SDP: ", err.Error()) - return nil - } - for _, m := range desc.MediaDescriptions { - for _, a := range m.Attributes { - if a.IsICECandidate() { - c, err := ice.UnmarshalCandidate(a.Value) - if err == nil { - ip := net.ParseIP(c.Address()) - if ip != nil && isRemoteAddress(ip) { - return ip - } - } - } - } - } - // Finally look for remote IP in "c=" Connection Data field - // https://tools.ietf.org/html/rfc4566#section-5.7 - for _, pattern := range remoteIPPatterns { - m := pattern.FindStringSubmatch(str) - if m != nil { - // Ignore parsing errors, ParseIP returns nil. - ip := net.ParseIP(m[1]) - if ip != nil && isRemoteAddress(ip) { - return ip - } - - } - } - - return nil -} - -type webRTCConn struct { - dc *webrtc.DataChannel - pc *webrtc.PeerConnection - pr *io.PipeReader - - lock sync.Mutex // Synchronization for DataChannel destruction - once sync.Once // Synchronization for PeerConnection destruction - - bytesLogger BytesLogger -} - -func (c *webRTCConn) Read(b []byte) (int, error) { - return c.pr.Read(b) -} - -func (c *webRTCConn) Write(b []byte) (int, error) { - c.bytesLogger.AddInbound(len(b)) - c.lock.Lock() - defer c.lock.Unlock() - if c.dc != nil { - c.dc.Send(b) - } - return len(b), nil -} - -func (c *webRTCConn) Close() (err error) { - c.once.Do(func() { - err = c.pc.Close() - }) - return -} - -func (c *webRTCConn) LocalAddr() net.Addr { - return nil -} - -func (c *webRTCConn) RemoteAddr() net.Addr { - //Parse Remote SDP offer and extract client IP - clientIP := remoteIPFromSDP(c.pc.RemoteDescription().SDP) - if clientIP == nil { - return nil - } - return &net.IPAddr{IP: clientIP, Zone: ""} -} - -func (c *webRTCConn) SetDeadline(t time.Time) error { - // nolint: golint - return fmt.Errorf("SetDeadline not implemented") -} - -func (c *webRTCConn) SetReadDeadline(t time.Time) error { - // nolint: golint - return fmt.Errorf("SetReadDeadline not implemented") -} - -func (c *webRTCConn) SetWriteDeadline(t time.Time) error { - // nolint: golint - return fmt.Errorf("SetWriteDeadline not implemented") -} - func genSessionID() string { buf := make([]byte, sessionIDLength) _, err := rand.Read(buf) diff --git a/proxy/webrtcconn.go b/proxy/webrtcconn.go new file mode 100644 index 0000000..5d95919 --- /dev/null +++ b/proxy/webrtcconn.go @@ -0,0 +1,121 @@ +package main + +import ( + "fmt" + "io" + "log" + "net" + "regexp" + "sync" + "time" + + "github.com/pion/ice/v2" + "github.com/pion/sdp/v3" + "github.com/pion/webrtc/v3" +) + +var remoteIPPatterns = []*regexp.Regexp{ + /* IPv4 */ + regexp.MustCompile(`(?m)^c=IN IP4 ([\d.]+)(?:(?:\/\d+)?\/\d+)?(:? |\r?\n)`), + /* IPv6 */ + regexp.MustCompile(`(?m)^c=IN IP6 ([0-9A-Fa-f:.]+)(?:\/\d+)?(:? |\r?\n)`), +} + +type webRTCConn struct { + dc *webrtc.DataChannel + pc *webrtc.PeerConnection + pr *io.PipeReader + + lock sync.Mutex // Synchronization for DataChannel destruction + once sync.Once // Synchronization for PeerConnection destruction + + bytesLogger BytesLogger +} + +func (c *webRTCConn) Read(b []byte) (int, error) { + return c.pr.Read(b) +} + +func (c *webRTCConn) Write(b []byte) (int, error) { + c.bytesLogger.AddInbound(len(b)) + c.lock.Lock() + defer c.lock.Unlock() + if c.dc != nil { + c.dc.Send(b) + } + return len(b), nil +} + +func (c *webRTCConn) Close() (err error) { + c.once.Do(func() { + err = c.pc.Close() + }) + return +} + +func (c *webRTCConn) LocalAddr() net.Addr { + return nil +} + +func (c *webRTCConn) RemoteAddr() net.Addr { + //Parse Remote SDP offer and extract client IP + clientIP := remoteIPFromSDP(c.pc.RemoteDescription().SDP) + if clientIP == nil { + return nil + } + return &net.IPAddr{IP: clientIP, Zone: ""} +} + +func (c *webRTCConn) SetDeadline(t time.Time) error { + // nolint: golint + return fmt.Errorf("SetDeadline not implemented") +} + +func (c *webRTCConn) SetReadDeadline(t time.Time) error { + // nolint: golint + return fmt.Errorf("SetReadDeadline not implemented") +} + +func (c *webRTCConn) SetWriteDeadline(t time.Time) error { + // nolint: golint + return fmt.Errorf("SetWriteDeadline not implemented") +} + +func remoteIPFromSDP(str string) net.IP { + // Look for remote IP in "a=candidate" attribute fields + // https://tools.ietf.org/html/rfc5245#section-15.1 + var desc sdp.SessionDescription + err := desc.Unmarshal([]byte(str)) + if err != nil { + log.Println("Error parsing SDP: ", err.Error()) + return nil + } + for _, m := range desc.MediaDescriptions { + for _, a := range m.Attributes { + if a.IsICECandidate() { + c, err := ice.UnmarshalCandidate(a.Value) + if err == nil { + ip := net.ParseIP(c.Address()) + if ip != nil && isRemoteAddress(ip) { + return ip + } + } + } + } + } + // Finally look for remote IP in "c=" Connection Data field + // https://tools.ietf.org/html/rfc4566#section-5.7 + for _, pattern := range remoteIPPatterns { + m := pattern.FindStringSubmatch(str) + if m != nil { + // Ignore parsing errors, ParseIP returns nil. + ip := net.ParseIP(m[1]) + if ip != nil && isRemoteAddress(ip) { + return ip + } + + } + } + + return nil +} From 015958fbe66bd91a003c6fc92a11bd5d13b887c3 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 20 May 2021 07:49:27 -0400 Subject: [PATCH 200/385] Intermediary refactor teasing apart http / ipc Introduces an IPC struct and moves the logic out of the http handlers and into methods on that. --- broker/broker.go | 346 ++++++++------------------------ broker/ipc.go | 293 +++++++++++++++++++++++++++ broker/snowflake-broker_test.go | 111 +++++----- common/messages/ipc.go | 18 ++ 4 files changed, 449 insertions(+), 319 deletions(-) create mode 100644 broker/ipc.go create mode 100644 common/messages/ipc.go diff --git a/broker/broker.go b/broker/broker.go index fc4727d..58f3955 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -6,15 +6,13 @@ SessionDescriptions in order to negotiate a WebRTC connection. package main import ( - "bytes" "container/heap" "crypto/tls" + "errors" "flag" - "fmt" "io" "io/ioutil" "log" - "net" "net/http" "os" "os/signal" @@ -31,23 +29,7 @@ import ( ) const ( - ClientTimeout = 10 - ProxyTimeout = 10 - readLimit = 100000 //Maximum number of bytes to be read from an HTTP request - - NATUnknown = "unknown" - NATRestricted = "restricted" - NATUnrestricted = "unrestricted" -) - -// We support two client message formats. The legacy format is for backwards -// combatability and relies heavily on HTTP headers and status codes to convey -// information. -type clientVersion int - -const ( - v0 clientVersion = iota //legacy version - v1 + readLimit = 100000 // Maximum number of bytes to be read from an HTTP request ) type BrokerContext struct { @@ -89,8 +71,8 @@ func NewBrokerContext(metricsLogger *log.Logger) *BrokerContext { // Implements the http.Handler interface type SnowflakeHandler struct { - *BrokerContext - handle func(*BrokerContext, http.ResponseWriter, *http.Request) + *IPC + handle func(*IPC, http.ResponseWriter, *http.Request) } // Implements the http.Handler interface @@ -106,7 +88,7 @@ func (sh SnowflakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if "OPTIONS" == r.Method { return } - sh.handle(sh.BrokerContext, w, r) + sh.handle(sh.IPC, w, r) } func (mh MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -199,7 +181,7 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri /* For snowflake proxies to request a client from the Broker. */ -func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { +func proxyPolls(i *IPC, w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) if err != nil { log.Println("Invalid data.") @@ -207,47 +189,28 @@ func proxyPolls(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { return } - sid, proxyType, natType, clients, err := messages.DecodePollRequest(body) - if err != nil { + arg := messages.Arg{ + Body: body, + RemoteAddr: r.RemoteAddr, + NatType: "", + } + + var response []byte + err = i.ProxyPolls(arg, &response) + switch { + case err == nil: + case errors.Is(err, messages.ErrBadRequest): w.WriteHeader(http.StatusBadRequest) return - } - - // Log geoip stats - remoteIP, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - log.Println("Error processing proxy IP: ", err.Error()) - } else { - ctx.metrics.lock.Lock() - ctx.metrics.UpdateCountryStats(remoteIP, proxyType, natType) - ctx.metrics.lock.Unlock() - } - - // Wait for a client to avail an offer to the snowflake, or timeout if nil. - offer := ctx.RequestOffer(sid, proxyType, natType, clients) - var b []byte - if nil == offer { - ctx.metrics.lock.Lock() - ctx.metrics.proxyIdleCount++ - ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "idle"}).Inc() - ctx.metrics.lock.Unlock() - - b, err = messages.EncodePollResponse("", false, "") - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write(b) - return - } - ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "matched"}).Inc() - b, err = messages.EncodePollResponse(string(offer.sdp), true, offer.natType) - if err != nil { + case errors.Is(err, messages.ErrInternal): + fallthrough + default: + log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } - if _, err := w.Write(b); err != nil { + + if _, err := w.Write(response); err != nil { log.Printf("proxyPolls unable to write offer with error: %v", err) } } @@ -258,162 +221,44 @@ type ClientOffer struct { sdp []byte } -// Sends an encoded response to the client and an -// HTTP server error if the response encoding fails -func sendClientResponse(resp *messages.ClientPollResponse, w http.ResponseWriter) { - data, err := resp.EncodePollResponse() - if err != nil { - log.Printf("error encoding answer") - w.WriteHeader(http.StatusInternalServerError) - } else { - if _, err := w.Write([]byte(data)); err != nil { - log.Printf("unable to write answer with error: %v", err) - } - } -} - /* Expects a WebRTC SDP offer in the Request to give to an assigned snowflake proxy, which responds with the SDP answer to be sent in the HTTP response back to the client. */ -func clientOffers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { - var err error - var version clientVersion - - startTime := time.Now() +func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) if err != nil { log.Printf("Error reading client request: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + + arg := messages.Arg{ + Body: body, + RemoteAddr: "", + NatType: r.Header.Get("Snowflake-NAT-Type"), + } + + var response []byte + err = i.ClientOffers(arg, &response) + switch { + case err == nil: + case errors.Is(err, messages.ErrUnavailable): + w.WriteHeader(http.StatusServiceUnavailable) + return + case errors.Is(err, messages.ErrTimeout): + w.WriteHeader(http.StatusGatewayTimeout) + return + default: + log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } - if len(body) > 0 && body[0] == '{' { - version = v0 - } else { - parts := bytes.SplitN(body, []byte("\n"), 2) - if len(parts) < 2 { - // no version number found - err := fmt.Errorf("unsupported message version") - sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, w) - return - } - body = parts[1] - if string(parts[0]) == "1.0" { - version = v1 - } else { - err := fmt.Errorf("unsupported message version") - sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, w) - return - } + if _, err := w.Write(response); err != nil { + log.Printf("clientOffers unable to write answer with error: %v", err) } - - var offer *ClientOffer - switch version { - case v0: - offer = &ClientOffer{ - natType: r.Header.Get("Snowflake-NAT-Type"), - sdp: body, - } - case v1: - req, err := messages.DecodeClientPollRequest(body) - if err != nil { - sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, w) - return - } - offer = &ClientOffer{ - natType: req.NAT, - sdp: []byte(req.Offer), - } - default: - panic("unknown version") - } - - // Only hand out known restricted snowflakes to unrestricted clients - var snowflakeHeap *SnowflakeHeap - if offer.natType == NATUnrestricted { - snowflakeHeap = ctx.restrictedSnowflakes - } else { - snowflakeHeap = ctx.snowflakes - } - - // Immediately fail if there are no snowflakes available. - ctx.snowflakeLock.Lock() - numSnowflakes := snowflakeHeap.Len() - ctx.snowflakeLock.Unlock() - if numSnowflakes <= 0 { - ctx.metrics.lock.Lock() - ctx.metrics.clientDeniedCount++ - ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "denied"}).Inc() - if offer.natType == NATUnrestricted { - ctx.metrics.clientUnrestrictedDeniedCount++ - } else { - ctx.metrics.clientRestrictedDeniedCount++ - } - ctx.metrics.lock.Unlock() - switch version { - case v0: - w.WriteHeader(http.StatusServiceUnavailable) - case v1: - resp := &messages.ClientPollResponse{Error: "no snowflake proxies currently available"} - sendClientResponse(resp, w) - default: - panic("unknown version") - } - return - } - // Otherwise, find the most available snowflake proxy, and pass the offer to it. - // Delete must be deferred in order to correctly process answer request later. - ctx.snowflakeLock.Lock() - snowflake := heap.Pop(snowflakeHeap).(*Snowflake) - ctx.snowflakeLock.Unlock() - snowflake.offerChannel <- offer - - // Wait for the answer to be returned on the channel or timeout. - select { - case answer := <-snowflake.answerChannel: - ctx.metrics.lock.Lock() - ctx.metrics.clientProxyMatchCount++ - ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "matched"}).Inc() - ctx.metrics.lock.Unlock() - switch version { - case v0: - if _, err := w.Write([]byte(answer)); err != nil { - log.Printf("unable to write answer with error: %v", err) - } - case v1: - resp := &messages.ClientPollResponse{Answer: answer} - sendClientResponse(resp, w) - default: - panic("unknown version") - } - // Initial tracking of elapsed time. - ctx.metrics.clientRoundtripEstimate = time.Since(startTime) / - time.Millisecond - case <-time.After(time.Second * ClientTimeout): - log.Println("Client: Timed out.") - switch version { - case v0: - w.WriteHeader(http.StatusGatewayTimeout) - if _, err := w.Write( - []byte("timed out waiting for answer!")); err != nil { - log.Printf("unable to write timeout error, failed with error: %v", - err) - } - case v1: - resp := &messages.ClientPollResponse{ - Error: "timed out waiting for answer!"} - sendClientResponse(resp, w) - default: - panic("unknown version") - } - } - - ctx.snowflakeLock.Lock() - ctx.metrics.promMetrics.AvailableProxies.With(prometheus.Labels{"nat": snowflake.natType, "type": snowflake.proxyType}).Dec() - delete(ctx.idToSnowflake, snowflake.id) - ctx.snowflakeLock.Unlock() } /* @@ -421,82 +266,51 @@ Expects snowflake proxes which have previously successfully received an offer from proxyHandler to respond with an answer in an HTTP POST, which the broker will pass back to the original client. */ -func proxyAnswers(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { - +func proxyAnswers(i *IPC, w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) - if nil != err || nil == body || len(body) <= 0 { + if err != nil { log.Println("Invalid data.") w.WriteHeader(http.StatusBadRequest) return } - answer, id, err := messages.DecodeAnswerRequest(body) - if err != nil || answer == "" { - w.WriteHeader(http.StatusBadRequest) - return + arg := messages.Arg{ + Body: body, + RemoteAddr: "", + NatType: "", } - var success = true - ctx.snowflakeLock.Lock() - snowflake, ok := ctx.idToSnowflake[id] - ctx.snowflakeLock.Unlock() - if !ok || nil == snowflake { - // The snowflake took too long to respond with an answer, so its client - // disappeared / the snowflake is no longer recognized by the Broker. - success = false - } - b, err := messages.EncodeAnswerResponse(success) - if err != nil { - log.Printf("Error encoding answer: %s", err.Error()) + var response []byte + err = i.ProxyAnswers(arg, &response) + switch { + case err == nil: + case errors.Is(err, messages.ErrBadRequest): + w.WriteHeader(http.StatusBadRequest) + return + case errors.Is(err, messages.ErrInternal): + fallthrough + default: + log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } - w.Write(b) - if success { - snowflake.answerChannel <- answer + if _, err := w.Write(response); err != nil { + log.Printf("proxyAnswers unable to write answer response with error: %v", err) } - } -func debugHandler(ctx *BrokerContext, w http.ResponseWriter, r *http.Request) { - - var webexts, browsers, standalones, unknowns int - var natRestricted, natUnrestricted, natUnknown int - ctx.snowflakeLock.Lock() - s := fmt.Sprintf("current snowflakes available: %d\n", len(ctx.idToSnowflake)) - for _, snowflake := range ctx.idToSnowflake { - if snowflake.proxyType == "badge" { - browsers++ - } else if snowflake.proxyType == "webext" { - webexts++ - } else if snowflake.proxyType == "standalone" { - standalones++ - } else { - unknowns++ - } - - switch snowflake.natType { - case NATRestricted: - natRestricted++ - case NATUnrestricted: - natUnrestricted++ - default: - natUnknown++ - } +func debugHandler(i *IPC, w http.ResponseWriter, r *http.Request) { + var response string + err := i.Debug(new(interface{}), &response) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return } - ctx.snowflakeLock.Unlock() - s += fmt.Sprintf("\tstandalone proxies: %d", standalones) - s += fmt.Sprintf("\n\tbrowser proxies: %d", browsers) - s += fmt.Sprintf("\n\twebext proxies: %d", webexts) - s += fmt.Sprintf("\n\tunknown proxies: %d", unknowns) - s += fmt.Sprintf("\nNAT Types available:") - s += fmt.Sprintf("\n\trestricted: %d", natRestricted) - s += fmt.Sprintf("\n\tunrestricted: %d", natUnrestricted) - s += fmt.Sprintf("\n\tunknown: %d", natUnknown) - if _, err := w.Write([]byte(s)); err != nil { + if _, err := w.Write([]byte(response)); err != nil { log.Printf("writing proxy information returned error: %v ", err) } } @@ -589,12 +403,14 @@ func main() { go ctx.Broker() + i := &IPC{ctx} + http.HandleFunc("/robots.txt", robotsTxtHandler) - http.Handle("/proxy", SnowflakeHandler{ctx, proxyPolls}) - http.Handle("/client", SnowflakeHandler{ctx, clientOffers}) - http.Handle("/answer", SnowflakeHandler{ctx, proxyAnswers}) - http.Handle("/debug", SnowflakeHandler{ctx, debugHandler}) + http.Handle("/proxy", SnowflakeHandler{i, proxyPolls}) + http.Handle("/client", SnowflakeHandler{i, clientOffers}) + http.Handle("/answer", SnowflakeHandler{i, proxyAnswers}) + http.Handle("/debug", SnowflakeHandler{i, debugHandler}) http.Handle("/metrics", MetricsHandler{metricsFilename, metricsHandler}) http.Handle("/prometheus", promhttp.HandlerFor(ctx.metrics.promMetrics.registry, promhttp.HandlerOpts{})) diff --git a/broker/ipc.go b/broker/ipc.go new file mode 100644 index 0000000..79ccf0f --- /dev/null +++ b/broker/ipc.go @@ -0,0 +1,293 @@ +package main + +import ( + "bytes" + "container/heap" + "fmt" + "log" + "net" + "time" + + "git.torproject.org/pluggable-transports/snowflake.git/common/messages" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + ClientTimeout = 10 + ProxyTimeout = 10 + + NATUnknown = "unknown" + NATRestricted = "restricted" + NATUnrestricted = "unrestricted" +) + +// We support two client message formats. The legacy format is for backwards +// combatability and relies heavily on HTTP headers and status codes to convey +// information. +type clientVersion int + +const ( + v0 clientVersion = iota //legacy version + v1 +) + +type IPC struct { + ctx *BrokerContext +} + +func (i *IPC) Debug(_ interface{}, response *string) error { + var webexts, browsers, standalones, unknowns int + var natRestricted, natUnrestricted, natUnknown int + + i.ctx.snowflakeLock.Lock() + s := fmt.Sprintf("current snowflakes available: %d\n", len(i.ctx.idToSnowflake)) + for _, snowflake := range i.ctx.idToSnowflake { + if snowflake.proxyType == "badge" { + browsers++ + } else if snowflake.proxyType == "webext" { + webexts++ + } else if snowflake.proxyType == "standalone" { + standalones++ + } else { + unknowns++ + } + + switch snowflake.natType { + case NATRestricted: + natRestricted++ + case NATUnrestricted: + natUnrestricted++ + default: + natUnknown++ + } + + } + i.ctx.snowflakeLock.Unlock() + + s += fmt.Sprintf("\tstandalone proxies: %d", standalones) + s += fmt.Sprintf("\n\tbrowser proxies: %d", browsers) + s += fmt.Sprintf("\n\twebext proxies: %d", webexts) + s += fmt.Sprintf("\n\tunknown proxies: %d", unknowns) + + s += fmt.Sprintf("\nNAT Types available:") + s += fmt.Sprintf("\n\trestricted: %d", natRestricted) + s += fmt.Sprintf("\n\tunrestricted: %d", natUnrestricted) + s += fmt.Sprintf("\n\tunknown: %d", natUnknown) + + *response = s + return nil +} + +func (i *IPC) ProxyPolls(arg messages.Arg, response *[]byte) error { + sid, proxyType, natType, clients, err := messages.DecodePollRequest(arg.Body) + if err != nil { + return messages.ErrBadRequest + } + + // Log geoip stats + remoteIP, _, err := net.SplitHostPort(arg.RemoteAddr) + if err != nil { + log.Println("Error processing proxy IP: ", err.Error()) + } else { + i.ctx.metrics.lock.Lock() + i.ctx.metrics.UpdateCountryStats(remoteIP, proxyType, natType) + i.ctx.metrics.lock.Unlock() + } + + var b []byte + + // Wait for a client to avail an offer to the snowflake, or timeout if nil. + offer := i.ctx.RequestOffer(sid, proxyType, natType, clients) + + if offer == nil { + i.ctx.metrics.lock.Lock() + i.ctx.metrics.proxyIdleCount++ + i.ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "idle"}).Inc() + i.ctx.metrics.lock.Unlock() + + b, err = messages.EncodePollResponse("", false, "") + if err != nil { + return messages.ErrInternal + } + + *response = b + return nil + } + + i.ctx.metrics.promMetrics.ProxyPollTotal.With(prometheus.Labels{"nat": natType, "status": "matched"}).Inc() + b, err = messages.EncodePollResponse(string(offer.sdp), true, offer.natType) + if err != nil { + return messages.ErrInternal + } + *response = b + + return nil +} + +func sendClientResponse(resp *messages.ClientPollResponse, response *[]byte) error { + data, err := resp.EncodePollResponse() + if err != nil { + log.Printf("error encoding answer") + return messages.ErrInternal + } else { + *response = []byte(data) + return nil + } +} + +func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error { + var version clientVersion + + startTime := time.Now() + body := arg.Body + + if len(body) > 0 && body[0] == '{' { + version = v0 + } else { + parts := bytes.SplitN(body, []byte("\n"), 2) + if len(parts) < 2 { + // no version number found + err := fmt.Errorf("unsupported message version") + return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response) + } + body = parts[1] + if string(parts[0]) == "1.0" { + version = v1 + + } else { + err := fmt.Errorf("unsupported message version") + return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response) + } + } + + var offer *ClientOffer + switch version { + case v0: + offer = &ClientOffer{ + natType: arg.NatType, + sdp: body, + } + case v1: + req, err := messages.DecodeClientPollRequest(body) + if err != nil { + return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response) + } + offer = &ClientOffer{ + natType: req.NAT, + sdp: []byte(req.Offer), + } + default: + panic("unknown version") + } + + // Only hand out known restricted snowflakes to unrestricted clients + var snowflakeHeap *SnowflakeHeap + if offer.natType == NATUnrestricted { + snowflakeHeap = i.ctx.restrictedSnowflakes + } else { + snowflakeHeap = i.ctx.snowflakes + } + + // Immediately fail if there are no snowflakes available. + i.ctx.snowflakeLock.Lock() + numSnowflakes := snowflakeHeap.Len() + i.ctx.snowflakeLock.Unlock() + if numSnowflakes <= 0 { + i.ctx.metrics.lock.Lock() + i.ctx.metrics.clientDeniedCount++ + i.ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "denied"}).Inc() + if offer.natType == NATUnrestricted { + i.ctx.metrics.clientUnrestrictedDeniedCount++ + } else { + i.ctx.metrics.clientRestrictedDeniedCount++ + } + i.ctx.metrics.lock.Unlock() + switch version { + case v0: + return messages.ErrUnavailable + case v1: + resp := &messages.ClientPollResponse{Error: "no snowflake proxies currently available"} + return sendClientResponse(resp, response) + default: + panic("unknown version") + } + } + + // Otherwise, find the most available snowflake proxy, and pass the offer to it. + // Delete must be deferred in order to correctly process answer request later. + i.ctx.snowflakeLock.Lock() + snowflake := heap.Pop(snowflakeHeap).(*Snowflake) + i.ctx.snowflakeLock.Unlock() + snowflake.offerChannel <- offer + + var err error + + // Wait for the answer to be returned on the channel or timeout. + select { + case answer := <-snowflake.answerChannel: + i.ctx.metrics.lock.Lock() + i.ctx.metrics.clientProxyMatchCount++ + i.ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "matched"}).Inc() + i.ctx.metrics.lock.Unlock() + switch version { + case v0: + *response = []byte(answer) + case v1: + resp := &messages.ClientPollResponse{Answer: answer} + err = sendClientResponse(resp, response) + default: + panic("unknown version") + } + // Initial tracking of elapsed time. + i.ctx.metrics.clientRoundtripEstimate = time.Since(startTime) / time.Millisecond + case <-time.After(time.Second * ClientTimeout): + log.Println("Client: Timed out.") + switch version { + case v0: + err = messages.ErrTimeout + case v1: + resp := &messages.ClientPollResponse{ + Error: "timed out waiting for answer!"} + err = sendClientResponse(resp, response) + default: + panic("unknown version") + } + } + + i.ctx.snowflakeLock.Lock() + i.ctx.metrics.promMetrics.AvailableProxies.With(prometheus.Labels{"nat": snowflake.natType, "type": snowflake.proxyType}).Dec() + delete(i.ctx.idToSnowflake, snowflake.id) + i.ctx.snowflakeLock.Unlock() + + return err +} + +func (i *IPC) ProxyAnswers(arg messages.Arg, response *[]byte) error { + answer, id, err := messages.DecodeAnswerRequest(arg.Body) + if err != nil || answer == "" { + return messages.ErrBadRequest + } + + var success = true + i.ctx.snowflakeLock.Lock() + snowflake, ok := i.ctx.idToSnowflake[id] + i.ctx.snowflakeLock.Unlock() + if !ok || snowflake == nil { + // The snowflake took too long to respond with an answer, so its client + // disappeared / the snowflake is no longer recognized by the Broker. + success = false + } + + b, err := messages.EncodeAnswerResponse(success) + if err != nil { + log.Printf("Error encoding answer: %s", err.Error()) + return messages.ErrInternal + } + *response = b + + if success { + snowflake.answerChannel <- answer + } + + return nil +} diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 825bc6f..77e62cf 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -28,6 +28,7 @@ func TestBroker(t *testing.T) { Convey("Context", t, func() { ctx := NewBrokerContext(NullLogger()) + i := &IPC{ctx} Convey("Adds Snowflake", func() { So(ctx.snowflakes.Len(), ShouldEqual, 0) @@ -76,7 +77,7 @@ func TestBroker(t *testing.T) { So(err, ShouldBeNil) Convey("with error when no snowflakes are available.", func() { - clientOffers(ctx, w, r) + clientOffers(i, w, r) So(w.Code, ShouldEqual, http.StatusOK) So(w.Body.String(), ShouldEqual, `{"error":"no snowflake proxies currently available"}`) }) @@ -86,7 +87,7 @@ func TestBroker(t *testing.T) { // Prepare a fake proxy to respond with. snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) go func() { - clientOffers(ctx, w, r) + clientOffers(i, w, r) done <- true }() offer := <-snowflake.offerChannel @@ -104,7 +105,7 @@ func TestBroker(t *testing.T) { done := make(chan bool) snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) go func() { - clientOffers(ctx, w, r) + clientOffers(i, w, r) // Takes a few seconds here... done <- true }() @@ -124,7 +125,7 @@ func TestBroker(t *testing.T) { r.Header.Set("Snowflake-NAT-TYPE", "restricted") Convey("with 503 when no snowflakes are available.", func() { - clientOffers(ctx, w, r) + clientOffers(i, w, r) So(w.Code, ShouldEqual, http.StatusServiceUnavailable) So(w.Body.String(), ShouldEqual, "") }) @@ -134,7 +135,7 @@ func TestBroker(t *testing.T) { // Prepare a fake proxy to respond with. snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) go func() { - clientOffers(ctx, w, r) + clientOffers(i, w, r) done <- true }() offer := <-snowflake.offerChannel @@ -152,7 +153,7 @@ func TestBroker(t *testing.T) { done := make(chan bool) snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) go func() { - clientOffers(ctx, w, r) + clientOffers(i, w, r) // Takes a few seconds here... done <- true }() @@ -173,7 +174,7 @@ func TestBroker(t *testing.T) { Convey("with a client offer if available.", func() { go func(ctx *BrokerContext) { - proxyPolls(ctx, w, r) + proxyPolls(i, w, r) done <- true }(ctx) // Pass a fake client offer to this proxy @@ -187,7 +188,7 @@ func TestBroker(t *testing.T) { Convey("return empty 200 OK when no client offer is available.", func() { go func(ctx *BrokerContext) { - proxyPolls(ctx, w, r) + proxyPolls(i, w, r) done <- true }(ctx) p := <-ctx.proxyPolls @@ -209,7 +210,7 @@ func TestBroker(t *testing.T) { r, err := http.NewRequest("POST", "snowflake.broker/answer", data) So(err, ShouldBeNil) go func(ctx *BrokerContext) { - proxyAnswers(ctx, w, r) + proxyAnswers(i, w, r) }(ctx) answer := <-s.answerChannel So(w.Code, ShouldEqual, http.StatusOK) @@ -220,7 +221,7 @@ func TestBroker(t *testing.T) { data = bytes.NewReader([]byte(`{"Version":"1.0","Sid":"invalid","Answer":"test"}`)) r, err := http.NewRequest("POST", "snowflake.broker/answer", data) So(err, ShouldBeNil) - proxyAnswers(ctx, w, r) + proxyAnswers(i, w, r) So(w.Code, ShouldEqual, http.StatusOK) b, err := ioutil.ReadAll(w.Body) So(err, ShouldBeNil) @@ -232,7 +233,7 @@ func TestBroker(t *testing.T) { data := bytes.NewReader(nil) r, err := http.NewRequest("POST", "snowflake.broker/answer", data) So(err, ShouldBeNil) - proxyAnswers(ctx, w, r) + proxyAnswers(i, w, r) So(w.Code, ShouldEqual, http.StatusBadRequest) }) @@ -240,7 +241,7 @@ func TestBroker(t *testing.T) { data := bytes.NewReader(make([]byte, 100001)) r, err := http.NewRequest("POST", "snowflake.broker/answer", data) So(err, ShouldBeNil) - proxyAnswers(ctx, w, r) + proxyAnswers(i, w, r) So(w.Code, ShouldEqual, http.StatusBadRequest) }) @@ -250,6 +251,7 @@ func TestBroker(t *testing.T) { Convey("End-To-End", t, func() { ctx := NewBrokerContext(NullLogger()) + i := &IPC{ctx} Convey("Check for client/proxy data race", func() { proxy_done := make(chan bool) @@ -264,7 +266,7 @@ func TestBroker(t *testing.T) { So(err, ShouldBeNil) go func(ctx *BrokerContext) { - proxyPolls(ctx, wp, rp) + proxyPolls(i, wp, rp) proxy_done <- true }(ctx) @@ -275,7 +277,7 @@ func TestBroker(t *testing.T) { So(err, ShouldBeNil) go func() { - clientOffers(ctx, wc, rc) + clientOffers(i, wc, rc) client_done <- true }() @@ -288,7 +290,7 @@ func TestBroker(t *testing.T) { rp, err = http.NewRequest("POST", "snowflake.broker/answer", datap) So(err, ShouldBeNil) go func(ctx *BrokerContext) { - proxyAnswers(ctx, wp, rp) + proxyAnswers(i, wp, rp) proxy_done <- true }(ctx) @@ -307,7 +309,7 @@ func TestBroker(t *testing.T) { rP, err := http.NewRequest("POST", "snowflake.broker/proxy", dataP) So(err, ShouldBeNil) go func() { - proxyPolls(ctx, wP, rP) + proxyPolls(i, wP, rP) polled <- true }() @@ -328,7 +330,7 @@ func TestBroker(t *testing.T) { rC, err := http.NewRequest("POST", "snowflake.broker/client", dataC) So(err, ShouldBeNil) go func() { - clientOffers(ctx, wC, rC) + clientOffers(i, wC, rC) done <- true }() @@ -341,7 +343,7 @@ func TestBroker(t *testing.T) { dataA := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"ymbcCMto7KHNGYlp","Answer":"test"}`)) rA, err := http.NewRequest("POST", "snowflake.broker/answer", dataA) So(err, ShouldBeNil) - proxyAnswers(ctx, wA, rA) + proxyAnswers(i, wA, rA) So(wA.Code, ShouldEqual, http.StatusOK) <-done @@ -503,6 +505,7 @@ func TestMetrics(t *testing.T) { done := make(chan bool) buf := new(bytes.Buffer) ctx := NewBrokerContext(log.New(buf, "", 0)) + i := &IPC{ctx} err := ctx.metrics.LoadGeoipDatabases("test_geoip", "test_geoip6") So(err, ShouldEqual, nil) @@ -514,10 +517,10 @@ func TestMetrics(t *testing.T) { r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) - go func(ctx *BrokerContext) { - proxyPolls(ctx, w, r) + go func(i *IPC) { + proxyPolls(i, w, r) done <- true - }(ctx) + }(i) p := <-ctx.proxyPolls //manually unblock poll p.offerChannel <- nil <-done @@ -527,10 +530,10 @@ func TestMetrics(t *testing.T) { r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) - go func(ctx *BrokerContext) { - proxyPolls(ctx, w, r) + go func(i *IPC) { + proxyPolls(i, w, r) done <- true - }(ctx) + }(i) p = <-ctx.proxyPolls //manually unblock poll p.offerChannel <- nil <-done @@ -540,10 +543,10 @@ func TestMetrics(t *testing.T) { r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) - go func(ctx *BrokerContext) { - proxyPolls(ctx, w, r) + go func(i *IPC) { + proxyPolls(i, w, r) done <- true - }(ctx) + }(i) p = <-ctx.proxyPolls //manually unblock poll p.offerChannel <- nil <-done @@ -553,10 +556,10 @@ func TestMetrics(t *testing.T) { r, err = http.NewRequest("POST", "snowflake.broker/proxy", data) r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) - go func(ctx *BrokerContext) { - proxyPolls(ctx, w, r) + go func(i *IPC) { + proxyPolls(i, w, r) done <- true - }(ctx) + }(i) p = <-ctx.proxyPolls //manually unblock poll p.offerChannel <- nil <-done @@ -573,7 +576,7 @@ func TestMetrics(t *testing.T) { r, err := http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) ctx.metrics.printMetrics() So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0") @@ -595,7 +598,7 @@ func TestMetrics(t *testing.T) { // Prepare a fake proxy to respond with. snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0) go func() { - clientOffers(ctx, w, r) + clientOffers(i, w, r) done <- true }() offer := <-snowflake.offerChannel @@ -614,49 +617,49 @@ func TestMetrics(t *testing.T) { r, err := http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) w = httptest.NewRecorder() data = bytes.NewReader( []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) r, err = http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) w = httptest.NewRecorder() data = bytes.NewReader( []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) r, err = http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) w = httptest.NewRecorder() data = bytes.NewReader( []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) r, err = http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) w = httptest.NewRecorder() data = bytes.NewReader( []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) r, err = http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) w = httptest.NewRecorder() data = bytes.NewReader( []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) r, err = http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) w = httptest.NewRecorder() data = bytes.NewReader( []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) r, err = http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) w = httptest.NewRecorder() data = bytes.NewReader( []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) r, err = http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) ctx.metrics.printMetrics() So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\n") @@ -666,7 +669,7 @@ func TestMetrics(t *testing.T) { []byte("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}")) r, err = http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) buf.Reset() ctx.metrics.printMetrics() So(buf.String(), ShouldContainSubstring, "client-denied-count 16\nclient-restricted-denied-count 16\nclient-unrestricted-denied-count 0\n") @@ -680,7 +683,7 @@ func TestMetrics(t *testing.T) { r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) go func(ctx *BrokerContext) { - proxyPolls(ctx, w, r) + proxyPolls(i, w, r) done <- true }(ctx) p := <-ctx.proxyPolls //manually unblock poll @@ -693,10 +696,10 @@ func TestMetrics(t *testing.T) { log.Printf("unable to get NewRequest with error: %v", err) } r.RemoteAddr = "129.97.208.23:8888" //CA geoip - go func(ctx *BrokerContext) { - proxyPolls(ctx, w, r) + go func(i *IPC) { + proxyPolls(i, w, r) done <- true - }(ctx) + }(i) p = <-ctx.proxyPolls //manually unblock poll p.offerChannel <- nil <-done @@ -711,10 +714,10 @@ func TestMetrics(t *testing.T) { r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) - go func(ctx *BrokerContext) { - proxyPolls(ctx, w, r) + go func(i *IPC) { + proxyPolls(i, w, r) done <- true - }(ctx) + }(i) p := <-ctx.proxyPolls //manually unblock poll p.offerChannel <- nil <-done @@ -728,10 +731,10 @@ func TestMetrics(t *testing.T) { log.Printf("unable to get NewRequest with error: %v", err) } r.RemoteAddr = "129.97.208.24:8888" //CA geoip - go func(ctx *BrokerContext) { - proxyPolls(ctx, w, r) + go func(i *IPC) { + proxyPolls(i, w, r) done <- true - }(ctx) + }(i) p = <-ctx.proxyPolls //manually unblock poll p.offerChannel <- nil <-done @@ -747,7 +750,7 @@ func TestMetrics(t *testing.T) { r, err := http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) ctx.metrics.printMetrics() So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0") @@ -760,7 +763,7 @@ func TestMetrics(t *testing.T) { r, err = http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) ctx.metrics.printMetrics() So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 0\nclient-unrestricted-denied-count 8\nclient-snowflake-match-count 0") @@ -773,7 +776,7 @@ func TestMetrics(t *testing.T) { r, err = http.NewRequest("POST", "snowflake.broker/client", data) So(err, ShouldBeNil) - clientOffers(ctx, w, r) + clientOffers(i, w, r) ctx.metrics.printMetrics() So(buf.String(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\nclient-snowflake-match-count 0") diff --git a/common/messages/ipc.go b/common/messages/ipc.go new file mode 100644 index 0000000..3f89200 --- /dev/null +++ b/common/messages/ipc.go @@ -0,0 +1,18 @@ +package messages + +import ( + "errors" +) + +type Arg struct { + Body []byte + RemoteAddr string + NatType string +} + +var ( + ErrBadRequest = errors.New("bad request") + ErrInternal = errors.New("internal error") + ErrUnavailable = errors.New("service unavailable") + ErrTimeout = errors.New("timeout") +) From 0ced1cc32497f3b47a614ef4a526af7925447252 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 20 May 2021 08:31:30 -0400 Subject: [PATCH 201/385] Move http handlers to a separate file --- broker/broker.go | 196 -------------------------------------------- broker/http.go | 205 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 196 deletions(-) create mode 100644 broker/http.go diff --git a/broker/broker.go b/broker/broker.go index 58f3955..437a4d1 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -8,10 +8,8 @@ package main import ( "container/heap" "crypto/tls" - "errors" "flag" "io" - "io/ioutil" "log" "net/http" "os" @@ -21,17 +19,12 @@ import ( "syscall" "time" - "git.torproject.org/pluggable-transports/snowflake.git/common/messages" "git.torproject.org/pluggable-transports/snowflake.git/common/safelog" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/crypto/acme/autocert" ) -const ( - readLimit = 100000 // Maximum number of bytes to be read from an HTTP request -) - type BrokerContext struct { snowflakes *SnowflakeHeap restrictedSnowflakes *SnowflakeHeap @@ -69,38 +62,6 @@ func NewBrokerContext(metricsLogger *log.Logger) *BrokerContext { } } -// Implements the http.Handler interface -type SnowflakeHandler struct { - *IPC - handle func(*IPC, http.ResponseWriter, *http.Request) -} - -// Implements the http.Handler interface -type MetricsHandler struct { - logFilename string - handle func(string, http.ResponseWriter, *http.Request) -} - -func (sh SnowflakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Session-ID") - // Return early if it's CORS preflight. - if "OPTIONS" == r.Method { - return - } - sh.handle(sh.IPC, w, r) -} - -func (mh MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Session-ID") - // Return early if it's CORS preflight. - if "OPTIONS" == r.Method { - return - } - mh.handle(mh.logFilename, w, r) -} - // Proxies may poll for client offers concurrently. type ProxyPoll struct { id string @@ -178,169 +139,12 @@ func (ctx *BrokerContext) AddSnowflake(id string, proxyType string, natType stri return snowflake } -/* -For snowflake proxies to request a client from the Broker. -*/ -func proxyPolls(i *IPC, w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) - if err != nil { - log.Println("Invalid data.") - w.WriteHeader(http.StatusBadRequest) - return - } - - arg := messages.Arg{ - Body: body, - RemoteAddr: r.RemoteAddr, - NatType: "", - } - - var response []byte - err = i.ProxyPolls(arg, &response) - switch { - case err == nil: - case errors.Is(err, messages.ErrBadRequest): - w.WriteHeader(http.StatusBadRequest) - return - case errors.Is(err, messages.ErrInternal): - fallthrough - default: - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if _, err := w.Write(response); err != nil { - log.Printf("proxyPolls unable to write offer with error: %v", err) - } -} - // Client offer contains an SDP and the NAT type of the client type ClientOffer struct { natType string sdp []byte } -/* -Expects a WebRTC SDP offer in the Request to give to an assigned -snowflake proxy, which responds with the SDP answer to be sent in -the HTTP response back to the client. -*/ -func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) - if err != nil { - log.Printf("Error reading client request: %s", err.Error()) - w.WriteHeader(http.StatusBadRequest) - return - } - - arg := messages.Arg{ - Body: body, - RemoteAddr: "", - NatType: r.Header.Get("Snowflake-NAT-Type"), - } - - var response []byte - err = i.ClientOffers(arg, &response) - switch { - case err == nil: - case errors.Is(err, messages.ErrUnavailable): - w.WriteHeader(http.StatusServiceUnavailable) - return - case errors.Is(err, messages.ErrTimeout): - w.WriteHeader(http.StatusGatewayTimeout) - return - default: - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if _, err := w.Write(response); err != nil { - log.Printf("clientOffers unable to write answer with error: %v", err) - } -} - -/* -Expects snowflake proxes which have previously successfully received -an offer from proxyHandler to respond with an answer in an HTTP POST, -which the broker will pass back to the original client. -*/ -func proxyAnswers(i *IPC, w http.ResponseWriter, r *http.Request) { - body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) - if err != nil { - log.Println("Invalid data.") - w.WriteHeader(http.StatusBadRequest) - return - } - - arg := messages.Arg{ - Body: body, - RemoteAddr: "", - NatType: "", - } - - var response []byte - err = i.ProxyAnswers(arg, &response) - switch { - case err == nil: - case errors.Is(err, messages.ErrBadRequest): - w.WriteHeader(http.StatusBadRequest) - return - case errors.Is(err, messages.ErrInternal): - fallthrough - default: - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if _, err := w.Write(response); err != nil { - log.Printf("proxyAnswers unable to write answer response with error: %v", err) - } -} - -func debugHandler(i *IPC, w http.ResponseWriter, r *http.Request) { - var response string - - err := i.Debug(new(interface{}), &response) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if _, err := w.Write([]byte(response)); err != nil { - log.Printf("writing proxy information returned error: %v ", err) - } -} - -func robotsTxtHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - if _, err := w.Write([]byte("User-agent: *\nDisallow: /\n")); err != nil { - log.Printf("robotsTxtHandler unable to write, with this error: %v", err) - } -} - -func metricsHandler(metricsFilename string, w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - - if metricsFilename == "" { - http.NotFound(w, r) - return - } - metricsFile, err := os.OpenFile(metricsFilename, os.O_RDONLY, 0644) - if err != nil { - log.Println("Error opening metrics file for reading") - http.NotFound(w, r) - return - } - - if _, err := io.Copy(w, metricsFile); err != nil { - log.Printf("copying metricsFile returned error: %v", err) - } -} - func main() { var acmeEmail string var acmeHostnamesCommas string diff --git a/broker/http.go b/broker/http.go new file mode 100644 index 0000000..6555d7a --- /dev/null +++ b/broker/http.go @@ -0,0 +1,205 @@ +package main + +import ( + "errors" + "io" + "io/ioutil" + "log" + "net/http" + "os" + + "git.torproject.org/pluggable-transports/snowflake.git/common/messages" +) + +const ( + readLimit = 100000 // Maximum number of bytes to be read from an HTTP request +) + +// Implements the http.Handler interface +type SnowflakeHandler struct { + *IPC + handle func(*IPC, http.ResponseWriter, *http.Request) +} + +func (sh SnowflakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Session-ID") + // Return early if it's CORS preflight. + if "OPTIONS" == r.Method { + return + } + sh.handle(sh.IPC, w, r) +} + +// Implements the http.Handler interface +type MetricsHandler struct { + logFilename string + handle func(string, http.ResponseWriter, *http.Request) +} + +func (mh MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Session-ID") + // Return early if it's CORS preflight. + if "OPTIONS" == r.Method { + return + } + mh.handle(mh.logFilename, w, r) +} + +func robotsTxtHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + if _, err := w.Write([]byte("User-agent: *\nDisallow: /\n")); err != nil { + log.Printf("robotsTxtHandler unable to write, with this error: %v", err) + } +} + +func metricsHandler(metricsFilename string, w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + + if metricsFilename == "" { + http.NotFound(w, r) + return + } + metricsFile, err := os.OpenFile(metricsFilename, os.O_RDONLY, 0644) + if err != nil { + log.Println("Error opening metrics file for reading") + http.NotFound(w, r) + return + } + + if _, err := io.Copy(w, metricsFile); err != nil { + log.Printf("copying metricsFile returned error: %v", err) + } +} + +func debugHandler(i *IPC, w http.ResponseWriter, r *http.Request) { + var response string + + err := i.Debug(new(interface{}), &response) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if _, err := w.Write([]byte(response)); err != nil { + log.Printf("writing proxy information returned error: %v ", err) + } +} + +/* +For snowflake proxies to request a client from the Broker. +*/ +func proxyPolls(i *IPC, w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) + if err != nil { + log.Println("Invalid data.") + w.WriteHeader(http.StatusBadRequest) + return + } + + arg := messages.Arg{ + Body: body, + RemoteAddr: r.RemoteAddr, + NatType: "", + } + + var response []byte + err = i.ProxyPolls(arg, &response) + switch { + case err == nil: + case errors.Is(err, messages.ErrBadRequest): + w.WriteHeader(http.StatusBadRequest) + return + case errors.Is(err, messages.ErrInternal): + fallthrough + default: + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if _, err := w.Write(response); err != nil { + log.Printf("proxyPolls unable to write offer with error: %v", err) + } +} + +/* +Expects a WebRTC SDP offer in the Request to give to an assigned +snowflake proxy, which responds with the SDP answer to be sent in +the HTTP response back to the client. +*/ +func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) + if err != nil { + log.Printf("Error reading client request: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + + arg := messages.Arg{ + Body: body, + RemoteAddr: "", + NatType: r.Header.Get("Snowflake-NAT-Type"), + } + + var response []byte + err = i.ClientOffers(arg, &response) + switch { + case err == nil: + case errors.Is(err, messages.ErrUnavailable): + w.WriteHeader(http.StatusServiceUnavailable) + return + case errors.Is(err, messages.ErrTimeout): + w.WriteHeader(http.StatusGatewayTimeout) + return + default: + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if _, err := w.Write(response); err != nil { + log.Printf("clientOffers unable to write answer with error: %v", err) + } +} + +/* +Expects snowflake proxes which have previously successfully received +an offer from proxyHandler to respond with an answer in an HTTP POST, +which the broker will pass back to the original client. +*/ +func proxyAnswers(i *IPC, w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit)) + if err != nil { + log.Println("Invalid data.") + w.WriteHeader(http.StatusBadRequest) + return + } + + arg := messages.Arg{ + Body: body, + RemoteAddr: "", + NatType: "", + } + + var response []byte + err = i.ProxyAnswers(arg, &response) + switch { + case err == nil: + case errors.Is(err, messages.ErrBadRequest): + w.WriteHeader(http.StatusBadRequest) + return + case errors.Is(err, messages.ErrInternal): + fallthrough + default: + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if _, err := w.Write(response); err != nil { + log.Printf("proxyAnswers unable to write answer response with error: %v", err) + } +} From 87ad06a5e2f1b0d72c64dd0ca17543f524ac1d63 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 3 Jun 2021 17:04:58 -0400 Subject: [PATCH 202/385] Get rid of legacy version Move the logic for the legacy version into the http handlers and use a shim when doing ipc. --- broker/http.go | 51 ++++++++++++++++++++++++++++++++---------- broker/ipc.go | 44 ++++++++++-------------------------- common/messages/ipc.go | 7 ++---- 3 files changed, 53 insertions(+), 49 deletions(-) diff --git a/broker/http.go b/broker/http.go index 6555d7a..2c45b2b 100644 --- a/broker/http.go +++ b/broker/http.go @@ -102,7 +102,6 @@ func proxyPolls(i *IPC, w http.ResponseWriter, r *http.Request) { arg := messages.Arg{ Body: body, RemoteAddr: r.RemoteAddr, - NatType: "", } var response []byte @@ -138,28 +137,57 @@ func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { return } + // Handle the legacy version + isLegacy := false + if len(body) > 0 && body[0] == '{' { + isLegacy = true + req := messages.ClientPollRequest{ + Offer: string(body), + NAT: r.Header.Get("Snowflake-NAT-Type"), + } + body, err = req.EncodePollRequest() + if err != nil { + log.Printf("Error shimming the legacy request: %s", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + } + arg := messages.Arg{ Body: body, RemoteAddr: "", - NatType: r.Header.Get("Snowflake-NAT-Type"), } var response []byte err = i.ClientOffers(arg, &response) - switch { - case err == nil: - case errors.Is(err, messages.ErrUnavailable): - w.WriteHeader(http.StatusServiceUnavailable) - return - case errors.Is(err, messages.ErrTimeout): - w.WriteHeader(http.StatusGatewayTimeout) - return - default: + if err != nil { + // Assert err == messages.ErrInternal log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } + if isLegacy { + resp, err := messages.DecodeClientPollResponse(response) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + switch resp.Error { + case "": + response = []byte(resp.Answer) + case "no snowflake proxies currently available": + w.WriteHeader(http.StatusServiceUnavailable) + return + case "timed out waiting for answer!": + w.WriteHeader(http.StatusGatewayTimeout) + return + default: + panic("unknown error") + } + } + if _, err := w.Write(response); err != nil { log.Printf("clientOffers unable to write answer with error: %v", err) } @@ -181,7 +209,6 @@ func proxyAnswers(i *IPC, w http.ResponseWriter, r *http.Request) { arg := messages.Arg{ Body: body, RemoteAddr: "", - NatType: "", } var response []byte diff --git a/broker/ipc.go b/broker/ipc.go index 79ccf0f..a05f560 100644 --- a/broker/ipc.go +++ b/broker/ipc.go @@ -21,14 +21,10 @@ const ( NATUnrestricted = "unrestricted" ) -// We support two client message formats. The legacy format is for backwards -// combatability and relies heavily on HTTP headers and status codes to convey -// information. type clientVersion int const ( - v0 clientVersion = iota //legacy version - v1 + v1 clientVersion = iota ) type IPC struct { @@ -141,32 +137,22 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error { startTime := time.Now() body := arg.Body - if len(body) > 0 && body[0] == '{' { - version = v0 + parts := bytes.SplitN(body, []byte("\n"), 2) + if len(parts) < 2 { + // no version number found + err := fmt.Errorf("unsupported message version") + return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response) + } + body = parts[1] + if string(parts[0]) == "1.0" { + version = v1 } else { - parts := bytes.SplitN(body, []byte("\n"), 2) - if len(parts) < 2 { - // no version number found - err := fmt.Errorf("unsupported message version") - return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response) - } - body = parts[1] - if string(parts[0]) == "1.0" { - version = v1 - - } else { - err := fmt.Errorf("unsupported message version") - return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response) - } + err := fmt.Errorf("unsupported message version") + return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response) } var offer *ClientOffer switch version { - case v0: - offer = &ClientOffer{ - natType: arg.NatType, - sdp: body, - } case v1: req, err := messages.DecodeClientPollRequest(body) if err != nil { @@ -203,8 +189,6 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error { } i.ctx.metrics.lock.Unlock() switch version { - case v0: - return messages.ErrUnavailable case v1: resp := &messages.ClientPollResponse{Error: "no snowflake proxies currently available"} return sendClientResponse(resp, response) @@ -230,8 +214,6 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error { i.ctx.metrics.promMetrics.ClientPollTotal.With(prometheus.Labels{"nat": offer.natType, "status": "matched"}).Inc() i.ctx.metrics.lock.Unlock() switch version { - case v0: - *response = []byte(answer) case v1: resp := &messages.ClientPollResponse{Answer: answer} err = sendClientResponse(resp, response) @@ -243,8 +225,6 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error { case <-time.After(time.Second * ClientTimeout): log.Println("Client: Timed out.") switch version { - case v0: - err = messages.ErrTimeout case v1: resp := &messages.ClientPollResponse{ Error: "timed out waiting for answer!"} diff --git a/common/messages/ipc.go b/common/messages/ipc.go index 3f89200..ee29a57 100644 --- a/common/messages/ipc.go +++ b/common/messages/ipc.go @@ -7,12 +7,9 @@ import ( type Arg struct { Body []byte RemoteAddr string - NatType string } var ( - ErrBadRequest = errors.New("bad request") - ErrInternal = errors.New("internal error") - ErrUnavailable = errors.New("service unavailable") - ErrTimeout = errors.New("timeout") + ErrBadRequest = errors.New("bad request") + ErrInternal = errors.New("internal error") ) From c3c84fdb48ec27fe8fa5527693d18176df40637f Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 8 Jul 2021 12:47:23 -0400 Subject: [PATCH 203/385] Use variables for string matching The legacy code does case matching on these exact strings so it's better to ensure they're constant. --- broker/http.go | 4 ++-- broker/ipc.go | 5 ++--- common/messages/ipc.go | 3 +++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/broker/http.go b/broker/http.go index 2c45b2b..e30e442 100644 --- a/broker/http.go +++ b/broker/http.go @@ -177,10 +177,10 @@ func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { switch resp.Error { case "": response = []byte(resp.Answer) - case "no snowflake proxies currently available": + case messages.StrNoProxies: w.WriteHeader(http.StatusServiceUnavailable) return - case "timed out waiting for answer!": + case messages.StrTimedOut: w.WriteHeader(http.StatusGatewayTimeout) return default: diff --git a/broker/ipc.go b/broker/ipc.go index a05f560..7ab27af 100644 --- a/broker/ipc.go +++ b/broker/ipc.go @@ -190,7 +190,7 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error { i.ctx.metrics.lock.Unlock() switch version { case v1: - resp := &messages.ClientPollResponse{Error: "no snowflake proxies currently available"} + resp := &messages.ClientPollResponse{Error: messages.StrNoProxies} return sendClientResponse(resp, response) default: panic("unknown version") @@ -226,8 +226,7 @@ func (i *IPC) ClientOffers(arg messages.Arg, response *[]byte) error { log.Println("Client: Timed out.") switch version { case v1: - resp := &messages.ClientPollResponse{ - Error: "timed out waiting for answer!"} + resp := &messages.ClientPollResponse{Error: messages.StrTimedOut} err = sendClientResponse(resp, response) default: panic("unknown version") diff --git a/common/messages/ipc.go b/common/messages/ipc.go index ee29a57..13e096f 100644 --- a/common/messages/ipc.go +++ b/common/messages/ipc.go @@ -12,4 +12,7 @@ type Arg struct { var ( ErrBadRequest = errors.New("bad request") ErrInternal = errors.New("internal error") + + StrTimedOut = "timed out waiting for answer!" + StrNoProxies = "no snowflake proxies currently available" ) From dfb68d7cfc4b69c5e1f5628f3db002c3595fa1f0 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 8 Jul 2021 15:31:56 -0400 Subject: [PATCH 204/385] Fix race is broker test reported by `go test -race` --- broker/snowflake-broker_test.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/broker/snowflake-broker_test.go b/broker/snowflake-broker_test.go index 77e62cf..9e1c9f1 100644 --- a/broker/snowflake-broker_test.go +++ b/broker/snowflake-broker_test.go @@ -173,10 +173,10 @@ func TestBroker(t *testing.T) { So(err, ShouldBeNil) Convey("with a client offer if available.", func() { - go func(ctx *BrokerContext) { + go func(i *IPC) { proxyPolls(i, w, r) done <- true - }(ctx) + }(i) // Pass a fake client offer to this proxy p := <-ctx.proxyPolls So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") @@ -187,10 +187,10 @@ func TestBroker(t *testing.T) { }) Convey("return empty 200 OK when no client offer is available.", func() { - go func(ctx *BrokerContext) { + go func(i *IPC) { proxyPolls(i, w, r) done <- true - }(ctx) + }(i) p := <-ctx.proxyPolls So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp") // nil means timeout @@ -202,6 +202,7 @@ func TestBroker(t *testing.T) { }) Convey("Responds to proxy answers...", func() { + done := make(chan bool) s := ctx.AddSnowflake("test", "", NATUnrestricted, 0) w := httptest.NewRecorder() data := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"test","Answer":"test"}`)) @@ -209,10 +210,12 @@ func TestBroker(t *testing.T) { Convey("by passing to the client if valid.", func() { r, err := http.NewRequest("POST", "snowflake.broker/answer", data) So(err, ShouldBeNil) - go func(ctx *BrokerContext) { + go func(i *IPC) { proxyAnswers(i, w, r) - }(ctx) + done <- true + }(i) answer := <-s.answerChannel + <-done So(w.Code, ShouldEqual, http.StatusOK) So(answer, ShouldResemble, "test") }) @@ -265,10 +268,10 @@ func TestBroker(t *testing.T) { rp, err := http.NewRequest("POST", "snowflake.broker/proxy", datap) So(err, ShouldBeNil) - go func(ctx *BrokerContext) { + go func(i *IPC) { proxyPolls(i, wp, rp) proxy_done <- true - }(ctx) + }(i) // Client offer wc := httptest.NewRecorder() @@ -289,10 +292,10 @@ func TestBroker(t *testing.T) { datap = bytes.NewReader([]byte(`{"Version":"1.0","Sid":"ymbcCMto7KHNGYlp","Answer":"test"}`)) rp, err = http.NewRequest("POST", "snowflake.broker/answer", datap) So(err, ShouldBeNil) - go func(ctx *BrokerContext) { + go func(i *IPC) { proxyAnswers(i, wp, rp) proxy_done <- true - }(ctx) + }(i) <-proxy_done <-client_done @@ -682,10 +685,10 @@ func TestMetrics(t *testing.T) { r, err := http.NewRequest("POST", "snowflake.broker/proxy", data) r.RemoteAddr = "129.97.208.23:8888" //CA geoip So(err, ShouldBeNil) - go func(ctx *BrokerContext) { + go func(i *IPC) { proxyPolls(i, w, r) done <- true - }(ctx) + }(i) p := <-ctx.proxyPolls //manually unblock poll p.offerChannel <- nil <-done From 2c2f93c022c64ad45f5d471b692eb6317b50d209 Mon Sep 17 00:00:00 2001 From: Arlo Breault Date: Thu, 8 Jul 2021 15:35:04 -0400 Subject: [PATCH 205/385] Remove and restore some comments, after review --- broker/http.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/broker/http.go b/broker/http.go index e30e442..9ae2560 100644 --- a/broker/http.go +++ b/broker/http.go @@ -138,6 +138,10 @@ func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { } // Handle the legacy version + // + // We support two client message formats. The legacy format is for backwards + // combatability and relies heavily on HTTP headers and status codes to convey + // information. isLegacy := false if len(body) > 0 && body[0] == '{' { isLegacy = true @@ -161,7 +165,6 @@ func clientOffers(i *IPC, w http.ResponseWriter, r *http.Request) { var response []byte err = i.ClientOffers(arg, &response) if err != nil { - // Assert err == messages.ErrInternal log.Println(err) w.WriteHeader(http.StatusInternalServerError) return From 4f7833b3840163f8ca256ada0f8292ed2bdc0ceb Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Tue, 13 Jul 2021 17:50:44 -0400 Subject: [PATCH 206/385] Version bump to v1.1.0 --- ChangeLog | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ChangeLog b/ChangeLog index b2d0733..6c9b992 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,14 @@ +Changes in version v1.1.0 - 2021-07-13 + +- Refactors of the Snowflake broker code +- Refactors of the Snowflake proxy code +- Issue 40048: assign proxies based on self-reported client load +- Issue 40052: fixed a memory leak in the server accept loop +- Version bump of kcp and smux libraries +- Bug fix to pass the correct client address to the Snowflake bridge metrics +counter +- Bug fixes to prevent race conditions in the Snowflake client + Changes in version v1.0.0 - 2021-06-07 - Initial release. From d9a83e26b5de158da481e87702183546a4ea4e65 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sun, 18 Jul 2021 13:11:29 -0600 Subject: [PATCH 207/385] Remove unused FakePeers. Unused since 1364d7d45bbec9de605a266a84ea60cdfa6676db. --- client/lib/lib_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index e0856a5..daeeaf9 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -52,12 +52,6 @@ func (f FakeSocksConn) Reject() error { } func (f FakeSocksConn) Grant(addr *net.TCPAddr) error { return nil } -type FakePeers struct{ toRelease *WebRTCPeer } - -func (f FakePeers) Collect() (*WebRTCPeer, error) { return &WebRTCPeer{}, nil } -func (f FakePeers) Pop() *WebRTCPeer { return nil } -func (f FakePeers) Melted() <-chan struct{} { return nil } - func TestSnowflakeClient(t *testing.T) { Convey("Peers", t, func() { From 2d7cd3f2b7d06094c0e2810441ba324c9dc37689 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sun, 18 Jul 2021 16:25:09 -0600 Subject: [PATCH 208/385] Use the readLimit constant in a test. Instead of copying the value. --- client/lib/lib_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index daeeaf9..c31a1a6 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -246,7 +246,7 @@ func TestSnowflakeClient(t *testing.T) { Convey("BrokerChannel.Negotiate fails with large read", func() { b, err := NewBrokerChannel("test.broker", "", - &MockTransport{http.StatusOK, make([]byte, 100001, 100001)}, + &MockTransport{http.StatusOK, make([]byte, readLimit+1)}, false) So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) From c1b0fdd8cfaa89d2507c4bb23ef907c40de73d61 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 15 Jul 2021 10:40:48 -0400 Subject: [PATCH 209/385] Cleaned up and reorganized READMEs --- README.md | 82 ++++++++++++++------------------------------- broker/README.md | 9 +++++ client/README.md | 54 +++++++++++++++++++++++------ probetest/README.md | 9 +++++ proxy/README.md | 49 +++++++++++++++++++++++++-- server/README.md | 9 +++++ 6 files changed, 144 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 6d47012..afc4ddf 100644 --- a/README.md +++ b/README.md @@ -8,66 +8,47 @@ Pluggable Transport using WebRTC, inspired by Flashproxy. **Table of Contents** +- [Structure of this Repository](#structure-of-this-repository) - [Usage](#usage) - - [Dependencies](#dependencies) - - [More Info](#more-info) - - [Building](#building) - - [Test Environment](#test-environment) + - [Using Snowflake with Tor](#using-snowflake-with-tor) + - [Running a Snowflake Proxy](#running-a-snowflake-proxy) + - [Using the Snowflake Library with Other Applications](#using-the-snowflake-library-with-other-applications) +- [Test Environment](#test-environment) - [FAQ](#faq) -- [Appendix](#appendix) - - [-- Testing with Standalone Proxy --](#---testing-with-standalone-proxy---) +- [More info and links](#more-info-and-links) +### Structure of this Repository + +- `broker/` contains code for the Snowflake broker +- `doc/` contains Snowflake documentation and manpages +- `client/` contains the Tor pluggable transport client and client library code +- `common/` contains generic libraries used by multiple pieces of Snowflake +- `proxy/` contains code for the Go standalone Snowflake proxy +- `probetest/` contains code for a NAT probetesting service +- `server/` contains the Tor pluggable transport server and server library code + ### Usage -``` -cd client/ -go get -go build -tor -f torrc -``` -This should start the client plugin, bootstrapping to 100% using WebRTC. +Snowflake is currently deployed as a pluggable transport for Tor. -#### Dependencies +#### Using Snowflake with Tor -Client: -- [pion/webrtc](https://github.com/pion/webrtc) -- Go 1.13+ +To use the Snowflake client with Tor, you will need to add the appropriate `Bridge` and `ClientTransportPlugin` lines to your [torrc](https://2019.www.torproject.org/docs/tor-manual.html.en) file. See the [client README](client) for more information on building and running the Snowflake client. ---- +#### Running a Snowflake Proxy -#### More Info +You can contribute to Snowflake by running a Snowflake proxy. We have the option to run a proxy in your browser or as a standalone Go program. See our [community documentation](https://community.torproject.org/relay/setup/snowflake/) for more details. -Tor can plug in the Snowflake client via a correctly configured `torrc`. -For example: +#### Using the Snowflake Library with Other Applications -``` -ClientTransportPlugin snowflake exec ./client \ --url https://snowflake-broker.azureedge.net/ \ --front ajax.aspnetcdn.com \ --ice stun:stun.l.google.com:19302 --max 3 -``` +Snowflake can be used as a Go API, and adheres to the [v2.1 pluggable transports specification](). For more information on using the Snowflake Go library, see the [Snowflake library documentation](doc/using-the-snowflake-library). -The flags `-url` and `-front` allow the Snowflake client to speak to the Broker, -in order to get connected with some volunteer's browser proxy. `-ice` is a -comma-separated list of ICE servers, which are required for NAT traversal. - -For logging, run `tail -F snowflake.log` in a second terminal. - -You can modify the `torrc` to use your own broker: - -``` -ClientTransportPlugin snowflake exec ./client --meek -``` - - -#### Test Environment +### Test Environment There is a Docker-based test environment at https://github.com/cohosh/snowbox. - ### FAQ **Q: How does it work?** @@ -103,17 +84,6 @@ manual port forwarding! It utilizes the "ICE" negotiation via WebRTC, and also involves a great abundance of ephemeral and short-lived (and special!) volunteer proxies... -### Appendix +### More info and links -##### -- Testing with Standalone Proxy -- - -``` -cd proxy -go build -./proxy -``` - -More documentation on the way. - -Also available at: -[torproject.org/pluggable-transports/snowflake](https://gitweb.torproject.org/pluggable-transports/snowflake.git/) +We have more documentation in the [Snowflake wiki](https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/wikis/home) and at https://snowflake.torproject.org/. diff --git a/broker/README.md b/broker/README.md index fb6181e..1e0a763 100644 --- a/broker/README.md +++ b/broker/README.md @@ -1,3 +1,12 @@ + + +**Table of Contents** + +- [Overview](#overview) +- [Running your own](#running-your-own) + + + This is the Broker component of Snowflake. ### Overview diff --git a/client/README.md b/client/README.md index 50bdba3..aed11c3 100644 --- a/client/README.md +++ b/client/README.md @@ -1,20 +1,54 @@ + + +**Table of Contents** + +- [Dependencies](#dependencies) +- [Building the Snowflake client](#building-the-snowflake-client) +- [Running the Snowflake client with Tor](#running-the-snowflake-client-with-tor) + + + This is the Tor client component of Snowflake. -It is based on goptlib. +It is based on the [goptlib](https://gitweb.torproject.org/pluggable-transports/goptlib.git/) pluggable transports library for Tor. -### Flags -The client uses these following `torrc` options by default: +### Dependencies + +- Go 1.13+ +- We use the [pion/webrtc](https://github.com/pion/webrtc) library for WebRTC communication with Snowflake proxies. Note: running `go get` will fetch this dependency automatically during the build process. + +### Building the Snowflake client + +To build the Snowflake client, make sure you are in the `client/` directory, and then run: + ``` +go get +go build +``` + +### Running the Snowflake client with Tor + +We have an example `torrc` file in this repository. The client uses these following `torrc` options by default: +``` +UseBridges 1 + ClientTransportPlugin snowflake exec ./client \ --url https://snowflake-broker.azureedge.net/ \ --front ajax.aspnetcdn.com \ --ice stun:stun.l.google.com:19302 +-url https://snowflake-broker.torproject.net.global.prod.fastly.net/ \ +-front cdn.sstatic.net \ +-ice stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 + +Bridge snowflake 192.0.2.3:1 ``` -`-url` should be the URL of a Broker instance. +`-url` is the URL of a broker instance. If you would like to try out Snowflake with your own broker, simply provide the URL of your broker instance with this option. -`-front` is an optional front domain for the Broker request. +`-front` is an optional front domain for the broker request. -`-ice` is a comma-separated list of ICE servers. These can be STUN or TURN -servers. +`-ice` is a comma-separated list of ICE servers. These can be STUN or TURN servers. We recommend using servers that have implemented NAT discovery. See our wiki page on [NAT traversal](https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/wikis/NAT-matching) for more information. + +To bootstrap Tor, run: +``` +tor -f torrc +``` +This should start the client plugin, bootstrapping to 100% using WebRTC. diff --git a/probetest/README.md b/probetest/README.md index 8af42f5..44c7837 100644 --- a/probetest/README.md +++ b/probetest/README.md @@ -1,3 +1,12 @@ + + +**Table of Contents** + +- [Overview](#overview) +- [Running your own](#running-your-own) + + + This is code for a remote probe test component of Snowflake. ### Overview diff --git a/proxy/README.md b/proxy/README.md index 381e3e5..a7496da 100644 --- a/proxy/README.md +++ b/proxy/README.md @@ -1,3 +1,48 @@ -This is a standalone (not browser-based) version of the Snowflake proxy. + + +**Table of Contents** -Usage: ./proxy +- [Dependencies](#dependencies) +- [Building the standalone Snowflake proxy](#building-the-standalone-snowflake-proxy) +- [Running a standalone Snowflake proxy](#running-a-standalone-snowflake-proxy) + + + +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.13+ +- We use the [pion/webrtc](https://github.com/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: + -broker string + broker URL (default "https://snowflake-broker.bamsoftware.com/") + -capacity uint + maximum concurrent clients + -keep-local-addresses + keep local LAN address ICE candidates + -log string + log filename + -relay string + websocket relay URL (default "wss://snowflake.bamsoftware.com/") + -stun string + stun URL (default "stun:stun.stunprotocol.org:3478") + -unsafe-logging + prevent logs from being scrubbed +``` + +For more information on how to run a Snowflake proxy in deployment, see our [community documentation](https://community.torproject.org/relay/setup/snowflake/standalone/). diff --git a/server/README.md b/server/README.md index 312a506..18b24a7 100644 --- a/server/README.md +++ b/server/README.md @@ -1,3 +1,12 @@ + + +**Table of Contents** + +- [Setup](#setup) +- [TLS](#tls) + + + This is the server transport plugin for Snowflake. The actual transport protocol it uses is [WebSocket](https://tools.ietf.org/html/rfc6455). From b4e964c682bd7deaa456ed0b4a80e1e7af994d11 Mon Sep 17 00:00:00 2001 From: Cecylia Bocovich Date: Thu, 15 Jul 2021 11:43:05 -0400 Subject: [PATCH 210/385] Added some Snowflake library documentation --- README.md | 2 +- doc/using-the-snowflake-library.md | 105 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 doc/using-the-snowflake-library.md diff --git a/README.md b/README.md index afc4ddf..0278c04 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ You can contribute to Snowflake by running a Snowflake proxy. We have the option #### Using the Snowflake Library with Other Applications -Snowflake can be used as a Go API, and adheres to the [v2.1 pluggable transports specification](). For more information on using the Snowflake Go library, see the [Snowflake library documentation](doc/using-the-snowflake-library). +Snowflake can be used as a Go API, and adheres to the [v2.1 pluggable transports specification](). For more information on using the Snowflake Go library, see the [Snowflake library documentation](doc/using-the-snowflake-library.md). ### Test Environment diff --git a/doc/using-the-snowflake-library.md b/doc/using-the-snowflake-library.md new file mode 100644 index 0000000..9308cdc --- /dev/null +++ b/doc/using-the-snowflake-library.md @@ -0,0 +1,105 @@ +Snowflake is available as a general-purpose pluggable transports library and adheres to the [pluggable transports v2.1 Go API](https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/master/releases/PTSpecV2.1/Pluggable%20Transport%20Specification%20v2.1%20-%20Go%20Transport%20API.pdf). + +### Client library + +The Snowflake client library contains functions for running a Snowflake client. + +Example usage: + +```Golang +package main + +import ( + "log" + + sf "git.torproject.org/pluggable-transports/snowflake.git/client/lib" +) + +func main() { + + transport, err := sf.NewSnowflakeClient("https://snowflake-broker.example.com", + "https://friendlyfrontdomain.net", + []string{"stun:stun.voip.blackberry.com:3478", "stun:stun.stunprotocol.org:3478"}, + false, 1) + if err != nil { + log.Fatal("Failed to start snowflake transport: ", err) + } + + // transport implements the ClientFactory interface and returns a net.Conn + conn, err := transport.Dial() + if err != nil { + log.Printf("dial error: %s", err) + return + } + defer conn.Close() + + // ... + +} +``` + +### Server library + +The Snowflake server library contains functions for running a Snowflake server. + +Example usage: +```Golang + +package main + +import ( + "log" + "net" + + sf "git.torproject.org/pluggable-transports/snowflake.git/server/lib" + "golang.org/x/crypto/acme/autocert" +) + +func main() { + + // The snowflake server runs a websocket server. To run this securely, you will + // need a valid certificate. + certManager := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist("snowflake.yourdomain.com"), + Email: "you@yourdomain.com", + } + + transport := sf.NewSnowflakeServer(certManager.GetCertificate) + + addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:443") + if err != nil { + log.Printf("error resolving bind address: %s", err.Error()) + } + ln, err := transport.Listen(addr) + if err != nil { + log.Printf("error opening listener: %s", err.Error()) + } + for { + conn, err := ln.Accept() + if err != nil { + if err, ok := err.(net.Error); ok && err.Temporary() { + continue + } + log.Printf("Snowflake accept error: %s", err) + break + } + go func() { + // ... + + defer conn.Close() + }() + } + + // ... + +} + +``` +### Running your own Snowflake infrastructure + +At the moment we do not have the ability to share Snowfake infrastructure between different types of applications. If you are planning on using Snowflake as a transport for your application, you will need to: + +- Run a Snowflake broker. See our [broker documentation](../broker/) and [installation guide](https://gitlab.torproject.org/tpo/anti-censorship/team/-/wikis/Survival-Guides/Snowflake-Broker-Installation-Guide) for more information + +- Run Snowflake proxies. These can be run as [standalone Go proxies](../proxy/) or [browser-based proxies](https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake-webext). From 099f4127ead13ca93d771ebb505c4610feb6fcee Mon Sep 17 00:00:00 2001 From: meskio Date: Wed, 21 Jul 2021 12:01:07 +0200 Subject: [PATCH 211/385] Refactor the poll offer to use a ticker Simplify the code to use a ticker. Using a pattern to allow a first run of the loop before hitting the ticker: https://github.com/golang/go/issues/17601#issuecomment-311955879 --- proxy/snowflake.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 12d97d3..78f226d 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -123,19 +123,12 @@ func (s *SignalingServer) Post(path string, payload io.Reader) ([]byte, error) { func (s *SignalingServer) pollOffer(sid string) *webrtc.SessionDescription { brokerPath := s.url.ResolveReference(&url.URL{Path: "proxy"}) - timeOfNextPoll := time.Now() - for { - // Sleep until we're scheduled to poll again. - now := time.Now() - time.Sleep(timeOfNextPoll.Sub(now)) - // Compute the next time to poll -- if it's in the past, that - // means that the POST took longer than pollInterval, so we're - // allowed to do another one immediately. - timeOfNextPoll = timeOfNextPoll.Add(pollInterval) - if timeOfNextPoll.Before(now) { - timeOfNextPoll = now - } + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + // Run the loop once before hitting the ticker + for ; true; <-ticker.C { numClients := int((tokens.count() / 8) * 8) // Round down to 8 body, err := messages.EncodePollRequest(sid, "standalone", currentNATType, numClients) if err != nil { @@ -163,6 +156,7 @@ func (s *SignalingServer) pollOffer(sid string) *webrtc.SessionDescription { } } + return nil } func (s *SignalingServer) sendAnswer(sid string, pc *webrtc.PeerConnection) error { From e3d376ca43db6420619afedfbc860a33e52d60bf Mon Sep 17 00:00:00 2001 From: meskio Date: Wed, 21 Jul 2021 12:02:16 +0200 Subject: [PATCH 212/385] Wait pollInterval between proxy offers Closes: #40055 --- proxy/snowflake.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/proxy/snowflake.go b/proxy/snowflake.go index 78f226d..d694471 100644 --- a/proxy/snowflake.go +++ b/proxy/snowflake.go @@ -485,7 +485,10 @@ func main() { checkNATType(config, defaultProbeURL) log.Printf("NAT type: %s", currentNATType) - for { + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + for ; true; <-ticker.C { tokens.get() sessionID := genSessionID() runSession(sessionID) From 191510c416db6b0229e62cc2b869aaf3cee907fa Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sun, 18 Jul 2021 11:44:43 -0600 Subject: [PATCH 213/385] Use a URL with a Host component in BrokerChannel tests. The tests were using a broker URL of "test.broker" (i.e., a schema-less, host-less, relative path), and running assertions on the value of b.url.Path. This is strange, especially in tests regarding domain fronting, where we care about b.url.Host, not b.url.Path. This commit changes the broker URL to "http://test.broker" and changes tests to check b.url.Host. I also added an additional assertion for an empty b.Host in the non-domain-fronted case. --- client/lib/lib_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index c31a1a6..03c53dd 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -198,24 +198,25 @@ func TestSnowflakeClient(t *testing.T) { } Convey("Construct BrokerChannel with no front domain", func() { - b, err := NewBrokerChannel("test.broker", "", transport, false) + b, err := NewBrokerChannel("http://test.broker", "", transport, false) So(b.url, ShouldNotBeNil) So(err, ShouldBeNil) - So(b.url.Path, ShouldResemble, "test.broker") + So(b.Host, ShouldResemble, "") + So(b.url.Host, ShouldResemble, "test.broker") So(b.transport, ShouldNotBeNil) }) Convey("Construct BrokerChannel *with* front domain", func() { - b, err := NewBrokerChannel("test.broker", "front", transport, false) + b, err := NewBrokerChannel("http://test.broker", "front", transport, false) So(b.url, ShouldNotBeNil) So(err, ShouldBeNil) - So(b.url.Path, ShouldResemble, "test.broker") + So(b.Host, ShouldResemble, "test.broker") So(b.url.Host, ShouldResemble, "front") So(b.transport, ShouldNotBeNil) }) Convey("BrokerChannel.Negotiate responds with answer", func() { - b, err := NewBrokerChannel("test.broker", "", transport, false) + b, err := NewBrokerChannel("http://test.broker", "", transport, false) So(err, ShouldBeNil) answer, err := b.Negotiate(fakeOffer) So(err, ShouldBeNil) @@ -224,7 +225,7 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("BrokerChannel.Negotiate fails", func() { - b, err := NewBrokerChannel("test.broker", "", + b, err := NewBrokerChannel("http://test.broker", "", &MockTransport{http.StatusOK, []byte(`{"error": "no snowflake proxies currently available"}`)}, false) So(err, ShouldBeNil) @@ -234,7 +235,7 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("BrokerChannel.Negotiate fails with unexpected error", func() { - b, err := NewBrokerChannel("test.broker", "", + b, err := NewBrokerChannel("http://test.broker", "", &MockTransport{http.StatusInternalServerError, []byte("\n")}, false) So(err, ShouldBeNil) @@ -245,7 +246,7 @@ func TestSnowflakeClient(t *testing.T) { }) Convey("BrokerChannel.Negotiate fails with large read", func() { - b, err := NewBrokerChannel("test.broker", "", + b, err := NewBrokerChannel("http://test.broker", "", &MockTransport{http.StatusOK, make([]byte, readLimit+1)}, false) So(err, ShouldBeNil) From 55f4814dfb5c196bb66416d4f3ba367498602489 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sun, 18 Jul 2021 10:39:51 -0600 Subject: [PATCH 214/385] Change the representation of domain fronting in HTTP rendezvous. Formerly, BrokerChannel represented the broker URL and possible domain fronting as bc.url *url.URL bc.Host string That is, bc.url is the URL of the server which we contact directly, and bc.Host is the Host header to use in the request. With no domain fronting, bc.url points directly at the broker itself, and bc.Host is blank. With domain fronting, we do the following reshuffling: if front != "" { bc.Host = bc.url.Host bc.url.Host = front } That is, we alter bc.url to reflect that the server to which we send requests directly is the CDN, not the broker, and store the broker's own URL in the HTTP Host header. The above representation was always confusing to me, because in my mental model, we are always conceptually communicating with the broker; but we may optionally be using a CDN proxy in the middle. The new representation is bc.url *url.URL bc.front string bc.url is the URL of the broker itself, and never changes. bc.front is the optional CDN front domain, and likewise never changes after initialization. When domain fronting is in use, we do the swap in the http.Request struct, not in BrokerChannel itself: if bc.front != "" { request.Host = request.URL.Host request.URL.Host = bc.front } Compare to the representation in meek-client: https://gitweb.torproject.org/pluggable-transports/meek.git/tree/meek-client/meek-client.go?h=v0.35.0#n94 var options struct { URL string Front string } https://gitweb.torproject.org/pluggable-transports/meek.git/tree/meek-client/meek-client.go?h=v0.35.0#n308 if ok { // if front is set info.Host = info.URL.Host info.URL.Host = front } --- client/lib/lib_test.go | 12 ++++++------ client/lib/rendezvous.go | 23 +++++++++++------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 03c53dd..9087eed 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -172,14 +172,14 @@ func TestSnowflakeClient(t *testing.T) { Convey("Dialers", t, func() { Convey("Can construct WebRTCDialer.", func() { - broker := &BrokerChannel{Host: "test"} + broker := &BrokerChannel{front: "test"} d := NewWebRTCDialer(broker, nil, 1) So(d, ShouldNotBeNil) So(d.BrokerChannel, ShouldNotBeNil) - So(d.BrokerChannel.Host, ShouldEqual, "test") + So(d.BrokerChannel.front, ShouldEqual, "test") }) SkipConvey("WebRTCDialer can Catch a snowflake.", func() { - broker := &BrokerChannel{Host: "test"} + broker := &BrokerChannel{} d := NewWebRTCDialer(broker, nil, 1) conn, err := d.Catch() So(conn, ShouldBeNil) @@ -201,8 +201,8 @@ func TestSnowflakeClient(t *testing.T) { b, err := NewBrokerChannel("http://test.broker", "", transport, false) So(b.url, ShouldNotBeNil) So(err, ShouldBeNil) - So(b.Host, ShouldResemble, "") So(b.url.Host, ShouldResemble, "test.broker") + So(b.front, ShouldResemble, "") So(b.transport, ShouldNotBeNil) }) @@ -210,8 +210,8 @@ func TestSnowflakeClient(t *testing.T) { b, err := NewBrokerChannel("http://test.broker", "front", transport, false) So(b.url, ShouldNotBeNil) So(err, ShouldBeNil) - So(b.Host, ShouldResemble, "test.broker") - So(b.url.Host, ShouldResemble, "front") + So(b.url.Host, ShouldResemble, "test.broker") + So(b.front, ShouldResemble, "front") So(b.transport, ShouldNotBeNil) }) diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index b89f432..caa4ae4 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -32,10 +32,8 @@ const ( // Signalling Channel to the Broker. type BrokerChannel struct { - // The Host header to put in the HTTP request (optional and may be - // different from the host name in URL). - Host string url *url.URL + front string // Optional front domain to replace url.Host in requests. transport http.RoundTripper // Used to make all requests. keepLocalAddresses bool NATType string @@ -61,14 +59,12 @@ func NewBrokerChannel(broker string, front string, transport http.RoundTripper, return nil, err } log.Println("Rendezvous using Broker at:", broker) + if front != "" { + log.Println("Domain fronting using:", front) + } bc := new(BrokerChannel) bc.url = targetURL - if front != "" { // Optional front domain. - log.Println("Domain fronting using:", front) - bc.Host = bc.url.Host - bc.url.Host = front - } - + bc.front = front bc.transport = transport bc.keepLocalAddresses = keepLocalAddresses bc.NATType = nat.NATUnknown @@ -92,7 +88,7 @@ func limitedRead(r io.Reader, limit int64) ([]byte, error) { func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( *webrtc.SessionDescription, error) { log.Println("Negotiating via BrokerChannel...\nTarget URL: ", - bc.Host, "\nFront URL: ", bc.url.Host) + bc.url.Host, "\nFront URL: ", bc.front) // Ideally, we could specify an `RTCIceTransportPolicy` that would handle // this for us. However, "public" was removed from the draft spec. // See https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration#RTCIceTransportPolicy_enum @@ -126,8 +122,11 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( if nil != err { return nil, err } - if "" != bc.Host { // Set true host if necessary. - request.Host = bc.Host + if bc.front != "" { + // Do domain fronting. Replace the domain in the URL's with the + // front, and store the original domain the HTTP Host header. + request.Host = request.URL.Host + request.URL.Host = bc.front } resp, err := bc.transport.RoundTrip(request) if nil != err { From 0f34a7778fa1f4c28c7cc161991080d146689591 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sun, 18 Jul 2021 12:36:16 -0600 Subject: [PATCH 215/385] Factor out httpRendezvous separate from BrokerChannel. Makes BrokerChannel abstract over a rendezvousMethod. BrokerChannel itself is responsible for keepLocalAddresses and the NAT type state, as well as encoding and decoding client poll messages. rendezvousMethod is only responsible for delivery of encoded messages. --- client/lib/lib_test.go | 93 +--------------------- client/lib/rendezvous.go | 99 ++++++++--------------- client/lib/rendezvous_http.go | 77 ++++++++++++++++++ client/lib/rendezvous_test.go | 145 ++++++++++++++++++++++++++++++++++ 4 files changed, 258 insertions(+), 156 deletions(-) create mode 100644 client/lib/rendezvous_http.go create mode 100644 client/lib/rendezvous_test.go diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index 9087eed..86601b1 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -1,33 +1,14 @@ package lib import ( - "bytes" "fmt" - "io/ioutil" "net" - "net/http" "testing" "time" - "git.torproject.org/pluggable-transports/snowflake.git/common/util" . "github.com/smartystreets/goconvey/convey" ) -type MockTransport struct { - statusOverride int - body []byte -} - -// Just returns a response with fake SDP answer. -func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { - s := ioutil.NopCloser(bytes.NewReader(m.body)) - r := &http.Response{ - StatusCode: m.statusOverride, - Body: s, - } - return r, nil -} - type FakeDialer struct { max int } @@ -172,11 +153,10 @@ func TestSnowflakeClient(t *testing.T) { Convey("Dialers", t, func() { Convey("Can construct WebRTCDialer.", func() { - broker := &BrokerChannel{front: "test"} + broker := &BrokerChannel{} d := NewWebRTCDialer(broker, nil, 1) So(d, ShouldNotBeNil) So(d.BrokerChannel, ShouldNotBeNil) - So(d.BrokerChannel.front, ShouldEqual, "test") }) SkipConvey("WebRTCDialer can Catch a snowflake.", func() { broker := &BrokerChannel{} @@ -187,77 +167,6 @@ func TestSnowflakeClient(t *testing.T) { }) }) - Convey("Rendezvous", t, func() { - transport := &MockTransport{ - http.StatusOK, - []byte(`{"answer": "{\"type\":\"answer\",\"sdp\":\"fake\"}" }`), - } - fakeOffer, err := util.DeserializeSessionDescription(`{"type":"offer","sdp":"test"}`) - if err != nil { - panic(err) - } - - Convey("Construct BrokerChannel with no front domain", func() { - b, err := NewBrokerChannel("http://test.broker", "", transport, false) - So(b.url, ShouldNotBeNil) - So(err, ShouldBeNil) - So(b.url.Host, ShouldResemble, "test.broker") - So(b.front, ShouldResemble, "") - So(b.transport, ShouldNotBeNil) - }) - - Convey("Construct BrokerChannel *with* front domain", func() { - b, err := NewBrokerChannel("http://test.broker", "front", transport, false) - So(b.url, ShouldNotBeNil) - So(err, ShouldBeNil) - So(b.url.Host, ShouldResemble, "test.broker") - So(b.front, ShouldResemble, "front") - So(b.transport, ShouldNotBeNil) - }) - - Convey("BrokerChannel.Negotiate responds with answer", func() { - b, err := NewBrokerChannel("http://test.broker", "", transport, false) - So(err, ShouldBeNil) - answer, err := b.Negotiate(fakeOffer) - So(err, ShouldBeNil) - So(answer, ShouldNotBeNil) - So(answer.SDP, ShouldResemble, "fake") - }) - - Convey("BrokerChannel.Negotiate fails", func() { - b, err := NewBrokerChannel("http://test.broker", "", - &MockTransport{http.StatusOK, []byte(`{"error": "no snowflake proxies currently available"}`)}, - false) - So(err, ShouldBeNil) - answer, err := b.Negotiate(fakeOffer) - So(err, ShouldNotBeNil) - So(answer, ShouldBeNil) - }) - - Convey("BrokerChannel.Negotiate fails with unexpected error", func() { - b, err := NewBrokerChannel("http://test.broker", "", - &MockTransport{http.StatusInternalServerError, []byte("\n")}, - false) - So(err, ShouldBeNil) - answer, err := b.Negotiate(fakeOffer) - So(err, ShouldNotBeNil) - So(answer, ShouldBeNil) - So(err.Error(), ShouldResemble, BrokerErrorUnexpected) - }) - - Convey("BrokerChannel.Negotiate fails with large read", func() { - b, err := NewBrokerChannel("http://test.broker", "", - &MockTransport{http.StatusOK, make([]byte, readLimit+1)}, - false) - So(err, ShouldBeNil) - answer, err := b.Negotiate(fakeOffer) - So(err, ShouldNotBeNil) - So(answer, ShouldBeNil) - So(err.Error(), ShouldResemble, "unexpected EOF") - }) - - }) - } func TestWebRTCPeer(t *testing.T) { diff --git a/client/lib/rendezvous.go b/client/lib/rendezvous.go index caa4ae4..8568120 100644 --- a/client/lib/rendezvous.go +++ b/client/lib/rendezvous.go @@ -9,13 +9,9 @@ package lib import ( - "bytes" "errors" - "io" - "io/ioutil" "log" "net/http" - "net/url" "sync" "time" @@ -30,11 +26,21 @@ const ( readLimit = 100000 //Maximum number of bytes to be read from an HTTP response ) -// Signalling Channel to the Broker. +// rendezvousMethod represents a way of communicating with the broker: sending +// an encoded client poll request (SDP offer) and receiving an encoded client +// poll response (SDP answer) in return. rendezvousMethod is used by +// BrokerChannel, which is in charge of encoding and decoding, and all other +// tasks that are independent of the rendezvous method. +type rendezvousMethod interface { + Exchange([]byte) ([]byte, error) +} + +// BrokerChannel contains a rendezvousMethod, as well as data that is not +// specific to any rendezvousMethod. BrokerChannel has the responsibility of +// encoding and decoding SDP offers and answers; rendezvousMethod is responsible +// for the exchange of encoded information. type BrokerChannel struct { - url *url.URL - front string // Optional front domain to replace url.Host in requests. - transport http.RoundTripper // Used to make all requests. + rendezvous rendezvousMethod keepLocalAddresses bool NATType string lock sync.Mutex @@ -54,31 +60,21 @@ func CreateBrokerTransport() http.RoundTripper { // |broker| is the full URL of the facilitating program which assigns proxies // to clients, and |front| is the option fronting domain. func NewBrokerChannel(broker string, front string, transport http.RoundTripper, keepLocalAddresses bool) (*BrokerChannel, error) { - targetURL, err := url.Parse(broker) - if err != nil { - return nil, err - } log.Println("Rendezvous using Broker at:", broker) if front != "" { log.Println("Domain fronting using:", front) } - bc := new(BrokerChannel) - bc.url = targetURL - bc.front = front - bc.transport = transport - bc.keepLocalAddresses = keepLocalAddresses - bc.NATType = nat.NATUnknown - return bc, nil -} -func limitedRead(r io.Reader, limit int64) ([]byte, error) { - p, err := ioutil.ReadAll(&io.LimitedReader{R: r, N: limit + 1}) + rendezvous, err := newHTTPRendezvous(broker, front, transport) if err != nil { - return p, err - } else if int64(len(p)) == limit+1 { - return p[0:limit], io.ErrUnexpectedEOF + return nil, err } - return p, err + + return &BrokerChannel{ + rendezvous: rendezvous, + keepLocalAddresses: keepLocalAddresses, + NATType: nat.NATUnknown, + }, nil } // Roundtrip HTTP POST using WebRTC SessionDescriptions. @@ -87,8 +83,6 @@ func limitedRead(r io.Reader, limit int64) ([]byte, error) { // with an SDP answer from a designated remote WebRTC peer. func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( *webrtc.SessionDescription, error) { - log.Println("Negotiating via BrokerChannel...\nTarget URL: ", - bc.url.Host, "\nFront URL: ", bc.front) // Ideally, we could specify an `RTCIceTransportPolicy` that would handle // this for us. However, "public" was removed from the draft spec. // See https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration#RTCIceTransportPolicy_enum @@ -103,57 +97,34 @@ func (bc *BrokerChannel) Negotiate(offer *webrtc.SessionDescription) ( return nil, err } - // Encode client poll request + // Encode the client poll request. bc.lock.Lock() req := &messages.ClientPollRequest{ Offer: offerSDP, NAT: bc.NATType, } - body, err := req.EncodePollRequest() + encReq, err := req.EncodePollRequest() bc.lock.Unlock() if err != nil { return nil, err } - data := bytes.NewReader([]byte(body)) - // Suffix with broker's client registration handler. - clientURL := bc.url.ResolveReference(&url.URL{Path: "client"}) - request, err := http.NewRequest("POST", clientURL.String(), data) - if nil != err { + // Do the exchange using our rendezvousMethod. + encResp, err := bc.rendezvous.Exchange(encReq) + if err != nil { return nil, err } - if bc.front != "" { - // Do domain fronting. Replace the domain in the URL's with the - // front, and store the original domain the HTTP Host header. - request.Host = request.URL.Host - request.URL.Host = bc.front - } - resp, err := bc.transport.RoundTrip(request) - if nil != err { + log.Printf("Received answer: %s", string(encResp)) + + // Decode the client poll response. + resp, err := messages.DecodeClientPollResponse(encResp) + if err != nil { return nil, err } - defer resp.Body.Close() - log.Printf("BrokerChannel Response:\n%s\n\n", resp.Status) - - switch resp.StatusCode { - case http.StatusOK: - body, err := limitedRead(resp.Body, readLimit) - if nil != err { - return nil, err - } - log.Printf("Received answer: %s", string(body)) - - resp, err := messages.DecodeClientPollResponse(body) - if err != nil { - return nil, err - } - if resp.Error != "" { - return nil, errors.New(resp.Error) - } - return util.DeserializeSessionDescription(resp.Answer) - default: - return nil, errors.New(BrokerErrorUnexpected) + if resp.Error != "" { + return nil, errors.New(resp.Error) } + return util.DeserializeSessionDescription(resp.Answer) } func (bc *BrokerChannel) SetNATType(NATType string) { diff --git a/client/lib/rendezvous_http.go b/client/lib/rendezvous_http.go new file mode 100644 index 0000000..01219cb --- /dev/null +++ b/client/lib/rendezvous_http.go @@ -0,0 +1,77 @@ +package lib + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" +) + +// httpRendezvous is a rendezvousMethod that communicates with the .../client +// route of the broker over HTTP or HTTPS, with optional domain fronting. +type httpRendezvous struct { + brokerURL *url.URL + front string // Optional front domain to replace url.Host in requests. + transport http.RoundTripper // Used to make all requests. +} + +// newHTTPRendezvous creates a new httpRendezvous that contacts the broker at +// the given URL, with an optional front domain. transport is the +// http.RoundTripper used to make all requests. +func newHTTPRendezvous(broker, front string, transport http.RoundTripper) (*httpRendezvous, error) { + brokerURL, err := url.Parse(broker) + if err != nil { + return nil, err + } + return &httpRendezvous{ + brokerURL: brokerURL, + front: front, + transport: transport, + }, nil +} + +func (r *httpRendezvous) Exchange(encPollReq []byte) ([]byte, error) { + log.Println("Negotiating via HTTP rendezvous...") + log.Println("Target URL: ", r.brokerURL.Host) + log.Println("Front URL: ", r.front) + + // Suffix the path with the broker's client registration handler. + reqURL := r.brokerURL.ResolveReference(&url.URL{Path: "client"}) + req, err := http.NewRequest("POST", reqURL.String(), bytes.NewReader(encPollReq)) + if err != nil { + return nil, err + } + + if r.front != "" { + // Do domain fronting. Replace the domain in the URL's with the + // front, and store the original domain the HTTP Host header. + req.Host = req.URL.Host + req.URL.Host = r.front + } + + resp, err := r.transport.RoundTrip(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + log.Printf("HTTP rendezvous response: %s", resp.Status) + if resp.StatusCode != http.StatusOK { + return nil, errors.New(BrokerErrorUnexpected) + } + + return limitedRead(resp.Body, readLimit) +} + +func limitedRead(r io.Reader, limit int64) ([]byte, error) { + p, err := ioutil.ReadAll(&io.LimitedReader{R: r, N: limit + 1}) + if err != nil { + return p, err + } else if int64(len(p)) == limit+1 { + return p[0:limit], io.ErrUnexpectedEOF + } + return p, err +} diff --git a/client/lib/rendezvous_test.go b/client/lib/rendezvous_test.go new file mode 100644 index 0000000..c263e37 --- /dev/null +++ b/client/lib/rendezvous_test.go @@ -0,0 +1,145 @@ +package lib + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "testing" + + "git.torproject.org/pluggable-transports/snowflake.git/common/messages" + "git.torproject.org/pluggable-transports/snowflake.git/common/nat" + . "github.com/smartystreets/goconvey/convey" +) + +// mockTransport's RoundTrip method returns a response with a fake status and +// body. +type mockTransport struct { + statusCode int + body []byte +} + +func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + Status: fmt.Sprintf("%d %s", t.statusCode, http.StatusText(t.statusCode)), + StatusCode: t.statusCode, + Body: ioutil.NopCloser(bytes.NewReader(t.body)), + }, nil +} + +// errorTransport's RoundTrip method returns an error. +type errorTransport struct { + err error +} + +func (t errorTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return nil, t.err +} + +// makeEncPollReq returns an encoded client poll request containing a given +// offer. +func makeEncPollReq(offer string) []byte { + encPollReq, err := (&messages.ClientPollRequest{ + Offer: offer, + NAT: nat.NATUnknown, + }).EncodePollRequest() + if err != nil { + panic(err) + } + return encPollReq +} + +// makeEncPollResp returns an encoded client poll response with given answer and +// error strings. +func makeEncPollResp(answer, errorStr string) []byte { + encPollResp, err := (&messages.ClientPollResponse{ + Answer: answer, + Error: errorStr, + }).EncodePollResponse() + if err != nil { + panic(err) + } + return encPollResp +} + +func TestHTTPRendezvous(t *testing.T) { + Convey("HTTP rendezvous", t, func() { + Convey("Construct httpRendezvous with no front domain", func() { + transport := &mockTransport{http.StatusOK, []byte{}} + rend, err := newHTTPRendezvous("http://test.broker", "", transport) + So(err, ShouldBeNil) + So(rend.brokerURL, ShouldNotBeNil) + So(rend.brokerURL.Host, ShouldResemble, "test.broker") + So(rend.front, ShouldResemble, "") + So(rend.transport, ShouldEqual, transport) + }) + + Convey("Construct httpRendezvous *with* front domain", func() { + transport := &mockTransport{http.StatusOK, []byte{}} + rend, err := newHTTPRendezvous("http://test.broker", "front", transport) + So(err, ShouldBeNil) + So(rend.brokerURL, ShouldNotBeNil) + So(rend.brokerURL.Host, ShouldResemble, "test.broker") + So(rend.front, ShouldResemble, "front") + So(rend.transport, ShouldEqual, transport) + }) + + fakeEncPollReq := makeEncPollReq(`{"type":"offer","sdp":"test"}`) + + Convey("httpRendezvous.Exchange responds with answer", func() { + fakeEncPollResp := makeEncPollResp( + `{"answer": "{\"type\":\"answer\",\"sdp\":\"fake\"}" }`, + "", + ) + rend, err := newHTTPRendezvous("http://test.broker", "", + &mockTransport{http.StatusOK, fakeEncPollResp}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldBeNil) + So(answer, ShouldResemble, fakeEncPollResp) + }) + + Convey("httpRendezvous.Exchange responds with no answer", func() { + fakeEncPollResp := makeEncPollResp( + "", + `{"error": "no snowflake proxies currently available"}`, + ) + rend, err := newHTTPRendezvous("http://test.broker", "", + &mockTransport{http.StatusOK, fakeEncPollResp}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldBeNil) + So(answer, ShouldResemble, fakeEncPollResp) + }) + + Convey("httpRendezvous.Exchange fails with unexpected HTTP status code", func() { + rend, err := newHTTPRendezvous("http://test.broker", "", + &mockTransport{http.StatusInternalServerError, []byte{}}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldNotBeNil) + So(answer, ShouldBeNil) + So(err.Error(), ShouldResemble, BrokerErrorUnexpected) + }) + + Convey("httpRendezvous.Exchange fails with error", func() { + transportErr := errors.New("error") + rend, err := newHTTPRendezvous("http://test.broker", "", + &errorTransport{err: transportErr}) + So(err, ShouldBeNil) + answer, err := rend.Exchange(fakeEncPollReq) + So(err, ShouldEqual, transportErr) + So(answer, ShouldBeNil) + }) + + Convey("httpRendezvous.Exchange fails with large read", func() { + rend, err := newHTTPRendezvous("http://test.broker", "", + &mockTransport{http.StatusOK, make([]byte, readLimit+1)}) + So(err, ShouldBeNil) + _, err = rend.Exchange(fakeEncPollReq) + So(err, ShouldEqual, io.ErrUnexpectedEOF) + }) + }) +} From c9e0dd287f30b2acb0145a7efc326c881792138a Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sun, 18 Jul 2021 15:22:03 -0600 Subject: [PATCH 216/385] amp package. This package contains a CacheURL function that modifies a URL to be accessed through an AMP cache, and the "AMP armor" data encoding scheme for encoding data into the AMP subset of HTML. --- common/amp/armor_decoder.go | 136 +++++++++++++++ common/amp/armor_encoder.go | 176 ++++++++++++++++++++ common/amp/armor_test.go | 227 +++++++++++++++++++++++++ common/amp/cache.go | 178 ++++++++++++++++++++ common/amp/cache_test.go | 320 ++++++++++++++++++++++++++++++++++++ common/amp/doc.go | 88 ++++++++++ common/amp/path.go | 44 +++++ common/amp/path_test.go | 54 ++++++ 8 files changed, 1223 insertions(+) create mode 100644 common/amp/armor_decoder.go create mode 100644 common/amp/armor_encoder.go create mode 100644 common/amp/armor_test.go create mode 100644 common/amp/cache.go create mode 100644 common/amp/cache_test.go create mode 100644 common/amp/doc.go create mode 100644 common/amp/path.go create mode 100644 common/amp/path_test.go diff --git a/common/amp/armor_decoder.go b/common/amp/armor_decoder.go new file mode 100644 index 0000000..fed44a6 --- /dev/null +++ b/common/amp/armor_decoder.go @@ -0,0 +1,136 @@ +package amp + +import ( + "bufio" + "bytes" + "encoding/base64" + "fmt" + "io" + + "golang.org/x/net/html" +) + +// ErrUnknownVersion is the error returned when the first character inside the +// element encoding (but outside the base64 encoding) is not '0'. +type ErrUnknownVersion byte + +func (err ErrUnknownVersion) Error() string { + return fmt.Sprintf("unknown armor version indicator %+q", byte(err)) +} + +func isASCIIWhitespace(b byte) bool { + switch b { + // https://infra.spec.whatwg.org/#ascii-whitespace + case '\x09', '\x0a', '\x0c', '\x0d', '\x20': + return true + default: + return false + } +} + +func splitASCIIWhitespace(data []byte, atEOF bool) (advance int, token []byte, err error) { + var i, j int + // Skip initial whitespace. + for i = 0; i < len(data); i++ { + if !isASCIIWhitespace(data[i]) { + break + } + } + // Look for next whitespace. + for j = i; j < len(data); j++ { + if isASCIIWhitespace(data[j]) { + return j + 1, data[i:j], nil + } + } + // We reached the end of data without finding more whitespace. Only + // consider it a token if we are at EOF. + if atEOF && i < j { + return j, data[i:j], nil + } + // Otherwise, request more data. + return i, nil, nil +} + +func decodeToWriter(w io.Writer, r io.Reader) (int64, error) { + tokenizer := html.NewTokenizer(r) + // Set a memory limit on token sizes, otherwise the tokenizer will + // buffer text indefinitely if it is not broken up by other token types. + tokenizer.SetMaxBuf(elementSizeLimit) + active := false + total := int64(0) + for { + tt := tokenizer.Next() + switch tt { + case html.ErrorToken: + err := tokenizer.Err() + if err == io.EOF { + err = nil + } + if err == nil && active { + return total, fmt.Errorf("missing tag") + } + return total, err + case html.TextToken: + if active { + // Re-join the separate chunks of text and + // feed them to the decoder. + scanner := bufio.NewScanner(bytes.NewReader(tokenizer.Text())) + scanner.Split(splitASCIIWhitespace) + for scanner.Scan() { + n, err := w.Write(scanner.Bytes()) + total += int64(n) + if err != nil { + return total, err + } + } + if err := scanner.Err(); err != nil { + return total, err + } + } + case html.StartTagToken: + tn, _ := tokenizer.TagName() + if string(tn) == "pre" { + if active { + // nesting not allowed + return total, fmt.Errorf("unexpected %s", tokenizer.Token()) + } + active = true + } + case html.EndTagToken: + tn, _ := tokenizer.TagName() + if string(tn) == "pre" { + if !active { + // stray end tag + return total, fmt.Errorf("unexpected %s", tokenizer.Token()) + } + active = false + } + } + } +} + +// NewArmorDecoder returns a new AMP armor decoder. +func NewArmorDecoder(r io.Reader) (io.Reader, error) { + pr, pw := io.Pipe() + go func() { + _, err := decodeToWriter(pw, r) + pw.CloseWithError(err) + }() + + // The first byte inside the element encoding is a server–client + // protocol version indicator. + var version [1]byte + _, err := pr.Read(version[:]) + if err != nil { + pr.CloseWithError(err) + return nil, err + } + switch version[0] { + case '0': + return base64.NewDecoder(base64.StdEncoding, pr), nil + default: + err := ErrUnknownVersion(version[0]) + pr.CloseWithError(err) + return nil, err + } +} diff --git a/common/amp/armor_encoder.go b/common/amp/armor_encoder.go new file mode 100644 index 0000000..5d6b0ae --- /dev/null +++ b/common/amp/armor_encoder.go @@ -0,0 +1,176 @@ +package amp + +import ( + "encoding/base64" + "io" +) + +// https://amp.dev/boilerplate/ +// https://amp.dev/documentation/guides-and-tutorials/learn/spec/amp-boilerplate/?format=websites +// https://amp.dev/documentation/guides-and-tutorials/learn/spec/amphtml/?format=websites#the-amp-html-format +const ( + boilerplateStart = ` + + + + + + + + + +` + boilerplateEnd = ` +` +) + +const ( + // We restrict the amount of text may go inside an HTML element, in + // order to limit the amount a decoder may have to buffer. + elementSizeLimit = 32 * 1024 + + // The payload is conceptually a long base64-encoded string, but we + // break the string into short chunks separated by whitespace. This is + // to protect against modification by AMP caches, which reportedly may + // truncate long words in text: + // https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/25985#note_2592348 + bytesPerChunk = 32 + + // We set the number of chunks per element so as to stay under + // elementSizeLimit. Here, we assume that there is 1 byte of whitespace + // after each chunk (with an additional whitespace byte at the beginning + // of the element). + chunksPerElement = (elementSizeLimit - 1) / (bytesPerChunk + 1) +) + +// The AMP armor encoder is a chain of a base64 encoder (base64.NewEncoder) and +// an HTML element encoder (elementEncoder). A top-level encoder (armorEncoder) +// coordinates these two, and handles prepending and appending the AMP +// boilerplate. armorEncoder's Write method writes data into the base64 encoder, +// where it makes its way through the chain. + +// NewArmorEncoder returns a new AMP armor encoder. Anything written to the +// returned io.WriteCloser will be encoded and written to w. The caller must +// call Close to flush any partially written data and output the AMP boilerplate +// trailer. +func NewArmorEncoder(w io.Writer) (io.WriteCloser, error) { + // Immediately write the AMP boilerplate header. + _, err := w.Write([]byte(boilerplateStart)) + if err != nil { + return nil, err + } + + element := &elementEncoder{w: w} + // Write a server–client protocol version indicator, outside the base64 + // layer. + _, err = element.Write([]byte{'0'}) + if err != nil { + return nil, err + } + + base64 := base64.NewEncoder(base64.StdEncoding, element) + return &armorEncoder{ + w: w, + element: element, + base64: base64, + }, nil +} + +type armorEncoder struct { + base64 io.WriteCloser + element *elementEncoder + w io.Writer +} + +func (enc *armorEncoder) Write(p []byte) (int, error) { + // Write into the chain base64 | element | w. + return enc.base64.Write(p) +} + +func (enc *armorEncoder) Close() error { + // Close the base64 encoder first, to flush out any buffered data and + // the final padding. + err := enc.base64.Close() + if err != nil { + return err + } + + // Next, close the element encoder, to close any open elements. + err = enc.element.Close() + if err != nil { + return err + } + + // Finally, output the AMP boilerplate trailer. + _, err = enc.w.Write([]byte(boilerplateEnd)) + if err != nil { + return err + } + + return nil +} + +// elementEncoder arranges written data into pre elements, with the text within +// separated into chunks. It does no HTML encoding, so data written must not +// contain any bytes that are meaningful in HTML. +type elementEncoder struct { + w io.Writer + chunkCounter int + elementCounter int +} + +func (enc *elementEncoder) Write(p []byte) (n int, err error) { + total := 0 + for len(p) > 0 { + if enc.elementCounter == 0 && enc.chunkCounter == 0 { + _, err := enc.w.Write([]byte("