Compare commits

...

387 commits

Author SHA1 Message Date
Tommaso Gragnato
9ce1de4eee 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.
2022-09-26 08:52:23 -07:00
Daniel Golle
a8829d49b7
Fix proxy command line help output 2022-09-26 10:37:29 -04:00
Shelikhoo
36f03dfd44
Record proxy type for proxy relay stats 2022-09-23 13:08:13 +01:00
itchyonion
03b2b56f87 Fix broker race condition 2022-07-19 18:25:27 -07:00
Shelikhoo
c983c13a84
Updated ChangeLog for v2.3.0 release 2022-06-23 11:40:29 +01:00
Shelikhoo
35e9ab8c0b
Use truncated hash instead crc64 for counted hash 2022-06-16 15:00:12 +01:00
Shelikhoo
b18e6fcfe4
Add document for Distinct IP file 2022-06-16 15:00:12 +01:00
Shelikhoo
af1134362a
Update distinct counter interface 2022-06-16 15:00:12 +01:00
Shelikhoo
be40b623a4
Add go sum for hyperloglog 2022-06-16 15:00:12 +01:00
Shelikhoo
2541b13166
Add distinct IP counter to broker 2022-06-16 15:00:10 +01:00
Shelikhoo
fa7d1e2bb7
Add distinct IP counter to metrics 2022-06-16 14:58:12 +01:00
Shelikhoo
211254fa98
Add distinct IP counter 2022-06-16 14:58:12 +01:00
Shelikhoo
97dea533da
Update Relay Pattern format to include dollar sign 2022-06-16 14:06:58 +01:00
Shelikhoo
ddf72025d1
Restrict Allowed Relay to Tor Pool by default 2022-06-16 14:06:58 +01:00
Shelikhoo
e5b799d618
Update documents for broker messages 2022-06-16 14:06:58 +01:00
Shelikhoo
0ae4d821f0
Move ErrExtraInfo to ipc.go 2022-06-16 14:06:58 +01:00
Shelikhoo
a4bbb728e6
Fix not zero metrics for 1.3 values 2022-06-16 14:06:58 +01:00
Shelikhoo
8ba89179f1
Add document for LoadBridgeInfo input 2022-06-16 14:06:58 +01:00
Shelikhoo
8ab45651d0
Disallow unknown bridge list file field 2022-06-16 14:06:58 +01:00
Shelikhoo
c5e5b45b06
Update message protocol version to 1.3 for RelayURL 2022-06-16 14:06:58 +01:00
Shelikhoo
f789dce6d2
Represent Bridge Fingerprint As String 2022-06-16 14:06:58 +01:00
Shelikhoo
dd61e2be0f
Add Proxy Relay URL Metrics Collection 2022-06-16 14:06:57 +01:00
Shelikhoo
b78eb74e42
Add Proxy Relay URL Rejection Metrics 2022-06-16 14:06:57 +01:00
Shelikhoo
7caab01785
Fixed desynchronized comment and behavior for log interval
In 64ce7dff1b, the log interval is modified while the comment is left unchanged.
2022-06-16 14:06:57 +01:00
Shelikhoo
b391d98679
Add Proxy Relay URL Support Counting Metrics Output 2022-06-16 14:06:57 +01:00
Shelikhoo
1b48ee14f4
Add test for proxy poll with Relay URL 2022-06-16 14:06:57 +01:00
Shelikhoo
6e8fbe54ee
Rejection reason feedback 2022-06-16 14:06:57 +01:00
Shelikhoo
3ebb5a4186
Show relay URL when connecting to relay 2022-06-16 14:06:57 +01:00
Shelikhoo
b18a9431b2
Add Broker Allowed Relay Pattern Indication Rejection for Proxy 2022-06-16 14:06:57 +01:00
Shelikhoo
2ebdc89c42
Add Allowed Relay Hostname Pattern Indication 2022-06-16 14:06:57 +01:00
Shelikhoo
b09a2e09b3
Add Relay URL Check in Snowflake Proxy 2022-06-16 14:06:56 +01:00
Shelikhoo
02c6f764c9
Add support for specifying bridge list file 2022-06-16 14:06:56 +01:00
Shelikhoo
c961b07459
Add Detailed Error Output for datachannelHandler 2022-06-16 14:06:56 +01:00
Shelikhoo
50c0d64e10
Add Detailed Error Output for proxyPolls, proxyAnswers 2022-06-16 14:06:56 +01:00
Shelikhoo
c7549d886e
Update default snowflake server address
Change snowflake broker test for updated address

Amend DefaultBridges Value

Add Default Fingerprint Info for Snowflake
2022-06-16 14:06:56 +01:00
Shelikhoo
5d7a3766d6
Add Relay Info Forwarding for Snowflake 2022-06-16 13:57:34 +01:00
Shelikhoo
d5a87c3c02
Guard Proxy Relay URL Acceptance with Pattern Check 2022-06-16 13:57:33 +01:00
Shelikhoo
863a8296e8
Add RelayURL support in proxy 2022-06-16 13:57:33 +01:00
Shelikhoo
613ceaf970
Add RelayURL and AllowedRelayPattern to snowflake signaling 2022-06-16 13:57:33 +01:00
Shelikhoo
38f0e00e5d
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
2022-06-16 13:57:33 +01:00
Shelikhoo
5578b4dd76
Add Bridge List Holder Test 2022-06-16 13:57:00 +01:00
Shelikhoo
0822c5f87b
Add Bridge List Holder 2022-06-16 13:56:59 +01:00
Shelikhoo
3d4f294241
Add Bridge List Definition 2022-06-16 13:56:56 +01:00
meskio
f38c91f906
Don't use entropy for test
Use math/rand instead of crypto/rand, so entropy is not a blocker when
running the tests.
2022-06-02 17:24:54 +02:00
itchyonion
e4c01f0595 Wrap client NAT log 2022-05-31 08:52:23 -07:00
Cecylia Bocovich
6310ca4381
Avoid performing two NAT probe tests at startup
After the initial NAT probe test, a full interval before starting the
recurring NAT retests.
2022-05-27 10:01:19 -04:00
Cecylia Bocovich
4e7f897527
Update snowflake CI to test with go 1.18 2022-05-26 12:51:54 -04:00
Cecylia Bocovich
ae5a71e6e5
Updated ChangeLog for v2.2.0 release 2022-05-25 12:17:54 -04:00
meskio
3473b438e5
Move ptEventLogger into the client/snowflake.go
Remove client/pt_event_logger.go file as is very minimal.
2022-05-25 18:09:09 +02:00
meskio
1d592b06e5
Implement String() method on events
To make it safe for logging safelog.Scrub function is now public.

Closes: #40141
2022-05-25 18:09:06 +02:00
itchyonion
9757784c5a
Wait some time before displaying the proxy usage log 2022-05-25 11:01:01 -04:00
Cecylia Bocovich
dd83b68efa
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
2022-05-24 11:45:47 -04:00
Cecylia Bocovich
b6875c6ae9
Bump webrtc library version
go get github.com/pion/webrtc/v3@latest
go mod tidy
2022-04-12 12:10:01 -04:00
itchyonion
e2838201ad
Scrub ptEvent logs 2022-04-12 11:52:21 -04:00
Cecylia Bocovich
aab806429f
Fix gitlab CI to work with multiple client .go files 2022-04-11 11:50:36 -04:00
Cecylia Bocovich
d807e9d370
Move tor-specific code outside of client library 2022-04-11 11:38:52 -04:00
Arlo Breault
2f89fbc2ed Represent fingerprint internally as byte array 2022-03-31 11:28:00 -04:00
Arlo Breault
fa2f6824d9 Add some test cases for client poll requests 2022-03-21 15:31:02 -04:00
Arlo Breault
b563141c6a Forward bridge fingerprint
gitlab 28651
2022-03-21 15:06:05 -04:00
Arlo Breault
281d917beb 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.
2022-03-21 15:06:05 -04:00
meskio
b73add1550
Make the proxy type configurable for users of the library
Closes: #40104
2022-03-21 19:24:51 +01:00
meskio
b265bd3092
Make easier to extend the list of known proxy types
And include iptproxy as a valid proxy type.
2022-03-21 19:23:49 +01:00
Arlo Breault
bd636a1374 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.
2022-03-16 16:33:24 -04:00
Arlo Breault
829cacac5f Parse ClientPollRequest version in DecodeClientPollRequest
Instead of IPC.ClientOffers.  This makes things consistent with
EncodeClientPollRequest which adds the version while serializing.
2022-03-16 15:43:10 -04:00
Arlo Breault
6fd0f1ae5d Rename *PollRequest methods to distinguish client/proxy 2022-03-16 15:43:10 -04:00
Shelikhoo
6e29dc676c
Add document for NewUTLSHTTPRoundTripper 2022-03-16 09:13:30 +00:00
Shelikhoo
ab9604476e
Move uTLS configuration to socks5 arg 2022-03-16 09:13:30 +00:00
Shelikhoo
3132f68012
Add connection expire time for uTLS pendingConn 2022-03-16 09:13:29 +00:00
Shelikhoo
8d5998b744
Harmonize identifiers to uTLS 2022-03-16 09:13:29 +00:00
Shelikhoo
e3aeb5fe5b
Add line wrap to NewBrokerChannelWithUTlsSettings 2022-03-16 09:13:29 +00:00
Shelikhoo
f525490032
Update utls test to match uTLS Round Tripper constructor 2022-03-16 09:13:29 +00:00
Shelikhoo
1573502e93
Use uTLS aware broker channel constructor 2022-03-16 09:13:29 +00:00
Shelikhoo
ccfdcab8fe
Add uTLS remove SNI to snowflake client 2022-03-16 09:13:29 +00:00
Shelikhoo
9af0ad119b
Add utls imitate setting to snowflake client 2022-03-16 09:13:29 +00:00
Max Bittman
c1c3596cf8
Add name to utls client hello id 2022-03-16 09:13:28 +00:00
Shelikhoo
c1b0f763ef
Add reformat for utls roundtripper 2022-03-16 09:13:28 +00:00
Shelikhoo
4447860661
Add repeated test for utls roundtripper 2022-03-16 09:13:28 +00:00
Shelikhoo
006abdead4
Add utls roundtripper 2022-03-16 09:13:25 +00:00
meskio
19e9e38415
Merge remote-tracking branch 'gitlab/mr/78' 2022-03-11 19:58:17 +01:00
Jake Vossen
99eb794a20
Fixed up/downstream metrics 2022-03-02 11:27:33 -05:00
pjsier
df22114fce Fix proxy logging verb tense 2022-02-28 18:38:17 -06:00
Anna “CyberTailor”
e18a4ac147
Generate tarballs in release CI
The `generate_tarball` job vendors all Go modules to make packaging for
distributions easier.
2022-02-27 10:01:50 +05:00
Cecylia Bocovich
01ae5b56e8
Fix client library test
Initialize eventsLogger for WebRTCPeer in client library test.
2022-02-14 15:11:41 -05:00
Cecylia Bocovich
3547b284a9
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).
2022-02-14 14:09:16 -05:00
Cecylia Bocovich
2c008d6589
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.
2022-02-14 14:00:01 -05:00
Cecylia Bocovich
bcc162898a
Initialize SnowflakeListener.closed
Fixes a bug where an uninitialized channel causes a panic when closed
(#40099).
2022-02-08 13:00:43 -05:00
Cecylia Bocovich
e6e5e20ae8
Update ChangeLog for v2.1.0 release 2022-02-08 10:56:19 -05:00
Cecylia Bocovich
c0b35076c9
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.
2022-02-07 11:39:23 -05:00
Shelikhoo
00e8415d8e
Add verbosity switch to suppress diagnostic output 2022-02-03 13:38:48 +00:00
Shelikhoo
e828b06076
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
2022-01-28 14:46:45 +00:00
Shelikhoo
bf3bd635f7
Fix build break in Go 1.16 for missing import
See also:
https://gitlab.torproject.org/shelikhoo/snowflake/-/jobs/86751
2022-01-26 13:39:12 +00:00
Shelikhoo
eb229d512b
Fix ProxyEventLogger output 2022-01-25 13:03:19 +00:00
Shelikhoo
88af9da4a2
Fix ProxyEventLogger output 2022-01-25 13:03:19 +00:00
Shelikhoo
1116bc81c8
Add Proxy Event Logger 2022-01-25 13:03:19 +00:00
Shelikhoo
9208364475
Extract traffic formatter 2022-01-25 13:03:19 +00:00
Shelikhoo
f12cfe6a9f
Add proxy event logger state propagate 2022-01-25 13:03:18 +00:00
Shelikhoo
e4305a4d2b
Add EventOnProxyConnectionOver Reporting 2022-01-25 13:03:18 +00:00
Shelikhoo
d64af31394
Add EventOnProxyConnectionOver Event 2022-01-25 13:03:18 +00:00
Shelikhoo
91379a42f3
Add Raw Data Output for bytesLogger 2022-01-25 13:03:14 +00:00
Shelikhoo
6cb82618a0
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
2022-01-25 12:49:59 +00:00
Shelikhoo
657aaa6ba8
Refactor event logger setting into function call
See also:
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/67#note_2770482
2022-01-25 12:49:59 +00:00
Shelikhoo
55bf117d1a
Reduce PT Event Logger Verbosity 2022-01-25 12:49:59 +00:00
Shelikhoo
7536dd6fb7
Add Propagate EventLogger Setting 2022-01-25 12:49:59 +00:00
Shelikhoo
8d2f662c8c
Emit non-pointer type event 2022-01-25 12:49:58 +00:00
Shelikhoo
128936c825
Enable PT Event Logger 2022-01-25 12:49:58 +00:00
Shelikhoo
ac64d17705
Add PT Event Logger 2022-01-25 12:49:58 +00:00
Shelikhoo
36ca610d6b
Add NewWebRTCPeer3E Initializer
This name includes [E]vent to reduce merge conflict with forward proxy change set.
2022-01-25 12:49:58 +00:00
Shelikhoo
9a7fcdec03
Add Snowflake Event Reporter for Peer Communication 2022-01-25 12:49:57 +00:00
Shelikhoo
c3f09994da
Add Snowflake Event Reporter for Broker Communication 2022-01-25 12:49:57 +00:00
Shelikhoo
cd6d837d85
Add snowflake event handler to client config 2022-01-25 12:49:57 +00:00
Shelikhoo
b5ef18803f
Add Event Bus Test 2022-01-25 12:49:57 +00:00
Shelikhoo
5f03f88d73
Add Event Bus Implementation
This event bus implementation favours simplicity over efficiency and is not suitable for frequent addition and removal of listeners.
2022-01-25 12:49:56 +00:00
Shelikhoo
75f770150d
Add Snowflake Event API interface 2022-01-25 12:49:51 +00:00
Shelikhoo
d2f6ea5417
increase clientIDAddrMapCapacity
See also:
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/issues/40084
2022-01-18 14:33:34 -05:00
Shelikhoo
50646698e3
Suppress connection end log output
This is an amendment of https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/30
2022-01-18 14:33:27 -05:00
Cecylia Bocovich
b35a79ac24
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.
2022-01-12 11:30:41 -05:00
David Fifield
aeb0794d28 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.
2021-12-16 09:46:55 -07:00
Cecylia Bocovich
9c11e479d0
Update go versions in CI tests
Debian packages Go 1.15 and 1.17, and we use 1.16 in Tor Browser.
2021-12-10 10:43:47 -05:00
Cecylia Bocovich
738bd464ea
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.
2021-12-10 10:39:44 -05:00
Hans-Christoph Steiner
221f1c41c9
gitlab-ci: include job number in the artfacts zipball filename 2021-12-01 11:48:08 +01:00
Hans-Christoph Steiner
51f2c026fd
gitlab-ci: include flags to make reproducible builds
* https://github.com/golang/go/issues/33772
2021-12-01 11:48:06 +01:00
Hans-Christoph Steiner
1318b6a9ec
stripped down Android build process for gitlab-ci and Vagrant 2021-12-01 11:48:03 +01:00
Hans-Christoph Steiner
c9399da566
gitlab-ci: expire artifacts in 1 week, improve gradle caching, etc. 2021-12-01 11:09:57 +01:00
Shelikhoo
40f44d6272
Add V2Ray/V2Fly License for task 2021-11-19 15:55:30 +00:00
Shelikhoo
0c62d806a4
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
2021-11-16 19:25:27 +00:00
Shelikhoo
c49f72eb0c
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
2021-11-16 15:58:57 +00:00
Shelikhoo
efdb850d2e
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
2021-11-16 11:22:44 +00:00
Shelikhoo
9bdb87eaf3
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
2021-11-16 11:20:27 +00:00
Shelikhoo
d4fdb35ee8
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
2021-11-12 10:56:57 +00:00
Shelikhoo
1b79962ca8
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
2021-11-12 10:49:32 +00:00
Shelikhoo
59af9927a5
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
2021-11-12 10:49:32 +00:00
Shelikhoo
2547883cf9
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
2021-11-12 10:49:32 +00:00
Shelikhoo
a6a53ff8ce
Add NAT Type test periodic task 2021-11-12 10:49:30 +00:00
Shelikhoo
ac97ce7136
Add NAT Type measurement command line flag
It is important to include unit in flag name to prevent user from making mistake.
2021-11-12 10:48:15 +00:00
Shelikhoo
4c8a166178
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.
2021-11-12 10:48:14 +00:00
Shelikhoo
04bc471a63
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.
2021-11-12 10:48:14 +00:00
Cecylia Bocovich
ead5a960d7
Bump snowflake library imports and go.mod to v2 2021-11-11 10:14:49 -05:00
Cecylia Bocovich
f6b6342a3a
Update ChangeLog for v2 release 2021-11-04 10:34:34 -04:00
Cecylia Bocovich
0a2598a1e8 Export ability to change the URL of NAT probe 2021-10-28 10:05:01 -04:00
Cecylia Bocovich
3caa83d84d Modify handling of misconfigurations and defaults 2021-10-28 10:05:01 -04:00
Cecylia Bocovich
0e8d41ba4b Update comments for exported items 2021-10-28 10:05:01 -04:00
Cecylia Bocovich
84e8a183e5 Comment package and minor changes exports 2021-10-26 15:49:46 -04:00
Cecylia Bocovich
b2edf948e2 Remove BytesLoggers from exported functions 2021-10-26 14:52:17 -04:00
idk
50e4f4fd61 Turn the proxy code into a library
Allow other go programs to easily import the snowflake proxy library and
start/stop a snowflake proxy.
2021-10-26 14:15:44 -04:00
Cecylia Bocovich
54ab79384f Unify broker/bridge domains to torproject.net 2021-10-14 11:14:22 -04:00
Cecylia Bocovich
04ba50a531 Change package name and add a package comment 2021-10-07 11:01:33 -04:00
Cecylia Bocovich
4623c7d3e1 Add documentation where necessary for exported items 2021-10-07 11:01:33 -04:00
Cecylia Bocovich
5339ed2dd7 Stop exporting internal code 2021-10-07 11:01:33 -04:00
Cecylia Bocovich
5927c2bdf9 Default to a maximum value of 1 Snowflake peer 2021-10-04 10:17:37 -04:00
Cecylia Bocovich
6c6a2e44ab Change package name and add a package comment 2021-10-04 10:17:37 -04:00
Cecylia Bocovich
767c07dc58 Update client library usage documentation 2021-10-04 10:17:37 -04:00
Cecylia Bocovich
638ec6c222 Update Snowflake client library documentation
Follow best practices for documenting the exported pieces of the
Snowflake client library.
2021-10-04 10:17:37 -04:00
Cecylia Bocovich
99887cd05d 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.
2021-10-04 10:17:37 -04:00
Cecylia Bocovich
624750d5a8 Stop exporting code that should be internal 2021-10-04 10:17:37 -04:00
meskio
4396d505a3
Use tpo geoip library
Now the geoip implmentation has being moved to it's own library to be
shared between projects.
2021-10-04 12:24:55 +02:00
Cecylia Bocovich
8c6f0dbae7 Check error for calls to preparePeerConnection 2021-09-30 11:46:39 -04:00
Cecylia Bocovich
c8136f4534 Update version of go used in .gitlab-ci.yml 2021-09-10 16:57:53 -04:00
meskio
cbd863d6b1
Fix proxy test
The broker is a global object.
2021-09-02 12:49:00 +02:00
Cecylia Bocovich
ace8df37ed Fix compile bug in client, caught by CI 2021-08-24 10:27:24 -04:00
Cecylia Bocovich
a39d6693e1 Call conn.Reject() if SOCKS arguments are invalid 2021-08-19 21:31:51 -04:00
Cecylia Bocovich
97175a91a5 Modify torrc example to pass client args in bridge line 2021-08-19 21:20:34 -04:00
Cecylia Bocovich
e762f58a31 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.
2021-08-19 21:20:34 -04:00
Cecylia Bocovich
4acc08cc60 Use a config struct for snowflake client options 2021-08-19 21:20:34 -04:00
Cecylia Bocovich
e6715cb4ee 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.
2021-08-10 15:38:11 -04:00
David Fifield
b203a75c41 Document -ampcache in snowflake-client man page. 2021-08-05 16:13:24 -06:00
David Fifield
f2dc41d778 Document /amp/client in broker-spec.txt. 2021-08-05 16:13:24 -06:00
David Fifield
521eb4d4d6 Add info about rendezvous methods to client README. 2021-08-05 16:13:24 -06:00
David Fifield
e833119bef Broker /amp/client route (AMP cache client registration). 2021-08-05 16:13:24 -06:00
David Fifield
5adb994028 Implement ampCacheRendezvous. 2021-08-05 16:13:24 -06:00
David Fifield
c13810192d Skeleton of ampCacheRendezvous.
Currently the same as httpRendezvous, but activated using the -ampcache
command-line option.
2021-08-05 16:13:24 -06:00
David Fifield
c9e0dd287f 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.
2021-08-05 16:13:24 -06:00
David Fifield
0f34a7778f 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.
2021-08-05 16:13:24 -06:00
David Fifield
55f4814dfb 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
	}
