-
Notifications
You must be signed in to change notification settings - Fork 1
/
snowflake.tex
3326 lines (3140 loc) · 179 KB
/
snowflake.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\documentclass[letterpaper,twocolumn]{article}
\usepackage{bigdelim}
\usepackage{graphicx}
\usepackage[marginal]{footmisc}
\usepackage{makecell}
\usepackage{multirow}
\usepackage{pifont} % For checkmarks in tab:nat-matching.
\usepackage{titling}
\usepackage{url}
\usepackage[table]{xcolor}
\usepackage[textsize=footnotesize]{todonotes}\setlength{\marginparwidth}{1.5cm}
% Better look for citations that include a section reference like \cite[\S 3]{foobar}.
\usepackage{cite}
\renewcommand{\citemid}{~}
% Load hyperref after other packages.
\usepackage[pdfa,hidelinks,pdfusetitle,pdfcreator={},pdfproducer={}]{hyperref}
\urlstyle{same}
\def\sectionautorefname{Section}
\def\subsectionautorefname{Section}
\usepackage{usenix-2020-09}
% Disable metadata for reproducible PDF.
% https://tex.stackexchange.com/a/313605
\usepackage{ifpdf}
\ifpdf
\pdfinfoomitdate=1
\pdftrailerid{}
\pdfsuppressptexinfo=-1
\fi
% Change URL line breaking preferences.
% http://mirrors.ctan.org/macros/latex/contrib/url/url.pdf §5.2 Changing linebreaks
% The considerations here are:
% 1. We must be able to break the URLs for Nasr2020a and uproxy-design-doc,
% which have a path component that is longer than the line width but which
% contains hyphens.
% 2. The URL for Barradas2020a looks much better if it is broken after a slash,
% rather than after the hyphen in the hostname.
% 3. GitLab URLs in footnotes that end in #note_XXXXXXX look bad if broken
% immediately after the '#'.
% \UrlBreaks have a lower penalty than \UrlBigBreaks; i.e., TeX will prefer to
% break lines at \UrlBreaks if possible, and only break at \UrlBigBreaks if
% necessary. We prefer to break at slashes only (which takes care of
% considerations 2 and 3), but will break at a hyphen when there is no slash
% available (which does consideration 1).
\def\UrlBreaks{\do-}
\def\UrlBigBreaks{\do/}
\hyphenation{Web-RTC}
\hyphenation{Web-Exten-sion}
\hyphenation{Java-Script}
\hyphenation{uProxy}
\hyphenation{off-line}
\hyphenation{mac-OS}
\hyphenation{Mull-vad}
% Highlight the first usage or definition of a technical term.
% Like the firstterm element in DocBook: https://tdg.docbook.org/tdg/5.2/firstterm.html.
\newcommand{\firstterm}[1]{\textit{#1}}
% Use a smaller font size for footnotes that consist entirely of a URL, so URLs like
% https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/XXXXX
% fit in one line.
\setlength{\footnotemargin}{1.5ex}
\newlength{\urlfootnotesize}
\setlength{\urlfootnotesize}{6.5pt}
\newcommand{\urlfootnote}[1]{\footnote{
\raggedright\hangindent\footnotemargin%
\fontsize{\urlfootnotesize}{\urlfootnotesize}\selectfont%
\url{#1}
}}
\begin{document}
\date{}
\title{\Large\bf Snowflake, a censorship circumvention system\\using temporary WebRTC proxies\\\strut\large (Draft \today)}
\author{%
{\rm Cecylia Bocovich}%
\and
{\rm Arlo Breault}%
\and
{\rm David Fifield}%
\and
{\rm Serene}%
\and
{\rm Xiaokang Wang}%
}
\renewcommand{\maketitlehookc}{\centering\normalsize Authors are listed alphabetically.}
\maketitle
\begin{abstract}
Snowflake is a system for circumventing Internet censorship.
Its blocking resistance comes from
the use of numerous, ultra-light, temporary proxies (``snowflakes''),
which accept traffic from censored clients using peer-to-peer WebRTC protocols
and forward it to a centralized bridge.
The temporary proxies are simple enough to be implemented in JavaScript,
in~a web page or browser extension,
making them much cheaper to run than
a traditional proxy or VPN server.
The large and constantly changing pool
of proxy addresses resists enumeration and blocking by a censor.
The system is built on the assumption
that proxies may appear or disappear at any time:
clients discover live proxies dynamically
using a secure rendezvous protocol;
when an \mbox{in-use} proxy goes offline,
its client switches to another on the fly,
invisibly to upper network layers.
Snowflake has been deployed with success
in Tor Browser and Orbot for several years.
It has been a significant circumvention tool
during high-profile network disruptions,
including in Russia in~2021 and Iran in~2022.
In this paper, we explain the composition of Snowflake's many parts,
give a history of deployment and blocking attempts,
and reflect on implications for circumvention generally.
\end{abstract}
% General references:
% https://keroserene.net/snowflake/technical/#history
% https://www.bamsoftware.com/papers/thesis/#chap:snowflake
\section{Introduction}
\label{sec:intro}
Snowflake is a censorship circumvention system,
a~system to enable network communication
despite interference by a censor.
Its blocking resistance comes from a large pool
of low-cost, temporary proxies
that varies over time
and offers a censor no fixed target for blocking.
The core research purpose of this paper
is to investigate experimentally to what extent
a circumvention system that makes the tradeoffs Snowflake does
can be effective against contemporary censors.
We~will present the design of the system,
listing the many challenges of circumvention
and showing how Snowflake addresses them.
We~will explain how Snowflake solves the technical challenge
of providing a good user experience
when proxies are individually unreliable.
We~will document the reactions of national censors
through case studies in
Russia, Iran, China, and Turkmenistan
over more than three years of deployment.
On~the way we will provide quantitative evaluations
of various facets of the system,
including the number of clients served,
and the size and composition of the proxy pool.
Censorship circumvention systems may
be characterized on multiple axes.
Some systems imitate a common network protocol;
others try
not to look like any protocol in particular.
Some distribute connections over numerous proxy servers;
others concentrate on a single proxy
that is, for one reason or another, difficult for a censor to block.
What all circumvention systems have in common
is that they strive to increase the \emph{cost}
to the censor of blocking them---whether that cost be in
research and development, human resources, and hardware;
or in the inevitable overblocking that results
when a censor tries to selectively block
some connections but not others.
On~the spectrum of imitation to randomization,
Snowflake falls on the side of imitation;
on the scale of diffuse to concentrated, it is diffuse.
Snowflake's defining characteristic is that it
pushes the idea of distributed, disposable
proxies to an extreme.
WebRTC is a suite of protocols
intended for real-time communication applications
on the web~\cite{rfc8825}.
Video and voice chat are typical applications.
Snowflake exchanges WebRTC data formats
in the course of establishing a connection,
and uses WebRTC protocols to traverse of NAT (network address translation)
and to connect clients and proxies.
Crucially for Snowflake, WebRTC APIs
are available to JavaScript code in web browsers,
meaning it is possible to implement a proxy
in a web page or browser extension.
WebRTC is also usable outside a browser,
which is how we implement the Snowflake client program
and alternative, command line--based proxies.
As~is usual in circumvention research,
we assume a threat model in which
\firstterm{clients} reside in a network
controlled by a \firstterm{censor}.
The censor has the power to inspect and interfere with
traffic that crosses the border of its network;
typical real-world censor behaviors include
inspecting IP addresses and hostnames,
checking packet contents for keywords,
blocking IP addresses, and injecting false DNS responses
and TCP RST packets.
The client wants to communicate with some
\firstterm{destination} outside the censor's network,
possibly with the aid of third-party \firstterm{proxies}.
The censor is motivated to block the contents
of the client's communication, or the destination itself.
The censor knows of the possibility of circumvention,
and therefore seeks to block not only direct communication with the destination,
but also indirect communication by way of a proxy or circumvention system.
Circumvention is accomplished when the client
can reliably reach any proxy,
because a proxy, being outside the censor's control,
can then forward the client's communication to any destination.
(In Snowflake, we separate the roles of temporary \firstterm{proxies}
and a stable long-term \firstterm{bridge}, but the idea is the same.)
The censor derives benefit
from permitting some forms of network access:
it~cannot trivially ``win''
by shutting down all communication,
but must be selective in its blocking decisions,
in order to optimize some objective of its own.
The art of censorship circumvention is
forcing the censor into a dilemma
of~overblocking or underblocking,
by making circumvention traffic difficult to distinguish
from traffic that the censor prefers not to block.
Snowflake originates in two earlier projects:
flash proxy and uProxy.
% https://lists.torproject.org/pipermail/tor-dev/2016-January/010310.html "Snowflake is a webrtc pluggable transport inspired by flashproxy."
% https://keroserene.net/snowflake/technical/#1-introduction "It is inspired by and builds upon the previous work of Flashproxy. Snowflake is much like a hybrid of previous Pluggable Transports..."
Flash proxy~\cite{Fifield2012a}, like Snowflake, used
untrusted temporary JavaScript proxies in web browsers
forwarding to a central bridge,
but the link between client and proxy was WebSocket
rather than WebRTC,
which was then an emerging technology.
Flash proxy was deployed
% 2013: https://blog.torproject.org/combined-flash-proxy-pyobfsproxy-browser-bundles
% 2016: "Remove Flashproxy from Tor Browser" https://bugs.torproject.org/17428#note_2203210
from 2013 to~2016,
but never saw much use,
probably because WebSocket,
which lacks the built-in NAT traversal of WebRTC,
required clients to do complicated port forwarding.
% https://gitlab.torproject.org/legacy/trac/-/wikis/doc/PluggableTransports/FlashProxy/Howto
% https://serene.cx/snowflake/#note-flashproxy "...one could say that uProxy and flashproxy are the ancestors of snowflake."
uProxy~\cite{uproxy}, in one of its early incarnations,
% "in one of its earlier incarnations": uProxy pivoted from friend proxies to cloud servers in 2016:
% https://web.archive.org/web/20161211194847/https://blog.uproxy.org/2016/02/get-access-24x7-through-your-own-uproxy.html
% and added support for proxying through Tor in 2016:
% https://lists.torproject.org/pipermail/tor-dev/2016-September/011489.html
pioneered the use of WebRTC proxies for circumvention.
uProxy's proxies were browser-based,
but its trust and deployment model was different
from flash proxy's and Snowflake's.
Censored clients would arrange, out of band,
for an acquaintance outside the censor's network
to run a proxy in their browser~\cite{uproxy-design-doc}.
% uProxy v1.2.5 Design Doc: https://docs.google.com/document/d/1t_30vX7RcrEGuWwcg0Jub-HiNI0Ko3kBOyqXgrQN3Kw
% "uProxy depends on leveraging existing trust relationships to to find and use a proxy."
A~personal trust relationship was necessary to prevent misuse,
since browser proxies fetched destination content directly---meaning
client activity would be attributed to the proxy,
and the proxy might inspect the client's traffic.
Clients did not change proxies on the fly.
uProxy supported protocol obfuscation:
its packets could be transformed to resemble something
other than WebRTC.
% https://github.com/uProxy/uproxy-obfuscators
This was possible because of
uProxy's implementation as a privileged browser extension
with access to real sockets.
Because Snowflake uses ordinary unprivileged APIs,
its WebRTC can only look like WebRTC;
on the other hand, for the same reason,
Snowflake proxies are easier to deploy.
Like flash proxy, uProxy was active in the years
2013--2016.
% 2013: Serene's 30C3 lightning talk on uProxy https://events.ccc.de/congress/2013/wiki/Static:Lightning_Talks#Day_3
% 2013–2016 contributions in uProxy-p2p: https://github.com/UWNetworksLab/uProxy-p2p/graphs/contributors
Among existing circumvention systems,
the one that is most similar to Snowflake is MassBrowser~\cite{Nasr2020a},
% reading group summary of MassBrowser https://github.com/net4people/bbs/issues/32
which offers
proxying though volunteer proxies, called buddies.
MassBrowser's architecture is similar to Snowflake's:
there is a centralized component that coordinates
connections between clients and buddies,
which corresponds to a piece in Snowflake called the broker;
buddies are similar to our proxies.
The~trust model is in between Snowflake's and uProxy's.
Buddies preferentially operate as one-hop proxies, as in uProxy,
but are not limited to proxying only for trusted friends.
To~deter misuse, buddies specify a policy of
what categories of content they are willing to proxy.
The buddy software is
not constrained by a web browser environment,
and can, like uProxy, use protocol obfuscation
on the client--buddy link.
% V-D: "We also implement traffic obfuscation to protect MassBrowser's traffic
% against traffic analysis attacks. Particularly, we have built a custom
% implementation of the obfsproxy Tor pluggable transport tailored to work with
% our MassBrowser implementation."
% VI-A: "MassBrowser uses a custom protocol over TCP/UDP for the communications
% between Clients and Buddies."
Other circumvention systems have used WebRTC,
though without Snowflake's focus on numerous proxies.
Protozoa~\cite{Barradas2020a}
% reading group summary of Protozoa https://github.com/net4people/bbs/issues/55
and Stegozoa~\cite{Figueira2022a}
demonstrate point-to-point covert tunnel over WebRTC,
the former by directly replacing encoded media
with its own ciphertexts,
the latter using video steganography.
Significantly, where Snowflake now uses WebRTC data channels,
Protozoa and Stegozoa use WebRTC media streams,
which may have advantages for blocking resistance.
We will say more on this point in \autoref{sec:fingerprinting}.
TorKameleon~\cite{arxiv.2303.17544} is a WebRTC-based system
with the dual goals of resisting blocking
and complicating traffic correlation attacks.
It~uses a recent draft programming interface called
WebRTC Encoded Transforms
% "Editor's Draft" status as of 2023-12-12 https://w3c.github.io/webrtc-encoded-transform/
% https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Using_Encoded_Transforms
to support Protozoa-like
embedding of data within media streams,
without invasive browser modifications.
% Examples of Encoded Transforms in TorKameleon source code:
% https://github.com/AfonsoVilalonga/TorKameleon/blob/c6ef7116043dfd74701ddb62f38606e748faf84b/PT/WebRTC/Client/public/js/main.js#L225
% const transformStream = new TransformStream({
% https://github.com/AfonsoVilalonga/TorKameleon/blob/c6ef7116043dfd74701ddb62f38606e748faf84b/PT/WebRTC/Client/public/js/modulators/addFrame.js#L8
% if (encodedFrame instanceof RTCEncodedVideoFrame && enconding.length > 0) {
% Encoded Transforms (earlier called Insertable Streams) had been mentioned as a future possibility in Stegozoa:
% https://dl.acm.org/doi/pdf/10.1145/3488932.3517419#page=12
% Snowflake team's notes:
% 2023-02-24 "WebRTC Encoded Transform (or Insertable Streams) for media channels in Snowflake?" https://lists.torproject.org/pipermail/anti-censorship-team/2023-February/000284.html
% Very early versions of Lantern (circa 2014) used social network–based trusted proxies:
% https://web.archive.org/web/20140326223853/http://techpresident.com/news/wegov/24455/why-remarkably-similar-circumvention-tools-uproxy-and-lantern-are-not-overkill
% https://lists.torproject.org/pipermail/tor-dev/2014-March/006356.html "HOWTO use Lantern as a pluggable transport"
% https://web.archive.org/web/20130831160152/https://www.youtube.com/watch?v=aiPkCugE-RY
% https://web.archive.org/web/2oe_/http://wayback-fakeurl.archive.org/yt/aiPkCugE-RY
% But it wasn't WebRTC, so was less like Snowflake than uProxy was.
% I'll draw the line here, since even Tor bridges are "volunteer-operated" in a sense.
Our goal in this paper is to provide a realistic assessment of Snowflake,
neither to exaggerate its advantages,
nor disproportionately emphasize the limitations
of other systems.
Circumvention research is a cooperative enterprise,
and we recognize and support our colleagues who
design and maintain their own systems.
With Snowflake, we have tried to explore a different point in the design space,
and by this exploration widen the scope of effective circumvention techniques.
We~acknowledge that Snowflake will be a better choice in some
censorship environments and
worse in others; indeed,
one of the ideas we hope to convey
is that blocking resistance
can be meaningfully understood only in relation to particular censor
and its resources, costs, and motivations.
As~of February 2024, Snowflake supports an estimated 42,000 average concurrent users
% > library("tidyverse")
% > WANTED_FINGERPRINTS <- c(
% "7659DA0F96B156C322FBFF3ACCC9B9DC01C27C73" = "snowman",
% "5481936581E23D2D178105D44DB6915AB06BFB7F" = "snowflake-01",
% "91DA221A149007D0FD9E5515F5786C3DD07E4BB0" = "snowflake-02"
% )
% > read_csv("figures/users/userstats-bridge-transport-multi.csv") %>%
% filter(transport == "snowflake" & fingerprint %in% names(WANTED_FINGERPRINTS)) %>%
% filter(date < "2024-02-01") %>%
% mutate(users = users / (coverage / pmax(num_instances, coverage))) %>%
% group_by(date, transport) %>% summarize(users = sum(users, na.rm = TRUE), .groups = "drop") %>%
% tail()
% # A tibble: 6 × 3
% date transport users
% <date> <chr> <dbl>
% 1 2024-01-26 snowflake 41763.
% 2 2024-01-27 snowflake 43015.
% 3 2024-01-28 snowflake 43146.
% 4 2024-01-29 snowflake 42820.
% 5 2024-01-30 snowflake 41927.
% 6 2024-01-31 snowflake 42281.
at an average total transfer rate of 3.5~Gbit/s,
which works out to around 38~TB of circumvention traffic per day.
% > library("tidyverse")
% > WANTED_FINGERPRINTS <- c(
% "7659DA0F96B156C322FBFF3ACCC9B9DC01C27C73" = "snowman",
% "5481936581E23D2D178105D44DB6915AB06BFB7F" = "snowflake-01",
% "91DA221A149007D0FD9E5515F5786C3DD07E4BB0" = "snowflake-02"
% )
% > options(width = 200)
% > userstats <- read_csv("figures/users/userstats-bridge-transport-multi.csv") %>%
% filter(fingerprint %in% names(WANTED_FINGERPRINTS)) %>%
% mutate(users = users / (coverage / pmax(num_instances, coverage)))
% > bandwidth <- read_csv("figures/users/bandwidth-multi.csv") %>%
% filter(fingerprint %in% names(WANTED_FINGERPRINTS)) %>%
% filter(coverage > 0) %>%
% mutate(bytes = bytes / (coverage / pmax(num_instances, coverage))) %>%
% pivot_wider(id_cols = c(date, fingerprint), names_from = c(type), values_from = c(bytes)) %>%
% mutate(
% good_read = read - `dirreq-read`,
% good_write = write - `dirreq-write`,
% good_avg = (good_read + good_write) / 2,
% good_avg_bps = good_avg*8/86400,
% )
% > left_join(userstats, bandwidth, by = c("date", "fingerprint")) %>%
% # Subtract out the pro-rated fraction of non-snowflake transports (basically negligible).
% group_by(date, fingerprint) %>%
% mutate(across(c(read, write, `dirreq-read`, `dirreq-write`, good_read, good_write, good_avg), ~ .x * users / sum(users))) %>%
% ungroup() %>%
% filter(transport == "snowflake") %>%
% filter(date < "2024-02-01") %>%
% group_by(date) %>%
% summarize(
% date = last(date),
% across(c(read, write, `dirreq-read`, `dirreq-write`, good_read, good_write, good_avg, good_avg_bps), sum, na.rm = TRUE)
% ) %>%
% mutate(
% across(c(read, write, `dirreq-read`, `dirreq-write`, good_read, good_write, good_avg), scales::label_bytes(units = "auto_si", accuracy = 0.01)),
% across(c(good_avg_bps), scales::label_number(scale_cut = scales::cut_si("bit"), accuracy = 0.01)),
% ) %>%
% arrange(date) %>% tail()
% # A tibble: 6 × 9
% date read write `dirreq-read` `dirreq-write` good_read good_write good_avg good_avg_bps
% <date> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
% 1 2024-01-26 35.81 TB 35.68 TB 11.10 GB 221.14 GB 35.79 TB 35.46 TB 35.63 TB 3.30 Gbit
% 2 2024-01-27 37.01 TB 36.90 TB 11.34 GB 234.62 GB 37.00 TB 36.67 TB 36.84 TB 3.41 Gbit
% 3 2024-01-28 38.55 TB 38.46 TB 11.58 GB 251.62 GB 38.54 TB 38.20 TB 38.37 TB 3.55 Gbit
% 4 2024-01-29 38.15 TB 38.06 TB 11.67 GB 252.27 GB 38.14 TB 37.80 TB 37.97 TB 3.52 Gbit
% 5 2024-01-30 37.57 TB 37.48 TB 11.89 GB 253.14 GB 37.56 TB 37.23 TB 37.39 TB 3.46 Gbit
% 6 2024-01-31 38.31 TB 38.19 TB 10.87 GB 226.36 GB 38.30 TB 37.97 TB 38.13 TB 3.53 Gbit
\section{How it works}
\label{sec:mechanics}
\begin{figure*}[t]
\includegraphics{figures/architecture/architecture.jpg}
\caption{
Architecture of Snowflake.
The client contacts the broker through a special rendezvous channel with high blocking resistance.
The broker matches the client with one of the proxies that are currently polling.
The client and proxy connect to one another using WebRTC.
The proxy connects to the bridge,
then begins copying traffic in both directions.
If~the proxy disappears,
the client does another rendezvous
and resumes its session with a new proxy.
}
\label{fig:architecture}
\todo[inline]{Make this graphic depict STUN servers and indirect rendezvous.}
\end{figure*}
A~Snowflake proxy connection proceeds in three phases.
First, there is rendezvous, in which a client
indicates its need for circumvention service
and is matched with a temporary proxy.
Rendezvous is facilitated by a central server called the broker.
Then, there is connection establishment,
where the client and its proxy connect to each other
with WebRTC, using information exchanged during rendezvous.
Finally, there is data transfer,
where the proxy transports data
between the client and the bridge.
The bridge is responsible for directing the client's traffic
to its eventual destination
(in~our case, by feeding it into the Tor network).
\autoref{fig:architecture} illustrates the process.
These phases repeat as needed, as temporary proxies come and~go.
Proxy failure is not an abnormal condition---it~happens whenever
a proxy is running in a browser that is closed, for example.
A~client builds a circumvention session over
a sequence of proxies, switching to a new one
whenever the current one stops working.
State variables stored at the client and the bridge
let the session pick up where it left off.
The change of proxies is invisible to the applications using Snowflake
(except for a brief delay for another rendezvous).
The Snowflake client presents an abstraction of one uninterrupted connection.
It~does not avail a censor to block the broker or bridge,
because Snowflake clients never contact either one directly.
Clients reach the broker over an indirect rendezvous channel.
Access to the bridge is always mediated by a temporary proxy.
\subsection{Rendezvous}
\label{sec:rendezvous}
A~session begins with the client sending a rendezvous message to the broker.
An~ambient population of proxies
constantly polls the broker to check for clients in need of service.
The broker matches the client with an available proxy,
taking into account factors like NAT compatibility.
% NAT type is currently the only constraint:
% matchSnowflake https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/blob/9edaee65470a1483bbdbe984e5e15a885f1e95d2/broker/ipc.go#L236
% But protocol versions may become a consideration in the future:
% "Analysis of speed deficiency of Snowflake in China, 2023 Q1" https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/40251#note_2903271
The client's rendezvous message
% ClientPollRequest https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/blob/9edaee65470a1483bbdbe984e5e15a885f1e95d2/common/messages/client.go#L64
is a bundle of data that the broker will need in order to match the client with a proxy,
and the proxy will need in order to connect to the client.
The most important part of the rendezvous message is a
Session Description Protocol (SDP) \firstterm{offer}~\cite{rfc8839},
which contains the information needed for a WebRTC connection,
such as the client's external IP addresses
and cryptographic data to secure a later key exchange.
% Specifically, a certificate fingerprint: https://www.rfc-editor.org/rfc/rfc8122.html#section-5
The broker gives the client's offer to a currently polling proxy,
which sends back an SDP \firstterm{answer}
with its share of connection details.
The broker forwards the proxy's answer to the client,
and client and proxy then connect to one other directly.
In~WebRTC terms, this offer/\allowbreak answer exchange is called
``signaling''~\cite[\S 2.2]{rfc8825}, and here the broker acts as a signaling server.
To~gather the information for an SDP offer or answer,
clients and proxies communicate with third-party servers,
called STUN servers,
before contacting the broker.
We~will say more about how STUN is used in \autoref{sec:connection}.
Connecting to STUN servers is a normal part of WebRTC,
though there are fingerprinting considerations
that we cover in \autoref{sec:fingerprinting}.
Interaction with the broker uses a ``long-polling'' model,
depicted abstractly in \autoref{fig:rendezvous}.
Proxies poll the broker periodically,
making an ordinary HTTPS request.
The broker holds the connection open for a few seconds
to await a client rendezvous message.
If~none arrives, the broker sends a response that says ``no clients''
and the proxy goes to sleep until its next poll.
When a client does arrive,
the broker responds to the proxy's poll request
with the client's SDP offer.
The proxy re-connects to the broker to send back its SDP answer.
The broker sends the SDP answer to the client
and an acknowledgement to the proxy.
At~this point rendezvous is finished:
client and proxy have what they need to connect.
\begin{figure}
\includegraphics{figures/rendezvous/rendezvous}
\caption{
Information exchange in Snowflake rendezvous.
When the broker makes a match,
the proxy receives the client's SDP offer,
then re-connects to send back its SDP answer.
It~all happens during one round trip,
from the client's point of view.
Not shown is the indirect channel
the client must use to access the broker.
}
\label{fig:rendezvous}
\end{figure}
Proxies are free to connect to the broker directly,
because they are assumed to be uncensored.
But clients must use an indirect,
blocking-resistant channel,
because any direct connection with the broker
would be easily blocked by a censor.
What is needed, essentially,
is a miniature circumvention system to
bootstrap the full system.
But if clients have access to a bootstrap rendezvous method
that is good enough to reach the broker,
why is a more extensive circumvention system needed at all?
The answer is that the restricted scope of rendezvous
admits a wider range of solutions than general circumvention.
Techniques that would be too slow or expensive
for high-volume or interactive circumvention
may yet be suited to rendezvous,
because rendezvous happens infrequently
and transmits only small amounts of data.
The nice thing about rendezvous that it is modular and separable.
More than one method may be used,
and the methods need not have anything in common with the main system.
Anything that can be persuaded to convey a message
of about 1,500 bytes indirectly to the broker,
and return a response of about the same size,
may work as a Snowflake rendezvous module.
% "Broker: investigate non-domain-fronting secure client / proxy registrations" https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/25594
% "DNS-based rendezvous for Snowflake" https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/25874
% "Example: there is a chat bot (say, Telegram), that acts as a broker." https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/25594#note_2823395
% Flash proxy email rendezvous would not work for Snowflake, because unidirectional.
% https://gitweb.torproject.org/flashproxy.git/tree/flashproxy-reg-email
% https://gitweb.torproject.org/flashproxy.git/tree/facilitator/fp-registrar-email
Snowflake now supports three rendezvous methods:
\begin{description}
\item[Domain fronting]
In~this method, the client does an HTTPS exchange with the broker
through an intermediary web service such as a content delivery network (CDN),
setting the externally visible hostname
(the TLS Server Name Indication, or SNI~\cite[\S 3]{rfc6066})
to a ``front domain'' different from the broker's.
The CDN routes the HTTPS request to the broker according to the
the HTTP Host header, which, under TLS encryption,
reflects the actual hostname of the broker~\cite{Fifield2015a}.
A~censor cannot easily block domain-fronted rendezvous
without also blocking unrelated connections to the front domain,
which should be selected to have high value to the censor.
The well-known drawback of domain fronting
is the high cost of CDN bandwidth,
but this is not a big problem
when it is used only for rendezvous.
\item[AMP cache]
AMP is a framework for web pages written in a restricted dialect of HTML.
Part of the framework is a free-to-use
cache server~\cite{amp-cache}.
The cache fetches AMP-conformant pages on demand,
making it effectively a restricted sort of HTTP proxy.
We~have a module that encodes rendezvous messages to conform to AMP requirements,
allowing them to be exchanged with the broker via the AMP cache.\urlfootnote{
% "AMP cache rendezvous"
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/50
}
This rendezvous method is not easily blocked
without blocking the cache server as a whole.
It~still technically requires domain fronting,
because the AMP cache protocol normally exposes the
broker's hostname in the TLS SNI,
but it enlarges the set of usable intermediaries and front domains.
% "Snowflake rendezvous using Amazon SQS" https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/26151
% "Deploy new SQS rendezvous method" https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/40323
% https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/blob/38352b22ade217bd1372772b9cb69f8eff93e919/doc/rendezvous-with-sqs.md
\item[SQS (Simple Queue Service)]
Amazon SQS is a message queuing service designed for
communication between microservices.
Snowflake has the ability
(new at the time of this writing)
to use a message queue as a one-way communication channel.\urlfootnote{
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/214
}
Clients write into a public queue, and the broker reads from~it.
Rendezvous response messages are sent back through
dynamically created, single-use queues.
Communication is indirect via SQS servers.
\end{description}
Rendezvous is not unique to Snowflake.
Other examples of rendezvous are
the DEFIANCE Rendezvous Protocol~\cite[\S 3]{Lincoln2012a},
the facilitator interaction in flash proxy~\cite[\S 3]{Fifield2012a},
and the registration proxy in Conjure~\cite[\S 4.1]{Frolov2019b}.
A~key property of Snowflake and the mentioned systems
is that their blocking resistance does not rely on preshared secret information.
Whatever information is needed to establish a circumvention session
is obtained dynamically at runtime.
This is in contrast to other systems in which,
before making a connection,
the client must acquire some secret,
such as an IP address or password,
through an out-of-band channel---and
blocking resistance depends on
keeping that information secret.
A~corollary of the no-secret-information property
is that an adversary is
at no special disadvantage in attacking the system.
There is no out-of-band channel which real clients have access to
but the censor does not.
The censor may pose as a client,
download the software,
study its network connections---and
the system must maintain its blocking resistance despite this.
The disadvantage of a separate rendezvous step
is that it is one more thing to get right.
Both the main circumvention channel
and the rendezvous must resist blocking:
the combination is only as strong as the weaker of the~two.
\subsection{Peer-to-peer connection establishment}
\label{sec:connection}
Now the client and the proxy connect to each other directly.
Even in the absence of censorship,
making a direct connection between two Internet peers is not always easy,
because of NAT (network address translation) and firewalls.
Snowflake clients and proxies alike run in diverse networks
with varying NATs and ingress policies.
Fortunately for us,
WebRTC is designed with this use case in mind,
and has built-in support for traversing NAT, in~the form of
ICE (Interactive Connectivity Establishment)~\cite{rfc8445},
a procedure for testing candidate pairs of peer network addresses
to find one that works.
ICE~makes use of third-party
STUN (Session Traversal Utilities for NAT)~\cite{rfc8489}
servers that, among other things,
enable a host to learn its external IP addresses.
The first part of ICE took place at the beginning of rendezvous,
when the client and proxy contacted STUN servers to gather
external address candidates and included them in their respective
SDP offer and answer.
There is no guarantee that two hosts will be able to make
a connection using the facilities of STUN alone.
Some address mapping and
filtering setups are simply incompatible.
In~such a case,
ICE would normally fall back to using
TURN (Traversal Using Relays around NAT)~\cite{rfc8656},
a~kind of UDP proxy.
Such a fallback would be problematic for Snowflake,
because the TURN relays themselves
would become a target of blocking by the censor.
% "Configure TURN servers for the proxy and/or client" https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/25596
But Snowflake has an advantage most WebRTC applications do not.
Most WebRTC applications want to connect \emph{a particular} pair of peers,
whereas we are satisfied when a client can connect to \emph{any} proxy.
Snowflake clients and proxies self-measure their NAT type
and report it to the broker,
which takes NAT compatibility into account
and avoids cases that would require a fallback to TURN.
% "Investigate Snowflake proxy failures" https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/33666#note_2595319
% "Okay here's a summary of what I've found: ..."
We condense the possible combinations of NAT and firewall features
that impact
a Snowflake client or proxy's
ability to make a peer-to-peer connection
into the following well-known variations:
% https://datatracker.ietf.org/doc/html/rfc3489#section-5
\begin{description}
\item[Full cone]
The same internal IP--port pair always maps to the same external port.
Any remote host may send a packet to an internal IP address and port by sending a packet to the
mapped external port.
\item[Restricted cone]
Like full cone,
but incoming packets
are allowed only if
there has recently been an outgoing packet
to the same remote IP address.
\item[Port-restricted cone]
Like restricted cone,
but incoming packets are allowed only if
there has recently been an outgoing packet
to the same remote IP--port pair.
\item[Symmetric]
The external port depends on both
the internal IP--port pair and the remote IP--port pair.
Incoming packets are allowed only if
there has recently been an outgoing
packet to the same remote address.
\end{description}
\begin{table}
\definecolor{Ycolor}{Gray}{14}
\definecolor{ncolor}{Gray}{13}
\newcommand{\Y}{\cellcolor{Ycolor}\ding{51}}
\newcommand{\n}{\cellcolor{ncolor}--}
\newcommand{\rotlabel}[1]{\rotatebox{30}{#1}}
% \vphantom is to make the labels take up vertical space;
% \rlap is so they don't expand the horizontal size of table columns.
\newcommand{\rot}[1]{\vphantom{\rotlabel{#1}}\rotlabel{\rlap{#1}}}
\centering
% Make normal cells taller by default.
\renewcommand{\arraystretch}{1.25}
% Tighten line spacing in \makecell cells.
\renewcommand\cellset{\renewcommand\arraystretch{0.8}\setlength\extrarowheight{0pt}}
\begin{tabular}{@{}rcccccl@{\hspace{0.5ex}}l@{}}
& % empty
\rot{No NAT} &
\rot{Full cone} &
\rot{Restricted cone} &
\rot{Port-restricted cone} &
\rot{Symmetric} &
&
\\
No NAT & \Y & \Y & \Y & \Y & \Y & \multirow{3}{*}{\kern-\tabcolsep\kern0.5ex\footnotesize\(\left.\kern-\nulldelimiterspace\rule[-19pt]{0pt}{38pt}\right\}\)} & \multirow{3}{*}{\footnotesize\makecell[l]{unrestricted\\proxy}} \\
Full cone & \Y & \Y & \Y & \Y & \Y & \\
Restricted cone & \Y & \Y & \Y & \Y & \Y & \\
Port-restricted cone & \Y & \Y & \Y & \Y & \n & \multirow{2}{*}{\kern-\tabcolsep\kern0.5ex\footnotesize\(\left.\kern-\nulldelimiterspace\rule[-12pt]{0pt}{24pt}\right\}\)} & \multirow{2}{*}{\footnotesize\makecell[l]{restricted\\proxy}} \\
Symmetric & \Y & \Y & \Y & \n & \n & \\[\dimexpr 0.5ex - \dimexpr 0.5\dimexpr\arraystretch\normalbaselineskip]
% \hfill hack to make \upbracefill work with colortbl:
% https://tex.stackexchange.com/questions/202138/upbracefill-filling-entire-tabular-cell-and-package-colortbl#comment474604_202143
& \multicolumn{4}{@{~}c@{~}}{\footnotesize\def\hfill{\hskip 0pt plus 1filll}\upbracefill} & \clap{\footnotesize\def\hfill{\hskip 0pt plus 1filll}\upbracefill} & \\
& \multicolumn{4}{c}{\footnotesize\makecell{unrestricted\\client}} & \clap{\footnotesize\makecell{restricted\\client}} \\
\end{tabular}
\caption{
Pairwise compatibility of NAT variants,
using the facilities of STUN alone
(no fallback to TURN).
The incompatible cases are when one peer's NAT is symmetric
and the other's is symmetric or port-restricted cone.
Note the asymmetry in what NAT variants we consider ``restricted''
in client and proxy.
}
\label{tab:nat-matching}
\end{table}
\autoref{tab:nat-matching}
shows the pairwise compatibility of NAT variations.
As~the incompatible cases always involve a symmetric NAT,
we further simplify matching by categorizing the variations into the two types
\firstterm{unrestricted} (works with most other NATs) and
\firstterm{restricted} (works only with more permissive NATs).
Unrestricted proxies may be matched with any client;
restricted proxies may be matched only with unrestricted clients.
The broker prefers to match unrestricted clients with restricted proxies,
% https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/blob/9edaee65470a1483bbdbe984e5e15a885f1e95d2/broker/ipc.go#L236
in~order to conserve unrestricted proxies
for the clients that need them.
Symmetric NAT is always considered restricted,
but port-restricted cone NAT differs
depending on the peer:
for proxies it is restricted, but
for clients it is unrestricted.
The asymmetric categorization is an approximation
to help conserve unrestricted proxies
for clients with symmetric NATs.
Though it creates the potential for an incompatible match,
we believe this to be uncommon in practice.
In~case of a connection failure,
clients re-rendezvous and try again.
To self-assess their NAT type,
clients use the NAT behavior discovery feature of STUN~\cite{rfc5780}.
% "Use STUN to determine NAT behaviour of peers" https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/34129
% "Add utility to help user discover their NAT type" https://github.com/pion/stun/issues/8
Proxies cannot use the same technique,
because the necessary STUN features are not exposed
to JavaScript.
Instead,
we adapt a technique from MassBrowser~\cite[\S \mbox{V-A}]{Nasr2020a}:
we~run a centralized, always-on WebRTC testing peer
behind a simulated symmetric NAT.\urlfootnote{
% "Have a remote probe service to test snowflake proxy NAT compatability"
https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/40013
}
Proxies try connecting to this peer:
if~the connection succeeds, the proxy's type is unrestricted;
otherwise it is restricted.
Clients and proxies retest their NAT type periodically,
to account for potential changes in their local networking environment.
If~a client or proxy is unable to determine its NAT type for some reason,
it reports the type ``unknown,''
which the broker conservatively treats as if it were restricted.
\begin{figure}
\includegraphics{figures/proxies/proxy-nat-type}
\caption{
Proxy NAT types, in unique IP addresses per day.
The places in 2021 and 2022
where there is an increase in the ``unknown'' NAT type
and a decrease in the other types
were the result of operational problems with
NAT type testing.
% "Increase of 'unknown' NAT assignments by probetest since 2021-10-25" https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/40071
% "Move snowflake-broker to a systemd based setup" https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/40147
}
\label{fig:proxy-nat-type}
\end{figure}
\autoref{fig:proxy-nat-type}
shows that unrestricted proxies form
a relatively small fraction of the proxy population.
In~absolute terms, there are enough,
thanks in large part to the volunteers who run
the command-line
version of the Snowflake proxy
on networks unencumbered by NAT.
Though stable, long-term proxies go
somewhat against the ethos of Snowflake,
it~has proved useful, as a matter of practicality,
to~sacrifice a measure of address diversity
for better NAT compatibility in a common case.
We~can estimate how many tries it takes a client
to be matched with a proxy, on average,
by counting failed and successful rendezvous attempts at the broker,
under the assumption that clients repeat rendezvous attempts
until getting a match.
In~July 2023,
unrestricted clients almost always got a match on the first attempt,
while restricted clients needed an average of
1.07 attempts (standard deviation 0.05).
% > library(tidyverse)
% > read_csv("figures/proxies/client-match.csv") %>%
% filter(date < "2024-02-01") %>%
% group_by(month = format(date, "%Y-%m")) %>%
% summarize(
% unrestricted_n = mean((matched_count + unrestricted_denied_count) / matched_count),
% unrestricted_sd = sd((matched_count + unrestricted_denied_count) / matched_count),
% restricted_n = mean((matched_count + restricted_denied_count) / matched_count),
% restricted_sd = sd((matched_count + restricted_denied_count) / matched_count),
% .groups = "drop"
% ) %>% tail(10)
% # A tibble: 10 × 5
% month unrestricted_n unrestricted_sd restricted_n restricted_sd
% <chr> <dbl> <dbl> <dbl> <dbl>
% 1 2023-04 1 0 1.15 0.152
% 2 2023-05 1.00 0.00204 1.23 0.113
% 3 2023-06 1.00 0.0000834 1.23 0.0734
% 4 2023-07 1 0 1.07 0.0498
% 5 2023-08 1 0 1.02 0.0568
% 6 2023-09 1 0 1.00 0.0148
% 7 2023-10 1.00 0.000000552 1.01 0.0137
% 8 2023-11 1 0 1.00 0.00499
% 9 2023-12 1 0 1.00 0.0121
% 10 2024-01 1 0 1.01 0.0168
While the proxy is connecting to its client,
% "While": https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/40228
it also connects to the bridge.
This connection
uses WebSocket~\cite{rfc6455}, which
offers a TCP-like, client--server connection
layered on HTTPS.
The choice of protocol for the proxy--bridge link is arbitrary,
and could be changed
without affecting the rest of the system.
It does not need to be resist blocking,
it~just needs to be available to JavaScript code in web browsers.
WebRTC, for example, could be used for this link as well.
\subsection{Data transfer}
\label{sec:data-transfer}
No~complicated processing takes place at the proxy.
The main value of a Snowflake proxy is its IP address:
it~gives the client a peer to connect to that is not on the censor's address blocklist.
Having provided that,
the proxy assumes a role of pure data transfer.
Snowflake uses a stack of nested protocol layers.
We~will walk though the layers and describe the purpose of each.
\bigskip
\noindent
\begin{tabular}{@{}l@{\,}l@{}l@{\,}l}
UDP & \rdelim\}{3}{*} & \multirow{3}{5em}{WebRTC data channel} & \rdelim\}{3}{*}[\,ephemeral, per proxy]\\
DTLS \\
SCTP \\
KCP & \rdelim\}{2}{*} & \multirow{2}{*}{Turbo Tunnel} & \rdelim\}{3}{*}[\,persistent, per session] \\
smux \\
\multicolumn{3}{@{}l}{Tor protocol} \\
\multicolumn{4}{@{}l}{application streams} \\
\end{tabular}
\bigskip
\noindent
This is the stack for the client--proxy link,
which is the place where WebRTC is used, and which is
exposed to observation by the censor (\autoref{fig:architecture}).
The stack for the proxy--bridge link is the same,
but with WebSocket in place of the
WebRTC data channel at the top.
The layers marked ``ephemeral'' are skimmed off
and replaced as proxies come and~go.
The layers marked ``persistent'' are instantiated once
in each circumvention session,
hold long-term state,
and are end-to-end between client and bridge.
The connection between a client and its proxy is
a WebRTC data channel~\cite{rfc8831},
which provides a way to send arbitrary binary messages between peers.
A~data channel is its own stack of three protocols:
UDP for network transport,
DTLS (Datagram TLS)
for confidentiality and integrity, and
SCTP (Stream Control Transmission Protocol)
for delimiting message boundaries
and other features like congestion control.
Working UDP port numbers will have been discovered
using ICE in the previous phase.
The peers authenticate one another
at the DTLS layer using certificate fingerprints
that were exchanged during rendezvous~\cite[\S 5.1]{rfc8842}.
Data channels are well-suited to Snowflake's needs.
(The specification even lists circumvention as a use case~\cite[\S 3.2]{rfc8831}.)
But data channels are not the only option:
WebRTC also offers \firstterm{media streams}
for unreliable transport of real-time audio and video.
Which of these is used may be a fingerprinting vector.