Explorar o código

Documentation and light refactoring.

David Fifield %!s(int64=6) %!d(string=hai) anos
pai
achega
d14deab12b
Modificáronse 11 ficheiros con 606 adicións e 262 borrados
  1. 1 1
      README
  2. 58 28
      dns/dns.go
  3. 105 16
      dnstt-client/dns.go
  4. 39 14
      dnstt-client/dns_test.go
  5. 64 32
      dnstt-client/http.go
  6. 57 34
      dnstt-client/main.go
  7. 27 0
      dnstt-client/main_test.go
  8. 26 5
      dnstt-client/tls.go
  9. 173 106
      dnstt-server/main.go
  10. 52 26
      noise/noise.go
  11. 4 0
      turbotunnel/consts.go

+ 1 - 1
README

@@ -66,7 +66,7 @@ $ go build
 First you need to generate the server keypair that will be used to
 authenticate the server and encrypt the tunnel.
 ```
-$ ./dnstt-server -gen-key -privkey-file server.key
+$ ./dnstt-server -gen-key -privkey-file server.key -pubkey-file server.pub
 privkey written to server.key
 pubkey  written to server.pub
 ```

+ 58 - 28
dns/dns.go

@@ -52,11 +52,11 @@ const (
 	ClassIN = 1
 
 	// https://tools.ietf.org/html/rfc1035#section-4.1.1
-	RcodeNoError         = 0
-	RcodeFormatError     = 1
-	RcodeNameError       = 3 // a.k.a. NXDOMAIN
-	RcodeNotImplemented  = 4
-	ExtendedRcodeBadVers = 16
+	RcodeNoError         = 0  // a.k.a. NOERROR
+	RcodeFormatError     = 1  // a.k.a. FORMERR
+	RcodeNameError       = 3  // a.k.a. NXDOMAIN
+	RcodeNotImplemented  = 4  // a.k.a. NOTIMPL
+	ExtendedRcodeBadVers = 16 // a.k.a. BADVERS
 )
 
 // Name represents a domain name, a sequence of labels each of which is 63
@@ -83,7 +83,7 @@ func NewName(labels [][]byte) (Name, error) {
 	}
 	// Check the total length.
 	builder := newMessageBuilder()
-	builder.writeName(name)
+	builder.WriteName(name)
 	if len(builder.Bytes()) > 255 {
 		return nil, ErrNameTooLong
 	}
@@ -145,11 +145,15 @@ type Message struct {
 }
 
 // Opcode extracts the OPCODE part of the Flags field.
+//
+// https://tools.ietf.org/html/rfc1035#section-4.1.1
 func (msg *Message) Opcode() uint16 {
 	return (msg.Flags >> 11) & 0xf
 }
 
 // Rcode extracts the RCODE part of the Flags field.
+//
+// https://tools.ietf.org/html/rfc1035#section-4.1.1
 func (msg *Message) Rcode() uint16 {
 	return msg.Flags & 0x000f
 }
@@ -174,6 +178,8 @@ type RR struct {
 	Data  []byte
 }
 
+// readName parses a DNS name from r. It leaves r positioned just after the
+// parsed named.
 func readName(r io.ReadSeeker) (Name, error) {
 	var labels [][]byte
 	// We limit the number of compression pointers we are willing to follow.
@@ -250,10 +256,12 @@ loop:
 	return NewName(labels)
 }
 