2021-08-05 16:13:24 -06:00
David Fifield
191510c416 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.
2021-08-05 16:13:24 -06:00
meskio
e3d376ca43
Wait pollInterval between proxy offers
Closes: #40055
2021-07-21 16:38:29 +02:00
meskio
099f4127ea
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
2021-07-21 16:38:27 +02:00
Cecylia Bocovich
b4e964c682 Added some Snowflake library documentation 2021-07-19 10:16:26 -04:00
Cecylia Bocovich
c1b0fdd8cf Cleaned up and reorganized READMEs 2021-07-19 10:16:26 -04:00
David Fifield
2d7cd3f2b7 Use the readLimit constant in a test.
Instead of copying the value.
2021-07-18 16:25:09 -06:00
David Fifield
d9a83e26b5 Remove unused FakePeers.
Unused since 1364d7d45b.
2021-07-18 13:11:29 -06:00
Cecylia Bocovich
4f7833b384 Version bump to v1.1.0 2021-07-13 17:50:44 -04:00
Arlo Breault
2c2f93c022 Remove and restore some comments, after review 2021-07-08 15:35:04 -04:00
Arlo Breault
dfb68d7cfc Fix race is broker test reported by go test -race 2021-07-08 15:32:25 -04:00
Arlo Breault
c3c84fdb48 Use variables for string matching
The legacy code does case matching on these exact strings so it's better
to ensure they're constant.
2021-07-08 12:47:23 -04:00
Arlo Breault
87ad06a5e2 Get rid of legacy version
Move the logic for the legacy version into the http handlers and use a
shim when doing ipc.
2021-07-08 12:32:37 -04:00
Arlo Breault
0ced1cc324 Move http handlers to a separate file 2021-07-08 12:32:37 -04:00
Arlo Breault
015958fbe6 Intermediary refactor teasing apart http / ipc
Introduces an IPC struct and moves the logic out of the http handlers
and into methods on that.
2021-07-08 12:32:35 -04:00
meskio
ced539f234
Refactor webRTCConn to its own file 2021-07-07 19:36:24 +02:00
meskio
7a1857c42f
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
2021-07-07 19:36:20 +02:00
Cecylia Bocovich
74bdb85b30 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.
2021-06-24 13:46:11 -04:00
Cecylia Bocovich
53a2365696 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.
2021-06-24 13:32:55 -04:00
Cecylia Bocovich
10b6075eaa Refactor checkForStaleness to take time.Duration 2021-06-24 11:20:44 -04:00
Cecylia Bocovich
e3351cb08a 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.
2021-06-24 11:16:24 -04:00
Cecylia Bocovich
95cbe36565 Add unit tests to check for webrtc peer data races 2021-06-24 11:16:24 -04:00
Cecylia Bocovich
bb7ff6180b Fix datarace for Peers.melted
Using the boolean value was unnecessary since we already have a channel
we can check for closure.
2021-06-24 11:16:24 -04:00
Cecylia Bocovich
ddcdfc4f09 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.
2021-06-24 11:16:24 -04:00
Simone Basso
ed2d5df87d 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.
2021-06-24 11:16:24 -04:00
Cecylia Bocovich
e84bc81e31 Bump version of kcp and smux libraries 2021-06-23 19:41:03 -04:00
Cecylia Bocovich
6634f2bec9 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.
2021-06-19 11:16:38 -04:00
Simone Basso
aefabe683f 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.
2021-06-14 10:10:02 +02:00
Cecylia Bocovich
8e0b5bd20a Add changelog and release v1.0.0 2021-06-07 10:24:19 -04:00
meskio
c5ca41f138
Add man pages for proxy and client commands
To be used by the debian package (#19409)
2021-06-02 16:47:50 +02:00
Cecylia Bocovich
270eb21803 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.
2021-06-02 09:52:42 -04:00
David Fifield
ae7cc478fd 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.
2021-05-24 15:28:13 -06:00
David Fifield
01a96c7d95 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
2021-05-24 14:40:50 -06:00
David Fifield
ef4d0a1da5
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
2021-05-21 10:09:00 +02:00
Arlo Breault
7ef49272fa 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.
2021-05-20 15:39:30 -04:00
Arlo Breault
160ae2dd71 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.
2021-05-18 20:07:43 -04:00
Cecylia Bocovich
0054cb2dec Update .gitlab-ci.yml after refactor of client 2021-05-12 10:50:06 -04:00
Cecylia Bocovich
7c9005bed3 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.
2021-05-12 09:32:07 -04:00
Cecylia Bocovich
11f0846264 Implement server as a v2.1 PT Go API 2021-05-12 09:08:41 -04:00
Cecylia Bocovich
e87b9175dd 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
2021-05-12 09:08:41 -04:00
Cecylia Bocovich
af6e2c30e1 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.
2021-04-26 14:18:50 -04:00
Cecylia Bocovich
2a310682b5 Add new gauge to show currently available proxies 2021-04-26 14:18:50 -04:00
Cecylia Bocovich
92bd900bc5 Implement binned counts for polling metrics 2021-04-26 14:07:55 -04:00
Cecylia Bocovich
83ef0b6f6d 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.
2021-04-22 10:39:35 -04:00
Cecylia Bocovich
eff73c3016 Switch front domain and host to fastly 2021-04-01 11:56:52 -04:00
Cecylia Bocovich
196c230ac7 Update Go version for .gitlab-ci.yml 2021-03-25 16:07:48 -04:00
Cecylia Bocovich
087a037f82 Update webrtc library to v3.0.15
This fixes a vulnerability in the library: CVE-2021-28681
2021-03-18 23:08:05 -04:00
Cecylia Bocovich
c0b6e082f2 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.
2021-03-18 22:05:40 -04:00
Cecylia Bocovich
720d2b8eb7 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.
2021-03-18 22:05:40 -04:00
David Fifield
850d2f0683 Update required Go version to 1.13 in README. 2021-03-05 23:26:35 -07:00
Cecylia Bocovich
7187f1009e 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.
2021-02-02 11:21:16 -05:00
Cecylia Bocovich
bae0bacbfd Classify proxies with unknown NATs as restricted 2021-01-25 14:05:24 -05:00
Cecylia Bocovich
1b29ad7de1 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.
2021-01-25 10:28:17 -05:00
Cecylia Bocovich
83c01565ef 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.
2021-01-12 10:37:26 -05:00
Cecylia Bocovich
f908576c60 Increase the KCP maximum window size 2020-12-17 09:54:18 -05:00
Cecylia Bocovich
8ec8a7cb63 Pass lock to socksAcceptLoop by reference
This fixes a bug where we were passing the lock by value to
socksAcceptLoop.
2020-12-16 10:52:19 -05:00
Cecylia Bocovich
3e8947bfc9 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.
2020-12-05 15:51:42 -05:00
Cecylia Bocovich
effc667544 Wait until all goroutines finish before shutdown 2020-12-05 15:50:16 -05:00
Cecylia Bocovich
b9cc54b3b7 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.
2020-12-05 15:50:16 -05:00
Cecylia Bocovich
114df695ce 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.
2020-12-04 11:17:13 -05:00
Philipp Winter
5efcde5187
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
2020-11-27 11:20:40 -08:00
Cecylia Bocovich
665d76c5b0 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.
2020-11-23 12:10:59 -05:00
Cecylia Bocovich
ece43cbfcf Note that isRestrictedFiltering is no longer used 2020-11-20 01:15:16 -05:00
Cecylia Bocovich
00f8f85f41 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.
2020-11-20 01:13:18 -05:00
Cecylia Bocovich
cf2eb5e6c0 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.
2020-11-18 15:57:51 -05:00
Cecylia Bocovich
0bed9c48b7 Redefine only symmetric NATs as restricted 2020-11-18 15:40:32 -05:00
Cecylia Bocovich
61beb9d996 Revert accidentally merged code
Some temporary testing code for the proxy got accidentally merged into
the latest changes. This commit undoes that mistake.
2020-11-05 19:28:20 -05:00
Cecylia Bocovich
4663599382 Make probetest wait for a datachannel to open 2020-11-05 16:48:00 -05:00
Cecylia Bocovich
b5ce259858 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.
2020-11-05 16:46:48 -05:00
Cecylia Bocovich
a4f10d9d6e 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.
2020-10-29 11:03:51 -04:00
Cecylia Bocovich
f368c87109 Add a remote service to test NAT compatability
Add a remote probetest service that will allow proxies to test their
compatability with symmetric NATs.
2020-10-29 11:03:51 -04:00
Cecylia Bocovich
7a0428e3b1 Refactor proxy to reuse signaling code
Simplify proxy interactions with the broker signaling server and prepare
for the introduction of an additional signaling server.
2020-10-29 11:03:51 -04:00
David Fifield
912bcae24e 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.
2020-10-22 23:01:45 -06:00
Cecylia Bocovich
6baa3c4d5f 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.
2020-10-15 14:47:51 -04:00
Cecylia Bocovich
d7aa9b8356 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.
2020-10-05 17:02:57 -04:00
Peter Gerber
8467c01e9e Consider more IPs to be local 2020-09-21 15:55:14 +00:00
Cecylia Bocovich
2d43dd26b1 Merge branch 'issue/21314' 2020-08-27 16:45:05 -04:00
Cecylia Bocovich
cc55481faf Set max number of snowflakes in the Tongue 2020-08-27 16:44:07 -04:00
Cecylia Bocovich
1364d7d45b 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.
2020-08-27 16:43:55 -04:00
Cecylia Bocovich
3c3317503e 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.
2020-08-24 09:39:17 -04:00
Cecylia Bocovich
d5ae7562ac 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.
2020-07-30 17:54:28 -04:00
Cecylia Bocovich
82031289a3 Refactor subsetting of ice servers into main
This moves the subsetting of ice servers out of the parseIceServers
function and into main.
2020-07-24 14:08:09 -04:00
Cecylia Bocovich
92520f681d Choose a random subset from given STUN servers
Only chooses a subset as long as we have over 2 STUN servers to choose
from.
2020-07-23 11:30:36 -04:00
Cecylia Bocovich
eaac9f5b6b 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.
2020-07-14 09:16:23 -04:00
Cecylia Bocovich
c1fa4efe4b Refactor android script to be in android job 2020-07-14 09:16:23 -04:00
Hans-Christoph Steiner
d44fc23815 update .gitlab-ci.yml 2020-07-14 09:16:23 -04:00
Cecylia Bocovich
8c875f0ba7 Use STUN server compatable with RFC 5780 in proxy 2020-07-09 09:55:41 -04:00
Cecylia Bocovich
818226acf2 Testing Gitlab sync. 2020-07-06 15:42:41 -04:00
Cecylia Bocovich
046dab865f 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.
2020-07-06 13:16:03 -04:00
Cecylia Bocovich
0052c0e10c 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.
2020-07-06 13:16:03 -04:00
Cecylia Bocovich
f6cf9a453b Implement NAT discover for go standalone proxies 2020-07-06 13:16:03 -04:00
Cecylia Bocovich
bf924445e3 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).
2020-07-06 13:16:03 -04:00
Cecylia Bocovich
1448c3885f Update documentation to include broker spec
Add broker messaging specification with endpoints for clients and
proxies.
2020-06-19 10:05:35 -04:00
Cecylia Bocovich
bbf11a97e4 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
2020-05-07 09:42:09 -04:00
David Fifield
7043a055f9 Reduce DataChannelTimeout from 30s to 10s.
https://bugs.torproject.org/34042
2020-05-04 19:43:48 -06:00
David Fifield
c8293a5de3 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...
2020-05-01 10:30:04 -06:00
David Fifield
72cfb96ede Restore check for nil writePipe in WebRTCPeer.Close.
I removed this check in 047d3214bf 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
2020-04-28 11:47:34 -06:00
Cecylia Bocovich
5e8f9ac538 Update proxy tests to check serialization errors 2020-04-28 13:01:32 -04:00
Cecylia Bocovich
1d2df3cd71 Update calls to session description utils in proxy 2020-04-28 12:55:58 -04:00
David Fifield
047d3214bf 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
2020-04-27 18:48:00 -06:00
David Fifield
e8c41650ae Move establishDataChannel to after exchangeSDP. 2020-04-27 18:48:00 -06:00
David Fifield
85277274fd Make exchangeSDP into a standalone function. 2020-04-27 18:48:00 -06:00
David Fifield
8295c87fbe Make preparePeerConnection a standalone function. 2020-04-27 18:48:00 -06:00
David Fifield
81d14ad33a 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.
2020-04-27 18:48:00 -06:00
David Fifield
5787d5b8b0 Simplify WebRTCPeer.exchangeSDP.
No need to run sendOfferToBroker in a goroutine.
2020-04-27 18:48:00 -06:00
David Fifield
8caa737700 Remove SnowflakeDataChannel interface.
Use *webrtc.DataChannel directly.
2020-04-27 18:48:00 -06:00
David Fifield
32207d6f06 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.
2020-04-27 18:47:59 -06:00
David Fifield
b48fb781ee Have util.{Serialize,Deserialize}SessionDescription return an error
https://bugs.torproject.org/33897#comment:4
2020-04-27 18:46:56 -06:00
David Fifield
76732155e7 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.
2020-04-27 17:51:21 -06:00
David Fifield
d9b076c32e 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.
2020-04-27 17:49:38 -06:00
David Fifield
51bb49fa6f Move pc.CreateOffer/pc.SetLocalDescription out of a goroutine.
This allows us to remove the internal errorChannel.
2020-04-27 17:47:14 -06:00
David Fifield
3520f4e8b9 Simplify Peers.Pop. 2020-04-24 15:45:15 -06:00
David Fifield
17c0d0ff82 Remove unused Resetter interface.
WaitForReset is not used since 70126177fb.
2020-04-24 13:31:04 -06:00
David Fifield
6c2e3adc41 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
2020-04-24 10:38:27 -06:00
David Fifield
73173cb698 Simplify BytesSyncLogger. 2020-04-23 21:38:44 -06:00
David Fifield
2853fc9362 Make BytesSyncLogger's implementation details internal.
Provide NewBytesSyncLogger that returns an opaque data structure.
Automatically start up the logging loop goroutine in NewBytesSyncLogger.
2020-04-23 21:38:44 -06:00
David Fifield
9a4e3e7bd9 Remove unused BytesSyncLogger.IsLogging. 2020-04-23 21:38:44 -06:00
David Fifield
d376d7036b Make WebRTCPeer and Peers not inherit the methods of BytesLogger.
You would have been able to do, for example,
snowflake.(*WebRTCPeer).AddInbound(...).
2020-04-23 21:38:44 -06:00
David Fifield
65ecb798ca Update a comment (no signal pipe anymore). 2020-04-23 20:36:55 -06:00
David Fifield
2f52217d2f Restore go 1.13 to go.mod, lost in the turbotunnel merge. 2020-04-23 17:08:49 -06:00
David Fifield
2022496d3b 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.
2020-04-23 16:03:03 -06:00
David Fifield
0790954020 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.
2020-04-23 16:03:02 -06:00
David Fifield
70126177fb 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.
2020-04-23 16:02:56 -06:00
David Fifield
222ab3d85a 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
c64a61c6da/obfs4proxy/turbotunnel_client.go
https://github.com/net4people/bbs/issues/14#issuecomment-544747519
2020-04-23 14:00:03 -06:00
David Fifield
904af9cb8a 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.
2020-04-23 14:00:03 -06:00
David Fifield
ee2fb42d33 Immediately and unconditionally grant new SOCKS connections. 2020-04-23 14:00:03 -06:00
Cecylia Bocovich
e9b218a65c Clean up .gitignore 2020-04-22 11:11:23 -04:00
Cecylia Bocovich
20180dcb04 Rename proxy-go/ directory to proxy/
Now that the web proxies are in a different repository, no need to
distinguish the two.
2020-04-22 11:11:16 -04:00
Cecylia Bocovich
3ff04c3c65 Update .travis.yml for proxy/ code removal 2020-04-22 11:07:57 -04:00
Cecylia Bocovich
da01bf2323 Remove web proxy instructions from README.md 2020-04-22 11:07:53 -04:00
Cecylia Bocovich
51b0b7ed2e Remove proxy/ subdirectory
We're moving all web proxy code to a different repsitory.
2020-04-16 10:01:18 -04:00
Cecylia Bocovich
6f89fc14f6 Remove proxy/translation submodule
We're moving all web proxy code to another repository.
2020-04-16 10:01:18 -04:00
David Fifield
8eef3b6348 Remove uniuri dependency.
https://bugs.torproject.org/33800
2020-04-03 17:52:44 -06:00
David Fifield
237fed1151 Update GitHub issue numbers to Trac ticket numbers. 2020-04-02 12:36:09 -06:00
Cecylia Bocovich
ea01bf41c3 Change dummy address for snowflake
This will prevent a bug where tor skips bandwidth events for local
addresses (see https://bugs.torproject.org/33693)
2020-04-01 12:55:37 -04:00
Arlo Breault
1867f89562 Remove local LAN address ICE candidates in proxy-go answer
Trac: 19026
2020-03-26 14:04:29 -04:00
Arlo Breault
670e4ba438 Move StripLocalAddresses to a common util
Trac: 19026
2020-03-26 13:13:15 -04:00
Arlo Breault
5fa7578655 Rename logToStateDir/keepLocalAddresses to kebab case
https://en.wikipedia.org/wiki/Letter_case#Special_case_styles
2020-03-25 11:53:24 -04:00
Arlo Breault
f58c865d82 Add unsafe logging 2020-03-25 11:53:24 -04:00
Cecylia Bocovich
e521a7217a Update license 2020-03-19 15:40:11 -04:00
Arlo Breault
d10af300c1 Refactor (De)SerializeSessionDescription as common utils 2020-03-17 20:16:58 -04:00
Cecylia Bocovich
c11461d339 Update go.mod and go.sum 2020-03-17 14:22:20 -04:00
Cecylia Bocovich
6054c09949 Remove the abandoned server-webrtc test code
This existed solely for testing purposes and is no longer being
maintained.
2020-03-17 14:16:57 -04:00
Cecylia Bocovich
58b52eb9f7 Remove go get commands from travis.yml
We no longer need standalone get commands now that we are using go
modules.
2020-03-05 09:21:17 -05:00
Cecylia Bocovich
920f6791f3 Add a go.mod and go.sum for snowflake 2020-03-05 09:21:17 -05:00
Cecylia Bocovich
03315dde02 bump version to 0.2.2 2020-03-04 16:20:34 -05:00
David Fifield
125e71fa6e Remove the now-unused appengine directory.
https://bugs.torproject.org/33429
2020-02-29 17:29:28 -07:00
Cecylia Bocovich
2e9e807178 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.
2020-02-25 18:08:34 -05:00
David Fifield
c2a12c25d1 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.
2020-02-24 00:15:54 -07:00
David Fifield
c124e8c643 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
2020-02-22 16:13:17 -07:00
David Fifield
380b133155 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
2020-02-18 14:10:47 -07:00
Arlo Breault
1220853a67 Restructure a bit based on review 2020-02-08 10:13:40 -05:00
Arlo Breault
846473b354 Unmarshal the SDP to filter attributes
Instead of string manipulation.
2020-02-08 10:13:40 -05:00
Arlo Breault
0fae4ee8ea 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
2020-02-08 10:13:40 -05:00
Arlo Breault
28cf70bb44 Remove unreachable code
go vet was complaining,

common/websocketconn/websocketconn.go:56:2: unreachable code
2020-02-08 10:12:43 -05:00
David Fifield
ca9ae12c38 Simplify a conditional. 2020-02-04 22:35:12 -07:00
David Fifield
256959ca65 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
2020-02-04 15:53:15 -07:00
David Fifield
01e28aa460 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
2020-02-04 15:53:15 -07:00
David Fifield
5708a1d57b websocketconn tests.
https://bugs.torproject.org/33144
2020-02-04 15:53:15 -07:00
Cecylia Bocovich
310890aa14 bump version to 0.2.1 2020-02-03 09:49:34 -05:00
David Fifield
564d1c8363 Remove unused maxMessageSize constant. 2020-01-31 00:15:11 -07:00
David Fifield
a2292ce35b Make timeout constants into time.Duration values.
This slightly changes some log messages.
2020-01-31 00:08:50 -07:00
David Fifield
dfb83c6606 Allow handling multiple SOCKS connections simultaneously.
Close the SOCKS connection in the same function that opens it.
2020-01-30 10:18:23 -07:00
David Fifield
20ac2029fd Have websocketconn.New return a pointer.
This makes the return type satisfy the io.ReadWriteCloser interface
directly.
2020-01-30 10:18:23 -07:00
David Fifield
e47dd5e2b4 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
2020-01-30 10:18:23 -07:00
David Fifield
5b01df9030 Initialize the global upgrader.CheckOrigin statically.
Only once, not again on every call to initServer.
2020-01-30 10:18:23 -07:00
David Fifield
a4287095c0 Also show message in the "error copying WebSocket to ORPort" case.
This was the only case out of the three not to show it.
2020-01-30 10:17:15 -07:00
Cecylia Bocovich
50673d4943 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.
2020-01-29 11:40:29 -05:00
Cecylia Bocovich
7682986a45 Update client tests for NewBrokerChannel errors
We changed NewBrokerChannel to return an error value on failure. This
updates the tests to check that value.
2020-01-29 11:27:44 -05:00
David Fifield
57d4b0b5bd Use lowercase variable names in copyLoop. 2020-01-28 03:04:33 -07:00
David Fifield
bc5498cb4b 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.
2020-01-28 03:04:14 -07:00
David Fifield
db1ba4791b Simplify NewWebRTCDialer. 2020-01-27 20:53:27 -07:00
David Fifield
2fb52c8639 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
2020-01-27 20:50:26 -07:00
David Fifield
f1ab65b1c0 Close the melt channel, don't just send once on it.
Closing the channel makes it always immediately selectable.
2020-01-23 11:24:00 -07:00
David Fifield
febb4936f6 Refactor SOCKS-related logging. 2020-01-23 11:24:00 -07:00
David Fifield
aa3999857f Move ICE server logging out of parseIceServers. 2020-01-23 11:24:00 -07:00
David Fifield
509f634506 NewWebRTCDialer cannot return an error. 2020-01-23 11:24:00 -07:00
David Fifield
d6467ff585 Formatting improvements. 2020-01-23 10:43:31 -07:00
David Fifield
e27709080a Update a comment: we no longer keep track of handlers. 2020-01-23 10:42:35 -07:00
David Fifield
5ff75e1034 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
b26c7a7a73
3ec9dd19fa
ed3d42e1ec
2020-01-20 23:57:31 -07:00
Jascha
37aaaffa15 proxy/make.js: add help output 2019-12-13 16:17:43 -07:00
Arlo Breault
1e45d48a3c Document setting the proxyType for metrics
Trac: 32499
2019-12-06 17:54:54 -05:00
Arlo Breault
af4cc52dc2 Add a build step / documentation for code reuse
Trac: 32499
2019-12-06 17:19:46 -05:00
Cecylia Bocovich
3bdcc3408e Increased test coverage for messages library 2019-12-06 11:30:34 -05:00
Cecylia Bocovich
0f99c5ab12 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.
2019-12-06 11:30:34 -05:00
Cecylia Bocovich
dabdd847ce 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.
2019-12-06 11:28:41 -05:00
Cecylia Bocovich
06298eec73 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.
2019-12-05 10:17:20 -05:00
Cecylia Bocovich
42e16021c4 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.
2019-12-05 10:16:34 -05:00
Cecylia Bocovich
dccc15a6e9 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.
2019-12-05 09:47:26 -05:00
Cecylia Bocovich
07f2cd8073 bump version to 0.2.0 2019-12-03 14:09:05 -05:00
Cecylia Bocovich
94de69aa36 Updated broker specification and comments 2019-11-28 13:52:58 -05:00
Cecylia Bocovich
97554e03e4 Updated proxyType variable name for readability 2019-11-28 13:52:58 -05:00
Cecylia Bocovich
981abffbd9 Add proxy type to stats exported by broker 2019-11-28 13:52:58 -05:00
Cecylia Bocovich
8ab81fc6cd Update proxy config to take proxy type
This allows badge and standalone proxies to tell the broker what proxy
type they are.
2019-11-28 13:52:58 -05:00
Cecylia Bocovich
7277bb37cd 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.
2019-11-28 13:52:58 -05:00
Arlo Breault
7092b2cb2c Revert abstracting copyloop 2019-11-21 19:33:39 -05:00
Arlo Breault
30b5ef8a9e Use gorilla websocket in proxy-go too
Trac: 32465
2019-11-20 19:33:28 -05:00
Cecylia Bocovich
7557e96a8d Remove unnecessary logging at broker 2019-11-13 15:01:03 -05:00
Cecylia Bocovich
742070a7fb Clean up proxy-go tests 2019-11-13 14:31:55 -05:00
Cecylia Bocovich
459286c143 Test proxy-go interactions with broker 2019-11-13 13:57:17 -05:00
Cecylia Bocovich
446f39a9e5 Use http.RoundTripper for connections to broker
This change makes it easier for us to write tests with mock transports
2019-11-13 13:57:14 -05:00
Cecylia Bocovich
574c57cc98 Created tests for proxy-go utility functions 2019-11-13 13:57:11 -05:00
Cecylia Bocovich
32bec89a84 Add tests for session descripion functions
Also removed some unnecessary code
2019-11-13 13:57:06 -05:00
Cecylia Bocovich
3ec2e8b89e Renamed existing test file 2019-11-13 13:57:02 -05:00
Cecylia Bocovich
2f37a73e71 bump version to 0.1.0 2019-11-13 13:36:30 -05:00
Cecylia Bocovich
a7040e2eee Update travis to use go v1.13.x 2019-11-13 11:39:33 -05:00
Cecylia Bocovich
b4b538a17f Implemented new broker messages for browser proxy 2019-11-13 10:54:48 -05:00
Cecylia Bocovich
c4ae64905b 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
2019-11-13 10:54:48 -05:00
Arlo Breault
abefae1587 Restore sending close message before closing
And simplify EOF check.
2019-11-11 17:20:00 -05:00
Arlo Breault
c417fd5599 Stop using custom websocket library in server
Trac: 31028
2019-11-11 17:20:00 -05:00
Cecylia Bocovich
300a23c6a0 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.
2019-10-31 12:08:43 -04:00
Cecylia Bocovich
64b66c855f 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.
2019-10-31 11:59:13 -04:00
Cecylia Bocovich
789285e0df Remove "active" property of proxyPairs
Use their existence in the proxy pair list to indicate they are active.
2019-10-31 11:59:13 -04:00
Cecylia Bocovich
d186fcd401 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).
2019-10-31 11:59:13 -04:00
Cecylia Bocovich
9b470fbe4b 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.
2019-10-31 11:59:13 -04:00
176 changed files with 12784 additions and 6951 deletions

