[Pkg-privacy-commits] [golang-goptlib] 02/20: Imported Upstream version 0.1

Ximin Luo infinity0 at moszumanska.debian.org
Sat Aug 22 10:04:06 UTC 2015


This is an automated email from the git hooks/post-receive script.

infinity0 pushed a commit to branch master
in repository golang-goptlib.

commit 208b883a959e6801f9b3d87fb43f8fb879aac58a
Author: Ximin Luo <infinity0 at pwned.gg>
Date:   Thu Feb 20 23:37:02 2014 +0000

    Imported Upstream version 0.1
---
 .gitignore                            |   2 +
 COPYING                               | 121 +++++
 README                                |  26 ++
 args.go                               | 220 +++++++++
 args_test.go                          | 358 +++++++++++++++
 examples/dummy-client/dummy-client.go | 140 ++++++
 examples/dummy-server/dummy-server.go | 137 ++++++
 pt.go                                 | 818 ++++++++++++++++++++++++++++++++++
 pt_test.go                            | 739 ++++++++++++++++++++++++++++++
 socks.go                              | 243 ++++++++++
 socks_test.go                         | 162 +++++++
 11 files changed, 2966 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d4d5132
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/examples/dummy-client/dummy-client
+/examples/dummy-server/dummy-server
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..0e259d4
--- /dev/null
+++ b/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/README b/README
new file mode 100644
index 0000000..f3ce5b3
--- /dev/null
+++ b/README
@@ -0,0 +1,26 @@
+goptlib is a library for writing Tor pluggable transports in Go.
+
+https://gitweb.torproject.org/torspec.git/blob/HEAD:/pt-spec.txt
+https://gitweb.torproject.org/torspec.git/blob/HEAD:/proposals/196-transport-control-ports.txt
+https://gitweb.torproject.org/torspec.git/blob/HEAD:/proposals/217-ext-orport-auth.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:
+http://godoc.org/git.torproject.org/pluggable-transports/goptlib.git
+
+Report bugs to the tor-dev at 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/args.go b/args.go
new file mode 100644
index 0000000..5e96589
--- /dev/null
+++ b/args.go
@@ -0,0 +1,220 @@
+package pt
+
+import (
+	"bytes"
+	"errors"
+	"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 not 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, "", errors.New(fmt.Sprintf("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.
+//
+// "If any [k=v] items are provided, they are configuration parameters for the
+// proxy: Tor should separate them with semicolons ... If a key or value value
+// must contain [an equals sign or] a semicolon or a backslash, it is 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 = errors.New(fmt.Sprintf("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 = errors.New(fmt.Sprintf("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.
+//
+// "<value> 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: trebuchet:secret=nou;trebuchet:cache=/tmp/cache;ballista:secret=yes
+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 = errors.New(fmt.Sprintf("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 = errors.New(fmt.Sprintf("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 = errors.New(fmt.Sprintf("empty method name in %q", s[begin:i]))
+			return
+		}
+		if len(key) == 0 {
+			err = errors.New(fmt.Sprintf("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/args_test.go b/args_test.go
new file mode 100644
index 0000000..8a77251
--- /dev/null
+++ b/args_test.go
@@ -0,0 +1,358 @@
+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"}},
+		},
+		{
+			"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": Args{"k": []string{"v"}},
+			},
+		},
+		{
+			"t1:k=v1;t2:k=v2;t1:k=v3",
+			map[string]Args{
+				"t1": Args{"k": []string{"v1", "v3"}},
+				"t2": Args{"k": []string{"v2"}},
+			},
+		},
+		{
+			"t\\:1:k=v;t\\=2:k=v;t\\;3:k=v;t\\\\4:k=v",
+			map[string]Args{
+				"t:1":  Args{"k": []string{"v"}},
+				"t=2":  Args{"k": []string{"v"}},
+				"t;3":  Args{"k": []string{"v"}},
+				"t\\4": Args{"k": []string{"v"}},
+			},
+		},
+		{
+			"t:k\\:1=v;t:k\\=2=v;t:k\\;3=v;t:k\\\\4=v",
+			map[string]Args{
+				"t": Args{
+					"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": Args{"k": []string{"v:1", "v=2", "v;3", "v\\4"}},
+			},
+		},
+		{
+			"trebuchet:secret=nou;trebuchet:cache=/tmp/cache;ballista:secret=yes",
+			map[string]Args{
+				"trebuchet": Args{"secret": []string{"nou"}, "cache": []string{"/tmp/cache"}},
+				"ballista":  Args{"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/examples/dummy-client/dummy-client.go b/examples/dummy-client/dummy-client.go
new file mode 100644
index 0000000..64f1ec1
--- /dev/null
+++ b/examples/dummy-client/dummy-client.go
@@ -0,0 +1,140 @@
+// 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 any
+// ordinary relay's ORPort in the Bridge line; it doesn't have to declare
+// support for the dummy transport.
+package main
+
+import (
+	"io"
+	"net"
+	"os"
+	"os/signal"
+	"sync"
+	"syscall"
+)
+
+import "git.torproject.org/pluggable-transports/goptlib.git"
+
+var ptInfo pt.ClientInfo
+
+// When a connection handler starts, +1 is written to this channel; when it
+// ends, -1 is written.
+var handlerChan = make(chan int)
+
+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 {
+	handlerChan <- 1
+	defer func() {
+		handlerChan <- -1
+	}()
+
+	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() {
+				return err
+			}
+			continue
+		}
+		go handler(conn)
+	}
+}
+
+func main() {
+	var err error
+
+	ptInfo, err = pt.ClientSetup([]string{"dummy"})
+	if err != nil {
+		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()
+
+	var numHandlers int = 0
+	var sig os.Signal
+	sigChan := make(chan os.Signal, 1)
+	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+	// wait for first signal
+	sig = nil
+	for sig == nil {
+		select {
+		case n := <-handlerChan:
+			numHandlers += n
+		case sig = <-sigChan:
+		}
+	}
+	for _, ln := range listeners {
+		ln.Close()
+	}
+
+	if sig == syscall.SIGTERM {
+		return
+	}
+
+	// wait for second signal or no more handlers
+	sig = nil
+	for sig == nil && numHandlers != 0 {
+		select {
+		case n := <-handlerChan:
+			numHandlers += n
+		case sig = <-sigChan:
+		}
+	}
+}
diff --git a/examples/dummy-server/dummy-server.go b/examples/dummy-server/dummy-server.go
new file mode 100644
index 0000000..ea91be9
--- /dev/null
+++ b/examples/dummy-server/dummy-server.go
@@ -0,0 +1,137 @@
+// 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"
+	"net"
+	"os"
+	"os/signal"
+	"sync"
+	"syscall"
+)
+
+import "git.torproject.org/pluggable-transports/goptlib.git"
+
+var ptInfo pt.ServerInfo
+
+// When a connection handler starts, +1 is written to this channel; when it
+// ends, -1 is written.
+var handlerChan = make(chan int)
+
+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()
+
+	handlerChan <- 1
+	defer func() {
+		handlerChan <- -1
+	}()
+
+	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() {
+				return err
+			}
+			continue
+		}
+		go handler(conn)
+	}
+}
+
+func main() {
+	var err error
+
+	ptInfo, err = pt.ServerSetup([]string{"dummy"})
+	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()
+
+	var numHandlers int = 0
+	var sig os.Signal
+	sigChan := make(chan os.Signal, 1)
+	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+	// wait for first signal
+	sig = nil
+	for sig == nil {
+		select {
+		case n := <-handlerChan:
+			numHandlers += n
+		case sig = <-sigChan:
+		}
+	}
+	for _, ln := range listeners {
+		ln.Close()
+	}
+
+	if sig == syscall.SIGTERM {
+		return
+	}
+
+	// wait for second signal or no more handlers
+	sig = nil
+	for sig == nil && numHandlers != 0 {
+		select {
+		case n := <-handlerChan:
+			numHandlers += n
+		case sig = <-sigChan:
+		}
+	}
+}
diff --git a/pt.go b/pt.go
new file mode 100644
index 0000000..cb9b42f
--- /dev/null
+++ b/pt.go
@@ -0,0 +1,818 @@
+// 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 or.
+// 		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() {
+// 					return err
+// 				}
+// 				continue
+// 			}
+// 			go handler(conn)
+// 		}
+// 		return nil
+// 	}
+// 	...
+// 	func main() {
+// 		var err error
+// 		ptInfo, err = pt.ClientSetup([]string{"foo"})
+// 		if err != nil {
+// 			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() {
+// 					return err
+// 				}
+// 				continue
+// 			}
+// 			go handler(conn)
+// 		}
+// 		return nil
+// 	}
+// 	...
+// 	func main() {
+// 		var err error
+// 		ptInfo, err = pt.ServerSetup([]string{"foo"})
+// 		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 SIGINT and shutdown properly. See
+// the example programs dummy-client and dummy-server.
+//
+// Tor pluggable transports specification:
+// https://gitweb.torproject.org/torspec.git/blob/HEAD:/pt-spec.txt.
+//
+// Extended ORPort:
+// https://gitweb.torproject.org/torspec.git/blob/HEAD:/proposals/196-transport-control-ports.txt.
+//
+// Extended ORPort Authentication:
+// https://gitweb.torproject.org/torspec.git/blob/HEAD:/proposals/217-ext-orport-auth.txt.
+//
+// The package implements a SOCKS4a server sufficient for a Tor client transport
+// plugin.
+//
+// http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol
+package pt
+
+import (
+	"bytes"
+	"crypto/hmac"
+	"crypto/rand"
+	"crypto/sha256"
+	"crypto/subtle"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"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
+}
+
+// Escape a string so it contains no byte values over 127 and doesn't contain
+// any of the characters '\x00' or '\n'.
+func escape(s string) string {
+	var buf bytes.Buffer
+	for _, b := range []byte(s) {
+		if b == '\n' {
+			buf.WriteString("\\n")
+		} else if b == '\\' {
+			buf.WriteString("\\\\")
+		} else if 0 < b && b < 128 {
+			buf.WriteByte(b)
+		} else {
+			fmt.Fprintf(&buf, "\\x%02x", b)
+		}
+	}
+	return buf.String()
+}
+
+func formatline(keyword string, v ...string) string {
+	var buf bytes.Buffer
+	buf.WriteString(keyword)
+	for _, x := range v {
+		buf.WriteString(" " + escape(x))
+	}
+	return buf.String()
+}
+
+// Print a pluggable transports protocol line to Stdout. The line consists of an
+// unescaped keyword, followed by any number of escaped strings.
+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 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")
+}
+
+// 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")
+}
+
+// Get the intersection of the method names offered by Tor and those in
+// methodNames. This function reads the environment variable
+// TOR_PT_CLIENT_TRANSPORTS.
+func getClientTransports(star []string) ([]string, error) {
+	clientTransports, err := getenvRequired("TOR_PT_CLIENT_TRANSPORTS")
+	if err != nil {
+		return nil, err
+	}
+	if clientTransports == "*" {
+		return star, nil
+	}
+	return strings.Split(clientTransports, ","), nil
+}
+
+// This structure is returned by ClientSetup. It consists of a list of method
+// names.
+type ClientInfo struct {
+	MethodNames []string
+}
+
+// Check the client pluggable transports environment, emitting an error message
+// and returning a non-nil error if any error is encountered. star is the list
+// of method names to use in case "*" is requested by Tor. Returns a ClientInfo
+// struct.
+func ClientSetup(star []string) (info ClientInfo, err error) {
+	ver, err := getManagedTransportVer()
+	if err != nil {
+		return
+	}
+	line("VERSION", ver)
+
+	info.MethodNames, err = getClientTransports(star)
+	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.
+		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. If TOR_PT_SERVER_TRANSPORTS
+// is "*", then keys are filtered by the entries in star instead.
+// Transport-specific options from TOR_PT_SERVER_TRANSPORT_OPTIONS are assigned
+// to the Options member.
+func getServerBindaddrs(star []string) ([]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
+	}
+	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]
+		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
+	}
+	if serverTransports == "*" {
+		result = filterBindaddrs(result, star)
+	} else {
+		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, errors.New(fmt.Sprintf("file is longer than 64 bytes"))
+	} else if err != io.EOF {
+		return nil, errors.New(fmt.Sprintf("did not find EOF at end of file"))
+	}
+	header := buf[0:32]
+	cookie := buf[32:64]
+	if subtle.ConstantTimeCompare(header, authCookieHeader) != 1 {
+		return nil, errors.New(fmt.Sprintf("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 pt-spec.txt.
+func readAuthCookieFile(filename string) ([]byte, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	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
+	AuthCookie     []byte
+}
+
+// Check the server pluggable transports environment, emitting an error message
+// and returning a non-nil error if any error is encountered. star is the list
+// of method names to use in case "*" is requested by Tor. Resolves the various
+// requested bind addresses, the server ORPort and extended ORPort, and reads
+// the auth cookie file. Returns a ServerInfo struct.
+func ServerSetup(star []string) (info ServerInfo, err error) {
+	ver, err := getManagedTransportVer()
+	if err != nil {
+		return
+	}
+	line("VERSION", ver)
+
+	info.Bindaddrs, err = getServerBindaddrs(star)
+	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
+		}
+	}
+
+	extendedOrPort := getenv("TOR_PT_EXTENDED_SERVER_PORT")
+	if extendedOrPort != "" {
+		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
+		}
+	}
+	authCookieFilename := getenv("TOR_PT_AUTH_COOKIE_FILE")
+	if authCookieFilename != "" {
+		info.AuthCookie, err = readAuthCookieFile(authCookieFilename)
+		if err != nil {
+			err = envError(fmt.Sprintf("error reading TOR_PT_AUTH_COOKIE_FILE %q: %s", authCookieFilename, err.Error()))
+			return
+		}
+	}
+
+	// Need either OrAddr or ExtendedOrAddr.
+	if info.OrAddr == nil && (info.ExtendedOrAddr == nil || info.AuthCookie == 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 errors.New(fmt.Sprintf("read 256 auth types without seeing \\x00"))
+	}
+
+	// We support only type 1, SAFE_COOKIE.
+	if !authTypes[1] {
+		return errors.New(fmt.Sprintf("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
+	}
+
+	expectedServerHash := computeServerHash(info.AuthCookie, clientNonce, serverNonce)
+	if subtle.ConstantTimeCompare(serverHash, expectedServerHash) != 1 {
+		return errors.New(fmt.Sprintf("mismatch in server hash"))
+	}
+
+	clientHash = computeClientHash(info.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 errors.New(fmt.Sprintf("server rejected authentication"))
+	}
+
+	return nil
+}
+
+// See section 3.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 errors.New(fmt.Sprintf("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 extOrPortSetup(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 errors.New("server returned DENY after our USERADDR and DONE")
+	} else if cmd != extOrCmdOkay {
+		return errors.New(fmt.Sprintf("server returned unknown command 0x%04x after our USERADDR and DONE", cmd))
+	}
+
+	return 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) {
+	if info.ExtendedOrAddr == nil || info.AuthCookie == nil {
+		return net.DialTCP("tcp", nil, info.OrAddr)
+	}
+
+	s, err := net.DialTCP("tcp", nil, info.ExtendedOrAddr)
+	if err != nil {
+		return nil, err
+	}
+	s.SetDeadline(time.Now().Add(5 * time.Second))
+	err = extOrPortAuthenticate(s, info)
+	if err != nil {
+		s.Close()
+		return nil, err
+	}
+	err = extOrPortSetup(s, addr, methodName)
+	if err != nil {
+		s.Close()
+		return nil, err
+	}
+	s.SetDeadline(time.Time{})
+
+	return s, nil
+}
diff --git a/pt_test.go b/pt_test.go
new file mode 100644
index 0000000..aa3ad04
--- /dev/null
+++ b/pt_test.go
@@ -0,0 +1,739 @@
+package pt
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net"
+	"os"
+	"sort"
+	"testing"
+)
+
+func stringIsSafe(s string) bool {
+	for _, c := range []byte(s) {
+		if c == '\x00' || c == '\n' || c > 127 {
+			return false
+		}
+	}
+	return true
+}
+
+func TestEscape(t *testing.T) {
+	tests := [...]string{
+		"",
+		"abc",
+		"a\nb",
+		"a\\b",
+		"ab\\",
+		"ab\\\n",
+		"ab\n\\",
+	}
+
+	check := func(input string) {
+		output := escape(input)
+		if !stringIsSafe(output) {
+			t.Errorf("escape(%q) → %q", input, output)
+		}
+	}
+	for _, input := range tests {
+		check(input)
+	}
+	for b := 0; b < 256; b++ {
+		// check one-byte string with each byte value 0–255
+		check(string([]byte{byte(b)}))
+		// check UTF-8 encoding of each character 0–255
+		check(string(b))
+	}
+}
+
+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 {
+		ptServerClientTransports string
+		star                     []string
+		expected                 []string
+	}{
+		{
+			"*",
+			[]string{},
+			[]string{},
+		},
+		{
+			"*",
+			[]string{"alpha", "beta", "gamma"},
+			[]string{"alpha", "beta", "gamma"},
+		},
+		{
+			"alpha,beta,gamma",
+			[]string{"alpha", "beta", "gamma"},
+			[]string{"alpha", "beta", "gamma"},
+		},
+		{
+			"alpha,beta",
+			[]string{"alpha", "beta", "gamma"},
+			[]string{"alpha", "beta"},
+		},
+		{
+			"alpha",
+			[]string{"alpha", "beta", "gamma"},
+			[]string{"alpha"},
+		},
+		{
+			"alpha,beta,gamma",
+			[]string{},
+			[]string{"alpha", "beta", "gamma"},
+		},
+		{
+			"alpha,beta",
+			[]string{},
+			[]string{"alpha", "beta"},
+		},
+		{
+			"alpha",
+			[]string{},
+			[]string{"alpha"},
+		},
+		// my reading of pt-spec.txt says that "*" has special meaning
+		// only when it is the entirety of the environment variable.
+		{
+			"alpha,*,gamma",
+			[]string{"alpha", "beta", "gamma"},
+			[]string{"alpha", "*", "gamma"},
+		},
+		{
+			"alpha",
+			[]string{"beta"},
+			[]string{"alpha"},
+		},
+	}
+
+	Stdout = ioutil.Discard
+
+	os.Clearenv()
+	_, err := getClientTransports([]string{"alpha", "beta", "gamma"})
+	if err == nil {
+		t.Errorf("empty environment unexpectedly succeeded")
+	}
+
+	for _, test := range tests {
+		os.Setenv("TOR_PT_CLIENT_TRANSPORTS", test.ptServerClientTransports)
+		output, err := getClientTransports(test.star)
+		if err != nil {
+			t.Errorf("TOR_PT_CLIENT_TRANSPORTS=%q unexpectedly returned an error: %s",
+				test.ptServerClientTransports, err)
+		}
+		if !stringSetsEqual(output, test.expected) {
+			t.Errorf("TOR_PT_CLIENT_TRANSPORTS=%q %q → %q (expected %q)",
+				test.ptServerClientTransports, test.star, 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
+		star                     []string
+	}{
+		// bad TOR_PT_SERVER_BINDADDR
+		{
+			"alpha",
+			"alpha",
+			"",
+			[]string{"alpha", "beta", "gamma"},
+		},
+		{
+			"alpha-1.2.3.4",
+			"alpha",
+			"",
+			[]string{"alpha", "beta", "gamma"},
+		},
+		// missing TOR_PT_SERVER_TRANSPORTS
+		{
+			"alpha-1.2.3.4:1111",
+			"",
+			"alpha:key=value",
+			[]string{"alpha"},
+		},
+		// bad TOR_PT_SERVER_TRANSPORT_OPTIONS
+		{
+			"alpha-1.2.3.4:1111",
+			"alpha",
+			"key=value",
+			[]string{"alpha"},
+		},
+	}
+	goodTests := [...]struct {
+		ptServerBindaddr         string
+		ptServerTransports       string
+		ptServerTransportOptions string
+		star                     []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",
+			[]string{"alpha", "beta"},
+			[]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",
+			"",
+			[]string{"alpha", "beta", "gamma"},
+			[]Bindaddr{},
+		},
+		{
+			"alpha-1.2.3.4:1111",
+			"alpha,beta,gamma",
+			"",
+			[]string{},
+			[]Bindaddr{
+				{"alpha", &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 1111}, Args{}},
+			},
+		},
+		{
+			"alpha-1.2.3.4:1111,beta-[1:2::3:4]:2222",
+			"*",
+			"",
+			[]string{"alpha", "beta"},
+			[]Bindaddr{
+				{"alpha", &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 1111}, Args{}},
+				{"beta", &net.TCPAddr{IP: net.ParseIP("1:2::3:4"), Port: 2222}, Args{}},
+			},
+		},
+		{
+			"alpha-1.2.3.4:1111,beta-[1:2::3:4]:2222",
+			"*",
+			"",
+			[]string{"alpha", "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",
+			[]string{"trebuchet", "ballista"},
+			[]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"}}},
+			},
+		},
+	}
+
+	Stdout = ioutil.Discard
+
+	os.Clearenv()
+	_, err := getServerBindaddrs([]string{"alpha", "beta", "gamma"})
+	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(test.star)
+		if err == nil {
+			t.Errorf("TOR_PT_SERVER_BINDADDR=%q TOR_PT_SERVER_TRANSPORTS=%q TOR_PT_SERVER_TRANSPORT_OPTIONS=%q %q unexpectedly succeeded",
+				test.ptServerBindaddr, test.ptServerTransports, test.ptServerTransportOptions, test.star)
+		}
+	}
+
+	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(test.star)
+		if err != nil {
+			t.Errorf("TOR_PT_SERVER_BINDADDR=%q TOR_PT_SERVER_TRANSPORTS=%q TOR_PT_SERVER_TRANSPORT_OPTIONS=%q %q unexpectedly returned an error: %s",
+				test.ptServerBindaddr, test.ptServerTransports, test.ptServerTransportOptions, test.star, 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 → %q (expected %q)",
+				test.ptServerBindaddr, test.ptServerTransports, test.ptServerTransportOptions, test.star, 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 extOrPortSetup can write to one buffer and read from another.
+type MockSetupBuf struct {
+	bytes.Buffer
+	ReadBuf bytes.Buffer
+}
+
+func (buf *MockSetupBuf) Read(p []byte) (int, error) {
+	n, err := buf.ReadBuf.Read(p)
+	return n, err
+}
+
+func testExtOrPortSetupIndividual(t *testing.T, addr, methodName string) {
+	var err error
+	var buf MockSetupBuf
+	// fake an OKAY response.
+	err = extOrPortSendCommand(&buf.ReadBuf, extOrCmdOkay, []byte{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = extOrPortSetup(&buf, addr, methodName)
+	if err != nil {
+		t.Fatalf("error in extOrPortSetup: %s", err)
+	}
+	for {
+		cmd, body, err := extOrPortRecvCommand(&buf.Buffer)
+		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 TestExtOrPortSetup(t *testing.T) {
+	const addr = "127.0.0.1:40000"
+	const methodName = "alpha"
+	testExtOrPortSetupIndividual(t, "", "")
+	testExtOrPortSetupIndividual(t, addr, "")
+	testExtOrPortSetupIndividual(t, "", methodName)
+	testExtOrPortSetupIndividual(t, addr, methodName)
+}
diff --git a/socks.go b/socks.go
new file mode 100644
index 0000000..f34f78f
--- /dev/null
+++ b/socks.go
@@ -0,0 +1,243 @@
+package pt
+
+import (
+	"bufio"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"time"
+)
+
+const (
+	socksVersion         = 0x04
+	socksCmdConnect      = 0x01
+	socksResponseVersion = 0x00
+	socksRequestGranted  = 0x5a
+	socksRequestRejected = 0x5b
+)
+
+// 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 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. If the IP field inside addr is not an IPv4 address, the IP portion
+// of the response will be four zero bytes.
+func (conn *SocksConn) Grant(addr *net.TCPAddr) error {
+	return sendSocks4aResponseGranted(conn, addr)
+}
+
+// Send a message to the proxy client that access was rejected or failed.
+func (conn *SocksConn) Reject() error {
+	return sendSocks4aResponseRejected(conn)
+}
+
+// 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() {
+// 				break
+// 			}
+// 			continue
+// 		}
+// 		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("permanent accept error; giving up: %s", err)
+// 				break
+// 			}
+// 			log.Printf("temporary accept error; trying again: %s", err)
+// 			continue
+// 		}
+// 		go handleConn(conn)
+// 	}
+func (ln *SocksListener) AcceptSocks() (*SocksConn, error) {
+	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 {
+		return nil, err
+	}
+	conn.Req, err = readSocks4aConnect(conn)
+	if err != nil {
+		conn.Close()
+		return nil, err
+	}
+	err = conn.SetDeadline(time.Time{})
+	if err != nil {
+		return nil, err
+	}
+	return conn, nil
+}
+
+// Returns "socks4", suitable to be included in a call to Cmethod.
+func (ln *SocksListener) Version() string {
+	return "socks4"
+}
+
+// Read a SOCKS4a connect request. Returns a SocksRequest.
+func readSocks4aConnect(s io.Reader) (req SocksRequest, err error) {
+	r := bufio.NewReader(s)
+
+	var h [8]byte
+	_, err = io.ReadFull(r, h[:])
+	if err != nil {
+		return
+	}
+	if h[0] != socksVersion {
+		err = errors.New(fmt.Sprintf("SOCKS header had version 0x%02x, not 0x%02x", h[0], socksVersion))
+		return
+	}
+	if h[1] != socksCmdConnect {
+		err = errors.New(fmt.Sprintf("SOCKS header had command 0x%02x, not 0x%02x", h[1], socksCmdConnect))
+		return
+	}
+
+	var usernameBytes []byte
+	usernameBytes, err = r.ReadBytes('\x00')
+	if err != nil {
+		return
+	}
+	req.Username = string(usernameBytes[:len(usernameBytes)-1])
+
+	req.Args, err = parseClientParameters(req.Username)
+	if err != nil {
+		return
+	}
+
+	var port int
+	var host string
+
+	port = int(h[2])<<8 | int(h[3])<<0
+	if h[4] == 0 && h[5] == 0 && h[6] == 0 && h[7] != 0 {
+		var hostBytes []byte
+		hostBytes, err = r.ReadBytes('\x00')
+		if err != nil {
+			return
+		}
+		host = string(hostBytes[:len(hostBytes)-1])
+	} else {
+		host = net.IPv4(h[4], h[5], h[6], h[7]).String()
+	}
+
+	if r.Buffered() != 0 {
+		err = errors.New(fmt.Sprintf("%d bytes left after SOCKS header", r.Buffered()))
+		return
+	}
+
+	req.Target = fmt.Sprintf("%s:%d", host, port)
+	return
+}
+
+// Send a SOCKS4a response with the given code and address. If the IP field
+// inside addr is not an IPv4 address, the IP portion of the response will be
+// four zero bytes.
+func sendSocks4aResponse(w io.Writer, code byte, addr *net.TCPAddr) error {
+	var resp [8]byte
+	resp[0] = socksResponseVersion
+	resp[1] = code
+	resp[2] = byte((addr.Port >> 8) & 0xff)
+	resp[3] = byte((addr.Port >> 0) & 0xff)
+	ipv4 := addr.IP.To4()
+	if ipv4 != nil {
+		resp[4] = ipv4[0]
+		resp[5] = ipv4[1]
+		resp[6] = ipv4[2]
+		resp[7] = ipv4[3]
+	}
+	_, err := w.Write(resp[:])
+	return err
+}
+
+var emptyAddr = net.TCPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 0}
+
+// Send a SOCKS4a response code 0x5a.
+func sendSocks4aResponseGranted(w io.Writer, addr *net.TCPAddr) error {
+	return sendSocks4aResponse(w, socksRequestGranted, addr)
+}
+
+// Send a SOCKS4a response code 0x5b (with an all-zero address).
+func sendSocks4aResponseRejected(w io.Writer) error {
+	return sendSocks4aResponse(w, socksRequestRejected, &emptyAddr)
+}
diff --git a/socks_test.go b/socks_test.go
new file mode 100644
index 0000000..18d141a
--- /dev/null
+++ b/socks_test.go
@@ -0,0 +1,162 @@
+package pt
+
+import (
+	"bytes"
+	"net"
+	"testing"
+)
+
+func TestReadSocks4aConnect(t *testing.T) {
+	badTests := [...][]byte{
+		[]byte(""),
+		// missing userid
+		[]byte("\x04\x01\x12\x34\x01\x02\x03\x04"),
+		// missing \x00 after userid
+		[]byte("\x04\x01\x12\x34\x01\x02\x03\x04key=value"),
+		// missing hostname
+		[]byte("\x04\x01\x12\x34\x00\x00\x00\x01key=value\x00"),
+		// missing \x00 after hostname
+		[]byte("\x04\x01\x12\x34\x00\x00\x00\x01key=value\x00hostname"),
+		// bad name–value mapping
+		[]byte("\x04\x01\x12\x34\x00\x00\x00\x01userid\x00hostname\x00"),
+		// bad version number
+		[]byte("\x03\x01\x12\x34\x01\x02\x03\x04\x00"),
+		// BIND request
+		[]byte("\x04\x02\x12\x34\x01\x02\x03\x04\x00"),
+		// SOCKS5
+		[]byte("\x05\x01\x00"),
+	}
+	ipTests := [...]struct {
+		input  []byte
+		addr   net.TCPAddr
+		userid string
+	}{
+		{
+			[]byte("\x04\x01\x12\x34\x01\x02\x03\x04key=value\x00"),
+			net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 0x1234},
+			"key=value",
+		},
+		{
+			[]byte("\x04\x01\x12\x34\x01\x02\x03\x04\x00"),
+			net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 0x1234},
+			"",
+		},
+	}
+	hostnameTests := [...]struct {
+		input  []byte
+		target string
+		userid string
+	}{
+		{
+			[]byte("\x04\x01\x12\x34\x00\x00\x00\x01key=value\x00hostname\x00"),
+			"hostname:4660",
+			"key=value",
+		},
+		{
+			[]byte("\x04\x01\x12\x34\x00\x00\x00\x01\x00hostname\x00"),
+			"hostname:4660",
+			"",
+		},
+		{
+			[]byte("\x04\x01\x12\x34\x00\x00\x00\x01key=value\x00\x00"),
+			":4660",
+			"key=value",
+		},
+		{
+			[]byte("\x04\x01\x12\x34\x00\x00\x00\x01\x00\x00"),
+			":4660",
+			"",
+		},
+	}
+
+	for _, input := range badTests {
+		var buf bytes.Buffer
+		buf.Write(input)
+		_, err := readSocks4aConnect(&buf)
+		if err == nil {
+			t.Errorf("%q unexpectedly succeeded", input)
+		}
+	}
+
+	for _, test := range ipTests {
+		var buf bytes.Buffer
+		buf.Write(test.input)
+		req, err := readSocks4aConnect(&buf)
+		if err != nil {
+			t.Errorf("%q unexpectedly returned an error: %s", test.input, err)
+		}
+		addr, err := net.ResolveTCPAddr("tcp", req.Target)
+		if err != nil {
+			t.Errorf("%q → target %q: cannot resolve: %s", test.input,
+				req.Target, err)
+		}
+		if !tcpAddrsEqual(addr, &test.addr) {
+			t.Errorf("%q → address %s (expected %s)", test.input,
+				req.Target, test.addr.String())
+		}
+		if req.Username != test.userid {
+			t.Errorf("%q → username %q (expected %q)", test.input,
+				req.Username, test.userid)
+		}
+		if req.Args == nil {
+			t.Errorf("%q → unexpected nil Args from username %q", test.input, req.Username)
+		}
+	}
+
+	for _, test := range hostnameTests {
+		var buf bytes.Buffer
+		buf.Write(test.input)
+		req, err := readSocks4aConnect(&buf)
+		if err != nil {
+			t.Errorf("%q unexpectedly returned an error: %s", test.input, err)
+		}
+		if req.Target != test.target {
+			t.Errorf("%q → target %q (expected %q)", test.input,
+				req.Target, test.target)
+		}
+		if req.Username != test.userid {
+			t.Errorf("%q → username %q (expected %q)", test.input,
+				req.Username, test.userid)
+		}
+		if req.Args == nil {
+			t.Errorf("%q → unexpected nil Args from username %q", test.input, req.Username)
+		}
+	}
+}
+
+func TestSendSocks4aResponse(t *testing.T) {
+	tests := [...]struct {
+		code     byte
+		addr     net.TCPAddr
+		expected []byte
+	}{
+		{
+			socksRequestGranted,
+			net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 0x1234},
+			[]byte("\x00\x5a\x12\x34\x01\x02\x03\x04"),
+		},
+		{
+			socksRequestRejected,
+			net.TCPAddr{IP: net.ParseIP("1:2::3:4"), Port: 0x1234},
+			[]byte("\x00\x5b\x12\x34\x00\x00\x00\x00"),
+		},
+	}
+
+	for _, test := range tests {
+		var buf bytes.Buffer
+		err := sendSocks4aResponse(&buf, test.code, &test.addr)
+		if err != nil {
+			t.Errorf("0x%02x %s unexpectedly returned an error: %s", test.code, &test.addr, 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("0x%02x %s → %v (expected %v)",
+				test.code, &test.addr, output, test.expected)
+		}
+	}
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/golang-goptlib.git



More information about the Pkg-privacy-commits mailing list