+// readQuestion parses one entry from the Question section. It leaves r
+// positioned just after the parsed entry.
+//
+// https://tools.ietf.org/html/rfc1035#section-4.1.2
 func readQuestion(r io.ReadSeeker) (Question, error) {
 	var question Question
-
-	// https://tools.ietf.org/html/rfc1035#section-4.1.2
 	var err error
 	question.Name, err = readName(r)
 	if err != nil {
@@ -269,10 +277,12 @@ func readQuestion(r io.ReadSeeker) (Question, error) {
 	return question, nil
 }
 
+// readRR parses one resource record. It leaves r positioned just after the
+// parsed resource record.
+//
+// https://tools.ietf.org/html/rfc1035#section-4.1.3
 func readRR(r io.ReadSeeker) (RR, error) {
 	var rr RR
-
-	// https://tools.ietf.org/html/rfc1035#section-4.1.3
 	var err error
 	rr.Name, err = readName(r)
 	if err != nil {
@@ -302,6 +312,8 @@ func readRR(r io.ReadSeeker) (RR, error) {
 	return rr, nil
 }
 
+// readMessage parses a complete DNS message. It leaves r positioned just after
+// the parsed message.
 func readMessage(r io.ReadSeeker) (Message, error) {
 	var message Message
 
@@ -350,8 +362,9 @@ func readMessage(r io.ReadSeeker) (Message, error) {
 	return message, nil
 }
 
-// MessageFromWireFormat parses a message from a buffer of bytes and returns a
-// Message object.
+// MessageFromWireFormat parses a message from buf and returns a Message object.
+// It returns ErrTrailingBytes if there are bytes remaining in buf after parsing
+// is done.
 func MessageFromWireFormat(buf []byte) (Message, error) {
 	r := bytes.NewReader(buf)
 	message, err := readMessage(r)
@@ -369,22 +382,29 @@ func MessageFromWireFormat(buf []byte) (Message, error) {
 	return message, err
 }
 
+// messageBuilder manages the state of serializing a DNS message. Its main
+// function is to keep track of names already written for the purpose of name
+// compression.
 type messageBuilder struct {
 	w         bytes.Buffer
 	nameCache map[string]int
 }
 
+// newMessageBuilder creates a new messageBuilder with an empty name cache.
 func newMessageBuilder() *messageBuilder {
 	return &messageBuilder{
 		nameCache: make(map[string]int),
 	}
 }
 
+// Bytes returns the serialized DNS message as a slice of bytes.
 func (builder *messageBuilder) Bytes() []byte {
 	return builder.w.Bytes()
 }
 
-func (builder *messageBuilder) writeName(name Name) {
+// WriteName appends name to the in-progress messageBuilder, employing
+// compression pointers to previously written names if possible.
+func (builder *messageBuilder) WriteName(name Name) {
 	// https://tools.ietf.org/html/rfc1035#section-3.1
 	for i := range name {
 		// Has this suffix already been encoded in the message?
@@ -406,17 +426,20 @@ func (builder *messageBuilder) writeName(name Name) {
 	builder.w.WriteByte(0)
 }
 
-func (builder *messageBuilder) writeQuestion(question *Question) error {
+// WriteQuestion appends a Question section entry to the in-progress
+// messageBuilder.
+func (builder *messageBuilder) WriteQuestion(question *Question) {
 	// https://tools.ietf.org/html/rfc1035#section-4.1.2
-	builder.writeName(question.Name)
+	builder.WriteName(question.Name)
 	binary.Write(&builder.w, binary.BigEndian, question.Type)
 	binary.Write(&builder.w, binary.BigEndian, question.Class)
-	return nil
 }
 
-func (builder *messageBuilder) writeRR(rr *RR) error {
+// WriteRR appends a resource record to the in-progress messageBuilder. It
+// returns ErrIntegerOverflow if the length of rr.Data does not fit in 16 bits.
+func (builder *messageBuilder) WriteRR(rr *RR) error {
 	// https://tools.ietf.org/html/rfc1035#section-4.1.3
-	builder.writeName(rr.Name)
+	builder.WriteName(rr.Name)
 	binary.Write(&builder.w, binary.BigEndian, rr.Type)
 	binary.Write(&builder.w, binary.BigEndian, rr.Class)
 	binary.Write(&builder.w, binary.BigEndian, rr.TTL)
@@ -429,7 +452,11 @@ func (builder *messageBuilder) writeRR(rr *RR) error {
 	return nil
 }
 
-func (builder *messageBuilder) writeMessage(message *Message) error {
+// WriteMessage appends a complete DNS message to the in-progress
+// messageBuilder. It returns ErrIntegerOverflow if the number of entries in any
+// section, or the length of the data in any resource record, does not fit in 16
+// bits.
+func (builder *messageBuilder) WriteMessage(message *Message) error {
 	// Header section
 	// https://tools.ietf.org/html/rfc1035#section-4.1.1
 	binary.Write(&builder.w, binary.BigEndian, message.ID)
@@ -450,17 +477,14 @@ func (builder *messageBuilder) writeMessage(message *Message) error {
 	// Question section
 	// https://tools.ietf.org/html/rfc1035#section-4.1.2
 	for _, question := range message.Question {
-		err := builder.writeQuestion(&question)
-		if err != nil {
-			return err
-		}
+		builder.WriteQuestion(&question)
 	}
 
 	// Answer, Authority, and Additional sections
 	// https://tools.ietf.org/html/rfc1035#section-4.1.3
 	for _, rrs := range [][]RR{message.Answer, message.Authority, message.Additional} {
 		for _, rr := range rrs {
-			err := builder.writeRR(&rr)
+			err := builder.WriteRR(&rr)
 			if err != nil {
 				return err
 			}
@@ -470,10 +494,12 @@ func (builder *messageBuilder) writeMessage(message *Message) error {
 	return nil
 }
 
-// WireFormat encodes a Message as a slice of bytes in wire format.
+// WireFormat encodes a Message as a slice of bytes in DNS wire format. It
+// returns ErrIntegerOverflow if the number of entries in any section, or the
+// length of the data in any resource record, does not fit in 16 bits.
 func (message *Message) WireFormat() ([]byte, error) {
 	builder := newMessageBuilder()
-	err := builder.writeMessage(message)
+	err := builder.WriteMessage(message)
 	if err != nil {
 		return nil, err
 	}
@@ -483,6 +509,8 @@ func (message *Message) WireFormat() ([]byte, error) {
 // DecodeRDataTXT decodes TXT-DATA (as found in the RDATA for a resource record
 // with TYPE=TXT) as a raw byte slice, by concatenating all the
 // <character-string>s it contains.
+//
+// https://tools.ietf.org/html/rfc1035#section-3.3.14
 func DecodeRDataTXT(p []byte) ([]byte, error) {
 	var buf bytes.Buffer
 	for {
@@ -504,8 +532,10 @@ func DecodeRDataTXT(p []byte) ([]byte, error) {
 }
 
 // EncodeRDataTXT encodes a slice of bytes as TXT-DATA, as appropriate for the
-// RDATA of a resource record with TYPE=TXT. There is no length restriction;
-// that must be checked at a higher level.
+// RDATA of a resource record with TYPE=TXT. No length restriction is enforced
+// here; that must be checked at a higher level.
+//
+// https://tools.ietf.org/html/rfc1035#section-3.3.14
 func EncodeRDataTXT(p []byte) []byte {
 	// https://tools.ietf.org/html/rfc1035#section-3.3
 	// https://tools.ietf.org/html/rfc1035#section-3.3.14

+ 105 - 16
dnstt-client/dns.go

@@ -22,22 +22,55 @@ const (
 	// to reduce the chance of a cache hit. Cannot be greater than 31,
 	// because the prefix codes indicating padding start at 224.
 	numPaddingForPoll = 8
+
+	// sendLoop has a poll timer that automatically sends an empty polling
+	// query when a certain amount of time has elapsed without a send. The
+	// poll timer is initially set to initPollDelay. It increases by a
+	// factor of pollDelayMultiplier every time the poll timer expires, up
+	// to a maximum of maxPollDelay. The poll timer is reset to
+	// initPollDelay whenever an a send occurs that is not the result of the
+	// poll timer expiring.
+	initPollDelay       = 500 * time.Millisecond
+	maxPollDelay        = 10 * time.Second
+	pollDelayMultiplier = 2.0
 )
 
-// A base32 encoding without padding.
+// base32Encoding is a base32 encoding without padding.
 var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding)
 
+// DNSPacketConn provides a packet-sending and -receiving interface over various
+// forms of DNS. It handles the details of how packets and padding are encoded
+// as a DNS name in the Question section of an upstream query, and as a TXT RR
+// in downstream responses.
+//
+// DNSPacketConn does not handle the mechanics of actually sending and receiving
+// encoded DNS messages. That is rather the responsibility of some other
+// net.PacketConn such as net.UDPConn, HTTPPacketConn, or TLSPacketConn, one of
+// which must be provided to NewDNSPacketConn.
+//
+// We don't have a need to match up a query and a response by ID. Queries and
+// responses are vehicles for carrying data and for our purposes don't need to
+// be correlated. When sending a query, we generate a random ID, and when
+// receiving a response, we ignore the ID.
 type DNSPacketConn struct {
 	clientID turbotunnel.ClientID
 	domain   dns.Name
+	// Sending on pollChan permits sendLoop to send an empty polling query.
+	// sendLoop also does its own polling according to a time schedule.
 	pollChan chan struct{}
+	// QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
+	// recvLoop and sendLoop take the messages out of the receive and send
+	// queues and actually put them on the network.
 	*turbotunnel.QueuePacketConn
 }
 
+// NewDNSPacketConn creates a new DNSPacketConn. transport, through its WriteTo
+// and ReadFrom methods, handles the actual sending and receiving the DNS
+// messages encoded by DNSPacketConn. addr is the address to be passed to
+// transport.WriteTo whenever a message needs to be sent.
 func NewDNSPacketConn(transport net.PacketConn, addr net.Addr, domain dns.Name) *DNSPacketConn {
 	// Generate a new random ClientID.
-	var clientID turbotunnel.ClientID
-	rand.Read(clientID[:])
+	clientID := turbotunnel.NewClientID()
 	c := &DNSPacketConn{
 		clientID:        clientID,
 		domain:          domain,
@@ -59,6 +92,9 @@ func NewDNSPacketConn(transport net.PacketConn, addr net.Addr, domain dns.Name)
 	return c
 }
 
+// dnsResponsePayload extracts the downstream payload of a DNS response, encoded
+// into the RDATA of a TXT RR. It returns nil if the message doesn't pass format
+// checks, or if the name in its Question entry is not a subdomain of domain.
 func dnsResponsePayload(resp *dns.Message, domain dns.Name) []byte {
 	if resp.Flags&0x8000 != 0x8000 {
 		// QR != 1, this is not a response.
@@ -91,26 +127,50 @@ func dnsResponsePayload(resp *dns.Message, domain dns.Name) []byte {
 	return payload
 }
 
+// nextPacket reads the next length-prefixed packet from r. It returns a nil
+// error only when a complete packet was read. It returns io.EOF only when there
+// were 0 bytes remaining to read from r. It returns io.ErrUnexpectedEOF when
+// EOF occurs in the middle of an encoded packet.
 func nextPacket(r *bytes.Reader) ([]byte, error) {
-	eof := func(err error) error {
-		if err == io.EOF {
-			err = io.ErrUnexpectedEOF
-		}
-		return err
-	}
-
 	for {
 		var n uint16
 		err := binary.Read(r, binary.BigEndian, &n)
 		if err != nil {
+			// We may return a real io.EOF only here.
 			return nil, err
 		}
 		p := make([]byte, n)
 		_, err = io.ReadFull(r, p)
-		return p, eof(err)
+		// Here we must change io.EOF to io.ErrUnexpectedEOF.
+		if err == io.EOF {
+			err = io.ErrUnexpectedEOF
+		}
+		return p, err
 	}
 }
 
+// recvLoop repeatedly calls transport.ReadFrom to receive a DNS message,
+// extracts its payload and breaks it into packets, and stores the packets in a
+// queue to be returned from a future call to c.ReadFrom.
+//
+// Whenever we receive a response with a non-empty payload, we send twice on
+// c.pollChan to permit sendLoop to send two immediate polling queries. The
+// intuition behind polling immediately after receiving is that we know the
+// server has just had something to send, it may need to send more, and the only
+// way it can send is if we give it a query to respond to. The intuition behind
+// doing *two* polls when we receive is similar to TCP slow start: we want to
+// maintain some number of queries "in flight", and the faster the server is
+// sending, the higher that number should be. If we polled only once in response
+// to received data, we would tend to have only one query in flight at a time,
+// ping-pong style. The first polling request replaces the in-flight request
+// that has just finished in our receiving data; the second grows the effective
+// in-flight window proportionally to the rate at which data-carrying responses
+// are being received. Compare to Eq. (2) of
+// https://tools.ietf.org/html/rfc5681#section-3.1; the differences are that we
+// count messages, not bytes, and we don't maintain an explicit window. If a
+// response comes back without data, or if a query or response is dropped by the
+// network, then we don't poll again, which decreases the effective in-flight
+// window.
 func (c *DNSPacketConn) recvLoop(transport net.PacketConn) error {
 	for {
 		var buf [4096]byte
@@ -155,6 +215,8 @@ func (c *DNSPacketConn) recvLoop(transport net.PacketConn) error {
 	}
 }
 
+// chunks breaks p into non-empty subslices of at most n bytes, greedily so that
+// only final subslice has length < n.
 func chunks(p []byte, n int) [][]byte {
 	var result [][]byte
 	for len(p) > 0 {
@@ -168,7 +230,28 @@ func chunks(p []byte, n int) [][]byte {
 	return result
 }
 
-// send sends a single packet in a DNS query.
+// send sends p as a single packet encoded into a DNS query, using
+// transport.WriteTo(query, addr). The length of p must be less than 224 bytes.
+//
+// Here is an example of how a packet is encoded into a DNS name, using
+//     p = "supercalifragilisticexpialidocious"
+//     c.clientID = "CLIENTID"
+//     domain = "t.example.com"
+//
+// 0. Start with the raw packet contents.
+//     supercalifragilisticexpialidocious
+// 1. Length-prefix the packet and add random padding. A length prefix L < 0xe0
+// means a data packet of L bytes. A length prefix L >= 0xe0 means padding of L -
+// 0xe0 bytes (not counting the length of the length prefix itself).
+//     \xe3\xd9\xa3\x15\x22supercalifragilisticexpialidocious
+// 2. Prefix the ClientID.
+//     CLIENTID\xe3\xd9\xa3\x15\x22supercalifragilisticexpialidocious
+// 3. Base32-encode, without padding and in lower case.
+//     ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3djmrxwg2lpovzq
+// 4. Break into labels of at most 63 octets.
+//     ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3d.jmrxwg2lpovzq
+// 5. Append the domain.
+//     ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3d.jmrxwg2lpovzq.t.example.com
 func (c *DNSPacketConn) send(transport net.PacketConn, p []byte, addr net.Addr) error {
 	var decoded []byte
 	{
@@ -235,6 +318,9 @@ func (c *DNSPacketConn) send(transport net.PacketConn, p []byte, addr net.Addr)
 	return err
 }
 
+// sendLoop takes packets that have been written using c.WriteTo, and sends them
+// on the network using send. It also does polling with empty packets when
+// requested by pollChan or after a timeout.
 func (c *DNSPacketConn) sendLoop(transport net.PacketConn, addr net.Addr) error {
 	pollDelay := initPollDelay
 	pollTimer := time.NewTimer(pollDelay)
@@ -242,9 +328,9 @@ func (c *DNSPacketConn) sendLoop(transport net.PacketConn, addr net.Addr) error
 		var p []byte
 		outgoingQueue := c.QueuePacketConn.OutgoingQueue(addr)
 		pollTimerExpired := false
+		// Prioritize sending an actual data packet from OutgoingQueue.
+		// Only consider a poll when OutgoingQueue is empty.
 		select {
-		// Give priority to sending an actual data packet from
-		// OutgoingQueue. Only when that is empty, consider a poll.
 		case p = <-outgoingQueue:
 		default:
 			select {
@@ -258,8 +344,8 @@ func (c *DNSPacketConn) sendLoop(transport net.PacketConn, addr net.Addr) error
 		}
 
 		if len(p) > 0 {
-			// We have an actual data-carrying packet, so discard a
-			// pending poll opportunity, if any.
+			// A data-carrying packet displaces one pending poll
+			// opportunity, if any.
 			select {
 			case <-c.pollChan:
 			default:
@@ -284,6 +370,9 @@ func (c *DNSPacketConn) sendLoop(transport net.PacketConn, addr net.Addr) error
 		}
 		pollTimer.Reset(pollDelay)
 
+		// Unlike in the server, in the client we assume that because
+		// the data capacity of queries is so limited, it's not worth
+		// trying to send more than one packet per query.
 		err := c.send(transport, p, addr)
 		if err != nil {
 			log.Printf("send: %v", err)

+ 39 - 14
dnstt-client/dns_test.go

@@ -2,26 +2,51 @@ package main
 
 import (
 	"bytes"
+	"io"
 	"testing"
-
-	"www.bamsoftware.com/git/dnstt.git/dns"
 )
 
-func TestDNSNameCapacity(t *testing.T) {
-	for domainLen := 0; domainLen < 255; domainLen++ {
-		domain, err := dns.NewName(chunks(bytes.Repeat([]byte{'x'}, domainLen), 63))
+func allPackets(buf []byte) ([][]byte, error) {
+	var packets [][]byte
+	r := bytes.NewReader(buf)
+	for {
+		p, err := nextPacket(r)
 		if err != nil {
-			continue
+			return packets, err
 		}
-		capacity := dnsNameCapacity(domain)
-		if capacity <= 0 {
-			continue
+		packets = append(packets, p)
+	}
+}
+
+func packetsEqual(a, b [][]byte) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i := range a {
+		if !bytes.Equal(a[i], b[i]) {
+			return false
 		}
-		prefix := []byte(base32Encoding.EncodeToString(bytes.Repeat([]byte{'y'}, capacity)))
-		labels := append(chunks(prefix, 63), domain...)
-		_, err = dns.NewName(labels)
-		if err != nil {
-			t.Errorf("length %v  capacity %v  %v", domainLen, capacity, err)
+	}
+	return true
+}
+
+func TestNextPacket(t *testing.T) {
+	for _, test := range []struct {
+		input   string
+		packets [][]byte
+		err     error
+	}{
+		{"", [][]byte{}, io.EOF},
+		{"\x00", [][]byte{}, io.ErrUnexpectedEOF},
+		{"\x00\x00", [][]byte{{}}, io.EOF},
+		{"\x00\x00\x00", [][]byte{{}}, io.ErrUnexpectedEOF},
+		{"\x00\x01", [][]byte{}, io.ErrUnexpectedEOF},
+		{"\x00\x05hello\x00\x05world", [][]byte{[]byte("hello"), []byte("world")}, io.EOF},
+	} {
+		packets, err := allPackets([]byte(test.input))
+		if !packetsEqual(packets, test.packets) || err != test.err {
+			t.Errorf("%x\nreturned %x %v\nexpected %x %v",
+				test.input, packets, err, test.packets, test.err)
 		}
 	}
 }

+ 64 - 32
dnstt-client/http.go

@@ -6,7 +6,6 @@ import (
 	"io"
 	"io/ioutil"
 	"log"
-	"net"
 	"net/http"
 	"strconv"
 	"sync"
@@ -15,50 +14,62 @@ import (
 	"www.bamsoftware.com/git/dnstt.git/turbotunnel"
 )
 
+// A default Retry-After delay to use when there is no explicit Retry-After
+// header in an HTTP response.
+const defaultRetryAfter = 10 * time.Second
+
+// The *http.Client shared by instances of HTTPPacketConn. We use this instead
+// of http.DefaultClient in order to set a timeout.
+var httpClient = &http.Client{Timeout: 1 * time.Minute}
+
+// HTTPPacketConn is an HTTP-based transport for DNS messages, used for DNS over
+// HTTPS (DoH). Its WriteTo and ReadFrom methods exchange DNS messages over HTTP
+// requests and responses.
+//
+// HTTPPacketConn deals only with alreaday formatted DNS messages. It does not
+// handle encoding information into the messages. That is rather the
+// responsibility of DNSPacketConn.
+//
+// https://tools.ietf.org/html/rfc8484
 type HTTPPacketConn struct {
-	urlString     string
-	client        *http.Client
+	// urlString is the URL to which HTTP requests will be sent, for example
+	// "https://doh.example/dns-query".
+	urlString string
+
+	// notBefore, if not zero, is a time before which we may not send any
+	// queries; queries are buffered or dropped until that time. notBefore
+	// is set when we get a 429 Too Many Requests HTTP response or other
+	// unexpected status code that causes us to need to slow down. It is set
+	// according to the Retry-After header if available, otherwise it is set
+	// to defaultRetryAfter in the future. notBeforeLock controls access to
+	// notBefore.
 	notBefore     time.Time
 	notBeforeLock sync.RWMutex
+
+	// QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
+	// sendLoop, via send, removes messages from the outgoing queue that
+	// were placed there by WriteTo, and inserts messages into the incoming
+	// queue to be returned from ReadFrom.
 	*turbotunnel.QueuePacketConn
 }
 
+// NewHTTPPacketConn creates a new HTTPPacketConn configured to use the HTTP
+// server at urlString as a DNS over HTTP resolver. urlString should include any
+// necessary path components; e.g., "/dns-query". numSenders is the number of
+// concurrent sender-receiver goroutines to run.
 func NewHTTPPacketConn(urlString string, numSenders int) (*HTTPPacketConn, error) {
 	c := &HTTPPacketConn{
-		urlString: urlString,
-		client: &http.Client{
-			Timeout: 1 * time.Minute,
-		},
+		urlString:       urlString,
 		QueuePacketConn: turbotunnel.NewQueuePacketConn(turbotunnel.DummyAddr{}, 0),
 	}
 	for i := 0; i < numSenders; i++ {
-		go func() {
-			for p := range c.QueuePacketConn.OutgoingQueue(turbotunnel.DummyAddr{}) {
-				err := c.send(p)
-				if err != nil {
-					log.Printf("sender thread: %v", err)
-				}
-			}
-		}()
+		go c.sendLoop()
 	}
 	return c, nil
 }
 
-func (c *HTTPPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
-	// Drop packets while we are rate-limiting ourselves (as a result of a
-	// Retry-After response header, for example).
-	c.notBeforeLock.RLock()
-	notBefore := c.notBefore
-	c.notBeforeLock.RUnlock()
-	if time.Now().Before(notBefore) {
-		return len(p), nil
-	}
-
-	// Ignore addr.
-	return c.QueuePacketConn.WriteTo(p, turbotunnel.DummyAddr{})
-}
-
-// send sends a single packet in an HTTP request.
+// send sends a message in an HTTP request, and queues the body HTTP response to
+// be returned from a future call to ReadFrom.
 func (c *HTTPPacketConn) send(p []byte) error {
 	req, err := http.NewRequest("POST", c.urlString, bytes.NewReader(p))
 	if err != nil {
@@ -67,7 +78,7 @@ func (c *HTTPPacketConn) send(p []byte) error {
 	req.Header.Set("Accept", "application/dns-message")
 	req.Header.Set("Content-Type", "application/dns-message")
 	req.Header.Set("User-Agent", "") // Disable default "Go-http-client/1.1".
-	resp, err := c.client.Do(req)
+	resp, err := httpClient.Do(req)
 	if err != nil {
 		return err
 	}
@@ -100,7 +111,7 @@ func (c *HTTPPacketConn) send(p []byte) error {
 		}
 		if retryAfter.IsZero() {
 			// Supply a default.
-			retryAfter = now.Add(10 * time.Second)
+			retryAfter = now.Add(defaultRetryAfter)
 		}
 		if retryAfter.Before(now) {
 			log.Printf("got %+q, but Retry-After is %v in the past",
@@ -122,6 +133,27 @@ func (c *HTTPPacketConn) send(p []byte) error {
 	return nil
 }
 
+// sendLoop loops over the contents of the outgoing queue and passes them to
+// send. It drops packets while c.notBefore is in the future.
+func (c *HTTPPacketConn) sendLoop() {
+	for p := range c.QueuePacketConn.OutgoingQueue(turbotunnel.DummyAddr{}) {
+		// Stop sending while we are rate-limiting ourselves (as a
+		// result of a Retry-After response header, for example).
+		c.notBeforeLock.RLock()
+		notBefore := c.notBefore
+		c.notBeforeLock.RUnlock()
+		if wait := notBefore.Sub(time.Now()); wait > 0 {
+			// Drop it.
+			continue
+		}
+
+		err := c.send(p)
+		if err != nil {
+			log.Printf("sendLoop: %v", err)
+		}
+	}
+}
+
 // parseRetryAfter parses the value of a Retry-After header as an absolute
 // time.Time.
 func parseRetryAfter(value string, now time.Time) (time.Time, error) {

+ 57 - 34
dnstt-client/main.go

@@ -1,3 +1,28 @@
+// dnstt-client is the client end of a DNS tunnel.
+//
+// Usage:
+//     dnstt-client [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR
+//
+// Examples:
+//     dnstt-client -doh https://resolver.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000
+//     dnstt-client -dot resolver.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000
+//
+// The program supports DNS over HTTPS (DoH), DNS over TLS (DoT), and UDP DNS.
+// Use one of these options:
+//     -doh https://resolver.example/dns-query
+//     -dot resolver.example:853
+//     -udp resolver.example:53
+//
+// You can give the server's public key as a file or as a hex string. Use
+// "dnstt-server -gen-key" to get the public key.
+//     -pubkey-file server.pub
+//     -pubkey 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
+//
+// DOMAIN is the root of the DNS zone reserved for the tunnel. See README for
+// instructions on setting it up.
+//
+// LOCALADDR is the TCP address that will listen for connections and forward
+// them over the tunnel.
 package main
 
 import (
@@ -17,12 +42,38 @@ import (
 	"www.bamsoftware.com/git/dnstt.git/turbotunnel"
 )
 
-const (
-	idleTimeout         = 10 * time.Minute
-	initPollDelay       = 500 * time.Millisecond
-	maxPollDelay        = 10 * time.Second
-	pollDelayMultiplier = 2.0
-)
+// smux streams will be closed after this much time without receiving data.
+const idleTimeout = 10 * time.Minute
+
+// dnsNameCapacity returns the number of bytes remaining for encoded data after
+// including domain in a DNS name.
+func dnsNameCapacity(domain dns.Name) int {
+	// Names must be 255 octets or shorter in total length.
+	// https://tools.ietf.org/html/rfc1035#section-2.3.4
+	capacity := 255
+	// Subtract the length of the null terminator.
+	capacity -= 1
+	for _, label := range domain {
+		// Subtract the length of the label and the length octet.
+		capacity -= len(label) + 1
+	}
+	// Each label may be up to 63 bytes long and requires 64 bytes to
+	// encode.
+	capacity = capacity * 63 / 64
+	// Base32 expands every 5 bytes to 8.
+	capacity = capacity * 5 / 8
+	return capacity
+}
+
+// readKeyFromFile reads a key from a named file.
+func readKeyFromFile(filename string) ([]byte, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return noise.ReadKey(f)
+}
 
 func handle(local *net.TCPConn, sess *smux.Session, conv uint32) error {
 	stream, err := sess.OpenStream()
@@ -64,34 +115,6 @@ func handle(local *net.TCPConn, sess *smux.Session, conv uint32) error {
 	return err
 }
 
-// dnsNameCapacity returns the number of bytes remaining for encoded data after
-// including domain in a DNS name.
-func dnsNameCapacity(domain dns.Name) int {
-	// https://tools.ietf.org/html/rfc1035#section-2.3.4
-	// Names must be 255 octets or shorter in total length.
-	capacity := 255
-	// Subtract the length of the null terminator.
-	capacity -= 1
-	for _, label := range domain {
-		// Subtract the length of the label and the length octet.
-		capacity -= len(label) + 1
-	}
-	// Each label may be up to 63 bytes long and requires 64
-	capacity = capacity * 63 / 64
-	// Base32 expands every 5 bytes to 8.
-	capacity = capacity * 5 / 8
-	return capacity
-}
-
-func readKeyFromFile(filename string) ([]byte, error) {
-	f, err := os.Open(filename)
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-	return noise.ReadKey(f)
-}
-
 func run(pubkey []byte, domain dns.Name, localAddr *net.TCPAddr, remoteAddr net.Addr, pconn net.PacketConn) error {
 	defer pconn.Close()
 

+ 27 - 0
dnstt-client/main_test.go

@@ -0,0 +1,27 @@
+package main
+
+import (
+	"bytes"
+	"testing"
+
+	"www.bamsoftware.com/git/dnstt.git/dns"
+)
+
+func TestDNSNameCapacity(t *testing.T) {
+	for domainLen := 0; domainLen < 255; domainLen++ {
+		domain, err := dns.NewName(chunks(bytes.Repeat([]byte{'x'}, domainLen), 63))
+		if err != nil {
+			continue
+		}
+		capacity := dnsNameCapacity(domain)
+		if capacity <= 0 {
+			continue
+		}
+		prefix := []byte(base32Encoding.EncodeToString(bytes.Repeat([]byte{'y'}, capacity)))
+		labels := append(chunks(prefix, 63), domain...)
+		_, err = dns.NewName(labels)
+		if err != nil {
+			t.Errorf("length %v  capacity %v  %v", domainLen, capacity, err)
+		}
+	}
+}

+ 26 - 5
dnstt-client/tls.go

@@ -11,14 +11,35 @@ import (
 	"www.bamsoftware.com/git/dnstt.git/turbotunnel"
 )
 
+// TLSPacketConn is a TLS- and TCP-based transport for DNS messages, used for
+// DNS over TLS (DoT). Its WriteTo and ReadFrom methods exchange DNS messages
+// over a TLS channel, prefixing each message with a two-octet length field as
+// in DNS over TCP.
+//
+// TLSPacketConn deals only with alreaday formatted DNS messages. It does not
+// handle encoding information into the messages. That is rather the
+// responsibility of DNSPacketConn.
+//
+// https://tools.ietf.org/html/rfc7858
 type TLSPacketConn struct {
+	// QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
+	// recvLoop and sendLoop take the messages out of the receive and send
+	// queues and actually put them on the network.
 	*turbotunnel.QueuePacketConn
 }
 
+// NewTLSPacketConn creates a new TLSPacketConn configured to use the TLS
+// server at addr as a DNS over TLS resolver. It maintains a TLS connection to
+// the resolver, reconnecting as necessary. It closes the connection if any
+// reconnection attempt fails.
 func NewTLSPacketConn(addr string) (*TLSPacketConn, error) {
 	c := &TLSPacketConn{
 		QueuePacketConn: turbotunnel.NewQueuePacketConn(turbotunnel.DummyAddr{}, 0),
 	}
+	// We maintain one TLS connection at a time, redialing it whenever it
+	// becomes disconnected. We do the first dial here, outside the
+	// goroutine, so that any immediate and permanent connection errors are
+	// reported directly to the caller of NewTLSPacketConn.
 	tlsConfig := &tls.Config{}
 	conn, err := tls.Dial("tcp", addr, tlsConfig)
 	if err != nil {
@@ -46,6 +67,7 @@ func NewTLSPacketConn(addr string) (*TLSPacketConn, error) {
 			wg.Wait()
 			conn.Close()
 
+			// Whenever the TLS connection dies, redial a new one.
 			conn, err = tls.Dial("tcp", addr, tlsConfig)
 			if err != nil {
 				log.Printf("tls.Dial: %v", err)
@@ -56,6 +78,8 @@ func NewTLSPacketConn(addr string) (*TLSPacketConn, error) {
 	return c, nil
 }
 
+// recvLoop reads length-prefixed messages from conn and passes them to the
+// incoming queue.
 func (c *TLSPacketConn) recvLoop(conn net.Conn) error {
 	for {
 		var length uint16
@@ -75,6 +99,8 @@ func (c *TLSPacketConn) recvLoop(conn net.Conn) error {
 	}
 }
 
+// sendLoop reads messages from the outgoing queue and writes them,
+// length-prefixed, to conn.
 func (c *TLSPacketConn) sendLoop(conn net.Conn) error {
 	for p := range c.QueuePacketConn.OutgoingQueue(turbotunnel.DummyAddr{}) {
 		length := uint16(len(p))
@@ -92,8 +118,3 @@ func (c *TLSPacketConn) sendLoop(conn net.Conn) error {
 	}
 	return nil
 }
-
-func (c *TLSPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
-	// Ignore addr.
-	return c.QueuePacketConn.WriteTo(p, turbotunnel.DummyAddr{})
-}

+ 173 - 106
dnstt-server/main.go

@@ -1,3 +1,32 @@
+// dnstt-server is the server end of a DNS tunnel.
+//
+// Usage:
+//     dnstt-server -gen-key [-privkey-file PRIVKEYFILE] [-pubkey-file PUBKEYFILE]
+//     dnstt-server -udp ADDR [-privkey PRIVKEY|-privkey-file PRIVKEYFILE] DOMAIN UPSTREAMADDR
+//
+// Example:
+//     dnstt-server -gen-key -privkey-file server.key -pubkey-file server.pub
+//     dnstt-server -udp 127.0.0.1:5300 -privkey-file server.key t.example.com 127.0.0.1:8000
+//
+// To generate a persistent server private key, first run with the -gen-key
+// option. By default the generated private and public keys are printed to
+// standard output. To save them to files instead, use the -privkey-file and
+// -pubkey-file options.
+//     dnstt-server -gen-key
+//     dnstt-server -gen-key -privkey-file server.key -pubkey-file server.pub
+//
+// You can give the server's private key as a file or as a hex string.
+//     -privkey-file server.key
+//     -privkey 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
+//
+// The -udp option controls the address that will listen for incoming DNS
+// queries.
+//
+// DOMAIN is the root of the DNS zone reserved for the tunnel. See README for
+// instructions on setting it up.
+//
+// UPSTREAMADDR is the TCP address to which incoming tunnelled streams will be
+// forwarded.
 package main
 
 import (
@@ -22,7 +51,10 @@ import (
 )
 
 const (
+	// smux streams will be closed after this much time without receiving data.
 	idleTimeout = 10 * time.Minute
+
+	// How to set the TTL field in Answer resource records.
 	responseTTL = 60
 
 	// We don't send UDP payloads larger than this, in an attempt to avoid
@@ -64,10 +96,96 @@ const (
 	maxResponseDelay = 1 * time.Second
 )
 
-// A base32 encoding without padding.
+// base32Encoding is a base32 encoding without padding.
 var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding)
 
-// handleStream bidirectionally connects a client stream with the ORPort.
+// generateKeypair generates a private key and the corresponding public key. If
+// privkeyFilename and pubkeyFilename are respectively empty, it prints the
+// corresponding key to standard output; otherwise it saves the key to the given
+// file name. In case of any error, it attempts to delete any files it has
+// created before returning.
+func generateKeypair(privkeyFilename, pubkeyFilename string) (err error) {
+	// Filenames to delete in case of error (avoid leaving partially written
+	// files).
+	var toDelete []string
+	defer func() {
+		for _, filename := range toDelete {
+			fmt.Fprintf(os.Stderr, "deleting partially written file %s\n", filename)
+			if closeErr := os.Remove(filename); closeErr != nil {
+				fmt.Fprintf(os.Stderr, "cannot remove %s: %v\n", filename, closeErr)
+				if err == nil {
+					err = closeErr
+				}
+			}
+		}
+	}()
+
+	privkey, pubkey, err := noise.GenerateKeypair()
+	if err != nil {
+		return err
+	}
+
+	if privkeyFilename != "" {
+		// Save the privkey to a file.
+		f, err := os.Create(privkeyFilename)
+		if err != nil {
+			return err
+		}
+		toDelete = append(toDelete, privkeyFilename)
+		err = noise.WriteKey(f, privkey)
+		if err2 := f.Close(); err == nil {
+			err = err2
+		}
+		if err != nil {
+			return err
+		}
+	}
+
+	if pubkeyFilename != "" {
+		// Save the pubkey to a file.
+		f, err := os.Create(pubkeyFilename)
+		if err != nil {
+			return err
+		}
+		toDelete = append(toDelete, pubkeyFilename)
+		err = noise.WriteKey(f, pubkey)
+		if err2 := f.Close(); err == nil {
+			err = err2
+		}
+		if err != nil {
+			return err
+		}
+	}
+
+	// All good, allow the written files to remain.
+	toDelete = nil
+
+	if privkeyFilename != "" {
+		fmt.Printf("privkey written to %s\n", privkeyFilename)
+	} else {
+		fmt.Printf("privkey %x\n", privkey)
+	}
+	if pubkeyFilename != "" {
+		fmt.Printf("pubkey  written to %s\n", pubkeyFilename)
+	} else {
+		fmt.Printf("pubkey  %x\n", pubkey)
+	}
+
+	return nil
+}
+
+// readKeyFromFile reads a key from a named file.
+func readKeyFromFile(filename string) ([]byte, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return noise.ReadKey(f)
+}
+
+// handleStream bidirectionally connects a client stream with a TCP socket
+// addressed by upstream.
 func handleStream(stream *smux.Stream, upstream *net.TCPAddr, conv uint32) error {
 	conn, err := net.DialTCP("tcp", nil, upstream)
 	if err != nil {
@@ -104,8 +222,8 @@ func handleStream(stream *smux.Stream, upstream *net.TCPAddr, conv uint32) error
 	return nil
 }
 
-// acceptStreams layers an smux.Session on a KCP connection and awaits streams
-// on it. It passes each stream to handleStream.
+// acceptStreams wraps a KCP session in a Noise channel and an smux.Session,
+// then awaits smux streams. It passes each stream to handleStream.
 func acceptStreams(conn *kcp.UDPSession, privkey, pubkey []byte, upstream *net.TCPAddr) error {
 	// Put a Noise channel on top of the KCP conn.
 	rw, err := noise.NewServer(conn, privkey, pubkey)
@@ -113,6 +231,7 @@ func acceptStreams(conn *kcp.UDPSession, privkey, pubkey []byte, upstream *net.T
 		return err
 	}
 
+	// Put an smux session on top of the encrypted Noise channel.
 	smuxConfig := smux.DefaultConfig()
 	smuxConfig.Version = 2
 	smuxConfig.KeepAliveTimeout = idleTimeout
@@ -183,7 +302,16 @@ func acceptSessions(ln *kcp.Listener, privkey, pubkey []byte, upstream *net.TCPA
 	}
 }
 
+// nextPacket reads the next length-prefixed packet from r, ignoring padding. It
+// returns a nil error only when a packet was read successfully. It returns
+// io.EOF only when there were 0 bytes remaining to read from r. It returns
+// io.ErrUnexpectedEOF when EOF occurs in the middle of an encoded packet.
+//
+// The prefixing scheme is as follows. A length prefix L < 0xe0 means a data
+// packet of L bytes. A length prefix L >= 0xe0 means padding of L - 0xe0 bytes
+// (not counting the length of the length prefix itself).
 func nextPacket(r *bytes.Reader) ([]byte, error) {
+	// Convert io.EOF to io.ErrUnexpectedEOF.
 	eof := func(err error) error {
 		if err == io.EOF {
 			err = io.ErrUnexpectedEOF
@@ -194,6 +322,7 @@ func nextPacket(r *bytes.Reader) ([]byte, error) {
 	for {
 		prefix, err := r.ReadByte()
 		if err != nil {
+			// We may return a real io.EOF only here.
 			return nil, err
 		}
 		if prefix >= 224 {
@@ -202,14 +331,20 @@ func nextPacket(r *bytes.Reader) ([]byte, error) {
 			if err != nil {
 				return nil, eof(err)
 			}
-			continue
+		} else {
+			p := make([]byte, int(prefix))
+			_, err = io.ReadFull(r, p)
+			return p, eof(err)
 		}
-		p := make([]byte, int(prefix))
-		_, err = io.ReadFull(r, p)
-		return p, eof(err)
 	}
 }
 
+// responseFor constructs a response dns.Message that is appropriate for query.
+// Along with the dns.Message, it returns the ClientID extracted from the query
+// and its decoded data payload. If the returned dns.Message is nil, it means
+// that there should be no response to this query. If the returned dns.Message
+// has an Rcode() of dns.RcodeNoError, the message is a candidate for for
+// carrying downstream data in a TXT record.
 func responseFor(query *dns.Message, domain dns.Name) (*dns.Message, turbotunnel.ClientID, []byte) {
 	var clientID turbotunnel.ClientID
 
@@ -344,30 +479,21 @@ func responseFor(query *dns.Message, domain dns.Name) (*dns.Message, turbotunnel
 	return resp, clientID, payload[len(clientID):]
 }
 
-// record represents a response set up with metadata appropriate for a response
-// to a previously received query. recvLoop sends instances of this type to
-// sendLoop via a channel. sendLoop may optionally fill in the response's Answer
-// section before sending it.
+// record represents a DNS message appropriate for a response to a previously
+// received query, along with metadata necessary for sending the response.
+// recvLoop sends instances of record to sendLoop via a channel. sendLoop
+// receives instances of record and may fill in the message's Answer section
+// before sending it.
 type record struct {
 	Resp     *dns.Message
 	Addr     net.Addr
 	ClientID turbotunnel.ClientID
 }
 
-func loop(dnsConn net.PacketConn, domain dns.Name, ttConn *turbotunnel.QueuePacketConn) error {
-	ch := make(chan *record, 100)
-	defer close(ch)
-
-	go func() {
-		err := sendLoop(dnsConn, ttConn, ch)
-		if err != nil {
-			log.Printf("sendLoop: %v", err)
-		}
-	}()
-
-	return recvLoop(domain, dnsConn, ttConn, ch)
-}
-
+// recvLoop repeatedly calls dnsConn.ReadFrom, extracts the packets contained in
+// the incoming DNS queries, and puts them on ttConn's incoming queue. Whenever
+// a query calls for a response, constructs a partial response and passes it to
+// sendLoop over ch.
 func recvLoop(domain dns.Name, dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch chan<- *record) error {
 	for {
 		var buf [4096]byte
@@ -408,6 +534,10 @@ func recvLoop(domain dns.Name, dnsConn net.PacketConn, ttConn *turbotunnel.Queue
 	}
 }
 
+// sendLoop repeatedly receives records from ch. Those that represent an error
+// response, it sends on the network immediately. Those that represent a
+// response capable of carrying data, it packs full of as many packets as will
+// fit, then sends it.
 func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <-chan *record) error {
 	var nextRec *record
 	var nextP []byte
@@ -450,6 +580,10 @@ func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <-
 			}
 			nextP = nil
 
+			// We loop and write as many packets from OutgoingQueue
+			// into the response as will fit. Any packet that would
+			// overflow the capacity of the DNS response, we save in
+			// nextP to be included in a future response.
 			timer := time.NewTimer(maxResponseDelay)
 		loop:
 			for {
@@ -503,6 +637,8 @@ func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <-
 			buf = buf[:maxUDPPayload]
 			buf[2] |= 0x02 // TC = 1
 		}
+
+		// Now we actually send the message as a UDP packet.
 		_, err = dnsConn.WriteTo(buf, rec.Addr)
 		if err != nil {
 			if err, ok := err.(net.Error); ok && err.Temporary() {
@@ -515,85 +651,6 @@ func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <-
 	return nil
 }
 
-func generateKeypair(privkeyFilename, pubkeyFilename string) (err error) {
-	// Filenames to delete in case of error (avoid leaving partially written
-	// files).
-	var toDelete []string
-	defer func() {
-		for _, filename := range toDelete {
-			fmt.Fprintf(os.Stderr, "deleting partially written file %s\n", filename)
-			if closeErr := os.Remove(filename); closeErr != nil {
-				fmt.Fprintf(os.Stderr, "cannot remove %s: %v\n", filename, closeErr)
-				if err == nil {
-					err = closeErr
-				}
-			}
-		}
-	}()
-
-	privkey, pubkey, err := noise.GenerateKeypair()
-	if err != nil {
-		return err
-	}
-
-	if privkeyFilename != "" {
-		// Save the privkey to a file.
-		f, err := os.Create(privkeyFilename)
-		if err != nil {
-			return err
-		}
-		toDelete = append(toDelete, privkeyFilename)
-		err = noise.WriteKey(f, privkey)
-		if err2 := f.Close(); err == nil {
-			err = err2
-		}
-		if err != nil {
-			return err
-		}
-	}
-
-	if pubkeyFilename != "" {
-		// Save the pubkey to a file.
-		f, err := os.Create(pubkeyFilename)
-		if err != nil {
-			return err
-		}
-		toDelete = append(toDelete, pubkeyFilename)
-		err = noise.WriteKey(f, pubkey)
-		if err2 := f.Close(); err == nil {
-			err = err2
-		}
-		if err != nil {
-			return err
-		}
-	}
-
-	// All good, allow the written files to remain.
-	toDelete = nil
-
-	if privkeyFilename != "" {
-		fmt.Printf("privkey written to %s\n", privkeyFilename)
-	} else {
-		fmt.Printf("privkey %x\n", privkey)
-	}
-	if pubkeyFilename != "" {
-		fmt.Printf("pubkey  written to %s\n", pubkeyFilename)
-	} else {
-		fmt.Printf("pubkey  %x\n", pubkey)
-	}
-
-	return nil
-}
-
-func readKeyFromFile(filename string) ([]byte, error) {
-	f, err := os.Open(filename)
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-	return noise.ReadKey(f)
-}
-
 func run(privkey, pubkey []byte, domain dns.Name, upstream net.Addr, dnsConn net.PacketConn) error {
 	defer dnsConn.Close()
 
@@ -613,7 +670,17 @@ func run(privkey, pubkey []byte, domain dns.Name, upstream net.Addr, dnsConn net
 
 	log.Printf("pubkey %x", pubkey)
 
-	return loop(dnsConn, domain, ttConn)
+	ch := make(chan *record, 100)
+	defer close(ch)
+
+	go func() {
+		err := sendLoop(dnsConn, ttConn, ch)
+		if err != nil {
+			log.Printf("sendLoop: %v", err)
+		}
+	}()
+
+	return recvLoop(domain, dnsConn, ttConn, ch)
 }
 
 func main() {

+ 52 - 26
noise/noise.go

@@ -1,3 +1,8 @@
+// Package noise provides a net.Conn-like interface for a
+// Noise_NK_25519_ChaChaPoly_BLAKE2s. It encodes Noise messages onto a reliable
+// stream using 16-bit length prefixes.
+//
+// https://noiseprotocol.org/noise.html
 package noise
 
 import (
@@ -17,6 +22,13 @@ import (
 // The length of public and private keys as returned by GenerateKeypair.
 const KeyLen = 32
 
+// cipherSuite represents 25519_ChaChaPoly_BLAKE2s.
+var cipherSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashBLAKE2s)
+
+// readMessage reads a length-prefixed message from r. It returns a nil error
+// only when a complete message was read. It returns io.EOF only when there were
+// 0 bytes remaining to read from r. It returns io.ErrUnexpectedEOF when EOF
+// occurs in the middle of an encoded message.
 func readMessage(r io.Reader) ([]byte, error) {
 	var length uint16
 	err := binary.Read(r, binary.BigEndian, &length)
@@ -33,6 +45,8 @@ func readMessage(r io.Reader) ([]byte, error) {
 	return msg, err
 }
 
+// writeMessage writes msg as a length-prefixed message to w. It panics if the
+// length of msg cannot be represented in 16 bits.
 func writeMessage(w io.Writer, msg []byte) error {
 	length := uint16(len(msg))
 	if int(length) != len(msg) {
@@ -46,20 +60,24 @@ func writeMessage(w io.Writer, msg []byte) error {
 	return err
 }
 
-type ReadWriter struct {
-	rw         io.ReadWriteCloser
+// socket is the internal type that represents a Noise-wrapped
+// io.ReadWriteCloser.
+type socket struct {
 	recvPipe   *io.PipeReader
 	sendCipher *noise.CipherState
+	io.ReadWriteCloser
 }
 
-func newReadWriter(rw io.ReadWriteCloser, recvCipher, sendCipher *noise.CipherState) *ReadWriter {
+func newSocket(rwc io.ReadWriteCloser, recvCipher, sendCipher *noise.CipherState) *socket {
 	pr, pw := io.Pipe()
+	// This loop calls readMessage, decrypts the messages, and feeds them
+	// into recvPipe where they will be returned from Read.
 	go func() (err error) {
 		defer func() {
 			pw.CloseWithError(err)
 		}()
 		for {
-			msg, err := readMessage(rw)
+			msg, err := readMessage(rwc)
 			if err != nil {
 				return err
 			}
@@ -73,25 +91,27 @@ func newReadWriter(rw io.ReadWriteCloser, recvCipher, sendCipher *noise.CipherSt
 			}
 		}
 	}()
-	return &ReadWriter{
-		rw:         rw,
-		sendCipher: sendCipher,
-		recvPipe:   pr,
+	return &socket{
+		sendCipher:      sendCipher,
+		recvPipe:        pr,
+		ReadWriteCloser: rwc,
 	}
 }
 
-func (rw *ReadWriter) Read(p []byte) (int, error) {
-	return rw.recvPipe.Read(p)
+// Read reads decrypted data from the wrapped io.Reader.
+func (s *socket) Read(p []byte) (int, error) {
+	return s.recvPipe.Read(p)
 }
 
-func (rw *ReadWriter) Write(p []byte) (int, error) {
+// Write writes encrypted data from the wrapped io.Writer.
+func (s *socket) Write(p []byte) (int, error) {
 	total := 0
 	for len(p) > 0 {
 		n := len(p)
 		if n > 4096 {
 			n = 4096
 		}
-		err := writeMessage(rw.rw, rw.sendCipher.Encrypt(nil, nil, p[:n]))
+		err := writeMessage(s.ReadWriteCloser, s.sendCipher.Encrypt(nil, nil, p[:n]))
 		if err != nil {
 			return total, err
 		}
@@ -101,12 +121,8 @@ func (rw *ReadWriter) Write(p []byte) (int, error) {
 	return total, nil
 }
 
-func (rw *ReadWriter) Close() error {
-	return rw.rw.Close()
-}
-
-var cipherSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashBLAKE2s)
-
+// newConfig instantiates configuration settings that are common to clients and
+// servers.
 func newConfig(initiator bool) noise.Config {
 	return noise.Config{
 		CipherSuite: cipherSuite,
@@ -116,7 +132,10 @@ func newConfig(initiator bool) noise.Config {
 	}
 }
 
-func NewClient(rw io.ReadWriteCloser, serverPubkey []byte) (*ReadWriter, error) {
+// NewClient wraps an io.ReadWriteCloser in a Noise protocol as a client, and
+// returns after completing the handshake. It returns a non-nil error if there
+// is an error during the handshake.
+func NewClient(rwc io.ReadWriteCloser, serverPubkey []byte) (io.ReadWriteCloser, error) {
 	config := newConfig(true)
 	config.PeerStatic = serverPubkey
 	handshakeState, err := noise.NewHandshakeState(config)
@@ -129,13 +148,13 @@ func NewClient(rw io.ReadWriteCloser, serverPubkey []byte) (*ReadWriter, error)
 	if err != nil {
 		return nil, err
 	}
-	err = writeMessage(rw, msg)
+	err = writeMessage(rwc, msg)
 	if err != nil {
 		return nil, err
 	}
 
 	// <- e, es
-	msg, err = readMessage(rw)
+	msg, err = readMessage(rwc)
 	if err != nil {
 		return nil, err
 	}
@@ -147,10 +166,13 @@ func NewClient(rw io.ReadWriteCloser, serverPubkey []byte) (*ReadWriter, error)
 		return nil, errors.New("unexpected server payload")
 	}
 
-	return newReadWriter(rw, recvCipher, sendCipher), nil
+	return newSocket(rwc, recvCipher, sendCipher), nil
 }
 
-func NewServer(rw io.ReadWriteCloser, serverPrivkey, serverPubkey []byte) (*ReadWriter, error) {
+// NewClient wraps an io.ReadWriteCloser in a Noise protocol as a server, and
+// returns after completing the handshake. It returns a non-nil error if there
+// is an error during the handshake.
+func NewServer(rwc io.ReadWriteCloser, serverPrivkey, serverPubkey []byte) (io.ReadWriteCloser, error) {
 	config := newConfig(false)
 	config.StaticKeypair = noise.DHKey{Private: serverPrivkey, Public: serverPubkey}
 	handshakeState, err := noise.NewHandshakeState(config)
@@ -159,7 +181,7 @@ func NewServer(rw io.ReadWriteCloser, serverPrivkey, serverPubkey []byte) (*Read
 	}
 
 	// -> e, es
-	msg, err := readMessage(rw)
+	msg, err := readMessage(rwc)
 	if err != nil {
 		return nil, err
 	}
@@ -176,14 +198,17 @@ func NewServer(rw io.ReadWriteCloser, serverPrivkey, serverPubkey []byte) (*Read
 	if err != nil {
 		return nil, err
 	}
-	err = writeMessage(rw, msg)
+	err = writeMessage(rwc, msg)
 	if err != nil {
 		return nil, err
 	}
 
-	return newReadWriter(rw, recvCipher, sendCipher), nil
+	return newSocket(rwc, recvCipher, sendCipher), nil
 }
 
+// GenerateKeypair generates a private key and the corresponding public key.
+//
+// https://noiseprotocol.org/noise.html#dh-functions
 func GenerateKeypair() (privkey, pubkey []byte, err error) {
 	pair, err := noise.DH25519.GenerateKeypair(rand.Reader)
 	if err != nil {
@@ -200,6 +225,7 @@ func GenerateKeypair() (privkey, pubkey []byte, err error) {
 	return pair.Private, pair.Public, nil
 }
 
+// PubkeyFromPrivkey returns the public key that corresponds to privkey.
 func PubkeyFromPrivkey(privkey []byte) []byte {
 	pair, err := noise.DH25519.GenerateKeypair(bytes.NewReader(privkey))
 	if err != nil {

+ 4 - 0
turbotunnel/consts.go

@@ -1,3 +1,7 @@
+// Package turbotunnel is facilities for embedding packet-based reliability
+// protocols inside other protocols.
+//
+// https://github.com/net4people/bbs/issues/9
 package turbotunnel
 
 import "errors"