21
.gitignore vendored
View file

@ -6,19 +6,14 @@
datadir/
broker/broker
client/client
server-webrtc/server-webrtc
server/server
proxy-go/proxy-go
proxy/proxy
probetest/probetest
snowflake.log
proxy/test
proxy/build
proxy/node_modules
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 running the vagrant setup
/.vagrant/
/sdk-tools-linux-*.zip*
/android-ndk-*
/tools/

View file

@ -1,29 +1,205 @@
image: golang:1.10-stretch
cache:
paths:
- .gradle/wrapper
- .gradle/caches
variables:
DEBIAN_FRONTEND: noninteractive
REPRODUCIBLE_FLAGS: -trimpath -ldflags=-buildid=
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
# set up apt for automated use
.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
build:
script:
- apt-get -qy update
- apt-get -qy install libx11-dev
- cd $src/gitlab.com/$CI_PROJECT_PATH/client
- go get ./...
- go build ./...
- go vet ./...
- go test -v -race ./...
after_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:
GOPATH: /usr/share/gocode
before_script:
- apt-get update
- 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-goptlib-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
before_script:
- apt-get update
- apt-get -qy install --no-install-recommends
ca-certificates
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 $REPRODUCIBLE_FLAGS
.test-template: &test-template
artifacts:
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_JOB_ID}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
paths:
- client/*.aar
- client/*.jar
- client/client
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 -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
- rm -rf $GRADLE_USER_HOME/caches/*/plugin-resolution/
# -- jobs ------------------------------------------------------------
android:
image: debian:bullseye-backports
variables:
ANDROID_HOME: /usr/lib/android-sdk
GOPATH: "/go"
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-template
- apt-get install
android-sdk-platform-23
android-sdk-platform-tools
build-essential
curl
default-jdk-headless
git
gnupg
unzip
wget
- 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=$CI_PROJECT_DIR/.gradle
- 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/gobind
- go install golang.org/x/mobile/cmd/gomobile
- gomobile init
- cd $CI_PROJECT_DIR/client
# gomobile builds a shared library not a CLI executable
- sed -i 's,^package main$,package snowflakeclient,' *.go
- go get golang.org/x/mobile/bind
- gomobile bind -v -target=android $REPRODUCIBLE_FLAGS .
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:
- *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
<<: *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'

4
.gitmodules vendored
View file

@ -1,4 +0,0 @@
[submodule "proxy/translation"]
path = proxy/translation
url = https://git.torproject.org/translation.git
branch = snowflakeaddon-messages.json_completed

View file

@ -2,43 +2,12 @@ language: go
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_import_path: git.torproject.org/pluggable-transports/snowflake.git/v2
go:
- 1.10.x
env:
- TRAVIS_NODE_VERSION="8" CC="gcc-5" CXX="g++-5"
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 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
- pushd proxy
- npm install
- popd
- 1.13.x
script:
- test -z "$(go fmt ./...)"
- go vet ./...
- go test -v -race ./...
- cd proxy
- npm run lint
- npm test

65
ChangeLog Normal file
View file

@ -0,0 +1,65 @@
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
- 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
- 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
- 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
- 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.

View file

@ -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:

120
README.md
View file

