[tor-commits] [pluggable-transports/meek] 09/09: Merge branch 'main' into turbotunnel
gitolite role
git at cupani.torproject.org
Fri Oct 28 07:13:08 UTC 2022
This is an automated email from the git hooks/post-receive script.
dcf pushed a commit to branch turbotunnel
in repository pluggable-transports/meek.
commit b93c5a027f7f60636c84bd405ec7c8089d4a8b0e
Merge: 46c988b e9eff72
Author: David Fifield <david at bamsoftware.com>
AuthorDate: Fri Oct 28 01:11:22 2022 -0600
Merge branch 'main' into turbotunnel
common/encapsulation/encapsulation.go | 22 ++-
doc/meek-client.1 | 208 ++++++++++++++++++++++-
doc/meek-client.1.txt | 24 ++-
doc/meek-server.1 | 24 ++-
doc/meek-server.1.txt | 6 +-
go.mod | 8 +-
go.sum | 28 ++-
meek-client-torbrowser/linux.go | 2 +
meek-client-torbrowser/mac.go | 2 +
meek-client-torbrowser/meek-client-torbrowser.go | 4 +-
meek-client-torbrowser/terminate_other.go | 1 +
meek-client-torbrowser/terminate_windows.go | 1 +
meek-client-torbrowser/windows.go | 2 +
meek-client/meek-client.go | 24 ++-
meek-client/proxy_test.go | 2 +-
meek-client/torrc | 1 -
meek-client/utls.go | 68 ++++----
meek-client/utls_test.go | 45 +----
meek-server/certificate.go | 1 +
meek-server/meek-server.go | 19 ++-
20 files changed, 363 insertions(+), 129 deletions(-)
diff --cc common/encapsulation/encapsulation.go
index 64a58d8,0000000..9fd7449
mode 100644,000000..100644
--- a/common/encapsulation/encapsulation.go
+++ b/common/encapsulation/encapsulation.go
@@@ -1,194 -1,0 +1,200 @@@
+// 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
++//
++// 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
++//
++// 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
++//
++// 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 be passed to
+// WriteData, whose total encoded size (including length prefix) is no larger
+// than n. Call this to find out if a chunk of data will fit into a length
+// budget. Panics if n == 0.
+func MaxDataForSize(n int) int {
+ if n == 0 {
+ panic("zero length")
+ }
+ prefix, err := dataPrefixForLength(n)
+ if err == ErrTooLong {
+ return (1 << (6 + 7 + 7)) - 1 - 3
+ } else if err != nil {
+ panic(err)
+ }
+ return n - len(prefix)
+}
diff --cc doc/meek-client.1
index 8215537,ec64796..70ae1e1
--- a/doc/meek-client.1
+++ b/doc/meek-client.1
@@@ -2,12 -2,12 +2,12 @@@
.\" Title: meek-client
.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author]
.\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/>
- .\" Date: 04/29/2020
-.\" Date: 10/20/2022
++.\" Date: 10/28/2022
.\" Manual: \ \&
.\" Source: \ \&
.\" Language: English
.\"
- .TH "MEEK\-CLIENT" "1" "04/29/2020" "\ \&" "\ \&"
-.TH "MEEK\-CLIENT" "1" "10/20/2022" "\ \&" "\ \&"
++.TH "MEEK\-CLIENT" "1" "10/28/2022" "\ \&" "\ \&"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
diff --cc go.mod
index e6c7dfb,cdeb2e8..99d67f7
--- a/go.mod
+++ b/go.mod
@@@ -4,10 -4,8 +4,10 @@@ go 1.1
require (
git.torproject.org/pluggable-transports/goptlib.git v1.1.0
- github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57
+ github.com/refraction-networking/utls v1.1.5
+ github.com/xtaci/kcp-go/v5 v5.5.14
+ github.com/xtaci/smux v1.5.14
- golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413
- golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
- golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8
+ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
+ golang.org/x/net v0.0.0-20220909164309-bea034e7d591
+ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10
)
diff --cc go.sum
index e22f000,3b60946..7b248c4
--- a/go.sum
+++ b/go.sum
@@@ -1,34 -1,24 +1,50 @@@
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/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
+ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+ github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
+ github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
+github.com/klauspost/cpuid v1.2.2 h1:1xAgYebNnsb9LKCdLOvFWtAxGU/33mjJtyOVbmUa0Us=
+github.com/klauspost/cpuid v1.2.2/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/reedsolomon v1.9.3 h1:N/VzgeMfHmLc+KHMD1UL/tNkfXAt8FnUqlgXGIduwAY=
+github.com/klauspost/reedsolomon v1.9.3/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
- github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57 h1:SL1K0QAuC1b54KoY1pjPWe6kSlsFHwK9/oC960fKrTY=
- github.com/refraction-networking/utls v0.0.0-20190909200633-43c36d3c1f57/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0=
+ github.com/refraction-networking/utls v1.1.5 h1:JtrojoNhbUQkBqEg05sP3gDgDj6hIEAAVKbI9lx4n6w=
+ github.com/refraction-networking/utls v1.1.5/go.mod h1:jRQxtYi7nkq1p28HF2lwOH5zQm9aC8rpK0O9lIIzGh8=
+github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY=
+github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
+github.com/templexxx/xorsimd v0.4.1 h1:iUZcywbOYDRAZUasAs2eSCUW8eobuZDy0I9FJiORkVg=
+github.com/templexxx/xorsimd v0.4.1/go.mod h1:W+ffZz8jJMH2SXwuKu9WhygqBMbFnp14G2fqEr8qaNo=
+github.com/tjfoc/gmsm v1.0.1 h1:R11HlqhXkDospckjZEihx9SW/2VW0RgdwrykyWMFOQU=
+github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc=
+github.com/xtaci/kcp-go/v5 v5.5.14 h1:YODIwvTyZmOTj3SduJeIcQPxthDoHllMm8YIBlK44Ik=
+github.com/xtaci/kcp-go/v5 v5.5.14/go.mod h1:H0T/EJ+lPNytnFYsKLH0JHUtiwZjG3KXlTM6c+Q4YUo=
+github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
+github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
+github.com/xtaci/smux v1.5.14 h1:1j+zJYDZRv9FHaWqCJfH5RPizIm0fSzJIFbfVn8zsfg=
+github.com/xtaci/smux v1.5.14/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
- golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
+golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
+ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
- golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+ golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
+ golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+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-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
- golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
+ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/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.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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --cc meek-client/meek-client.go
index 813a373,8905612..28943a8
--- a/meek-client/meek-client.go
+++ b/meek-client/meek-client.go
@@@ -98,14 -121,28 +104,14 @@@ type RequestInfo struct
RoundTripper http.RoundTripper
}
-// Make an http.Request from the payload data in buf and the request metadata in
-// info.
-func makeRequest(buf []byte, info *RequestInfo) (*http.Request, error) {
- var body io.Reader
- if len(buf) > 0 {
- // Leave body == nil when buf is empty. A nil body is an
- // explicit signal that the body is empty. An empty
- // *bytes.Reader or the magic value http.NoBody are supposed to
- // be equivalent ways to signal an empty body, but in Go 1.8 the
- // HTTP/2 code only understands nil. Not leaving body == nil
- // causes the Content-Length header to be omitted from HTTP/2
- // requests, which in some cases can cause the server to return
- // a 411 "Length Required" error. See
- // https://bugs.torproject.org/22865.
- body = bytes.NewReader(buf)
- }
- req, err := http.NewRequest("POST", info.URL.String(), body)
+func (info *RequestInfo) Poll(ctx context.Context, out io.Reader) (in io.ReadCloser, err error) {
+ req, err := http.NewRequest("POST", info.URL.String(), out)
- // Prevent Content-Type sniffing by net/http and middleboxes.
- req.Header.Set("Content-Type", "application/octet-stream")
if err != nil {
return nil, err
}
+ req = req.WithContext(ctx)
+ // Prevent Content-Type sniffing by net/http and middleboxes.
+ req.Header.Set("Content-Type", "application/octet-stream")
if info.Host != "" {
req.Host = info.Host
}
diff --cc meek-server/meek-server.go
index f715107,626ddb0..a4e5683
--- a/meek-server/meek-server.go
+++ b/meek-server/meek-server.go
@@@ -48,9 -49,13 +53,9 @@@ import
)
const (
- programVersion = "0.34"
+ programVersion = "0.37.0"
ptMethodName = "meek"
- // Reject session ids shorter than this, as a weak defense against
- // client bugs that send an empty session id or something similarly
- // likely to collide.
- minSessionIDLength = 8
// The largest request body we are willing to process, and the largest
// chunk of data we'll send back in a response.
maxPayloadLength = 0x10000
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the tor-commits
mailing list