diff --git a/cmd/gost/route.go b/cmd/gost/route.go index 360bc2d1..4b3a47c4 100644 --- a/cmd/gost/route.go +++ b/cmd/gost/route.go @@ -170,6 +170,7 @@ func parseChainNode(ns string) (nodes []gost.Node, err error) { wsOpts.WriteBufferSize = node.GetInt("wbuf") wsOpts.UserAgent = node.Get("agent") wsOpts.Path = node.Get("path") + wsOpts.Host = node.Get("host") timeout := node.GetDuration("timeout") diff --git a/go.mod b/go.mod index d6336663..87fa49ad 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/ginuerzh/gost go 1.22 -replace github.com/templexxx/cpu v0.0.7 => github.com/templexxx/cpu v0.0.10-0.20211111114238-98168dcec14a +replace ( + git.torproject.org/pluggable-transports/goptlib.git v1.3.0 => ./pkg/goptlib-v1.3.0 + github.com/templexxx/cpu v0.0.7 => github.com/templexxx/cpu v0.0.10-0.20211111114238-98168dcec14a +) require ( git.torproject.org/pluggable-transports/goptlib.git v1.3.0 diff --git a/go.sum b/go.sum index 1b988e44..1a08c997 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT filippo.io/edwards25519 v1.0.0-rc.1.0.20210721174708-390f27c3be20 h1:iJoUgXvhagsNMrJrvavw7vu1eG8+hm6jLOxlLFcoODw= filippo.io/edwards25519 v1.0.0-rc.1.0.20210721174708-390f27c3be20/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= git.torproject.org/pluggable-transports/goptlib.git v1.0.0/go.mod h1:YT4XMSkuEXbtqlydr9+OxqFAyspUv0Gr9qhM3B++o/Q= -git.torproject.org/pluggable-transports/goptlib.git v1.3.0 h1:G+iuRUblCCC2xnO+0ag1/4+aaM98D5mjWP1M0v9s8a0= -git.torproject.org/pluggable-transports/goptlib.git v1.3.0/go.mod h1:4PBMl1dg7/3vMWSoWb46eGWlrxkUyn/CAJmxhDLAlDs= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/LiamHaworth/go-tproxy v0.0.0-20190726054950-ef7efd7f24ed h1:eqa6queieK8SvoszxCu0WwH7lSVeL4/N/f1JwOMw1G4= github.com/LiamHaworth/go-tproxy v0.0.0-20190726054950-ef7efd7f24ed/go.mod h1:rA52xkgZwql9LRZXWb2arHEFP6qSR48KY2xOfWzEciQ= diff --git a/pkg/goptlib-v1.3.0/.gitignore b/pkg/goptlib-v1.3.0/.gitignore new file mode 100644 index 00000000..d4d51322 --- /dev/null +++ b/pkg/goptlib-v1.3.0/.gitignore @@ -0,0 +1,2 @@ +/examples/dummy-client/dummy-client +/examples/dummy-server/dummy-server diff --git a/pkg/goptlib-v1.3.0/COPYING b/pkg/goptlib-v1.3.0/COPYING new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/pkg/goptlib-v1.3.0/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/pkg/goptlib-v1.3.0/ChangeLog b/pkg/goptlib-v1.3.0/ChangeLog new file mode 100644 index 00000000..360baf3f --- /dev/null +++ b/pkg/goptlib-v1.3.0/ChangeLog @@ -0,0 +1,80 @@ +== v1.3.0 + +Added a DialOrWithDialer function that allows you to, for example, use a +specific source address when dialing the ORPort. + +== v1.2.0 + +The default and development branch is now "main" rather than "master". +The master branch will no longer be updated. +https://lists.torproject.org/pipermail/anti-censorship-team/2021-May/000168.html +If you have an existing clone of the master branch, run these commands +to update it: + git fetch origin + git remote set-head origin -a + git branch --move master main + git branch --set-upstream-to=origin/main main + +Added a go.mod file. +https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/40065 + +== v1.1.0 + +Added the Log function. +https://bugs.torproject.org/28940 + +== v1.0.0 + +Changed the tag naming scheme to work better with Go modules. +https://github.com/golang/go/wiki/Modules#semantic-import-versioning + +== 0.7 + +Fixed the ProxyError function; previously it would always panic. + +Repeated transport names in TOR_PT_SERVER_BINDADDR now result in an +ENV-ERROR. +https://bugs.torproject.org/21261 + +== 0.6 + +Remove all support for the "*" transport specification. The argument to +the ClientSetup and ServerSetup functions is now unused. +https://bugs.torproject.org/15612 + +Replaced SOCKS4a with SOCKS5. +https://bugs.torproject.org/12535 + +== 0.5 + +The AcceptSocks function no longer reports non-permanent errors, such as +those caused by a faulty SOCKS handshake. + +Added support for an upstream proxy (TOR_PT_PROXY). The two new +functions are ProxyError and ProxyDone. The ClientInfo struct has a new +ProxyURL member. +https://bugs.torproject.org/12125 + +== 0.4 + +Read the ExtORPort cookie file on every call to DialOr, instead of +reading it once and caching the result. This is to work around a tor bug +where tor doesn't ensure a new cookie file is written before starting +pluggable transports. +https://bugs.torproject.org/15240 + +== 0.3 + +Made output functions panic intead of backslash-escaping. Escaping of +invalid bytes is not specified by pt-spec, and backslashes conflicted +with the specified escaping of SMETHOD ARGS. +https://bugs.torproject.org/13370 + +== 0.2 + +Added the MakeStateDir function. + +== 0.1 +== 0.0 + +Initial release. diff --git a/pkg/goptlib-v1.3.0/README b/pkg/goptlib-v1.3.0/README new file mode 100644 index 00000000..acab1298 --- /dev/null +++ b/pkg/goptlib-v1.3.0/README @@ -0,0 +1,25 @@ +goptlib is a library for writing Tor pluggable transports in Go. + +https://spec.torproject.org/pt-spec +https://gitweb.torproject.org/torspec.git/tree/ext-orport-spec.txt + +To download a copy of the library into $GOPATH: + go get git.torproject.org/pluggable-transports/goptlib.git + +See the included example programs for examples of how to use the +library. To build them, enter their directory and run "go build". + examples/dummy-client/dummy-client.go + examples/dummy-server/dummy-server.go +The recommended way to start writing a new transport plugin is to copy +dummy-client or dummy-server and make changes to it. + +There is browseable documentation here: +https://godoc.org/git.torproject.org/pluggable-transports/goptlib.git + +Report bugs to the tor-dev@lists.torproject.org mailing list or to the +bug tracker at https://trac.torproject.org/projects/tor. + +To the extent possible under law, the authors have dedicated all +copyright and related and neighboring rights to this software to the +public domain worldwide. This software is distributed without any +warranty. See COPYING. diff --git a/pkg/goptlib-v1.3.0/args.go b/pkg/goptlib-v1.3.0/args.go new file mode 100644 index 00000000..c50bc93d --- /dev/null +++ b/pkg/goptlib-v1.3.0/args.go @@ -0,0 +1,219 @@ +package pt + +import ( + "bytes" + "fmt" + "sort" + "strings" +) + +// Key–value mappings for the representation of client and server options. + +// Args maps a string key to a list of values. It is similar to url.Values. +type Args map[string][]string + +// Get the first value associated with the given key. If there are any values +// associated with the key, the value return has the value and ok is set to +// true. If there are no values for the given key, value is "" and ok is false. +// If you need access to multiple values, use the map directly. +func (args Args) Get(key string) (value string, ok bool) { + if args == nil { + return "", false + } + vals, ok := args[key] + if !ok || len(vals) == 0 { + return "", false + } + return vals[0], true +} + +// Append value to the list of values for key. +func (args Args) Add(key, value string) { + args[key] = append(args[key], value) +} + +// Return the index of the next unescaped byte in s that is in the term set, or +// else the length of the string if no terminators appear. Additionally return +// the unescaped string up to the returned index. +func indexUnescaped(s string, term []byte) (int, string, error) { + var i int + unesc := make([]byte, 0) + for i = 0; i < len(s); i++ { + b := s[i] + // A terminator byte? + if bytes.IndexByte(term, b) != -1 { + break + } + if b == '\\' { + i++ + if i >= len(s) { + return 0, "", fmt.Errorf("nothing following final escape in %q", s) + } + b = s[i] + } + unesc = append(unesc, b) + } + return i, string(unesc), nil +} + +// Parse a name–value mapping as from an encoded SOCKS username/password. +// +// "First the '=' formatted arguments MUST be escaped, such that all +// backslash, equal sign, and semicolon characters are escaped with a +// backslash." +func parseClientParameters(s string) (args Args, err error) { + args = make(Args) + if len(s) == 0 { + return + } + i := 0 + for { + var key, value string + var offset, begin int + + begin = i + // Read the key. + offset, key, err = indexUnescaped(s[i:], []byte{'=', ';'}) + if err != nil { + return + } + i += offset + // End of string or no equals sign? + if i >= len(s) || s[i] != '=' { + err = fmt.Errorf("no equals sign in %q", s[begin:i]) + return + } + // Skip the equals sign. + i++ + // Read the value. + offset, value, err = indexUnescaped(s[i:], []byte{';'}) + if err != nil { + return + } + i += offset + if len(key) == 0 { + err = fmt.Errorf("empty key in %q", s[begin:i]) + return + } + args.Add(key, value) + if i >= len(s) { + break + } + // Skip the semicolon. + i++ + } + return args, nil +} + +// Parse a transport–name–value mapping as from TOR_PT_SERVER_TRANSPORT_OPTIONS. +// +// "...a semicolon-separated list of : pairs, where is a PT +// name and is a k=v string value with options that are to be passed to +// the transport. Colons, semicolons, equal signs and backslashes must be +// escaped with a backslash." +// Example: scramblesuit:key=banana;automata:rule=110;automata:depth=3 +func parseServerTransportOptions(s string) (opts map[string]Args, err error) { + opts = make(map[string]Args) + if len(s) == 0 { + return + } + i := 0 + for { + var methodName, key, value string + var offset, begin int + + begin = i + // Read the method name. + offset, methodName, err = indexUnescaped(s[i:], []byte{':', '=', ';'}) + if err != nil { + return + } + i += offset + // End of string or no colon? + if i >= len(s) || s[i] != ':' { + err = fmt.Errorf("no colon in %q", s[begin:i]) + return + } + // Skip the colon. + i++ + // Read the key. + offset, key, err = indexUnescaped(s[i:], []byte{'=', ';'}) + if err != nil { + return + } + i += offset + // End of string or no equals sign? + if i >= len(s) || s[i] != '=' { + err = fmt.Errorf("no equals sign in %q", s[begin:i]) + return + } + // Skip the equals sign. + i++ + // Read the value. + offset, value, err = indexUnescaped(s[i:], []byte{';'}) + if err != nil { + return + } + i += offset + if len(methodName) == 0 { + err = fmt.Errorf("empty method name in %q", s[begin:i]) + return + } + if len(key) == 0 { + err = fmt.Errorf("empty key in %q", s[begin:i]) + return + } + if opts[methodName] == nil { + opts[methodName] = make(Args) + } + opts[methodName].Add(key, value) + if i >= len(s) { + break + } + // Skip the semicolon. + i++ + } + return opts, nil +} + +// Escape backslashes and all the bytes that are in set. +func backslashEscape(s string, set []byte) string { + var buf bytes.Buffer + for _, b := range []byte(s) { + if b == '\\' || bytes.IndexByte(set, b) != -1 { + buf.WriteByte('\\') + } + buf.WriteByte(b) + } + return buf.String() +} + +// Encode a name–value mapping so that it is suitable to go in the ARGS option +// of an SMETHOD line. The output is sorted by key. The "ARGS:" prefix is not +// added. +// +// "Equal signs and commas [and backslashes] MUST be escaped with a backslash." +func encodeSmethodArgs(args Args) string { + if args == nil { + return "" + } + + keys := make([]string, 0, len(args)) + for key := range args { + keys = append(keys, key) + } + sort.Strings(keys) + + escape := func(s string) string { + return backslashEscape(s, []byte{'=', ','}) + } + + var pairs []string + for _, key := range keys { + for _, value := range args[key] { + pairs = append(pairs, escape(key)+"="+escape(value)) + } + } + + return strings.Join(pairs, ",") +} diff --git a/pkg/goptlib-v1.3.0/args_test.go b/pkg/goptlib-v1.3.0/args_test.go new file mode 100644 index 00000000..ff8a950d --- /dev/null +++ b/pkg/goptlib-v1.3.0/args_test.go @@ -0,0 +1,374 @@ +package pt + +import ( + "testing" +) + +func stringSlicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func argsEqual(a, b Args) bool { + for k, av := range a { + bv := b[k] + if !stringSlicesEqual(av, bv) { + return false + } + } + for k, bv := range b { + av := a[k] + if !stringSlicesEqual(av, bv) { + return false + } + } + return true +} + +func TestArgsGet(t *testing.T) { + args := Args{ + "a": []string{}, + "b": []string{"value"}, + "c": []string{"v1", "v2", "v3"}, + } + var uninit Args + + var v string + var ok bool + + // Get on nil map should be the same as Get on empty map. + v, ok = uninit.Get("a") + if !(v == "" && !ok) { + t.Errorf("unexpected result from Get on nil Args: %q %v", v, ok) + } + + v, ok = args.Get("a") + if ok { + t.Errorf("Unexpected Get success for %q", "a") + } + if v != "" { + t.Errorf("Get failure returned other than %q: %q", "", v) + } + v, ok = args.Get("b") + if !ok { + t.Errorf("Unexpected Get failure for %q", "b") + } + if v != "value" { + t.Errorf("Get(%q) → %q (expected %q)", "b", v, "value") + } + v, ok = args.Get("c") + if !ok { + t.Errorf("Unexpected Get failure for %q", "c") + } + if v != "v1" { + t.Errorf("Get(%q) → %q (expected %q)", "c", v, "v1") + } + v, ok = args.Get("d") + if ok { + t.Errorf("Unexpected Get success for %q", "d") + } +} + +func TestArgsAdd(t *testing.T) { + args := make(Args) + expected := Args{} + if !argsEqual(args, expected) { + t.Fatalf("%q != %q", args, expected) + } + args.Add("k1", "v1") + expected = Args{"k1": []string{"v1"}} + if !argsEqual(args, expected) { + t.Fatalf("%q != %q", args, expected) + } + args.Add("k2", "v2") + expected = Args{"k1": []string{"v1"}, "k2": []string{"v2"}} + if !argsEqual(args, expected) { + t.Fatalf("%q != %q", args, expected) + } + args.Add("k1", "v3") + expected = Args{"k1": []string{"v1", "v3"}, "k2": []string{"v2"}} + if !argsEqual(args, expected) { + t.Fatalf("%q != %q", args, expected) + } +} + +func TestParseClientParameters(t *testing.T) { + badTests := [...]string{ + "key", + "key\\", + "=value", + "==value", + "==key=value", + "key=value\\", + "a=b;key=value\\", + "a;b=c", + ";", + "key=value;", + ";key=value", + "key\\=value", + } + goodTests := [...]struct { + input string + expected Args + }{ + { + "", + Args{}, + }, + { + "key=", + Args{"key": []string{""}}, + }, + { + "key==", + Args{"key": []string{"="}}, + }, + { + "key=value", + Args{"key": []string{"value"}}, + }, + { + "a=b=c", + Args{"a": []string{"b=c"}}, + }, + { + "a=bc==", + Args{"a": []string{"bc=="}}, + }, + { + "key=a\nb", + Args{"key": []string{"a\nb"}}, + }, + { + "key=value\\;", + Args{"key": []string{"value;"}}, + }, + { + "key=\"value\"", + Args{"key": []string{"\"value\""}}, + }, + { + "key=\"\"value\"\"", + Args{"key": []string{"\"\"value\"\""}}, + }, + { + "\"key=value\"", + Args{"\"key": []string{"value\""}}, + }, + { + "key=value;key=value", + Args{"key": []string{"value", "value"}}, + }, + { + "key=value1;key=value2", + Args{"key": []string{"value1", "value2"}}, + }, + { + "key1=value1;key2=value2;key1=value3", + Args{"key1": []string{"value1", "value3"}, "key2": []string{"value2"}}, + }, + { + "\\;=\\;;\\\\=\\;", + Args{";": []string{";"}, "\\": []string{";"}}, + }, + { + "a\\=b=c", + Args{"a=b": []string{"c"}}, + }, + { + "shared-secret=rahasia;secrets-file=/tmp/blob", + Args{"shared-secret": []string{"rahasia"}, "secrets-file": []string{"/tmp/blob"}}, + }, + { + "rocks=20;height=5.6", + Args{"rocks": []string{"20"}, "height": []string{"5.6"}}, + }, + } + + for _, input := range badTests { + _, err := parseClientParameters(input) + if err == nil { + t.Errorf("%q unexpectedly succeeded", input) + } + } + + for _, test := range goodTests { + args, err := parseClientParameters(test.input) + if err != nil { + t.Errorf("%q unexpectedly returned an error: %s", test.input, err) + } + if !argsEqual(args, test.expected) { + t.Errorf("%q → %q (expected %q)", test.input, args, test.expected) + } + } +} + +func optsEqual(a, b map[string]Args) bool { + for k, av := range a { + bv, ok := b[k] + if !ok || !argsEqual(av, bv) { + return false + } + } + for k, bv := range b { + av, ok := a[k] + if !ok || !argsEqual(av, bv) { + return false + } + } + return true +} + +func TestParseServerTransportOptions(t *testing.T) { + badTests := [...]string{ + "t\\", + ":=", + "t:=", + ":k=", + ":=v", + "t:=v", + "t:=v", + "t:k\\", + "t:k=v;", + "abc", + "t:", + "key=value", + "=value", + "t:k=v\\", + "t1:k=v;t2:k=v\\", + "t:=key=value", + "t:==key=value", + "t:;key=value", + "t:key\\=value", + } + goodTests := [...]struct { + input string + expected map[string]Args + }{ + { + "", + map[string]Args{}, + }, + { + "t:k=v", + map[string]Args{ + "t": {"k": []string{"v"}}, + }, + }, + { + "t:k=v=v", + map[string]Args{ + "t": {"k": []string{"v=v"}}, + }, + }, + { + "t:k=vv==", + map[string]Args{ + "t": {"k": []string{"vv=="}}, + }, + }, + { + "t1:k=v1;t2:k=v2;t1:k=v3", + map[string]Args{ + "t1": {"k": []string{"v1", "v3"}}, + "t2": {"k": []string{"v2"}}, + }, + }, + { + "t\\:1:k=v;t\\=2:k=v;t\\;3:k=v;t\\\\4:k=v", + map[string]Args{ + "t:1": {"k": []string{"v"}}, + "t=2": {"k": []string{"v"}}, + "t;3": {"k": []string{"v"}}, + "t\\4": {"k": []string{"v"}}, + }, + }, + { + "t:k\\:1=v;t:k\\=2=v;t:k\\;3=v;t:k\\\\4=v", + map[string]Args{ + "t": { + "k:1": []string{"v"}, + "k=2": []string{"v"}, + "k;3": []string{"v"}, + "k\\4": []string{"v"}, + }, + }, + }, + { + "t:k=v\\:1;t:k=v\\=2;t:k=v\\;3;t:k=v\\\\4", + map[string]Args{ + "t": {"k": []string{"v:1", "v=2", "v;3", "v\\4"}}, + }, + }, + { + "trebuchet:secret=nou;trebuchet:cache=/tmp/cache;ballista:secret=yes", + map[string]Args{ + "trebuchet": {"secret": []string{"nou"}, "cache": []string{"/tmp/cache"}}, + "ballista": {"secret": []string{"yes"}}, + }, + }, + } + + for _, input := range badTests { + _, err := parseServerTransportOptions(input) + if err == nil { + t.Errorf("%q unexpectedly succeeded", input) + } + } + + for _, test := range goodTests { + opts, err := parseServerTransportOptions(test.input) + if err != nil { + t.Errorf("%q unexpectedly returned an error: %s", test.input, err) + } + if !optsEqual(opts, test.expected) { + t.Errorf("%q → %q (expected %q)", test.input, opts, test.expected) + } + } +} + +func TestEncodeSmethodArgs(t *testing.T) { + tests := [...]struct { + args Args + expected string + }{ + { + nil, + "", + }, + { + Args{}, + "", + }, + { + Args{"j": []string{"v1", "v2", "v3"}, "k": []string{"v1", "v2", "v3"}}, + "j=v1,j=v2,j=v3,k=v1,k=v2,k=v3", + }, + { + Args{"=,\\": []string{"=", ",", "\\"}}, + "\\=\\,\\\\=\\=,\\=\\,\\\\=\\,,\\=\\,\\\\=\\\\", + }, + { + Args{"secret": []string{"yes"}}, + "secret=yes", + }, + { + Args{"secret": []string{"nou"}, "cache": []string{"/tmp/cache"}}, + "cache=/tmp/cache,secret=nou", + }, + } + + for _, test := range tests { + encoded := encodeSmethodArgs(test.args) + if encoded != test.expected { + t.Errorf("%q → %q (expected %q)", test.args, encoded, test.expected) + } + } +} diff --git a/pkg/goptlib-v1.3.0/examples/dummy-client/dummy-client.go b/pkg/goptlib-v1.3.0/examples/dummy-client/dummy-client.go new file mode 100644 index 00000000..b1a9647f --- /dev/null +++ b/pkg/goptlib-v1.3.0/examples/dummy-client/dummy-client.go @@ -0,0 +1,125 @@ +// Dummy no-op pluggable transport client. Works only as a managed proxy. +// +// Usage (in torrc): +// UseBridges 1 +// Bridge dummy X.X.X.X:YYYY +// ClientTransportPlugin dummy exec dummy-client +// +// Because this transport doesn't do anything to the traffic, you can use the +// ORPort of any ordinary bridge (or relay that has DirPort set) in the bridge +// line; it doesn't have to declare support for the dummy transport. +package main + +import ( + "io" + "io/ioutil" + "net" + "os" + "os/signal" + "sync" + "syscall" +) + +import "git.torproject.org/pluggable-transports/goptlib.git" + +var ptInfo pt.ClientInfo + +func copyLoop(a, b net.Conn) { + var wg sync.WaitGroup + wg.Add(2) + + go func() { + io.Copy(b, a) + wg.Done() + }() + go func() { + io.Copy(a, b) + wg.Done() + }() + + wg.Wait() +} + +func handler(conn *pt.SocksConn) error { + defer conn.Close() + remote, err := net.Dial("tcp", conn.Req.Target) + if err != nil { + conn.Reject() + return err + } + defer remote.Close() + err = conn.Grant(remote.RemoteAddr().(*net.TCPAddr)) + if err != nil { + return err + } + + copyLoop(conn, remote) + + return nil +} + +func acceptLoop(ln *pt.SocksListener) error { + defer ln.Close() + for { + conn, err := ln.AcceptSocks() + if err != nil { + if e, ok := err.(net.Error); ok && e.Temporary() { + continue + } + return err + } + go handler(conn) + } +} + +func main() { + var err error + + ptInfo, err = pt.ClientSetup(nil) + if err != nil { + os.Exit(1) + } + + if ptInfo.ProxyURL != nil { + pt.ProxyError("proxy is not supported") + os.Exit(1) + } + + listeners := make([]net.Listener, 0) + for _, methodName := range ptInfo.MethodNames { + switch methodName { + case "dummy": + ln, err := pt.ListenSocks("tcp", "127.0.0.1:0") + if err != nil { + pt.CmethodError(methodName, err.Error()) + break + } + go acceptLoop(ln) + pt.Cmethod(methodName, ln.Version(), ln.Addr()) + listeners = append(listeners, ln) + default: + pt.CmethodError(methodName, "no such method") + } + } + pt.CmethodsDone() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGTERM) + + if os.Getenv("TOR_PT_EXIT_ON_STDIN_CLOSE") == "1" { + // This environment variable means we should treat EOF on stdin + // just like SIGTERM: https://bugs.torproject.org/15435. + go func() { + io.Copy(ioutil.Discard, os.Stdin) + sigChan <- syscall.SIGTERM + }() + } + + // wait for a signal + <-sigChan + + // signal received, shut down + for _, ln := range listeners { + ln.Close() + } +} diff --git a/pkg/goptlib-v1.3.0/examples/dummy-server/dummy-server.go b/pkg/goptlib-v1.3.0/examples/dummy-server/dummy-server.go new file mode 100644 index 00000000..f3b0c8f5 --- /dev/null +++ b/pkg/goptlib-v1.3.0/examples/dummy-server/dummy-server.go @@ -0,0 +1,117 @@ +// Dummy no-op pluggable transport server. Works only as a managed proxy. +// +// Usage (in torrc): +// BridgeRelay 1 +// ORPort 9001 +// ExtORPort 6669 +// ServerTransportPlugin dummy exec dummy-server +// +// Because the dummy transport doesn't do anything to the traffic, you can +// connect to it with any ordinary Tor client; you don't have to use +// dummy-client. +package main + +import ( + "io" + "io/ioutil" + "net" + "os" + "os/signal" + "sync" + "syscall" +) + +import "git.torproject.org/pluggable-transports/goptlib.git" + +var ptInfo pt.ServerInfo + +func copyLoop(a, b net.Conn) { + var wg sync.WaitGroup + wg.Add(2) + + go func() { + io.Copy(b, a) + wg.Done() + }() + go func() { + io.Copy(a, b) + wg.Done() + }() + + wg.Wait() +} + +func handler(conn net.Conn) error { + defer conn.Close() + + or, err := pt.DialOr(&ptInfo, conn.RemoteAddr().String(), "dummy") + if err != nil { + return err + } + defer or.Close() + + copyLoop(conn, or) + + return nil +} + +func acceptLoop(ln net.Listener) error { + defer ln.Close() + for { + conn, err := ln.Accept() + if err != nil { + if e, ok := err.(net.Error); ok && e.Temporary() { + continue + } + return err + } + go handler(conn) + } +} + +func main() { + var err error + + ptInfo, err = pt.ServerSetup(nil) + if err != nil { + os.Exit(1) + } + + listeners := make([]net.Listener, 0) + for _, bindaddr := range ptInfo.Bindaddrs { + switch bindaddr.MethodName { + case "dummy": + ln, err := net.ListenTCP("tcp", bindaddr.Addr) + if err != nil { + pt.SmethodError(bindaddr.MethodName, err.Error()) + break + } + go acceptLoop(ln) + pt.Smethod(bindaddr.MethodName, ln.Addr()) + listeners = append(listeners, ln) + default: + pt.SmethodError(bindaddr.MethodName, "no such method") + } + } + pt.SmethodsDone() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGTERM) + + if os.Getenv("TOR_PT_EXIT_ON_STDIN_CLOSE") == "1" { + // This environment variable means we should treat EOF on stdin + // just like SIGTERM: https://bugs.torproject.org/15435. + go func() { + io.Copy(ioutil.Discard, os.Stdin) + sigChan <- syscall.SIGTERM + }() + } + + // wait for a signal + <-sigChan + + // signal received, shut down + for _, ln := range listeners { + ln.Close() + } +} diff --git a/pkg/goptlib-v1.3.0/go.mod b/pkg/goptlib-v1.3.0/go.mod new file mode 100644 index 00000000..aec837fa --- /dev/null +++ b/pkg/goptlib-v1.3.0/go.mod @@ -0,0 +1,3 @@ +module git.torproject.org/pluggable-transports/goptlib.git + +go 1.11 diff --git a/pkg/goptlib-v1.3.0/proxy_test.go b/pkg/goptlib-v1.3.0/proxy_test.go new file mode 100644 index 00000000..c7a113b4 --- /dev/null +++ b/pkg/goptlib-v1.3.0/proxy_test.go @@ -0,0 +1,80 @@ +package pt + +import ( + "os" + "testing" +) + +func TestGetProxyURL(t *testing.T) { + badTests := [...]string{ + "bogus", + "http:", + "://127.0.0.1", + "//127.0.0.1", + "http:127.0.0.1", + "://[::1]", + "//[::1]", + "http:[::1]", + "://localhost", + "//localhost", + "http:localhost", + // No port in these. + "http://127.0.0.1", + "socks4a://127.0.0.1", + "socks5://127.0.0.1", + "http://127.0.0.1:", + "http://[::1]", + "http://localhost", + "unknown://localhost/whatever", + // No host in these. + "http://:8080", + "socks4a://:1080", + "socks5://:1080", + } + goodTests := [...]struct { + input, expected string + }{ + {"http://127.0.0.1:8080", "http://127.0.0.1:8080"}, + {"http://127.0.0.1:8080/", "http://127.0.0.1:8080/"}, + {"http://127.0.0.1:8080/path", "http://127.0.0.1:8080/path"}, + {"http://[::1]:8080", "http://[::1]:8080"}, + {"http://[::1]:8080/", "http://[::1]:8080/"}, + {"http://[::1]:8080/path", "http://[::1]:8080/path"}, + {"http://localhost:8080", "http://localhost:8080"}, + {"http://localhost:8080/", "http://localhost:8080/"}, + {"http://localhost:8080/path", "http://localhost:8080/path"}, + {"http://user@localhost:8080", "http://user@localhost:8080"}, + {"http://user:password@localhost:8080", "http://user:password@localhost:8080"}, + {"socks5://localhost:1080", "socks5://localhost:1080"}, + {"socks4a://localhost:1080", "socks4a://localhost:1080"}, + {"unknown://localhost:9999/whatever", "unknown://localhost:9999/whatever"}, + } + + os.Clearenv() + u, err := getProxyURL() + if err != nil { + t.Errorf("empty environment unexpectedly returned an error: %s", err) + } + if u != nil { + t.Errorf("empty environment returned %q", u) + } + + for _, input := range badTests { + os.Setenv("TOR_PT_PROXY", input) + u, err = getProxyURL() + if err == nil { + t.Errorf("TOR_PT_PROXY=%q unexpectedly succeeded and returned %q", input, u) + } + } + + for _, test := range goodTests { + os.Setenv("TOR_PT_PROXY", test.input) + u, err := getProxyURL() + if err != nil { + t.Errorf("TOR_PT_PROXY=%q unexpectedly returned an error: %s", test.input, err) + } + if u == nil || u.String() != test.expected { + t.Errorf("TOR_PT_PROXY=%q → %q (expected %q)", test.input, u, test.expected) + } + } +} diff --git a/pkg/goptlib-v1.3.0/pt.go b/pkg/goptlib-v1.3.0/pt.go new file mode 100644 index 00000000..d01e497e --- /dev/null +++ b/pkg/goptlib-v1.3.0/pt.go @@ -0,0 +1,1040 @@ +// Package pt implements the Tor pluggable transports specification. +// +// Sample client usage: +// var ptInfo pt.ClientInfo +// ... +// func handler(conn *pt.SocksConn) error { +// defer conn.Close() +// remote, err := net.Dial("tcp", conn.Req.Target) +// if err != nil { +// conn.Reject() +// return err +// } +// defer remote.Close() +// err = conn.Grant(remote.RemoteAddr().(*net.TCPAddr)) +// if err != nil { +// return err +// } +// // do something with conn and remote. +// return nil +// } +// func acceptLoop(ln *pt.SocksListener) error { +// defer ln.Close() +// for { +// conn, err := ln.AcceptSocks() +// if err != nil { +// if e, ok := err.(net.Error); ok && e.Temporary() { +// pt.Log(pt.LogSeverityError, "accept error: " + err.Error()) +// continue +// } +// return err +// } +// go handler(conn) +// } +// return nil +// } +// ... +// func main() { +// var err error +// ptInfo, err = pt.ClientSetup(nil) +// if err != nil { +// os.Exit(1) +// } +// if ptInfo.ProxyURL != nil { +// // you need to interpret the proxy URL yourself +// // call pt.ProxyDone instead if it's a type you understand +// pt.ProxyError(fmt.Sprintf("proxy %s is not supported", ptInfo.ProxyURL)) +// os.Exit(1) +// } +// for _, methodName := range ptInfo.MethodNames { +// switch methodName { +// case "foo": +// ln, err := pt.ListenSocks("tcp", "127.0.0.1:0") +// if err != nil { +// pt.CmethodError(methodName, err.Error()) +// break +// } +// go acceptLoop(ln) +// pt.Cmethod(methodName, ln.Version(), ln.Addr()) +// default: +// pt.CmethodError(methodName, "no such method") +// } +// } +// pt.CmethodsDone() +// } +// +// Sample server usage: +// var ptInfo pt.ServerInfo +// ... +// func handler(conn net.Conn) error { +// defer conn.Close() +// or, err := pt.DialOr(&ptInfo, conn.RemoteAddr().String(), "foo") +// if err != nil { +// return +// } +// defer or.Close() +// // do something with or and conn +// return nil +// } +// func acceptLoop(ln net.Listener) error { +// defer ln.Close() +// for { +// conn, err := ln.Accept() +// if err != nil { +// if e, ok := err.(net.Error); ok && e.Temporary() { +// continue +// } +// pt.Log(pt.LogSeverityError, "accept error: " + err.Error()) +// return err +// } +// go handler(conn) +// } +// return nil +// } +// ... +// func main() { +// var err error +// ptInfo, err = pt.ServerSetup(nil) +// if err != nil { +// os.Exit(1) +// } +// for _, bindaddr := range ptInfo.Bindaddrs { +// switch bindaddr.MethodName { +// case "foo": +// ln, err := net.ListenTCP("tcp", bindaddr.Addr) +// if err != nil { +// pt.SmethodError(bindaddr.MethodName, err.Error()) +// break +// } +// go acceptLoop(ln) +// pt.Smethod(bindaddr.MethodName, ln.Addr()) +// default: +// pt.SmethodError(bindaddr.MethodName, "no such method") +// } +// } +// pt.SmethodsDone() +// } +// +// Some additional care is needed to handle signals and shutdown properly. See +// the example programs dummy-client and dummy-server. +// +// Tor pluggable transports specification: +// https://spec.torproject.org/pt-spec +// +// Extended ORPort: +// https://gitweb.torproject.org/torspec.git/tree/ext-orport-spec.txt +// +// The package implements a SOCKS5 server sufficient for a Tor client transport +// plugin. +// +// https://www.ietf.org/rfc/rfc1928.txt +// https://www.ietf.org/rfc/rfc1929.txt +package pt + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/binary" + "fmt" + "io" + "net" + "net/url" + "os" + "strconv" + "strings" + "time" +) + +// This type wraps a Write method and calls Sync after each Write. +type syncWriter struct { + *os.File +} + +// Call File.Write and then Sync. An error is returned if either operation +// returns an error. +func (w syncWriter) Write(p []byte) (n int, err error) { + n, err = w.File.Write(p) + if err != nil { + return + } + err = w.Sync() + return +} + +// Writer to which pluggable transports negotiation messages are written. It +// defaults to a Writer that writes to os.Stdout and calls Sync after each +// write. +// +// You may, for example, log pluggable transports messages by defining a Writer +// that logs what is written to it: +// type logWriteWrapper struct { +// io.Writer +// } +// +// func (w logWriteWrapper) Write(p []byte) (int, error) { +// log.Print(string(p)) +// return w.Writer.Write(p) +// } +// and then redefining Stdout: +// pt.Stdout = logWriteWrapper{pt.Stdout} +var Stdout io.Writer = syncWriter{os.Stdout} + +// Represents an error that can happen during negotiation, for example +// ENV-ERROR. When an error occurs, we print it to stdout and also pass it up +// the return chain. +type ptErr struct { + Keyword string + Args []string +} + +// Implements the error interface. +func (err *ptErr) Error() string { + return formatline(err.Keyword, err.Args...) +} + +func getenv(key string) string { + return os.Getenv(key) +} + +// Returns an ENV-ERROR if the environment variable isn't set. +func getenvRequired(key string) (string, error) { + value := os.Getenv(key) + if value == "" { + return "", envError(fmt.Sprintf("no %s environment variable", key)) + } + return value, nil +} + +// Returns true iff keyword contains only bytes allowed in a PT→Tor output line +// keyword. +// ::= +func keywordIsSafe(keyword string) bool { + for _, b := range []byte(keyword) { + switch { + case '0' <= b && b <= '9': + continue + case 'A' <= b && b <= 'Z': + continue + case 'a' <= b && b <= 'z': + continue + case b == '-' || b == '_': + continue + default: + return false + } + } + return true +} + +// Returns true iff arg contains only bytes allowed in a PT→Tor output line arg. +// ::= +func argIsSafe(arg string) bool { + for _, b := range []byte(arg) { + if b >= '\x80' || b == '\x00' || b == '\n' { + return false + } + } + return true +} + +func formatline(keyword string, v ...string) string { + var buf bytes.Buffer + if !keywordIsSafe(keyword) { + panic(fmt.Sprintf("keyword %q contains forbidden bytes", keyword)) + } + buf.WriteString(keyword) + for _, x := range v { + if !argIsSafe(x) { + panic(fmt.Sprintf("arg %q contains forbidden bytes", x)) + } + buf.WriteString(" " + x) + } + return buf.String() +} + +// Print a pluggable transports protocol line to Stdout. The line consists of a +// keyword followed by any number of space-separated arg strings. Panics if +// there are forbidden bytes in the keyword or the args (pt-spec.txt 2.2.1). +func line(keyword string, v ...string) { + fmt.Fprintln(Stdout, formatline(keyword, v...)) +} + +// Emit and return the given error as a ptErr. +func doError(keyword string, v ...string) *ptErr { + line(keyword, v...) + return &ptErr{keyword, v} +} + +// Emit an ENV-ERROR line with explanation text. Returns a representation of the +// error. +func envError(msg string) error { + return doError("ENV-ERROR", msg) +} + +// Emit a VERSION-ERROR line with explanation text. Returns a representation of +// the error. +func versionError(msg string) error { + return doError("VERSION-ERROR", msg) +} + +// Emit a CMETHOD-ERROR line with explanation text. Returns a representation of +// the error. +func CmethodError(methodName, msg string) error { + return doError("CMETHOD-ERROR", methodName, msg) +} + +// Emit an SMETHOD-ERROR line with explanation text. Returns a representation of +// the error. +func SmethodError(methodName, msg string) error { + return doError("SMETHOD-ERROR", methodName, msg) +} + +// Emit a PROXY-ERROR line with explanation text. Returns a representation of +// the error. +func ProxyError(msg string) error { + return doError("PROXY-ERROR", msg) +} + +// Emit a CMETHOD line. socks must be "socks4" or "socks5". Call this once for +// each listening client SOCKS port. +func Cmethod(name string, socks string, addr net.Addr) { + line("CMETHOD", name, socks, addr.String()) +} + +// Emit a CMETHODS DONE line. Call this after opening all client listeners. +func CmethodsDone() { + line("CMETHODS", "DONE") +} + +// Emit an SMETHOD line. Call this once for each listening server port. +func Smethod(name string, addr net.Addr) { + line("SMETHOD", name, addr.String()) +} + +// Emit an SMETHOD line with an ARGS option. args is a name–value mapping that +// will be added to the server's extrainfo document. +// +// This is an example of how to check for a required option: +// secret, ok := bindaddr.Options.Get("shared-secret") +// if ok { +// args := pt.Args{} +// args.Add("shared-secret", secret) +// pt.SmethodArgs(bindaddr.MethodName, ln.Addr(), args) +// } else { +// pt.SmethodError(bindaddr.MethodName, "need a shared-secret option") +// } +// Or, if you just want to echo back the options provided by Tor from the +// TransportServerOptions configuration, +// pt.SmethodArgs(bindaddr.MethodName, ln.Addr(), bindaddr.Options) +func SmethodArgs(name string, addr net.Addr, args Args) { + line("SMETHOD", name, addr.String(), "ARGS:"+encodeSmethodArgs(args)) +} + +// Emit an SMETHODS DONE line. Call this after opening all server listeners. +func SmethodsDone() { + line("SMETHODS", "DONE") +} + +// Emit a PROXY DONE line. Call this after parsing ClientInfo.ProxyURL. +func ProxyDone() { + fmt.Fprintf(Stdout, "PROXY DONE\n") +} + +// Unexported type to represent log severities, preventing external callers from +// inventing new severity strings that may violate quoting rules. +// +// pt-spec.txt section 3.3.4 specifies quoting for MESSAGE, but not for +// SEVERITY, and the example shows an unquoted "SEVERITY=debug". While we know +// tor's parser permits quoting of SEVERITY, it's not actually specified. +// Therefore we we need to guard against callers passing a string that violates +// the global protocol constraint of "any US-ASCII character but NUL or NL." So +// here, we instantiate exactly the five supported severities, using a type that +// cannot be constructed outside the package. +type logSeverity struct { + string +} + +// Severity levels for the Log function. +var ( + LogSeverityError = logSeverity{"error"} + LogSeverityWarning = logSeverity{"warning"} + LogSeverityNotice = logSeverity{"notice"} + LogSeverityInfo = logSeverity{"info"} + LogSeverityDebug = logSeverity{"debug"} +) + +// Encode a string according to the CString rules of section 2.1.1 in +// control-spec.txt. +// CString = DQUOTE *qcontent DQUOTE +// "...in a CString, the escapes '\n', '\t', '\r', and the octal escapes '\0' +// ... '\377' represent newline, tab, carriage return, and the 256 possible +// octet values respectively." +// RFC 2822 section 3.2.5 in turn defines what byte values we need to escape: +// everything but +// NO-WS-CTL / ; Non white space controls +// %d33 / ; The rest of the US-ASCII +// %d35-91 / ; characters not including "\" +// %d93-126 ; or the quote character +// Technically control-spec.txt requires us to escape the space character (32), +// but it is an error in the spec: https://bugs.torproject.org/29432. +// +// We additionally need to ensure that whatever we return passes argIsSafe, +// because strings encoded by this function are printed verbatim by Log. +func encodeCString(s string) string { + result := bytes.NewBuffer([]byte{}) + result.WriteByte('"') + for _, c := range []byte(s) { + if c == 32 || c == 33 || (35 <= c && c <= 91) || (93 <= c && c <= 126) { + result.WriteByte(c) + } else { + fmt.Fprintf(result, "\\%03o", c) + } + } + result.WriteByte('"') + return result.String() +} + +// Emit a LOG message with the given severity (one of LogSeverityError, +// LogSeverityWarning, LogSeverityNotice, LogSeverityInfo, or LogSeverityDebug). +func Log(severity logSeverity, message string) { + // " contains the log message which can be a String or CString..." + // encodeCString always makes the string safe to emit; i.e., it + // satisfies argIsSafe. + line("LOG", "SEVERITY="+severity.string, "MESSAGE="+encodeCString(message)) +} + +// Get a pluggable transports version offered by Tor and understood by us, if +// any. The only version we understand is "1". This function reads the +// environment variable TOR_PT_MANAGED_TRANSPORT_VER. +func getManagedTransportVer() (string, error) { + const transportVersion = "1" + managedTransportVer, err := getenvRequired("TOR_PT_MANAGED_TRANSPORT_VER") + if err != nil { + return "", err + } + for _, offered := range strings.Split(managedTransportVer, ",") { + if offered == transportVersion { + return offered, nil + } + } + return "", versionError("no-version") +} + +// Return the directory name in the TOR_PT_STATE_LOCATION environment variable, +// creating it if it doesn't exist. Returns non-nil error if +// TOR_PT_STATE_LOCATION is not set or if there is an error creating the +// directory. +func MakeStateDir() (string, error) { + dir, err := getenvRequired("TOR_PT_STATE_LOCATION") + if err != nil { + return "", err + } + err = os.MkdirAll(dir, 0700) + return dir, err +} + +// Get the list of method names requested by Tor. This function reads the +// environment variable TOR_PT_CLIENT_TRANSPORTS. +func getClientTransports() ([]string, error) { + clientTransports, err := getenvRequired("TOR_PT_CLIENT_TRANSPORTS") + if err != nil { + return nil, err + } + return strings.Split(clientTransports, ","), nil +} + +// Get the upstream proxy URL. Returns nil if no proxy is requested. The +// function ensures that the Scheme and Host fields are set; i.e., that the URL +// is absolute. It additionally checks that the Host field contains both a host +// and a port part. This function reads the environment variable TOR_PT_PROXY. +// +// This function doesn't check that the scheme is one of Tor's supported proxy +// schemes; that is, one of "http", "socks5", or "socks4a". The caller must be +// able to handle any returned scheme (which may be by calling ProxyError if +// it doesn't know how to handle the scheme). +func getProxyURL() (*url.URL, error) { + rawurl := os.Getenv("TOR_PT_PROXY") + if rawurl == "" { + return nil, nil + } + u, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + if u.Scheme == "" { + return nil, fmt.Errorf("missing scheme") + } + if u.Host == "" { + return nil, fmt.Errorf("missing authority") + } + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return nil, err + } + if host == "" { + return nil, fmt.Errorf("missing host") + } + if port == "" { + return nil, fmt.Errorf("missing port") + } + return u, nil +} + +// This structure is returned by ClientSetup. It consists of a list of method +// names and the upstream proxy URL, if any. +type ClientInfo struct { + MethodNames []string + ProxyURL *url.URL +} + +// Check the client pluggable transports environment, emitting an error message +// and returning a non-nil error if any error is encountered. Returns a +// ClientInfo struct. +// +// If your program needs to know whether to call ClientSetup or ServerSetup +// (i.e., if the same program can be run as either a client or a server), check +// whether the TOR_PT_CLIENT_TRANSPORTS environment variable is set: +// if os.Getenv("TOR_PT_CLIENT_TRANSPORTS") != "" { +// // Client mode; call pt.ClientSetup. +// } else { +// // Server mode; call pt.ServerSetup. +// } +// +// Always pass nil for the unused single parameter. In the past, the parameter +// was a list of transport names to use in case Tor requested "*". That feature +// was never implemented and has been removed from the pluggable transports +// specification. +// https://bugs.torproject.org/15612 +func ClientSetup(_ []string) (info ClientInfo, err error) { + ver, err := getManagedTransportVer() + if err != nil { + return + } + line("VERSION", ver) + + info.MethodNames, err = getClientTransports() + if err != nil { + return + } + + info.ProxyURL, err = getProxyURL() + if err != nil { + return + } + + return info, nil +} + +// A combination of a method name and an address, as extracted from +// TOR_PT_SERVER_BINDADDR. +type Bindaddr struct { + MethodName string + Addr *net.TCPAddr + // Options from TOR_PT_SERVER_TRANSPORT_OPTIONS that pertain to this + // transport. + Options Args +} + +func parsePort(portStr string) (int, error) { + port, err := strconv.ParseUint(portStr, 10, 16) + return int(port), err +} + +// Resolve an address string into a net.TCPAddr. We are a bit more strict than +// net.ResolveTCPAddr; we don't allow an empty host or port, and the host part +// must be a literal IP address. +func resolveAddr(addrStr string) (*net.TCPAddr, error) { + ipStr, portStr, err := net.SplitHostPort(addrStr) + if err != nil { + // Before the fixing of bug #7011, tor doesn't put brackets around IPv6 + // addresses. Split after the last colon, assuming it is a port + // separator, and try adding the brackets. + // https://bugs.torproject.org/7011 + parts := strings.Split(addrStr, ":") + if len(parts) <= 2 { + return nil, err + } + addrStr := "[" + strings.Join(parts[:len(parts)-1], ":") + "]:" + parts[len(parts)-1] + ipStr, portStr, err = net.SplitHostPort(addrStr) + } + if err != nil { + return nil, err + } + if ipStr == "" { + return nil, net.InvalidAddrError(fmt.Sprintf("address string %q lacks a host part", addrStr)) + } + if portStr == "" { + return nil, net.InvalidAddrError(fmt.Sprintf("address string %q lacks a port part", addrStr)) + } + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, net.InvalidAddrError(fmt.Sprintf("not an IP string: %q", ipStr)) + } + port, err := parsePort(portStr) + if err != nil { + return nil, err + } + return &net.TCPAddr{IP: ip, Port: port}, nil +} + +// Return a new slice, the members of which are those members of addrs having a +// MethodName in methodNames. +func filterBindaddrs(addrs []Bindaddr, methodNames []string) []Bindaddr { + var result []Bindaddr + + for _, ba := range addrs { + for _, methodName := range methodNames { + if ba.MethodName == methodName { + result = append(result, ba) + break + } + } + } + + return result +} + +// Return an array of Bindaddrs, being the contents of TOR_PT_SERVER_BINDADDR +// with keys filtered by TOR_PT_SERVER_TRANSPORTS. Transport-specific options +// from TOR_PT_SERVER_TRANSPORT_OPTIONS are assigned to the Options member. +func getServerBindaddrs() ([]Bindaddr, error) { + var result []Bindaddr + + // Parse the list of server transport options. + serverTransportOptions := getenv("TOR_PT_SERVER_TRANSPORT_OPTIONS") + optionsMap, err := parseServerTransportOptions(serverTransportOptions) + if err != nil { + return nil, envError(fmt.Sprintf("TOR_PT_SERVER_TRANSPORT_OPTIONS: %q: %s", serverTransportOptions, err.Error())) + } + + // Get the list of all requested bindaddrs. + serverBindaddr, err := getenvRequired("TOR_PT_SERVER_BINDADDR") + if err != nil { + return nil, err + } + seenMethods := make(map[string]bool) + for _, spec := range strings.Split(serverBindaddr, ",") { + var bindaddr Bindaddr + + parts := strings.SplitN(spec, "-", 2) + if len(parts) != 2 { + return nil, envError(fmt.Sprintf("TOR_PT_SERVER_BINDADDR: %q: doesn't contain \"-\"", spec)) + } + bindaddr.MethodName = parts[0] + // Check for duplicate method names: "Applications MUST NOT set + // more than one
: pair per PT name." + if seenMethods[bindaddr.MethodName] { + return nil, envError(fmt.Sprintf("TOR_PT_SERVER_BINDADDR: %q: duplicate method name %q", spec, bindaddr.MethodName)) + } + seenMethods[bindaddr.MethodName] = true + addr, err := resolveAddr(parts[1]) + if err != nil { + return nil, envError(fmt.Sprintf("TOR_PT_SERVER_BINDADDR: %q: %s", spec, err.Error())) + } + bindaddr.Addr = addr + bindaddr.Options = optionsMap[bindaddr.MethodName] + result = append(result, bindaddr) + } + + // Filter by TOR_PT_SERVER_TRANSPORTS. + serverTransports, err := getenvRequired("TOR_PT_SERVER_TRANSPORTS") + if err != nil { + return nil, err + } + result = filterBindaddrs(result, strings.Split(serverTransports, ",")) + + return result, nil +} + +func readAuthCookie(f io.Reader) ([]byte, error) { + authCookieHeader := []byte("! Extended ORPort Auth Cookie !\x0a") + buf := make([]byte, 64) + + n, err := io.ReadFull(f, buf) + if err != nil { + return nil, err + } + // Check that the file ends here. + n, err = f.Read(make([]byte, 1)) + if n != 0 { + return nil, fmt.Errorf("file is longer than 64 bytes") + } else if err != io.EOF { + return nil, fmt.Errorf("did not find EOF at end of file") + } + header := buf[0:32] + cookie := buf[32:64] + if subtle.ConstantTimeCompare(header, authCookieHeader) != 1 { + return nil, fmt.Errorf("missing auth cookie header") + } + + return cookie, nil +} + +// Read and validate the contents of an auth cookie file. Returns the 32-byte +// cookie. See section 4.2.1.2 of 217-ext-orport-auth.txt. +func readAuthCookieFile(filename string) (cookie []byte, err error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer func() { + closeErr := f.Close() + if err == nil { + err = closeErr + } + }() + + return readAuthCookie(f) +} + +// This structure is returned by ServerSetup. It consists of a list of +// Bindaddrs, an address for the ORPort, an address for the extended ORPort (if +// any), and an authentication cookie (if any). +type ServerInfo struct { + Bindaddrs []Bindaddr + OrAddr *net.TCPAddr + ExtendedOrAddr *net.TCPAddr + AuthCookiePath string +} + +// Check the server pluggable transports environment, emitting an error message +// and returning a non-nil error if any error is encountered. Resolves the +// various requested bind addresses, the server ORPort and extended ORPort, and +// reads the auth cookie file. Returns a ServerInfo struct. +// +// If your program needs to know whether to call ClientSetup or ServerSetup +// (i.e., if the same program can be run as either a client or a server), check +// whether the TOR_PT_CLIENT_TRANSPORTS environment variable is set: +// if os.Getenv("TOR_PT_CLIENT_TRANSPORTS") != "" { +// // Client mode; call pt.ClientSetup. +// } else { +// // Server mode; call pt.ServerSetup. +// } +// +// Always pass nil for the unused single parameter. In the past, the parameter +// was a list of transport names to use in case Tor requested "*". That feature +// was never implemented and has been removed from the pluggable transports +// specification. +// https://bugs.torproject.org/15612 +func ServerSetup(_ []string) (info ServerInfo, err error) { + ver, err := getManagedTransportVer() + if err != nil { + return + } + line("VERSION", ver) + + info.Bindaddrs, err = getServerBindaddrs() + if err != nil { + return + } + + orPort := getenv("TOR_PT_ORPORT") + if orPort != "" { + info.OrAddr, err = resolveAddr(orPort) + if err != nil { + err = envError(fmt.Sprintf("cannot resolve TOR_PT_ORPORT %q: %s", orPort, err.Error())) + return + } + } + + info.AuthCookiePath = getenv("TOR_PT_AUTH_COOKIE_FILE") + + extendedOrPort := getenv("TOR_PT_EXTENDED_SERVER_PORT") + if extendedOrPort != "" { + if info.AuthCookiePath == "" { + err = envError("need TOR_PT_AUTH_COOKIE_FILE environment variable with TOR_PT_EXTENDED_SERVER_PORT") + return + } + info.ExtendedOrAddr, err = resolveAddr(extendedOrPort) + if err != nil { + err = envError(fmt.Sprintf("cannot resolve TOR_PT_EXTENDED_SERVER_PORT %q: %s", extendedOrPort, err.Error())) + return + } + } + + // Need either OrAddr or ExtendedOrAddr. + if info.OrAddr == nil && info.ExtendedOrAddr == nil { + err = envError("need TOR_PT_ORPORT or TOR_PT_EXTENDED_SERVER_PORT environment variable") + return + } + + return info, nil +} + +// See 217-ext-orport-auth.txt section 4.2.1.3. +func computeServerHash(authCookie, clientNonce, serverNonce []byte) []byte { + h := hmac.New(sha256.New, authCookie) + io.WriteString(h, "ExtORPort authentication server-to-client hash") + h.Write(clientNonce) + h.Write(serverNonce) + return h.Sum([]byte{}) +} + +// See 217-ext-orport-auth.txt section 4.2.1.3. +func computeClientHash(authCookie, clientNonce, serverNonce []byte) []byte { + h := hmac.New(sha256.New, authCookie) + io.WriteString(h, "ExtORPort authentication client-to-server hash") + h.Write(clientNonce) + h.Write(serverNonce) + return h.Sum([]byte{}) +} + +func extOrPortAuthenticate(s io.ReadWriter, info *ServerInfo) error { + // Read auth types. 217-ext-orport-auth.txt section 4.1. + var authTypes [256]bool + var count int + for count = 0; count < 256; count++ { + buf := make([]byte, 1) + _, err := io.ReadFull(s, buf) + if err != nil { + return err + } + b := buf[0] + if b == 0 { + break + } + authTypes[b] = true + } + if count >= 256 { + return fmt.Errorf("read 256 auth types without seeing \\x00") + } + + // We support only type 1, SAFE_COOKIE. + if !authTypes[1] { + return fmt.Errorf("server didn't offer auth type 1") + } + _, err := s.Write([]byte{1}) + if err != nil { + return err + } + + clientNonce := make([]byte, 32) + clientHash := make([]byte, 32) + serverNonce := make([]byte, 32) + serverHash := make([]byte, 32) + + _, err = io.ReadFull(rand.Reader, clientNonce) + if err != nil { + return err + } + _, err = s.Write(clientNonce) + if err != nil { + return err + } + + _, err = io.ReadFull(s, serverHash) + if err != nil { + return err + } + _, err = io.ReadFull(s, serverNonce) + if err != nil { + return err + } + + // Work around tor bug #15240 where the auth cookie is generated after + // pluggable transports are launched, leading to a stale cookie getting + // cached forever if it is only read once as part of ServerSetup. + // https://bugs.torproject.org/15240 + authCookie, err := readAuthCookieFile(info.AuthCookiePath) + if err != nil { + return fmt.Errorf("error reading TOR_PT_AUTH_COOKIE_FILE %q: %s", info.AuthCookiePath, err.Error()) + } + + expectedServerHash := computeServerHash(authCookie, clientNonce, serverNonce) + if subtle.ConstantTimeCompare(serverHash, expectedServerHash) != 1 { + return fmt.Errorf("mismatch in server hash") + } + + clientHash = computeClientHash(authCookie, clientNonce, serverNonce) + _, err = s.Write(clientHash) + if err != nil { + return err + } + + status := make([]byte, 1) + _, err = io.ReadFull(s, status) + if err != nil { + return err + } + if status[0] != 1 { + return fmt.Errorf("server rejected authentication") + } + + return nil +} + +// See section 3.1.1 of 196-transport-control-ports.txt. +const ( + extOrCmdDone = 0x0000 + extOrCmdUserAddr = 0x0001 + extOrCmdTransport = 0x0002 + extOrCmdOkay = 0x1000 + extOrCmdDeny = 0x1001 +) + +func extOrPortSendCommand(s io.Writer, cmd uint16, body []byte) error { + var buf bytes.Buffer + if len(body) > 65535 { + return fmt.Errorf("body length %d exceeds maximum of 65535", len(body)) + } + err := binary.Write(&buf, binary.BigEndian, cmd) + if err != nil { + return err + } + err = binary.Write(&buf, binary.BigEndian, uint16(len(body))) + if err != nil { + return err + } + err = binary.Write(&buf, binary.BigEndian, body) + if err != nil { + return err + } + _, err = s.Write(buf.Bytes()) + if err != nil { + return err + } + + return nil +} + +// Send a USERADDR command on s. See section 3.1.2.1 of +// 196-transport-control-ports.txt. +func extOrPortSendUserAddr(s io.Writer, addr string) error { + return extOrPortSendCommand(s, extOrCmdUserAddr, []byte(addr)) +} + +// Send a TRANSPORT command on s. See section 3.1.2.2 of +// 196-transport-control-ports.txt. +func extOrPortSendTransport(s io.Writer, methodName string) error { + return extOrPortSendCommand(s, extOrCmdTransport, []byte(methodName)) +} + +// Send a DONE command on s. See section 3.1 of 196-transport-control-ports.txt. +func extOrPortSendDone(s io.Writer) error { + return extOrPortSendCommand(s, extOrCmdDone, []byte{}) +} + +func extOrPortRecvCommand(s io.Reader) (cmd uint16, body []byte, err error) { + var bodyLen uint16 + data := make([]byte, 4) + + _, err = io.ReadFull(s, data) + if err != nil { + return + } + buf := bytes.NewBuffer(data) + err = binary.Read(buf, binary.BigEndian, &cmd) + if err != nil { + return + } + err = binary.Read(buf, binary.BigEndian, &bodyLen) + if err != nil { + return + } + body = make([]byte, bodyLen) + _, err = io.ReadFull(s, body) + if err != nil { + return + } + + return cmd, body, err +} + +// Send USERADDR and TRANSPORT commands followed by a DONE command. Wait for an +// OKAY or DENY response command from the server. If addr or methodName is "", +// the corresponding command is not sent. Returns nil if and only if OKAY is +// received. +func extOrPortSetMetadata(s io.ReadWriter, addr, methodName string) error { + var err error + + if addr != "" { + err = extOrPortSendUserAddr(s, addr) + if err != nil { + return err + } + } + if methodName != "" { + err = extOrPortSendTransport(s, methodName) + if err != nil { + return err + } + } + err = extOrPortSendDone(s) + if err != nil { + return err + } + cmd, _, err := extOrPortRecvCommand(s) + if err != nil { + return err + } + if cmd == extOrCmdDeny { + return fmt.Errorf("server returned DENY after our USERADDR and DONE") + } else if cmd != extOrCmdOkay { + return fmt.Errorf("server returned unknown command 0x%04x after our USERADDR and DONE", cmd) + } + + return nil +} + +func extOrPortSetup(s net.Conn, timeout time.Duration, + info *ServerInfo, addr, methodName string) error { + err := s.SetDeadline(time.Now().Add(timeout)) + if err != nil { + return err + } + err = extOrPortAuthenticate(s, info) + if err != nil { + return err + } + err = extOrPortSetMetadata(s, addr, methodName) + if err != nil { + return err + } + err = s.SetDeadline(time.Time{}) + if err != nil { + return err + } + return nil +} + +// Dial (using the given net.Dialer) info.ExtendedOrAddr if defined, or else +// info.OrAddr, and return an open net.Conn. If connecting to the extended +// OR port, extended OR port authentication à la 217-ext-orport-auth.txt is done +// before returning; an error is returned if authentication fails. +// +// The addr and methodName arguments are put in USERADDR and TRANSPORT ExtOrPort +// commands, respectively. If either is "", the corresponding command is not +// sent. +func DialOrWithDialer(dialer *net.Dialer, info *ServerInfo, addr, methodName string) (net.Conn, error) { + if info.ExtendedOrAddr == nil || info.AuthCookiePath == "" { + return dialer.Dial("tcp", info.OrAddr.String()) + } + + s, err := dialer.Dial("tcp", info.ExtendedOrAddr.String()) + if err != nil { + return nil, err + } + err = extOrPortSetup(s, 5*time.Second, info, addr, methodName) + if err != nil { + s.Close() + return nil, err + } + + return s, nil +} + +// Dial info.ExtendedOrAddr if defined, or else info.OrAddr, and return an open +// *net.TCPConn. If connecting to the extended OR port, extended OR port +// authentication à la 217-ext-orport-auth.txt is done before returning; an +// error is returned if authentication fails. +// +// The addr and methodName arguments are put in USERADDR and TRANSPORT ExtOrPort +// commands, respectively. If either is "", the corresponding command is not +// sent. +func DialOr(info *ServerInfo, addr, methodName string) (*net.TCPConn, error) { + c, err := DialOrWithDialer(&net.Dialer{}, info, addr, methodName) + return c.(*net.TCPConn), err +} diff --git a/pkg/goptlib-v1.3.0/pt_test.go b/pkg/goptlib-v1.3.0/pt_test.go new file mode 100644 index 00000000..d74d6d74 --- /dev/null +++ b/pkg/goptlib-v1.3.0/pt_test.go @@ -0,0 +1,1089 @@ +package pt + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "path" + "sort" + "testing" + "time" +) + +const testAuthCookiePath = "test_authcookie" + +func TestErrors(t *testing.T) { + Stdout = ioutil.Discard + + var err error + err = envError("XYZ") + if err.Error() != "ENV-ERROR XYZ" { + t.Errorf("unexpected string %q from envError", err.Error()) + } + err = versionError("XYZ") + if err.Error() != "VERSION-ERROR XYZ" { + t.Errorf("unexpected string %q from versionError", err.Error()) + } + err = CmethodError("method", "XYZ") + if err.Error() != "CMETHOD-ERROR method XYZ" { + t.Errorf("unexpected string %q from CmethodError", err.Error()) + } + err = SmethodError("method", "XYZ") + if err.Error() != "SMETHOD-ERROR method XYZ" { + t.Errorf("unexpected string %q from SmethodError", err.Error()) + } + err = ProxyError("XYZ") + if err.Error() != "PROXY-ERROR XYZ" { + t.Errorf("unexpected string %q from ProxyError", err.Error()) + } +} + +func TestKeywordIsSafe(t *testing.T) { + tests := [...]struct { + keyword string + expected bool + }{ + {"", true}, + {"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_", true}, + {"CMETHOD", true}, + {"CMETHOD:", false}, + {"a b c", false}, + {"CMETHOD\x7f", false}, + {"CMETHOD\x80", false}, + {"CMETHOD\x81", false}, + {"CMETHOD\xff", false}, + {"\xffCMETHOD", false}, + {"CMÉTHOD", false}, + } + + for _, input := range tests { + isSafe := keywordIsSafe(input.keyword) + if isSafe != input.expected { + t.Errorf("keywordIsSafe(%q) → %v (expected %v)", + input.keyword, isSafe, input.expected) + } + } +} + +func TestArgIsSafe(t *testing.T) { + tests := [...]struct { + arg string + expected bool + }{ + {"", true}, + {"abc", true}, + {"127.0.0.1:8000", true}, + {"étude", false}, + {"a\nb", false}, + {"a\\b", true}, + {"ab\\", true}, + {"ab\\\n", false}, + {"ab\n\\", false}, + {"abc\x7f", true}, + {"abc\x80", false}, + {"abc\x81", false}, + {"abc\xff", false}, + {"abc\xff", false}, + {"var=GVsbG8\\=", true}, + } + + for _, input := range tests { + isSafe := argIsSafe(input.arg) + if isSafe != input.expected { + t.Errorf("argIsSafe(%q) → %v (expected %v)", + input.arg, isSafe, input.expected) + } + } +} + +func TestGetManagedTransportVer(t *testing.T) { + badTests := [...]string{ + "", + "2", + } + goodTests := [...]struct { + input, expected string + }{ + {"1", "1"}, + {"1,1", "1"}, + {"1,2", "1"}, + {"2,1", "1"}, + } + + Stdout = ioutil.Discard + + os.Clearenv() + _, err := getManagedTransportVer() + if err == nil { + t.Errorf("empty environment unexpectedly succeeded") + } + + for _, input := range badTests { + os.Setenv("TOR_PT_MANAGED_TRANSPORT_VER", input) + _, err := getManagedTransportVer() + if err == nil { + t.Errorf("TOR_PT_MANAGED_TRANSPORT_VER=%q unexpectedly succeeded", input) + } + } + + for _, test := range goodTests { + os.Setenv("TOR_PT_MANAGED_TRANSPORT_VER", test.input) + output, err := getManagedTransportVer() + if err != nil { + t.Errorf("TOR_PT_MANAGED_TRANSPORT_VER=%q unexpectedly returned an error: %s", test.input, err) + } + if output != test.expected { + t.Errorf("TOR_PT_MANAGED_TRANSPORT_VER=%q → %q (expected %q)", test.input, output, test.expected) + } + } +} + +// return true iff the two slices contain the same elements, possibly in a +// different order. +func stringSetsEqual(a, b []string) bool { + ac := make([]string, len(a)) + bc := make([]string, len(b)) + copy(ac, a) + copy(bc, b) + sort.Strings(ac) + sort.Strings(bc) + if len(ac) != len(bc) { + return false + } + for i := 0; i < len(ac); i++ { + if ac[i] != bc[i] { + return false + } + } + return true +} + +func tcpAddrsEqual(a, b *net.TCPAddr) bool { + return a.IP.Equal(b.IP) && a.Port == b.Port +} + +func TestGetClientTransports(t *testing.T) { + tests := [...]struct { + ptClientTransports string + expected []string + }{ + { + "alpha", + []string{"alpha"}, + }, + { + "alpha,beta", + []string{"alpha", "beta"}, + }, + { + "alpha,beta,gamma", + []string{"alpha", "beta", "gamma"}, + }, + // In the past, "*" meant to return all known transport names. + // But now it has no special meaning. + // https://bugs.torproject.org/15612 + { + "*", + []string{"*"}, + }, + { + "alpha,*,gamma", + []string{"alpha", "*", "gamma"}, + }, + // No escaping is defined for TOR_PT_CLIENT_TRANSPORTS. + { + `alpha\,beta`, + []string{`alpha\`, `beta`}, + }, + } + + Stdout = ioutil.Discard + + os.Clearenv() + _, err := getClientTransports() + if err == nil { + t.Errorf("empty environment unexpectedly succeeded") + } + + for _, test := range tests { + os.Setenv("TOR_PT_CLIENT_TRANSPORTS", test.ptClientTransports) + output, err := getClientTransports() + if err != nil { + t.Errorf("TOR_PT_CLIENT_TRANSPORTS=%q unexpectedly returned an error: %s", + test.ptClientTransports, err) + } + if !stringSetsEqual(output, test.expected) { + t.Errorf("TOR_PT_CLIENT_TRANSPORTS=%q → %q (expected %q)", + test.ptClientTransports, output, test.expected) + } + } +} + +func TestResolveAddr(t *testing.T) { + badTests := [...]string{ + "", + "1.2.3.4", + "1.2.3.4:", + "9999", + ":9999", + "[1:2::3:4]", + "[1:2::3:4]:", + "[1::2::3:4]", + "1:2::3:4::9999", + "1:2:3:4::9999", + "localhost:9999", + "[localhost]:9999", + "1.2.3.4:http", + "1.2.3.4:0x50", + "1.2.3.4:-65456", + "1.2.3.4:65536", + "1.2.3.4:80\x00", + "1.2.3.4:80 ", + " 1.2.3.4:80", + "1.2.3.4 : 80", + } + goodTests := [...]struct { + input string + expected net.TCPAddr + }{ + {"1.2.3.4:9999", net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 9999}}, + {"[1:2::3:4]:9999", net.TCPAddr{IP: net.ParseIP("1:2::3:4"), Port: 9999}}, + {"1:2::3:4:9999", net.TCPAddr{IP: net.ParseIP("1:2::3:4"), Port: 9999}}, + } + + for _, input := range badTests { + output, err := resolveAddr(input) + if err == nil { + t.Errorf("%q unexpectedly succeeded: %q", input, output) + } + } + + for _, test := range goodTests { + output, err := resolveAddr(test.input) + if err != nil { + t.Errorf("%q unexpectedly returned an error: %s", test.input, err) + } + if !tcpAddrsEqual(output, &test.expected) { + t.Errorf("%q → %q (expected %q)", test.input, output, test.expected) + } + } +} + +func bindaddrSliceContains(s []Bindaddr, v Bindaddr) bool { + for _, sv := range s { + if sv.MethodName == v.MethodName && tcpAddrsEqual(sv.Addr, v.Addr) { + return true + } + } + return false +} + +func bindaddrSetsEqual(a, b []Bindaddr) bool { + for _, v := range a { + if !bindaddrSliceContains(b, v) { + return false + } + } + for _, v := range b { + if !bindaddrSliceContains(a, v) { + return false + } + } + return true +} + +func TestGetServerBindaddrs(t *testing.T) { + badTests := [...]struct { + ptServerBindaddr string + ptServerTransports string + ptServerTransportOptions string + }{ + // bad TOR_PT_SERVER_BINDADDR + { + "alpha", + "alpha", + "", + }, + { + "alpha-1.2.3.4", + "alpha", + "", + }, + // missing TOR_PT_SERVER_TRANSPORTS + { + "alpha-1.2.3.4:1111", + "", + "alpha:key=value", + }, + // bad TOR_PT_SERVER_TRANSPORT_OPTIONS + { + "alpha-1.2.3.4:1111", + "alpha", + "key=value", + }, + // no escaping is defined for TOR_PT_SERVER_TRANSPORTS or + // TOR_PT_SERVER_BINDADDR. + { + `alpha\,beta-1.2.3.4:1111`, + `alpha\,beta`, + "", + }, + // duplicates in TOR_PT_SERVER_BINDADDR + // https://bugs.torproject.org/21261 + { + `alpha-0.0.0.0:1234,alpha-[::]:1234`, + `alpha`, + "", + }, + { + `alpha-0.0.0.0:1234,alpha-0.0.0.0:1234`, + `alpha`, + "", + }, + } + goodTests := [...]struct { + ptServerBindaddr string + ptServerTransports string + ptServerTransportOptions string + expected []Bindaddr + }{ + { + "alpha-1.2.3.4:1111,beta-[1:2::3:4]:2222", + "alpha,beta,gamma", + "alpha:k1=v1,beta:k2=v2,gamma:k3=v3", + []Bindaddr{ + {"alpha", &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 1111}, Args{"k1": []string{"v1"}}}, + {"beta", &net.TCPAddr{IP: net.ParseIP("1:2::3:4"), Port: 2222}, Args{"k2": []string{"v2"}}}, + }, + }, + { + "alpha-1.2.3.4:1111", + "xxx", + "", + []Bindaddr{}, + }, + { + "alpha-1.2.3.4:1111", + "alpha,beta,gamma", + "", + []Bindaddr{ + {"alpha", &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 1111}, Args{}}, + }, + }, + { + "trebuchet-127.0.0.1:1984,ballista-127.0.0.1:4891", + "trebuchet,ballista", + "trebuchet:secret=nou;trebuchet:cache=/tmp/cache;ballista:secret=yes", + []Bindaddr{ + {"trebuchet", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1984}, Args{"secret": []string{"nou"}, "cache": []string{"/tmp/cache"}}}, + {"ballista", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 4891}, Args{"secret": []string{"yes"}}}, + }, + }, + // In the past, "*" meant to return all known transport names. + // But now it has no special meaning. + // https://bugs.torproject.org/15612 + { + "alpha-1.2.3.4:1111,beta-[1:2::3:4]:2222", + "*", + "", + []Bindaddr{}, + }, + } + + Stdout = ioutil.Discard + + os.Clearenv() + _, err := getServerBindaddrs() + if err == nil { + t.Errorf("empty environment unexpectedly succeeded") + } + + for _, test := range badTests { + os.Setenv("TOR_PT_SERVER_BINDADDR", test.ptServerBindaddr) + os.Setenv("TOR_PT_SERVER_TRANSPORTS", test.ptServerTransports) + os.Setenv("TOR_PT_SERVER_TRANSPORT_OPTIONS", test.ptServerTransportOptions) + _, err := getServerBindaddrs() + if err == nil { + t.Errorf("TOR_PT_SERVER_BINDADDR=%q TOR_PT_SERVER_TRANSPORTS=%q TOR_PT_SERVER_TRANSPORT_OPTIONS=%q unexpectedly succeeded", + test.ptServerBindaddr, test.ptServerTransports, test.ptServerTransportOptions) + } + } + + for _, test := range goodTests { + os.Setenv("TOR_PT_SERVER_BINDADDR", test.ptServerBindaddr) + os.Setenv("TOR_PT_SERVER_TRANSPORTS", test.ptServerTransports) + os.Setenv("TOR_PT_SERVER_TRANSPORT_OPTIONS", test.ptServerTransportOptions) + output, err := getServerBindaddrs() + if err != nil { + t.Errorf("TOR_PT_SERVER_BINDADDR=%q TOR_PT_SERVER_TRANSPORTS=%q TOR_PT_SERVER_TRANSPORT_OPTIONS=%q unexpectedly returned an error: %s", + test.ptServerBindaddr, test.ptServerTransports, test.ptServerTransportOptions, err) + } + if !bindaddrSetsEqual(output, test.expected) { + t.Errorf("TOR_PT_SERVER_BINDADDR=%q TOR_PT_SERVER_TRANSPORTS=%q TOR_PT_SERVER_TRANSPORT_OPTIONS=%q → %q (expected %q)", + test.ptServerBindaddr, test.ptServerTransports, test.ptServerTransportOptions, output, test.expected) + } + } +} + +func TestReadAuthCookie(t *testing.T) { + badTests := [...][]byte{ + []byte(""), + // bad header + []byte("! Impostor ORPort Auth Cookie !\x0a0123456789ABCDEF0123456789ABCDEF"), + // too short + []byte("! Extended ORPort Auth Cookie !\x0a0123456789ABCDEF0123456789ABCDE"), + // too long + []byte("! Extended ORPort Auth Cookie !\x0a0123456789ABCDEF0123456789ABCDEFX"), + } + goodTests := [...][]byte{ + []byte("! Extended ORPort Auth Cookie !\x0a0123456789ABCDEF0123456789ABCDEF"), + } + + for _, input := range badTests { + var buf bytes.Buffer + buf.Write(input) + _, err := readAuthCookie(&buf) + if err == nil { + t.Errorf("%q unexpectedly succeeded", input) + } + } + + for _, input := range goodTests { + var buf bytes.Buffer + buf.Write(input) + cookie, err := readAuthCookie(&buf) + if err != nil { + t.Errorf("%q unexpectedly returned an error: %s", input, err) + } + if !bytes.Equal(cookie, input[32:64]) { + t.Errorf("%q → %q (expected %q)", input, cookie, input[:32]) + } + } +} + +func TestComputeServerHash(t *testing.T) { + authCookie := make([]byte, 32) + clientNonce := make([]byte, 32) + serverNonce := make([]byte, 32) + // hmac.new("\x00"*32, "ExtORPort authentication server-to-client hash" + "\x00"*64, hashlib.sha256).hexdigest() + expected := []byte("\x9e\x22\x19\x19\x98\x2a\x84\xf7\x5f\xaf\x60\xef\x92\x69\x49\x79\x62\x68\xc9\x78\x33\xe0\x69\x60\xff\x26\x53\x69\xa9\x0f\xd6\xd8") + hash := computeServerHash(authCookie, clientNonce, serverNonce) + if !bytes.Equal(hash, expected) { + t.Errorf("%x %x %x → %x (expected %x)", authCookie, + clientNonce, serverNonce, hash, expected) + } +} + +func TestComputeClientHash(t *testing.T) { + authCookie := make([]byte, 32) + clientNonce := make([]byte, 32) + serverNonce := make([]byte, 32) + // hmac.new("\x00"*32, "ExtORPort authentication client-to-server hash" + "\x00"*64, hashlib.sha256).hexdigest() + expected := []byte("\x0f\x36\x8b\x1b\xee\x24\xaa\xbc\x54\xa9\x11\x4c\xe0\x6c\x07\x0f\x3e\xd9\x9d\x0d\x36\x8f\x59\x9c\xcc\x6d\xfd\xc8\xbf\x45\x7a\x62") + hash := computeClientHash(authCookie, clientNonce, serverNonce) + if !bytes.Equal(hash, expected) { + t.Errorf("%x %x %x → %x (expected %x)", authCookie, + clientNonce, serverNonce, hash, expected) + } +} + +// Elide a byte slice in case it's really long. +func fmtBytes(s []byte) string { + if len(s) > 100 { + return fmt.Sprintf("%q...(%d bytes)", s[:5], len(s)) + } else { + return fmt.Sprintf("%q", s) + } +} + +func TestExtOrSendCommand(t *testing.T) { + badTests := [...]struct { + cmd uint16 + body []byte + }{ + {0x0, make([]byte, 65536)}, + {0x1234, make([]byte, 65536)}, + } + longBody := [65535 + 2 + 2]byte{0x12, 0x34, 0xff, 0xff} + goodTests := [...]struct { + cmd uint16 + body []byte + expected []byte + }{ + {0x0, []byte(""), []byte("\x00\x00\x00\x00")}, + {0x5, []byte(""), []byte("\x00\x05\x00\x00")}, + {0xfffe, []byte(""), []byte("\xff\xfe\x00\x00")}, + {0xffff, []byte(""), []byte("\xff\xff\x00\x00")}, + {0x1234, []byte("hello"), []byte("\x12\x34\x00\x05hello")}, + {0x1234, make([]byte, 65535), longBody[:]}, + } + + for _, test := range badTests { + var buf bytes.Buffer + err := extOrPortSendCommand(&buf, test.cmd, test.body) + if err == nil { + t.Errorf("0x%04x %s unexpectedly succeeded", test.cmd, fmtBytes(test.body)) + } + } + + for _, test := range goodTests { + var buf bytes.Buffer + err := extOrPortSendCommand(&buf, test.cmd, test.body) + if err != nil { + t.Errorf("0x%04x %s unexpectedly returned an error: %s", test.cmd, fmtBytes(test.body), err) + } + p := make([]byte, 65535+2+2) + n, err := buf.Read(p) + if err != nil { + t.Fatal(err) + } + output := p[:n] + if !bytes.Equal(output, test.expected) { + t.Errorf("0x%04x %s → %s (expected %s)", test.cmd, fmtBytes(test.body), + fmtBytes(output), fmtBytes(test.expected)) + } + } +} + +func TestExtOrSendUserAddr(t *testing.T) { + addrs := [...]string{ + "0.0.0.0:0", + "1.2.3.4:9999", + "255.255.255.255:65535", + "[::]:0", + "[ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255]:63335", + } + + for _, addr := range addrs { + var buf bytes.Buffer + err := extOrPortSendUserAddr(&buf, addr) + if err != nil { + t.Errorf("%s unexpectedly returned an error: %s", addr, err) + } + var cmd, length uint16 + binary.Read(&buf, binary.BigEndian, &cmd) + if cmd != extOrCmdUserAddr { + t.Errorf("%s → cmd 0x%04x (expected 0x%04x)", addr, cmd, extOrCmdUserAddr) + } + binary.Read(&buf, binary.BigEndian, &length) + p := make([]byte, length+1) + n, err := buf.Read(p) + if n != int(length) { + t.Errorf("%s said length %d but had at least %d", addr, length, n) + } + // test that parsing the address gives something equivalent to + // parsing the original. + inputAddr, err := resolveAddr(addr) + if err != nil { + t.Fatal(err) + } + outputAddr, err := resolveAddr(string(p[:n])) + if err != nil { + t.Fatal(err) + } + if !tcpAddrsEqual(inputAddr, outputAddr) { + t.Errorf("%s → %s", addr, outputAddr) + } + } +} + +func TestExtOrPortSendTransport(t *testing.T) { + tests := [...]struct { + methodName string + expected []byte + }{ + {"", []byte("\x00\x02\x00\x00")}, + {"a", []byte("\x00\x02\x00\x01a")}, + {"alpha", []byte("\x00\x02\x00\x05alpha")}, + } + + for _, test := range tests { + var buf bytes.Buffer + err := extOrPortSendTransport(&buf, test.methodName) + if err != nil { + t.Errorf("%q unexpectedly returned an error: %s", test.methodName, err) + } + p := make([]byte, 1024) + n, err := buf.Read(p) + if err != nil { + t.Fatal(err) + } + output := p[:n] + if !bytes.Equal(output, test.expected) { + t.Errorf("%q → %s (expected %s)", test.methodName, + fmtBytes(output), fmtBytes(test.expected)) + } + } +} + +func TestExtOrPortSendDone(t *testing.T) { + expected := []byte("\x00\x00\x00\x00") + + var buf bytes.Buffer + err := extOrPortSendDone(&buf) + if err != nil { + t.Errorf("unexpectedly returned an error: %s", err) + } + p := make([]byte, 1024) + n, err := buf.Read(p) + if err != nil { + t.Fatal(err) + } + output := p[:n] + if !bytes.Equal(output, expected) { + t.Errorf("→ %s (expected %s)", fmtBytes(output), fmtBytes(expected)) + } +} + +func TestExtOrPortRecvCommand(t *testing.T) { + badTests := [...][]byte{ + []byte(""), + []byte("\x12"), + []byte("\x12\x34"), + []byte("\x12\x34\x00"), + []byte("\x12\x34\x00\x01"), + } + goodTests := [...]struct { + input []byte + cmd uint16 + body []byte + leftover []byte + }{ + { + []byte("\x12\x34\x00\x00"), + 0x1234, []byte(""), []byte(""), + }, + { + []byte("\x12\x34\x00\x00more"), + 0x1234, []byte(""), []byte("more"), + }, + { + []byte("\x12\x34\x00\x04body"), + 0x1234, []byte("body"), []byte(""), + }, + { + []byte("\x12\x34\x00\x04bodymore"), + 0x1234, []byte("body"), []byte("more"), + }, + } + + for _, input := range badTests { + var buf bytes.Buffer + buf.Write(input) + _, _, err := extOrPortRecvCommand(&buf) + if err == nil { + t.Errorf("%q unexpectedly succeeded", fmtBytes(input)) + } + } + + for _, test := range goodTests { + var buf bytes.Buffer + buf.Write(test.input) + cmd, body, err := extOrPortRecvCommand(&buf) + if err != nil { + t.Errorf("%s unexpectedly returned an error: %s", fmtBytes(test.input), err) + } + if cmd != test.cmd { + t.Errorf("%s → cmd 0x%04x (expected 0x%04x)", fmtBytes(test.input), cmd, test.cmd) + } + if !bytes.Equal(body, test.body) { + t.Errorf("%s → body %s (expected %s)", fmtBytes(test.input), + fmtBytes(body), fmtBytes(test.body)) + } + p := make([]byte, 1024) + n, err := buf.Read(p) + if err != nil && err != io.EOF { + t.Fatal(err) + } + leftover := p[:n] + if !bytes.Equal(leftover, test.leftover) { + t.Errorf("%s → leftover %s (expected %s)", fmtBytes(test.input), + fmtBytes(leftover), fmtBytes(test.leftover)) + } + } +} + +// set up so that extOrPortSetMetadata can write to one buffer and read from another. +type mockSetMetadataBuf struct { + ReadBuf bytes.Buffer + WriteBuf bytes.Buffer +} + +func (buf *mockSetMetadataBuf) Read(p []byte) (int, error) { + return buf.ReadBuf.Read(p) +} + +func (buf *mockSetMetadataBuf) Write(p []byte) (int, error) { + return buf.WriteBuf.Write(p) +} + +func testExtOrPortSetMetadataIndividual(t *testing.T, addr, methodName string) { + var err error + var buf mockSetMetadataBuf + // fake an OKAY response. + err = extOrPortSendCommand(&buf.ReadBuf, extOrCmdOkay, []byte{}) + if err != nil { + panic(err) + } + err = extOrPortSetMetadata(&buf, addr, methodName) + if err != nil { + t.Fatalf("error in extOrPortSetMetadata: %s", err) + } + for { + cmd, body, err := extOrPortRecvCommand(&buf.WriteBuf) + if err != nil { + t.Fatalf("error in extOrPortRecvCommand: %s", err) + } + if cmd == extOrCmdDone { + break + } + if addr != "" && cmd == extOrCmdUserAddr { + if string(body) != addr { + t.Errorf("addr=%q methodName=%q got USERADDR with body %q (expected %q)", addr, methodName, body, addr) + } + continue + } + if methodName != "" && cmd == extOrCmdTransport { + if string(body) != methodName { + t.Errorf("addr=%q methodName=%q got TRANSPORT with body %q (expected %q)", addr, methodName, body, methodName) + } + continue + } + t.Errorf("addr=%q methodName=%q got unknown cmd %d body %q", addr, methodName, cmd, body) + } +} + +func TestExtOrPortSetMetadata(t *testing.T) { + const addr = "127.0.0.1:40000" + const methodName = "alpha" + testExtOrPortSetMetadataIndividual(t, "", "") + testExtOrPortSetMetadataIndividual(t, addr, "") + testExtOrPortSetMetadataIndividual(t, "", methodName) + testExtOrPortSetMetadataIndividual(t, addr, methodName) +} + +func simulateServerExtOrPortAuth(r io.Reader, w io.Writer, authCookie []byte) error { + // send auth types + _, err := w.Write([]byte{1, 0}) + if err != nil { + return err + } + // read client auth type + buf := make([]byte, 1) + _, err = io.ReadFull(r, buf) + if err != nil { + return err + } + if buf[0] != 1 { + return fmt.Errorf("didn't get client auth type 1") + } + // read client nonce + clientNonce := make([]byte, 32) + _, err = io.ReadFull(r, clientNonce) + if err != nil { + return err + } + // send server hash and nonce + serverNonce := make([]byte, 32) + serverHash := computeServerHash(authCookie, clientNonce, serverNonce) + _, err = w.Write(serverHash) + if err != nil { + return err + } + _, err = w.Write(serverNonce) + if err != nil { + return err + } + // read client hash + clientHash := make([]byte, 32) + _, err = io.ReadFull(r, clientHash) + if err != nil { + return err + } + // send success status + _, err = w.Write([]byte{1}) + if err != nil { + return err + } + return nil +} + +type failSetDeadlineAfter struct { + n int + err error +} + +func (c *failSetDeadlineAfter) try() error { + if c.n > 0 { + c.n-- + return nil + } + return c.err +} + +func (c *failSetDeadlineAfter) SetDeadline(_ time.Time) error { + return c.try() +} + +func (c *failSetDeadlineAfter) SetReadDeadline(_ time.Time) error { + return c.try() +} + +func (c *failSetDeadlineAfter) SetWriteDeadline(_ time.Time) error { + return c.try() +} + +// a fake Conn whose Set*Deadline functions fail after a certain number of +// calls. +type connFailSetDeadline struct { + io.Reader + io.Writer + failSetDeadlineAfter +} + +func (c *connFailSetDeadline) Close() error { + return nil +} + +func (c *connFailSetDeadline) LocalAddr() net.Addr { + return &net.IPAddr{net.IPv4(0, 0, 0, 0), ""} +} + +func (c *connFailSetDeadline) RemoteAddr() net.Addr { + return &net.IPAddr{net.IPv4(0, 0, 0, 0), ""} +} + +// Test that a failure of SetDeadline is reported. +func TestExtOrPortSetupFailSetDeadline(t *testing.T) { + authCookie, err := readAuthCookieFile(testAuthCookiePath) + if err != nil { + panic(err) + } + + // extOrPortSetup calls SetDeadline twice, so try failing the call after + // differing delays. + expectedErr := fmt.Errorf("distinguished error") + for _, delay := range []int{0, 1, 2} { + upstreamR, upstreamW := io.Pipe() + downstreamR, downstreamW := io.Pipe() + + // mock ExtORPort to talk to + go func() { + // handle auth + err := simulateServerExtOrPortAuth(upstreamR, downstreamW, authCookie) + if err != nil { + return + } + // discard succeeding client data + go func() { + io.Copy(ioutil.Discard, upstreamR) + }() + // fake an OKAY response. + err = extOrPortSendCommand(downstreamW, extOrCmdOkay, []byte{}) + if err != nil { + return + } + }() + + // make a Conn that will fail SetDeadline after a certain number + // of calls. + s := &connFailSetDeadline{downstreamR, upstreamW, failSetDeadlineAfter{delay, expectedErr}} + serverInfo := &ServerInfo{AuthCookiePath: testAuthCookiePath} + err = extOrPortSetup(s, 1*time.Second, serverInfo, "", "") + if delay < 2 && err != expectedErr { + t.Fatalf("delay %v: expected error %v, got %v", delay, expectedErr, err) + } else if delay >= 2 && err != nil { + t.Fatalf("delay %v: got error %v", delay, err) + } + } +} + +func TestMakeStateDir(t *testing.T) { + os.Clearenv() + + // TOR_PT_STATE_LOCATION not set. + _, err := MakeStateDir() + if err == nil { + t.Errorf("empty environment unexpectedly succeeded") + } + + // Setup the scratch directory. + tempDir, err := ioutil.TempDir("", "testMakeStateDir") + if err != nil { + t.Fatalf("ioutil.TempDir failed: %s", err) + } + defer os.RemoveAll(tempDir) + + goodTests := [...]string{ + // Already existing directory. + tempDir, + + // Nonexistent directory, parent exists. + path.Join(tempDir, "parentExists"), + + // Nonexistent directory, parent doesn't exist. + path.Join(tempDir, "missingParent", "parentMissing"), + } + for _, test := range goodTests { + os.Setenv("TOR_PT_STATE_LOCATION", test) + dir, err := MakeStateDir() + if err != nil { + t.Errorf("MakeStateDir unexpectedly failed: %s", err) + } + if dir != test { + t.Errorf("MakeStateDir returned an unexpected path %s (expecting %s)", dir, test) + } + } + + // Name already exists, but is an ordinary file. + tempFile := path.Join(tempDir, "file") + f, err := os.Create(tempFile) + if err != nil { + t.Fatalf("os.Create failed: %s", err) + } + defer f.Close() + os.Setenv("TOR_PT_STATE_LOCATION", tempFile) + _, err = MakeStateDir() + if err == nil { + t.Errorf("MakeStateDir with a file unexpectedly succeeded") + } + + // Directory name that cannot be created. (Subdir of a file) + os.Setenv("TOR_PT_STATE_LOCATION", path.Join(tempFile, "subDir")) + _, err = MakeStateDir() + if err == nil { + t.Errorf("MakeStateDir with a subdirectory of a file unexpectedly succeeded") + } +} + +// Compare with unescape_string in tor's src/lib/encoding/cstring.c. That +// function additionally allows hex escapes, but control-spec.txt's CString +// doesn't say anything about that. +func decodeCString(enc string) (string, error) { + var result bytes.Buffer + b := []byte(enc) + state := "^" + number := 0 + i := 0 + for i < len(b) { + c := b[i] + switch state { + case "^": + if c != '"' { + return "", fmt.Errorf("missing start quote") + } + state = "." + case ".": + switch c { + case '\\': + state = "\\" + case '"': + state = "$" + default: + result.WriteByte(c) + } + case "\\": + switch c { + case 'n': + result.WriteByte('\n') + state = "." + case 't': + result.WriteByte('\t') + state = "." + case 'r': + result.WriteByte('\r') + state = "." + case '"', '\\': + result.WriteByte(c) + state = "." + case '0', '1', '2', '3', '4', '5', '6', '7': + number = int(c - '0') + state = "o1" + default: + return "", fmt.Errorf("unknown escape \\%c", c) + } + case "o1": // 1 octal digit read + switch c { + case '0', '1', '2', '3', '4', '5', '6', '7': + number = number*8 + int(c-'0') + state = "o2" + default: + if number > 255 { + return "", fmt.Errorf("invalid octal escape") + } + result.WriteByte(byte(number)) + state = "." + continue // process the current byte again + } + case "o2": // 2 octal digits read + switch c { + case '0', '1', '2', '3', '4', '5', '6', '7': + number = number*8 + int(c-'0') + if number > 255 { + return "", fmt.Errorf("invalid octal escape") + } + result.WriteByte(byte(number)) + state = "." + default: + if number > 255 { + return "", fmt.Errorf("invalid octal escape") + } + result.WriteByte(byte(number)) + state = "." + continue // process the current byte again + } + case "$": + return "", fmt.Errorf("trailing garbage") + } + i++ + } + if state != "$" { + return "", fmt.Errorf("unexpected end of string") + } + return result.String(), nil +} + +func roundtripCString(src string) (string, error) { + enc := encodeCString(src) + dec, err := decodeCString(enc) + if err != nil { + return enc, fmt.Errorf("failed to decode: %+q → %+q: %v", src, enc, err) + } + if dec != src { + return enc, fmt.Errorf("roundtrip failed: %+q → %+q → %+q", src, enc, dec) + } + return enc, nil +} + +func TestEncodeCString(t *testing.T) { + tests := []string{ + "", + "\"", + "\"\"", + "abc\"def", + "\\", + "\\\\", + "\x0123abc", // trap here is if you encode '\x01' as "\\1"; it would join with the following digits: "\\123abc". + "\n\r\t\x7f", + "\\377", + } + allBytes := make([]byte, 256) + for i := 0; i < len(allBytes); i++ { + allBytes[i] = byte(i) + } + tests = append(tests, string(allBytes)) + + for _, test := range tests { + enc, err := roundtripCString(test) + if err != nil { + t.Error(err) + } + if !argIsSafe(enc) { + t.Errorf("escaping %+q resulted in non-safe %+q", test, enc) + } + } +} diff --git a/pkg/goptlib-v1.3.0/socks.go b/pkg/goptlib-v1.3.0/socks.go new file mode 100644 index 00000000..29827d96 --- /dev/null +++ b/pkg/goptlib-v1.3.0/socks.go @@ -0,0 +1,507 @@ +package pt + +import ( + "bufio" + "fmt" + "io" + "net" + "time" +) + +const ( + socksVersion = 0x05 + + socksAuthNoneRequired = 0x00 + socksAuthUsernamePassword = 0x02 + socksAuthNoAcceptableMethods = 0xff + + socksCmdConnect = 0x01 + socksRsv = 0x00 + + socksAtypeV4 = 0x01 + socksAtypeDomainName = 0x03 + socksAtypeV6 = 0x04 + + socksAuthRFC1929Ver = 0x01 + socksAuthRFC1929Success = 0x00 + socksAuthRFC1929Fail = 0x01 + + socksRepSucceeded = 0x00 + // "general SOCKS server failure" + SocksRepGeneralFailure = 0x01 + // "connection not allowed by ruleset" + SocksRepConnectionNotAllowed = 0x02 + // "Network unreachable" + SocksRepNetworkUnreachable = 0x03 + // "Host unreachable" + SocksRepHostUnreachable = 0x04 + // "Connection refused" + SocksRepConnectionRefused = 0x05 + // "TTL expired" + SocksRepTTLExpired = 0x06 + // "Command not supported" + SocksRepCommandNotSupported = 0x07 + // "Address type not supported" + SocksRepAddressNotSupported = 0x08 +) + +// Put a sanity timeout on how long we wait for a SOCKS request. +const socksRequestTimeout = 5 * time.Second + +// SocksRequest describes a SOCKS request. +type SocksRequest struct { + // The endpoint requested by the client as a "host:port" string. + Target string + // The userid string sent by the client. + Username string + // The password string sent by the client. + Password string + // The parsed contents of Username as a key–value mapping. + Args Args +} + +// SocksConn encapsulates a net.Conn and information associated with a SOCKS request. +type SocksConn struct { + net.Conn + Req SocksRequest +} + +// Send a message to the proxy client that access to the given address is +// granted. Addr is ignored, and "0.0.0.0:0" is always sent back for +// BND.ADDR/BND.PORT in the SOCKS response. +func (conn *SocksConn) Grant(addr *net.TCPAddr) error { + return sendSocks5ResponseGranted(conn) +} + +// Send a message to the proxy client that access was rejected or failed. This +// sends back a "General Failure" error code. RejectReason should be used if +// more specific error reporting is desired. +func (conn *SocksConn) Reject() error { + return conn.RejectReason(SocksRepGeneralFailure) +} + +// Send a message to the proxy client that access was rejected, with the +// specific error code indicating the reason behind the rejection. +func (conn *SocksConn) RejectReason(reason byte) error { + return sendSocks5ResponseRejected(conn, reason) +} + +// SocksListener wraps a net.Listener in order to read a SOCKS request on Accept. +// +// func handleConn(conn *pt.SocksConn) error { +// defer conn.Close() +// remote, err := net.Dial("tcp", conn.Req.Target) +// if err != nil { +// conn.Reject() +// return err +// } +// defer remote.Close() +// err = conn.Grant(remote.RemoteAddr().(*net.TCPAddr)) +// if err != nil { +// return err +// } +// // do something with conn and remote +// return nil +// } +// ... +// ln, err := pt.ListenSocks("tcp", "127.0.0.1:0") +// if err != nil { +// panic(err.Error()) +// } +// for { +// conn, err := ln.AcceptSocks() +// if err != nil { +// log.Printf("accept error: %s", err) +// if e, ok := err.(net.Error); ok && e.Temporary() { +// continue +// } +// break +// } +// go handleConn(conn) +// } +type SocksListener struct { + net.Listener +} + +// Open a net.Listener according to network and laddr, and return it as a +// SocksListener. +func ListenSocks(network, laddr string) (*SocksListener, error) { + ln, err := net.Listen(network, laddr) + if err != nil { + return nil, err + } + return NewSocksListener(ln), nil +} + +// Create a new SocksListener wrapping the given net.Listener. +func NewSocksListener(ln net.Listener) *SocksListener { + return &SocksListener{ln} +} + +// Accept is the same as AcceptSocks, except that it returns a generic net.Conn. +// It is present for the sake of satisfying the net.Listener interface. +func (ln *SocksListener) Accept() (net.Conn, error) { + return ln.AcceptSocks() +} + +// Call Accept on the wrapped net.Listener, do SOCKS negotiation, and return a +// SocksConn. After accepting, you must call either conn.Grant or conn.Reject +// (presumably after trying to connect to conn.Req.Target). +// +// Errors returned by AcceptSocks may be temporary (for example, EOF while +// reading the request, or a badly formatted userid string), or permanent (e.g., +// the underlying socket is closed). You can determine whether an error is +// temporary and take appropriate action with a type conversion to net.Error. +// For example: +// +// for { +// conn, err := ln.AcceptSocks() +// if err != nil { +// if e, ok := err.(net.Error); ok && e.Temporary() { +// log.Printf("temporary accept error; trying again: %s", err) +// continue +// } +// log.Printf("permanent accept error; giving up: %s", err) +// break +// } +// go handleConn(conn) +// } +func (ln *SocksListener) AcceptSocks() (*SocksConn, error) { +retry: + c, err := ln.Listener.Accept() + if err != nil { + return nil, err + } + conn := new(SocksConn) + conn.Conn = c + err = conn.SetDeadline(time.Now().Add(socksRequestTimeout)) + if err != nil { + conn.Close() + goto retry + } + conn.Req, err = socks5Handshake(conn) + if err != nil { + conn.Close() + goto retry + } + err = conn.SetDeadline(time.Time{}) + if err != nil { + conn.Close() + goto retry + } + return conn, nil +} + +// Returns "socks5", suitable to be included in a call to Cmethod. +func (ln *SocksListener) Version() string { + return "socks5" +} + +// socks5handshake conducts the SOCKS5 handshake up to the point where the +// client command is read and the proxy must open the outgoing connection. +// Returns a SocksRequest. +func socks5Handshake(s io.ReadWriter) (req SocksRequest, err error) { + rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s)) + + // Negotiate the authentication method. + var method byte + if method, err = socksNegotiateAuth(rw); err != nil { + return + } + + // Authenticate the client. + if err = socksAuthenticate(rw, method, &req); err != nil { + return + } + + // Read the command. + err = socksReadCommand(rw, &req) + return +} + +// socksNegotiateAuth negotiates the authentication method and returns the +// selected method as a byte. On negotiation failures an error is returned. +func socksNegotiateAuth(rw *bufio.ReadWriter) (method byte, err error) { + // Validate the version. + if err = socksReadByteVerify(rw, "version", socksVersion); err != nil { + return + } + + // Read the number of methods. + var nmethods byte + if nmethods, err = socksReadByte(rw); err != nil { + return + } + + // Read the methods. + var methods []byte + if methods, err = socksReadBytes(rw, int(nmethods)); err != nil { + return + } + + // Pick the most "suitable" method. + method = socksAuthNoAcceptableMethods + for _, m := range methods { + switch m { + case socksAuthNoneRequired: + // Pick Username/Password over None if the client happens to + // send both. + if method == socksAuthNoAcceptableMethods { + method = m + } + + case socksAuthUsernamePassword: + method = m + } + } + + // Send the negotiated method. + var msg [2]byte + msg[0] = socksVersion + msg[1] = method + if _, err = rw.Writer.Write(msg[:]); err != nil { + return + } + + if err = socksFlushBuffers(rw); err != nil { + return + } + return +} + +// socksAuthenticate authenticates the client via the chosen authentication +// mechanism. +func socksAuthenticate(rw *bufio.ReadWriter, method byte, req *SocksRequest) (err error) { + switch method { + case socksAuthNoneRequired: + // Straight into reading the connect. + + case socksAuthUsernamePassword: + if err = socksAuthRFC1929(rw, req); err != nil { + return + } + + case socksAuthNoAcceptableMethods: + err = fmt.Errorf("SOCKS method select had no compatible methods") + return + + default: + err = fmt.Errorf("SOCKS method select picked a unsupported method 0x%02x", method) + return + } + + if err = socksFlushBuffers(rw); err != nil { + return + } + return +} + +// socksAuthRFC1929 authenticates the client via RFC 1929 username/password +// auth. As a design decision any valid username/password is accepted as this +// field is primarily used as an out-of-band argument passing mechanism for +// pluggable transports. +func socksAuthRFC1929(rw *bufio.ReadWriter, req *SocksRequest) (err error) { + sendErrResp := func() { + // Swallow the write/flush error here, we are going to close the + // connection and the original failure is more useful. + resp := []byte{socksAuthRFC1929Ver, socksAuthRFC1929Fail} + rw.Write(resp[:]) + socksFlushBuffers(rw) + } + + // Validate the fixed parts of the command message. + if err = socksReadByteVerify(rw, "auth version", socksAuthRFC1929Ver); err != nil { + sendErrResp() + return + } + + // Read the username. + var ulen byte + if ulen, err = socksReadByte(rw); err != nil { + return + } + if ulen < 1 { + sendErrResp() + err = fmt.Errorf("RFC1929 username with 0 length") + return + } + var uname []byte + if uname, err = socksReadBytes(rw, int(ulen)); err != nil { + return + } + req.Username = string(uname) + + // Read the password. + var plen byte + if plen, err = socksReadByte(rw); err != nil { + return + } + if plen < 1 { + sendErrResp() + err = fmt.Errorf("RFC1929 password with 0 length") + return + } + var passwd []byte + if passwd, err = socksReadBytes(rw, int(plen)); err != nil { + return + } + if !(plen == 1 && passwd[0] == 0x00) { + // tor will set the password to 'NUL' if there are no arguments. + req.Password = string(passwd) + } + + // Mash the username/password together and parse it as a pluggable + // transport argument string. + if req.Args, err = parseClientParameters(req.Username + req.Password); err != nil { + sendErrResp() + } else { + resp := []byte{socksAuthRFC1929Ver, socksAuthRFC1929Success} + _, err = rw.Write(resp[:]) + } + return +} + +// socksReadCommand reads a SOCKS5 client command and parses out the relevant +// fields into a SocksRequest. Only CMD_CONNECT is supported. +func socksReadCommand(rw *bufio.ReadWriter, req *SocksRequest) (err error) { + sendErrResp := func(reason byte) { + // Swallow errors that occur when writing/flushing the response, + // connection will be closed anyway. + sendSocks5ResponseRejected(rw, reason) + socksFlushBuffers(rw) + } + + // Validate the fixed parts of the command message. + if err = socksReadByteVerify(rw, "version", socksVersion); err != nil { + sendErrResp(SocksRepGeneralFailure) + return + } + if err = socksReadByteVerify(rw, "command", socksCmdConnect); err != nil { + sendErrResp(SocksRepCommandNotSupported) + return + } + if err = socksReadByteVerify(rw, "reserved", socksRsv); err != nil { + sendErrResp(SocksRepGeneralFailure) + return + } + + // Read the destination address/port. + // XXX: This should probably eventually send socks 5 error messages instead + // of rudely closing connections on invalid addresses. + var atype byte + if atype, err = socksReadByte(rw); err != nil { + return + } + var host string + switch atype { + case socksAtypeV4: + var addr []byte + if addr, err = socksReadBytes(rw, net.IPv4len); err != nil { + return + } + host = net.IPv4(addr[0], addr[1], addr[2], addr[3]).String() + + case socksAtypeDomainName: + var alen byte + if alen, err = socksReadByte(rw); err != nil { + return + } + if alen == 0 { + err = fmt.Errorf("SOCKS request had domain name with 0 length") + return + } + var addr []byte + if addr, err = socksReadBytes(rw, int(alen)); err != nil { + return + } + host = string(addr) + + case socksAtypeV6: + var rawAddr []byte + if rawAddr, err = socksReadBytes(rw, net.IPv6len); err != nil { + return + } + addr := make(net.IP, net.IPv6len) + copy(addr[:], rawAddr[:]) + host = fmt.Sprintf("[%s]", addr.String()) + + default: + sendErrResp(SocksRepAddressNotSupported) + err = fmt.Errorf("SOCKS request had unsupported address type 0x%02x", atype) + return + } + var rawPort []byte + if rawPort, err = socksReadBytes(rw, 2); err != nil { + return + } + port := int(rawPort[0])<<8 | int(rawPort[1])<<0 + + if err = socksFlushBuffers(rw); err != nil { + return + } + + req.Target = fmt.Sprintf("%s:%d", host, port) + return +} + +// Send a SOCKS5 response with the given code. BND.ADDR/BND.PORT is always the +// IPv4 address/port "0.0.0.0:0". +func sendSocks5Response(w io.Writer, code byte) error { + resp := make([]byte, 4+4+2) + resp[0] = socksVersion + resp[1] = code + resp[2] = socksRsv + resp[3] = socksAtypeV4 + + // BND.ADDR/BND.PORT should be the address and port that the outgoing + // connection is bound to on the proxy, but Tor does not use this + // information, so all zeroes are sent. + + _, err := w.Write(resp[:]) + return err +} + +// Send a SOCKS5 response code 0x00. +func sendSocks5ResponseGranted(w io.Writer) error { + return sendSocks5Response(w, socksRepSucceeded) +} + +// Send a SOCKS5 response with the provided failure reason. +func sendSocks5ResponseRejected(w io.Writer, reason byte) error { + return sendSocks5Response(w, reason) +} + +func socksFlushBuffers(rw *bufio.ReadWriter) error { + if err := rw.Writer.Flush(); err != nil { + return err + } + if rw.Reader.Buffered() > 0 { + return fmt.Errorf("%d bytes left after SOCKS message", rw.Reader.Buffered()) + } + return nil +} + +func socksReadByte(rw *bufio.ReadWriter) (byte, error) { + return rw.Reader.ReadByte() +} + +func socksReadBytes(rw *bufio.ReadWriter, n int) ([]byte, error) { + ret := make([]byte, n) + if _, err := io.ReadFull(rw.Reader, ret); err != nil { + return nil, err + } + return ret, nil +} + +func socksReadByteVerify(rw *bufio.ReadWriter, descr string, expected byte) error { + val, err := socksReadByte(rw) + if err != nil { + return err + } + if val != expected { + return fmt.Errorf("SOCKS message field %s was 0x%02x, not 0x%02x", descr, val, expected) + } + return nil +} + +var _ net.Listener = (*SocksListener)(nil) diff --git a/pkg/goptlib-v1.3.0/socks_test.go b/pkg/goptlib-v1.3.0/socks_test.go new file mode 100644 index 00000000..2acc414e --- /dev/null +++ b/pkg/goptlib-v1.3.0/socks_test.go @@ -0,0 +1,474 @@ +package pt + +import ( + "bufio" + "bytes" + "encoding/hex" + "errors" + "io" + "net" + "testing" + "time" +) + +// testReadWriter is a bytes.Buffer backed io.ReadWriter used for testing. The +// Read and Write routines are to be used by the component being tested. Data +// can be written to and read back via the writeHex and readHex routines. +type testReadWriter struct { + readBuf bytes.Buffer + writeBuf bytes.Buffer +} + +func (c *testReadWriter) Read(buf []byte) (n int, err error) { + return c.readBuf.Read(buf) +} + +func (c *testReadWriter) Write(buf []byte) (n int, err error) { + return c.writeBuf.Write(buf) +} + +func (c *testReadWriter) writeHex(str string) (n int, err error) { + var buf []byte + if buf, err = hex.DecodeString(str); err != nil { + return + } + return c.readBuf.Write(buf) +} + +func (c *testReadWriter) readHex() string { + return hex.EncodeToString(c.writeBuf.Bytes()) +} + +func (c *testReadWriter) toBufio() *bufio.ReadWriter { + return bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c)) +} + +func (c *testReadWriter) reset() { + c.readBuf.Reset() + c.writeBuf.Reset() +} + +// TestAuthInvalidVersion tests auth negotiation with an invalid version. +func TestAuthInvalidVersion(t *testing.T) { + c := new(testReadWriter) + + // VER = 03, NMETHODS = 01, METHODS = [00] + c.writeHex("030100") + if _, err := socksNegotiateAuth(c.toBufio()); err == nil { + t.Error("socksNegotiateAuth(InvalidVersion) succeded") + } +} + +// TestAuthInvalidNMethods tests auth negotiaton with no methods. +func TestAuthInvalidNMethods(t *testing.T) { + c := new(testReadWriter) + var err error + var method byte + + // VER = 05, NMETHODS = 00 + c.writeHex("0500") + if method, err = socksNegotiateAuth(c.toBufio()); err != nil { + t.Error("socksNegotiateAuth(No Methods) failed:", err) + } + if method != socksAuthNoAcceptableMethods { + t.Error("socksNegotiateAuth(No Methods) picked unexpected method:", method) + } + if msg := c.readHex(); msg != "05ff" { + t.Error("socksNegotiateAuth(No Methods) invalid response:", msg) + } +} + +// TestAuthNoneRequired tests auth negotiaton with NO AUTHENTICATION REQUIRED. +func TestAuthNoneRequired(t *testing.T) { + c := new(testReadWriter) + var err error + var method byte + + // VER = 05, NMETHODS = 01, METHODS = [00] + c.writeHex("050100") + if method, err = socksNegotiateAuth(c.toBufio()); err != nil { + t.Error("socksNegotiateAuth(None) failed:", err) + } + if method != socksAuthNoneRequired { + t.Error("socksNegotiateAuth(None) unexpected method:", method) + } + if msg := c.readHex(); msg != "0500" { + t.Error("socksNegotiateAuth(None) invalid response:", msg) + } +} + +// TestAuthUsernamePassword tests auth negotiation with USERNAME/PASSWORD. +func TestAuthUsernamePassword(t *testing.T) { + c := new(testReadWriter) + var err error + var method byte + + // VER = 05, NMETHODS = 01, METHODS = [02] + c.writeHex("050102") + if method, err = socksNegotiateAuth(c.toBufio()); err != nil { + t.Error("socksNegotiateAuth(UsernamePassword) failed:", err) + } + if method != socksAuthUsernamePassword { + t.Error("socksNegotiateAuth(UsernamePassword) unexpected method:", method) + } + if msg := c.readHex(); msg != "0502" { + t.Error("socksNegotiateAuth(UsernamePassword) invalid response:", msg) + } +} + +var fakeListenerDistinguishedError = errors.New("distinguished error") + +// fakeListener is a fake dummy net.Listener that returns the given net.Conn and +// error the first time Accept is called. After the first call, it returns +// (nil, fakeListenerDistinguishedError). +type fakeListener struct { + c net.Conn + err error +} + +func (ln *fakeListener) Accept() (net.Conn, error) { + c := ln.c + err := ln.err + ln.c = nil + ln.err = fakeListenerDistinguishedError + return c, err +} + +func (ln *fakeListener) Close() error { + return nil +} + +func (ln *fakeListener) Addr() net.Addr { + return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0, Zone: ""} +} + +// A trivial net.Error that lets you control whether it is considered Temporary. +type netError struct { + errString string + temporary bool +} + +func (e *netError) Error() string { + return e.errString +} + +func (e *netError) Temporary() bool { + return e.temporary +} + +func (e *netError) Timeout() bool { + return false +} + +// The purpose of ignoreDeadlineConn is to wrap net.Pipe so that the deadline +// functions don't return an error ("net.Pipe does not support deadlines"). +type ignoreDeadlineConn struct { + net.Conn +} + +func (c *ignoreDeadlineConn) SetDeadline(t time.Time) error { + return nil +} + +func (c *ignoreDeadlineConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *ignoreDeadlineConn) SetWriteDeadline(t time.Time) error { + return nil +} + +func TestAcceptErrors(t *testing.T) { + // Check that AcceptSocks accurately reflects net.Errors returned by the + // underlying call to Accept. This is important for the handling of + // Temporary and non-Temporary errors. The loop iterates over + // non-net.Error, non-Temporary net.Error, and Temporary net.Error. + for _, expectedErr := range []error{io.EOF, &netError{"non-temp", false}, &netError{"temp", true}} { + ln := NewSocksListener(&fakeListener{nil, expectedErr}) + _, err := ln.AcceptSocks() + if expectedNerr, ok := expectedErr.(net.Error); ok { + nerr, ok := err.(net.Error) + if !ok { + t.Errorf("AcceptSocks returned non-net.Error %v", nerr) + } else { + if expectedNerr.Temporary() != expectedNerr.Temporary() { + t.Errorf("AcceptSocks did not keep Temporary status of net.Error: %v", nerr) + } + } + } + } + + c1, c2 := net.Pipe() + go func() { + // Bogus request: SOCKS 5 then EOF. + c2.Write([]byte("\x05\x01\x00")) + c2.Close() + }() + ln := NewSocksListener(&fakeListener{c: &ignoreDeadlineConn{c1}, err: nil}) + _, err := ln.AcceptSocks() + // The error in parsing the SOCKS request must be either silently + // ignored, or else must be a Temporary net.Error. I.e., it must not be + // the io.ErrUnexpectedEOF caused by the short request. + if err == fakeListenerDistinguishedError { + // Was silently ignored. + } else if nerr, ok := err.(net.Error); ok { + if !nerr.Temporary() { + t.Errorf("AcceptSocks returned non-Temporary net.Error: %v", nerr) + } + } else { + t.Errorf("AcceptSocks returned non-net.Error: %v", err) + } +} + +// TestAuthBoth tests auth negotiation containing both NO AUTHENTICATION +// REQUIRED and USERNAME/PASSWORD. +func TestAuthBoth(t *testing.T) { + c := new(testReadWriter) + var err error + var method byte + + // VER = 05, NMETHODS = 02, METHODS = [00, 02] + c.writeHex("05020002") + if method, err = socksNegotiateAuth(c.toBufio()); err != nil { + t.Error("socksNegotiateAuth(Both) failed:", err) + } + if method != socksAuthUsernamePassword { + t.Error("socksNegotiateAuth(Both) unexpected method:", method) + } + if msg := c.readHex(); msg != "0502" { + t.Error("socksNegotiateAuth(Both) invalid response:", msg) + } +} + +// TestAuthUnsupported tests auth negotiation with a unsupported method. +func TestAuthUnsupported(t *testing.T) { + c := new(testReadWriter) + var err error + var method byte + + // VER = 05, NMETHODS = 01, METHODS = [01] (GSSAPI) + c.writeHex("050101") + if method, err = socksNegotiateAuth(c.toBufio()); err != nil { + t.Error("socksNegotiateAuth(Unknown) failed:", err) + } + if method != socksAuthNoAcceptableMethods { + t.Error("socksNegotiateAuth(Unknown) picked unexpected method:", method) + } + if msg := c.readHex(); msg != "05ff" { + t.Error("socksNegotiateAuth(Unknown) invalid response:", msg) + } +} + +// TestAuthUnsupported2 tests auth negotiation with supported and unsupported +// methods. +func TestAuthUnsupported2(t *testing.T) { + c := new(testReadWriter) + var err error + var method byte + + // VER = 05, NMETHODS = 03, METHODS = [00,01,02] + c.writeHex("0503000102") + if method, err = socksNegotiateAuth(c.toBufio()); err != nil { + t.Error("socksNegotiateAuth(Unknown2) failed:", err) + } + if method != socksAuthUsernamePassword { + t.Error("socksNegotiateAuth(Unknown2) picked unexpected method:", method) + } + if msg := c.readHex(); msg != "0502" { + t.Error("socksNegotiateAuth(Unknown2) invalid response:", msg) + } +} + +// TestRFC1929InvalidVersion tests RFC1929 auth with an invalid version. +func TestRFC1929InvalidVersion(t *testing.T) { + c := new(testReadWriter) + var req SocksRequest + + // VER = 03, ULEN = 5, UNAME = "ABCDE", PLEN = 5, PASSWD = "abcde" + c.writeHex("03054142434445056162636465") + if err := socksAuthenticate(c.toBufio(), socksAuthUsernamePassword, &req); err == nil { + t.Error("socksAuthenticate(InvalidVersion) succeded") + } + if msg := c.readHex(); msg != "0101" { + t.Error("socksAuthenticate(InvalidVersion) invalid response:", msg) + } +} + +// TestRFC1929InvalidUlen tests RFC1929 auth with an invalid ULEN. +func TestRFC1929InvalidUlen(t *testing.T) { + c := new(testReadWriter) + var req SocksRequest + + // VER = 01, ULEN = 0, UNAME = "", PLEN = 5, PASSWD = "abcde" + c.writeHex("0100056162636465") + if err := socksAuthenticate(c.toBufio(), socksAuthUsernamePassword, &req); err == nil { + t.Error("socksAuthenticate(InvalidUlen) succeded") + } + if msg := c.readHex(); msg != "0101" { + t.Error("socksAuthenticate(InvalidUlen) invalid response:", msg) + } +} + +// TestRFC1929InvalidPlen tests RFC1929 auth with an invalid PLEN. +func TestRFC1929InvalidPlen(t *testing.T) { + c := new(testReadWriter) + var req SocksRequest + + // VER = 01, ULEN = 5, UNAME = "ABCDE", PLEN = 0, PASSWD = "" + c.writeHex("0105414243444500") + if err := socksAuthenticate(c.toBufio(), socksAuthUsernamePassword, &req); err == nil { + t.Error("socksAuthenticate(InvalidPlen) succeded") + } + if msg := c.readHex(); msg != "0101" { + t.Error("socksAuthenticate(InvalidPlen) invalid response:", msg) + } +} + +// TestRFC1929InvalidArgs tests RFC1929 auth with invalid pt args. +func TestRFC1929InvalidPTArgs(t *testing.T) { + c := new(testReadWriter) + var req SocksRequest + + // VER = 01, ULEN = 5, UNAME = "ABCDE", PLEN = 5, PASSWD = "abcde" + c.writeHex("01054142434445056162636465") + if err := socksAuthenticate(c.toBufio(), socksAuthUsernamePassword, &req); err == nil { + t.Error("socksAuthenticate(InvalidArgs) succeded") + } + if msg := c.readHex(); msg != "0101" { + t.Error("socksAuthenticate(InvalidArgs) invalid response:", msg) + } +} + +// TestRFC1929Success tests RFC1929 auth with valid pt args. +func TestRFC1929Success(t *testing.T) { + c := new(testReadWriter) + var req SocksRequest + + // VER = 01, ULEN = 9, UNAME = "key=value", PLEN = 1, PASSWD = "\0" + c.writeHex("01096b65793d76616c75650100") + if err := socksAuthenticate(c.toBufio(), socksAuthUsernamePassword, &req); err != nil { + t.Error("socksAuthenticate(Success) failed:", err) + } + if msg := c.readHex(); msg != "0100" { + t.Error("socksAuthenticate(Success) invalid response:", msg) + } + v, ok := req.Args.Get("key") + if v != "value" || !ok { + t.Error("RFC1929 k,v parse failure:", v) + } +} + +// TestRequestInvalidHdr tests SOCKS5 requests with invalid VER/CMD/RSV/ATYPE +func TestRequestInvalidHdr(t *testing.T) { + c := new(testReadWriter) + var req SocksRequest + + // VER = 03, CMD = 01, RSV = 00, ATYPE = 01, DST.ADDR = 127.0.0.1, DST.PORT = 9050 + c.writeHex("030100017f000001235a") + if err := socksReadCommand(c.toBufio(), &req); err == nil { + t.Error("socksReadCommand(InvalidVer) succeded") + } + if msg := c.readHex(); msg != "05010001000000000000" { + t.Error("socksReadCommand(InvalidVer) invalid response:", msg) + } + c.reset() + + // VER = 05, CMD = 05, RSV = 00, ATYPE = 01, DST.ADDR = 127.0.0.1, DST.PORT = 9050 + c.writeHex("050500017f000001235a") + if err := socksReadCommand(c.toBufio(), &req); err == nil { + t.Error("socksReadCommand(InvalidCmd) succeded") + } + if msg := c.readHex(); msg != "05070001000000000000" { + t.Error("socksReadCommand(InvalidCmd) invalid response:", msg) + } + c.reset() + + // VER = 05, CMD = 01, RSV = 30, ATYPE = 01, DST.ADDR = 127.0.0.1, DST.PORT = 9050 + c.writeHex("050130017f000001235a") + if err := socksReadCommand(c.toBufio(), &req); err == nil { + t.Error("socksReadCommand(InvalidRsv) succeded") + } + if msg := c.readHex(); msg != "05010001000000000000" { + t.Error("socksReadCommand(InvalidRsv) invalid response:", msg) + } + c.reset() + + // VER = 05, CMD = 01, RSV = 01, ATYPE = 05, DST.ADDR = 127.0.0.1, DST.PORT = 9050 + c.writeHex("050100057f000001235a") + if err := socksReadCommand(c.toBufio(), &req); err == nil { + t.Error("socksReadCommand(InvalidAtype) succeded") + } + if msg := c.readHex(); msg != "05080001000000000000" { + t.Error("socksAuthenticate(InvalidAtype) invalid response:", msg) + } + c.reset() +} + +// TestRequestIPv4 tests IPv4 SOCKS5 requests. +func TestRequestIPv4(t *testing.T) { + c := new(testReadWriter) + var req SocksRequest + + // VER = 05, CMD = 01, RSV = 00, ATYPE = 01, DST.ADDR = 127.0.0.1, DST.PORT = 9050 + c.writeHex("050100017f000001235a") + if err := socksReadCommand(c.toBufio(), &req); err != nil { + t.Error("socksReadCommand(IPv4) failed:", err) + } + addr, err := net.ResolveTCPAddr("tcp", req.Target) + if err != nil { + t.Error("net.ResolveTCPAddr failed:", err) + } + if !tcpAddrsEqual(addr, &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9050}) { + t.Error("Unexpected target:", addr) + } +} + +// TestRequestIPv6 tests IPv4 SOCKS5 requests. +func TestRequestIPv6(t *testing.T) { + c := new(testReadWriter) + var req SocksRequest + + // VER = 05, CMD = 01, RSV = 00, ATYPE = 04, DST.ADDR = 0102:0304:0506:0708:090a:0b0c:0d0e:0f10, DST.PORT = 9050 + c.writeHex("050100040102030405060708090a0b0c0d0e0f10235a") + if err := socksReadCommand(c.toBufio(), &req); err != nil { + t.Error("socksReadCommand(IPv6) failed:", err) + } + addr, err := net.ResolveTCPAddr("tcp", req.Target) + if err != nil { + t.Error("net.ResolveTCPAddr failed:", err) + } + if !tcpAddrsEqual(addr, &net.TCPAddr{IP: net.ParseIP("0102:0304:0506:0708:090a:0b0c:0d0e:0f10"), Port: 9050}) { + t.Error("Unexpected target:", addr) + } +} + +// TestRequestFQDN tests FQDN (DOMAINNAME) SOCKS5 requests. +func TestRequestFQDN(t *testing.T) { + c := new(testReadWriter) + var req SocksRequest + + // VER = 05, CMD = 01, RSV = 00, ATYPE = 04, DST.ADDR = example.com, DST.PORT = 9050 + c.writeHex("050100030b6578616d706c652e636f6d235a") + if err := socksReadCommand(c.toBufio(), &req); err != nil { + t.Error("socksReadCommand(FQDN) failed:", err) + } + if req.Target != "example.com:9050" { + t.Error("Unexpected target:", req.Target) + } +} + +// TestResponseNil tests nil address SOCKS5 responses. +func TestResponseNil(t *testing.T) { + c := new(testReadWriter) + + b := c.toBufio() + if err := sendSocks5ResponseGranted(b); err != nil { + t.Error("sendSocks5ResponseGranted() failed:", err) + } + b.Flush() + if msg := c.readHex(); msg != "05000001000000000000" { + t.Error("sendSocks5ResponseGranted(nil) invalid response:", msg) + } +} + +var _ io.ReadWriter = (*testReadWriter)(nil) diff --git a/pkg/goptlib-v1.3.0/test_authcookie b/pkg/goptlib-v1.3.0/test_authcookie new file mode 100644 index 00000000..0df0cfbc --- /dev/null +++ b/pkg/goptlib-v1.3.0/test_authcookie @@ -0,0 +1,2 @@ +! Extended ORPort Auth Cookie ! +this file is used in test code. diff --git a/ws.go b/ws.go index 9dc8f0dc..322adf59 100644 --- a/ws.go +++ b/ws.go @@ -5,15 +5,15 @@ import ( "crypto/sha1" "crypto/tls" "encoding/base64" + "fmt" "io" "net" "net/http" "net/http/httputil" + "net/url" "sync" "time" - "net/url" - "github.com/go-log/log" "github.com/gorilla/websocket" smux "github.com/xtaci/smux" @@ -31,6 +31,7 @@ type WSOptions struct { EnableCompression bool UserAgent string Path string + Host string } type wsTransporter struct { @@ -749,6 +750,13 @@ func websocketClientConn(url string, conn net.Conn, tlsConfig *tls.Config, optio if options.UserAgent != "" { header.Set("User-Agent", options.UserAgent) } + if options.Host != "" { + fmt.Println("获取到命令行传参host==>", options.Host) + dialer.TLSClientConfig.ServerName = options.Host + header.Set("Host", options.Host) + } + + fmt.Printf("TLSClientConfig==>%+v\n", dialer.TLSClientConfig) c, resp, err := dialer.Dial(url, header) if err != nil { return nil, err