@ -8,95 +8,47 @@ Pluggable Transport using WebRTC, inspired by Flashproxy.
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**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 directly via WebRTC Server --](#---testing-directly-via-webrtc-server---)
- [More info and links](#more-info-and-links)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### 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.10+
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.
Proxy:
- JavaScript
#### Running a Snowflake Proxy
---
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.
#### More Info
#### Using the Snowflake Library with Other Applications
Tor can plug in the Snowflake client via a correctly configured `torrc`.
For example:
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).
```
ClientTransportPlugin snowflake exec ./client \
-url https://snowflake-broker.azureedge.net/ \
-front ajax.aspnetcdn.com \
-ice stun:stun.l.google.com:19302
-max 3
```
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
```
#### 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
### Test Environment
There is a Docker-based test environment at https://github.com/cohosh/snowbox.
### FAQ
**Q: How does it work?**
@ -132,22 +84,16 @@ 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 --
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/.
```
cd proxy-go
go build
./proxy-go
```
##### -- Testing directly via WebRTC Server --
##### -- Android AAR Reproducible Build Setup --
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:
[torproject.org/pluggable-transports/snowflake](https://gitweb.torproject.org/pluggable-transports/snowflake.git/)
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.

67
Vagrantfile vendored Normal file
View file

@ -0,0 +1,67 @@
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['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)
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

View file

@ -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 "<appname>" 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 <appname>
google-cloud-sdk/bin/gcloud app create --project=<appname>
Then to deploy the project, run:
google-cloud-sdk/bin/gcloud app deploy --project=<appname>
To configure the Snowflake client to talk to the App Engine app, provide
"https://<appname>.appspot.com/" as the --url option.
UseBridges 1
Bridge snowflake 0.0.2.0:1
ClientTransportPlugin snowflake exec ./client -url https://<appname>.appspot.com/ -front www.google.com

View file

@ -1,7 +0,0 @@
runtime: go
api_version: go1
handlers:
- url: /.*
script: _go_app
secure: always

View file

@ -1,111 +0,0 @@
// A web app for Google App Engine that proxies HTTP requests and responses to
// the Snowflake broker.
package reflect
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)
}

View file

@ -1,3 +1,12 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Overview](#overview)
- [Running your own](#running-your-own)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
This is the Broker component of Snowflake.
### Overview

76
broker/amp.go Normal file
View file

@ -0,0 +1,76 @@
package main
import (
"log"
"net/http"
"strings"
"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,
// 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)
}
}

94
broker/bridge-list.go Normal file
View file

@ -0,0 +1,94 @@
/* (*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 (
"bufio"
"bytes"
"encoding/json"
"errors"
"io"
"sync"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
)
var ErrBridgeNotFound = errors.New("bridge not found")
func NewBridgeListHolder() BridgeListHolderFileBased {
return &bridgeListHolder{}
}
type bridgeListHolder struct {
bridgeInfo map[bridgefingerprint.Fingerprint]BridgeInfo
accessBridgeInfo sync.RWMutex
}
type BridgeListHolder interface {
GetBridgeInfo(bridgefingerprint.Fingerprint) (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 bridgefingerprint.Fingerprint) (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[bridgefingerprint.Fingerprint]BridgeInfo{}
inputScanner := bufio.NewScanner(reader)
for inputScanner.Scan() {
inputLine := inputScanner.Bytes()
bridgeInfo := BridgeInfo{}
decoder := json.NewDecoder(bytes.NewReader(inputLine))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&bridgeInfo); err != nil {
return err
}
var bridgeFingerprint bridgefingerprint.Fingerprint
var err error
if bridgeFingerprint, err = bridgefingerprint.FingerprintFromHexString(bridgeInfo.Fingerprint); err != nil {
return err
}
bridgeInfoMap[bridgeFingerprint] = bridgeInfo
}
h.accessBridgeInfo.Lock()
defer h.accessBridgeInfo.Unlock()
h.bridgeInfo = bridgeInfoMap
return nil
}

View file

@ -0,0 +1,64 @@
package main
import (
"bytes"
"encoding/hex"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
. "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)
}
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")
}
})
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)
}
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")
}
})
}

View file

@ -6,43 +6,56 @@ SessionDescriptions in order to negotiate a WebRTC connection.
package main
import (
"bytes"
"container/heap"
"crypto/tls"
"flag"
"fmt"
"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"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
"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"
"golang.org/x/crypto/acme/autocert"
)
const (
ClientTimeout = 10
ProxyTimeout = 10
readLimit = 100000 //Maximum number of bytes to be read from an HTTP request
)
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
proxyPolls chan *ProxyPoll
metrics *Metrics
bridgeList BridgeListHolderFileBased
allowedRelayPattern string
presumedPatternForLegacyClient string
}
func (ctx *BrokerContext) GetBridgeInfo(fingerprint bridgefingerprint.Fingerprint) (BridgeInfo, error) {
return ctx.bridgeList.GetBridgeInfo(fingerprint)
}
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 {
@ -53,58 +66,40 @@ 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,
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,
bridgeList: bridgeListHolder,
}
}
// Implements the http.Handler interface
type SnowflakeHandler struct {
*BrokerContext
handle func(*BrokerContext, 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.BrokerContext, 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
offerChannel chan []byte
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) []byte {
func (ctx *BrokerContext) RequestOffer(id string, proxyType string, natType string, clients int) *ClientOffer {
request := new(ProxyPoll)
request.id = id
request.offerChannel = make(chan []byte)
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.
offer := <-request.offerChannel
@ -116,7 +111,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.proxyType, request.natType, request.clients)
// Wait for a client to avail an offer to the snowflake.
go func(request *ProxyPoll) {
select {
@ -124,10 +119,18 @@ 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 {
if request.natType == NATUnrestricted {
heap.Remove(ctx.snowflakes, snowflake.index)
} else {
heap.Remove(ctx.restrictedSnowflakes, snowflake.index)
}
ctx.metrics.promMetrics.AvailableProxies.With(prometheus.Labels{"nat": request.natType, "type": request.proxyType}).Dec()
delete(ctx.idToSnowflake, snowflake.id)
close(request.offerChannel)
}
}
}(request)
}
@ -136,165 +139,49 @@ 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, proxyType string, natType string, clients int) *Snowflake {
snowflake := new(Snowflake)
snowflake.id = id
snowflake.clients = 0
snowflake.offerChannel = make(chan []byte)
snowflake.answerChannel = make(chan []byte)
heap.Push(ctx.snowflakes, snowflake)
snowflake.clients = clients
snowflake.proxyType = proxyType
snowflake.natType = natType
snowflake.offerChannel = make(chan *ClientOffer)
snowflake.answerChannel = make(chan string)
ctx.snowflakeLock.Lock()
if natType == NATUnrestricted {
heap.Push(ctx.snowflakes, snowflake)
} else {
heap.Push(ctx.restrictedSnowflakes, snowflake)
}
ctx.metrics.promMetrics.AvailableProxies.With(prometheus.Labels{"nat": natType, "type": proxyType}).Inc()
ctx.idToSnowflake[id] = snowflake
ctx.snowflakeLock.Unlock()
return 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 {
log.Println("Invalid data.")
w.WriteHeader(http.StatusBadRequest)
return
}
if string(body) != id {
log.Println("Mismatched IDs!")
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.UpdateCountryStats(remoteIP)
}
// Wait for a client to avail an offer to the snowflake, or timeout if nil.
offer := ctx.RequestOffer(id)
if nil == offer {
ctx.metrics.proxyIdleCount++
w.WriteHeader(http.StatusGatewayTimeout)
return
}
log.Println("Passing client offer to snowflake.")
if _, err := w.Write(offer); err != nil {
log.Printf("proxyPolls unable to write offer with error: %v", err)
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
}
/*
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) {
startTime := time.Now()
offer, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, readLimit))
if nil != err {
log.Println("Invalid data.")
w.WriteHeader(http.StatusBadRequest)
return
}
// Immediately fail if there are no snowflakes available.
if ctx.snowflakes.Len() <= 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.
snowflake := heap.Pop(ctx.snowflakes).(*Snowflake)
defer delete(ctx.idToSnowflake, snowflake.id)
snowflake.offerChannel <- offer
// Wait for the answer to be returned on the channel or timeout.
select {
case answer := <-snowflake.answerChannel:
ctx.metrics.clientProxyMatchCount++
if _, err := w.Write(answer); err != nil {
log.Printf("unable to write answer with error: %v", err)
}
// 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)
}
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)
}
/*
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) {
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.")
w.WriteHeader(http.StatusBadRequest)
return
}
snowflake.answerChannel <- body
}
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
for _, snowflake := range ctx.idToSnowflake {
if len(snowflake.id) < 16 {
browsers++
} else {
standalones++
}
}
s += fmt.Sprintf("\tstandalone proxies: %d", standalones)
s += fmt.Sprintf("\n\tbrowser proxies: %d", browsers)
if _, err := w.Write([]byte(s)); 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)
}
// Client offer contains an SDP, bridge fingerprint and the NAT type of the client
type ClientOffer struct {
natType string
sdp []byte
fingerprint []byte
}
func main() {
@ -304,10 +191,14 @@ func main() {
var addr string
var geoipDatabase string
var geoip6Database string
var bridgeListFilePath, allowedRelayPattern, presumedPatternForLegacyClient string
var disableTLS bool
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")
flag.StringVar(&acmeHostnamesCommas, "acme-hostnames", "", "comma-separated hostnames for TLS certificate")
@ -317,16 +208,27 @@ 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.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")
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()
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)
@ -344,6 +246,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, presumedPatternForLegacyClient)
if err != nil {
log.Fatal(err.Error())
}
}
if !disableGeoip {
err = ctx.metrics.LoadGeoipDatabases(geoipDatabase, geoip6Database)
if err != nil {
@ -351,15 +264,30 @@ 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}
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{}))
http.Handle("/amp/client/", SnowflakeHandler{i, ampClientOffers})
server := http.Server{
Addr: addr,

View file

@ -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
}

235
broker/http.go Normal file
View file

@ -0,0 +1,235 @@
package main
import (
"errors"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"git.torproject.org/pluggable-transports/snowflake.git/v2/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.", err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}
arg := messages.Arg{
Body: body,
RemoteAddr: r.RemoteAddr,
}
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
}
// 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
req := messages.ClientPollRequest{
Offer: string(body),
NAT: r.Header.Get("Snowflake-NAT-Type"),
}
body, err = req.EncodeClientPollRequest()
if err != nil {
log.Printf("Error shimming the legacy request: %s", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
}
arg := messages.Arg{
Body: body,
RemoteAddr: "",
}
var response []byte
err = i.ClientOffers(arg, &response)
if err != nil {
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 messages.StrNoProxies:
w.WriteHeader(http.StatusServiceUnavailable)
return
case messages.StrTimedOut:
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)
}
}
/*
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.", err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}
arg := messages.Arg{
Body: body,
RemoteAddr: "",
}
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)
}
}

282
broker/ipc.go Normal file
View file

@ -0,0 +1,282 @@
package main
import (
"container/heap"
"encoding/hex"
"fmt"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
"log"
"net"
"time"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
"github.com/prometheus/client_golang/prometheus"
)
const (
ClientTimeout = 10
ProxyTimeout = 10
NATUnknown = "unknown"
NATRestricted = "restricted"
NATUnrestricted = "unrestricted"
)
type IPC struct {
ctx *BrokerContext
}
func (i *IPC) Debug(_ interface{}, response *string) error {
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 messages.KnownProxyTypes[snowflake.proxyType] {
proxyTypes[snowflake.proxyType]++
} else {
unknowns++
}
switch snowflake.natType {
case NATRestricted:
natRestricted++
case NATUnrestricted:
natUnrestricted++
default:
natUnknown++
}
}
i.ctx.snowflakeLock.Unlock()
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)
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, relayPattern, relayPatternSupported, err := messages.DecodeProxyPollRequestWithRelayPrefix(arg.Body)
if err != nil {
return messages.ErrBadRequest
}
if !relayPatternSupported {
i.ctx.metrics.lock.Lock()
i.ctx.metrics.proxyPollWithoutRelayURLExtension++
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, "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, "type": proxyType}).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
if err != nil {
return messages.ErrInternal
}
return nil
}
// 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.RecordIPAddress(remoteIP)
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()
var relayURL string
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
}
b, err = messages.EncodePollResponseWithRelayURL(string(offer.sdp), true, offer.natType, relayURL, "")
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 {
startTime := time.Now()
req, err := messages.DecodeClientPollRequest(arg.Body)
if err != nil {
return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response)
}
offer := &ClientOffer{
natType: req.NAT,
sdp: []byte(req.Offer),
}
fingerprint, err := hex.DecodeString(req.Fingerprint)
if err != nil {
return sendClientResponse(&messages.ClientPollResponse{Error: err.Error()}, response)
}
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()
snowflake := i.matchSnowflake(offer.natType)
if snowflake != nil {
snowflake.offerChannel <- offer
} else {
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()
resp := &messages.ClientPollResponse{Error: messages.StrNoProxies}
return sendClientResponse(resp, response)
}
// 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()
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.")
resp := &messages.ClientPollResponse{Error: messages.StrTimedOut}
err = sendClientResponse(resp, response)
}
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) 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 == "" {
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
}

View file

@ -1,84 +1,93 @@
/*
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-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"
"net"
"sort"
"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"
)
var (
once sync.Once
const (
prometheusNamespace = "snowflake"
metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds
)
const metricsResolution = 60 * 60 * 24 * time.Second //86400 seconds
type CountryStats struct {
addrs 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
natUnknown map[string]bool
counts map[string]int
}
// Implements Observable
type Metrics struct {
logger *log.Logger
tablev4 *GeoIPv4Table
tablev6 *GeoIPv6Table
geoipdb *geoip.Geoip
countryStats CountryStats
clientRoundtripEstimate time.Duration
proxyIdleCount uint
clientDeniedCount uint
clientProxyMatchCount uint
distinctIPWriter *sinkcluster.ClusterWriter
countryStats CountryStats
clientRoundtripEstimate time.Duration
proxyIdleCount uint
clientDeniedCount uint
clientRestrictedDeniedCount uint
clientUnrestrictedDeniedCount uint
clientProxyMatchCount uint
proxyPollWithRelayURLExtension uint
proxyPollWithoutRelayURLExtension uint
proxyPollRejectedWithRelayURLExtension uint
// synchronization for access to snowflake metrics
lock sync.Mutex
promMetrics *PromMetrics
}
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 ","
@ -89,73 +98,80 @@ func (s CountryStats) Display() string {
return output
}
func (m *Metrics) UpdateCountryStats(addr string) {
func (m *Metrics) UpdateCountryStats(addr string, proxyType string, natType string) {
var country string
var ok bool
if m.countryStats.addrs[addr] {
return
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)
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 = "??"
}
//update map of unique ips and counts
m.countryStats.counts[country]++
m.countryStats.addrs[addr] = true
m.promMetrics.ProxyTotal.With(prometheus.Labels{
"nat": natType,
"type": proxyType,
"cc": country,
}).Inc()
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 {
// 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) {
m := new(Metrics)
m.countryStats = CountryStats{
counts: make(map[string]int),
addrs: make(map[string]bool),
counts: make(map[string]int),
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()
// Write to log file every hour with updated metrics
go once.Do(m.logMetrics)
// Write to log file every day with updated metrics
go m.logMetrics()
return m, nil
}
@ -170,24 +186,154 @@ 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.addrs))
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("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))
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()
}
// Restores all metrics to original values
func (m *Metrics) zeroMetrics() {
m.proxyIdleCount = 0
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)
m.countryStats.addrs = 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)
m.countryStats.natUnknown = make(map[string]bool)
}
// Rounds up a count to the nearest multiple of 8.
func binCount(count uint) uint {
return uint((math.Ceil(float64(count) / 8)) * 8)
}
type PromMetrics struct {
registry *prometheus.Registry
ProxyTotal *prometheus.CounterVec
ProxyPollTotal *RoundedCounterVec
ClientPollTotal *RoundedCounterVec
AvailableProxies *prometheus.GaugeVec
ProxyPollWithRelayURLExtensionTotal *RoundedCounterVec
ProxyPollWithoutRelayURLExtensionTotal *RoundedCounterVec
ProxyPollRejectedForRelayURLExtensionTotal *RoundedCounterVec
}
// Initialize metrics for prometheus exporter
func initPrometheus() *PromMetrics {
promMetrics := &PromMetrics{}
promMetrics.registry = prometheus.NewRegistry()
promMetrics.ProxyTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: prometheusNamespace,
Name: "proxy_total",
Help: "The number of unique snowflake IPs",
},
[]string{"type", "nat", "cc"},
)
promMetrics.AvailableProxies = prometheus.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,
Name: "rounded_proxy_poll_total",
Help: "The number of snowflake proxy polls, rounded up to a multiple of 8",
},
[]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", "type"},
)
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", "type"},
)
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", "type"},
)
promMetrics.ClientPollTotal = NewRoundedCounterVec(
prometheus.CounterOpts{
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 our metrics so they can be exported.
promMetrics.registry.MustRegister(
promMetrics.ClientPollTotal, promMetrics.ProxyPollTotal,
promMetrics.ProxyTotal, promMetrics.AvailableProxies,
promMetrics.ProxyPollWithRelayURLExtensionTotal,
promMetrics.ProxyPollWithoutRelayURLExtensionTotal,
promMetrics.ProxyPollRejectedForRelayURLExtensionTotal,
)
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
}

83
broker/prometheus.go Normal file
View file

@ -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)
}

View file

@ -3,15 +3,18 @@ package main
import (
"bytes"
"container/heap"
"encoding/hex"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptest"
"os"
"sync"
"testing"
"time"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/amp"
. "github.com/smartystreets/goconvey/convey"
)
@ -21,15 +24,31 @@ func NullLogger() *log.Logger {
return 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) {
defaultBridgeValue, _ := hex.DecodeString("2B280B23E1107BB62ABFC40DDCC8824814F80A72")
var defaultBridge [20]byte
copy(defaultBridge[:], defaultBridgeValue)
Convey("Context", t, func() {
ctx := NewBrokerContext(NullLogger())
i := &IPC{ctx}
Convey("Adds Snowflake", func() {
So(ctx.snowflakes.Len(), ShouldEqual, 0)
So(len(ctx.idToSnowflake), ShouldEqual, 0)
ctx.AddSnowflake("foo")
ctx.AddSnowflake("foo", "", NATUnrestricted, 0)
So(ctx.snowflakes.Len(), ShouldEqual, 1)
So(len(ctx.idToSnowflake), ShouldEqual, 1)
})
@ -37,7 +56,8 @@ 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.natType = "unrestricted"
p.offerChannel = make(chan *ClientOffer)
go func(ctx *BrokerContext) {
ctx.proxyPolls <- p
close(ctx.proxyPolls)
@ -45,33 +65,82 @@ 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")
offer := ctx.RequestOffer("test", "", NATUnrestricted, 0)
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() {
Convey("Responds to HTTP 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(i, 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, 0)
go func() {
clientOffers(i, 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, 0)
go func() {
clientOffers(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)
So(w.Body.String(), ShouldEqual, `{"error":"timed out waiting for answer!"}`)
})
})
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)
So(err, ShouldBeNil)
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, "")
})
@ -79,14 +148,14 @@ 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", "", NATUnrestricted, 0)
go func() {
clientOffers(ctx, w, r)
clientOffers(i, w, r)
done <- true
}()
offer := <-snowflake.offerChannel
So(offer, 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)
@ -97,154 +166,271 @@ func TestBroker(t *testing.T) {
return
}
done := make(chan bool)
snowflake := ctx.AddSnowflake("fake")
snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0)
go func() {
clientOffers(ctx, w, r)
clientOffers(i, w, r)
// Takes a few seconds here...
done <- true
}()
offer := <-snowflake.offerChannel
So(offer, ShouldResemble, []byte("test"))
So(offer.sdp, ShouldResemble, []byte("{test}"))
<-done
So(w.Code, ShouldEqual, http.StatusGatewayTimeout)
})
})
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()
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() {
go func(ctx *BrokerContext) {
proxyPolls(ctx, w, r)
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, "test")
p.offerChannel <- []byte("fake offer")
So(p.id, ShouldEqual, "ymbcCMto7KHNGYlp")
p.offerChannel <- &ClientOffer{sdp: []byte("fake offer"), fingerprint: defaultBridge[:]}
<-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","NAT":"","RelayURL":"wss://snowflake.torproject.net/"}`)
})
Convey("times out when no client offer is available.", func() {
go func(ctx *BrokerContext) {
proxyPolls(ctx, w, r)
Convey("return empty 200 OK when no client offer is available.", func() {
go func(i *IPC) {
proxyPolls(i, w, r)
done <- true
}(ctx)
}(i)
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":"","NAT":"","RelayURL":""}`)
So(w.Code, ShouldEqual, http.StatusOK)
})
})
Convey("Responds to proxy answers...", func() {
s := ctx.AddSnowflake("test")
done := make(chan bool)
s := ctx.AddSnowflake("test", "", NATUnrestricted, 0)
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)
go func(i *IPC) {
proxyAnswers(i, w, r)
done <- true
}(i)
answer := <-s.answerChannel
<-done
So(w.Code, ShouldEqual, http.StatusOK)
So(answer, ShouldResemble, []byte("fake answer"))
So(answer, ShouldResemble, "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)
proxyAnswers(i, w, r)
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)
proxyAnswers(i, w, r)
So(w.Code, ShouldEqual, http.StatusBadRequest)
})
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)
proxyAnswers(i, w, r)
So(w.Code, ShouldEqual, http.StatusBadRequest)
})
})
})
Convey("End-To-End", t, func() {
done := make(chan bool)
polled := make(chan bool)
ctx := NewBrokerContext(NullLogger())
i := &IPC{ctx}
// Proxy polls with its ID first...
dataP := bytes.NewReader([]byte("test"))
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
}()
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, "test")
s := ctx.AddSnowflake(p.id)
go func() {
offer := <-s.offerChannel
p.offerChannel <- offer
}()
So(ctx.idToSnowflake["test"], 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, "fake offer")
So(ctx.idToSnowflake["test"], 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)
So(err, ShouldBeNil)
rA.Header.Set("X-Session-ID", "test")
proxyAnswers(ctx, wA, rA)
So(wA.Code, ShouldEqual, http.StatusOK)
go func(i *IPC) {
proxyPolls(i, wp, rp)
proxy_done <- true
}(i)
<-done
So(wC.Code, ShouldEqual, http.StatusOK)
So(wC.Body.String(), ShouldEqual, "fake answer")
// 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(i, 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(i *IPC) {
proxyAnswers(i, wp, rp)
proxy_done <- true
}(i)
<-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(i, 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, "", NATUnrestricted, 0)
go func() {
offer := <-s.offerChannel
p.offerChannel <- offer
}()
So(ctx.idToSnowflake["ymbcCMto7KHNGYlp"], ShouldNotBeNil)
// Client request blocks until proxy answer arrives.
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)
go func() {
clientOffers(i, wC, rC)
done <- true
}()
<-polled
So(wP.Code, ShouldEqual, http.StatusOK)
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()
dataA := bytes.NewReader([]byte(`{"Version":"1.0","Sid":"ymbcCMto7KHNGYlp","Answer":"test"}`))
rA, err := http.NewRequest("POST", "snowflake.broker/answer", dataA)
So(err, ShouldBeNil)
proxyAnswers(i, wA, rA)
So(wA.Code, ShouldEqual, http.StatusOK)
<-done
So(wC.Code, ShouldEqual, http.StatusOK)
So(wC.Body.String(), ShouldEqual, `{"answer":"test"}`)
})
})
}
@ -291,116 +477,25 @@ 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")
So(ctx.metrics.tablev4, ShouldEqual, nil)
ctx.metrics.UpdateCountryStats("127.0.0.1", "", NATUnrestricted)
So(ctx.metrics.geoipdb, ShouldEqual, nil)
})
}
func TestMetrics(t *testing.T) {
Convey("Test metrics...", t, func() {
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)
@ -408,120 +503,299 @@ 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
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
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(i *IPC) {
proxyPolls(i, w, r)
done <- true
}(i)
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(i *IPC) {
proxyPolls(i, w, r)
done <- true
}(i)
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(i *IPC) {
proxyPolls(i, w, r)
done <- true
}(i)
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")
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\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
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)
clientOffers(ctx, w, r)
clientOffers(i, 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(), 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-idle-count 0\nclient-denied-count 0\nclient-snowflake-match-count 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\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() {
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)
// Prepare a fake proxy to respond with.
snowflake := ctx.AddSnowflake("fake")
snowflake := ctx.AddSnowflake("fake", "", NATUnrestricted, 0)
go func() {
clientOffers(ctx, w, r)
clientOffers(i, w, r)
done <- true
}()
offer := <-snowflake.offerChannel
So(offer, ShouldResemble, []byte("test"))
snowflake.answerChannel <- []byte("fake answer")
So(offer.sdp, ShouldResemble, []byte("fake"))
snowflake.answerChannel <- "fake answer"
<-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(), 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() {
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)
clientOffers(ctx, w, r)
clientOffers(ctx, w, r)
clientOffers(ctx, w, r)
clientOffers(ctx, w, r)
clientOffers(ctx, w, r)
clientOffers(ctx, w, r)
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(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(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(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(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(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(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(i, 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(), ShouldContainSubstring, "client-denied-count 8\nclient-restricted-denied-count 8\nclient-unrestricted-denied-count 0\n")
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(i, 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(), ShouldContainSubstring, "client-denied-count 16\nclient-restricted-denied-count 16\nclient-unrestricted-denied-count 0\n")
})
//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
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
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)
}
r.Header.Set("X-Session-ID", "test")
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
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")
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() {
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(i *IPC) {
proxyPolls(i, w, r)
done <- true
}(i)
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(i *IPC) {
proxyPolls(i, w, r)
done <- true
}(i)
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("1.0\n{\"offer\": \"fake\", \"nat\": \"restricted\"}"))
r, err := http.NewRequest("POST", "snowflake.broker/client", data)
So(err, ShouldBeNil)
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")
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)
So(err, ShouldBeNil)
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")
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)
So(err, ShouldBeNil)
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")
})
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")
})
})
}

