From d14deab12bdbff93281c933f32f85b2ead682e2f Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sun, 19 Apr 2020 11:06:04 -0600 Subject: [PATCH] Documentation and light refactoring. --- README | 2 +- dns/dns.go | 86 ++++++++---- dnstt-client/dns.go | 121 ++++++++++++++--- dnstt-client/dns_test.go | 53 ++++++-- dnstt-client/http.go | 96 ++++++++----- dnstt-client/main.go | 91 ++++++++----- dnstt-client/main_test.go | 27 ++++ dnstt-client/tls.go | 31 ++++- dnstt-server/main.go | 279 +++++++++++++++++++++++--------------- noise/noise.go | 78 +++++++---- turbotunnel/consts.go | 4 + 11 files changed, 606 insertions(+), 262 deletions(-) create mode 100644 dnstt-client/main_test.go diff --git a/README b/README index 42eb38b..f73640d 100644 --- a/README +++ b/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 ``` diff --git a/dns/dns.go b/dns/dns.go index bdf1003..0aa5f55 100644 --- a/dns/dns.go +++ b/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 // 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 diff --git a/dnstt-client/dns.go b/dnstt-client/dns.go index d280da4..8e45e0e 100644 --- a/dnstt-client/dns.go +++ b/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) diff --git a/dnstt-client/dns_test.go b/dnstt-client/dns_test.go index 071fc50..e09ccc2 100644 --- a/dnstt-client/dns_test.go +++ b/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) } } } diff --git a/dnstt-client/http.go b/dnstt-client/http.go index bce34fb..974abe5 100644 --- a/dnstt-client/http.go +++ b/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) { diff --git a/dnstt-client/main.go b/dnstt-client/main.go index 18e0736..13776a0 100644 --- a/dnstt-client/main.go +++ b/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() diff --git a/dnstt-client/main_test.go b/dnstt-client/main_test.go new file mode 100644 index 0000000..071fc50 --- /dev/null +++ b/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) + } + } +} diff --git a/dnstt-client/tls.go b/dnstt-client/tls.go index f5406e0..cf4d775 100644 --- a/dnstt-client/tls.go +++ b/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{}) -} diff --git a/dnstt-server/main.go b/dnstt-server/main.go index 6b89ffe..4970795 100644 --- a/dnstt-server/main.go +++ b/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() { diff --git a/noise/noise.go b/noise/noise.go index eb1dc41..a0af4cf 100644 --- a/noise/noise.go +++ b/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 { diff --git a/turbotunnel/consts.go b/turbotunnel/consts.go index f8138e5..db54589 100644 --- a/turbotunnel/consts.go +++ b/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"