View file

@ -10,8 +10,10 @@ over the offer and answer channels.
*/
type Snowflake struct {
id string
offerChannel chan []byte
answerChannel chan []byte
proxyType string
natType string
offerChannel chan *ClientOffer
answerChannel chan string
clients int
index int
}

View file

@ -1,20 +1,110 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**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)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
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
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
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.
### 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.

View file

@ -1,56 +1,24 @@
package lib
package snowflake_client
import (
"io"
"net"
)
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
}
// Interface for catching Snowflakes. (aka the remote dialer)
// Tongue is an interface for catching Snowflakes. (aka the remote dialer)
type Tongue interface {
Catch() (Snowflake, error)
// Catch makes a connection to a new snowflake.
Catch() (*WebRTCPeer, error)
// 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 {
// 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)
// Add a Snowflake to the collection.
// Implementation should decide how to connect and maintain the webRTCConn.
Collect() (Snowflake, error)
// Pop removes and returns the most available snowflake from the collection.
Pop() *WebRTCPeer
// Remove and return the most available Snowflake from the collection.
Pop() Snowflake
// Signal when the collector has stopped collecting.
// Melted returns a channel that will signal when the collector has stopped.
Melted() <-chan struct{}
}
// Interface to adapt to goptlib's SocksConn struct.
type SocksConnector interface {
Grant(*net.TCPAddr) error
Reject() error
net.Conn
}
// Interface for the Snowflake's transport. (Typically just webrtc.DataChannel)
type SnowflakeDataChannel interface {
io.Closer
Send([]byte) error
}

View file

@ -1,58 +1,26 @@
package lib
package snowflake_client
import (
"bytes"
"fmt"
"io/ioutil"
"net"
"net/http"
"testing"
"time"
"github.com/pion/webrtc"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
. "github.com/smartystreets/goconvey/convey"
)
type MockDataChannel struct {
destination bytes.Buffer
done chan bool
type FakeDialer struct {
max int
}
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
}
// 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{}
func (w FakeDialer) Catch() (Snowflake, error) {
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 {
return w.max
}
type FakeSocksConn struct {
@ -66,29 +34,23 @@ func (f FakeSocksConn) Reject() error {
}
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 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)
@ -98,8 +60,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)
@ -113,7 +74,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)
@ -125,8 +86,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()
@ -142,9 +102,9 @@ 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{})
p.activePeers.PushBack(&WebRTCPeer{closed: make(chan struct{})})
}
So(p.Count(), ShouldEqual, cnt)
p.End()
@ -153,8 +113,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()
@ -172,162 +131,95 @@ 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)
Convey("Snowflake", t, func() {
SkipConvey("Handler Grants correctly", func() {
socks := &FakeSocksConn{}
snowflakes := &FakePeers{}
So(socks.rejected, ShouldEqual, false)
snowflakes.toRelease = nil
Handler(socks, snowflakes)
So(socks.rejected, ShouldEqual, true)
p.End()
<-p.Melted()
So(p.Count(), ShouldEqual, 0)
})
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.preparePeerConnection()
c.offerChannel <- nil
answer := deserializeSessionDescription(
`{"type":"answer","sdp":""}`)
c.answerChannel <- answer
c.exchangeSDP()
})
SkipConvey("Exchange SDP fails on nil answer", func() {
c.reset = make(chan struct{})
c.offerChannel = make(chan *webrtc.SessionDescription, 1)
c.answerChannel = make(chan *webrtc.SessionDescription, 1)
c.offerChannel <- nil
c.answerChannel <- nil
c.exchangeSDP()
<-c.reset
})
})
})
Convey("Dialers", t, func() {
Convey("Can construct WebRTCDialer.", func() {
broker := &BrokerChannel{Host: "test"}
d := NewWebRTCDialer(broker, nil)
broker := &BrokerChannel{}
d := NewWebRTCDialer(broker, nil, 1)
So(d, ShouldNotBeNil)
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)
broker := &BrokerChannel{}
d := NewWebRTCDialer(broker, nil, 1)
conn, err := d.Catch()
So(conn, ShouldBeNil)
So(err, ShouldNotBeNil)
})
})
Convey("Rendezvous", t, func() {
transport := &MockTransport{
http.StatusOK,
[]byte(`{"type":"answer","sdp":"fake"}`),
}
fakeOffer := deserializeSessionDescription(`{"type":"offer","sdp":"test"}`)
}
Convey("Construct BrokerChannel with no front domain", func() {
b := NewBrokerChannel("test.broker", "", transport)
So(b.url, ShouldNotBeNil)
So(b.url.Path, ShouldResemble, "test.broker")
So(b.transport, ShouldNotBeNil)
})
Convey("Construct BrokerChannel *with* front domain", func() {
b := NewBrokerChannel("test.broker", "front", transport)
So(b.url, ShouldNotBeNil)
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)
answer, err := b.Negotiate(fakeOffer)
So(err, ShouldBeNil)
So(answer, ShouldNotBeNil)
So(answer.SDP, ShouldResemble, "fake")
})
Convey("BrokerChannel.Negotiate fails with 503", func() {
b := NewBrokerChannel("test.broker", "",
&MockTransport{http.StatusServiceUnavailable, []byte("\n")})
answer, err := b.Negotiate(fakeOffer)
So(err, ShouldNotBeNil)
So(answer, ShouldBeNil)
So(err.Error(), ShouldResemble, BrokerError503)
})
Convey("BrokerChannel.Negotiate fails with 400", func() {
b := NewBrokerChannel("test.broker", "",
&MockTransport{http.StatusBadRequest, []byte("\n")})
answer, err := b.Negotiate(fakeOffer)
So(err, ShouldNotBeNil)
So(answer, ShouldBeNil)
So(err.Error(), ShouldResemble, BrokerError400)
})
Convey("BrokerChannel.Negotiate fails with large read", func() {
b := NewBrokerChannel("test.broker", "",
&MockTransport{http.StatusOK, make([]byte, 100001, 100001)})
answer, err := b.Negotiate(fakeOffer)
So(err, ShouldNotBeNil)
So(answer, ShouldBeNil)
So(err.Error(), ShouldResemble, "unexpected EOF")
})
Convey("BrokerChannel.Negotiate fails with unexpected error", func() {
b := NewBrokerChannel("test.broker", "",
&MockTransport{123, []byte("")})
answer, err := b.Negotiate(fakeOffer)
So(err, ShouldNotBeNil)
So(answer, ShouldBeNil)
So(err.Error(), ShouldResemble, BrokerErrorUnexpected)
func TestWebRTCPeer(t *testing.T) {
Convey("WebRTCPeer", t, func(c C) {
p := &WebRTCPeer{closed: make(chan struct{}),
eventsLogger: event.NewSnowflakeEventDispatcher()}
Convey("checks for staleness", func() {
go p.checkForStaleness(time.Second)
<-time.After(2 * time.Second)
So(p.Closed(), ShouldEqual, true)
})
})
}
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)
}
}
})
}

View file

@ -1,13 +1,14 @@
package lib
package snowflake_client
import (
"container/list"
"errors"
"fmt"
"log"
"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
@ -20,38 +21,50 @@ import (
// version of Snowflake)
type Peers struct {
Tongue
BytesLogger
bytesLogger bytesLogger
snowflakeChan chan Snowflake
snowflakeChan chan *WebRTCPeer
activePeers *list.List
capacity int
melt chan struct{}
collectLock sync.Mutex
}
// Construct a fresh container of remote peers.
func NewPeers(max int) *Peers {
p := &Peers{capacity: max}
// 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.
p.snowflakeChan = make(chan Snowflake, 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{}, 1)
return p
p.melt = make(chan struct{})
p.Tongue = tongue
return p, nil
}
// As part of |SnowflakeCollector| interface.
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)
}
log.Println("WebRTC: Collecting a new Snowflake.", s)
// 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()
defer p.collectLock.Unlock()
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")
}
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 {
@ -63,32 +76,30 @@ func (p *Peers) Collect() (Snowflake, error) {
return connection, nil
}
// As part of |SnowflakeCollector| interface.
func (p *Peers) Pop() Snowflake {
// Blocks until an available, valid snowflake appears.
var snowflake Snowflake
var ok bool
for nil == snowflake {
snowflake, ok = <-p.snowflakeChan
conn := snowflake.(*WebRTCPeer)
// 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
if !ok {
return nil
}
if conn.closed {
snowflake = nil
if snowflake.Closed() {
continue
}
// Set to use the same rate-limited traffic logger to keep consistency.
snowflake.bytesLogger = p.bytesLogger
return snowflake
}
// Set to use the same rate-limited traffic logger to keep consistency.
snowflake.(*WebRTCPeer).BytesLogger = p.BytesLogger
return snowflake
}
// 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 {
@ -101,17 +112,20 @@ 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
}
}
// 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()
defer p.collectLock.Unlock()
close(p.snowflakeChan)
p.melt <- struct{}{}
cnt := p.Count()
for e := p.activePeers.Front(); e != nil; {
next := e.Next()
@ -120,5 +134,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)
}

View file

@ -1,154 +1,195 @@
// 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
package snowflake_client
import (
"bytes"
"errors"
"io"
"io/ioutil"
"fmt"
"log"
"net/http"
"net/url"
"sync"
"time"
"github.com/pion/webrtc"
"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"
utlsutil "git.torproject.org/pluggable-transports/snowflake.git/v2/common/utls"
"github.com/pion/webrtc/v3"
utls "github.com/refraction-networking/utls"
)
const (
BrokerError503 string = "No snowflake proxies currently available."
BrokerError400 string = "You sent an invalid offer in the request."
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
)
// 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 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 {
// 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.
Rendezvous RendezvousMethod
keepLocalAddresses bool
natType string
lock sync.Mutex
BridgeFingerprint string
}
// 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
return transport
}
// 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 {
targetURL, err := url.Parse(broker)
if nil != err {
return nil
}
log.Println("Rendezvous using Broker at:", broker)
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
func newBrokerChannelFromConfig(config ClientConfig) (*BrokerChannel, error) {
log.Println("Rendezvous using Broker at:", config.BrokerURL)
if config.FrontDomain != "" {
log.Println("Domain fronting using:", config.FrontDomain)
}
bc.transport = transport
return bc
}
brokerTransport := createBrokerTransport()
func limitedRead(r io.Reader, limit int64) ([]byte, error) {
p, err := ioutil.ReadAll(&io.LimitedReader{R: r, N: limit + 1})
if config.UTLSClientID != "" {
utlsClientHelloID, err := utlsutil.NameToUTLSID(config.UTLSClientID)
if err != nil {
return nil, fmt.Errorf("unable to create broker channel: %v", err)
}
utlsConfig := &utls.Config{}
brokerTransport = utlsutil.NewUTLSHTTPRoundTripper(utlsClientHelloID, utlsConfig, brokerTransport, config.UTLSRemoveSNI)
}
var rendezvous RendezvousMethod
var err error
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(
config.BrokerURL, config.FrontDomain, brokerTransport)
}
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: config.KeepLocalAddresses,
natType: nat.NATUnknown,
BridgeFingerprint: config.BridgeFingerprint,
}, 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) {
log.Println("Negotiating via BrokerChannel...\nTarget URL: ",
bc.Host, "\nFront URL: ", bc.url.Host)
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)
if nil != err {
return nil, err
}
if "" != bc.Host { // Set true host if necessary.
request.Host = bc.Host
}
resp, err := bc.transport.RoundTrip(request)
if nil != err {
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
// 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
if !bc.keepLocalAddresses {
offer = &webrtc.SessionDescription{
Type: offer.Type,
SDP: util.StripLocalAddresses(offer.SDP),
}
answer := deserializeSessionDescription(string(body))
return answer, nil
case http.StatusServiceUnavailable:
return nil, errors.New(BrokerError503)
case http.StatusBadRequest:
return nil, errors.New(BrokerError400)
default:
return nil, errors.New(BrokerErrorUnexpected)
}
offerSDP, err := util.SerializeSessionDescription(offer)
if err != nil {
return nil, err
}
// Encode the client poll request.
bc.lock.Lock()
req := &messages.ClientPollRequest{
Offer: offerSDP,
NAT: bc.natType,
Fingerprint: bc.BridgeFingerprint,
}
encReq, err := req.EncodeClientPollRequest()
bc.lock.Unlock()
if err != nil {
return nil, err
}
// Do the exchange using our RendezvousMethod.
encResp, err := bc.Rendezvous.Exchange(encReq)
if err != nil {
return 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
}
if resp.Error != "" {
return nil, errors.New(resp.Error)
}
return util.DeserializeSessionDescription(resp.Answer)
}
// Implements the |Tongue| interface to catch snowflakes, using BrokerChannel.
// 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
bc.lock.Unlock()
log.Printf("NAT Type: %s", NATType)
}
// WebRTCDialer implements the |Tongue| interface to catch snowflakes, using BrokerChannel.
type WebRTCDialer struct {
*BrokerChannel
webrtcConfig *webrtc.Configuration
max int
eventLogger event.SnowflakeEventReceiver
}
func NewWebRTCDialer(
broker *BrokerChannel, iceServers []webrtc.ICEServer) *WebRTCDialer {
var config webrtc.Configuration
if iceServers != nil {
config = webrtc.Configuration{
ICEServers: iceServers,
}
} else {
config = webrtc.Configuration{}
func NewWebRTCDialer(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int) *WebRTCDialer {
return NewWebRTCDialerWithEvents(broker, iceServers, max, nil)
}
// NewWebRTCDialerWithEvents constructs a new WebRTCDialer.
func NewWebRTCDialerWithEvents(broker *BrokerChannel, iceServers []webrtc.ICEServer, max int, eventLogger event.SnowflakeEventReceiver) *WebRTCDialer {
config := webrtc.Configuration{
ICEServers: iceServers,
}
return &WebRTCDialer{
BrokerChannel: broker,
webrtcConfig: &config,
max: max,
eventLogger: eventLogger,
}
}
// 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)
err := connection.Connect()
return connection, err
// 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 NewWebRTCPeerWithEvents(w.webrtcConfig, w.BrokerChannel, w.eventLogger)
}
// GetMax returns the maximum number of snowflakes to collect.
func (w WebRTCDialer) GetMax() int {
return w.max
}

View file

@ -0,0 +1,124 @@
package snowflake_client
import (
"errors"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/amp"
)
// 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 &ampCacheRendezvous{
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)
// 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
}
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 {
// 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)
}
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
}

View file

@ -0,0 +1,77 @@
package snowflake_client
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
}

View file

@ -0,0 +1,273 @@
package snowflake_client
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"testing"
"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"
)
// 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,
}).EncodeClientPollRequest()
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
}
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() {
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)
})
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)
})
})
}
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 </pre> tag".
So(err, ShouldNotBeNil)
})
})
}

View file

@ -1,68 +1,368 @@
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",
// ...
}
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"
"errors"
"io"
"log"
"math/rand"
"net"
"sync"
"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"
"github.com/xtaci/kcp-go/v5"
"github.com/xtaci/smux"
)
const (
ReconnectTimeout = 10
SnowflakeTimeout = 30
// 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
// 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
)
// Given an accepted SOCKS connection, establish a WebRTC connection to the
// remote peer and exchange traffic.
func Handler(socks SocksConnector, 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)
}
type dummyAddr struct{}
return errors.New("handler: Received invalid Snowflake")
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
// 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.
type ClientConfig struct {
// 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 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
// 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
// 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(config ClientConfig) (*Transport, error) {
log.Println("\n\n\n --- Starting Snowflake Client ---")
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) {
iceServers[i], iceServers[j] = iceServers[j], iceServers[i]
})
if len(iceServers) > 2 {
iceServers = iceServers[:(len(iceServers)+1)/2]
}
defer socks.Close()
defer snowflake.Close()
log.Println("---- Handler: snowflake assigned ----")
err := socks.Grant(&net.TCPAddr{IP: net.IPv4zero, Port: 0})
log.Printf("Using ICE servers:")
for _, server := range iceServers {
log.Printf("url: %v", strings.Join(server.URLs, " "))
}
// Rendezvous with broker using the given parameters.
broker, err := newBrokerChannelFromConfig(config)
if err != nil {
return err
return nil, err
}
go updateNATType(iceServers, broker)
max := 1
if config.Max > max {
max = config.Max
}
eventsLogger := event.NewSnowflakeEventDispatcher()
transport := &Transport{dialer: NewWebRTCDialerWithEvents(broker, iceServers, max, eventsLogger), eventDispatcher: eventsLogger}
return transport, nil
}
// 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()
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()
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
}
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
}
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) {
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
pconn net.PacketConn
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()
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 compatible 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 {
log.Printf("Warning: NAT checking failed for server at %s: %s", addr, err)
} else {
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.
func newSession(snowflakes SnowflakeCollector) (net.PacketConn, *smux.Session, error) {
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
}
pconn := turbotunnel.NewRedialPacketConn(dummyAddr{}, dummyAddr{}, dialContext)
// 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 {
pconn.Close()
return nil, nil, 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(WindowSize, WindowSize)
// 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
smuxConfig.MaxStreamBuffer = StreamSize
sess, err := smux.Client(conn, smuxConfig)
if err != nil {
conn.Close()
pconn.Close()
return nil, nil, err
}
go func() {
// When WebRTC resets, close the SOCKS connection too.
snowflake.WaitForReset()
socks.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 ---")
return nil
return pconn, sess, err
}
// Exchanges bytes between two ReadWriters.
// (In this case, between a SOCKS and WebRTC connection.)
func copyLoop(WebRTC, SOCKS io.ReadWriter) {
var wg sync.WaitGroup
wg.Add(2)
go func() {
if _, err := io.Copy(SOCKS, WebRTC); err != nil {
log.Printf("copying WebRTC to SOCKS resulted in error: %v", err)
// Maintain |SnowflakeCapacity| number of available WebRTC connections, to
// 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...", err)
}
wg.Done()
}()
go func() {
if _, err := io.Copy(WebRTC, SOCKS); err != nil {
log.Printf("copying SOCKS to WebRTC resulted in error: %v", err)
select {
case <-timer:
continue
case <-snowflakes.Melted():
log.Println("ConnectLoop: stopped.")
return
}
wg.Done()
}()
wg.Wait()
log.Println("copy loop ended")
}
}

68
client/lib/turbotunnel.go Normal file
View file

@ -0,0 +1,68 @@
package snowflake_client
import (
"bufio"
"errors"
"io"
"net"
"time"
"git.torproject.org/pluggable-transports/snowflake.git/v2/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 }

View file

@ -1,137 +1,70 @@
package lib
package snowflake_client
import (
"encoding/json"
"log"
"time"
"github.com/pion/webrtc"
)
const (
LogTimeInterval = 5
LogTimeInterval = 5 * time.Second
)
type BytesLogger interface {
Log()
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) Log() {}
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 {
OutboundChan chan int
InboundChan chan int
Outbound int
Inbound int
OutEvents int
InEvents int
IsLogging bool
type bytesSyncLogger struct {
outboundChan chan int
inboundChan chan int
}
func (b *BytesSyncLogger) Log() {
b.IsLogging = true
var amount 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
// newBytesSyncLogger returns a new bytesSyncLogger and starts it loggin.
func newBytesSyncLogger() *bytesSyncLogger {
b := &bytesSyncLogger{
outboundChan: make(chan int, 5),
inboundChan: make(chan int, 5),
}
last := time.Now()
go b.log()
return b
}
func (b *bytesSyncLogger) log() {
var outbound, inbound, outEvents, inEvents int
ticker := time.NewTicker(LogTimeInterval)
for {
select {
case amount = <-b.OutboundChan:
b.Outbound += amount
b.OutEvents++
if time.Since(last) > time.Second*LogTimeInterval {
last = time.Now()
output()
}
case amount = <-b.InboundChan:
b.Inbound += amount
b.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 {
output()
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++
case amount := <-b.inboundChan:
inbound += amount
inEvents++
}
}
}
func (b *BytesSyncLogger) AddOutbound(amount int) {
if !b.IsLogging {
return
}
b.OutboundChan <- amount
func (b *bytesSyncLogger) addOutbound(amount int) {
b.outboundChan <- amount
}
func (b *BytesSyncLogger) AddInbound(amount int) {
if !b.IsLogging {
return
}
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)
func (b *bytesSyncLogger) addInbound(amount int) {
b.inboundChan <- amount
}

View file

@ -1,67 +1,82 @@
package lib
package snowflake_client
import (
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"io"
"log"
"sync"
"time"
"github.com/dchest/uniuri"
"github.com/pion/webrtc"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/event"
"github.com/pion/ice/v2"
"github.com/pion/webrtc/v3"
)
// Remote WebRTC peer.
// Implements the |Snowflake| interface, which includes
// |io.ReadWriter|, |Resetter|, and |Connector|.
// 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
config *webrtc.Configuration
pc *webrtc.PeerConnection
transport SnowflakeDataChannel // Holds the WebRTC DataChannel.
broker *BrokerChannel
transport *webrtc.DataChannel
offerChannel chan *webrtc.SessionDescription
answerChannel chan *webrtc.SessionDescription
errorChannel chan error
recvPipe *io.PipeReader
writePipe *io.PipeWriter
lastReceive time.Time
buffer bytes.Buffer
reset chan struct{}
recvPipe *io.PipeReader
writePipe *io.PipeWriter
closed bool
mu sync.Mutex // protects the following:
lastReceive time.Time
lock sync.Mutex // Synchronization for DataChannel destruction
once sync.Once // Synchronization for PeerConnection destruction
open chan struct{} // Channel to notify when datachannel opens
closed chan struct{}
BytesLogger
once sync.Once // Synchronization for PeerConnection destruction
bytesLogger bytesLogger
eventsLogger event.SnowflakeEventReceiver
}
// Construct a WebRTC PeerConnection.
func NewWebRTCPeer(config *webrtc.Configuration,
broker *BrokerChannel) *WebRTCPeer {
broker *BrokerChannel) (*WebRTCPeer, error) {
return NewWebRTCPeerWithEvents(config, broker, nil)
}
// 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 NewWebRTCPeerWithEvents(config *webrtc.Configuration,
broker *BrokerChannel, eventsLogger event.SnowflakeEventReceiver) (*WebRTCPeer, error) {
if eventsLogger == nil {
eventsLogger = event.NewSnowflakeEventDispatcher()
}
connection := new(WebRTCPeer)
connection.id = "snowflake-" + uniuri.New()
connection.config = config
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)
connection.reset = make(chan struct{}, 1)
{
var buf [8]byte
if _, err := rand.Read(buf[:]); err != nil {
panic(err)
}
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{}
connection.bytesLogger = &bytesNullLogger{}
// Pipes remain the same even when DataChannel gets switched.
connection.recvPipe, connection.writePipe = io.Pipe()
return connection
connection.eventsLogger = eventsLogger
err := connection.connect(config, broker)
if err != nil {
connection.Close()
return nil, err
}
return connection, nil
}
// Read bytes from local SOCKS.
@ -73,200 +88,145 @@ 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
}
// As part of |Snowflake|
// Closed returns a boolean indicated whether the peer is closed.
func (c *WebRTCPeer) Closed() bool {
select {
case <-c.closed:
return true
default:
}
return false
}
// Close closes the connection the snowflake proxy.
func (c *WebRTCPeer) Close() error {
c.once.Do(func() {
c.closed = true
close(c.closed)
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.
func (c *WebRTCPeer) checkForStaleness() {
func (c *WebRTCPeer) checkForStaleness(timeout time.Duration) {
c.mu.Lock()
c.lastReceive = time.Now()
c.mu.Unlock()
for {
if c.closed {
return
}
if time.Since(c.lastReceive).Seconds() > SnowflakeTimeout {
log.Println("WebRTC: No messages received for", SnowflakeTimeout,
"seconds -- closing stale connection.")
c.mu.Lock()
lastReceive := c.lastReceive
c.mu.Unlock()
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
}
<-time.After(time.Second)
select {
case <-c.closed:
return
case <-time.After(time.Second):
}
}
}
// As part of |Connector| interface.
func (c *WebRTCPeer) Connect() 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.
err := c.preparePeerConnection()
err := c.preparePeerConnection(config)
localDescription := c.pc.LocalDescription()
c.eventsLogger.OnNewSnowflakeEvent(event.EventOnOfferCreated{
WebRTCLocalDescription: localDescription,
Error: err,
})
if err != nil {
return err
}
err = c.establishDataChannel()
if err != nil {
// nolint: golint
return errors.New("WebRTC: Could not establish DataChannel")
}
err = c.exchangeSDP()
answer, err := broker.Negotiate(localDescription)
c.eventsLogger.OnNewSnowflakeEvent(event.EventOnBrokerRendezvous{
WebRTCRemoteDescription: answer,
Error: err,
})
if err != nil {
return err
}
go c.checkForStaleness()
log.Printf("Received Answer.\n")
err = c.pc.SetRemoteDescription(*answer)
if nil != err {
log.Println("WebRTC: Unable to SetRemoteDescription:", err)
return err
}
// Wait for the datachannel to open or time out
select {
case <-c.open:
case <-time.After(DataChannelTimeout):
c.transport.Close()
err = errors.New("timeout waiting for DataChannel.OnOpen")
c.eventsLogger.OnNewSnowflakeEvent(event.EventOnSnowflakeConnectionFailed{Error: err})
return err
}
go c.checkForStaleness(SnowflakeTimeout)
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
}
// preparePeerConnection creates a new WebRTC PeerConnection and returns it
// after ICE candidate gathering is complete..
func (c *WebRTCPeer) preparePeerConnection(config *webrtc.Configuration) error {
var err error
s := webrtc.SettingEngine{}
s.SetTrickle(true)
s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
api := webrtc.NewAPI(webrtc.WithSettingEngine(s))
pc, err := api.NewPeerConnection(*c.config)
c.pc, err = api.NewPeerConnection(*config)
if err != nil {
log.Printf("NewPeerConnection ERROR: %s", err)
return err
}
// Prepare PeerConnection callbacks.
// Allow candidates to accumulate until ICEGatheringStateComplete.
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
if candidate == nil {
log.Printf("WebRTC: Done gathering candidates")
} 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)
// 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")
}()
log.Println("WebRTC: PeerConnection created.")
return 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!")
}
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)
// 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
}
dc.OnOpen(func() {
c.lock.Lock()
defer c.lock.Unlock()
c.eventsLogger.OnNewSnowflakeEvent(event.EventOnSnowflakeConnected{})
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(c.open)
})
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.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---")
}
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")
@ -274,93 +234,46 @@ 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.mu.Lock()
c.lastReceive = time.Now()
c.mu.Unlock()
})
c.transport = dc
c.open = make(chan struct{})
log.Println("WebRTC: DataChannel created.")
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
}
// Block until an SDP offer is available, send it to either
// the Broker or signal pipe, then await for the SDP answer.
func (c *WebRTCPeer) exchangeSDP() error {
select {
case <-c.offerChannel:
case err := <-c.errorChannel:
// 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.Close()
c.pc.Close()
return err
}
// 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 %d seconds", ReconnectTimeout)
<-time.After(time.Second * ReconnectTimeout)
answer = nil
}
}
log.Printf("Received Answer.\n")
err := c.pc.SetRemoteDescription(*answer)
if nil != err {
log.Println("WebRTC: Unable to SetRemoteDescription:", 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
// cleanup closes all channels and transports
func (c *WebRTCPeer) cleanup() {
if nil != c.offerChannel {
close(c.offerChannel)
}
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 {
if c.writePipe != nil { // c.writePipe can be nil in tests.
c.writePipe.Close()
c.writePipe = nil
}
c.lock.Lock()
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.(*webrtc.DataChannel).Close()
} else {
c.lock.Unlock()
c.transport.Close()
}
if nil != c.pc {
log.Printf("WebRTC: closing PeerConnection")
@ -368,6 +281,5 @@ func (c *WebRTCPeer) cleanup() {
if nil != err {
log.Printf("Error closing peerconnection...")
}
c.pc = nil
}
}

View file

@ -10,101 +10,170 @@ import (
"os"
"os/signal"
"path/filepath"
"strconv"
"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/safelog"
"github.com/pion/webrtc"
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"
)
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 nil != err {
log.Println("WebRTC:", err,
" Retrying in", sf.ReconnectTimeout, "seconds...")
}
select {
case <-time.After(time.Second * sf.ReconnectTimeout):
continue
case <-snowflakes.Melted():
log.Println("ConnectLoop: stopped.")
return
}
}
type ptEventLogger struct {
}
// Accept local SOCKS connections and pass them to the handler.
func socksAcceptLoop(ln *pt.SocksListener, snowflakes sf.SnowflakeCollector) {
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) {
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, config sf.ClientConfig, shutdown chan struct{}, wg *sync.WaitGroup) {
defer ln.Close()
log.Println("Started SOCKS listener.")
for {
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)
break
}
log.Println("SOCKS accepted: ", conn.Req)
err = sf.Handler(conn, snowflakes)
if err != nil {
log.Printf("handler error: %s", err)
}
}
}
log.Printf("SOCKS accepted: %v", conn.Req)
wg.Add(1)
go func() {
defer wg.Done()
defer conn.Close()
//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
// 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 {
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
}
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
}
if arg, ok := conn.Req.Args.Get("fingerprint"); ok {
config.BridgeFingerprint = arg
}
transport, err := sf.NewSnowflakeClient(config)
if err != nil {
conn.Reject()
log.Println("Failed to start snowflake transport: ", err)
return
}
transport.AddSnowflakeEventListener(NewPTEventLogger())
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{})
go func() {
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)
}()
select {
case <-shutdown:
log.Println("Received shutdown signal")
case <-handler:
log.Println("Handler ended")
}
return
}()
}
urls := strings.Split(s, ",")
log.Printf("Using ICE Servers:")
for _, url := range urls {
log.Printf("url: %s", 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")
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("logToStateDir", false, "resolve the log file relative to tor's pt state dir")
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)
// 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
if *logFilename != "" {
if *logToStateDir {
if *logToStateDir || *oldLogToStateDir {
stateDir, err := pt.MakeStateDir()
if err != nil {
log.Fatal(err)
@ -119,36 +188,23 @@ func main() {
defer logFile.Close()
logOutput = logFile
}
//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 ---")
iceServers := parseIceServers(*iceServersCommas)
// Prepare to collect remote WebRTC peers.
snowflakes := sf.NewPeers(*max)
// Use potentially domain-fronting broker to rendezvous.
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
if *unsafeLogging {
log.SetOutput(logOutput)
} else {
// We want to send the log output through our scrubber first
log.SetOutput(&safelog.LogScrubber{Output: logOutput})
}
// 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()
go ConnectLoop(snowflakes)
iceAddresses := strings.Split(strings.TrimSpace(*iceServersCommas), ",")
config := sf.ClientConfig{
BrokerURL: *brokerURL,
AmpCacheURL: *ampCacheURL,
FrontDomain: *frontDomain,
ICEAddresses: iceAddresses,
KeepLocalAddresses: *keepLocalAddresses || *oldKeepLocalAddresses,
Max: *max,
}
// Begin goptlib client process.
ptInfo, err := pt.ClientSetup(nil)
@ -156,30 +212,27 @@ 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)
shutdown := make(chan struct{})
var wg sync.WaitGroup
for _, methodName := range ptInfo.MethodNames {
switch methodName {
case "snowflake":
// 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)
log.Printf("Started SOCKS listener at %v.", ln.Addr())
go socksAcceptLoop(ln, config, shutdown, &wg)
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()
@ -199,13 +252,15 @@ func main() {
}()
}
// keep track of handlers and wait for a signal
// Wait for a signal.
<-sigChan
log.Println("stopping snowflake")
// signal received, shut down
// Signal received, shut down.
for _, ln := range listeners {
ln.Close()
}
snowflakes.End()
close(shutdown)
wg.Wait()
log.Println("snowflake is done.")
}

View file

@ -1,10 +1,8 @@
UseBridges 1
DataDirectory datadir
ClientTransportPlugin snowflake exec ./client \
-url https://snowflake-broker.azureedge.net/ \
-front ajax.aspnetcdn.com \
-ice stun:stun.l.google.com:19302 \
-max 3
ClientTransportPlugin snowflake exec ./client -log snowflake.log
Bridge snowflake 0.0.3.0:1
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

View file

@ -1,7 +0,0 @@
UseBridges 1
DataDirectory datadir
ClientTransportPlugin snowflake exec ./client \
-url http://localhost:8080/ \
Bridge snowflake 0.0.3.0:1

6
client/torrc.localhost Normal file
View file

@ -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/

136
common/amp/armor_decoder.go Normal file
View file

@ -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 </pre> 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 serverclient
// 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
}
}

176
common/amp/armor_encoder.go Normal file
View file

@ -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 = `<!doctype html>
<html amp>
<head>
<meta charset="utf-8">
<script async src="https://cdn.ampproject.org/v0.js"></script>
<link rel="canonical" href="#">
<meta name="viewport" content="width=device-width">
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
</head>
<body>
`
boilerplateEnd = `</body>
</html>`
)
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 serverclient 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("<pre>\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("</pre>\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("</pre>\n"))
} else {
_, err = enc.w.Write([]byte("\n</pre>\n"))
}
}
return err
}

227
common/amp/armor_test.go Normal file
View file

@ -0,0 +1,227 @@
package amp
import (
"io"
"io/ioutil"
"math/rand"
"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
}{
{`
<pre>
0
</pre>
`,
"",
false,
},
{`
<pre>
0aGVsbG8gd29ybGQK
</pre>
`,
"hello world\n",
false,
},
// bad version indicator
{`
<pre>
1aGVsbG8gd29ybGQK
</pre>
`,
"",
true,
},
// text outside <pre> elements
{`
0aGVsbG8gd29ybGQK
blah blah blah
<pre>
0aGVsbG8gd29ybGQK
</pre>
0aGVsbG8gd29ybGQK
blah blah blah
`,
"hello world\n",
false,
},
{`
<pre>
0QUJDREV
GR0hJSkt
MTU5PUFF
SU1RVVld
</pre>
junk
<pre>
YWVowMTI
zNDU2Nzg
5Cg
=
</pre>
<pre>
=
</pre>
`,
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n",
false,
},
// no <pre> elements, hence no version indicator
{`
aGVsbG8gd29ybGQK
blah blah blah
aGVsbG8gd29ybGQK
aGVsbG8gd29ybGQK
blah blah blah
`,
"",
true,
},
// empty <pre> elements, hence no version indicator
{`
aGVsbG8gd29ybGQK
blah blah blah
<pre> </pre>
aGVsbG8gd29ybGQK
aGVsbG8gd29ybGQK<pre></pre>
blah blah blah
`,
"",
true,
},
// other elements inside <pre>
{
"blah <pre>0aGVsb<p>G8gd29</p>ybGQK</pre>",
"hello world\n",
false,
},
// HTML comment
{
"blah <!-- <pre>aGVsbG8gd29ybGQK</pre> -->",
"",
true,
},
// all kinds of ASCII whitespace
{
"blah <pre>\x200\x09aG\x0aV\x0csb\x0dG8\x20gd29ybGQK</pre>",
"hello world\n",
false,
},
// bad padding
{`
<pre>
0QUJDREV
GR0hJSkt
MTU5PUFF
SU1RVVld
</pre>
junk
<pre>
YWVowMTI
zNDU2Nzg
5Cg
=
</pre>
`,
"",
true,
},
/*
// per-chunk base64
// test disabled because Go stdlib handles this incorrectly:
// https://github.com/golang/go/issues/31626
{
"<pre>QQ==</pre><pre>Qg==</pre>",
"",
true,
},
*/
// missing </pre>
{
"blah <pre></pre><pre>0aGVsbG8gd29ybGQK",
"",
true,
},
// nested <pre>
{
"blah <pre>0aGVsb<pre>G8gd29</pre>ybGQK</pre>",
"",
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
}
}
}

178
common/amp/cache.go Normal file
View file

@ -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
}

320
common/amp/cache_test.go Normal file
View file

@ -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
}
}
}

88
common/amp/doc.go Normal file
View file

@ -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>/<base64 of data>
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.":
<!doctype html>
<html amp>
<head>
<meta charset="utf-8">
<script async src="https://cdn.ampproject.org/v0.js"></script>
<link rel="canonical" href="#">
<meta name="viewport" content="width=device-width">
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
</head>
<body>
<pre>
0VGhpcyB3YXMgZW5jb2RlZCB3aXRoIEF
NUCBhcm1vci4=
</pre>
</body>
</html>
*/
package amp

44
common/amp/path.go Normal file
View file

@ -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)
}
}

54
common/amp/path_test.go Normal file
View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}
}
}

39
common/event/bus.go Normal file
View file

@ -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
}

32
common/event/bus_test.go Normal file
View file

@ -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)
}

85
common/event/interface.go Normal file
View file

@ -0,0 +1,85 @@
package event
import (
"fmt"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/safelog"
"github.com/pion/webrtc/v3"
)
type SnowflakeEvent interface {
IsSnowflakeEvent()
String() string
}
type EventOnOfferCreated struct {
SnowflakeEvent
WebRTCLocalDescription *webrtc.SessionDescription
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
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)
}

55
common/ipsetsink/sink.go Normal file
View file

@ -0,0 +1,55 @@
package ipsetsink
import (
"bytes"
"crypto/hmac"
"encoding/binary"
"hash"
"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(truncatedHash64FromBytes{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 truncatedHash64FromBytes struct {
hashValue
}
func (c truncatedHash64FromBytes) Sum64() uint64 {
var value uint64
binary.Read(bytes.NewReader(c.hashValue), binary.BigEndian, &value)
return value
}

View file

@ -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)
}
})
}

View file

@ -0,0 +1,24 @@
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 {
RecordingStart time.Time `json:"recordingStart"`
RecordingEnd time.Time `json:"recordingEnd"`
Recorded []byte `json:"recorded"`
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
})
}

151
common/messages/client.go Normal file
View file

@ -0,0 +1,151 @@
//Package for communication with the snowflake broker
//import "git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
package messages
import (
"bytes"
"encoding/json"
"fmt"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/bridgefingerprint"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
)
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
<message> := <version>\n<body>
<version> := <digit>.<digit>
<body> := <poll request>|<poll response>
There are two different types of body messages,
each encoded in JSON format
== ClientPollRequest ==
<poll request> :=
{
offer: <sdp offer>
[nat: (unknown|restricted|unrestricted)]
[fingerprint: <fingerprint string>]
}
The NAT field is optional, and if it is missing a
value of "unknown" will be assumed. The fingerprint
is also optional and, if absent, will be assigned the
fingerprint of the default bridge.
== ClientPollResponse ==
<poll response> :=
{
[answer: <sdp answer>]
[error: <error string>]
}
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.
*/
// 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"`
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
}
return append([]byte(ClientVersion+"\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
if string(parts[0]) != ClientVersion {
return nil, fmt.Errorf("unsupported message version")
}
err := json.Unmarshal(parts[1], &message)
if err != nil {
return nil, err
}
if message.Offer == "" {
return nil, fmt.Errorf("no supplied offer")
}
if message.Fingerprint == "" {
message.Fingerprint = defaultBridgeFingerprint
}
if _, err := bridgefingerprint.FingerprintFromHexString(message.Fingerprint); err != nil {
return nil, fmt.Errorf("cannot decode fingerprint")
}
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
}
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
}

19
common/messages/ipc.go Normal file
View file

@ -0,0 +1,19 @@
package messages
import (
"errors"
)
type Arg struct {
Body []byte
RemoteAddr string
}
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"
)

View file

@ -0,0 +1,472 @@
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
proxyType string
natType string
clients int
data string
err error
acceptedRelayPattern string
}{
{
//Version 1.0 proxy message
sid: "ymbcCMto7KHNGYlp",
proxyType: "unknown",
natType: "unknown",
clients: 0,
data: `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.0"}`,
err: nil,
},
{
//Version 1.1 proxy message
sid: "ymbcCMto7KHNGYlp",
proxyType: "standalone",
natType: "unknown",
clients: 0,
data: `{"Sid":"ymbcCMto7KHNGYlp","Version":"1.1","Type":"standalone"}`,
err: nil,
},
{
//Version 1.2 proxy message
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
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:
sid: "",
proxyType: "",
natType: "",
clients: 0,
data: "",
err: &json.SyntaxError{},
},
{
sid: "",
proxyType: "",
natType: "",
clients: 0,
data: `{"Sid":"ymbcCMto7KHNGYlp"}`,
err: fmt.Errorf(""),
},
{
sid: "",
proxyType: "",
natType: "",
clients: 0,
data: "{}",
err: fmt.Errorf(""),
},
{
sid: "",
proxyType: "",
natType: "",
clients: 0,
data: `{"Version":"1.0"}`,
err: fmt.Errorf(""),
},
{
sid: "",
proxyType: "",
natType: "",
clients: 0,
data: `{"Version":"2.0"}`,
err: fmt.Errorf(""),
},
} {
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)
}
})
}
func TestEncodeProxyPollRequests(t *testing.T) {
Convey("Context", t, func() {
b, err := EncodeProxyPollRequest("ymbcCMto7KHNGYlp", "standalone", "unknown", 16)
So(err, ShouldEqual, nil)
sid, proxyType, natType, clients, err := DecodeProxyPollRequest(b)
So(sid, ShouldEqual, "ymbcCMto7KHNGYlp")
So(proxyType, ShouldEqual, "standalone")
So(natType, ShouldEqual, "unknown")
So(clients, ShouldEqual, 16)
So(err, ShouldEqual, nil)
})
}
func TestDecodeProxyPollResponse(t *testing.T) {
Convey("Context", t, func() {
for _, test := range []struct {
offer string
data string
relayURL string
err error
}{
{
offer: "fake offer",
data: `{"Status":"client match","Offer":"fake offer","NAT":"unknown"}`,
err: 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,
},
{
offer: "",
data: `{"Status":"no match"}`,
err: nil,
},
{
offer: "",
data: `{"Status":"client match"}`,
err: fmt.Errorf("no supplied offer"),
},
{
offer: "",
data: `{"Test":"test"}`,
err: fmt.Errorf(""),
},
} {
offer, _, relayURL, err := DecodePollResponseWithRelayURL([]byte(test.data))
So(err, ShouldHaveSameTypeAs, test.err)
So(offer, ShouldResemble, test.offer)
So(relayURL, ShouldResemble, test.relayURL)
}
})
}
func TestEncodeProxyPollResponse(t *testing.T) {
Convey("Context", t, func() {
b, err := EncodePollResponse("fake offer", true, "restricted")
So(err, ShouldEqual, nil)
offer, natType, err := DecodePollResponse(b)
So(offer, ShouldEqual, "fake offer")
So(natType, ShouldEqual, "restricted")
So(err, ShouldEqual, nil)
b, err = EncodePollResponse("", false, "unknown")
So(err, ShouldEqual, nil)
offer, natType, err = DecodePollResponse(b)
So(offer, ShouldEqual, "")
So(natType, ShouldEqual, "unknown")
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 {
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)
b, err = EncodeAnswerResponse(false)
So(err, ShouldEqual, nil)
success, err = DecodeAnswerResponse(b)
So(success, ShouldEqual, false)
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",
`1.0
{"nat":"unknown","offer":"fake"}`,
nil,
},
{
//version 1.0 client message
"unknown",
"fake",
`1.0
{"offer":"fake"}`,
nil,
},
{
//unknown version
"",
"",
`{"version":"2.0"}`,
fmt.Errorf(""),
},
{
//no offer
"",
"",
`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)
}
}
})
}
func TestEncodeClientPollRequests(t *testing.T) {
Convey("Context", t, func() {
for i, test := range []struct {
natType string
offer string
fingerprint string
err error
}{
{
"unknown",
"fake",
"",
nil,
},
{
"unknown",
"fake",
defaultBridgeFingerprint,
nil,
},
{
"unknown",
"fake",
"123123",
fmt.Errorf(""),
},
} {
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)
}
}
})
}
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)
})
}

310
common/messages/proxy.go Normal file
View file

@ -0,0 +1,310 @@
//Package for communication with the snowflake broker
//import "git.torproject.org/pluggable-transports/snowflake.git/v2/common/messages"
package messages
import (
"encoding/json"
"errors"
"fmt"
"strings"
"git.torproject.org/pluggable-transports/snowflake.git/v2/common/nat"
)
const (
version = "1.3"
ProxyUnknown = "unknown"
)
var KnownProxyTypes = map[string]bool{
"standalone": true,
"webext": true,
"badge": true,
"iptproxy": true,
}
/* Version 1.3 specification:
== ProxyPollRequest ==
{
Sid: [generated session id of proxy],
Version: 1.3,
Type: ["badge"|"webext"|"standalone"],
NAT: ["unknown"|"restricted"|"unrestricted"],
Clients: [number of current clients, rounded down to multiples of 8],
AcceptedRelayPattern: [a pattern representing accepted set of relay domains]
}
== ProxyPollResponse ==
1) If a client is matched:
HTTP 200 OK
{
Status: "client match",
{
type: offer,
sdp: [WebRTC SDP]
},
NAT: ["unknown"|"restricted"|"unrestricted"],
RelayURL: [the WebSocket URL proxy should connect to relay Snowflake traffic]
}
2) If a client is not matched:
HTTP 200 OK
{
Status: "no match"
}
3) If the request is malformed:
HTTP 400 BadRequest
== ProxyAnswerRequest ==
{
Sid: [generated session id of proxy],
Version: 1.3,
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
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,
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 DecodeProxyPollRequestWithRelayPrefix(data []byte) (
sid string, proxyType string, natType string, clients int, relayPrefix string, relayPrefixAware bool, err error) {
var message ProxyPollRequest
err = json.Unmarshal(data, &message)
if err != nil {
return
}
majorVersion := strings.Split(message.Version, ".")[0]
if majorVersion != "1" {
err = fmt.Errorf("using unknown version")
return
}
// Version 1.x requires an Sid
if message.Sid == "" {
err = fmt.Errorf("no supplied session id")
return
}
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
}
// we don't reject polls with an unknown proxy type because we encourage
// projects that embed proxy code to include their own type
if !KnownProxyTypes[message.Type] {
message.Type = ProxyUnknown
}
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 {
Status string
Offer string
NAT string
RelayURL string
}
func EncodePollResponse(offer string, success bool, natType string) ([]byte, error) {
return EncodePollResponseWithRelayURL(offer, success, natType, "", "no match")
}
func EncodePollResponseWithRelayURL(offer string, success bool, natType, relayURL, failReason string) ([]byte, error) {
if success {
return json.Marshal(ProxyPollResponse{
Status: "client match",
Offer: offer,
NAT: natType,
RelayURL: relayURL,
})
}
return json.Marshal(ProxyPollResponse{
Status: failReason,
})
}
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 DecodePollResponseWithRelayURL(data []byte) (string, string, string, error) {
var message ProxyPollResponse
err := json.Unmarshal(data, &message)
if err != nil {
return "", "", "", err
}
if message.Status == "" {
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
if natType == "" {
natType = "unknown"
}
return message.Offer, natType, message.RelayURL, err
}
type ProxyAnswerRequest struct {
Version string
Sid string
Answer string
}
func EncodeAnswerRequest(answer string, sid string) ([]byte, error) {
return json.Marshal(ProxyAnswerRequest{
Version: version,
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
}
majorVersion := strings.Split(message.Version, ".")[0]
if majorVersion != "1" {
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
}

View file

@ -0,0 +1,31 @@
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
}
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)
}

View file

@ -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)
})
})
}
}

231
common/nat/nat.go Normal file
View file

@ -0,0 +1,231 @@
/*
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"
"log"
"net"
"time"
"github.com/pion/stun"
)
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) {
return isRestrictedMapping(server)
}
// 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 {
return false, fmt.Errorf("Error creating STUN connection: %w", 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 != nil {
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 {
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 {
return false, fmt.Errorf("NAT discovery feature not supported: %w", err)
}
if err = mapTestConn.AddOtherAddr(otherAddr.String()); err != nil {
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 != nil {
return false, fmt.Errorf("Error retrieveing server response: %w", err)
}
// Decoding XOR-MAPPED-ADDRESS attribute from message.
if err = xorAddr2.GetFrom(resp); err != nil {
return false, fmt.Errorf("Error retrieving XOR-MAPPED-ADDRESS resonse: %w", 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
// 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
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
}

View file

@ -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 (
@ -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
}

116
common/task/periodic.go Normal file
View file

@ -0,0 +1,116 @@
// 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 (
"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
}
func (t *Periodic) WaitThenStart() {
time.AfterFunc(t.Interval, func() {
t.Start()
})
}
// 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
}

View file

@ -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[:]) }

View file

@ -0,0 +1,145 @@
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)
close(record.SendQueue)
return record
}

View file

@ -0,0 +1,17 @@
// 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"
// 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 = 2048
var errClosedPacketConn = errors.New("operation on closed connection")
var errNotImplemented = errors.New("not implemented")

View file

@ -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 }

View file

@ -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 }

99
common/util/util.go Normal file
View file

@ -0,0 +1,99 @@
package util
import (
"encoding/json"
"errors"
"net"
"github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
)
func SerializeSessionDescription(desc *webrtc.SessionDescription) (string, error) {
bytes, err := json.Marshal(*desc)
if err != nil {
return "", err
}
return string(bytes), nil
}
func DeserializeSessionDescription(msg string) (*webrtc.SessionDescription, error) {
var parsed map[string]interface{}
err := json.Unmarshal([]byte(msg), &parsed)
if err != nil {
return nil, err
}
if _, ok := parsed["type"]; !ok {
return nil, errors.New("cannot deserialize SessionDescription without type field")
}
if _, ok := parsed["sdp"]; !ok {
return nil, errors.New("cannot deserialize SessionDescription without sdp field")
}
var stype webrtc.SDPType
switch parsed["type"].(string) {
default:
return nil, errors.New("Unknown SDP type")
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),
}, nil
}
// 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) ||
// 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
}
// 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() {
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
}
}
}
attrs = append(attrs, a)
}
m.Attributes = attrs
}
bts, err := desc.Marshal()
if err != nil {
return str
}
return string(bts)
}

28
common/util/util_test.go Normal file
View file

@ -0,0 +1,28 @@
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 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
"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)
})
}

View file

@ -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
}

238
common/utls/roundtripper.go Normal file
View file

@ -0,0 +1,238 @@
package utls
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
"sync"
"time"
utls "github.com/refraction-networking/utls"
"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{
clientHelloID: clientHelloID,
config: uTlsConfig,
connectWithH1: map[string]bool{},
backdropTransport: backdropTransport,
pendingConn: map[pendingConnKey]*unclaimedConnection{},
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]*unclaimedConnection
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")
var errExpired = errors.New("connection have expired")
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] = 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 {
delete(r.pendingConn, connId)
if claimedConnection, err := conn.claimConnection(); err == nil {
return claimedConnection
}
}
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)
},
}
}
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
}
}

View file

@ -0,0 +1,164 @@
package utls
import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"math/rand"
"net/http"
"testing"
"time"
stdcontext "context"
utls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
. "github.com/smartystreets/goconvey/convey"
)
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
// 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),
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(insecureRandReader, &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, false)
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)
}
})
}
})
}
cancel()
}

View file

@ -0,0 +1,120 @@
package websocketconn
import (
"io"
"time"
"github.com/gorilla/websocket"
)
// An abstraction that makes an underlying WebSocket connection look like a
// net.Conn.
type Conn struct {
*websocket.Conn
Reader io.Reader
Writer io.Writer
}
func (conn *Conn) Read(b []byte) (n int, err error) {
return conn.Reader.Read(b)
}
func (conn *Conn) Write(b []byte) (n int, err error) {
return conn.Writer.Write(b)
}
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()
}
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 {
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
}
}
}
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 {
// 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{
Conn: ws,
Reader: pr1,
Writer: pw2,
}
}

View file

@ -0,0 +1,263 @@
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)
}
}
// 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)
}
}

View file

@ -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("from", "", "")
end := flag.String("to", "", "")
flag.Parse()
startTime, err := time.Parse(time.RFC3339, *start)
if err != nil {
log.Fatal("unable to parse start time:", err)
}
endTime, err := time.Parse(time.RFC3339, *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)
}

View file

@ -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:
@ -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.]
@ -44,8 +62,212 @@ 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.
"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
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:
```
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.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 <pre>
elements. The <pre> elements are then surrounded by AMP boilerplate. To
decode, search the HTML for <pre> elements, concatenate their contents
and join on whitespace, discard the "0" prefix, and base64 decode.
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.3,
Type: ["badge"|"webext"|"standalone"|"mobile"],
NAT: ["unknown"|"restricted"|"unrestricted"],
Clients: [number of current clients, rounded down to multiples of 8],
AcceptedRelayPattern: [a pattern representing accepted set of relay domains]
}
```
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]
},
RelayURL: [the WebSocket URL proxy should connect to relay Snowflake traffic]
}
```
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.3,
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
```

50
doc/snowflake-client.1 Normal file
View file

@ -0,0 +1,50 @@
.TH SNOWFLAKE-CLIENT "1" "July 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\-ampcache\fR string
.IP
URL of AMP cache to use as a proxy for signaling
.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

38
doc/snowflake-proxy.1 Normal file
View file

@ -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.torproject.net/")
.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.torproject.net/")
.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

View file

@ -0,0 +1,164 @@
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/v2/client/lib"
)
func main() {
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)
}
// 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()
// ...
}
```
#### 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/v2/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.
Example usage:
```Golang
package main
import (
"log"
"net"
sf "git.torproject.org/pluggable-transports/snowflake.git/v2/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).

24
go.mod Normal file
View file

@ -0,0 +1,24 @@
module git.torproject.org/pluggable-transports/snowflake.git/v2
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
github.com/pion/stun v0.3.5
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
github.com/smartystreets/goconvey v1.6.4
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-20220516162934-403b01795ae8
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
google.golang.org/protobuf v1.26.0
)

558
go.sum Normal file
View file

@ -0,0 +1,558 @@
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/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=
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/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/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=
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/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/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=
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/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.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=
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/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.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=
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/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=
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/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.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.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=
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.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
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.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.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.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=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
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.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=
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/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=
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/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/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=
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.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.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.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=
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/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-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/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=
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/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=
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-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-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-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/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/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=
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-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=
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/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-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-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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/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=
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-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/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=
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/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=
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/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/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/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=
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=

3
probetest/Dockerfile Normal file
View file

@ -0,0 +1,3 @@
FROM golang:1.13
COPY probetest /go/bin

53
probetest/README.md Normal file
View file

@ -0,0 +1,53 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Overview](#overview)
- [Running your own](#running-your-own)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
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
```

View file

@ -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"]

238
probetest/probetest.go Normal file
View file

@ -0,0 +1,238 @@
/*
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/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"
)
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) {
dc.OnOpen(func() {
close(dataChan)
})
dc.OnClose(func() {
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 {
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
}
// Wait for ICE candidate gathering to complete
<-done
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, "stub-sid")
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.
go func() {
timer := time.NewTimer(dataChannelTimeout)
defer timer.Stop()
select {
case <-dataChan:
case <-timer.C:
}
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)
}
}

View file

@ -1,3 +0,0 @@
This is a standalone (not browser-based) version of the Snowflake proxy.
Usage: ./proxy-go

View file

@ -1,482 +0,0 @@
package main
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"sync"
"time"
"git.torproject.org/pluggable-transports/snowflake.git/common/safelog"
"github.com/pion/webrtc"
"golang.org/x/net/websocket"
)
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
//amount of time after sending an SDP answer before the proxy assumes the
//client is not going to connect
const dataChannelTimeout = 20 * time.Second
const readLimit = 100000 //Maximum number of bytes to be read from an HTTP request
var brokerURL *url.URL
var relayURL string
const (
sessionIDLength = 16
)
var (
tokens chan bool
config webrtc.Configuration
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)`),
}
// 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])
}
}
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
}
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.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 getToken() {
<-tokens
}
func retToken() {
tokens <- true
}
func genSessionID() string {
buf := make([]byte, sessionIDLength)
_, err := rand.Read(buf)
if err != nil {
panic(err.Error())
}
return strings.TrimRight(base64.StdEncoding.EncodeToString(buf), "=")
}
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
}
func pollOffer(sid string) *webrtc.SessionDescription {
broker := brokerURL.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
}
req, _ := http.NewRequest("POST", broker.String(), bytes.NewBuffer([]byte(sid)))
req.Header.Set("X-Session-ID", sid)
resp, err := client.Do(req)
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 {
return deserializeSessionDescription(string(body))
}
}
}
}
}
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)
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("broker returned %d", resp.StatusCode)
}
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
// RemoteAddr). https://bugs.torproject.org/18628#comment:8
func datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) {
defer conn.Close()
defer retToken()
u, err := url.Parse(relayURL)
if err != nil {
log.Fatalf("invalid relay url: %s", err)
}
// Retrieve client IP address
if remoteAddr != nil {
// Encode client IP address in relay URL
q := u.Query()
clientIP := remoteAddr.String()
q.Set("client_ip", clientIP)
u.RawQuery = q.Encode()
} else {
log.Printf("no remote address given in websocket")
}
wsConn, err := websocket.Dial(u.String(), "", relayURL)
if err != nil {
log.Printf("error dialing relay: %s", err)
return
}
log.Printf("connected to relay")
defer wsConn.Close()
wsConn.PayloadType = websocket.BinaryFrame
CopyLoop(conn, wsConn)
log.Printf("datachannelHandler ends")
}
// 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, dataChan chan struct{}) (*webrtc.PeerConnection, error) {
pc, err := webrtc.NewPeerConnection(config)
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}
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) {
log.Printf("OnMessage <--- %d bytes", len(msg.Data))
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 datachannelHandler(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)
}
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
}
func runSession(sid string) {
offer := pollOffer(sid)
if offer == nil {
log.Printf("bad offer from broker")
retToken()
return
}
dataChan := make(chan struct{})
pc, err := makePeerConnectionFromOffer(offer, config, dataChan)
if err != nil {
log.Printf("error making WebRTC connection: %s", err)
retToken()
return
}
err = sendAnswer(sid, pc)
if err != nil {
log.Printf("error sending answer to client through broker: %s", err)
if inerr := pc.Close(); inerr != nil {
log.Printf("error calling pc.Close: %v", inerr)
}
retToken()
return
}
// 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:
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)
}
retToken()
}
}
func main() {
var capacity uint
var stunURL string
var logFilename string
var rawBrokerURL string
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.Parse()
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)
}
//We want to send the log output through our scrubber first
log.SetOutput(&safelog.LogScrubber{Output: logOutput})
log.Println("starting")
var err error
brokerURL, err = url.Parse(rawBrokerURL)
if err != nil {
log.Fatalf("invalid broker url: %s", err)
}
_, err = url.Parse(stunURL)
if err != nil {
log.Fatalf("invalid stun url: %s", err)
}
_, err = url.Parse(relayURL)
if err != nil {
log.Fatalf("invalid relay url: %s", err)
}
config = webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{stunURL},
},
},
}
tokens = make(chan bool, capacity)
for i := uint(0); i < capacity; i++ {
tokens <- true
}
for {
getToken()
sessionID := genSessionID()
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
}
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)
}

View file

@ -1,109 +0,0 @@
package main
import (
"net"
"strings"
"testing"
)
func TestRemoteIPFromSDP(t *testing.T) {
tests := []struct {
sdp string
expected net.IP
}{
// https://tools.ietf.org/html/rfc4566#section-5
{`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 224.2.17.12/127
t=2873397496 2873404696
a=recvonly
m=audio 49170 RTP/AVP 0
m=video 51372 RTP/AVP 99
a=rtpmap:99 h263-1998/90000
`, net.ParseIP("224.2.17.12")},
// Missing c= line
{`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)
t=2873397496 2873404696
a=recvonly
m=audio 49170 RTP/AVP 0
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
`, net.ParseIP("224.2.1.1")},
// Same, with TTL
{`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
`, net.ParseIP("224.2.1.1")},
// IPv6, address only
{`c=IN IP6 FF15::101
`, net.ParseIP("ff15::101")},
// Same, with multicast addresses
{`c=IN IP6 FF15::101/3
`, net.ParseIP("ff15::101")},
// Multiple c= lines
{`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.
{`v=0
o=- 7860378660295630295 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 54653 DTLS/SCTP 5000
c=IN IP4 1.2.3.4
a=candidate:3581707038 1 udp 2122260223 192.168.0.1 54653 typ host generation 0 network-id 1 network-cost 50
a=candidate:2617212910 1 tcp 1518280447 192.168.0.1 59673 typ host tcptype passive generation 0 network-id 1 network-cost 50
a=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
a=ice-ufrag:IBdf
a=ice-pwd:G3lTrrC9gmhQx481AowtkhYz
a=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
a=setup:actpass
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
`, nil},
// Improper character within IPv6
{`c=IN IP6 ff15:g::101
`, nil},
// Bogus "IP7" addrtype
{`c=IN IP7 1.2.3.4
`, nil},
}
for _, test := range tests {
// 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.
lfSDP := test.sdp
crlfSDP := strings.Replace(lfSDP, "\n", "\r\n", -1)
ip := remoteIPFromSDP(lfSDP)
if !ip.Equal(test.expected) {
t.Errorf("expected %q, got %q from %q", test.expected, ip, lfSDP)
}
ip = remoteIPFromSDP(crlfSDP)
if !ip.Equal(test.expected) {
t.Errorf("expected %q, got %q from %q", test.expected, ip, crlfSDP)
}
}
}

View file

@ -1,7 +0,0 @@
build/
test/
webext/snowflake.js
# FIXME: Whittle these away
spec/
shims.js

View file

@ -1,13 +0,0 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"rules": {
"indent": ["error", 2, {
"SwitchCase": 1,
"MemberExpression": 0
}]
}
}

View file

@ -1,75 +1,48 @@
This is the browser proxy component of Snowflake.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
### Embedding
- [Dependencies](#dependencies)
- [Building the standalone Snowflake proxy](#building-the-standalone-snowflake-proxy)
- [Running a standalone Snowflake proxy](#running-a-standalone-snowflake-proxy)
See https://snowflake.torproject.org/ for more info:
```
<iframe src="https://snowflake.torproject.org/embed.html" width="88" height="16" frameborder="0" scrolling="no"></iframe>
```
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### Building
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:
```
npm run build
go get
go build
```
### Testing
### Running a standalone Snowflake proxy
Unit testing with Jasmine are available with:
The Snowflake proxy can be run with the following options:
```
npm install
npm test
Usage of ./proxy:
-broker string
broker URL (default "https://snowflake-broker.torproject.net/")
-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.torproject.net/")
-stun string
stun URL (default "stun:stun.stunprotocol.org:3478")
-unsafe-logging
prevent logs from being scrubbed
```
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 <your user name>
ProxyJump people.torproject.org
IdentityFile ~/.ssh/tor
```
### Deploying
```
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 "<dirname>/": 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/`.
For more information on how to run a Snowflake proxy in deployment, see our [community documentation](https://community.torproject.org/relay/setup/snowflake/standalone/).

View file

@ -1,122 +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(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)) {
// 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 += '/';
}
}
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.STATUS.OK:
return fulfill(xhr.responseText); // Should contain offer.
case Broker.STATUS.GATEWAY_TIMEOUT:
return reject(Broker.MESSAGE.TIMEOUT);
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
return this._postRequest(id, xhr, 'proxy', id);
});
}
// 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.STATUS.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));
}
_postRequest(id, xhr, urlSuffix, payload) {
var err;
try {
xhr.open('POST', this.url + urlSuffix);
xhr.setRequestHeader('X-Session-ID', id);
} 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.STATUS = {
OK: 200,
GONE: 410,
GATEWAY_TIMEOUT: 504
};
Broker.MESSAGE = {
TIMEOUT: 'Timed out waiting for a client offer.',
UNEXPECTED: 'Unexpected status.'
};
Broker.prototype.clients = 0;

View file

@ -1,36 +0,0 @@
class Config {}
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.connectionsPerClient = 1;
// TODO: Different ICE servers.
Config.prototype.pcConfig = {
iceServers: [
{
urls: ['stun:stun.l.google.com:19302']
}
]
};

View file

@ -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;
if ('off' !== query.get('ratelimit')) {
config.rateLimitBytes = Params.getByteCount(query, 'ratelimit', config.rateLimitBytes);
}
broker = new Broker(config.brokerUrl);
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 &&
Snowflake.MODE.WEBRTC_READY === snowflake.state
) {
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();
});
}
}());

Some files were not shown because too many files have changed in this diff Show more