mirror of
https://github.com/tladesignz/dnstt.git
synced 2026-09-27 09:28:03 +03:00
The dnstt-server log line "NXDOMAIN: not authoritative for %s" copied bytes directly from an attacker-controlled DNS name to the log. Because DNS labels may contain any byte values, this made possible various injection attacks, for example: * A label containing a newline byte could break the format of a log file, or be used to inject false log lines. * If the log output were going to a terminal (as it does by default), a DNS name could affect the terminal by including escape sequences. * A DNS label containing the dot character (\x2e) could give a misleading impression of the contents of a query; for example the names ["a" "example" "com"] and ["a\x2eexample" "com"] would both be logged as "a.example.com". The former ambiguity with the dot character might have confused the name compressor in messageBuilder.WriteName, but I do not think any of dnstt's uses of messageBuilder could have been affected. The Name.String method now does backslash hex escaping of unusual bytes in labels. This vulnerability was called to mind by "Injection Attacks Reloaded: Tunnelling Malicious Payloads over DNS" by Jeitner and Shulman. See particularly Section 3.2 for \x2e injection. https://www.usenix.org/conference/usenixsecurity21/presentation/jeitner
948 lines
29 KiB
Go
948 lines
29 KiB
Go
// 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 :53 -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.
|
|
//
|
|
// The -mtu option controls the maximum size of response UDP payloads.
|
|
// Queries that do not advertise requester support for responses of at least
|
|
// this size at least this size will be responded to with a FORMERR. The default
|
|
// value is maxUDPPayload.
|
|
//
|
|
// 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 (
|
|
"bytes"
|
|
"encoding/base32"
|
|
"encoding/binary"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/xtaci/kcp-go/v5"
|
|
"github.com/xtaci/smux"
|
|
"www.bamsoftware.com/git/dnstt.git/dns"
|
|
"www.bamsoftware.com/git/dnstt.git/noise"
|
|
"www.bamsoftware.com/git/dnstt.git/turbotunnel"
|
|
)
|
|
|
|
const (
|
|
// smux streams will be closed after this much time without receiving data.
|
|
idleTimeout = 2 * time.Minute
|
|
|
|
// How to set the TTL field in Answer resource records.
|
|
responseTTL = 60
|
|
|
|
// How long we may wait for downstream data before sending an empty
|
|
// response. If another query comes in while we are waiting, we'll send
|
|
// an empty response anyway and restart the delay timer for the next
|
|
// response.
|
|
//
|
|
// This number should be less than 2 seconds, which in 2019 was reported
|
|
// to be the query timeout of the Quad9 DoH server.
|
|
// https://dnsencryption.info/imc19-doe.html Section 4.2, Finding 2.4
|
|
maxResponseDelay = 1 * time.Second
|
|
|
|
// How long to wait for a TCP connection to upstream to be established.
|
|
upstreamDialTimeout = 30 * time.Second
|
|
)
|
|
|
|
var (
|
|
// We don't send UDP payloads larger than this, in an attempt to avoid
|
|
// network-layer fragmentation. 1280 is the minimum IPv6 MTU, 40 bytes
|
|
// is the size of an IPv6 header (though without any extension headers),
|
|
// and 8 bytes is the size of a UDP header.
|
|
//
|
|
// Control this value with the -mtu command-line option.
|
|
//
|
|
// https://dnsflagday.net/2020/#message-size-considerations
|
|
// "An EDNS buffer size of 1232 bytes will avoid fragmentation on nearly
|
|
// all current networks."
|
|
//
|
|
// On 2020-04-19, the Quad9 resolver was seen to have a UDP payload size
|
|
// of 1232. Cloudflare's was 1452, and Google's was 4096.
|
|
maxUDPPayload = 1280 - 40 - 8
|
|
)
|
|
|
|
// base32Encoding is a base32 encoding without padding.
|
|
var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding)
|
|
|
|
// 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. The private key is saved with mode 0400 and the public key is
|
|
// saved with 0666 (before umask). 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, err := noise.GeneratePrivkey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pubkey := noise.PubkeyFromPrivkey(privkey)
|
|
|
|
if privkeyFilename != "" {
|
|
// Save the privkey to a file.
|
|
f, err := os.OpenFile(privkeyFilename, os.O_RDWR|os.O_CREATE, 0400)
|
|
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 string, conv uint32) error {
|
|
dialer := net.Dialer{
|
|
Timeout: upstreamDialTimeout,
|
|
}
|
|
upstreamConn, err := dialer.Dial("tcp", upstream)
|
|
if err != nil {
|
|
return fmt.Errorf("stream %08x:%d connect upstream: %v", conv, stream.ID(), err)
|
|
}
|
|
defer upstreamConn.Close()
|
|
upstreamTCPConn := upstreamConn.(*net.TCPConn)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
_, err := io.Copy(stream, upstreamTCPConn)
|
|
if err == io.EOF {
|
|
// smux Stream.Write may return io.EOF.
|
|
err = nil
|
|
}
|
|
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
|
log.Printf("stream %08x:%d copy stream←upstream: %v", conv, stream.ID(), err)
|
|
}
|
|
upstreamTCPConn.CloseRead()
|
|
stream.Close()
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
_, err := io.Copy(upstreamTCPConn, stream)
|
|
if err == io.EOF {
|
|
// smux Stream.WriteTo may return io.EOF.
|
|
err = nil
|
|
}
|
|
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
|
log.Printf("stream %08x:%d copy upstream←stream: %v", conv, stream.ID(), err)
|
|
}
|
|
upstreamTCPConn.CloseWrite()
|
|
}()
|
|
wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 []byte, upstream string) error {
|
|
// Put a Noise channel on top of the KCP conn.
|
|
rw, err := noise.NewServer(conn, privkey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Put an smux session on top of the encrypted Noise channel.
|
|
smuxConfig := smux.DefaultConfig()
|
|
smuxConfig.Version = 2
|
|
smuxConfig.KeepAliveTimeout = idleTimeout
|
|
smuxConfig.MaxStreamBuffer = 1 * 1024 * 1024 // default is 65536
|
|
sess, err := smux.Server(rw, smuxConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer sess.Close()
|
|
|
|
for {
|
|
stream, err := sess.AcceptStream()
|
|
if err != nil {
|
|
if err, ok := err.(net.Error); ok && err.Temporary() {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
log.Printf("begin stream %08x:%d", conn.GetConv(), stream.ID())
|
|
go func() {
|
|
defer func() {
|
|
log.Printf("end stream %08x:%d", conn.GetConv(), stream.ID())
|
|
stream.Close()
|
|
}()
|
|
err := handleStream(stream, upstream, conn.GetConv())
|
|
if err != nil {
|
|
log.Printf("stream %08x:%d handleStream: %v", conn.GetConv(), stream.ID(), err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// acceptSessions listens for incoming KCP connections and passes them to
|
|
// acceptStreams.
|
|
func acceptSessions(ln *kcp.Listener, privkey []byte, mtu int, upstream string) error {
|
|
for {
|
|
conn, err := ln.AcceptKCP()
|
|
if err != nil {
|
|
if err, ok := err.(net.Error); ok && err.Temporary() {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
log.Printf("begin session %08x", conn.GetConv())
|
|
// Permit coalescing the payloads of consecutive sends.
|
|
conn.SetStreamMode(true)
|
|
// Disable the dynamic congestion window (limit only by the
|
|
// maximum of local and remote static windows).
|
|
conn.SetNoDelay(
|
|
0, // default nodelay
|
|
0, // default interval
|
|
0, // default resend
|
|
1, // nc=1 => congestion window off
|
|
)
|
|
conn.SetWindowSize(turbotunnel.QueueSize/2, turbotunnel.QueueSize/2)
|
|
if rc := conn.SetMtu(mtu); !rc {
|
|
panic(rc)
|
|
}
|
|
go func() {
|
|
defer func() {
|
|
log.Printf("end session %08x", conn.GetConv())
|
|
conn.Close()
|
|
}()
|
|
err := acceptStreams(conn, privkey, upstream)
|
|
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
|
log.Printf("session %08x acceptStreams: %v", conn.GetConv(), err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
return err
|
|
}
|
|
|
|
for {
|
|
prefix, err := r.ReadByte()
|
|
if err != nil {
|
|
// We may return a real io.EOF only here.
|
|
return nil, err
|
|
}
|
|
if prefix >= 224 {
|
|
paddingLen := prefix - 224
|
|
_, err := io.CopyN(ioutil.Discard, r, int64(paddingLen))
|
|
if err != nil {
|
|
return nil, eof(err)
|
|
}
|
|
} else {
|
|
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 query's 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, []byte) {
|
|
resp := &dns.Message{
|
|
ID: query.ID,
|
|
Flags: 0x8000, // QR = 1, RCODE = no error
|
|
Question: query.Question,
|
|
}
|
|
|
|
if query.Flags&0x8000 != 0 {
|
|
// QR != 0, this is not a query. Don't even send a response.
|
|
return nil, nil
|
|
}
|
|
|
|
// Check for EDNS(0) support. Include our own OPT RR only if we receive
|
|
// one from the requester.
|
|
// https://tools.ietf.org/html/rfc6891#section-6.1.1
|
|
// "Lack of presence of an OPT record in a request MUST be taken as an
|
|
// indication that the requester does not implement any part of this
|
|
// specification and that the responder MUST NOT include an OPT record
|
|
// in its response."
|
|
payloadSize := 0
|
|
for _, rr := range query.Additional {
|
|
if rr.Type != dns.RRTypeOPT {
|
|
continue
|
|
}
|
|
if len(resp.Additional) != 0 {
|
|
// https://tools.ietf.org/html/rfc6891#section-6.1.1
|
|
// "If a query message with more than one OPT RR is
|
|
// received, a FORMERR (RCODE=1) MUST be returned."
|
|
resp.Flags |= dns.RcodeFormatError
|
|
log.Printf("FORMERR: more than one OPT RR")
|
|
return resp, nil
|
|
}
|
|
resp.Additional = append(resp.Additional, dns.RR{
|
|
Name: dns.Name{},
|
|
Type: dns.RRTypeOPT,
|
|
Class: 4096, // responder's UDP payload size
|
|
TTL: 0,
|
|
Data: []byte{},
|
|
})
|
|
additional := &resp.Additional[0]
|
|
|
|
version := (rr.TTL >> 16) & 0xff
|
|
if version != 0 {
|
|
// https://tools.ietf.org/html/rfc6891#section-6.1.1
|
|
// "If a responder does not implement the VERSION level
|
|
// of the request, then it MUST respond with
|
|
// RCODE=BADVERS."
|
|
resp.Flags |= dns.ExtendedRcodeBadVers & 0xf
|
|
additional.TTL = (dns.ExtendedRcodeBadVers >> 4) << 24
|
|
log.Printf("BADVERS: EDNS version %d != 0", version)
|
|
return resp, nil
|
|
}
|
|
|
|
payloadSize = int(rr.Class)
|
|
}
|
|
if payloadSize < 512 {
|
|
// https://tools.ietf.org/html/rfc6891#section-6.1.1 "Values
|
|
// lower than 512 MUST be treated as equal to 512."
|
|
payloadSize = 512
|
|
}
|
|
// We will return RcodeFormatError if payloadSize is too small, but
|
|
// first, check the name in order to set the AA bit properly.
|
|
|
|
// There must be exactly one question.
|
|
if len(query.Question) != 1 {
|
|
resp.Flags |= dns.RcodeFormatError
|
|
log.Printf("FORMERR: too few or too many questions (%d)", len(query.Question))
|
|
return resp, nil
|
|
}
|
|
question := query.Question[0]
|
|
// Check the name to see if it ends in our chosen domain, and extract
|
|
// all that comes before the domain if it does. If it does not, we will
|
|
// return RcodeNameError below, but prefer to return RcodeFormatError
|
|
// for payload size if that applies as well.
|
|
prefix, ok := question.Name.TrimSuffix(domain)
|
|
if !ok {
|
|
// Not a name we are authoritative for.
|
|
resp.Flags |= dns.RcodeNameError
|
|
log.Printf("NXDOMAIN: not authoritative for %s", question.Name)
|
|
return resp, nil
|
|
}
|
|
resp.Flags |= 0x0400 // AA = 1
|
|
|
|
if query.Opcode() != 0 {
|
|
// We don't support OPCODE != QUERY.
|
|
resp.Flags |= dns.RcodeNotImplemented
|
|
log.Printf("NOTIMPL: unrecognized OPCODE %d", query.Opcode())
|
|
return resp, nil
|
|
}
|
|
|
|
if question.Type != dns.RRTypeTXT {
|
|
// We only support QTYPE == TXT.
|
|
resp.Flags |= dns.RcodeNameError
|
|
// No log message here; it's common for recursive resolvers to
|
|
// send NS or A queries when the client only asked for a TXT. I
|
|
// suspect this is related to QNAME minimization, but I'm not
|
|
// sure. https://tools.ietf.org/html/rfc7816
|
|
// log.Printf("NXDOMAIN: QTYPE %d != TXT", question.Type)
|
|
return resp, nil
|
|
}
|
|
|
|
encoded := bytes.ToUpper(bytes.Join(prefix, nil))
|
|
payload := make([]byte, base32Encoding.DecodedLen(len(encoded)))
|
|
n, err := base32Encoding.Decode(payload, encoded)
|
|
if err != nil {
|
|
// Base32 error, make like the name doesn't exist.
|
|
resp.Flags |= dns.RcodeNameError
|
|
log.Printf("NXDOMAIN: base32 decoding: %v", err)
|
|
return resp, nil
|
|
}
|
|
payload = payload[:n]
|
|
|
|
// We require clients to support EDNS(0) with a minimum payload size;
|
|
// otherwise we would have to set a small KCP MTU (only around 200
|
|
// bytes). https://tools.ietf.org/html/rfc6891#section-7 "If there is a
|
|
// problem with processing the OPT record itself, such as an option
|
|
// value that is badly formatted or that includes out-of-range values, a
|
|
// FORMERR MUST be returned."
|
|
if payloadSize < maxUDPPayload {
|
|
resp.Flags |= dns.RcodeFormatError
|
|
log.Printf("FORMERR: requester payload size %d is too small (minimum %d)", payloadSize, maxUDPPayload)
|
|
return resp, nil
|
|
}
|
|
|
|
return resp, payload
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
n, addr, err := dnsConn.ReadFrom(buf[:])
|
|
if err != nil {
|
|
if err, ok := err.(net.Error); ok && err.Temporary() {
|
|
log.Printf("ReadFrom temporary error: %v", err)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Got a UDP packet. Try to parse it as a DNS message.
|
|
query, err := dns.MessageFromWireFormat(buf[:n])
|
|
if err != nil {
|
|
log.Printf("cannot parse DNS query: %v", err)
|
|
continue
|
|
}
|
|
|
|
resp, payload := responseFor(&query, domain)
|
|
// Extract the ClientID from the payload.
|
|
var clientID turbotunnel.ClientID
|
|
n = copy(clientID[:], payload)
|
|
payload = payload[n:]
|
|
if n == len(clientID) {
|
|
// Discard padding and pull out the packets contained in
|
|
// the payload.
|
|
r := bytes.NewReader(payload)
|
|
for {
|
|
p, err := nextPacket(r)
|
|
if err != nil {
|
|
break
|
|
}
|
|
// Feed the incoming packet to KCP.
|
|
ttConn.QueueIncoming(p, clientID)
|
|
}
|
|
} else {
|
|
// Payload is not long enough to contain a ClientID.
|
|
if resp != nil && resp.Rcode() == dns.RcodeNoError {
|
|
resp.Flags |= dns.RcodeNameError
|
|
log.Printf("NXDOMAIN: %d bytes are too short to contain a ClientID", n)
|
|
}
|
|
}
|
|
// If a response is called for, pass it to sendLoop via the channel.
|
|
if resp != nil {
|
|
select {
|
|
case ch <- &record{resp, addr, clientID}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 while keeping the total size under maxEncodedPayload, then sends it.
|
|
func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <-chan *record, maxEncodedPayload int) error {
|
|
var nextRec *record
|
|
for {
|
|
rec := nextRec
|
|
nextRec = nil
|
|
|
|
if rec == nil {
|
|
var ok bool
|
|
rec, ok = <-ch
|
|
if !ok {
|
|
break
|
|
}
|
|
}
|
|
|
|
if rec.Resp.Rcode() == dns.RcodeNoError && len(rec.Resp.Question) == 1 {
|
|
// If it's a non-error response, we can fill the Answer
|
|
// section with downstream packets.
|
|
|
|
// Any changes to how responses are built need to happen
|
|
// also in computeMaxEncodedPayload.
|
|
rec.Resp.Answer = []dns.RR{
|
|
{
|
|
Name: rec.Resp.Question[0].Name,
|
|
Type: rec.Resp.Question[0].Type,
|
|
Class: rec.Resp.Question[0].Class,
|
|
TTL: responseTTL,
|
|
Data: nil, // will be filled in below
|
|
},
|
|
}
|
|
|
|
var payload bytes.Buffer
|
|
limit := maxEncodedPayload
|
|
// We loop and bundle as many packets from OutgoingQueue
|
|
// into the response as will fit. Any packet that would
|
|
// overflow the capacity of the DNS response, we stash
|
|
// to be bundled into a future response.
|
|
timer := time.NewTimer(maxResponseDelay)
|
|
for {
|
|
var p []byte
|
|
unstash := ttConn.Unstash(rec.ClientID)
|
|
outgoing := ttConn.OutgoingQueue(rec.ClientID)
|
|
// Prioritize taking a packet first from the
|
|
// stash, then from the outgoing queue, then
|
|
// finally check for the expiration of the timer
|
|
// or for a receive on ch (indicating a new
|
|
// query that we must respond to).
|
|
select {
|
|
case p = <-unstash:
|
|
default:
|
|
select {
|
|
case p = <-unstash:
|
|
case p = <-outgoing:
|
|
default:
|
|
select {
|
|
case p = <-unstash:
|
|
case p = <-outgoing:
|
|
case <-timer.C:
|
|
case nextRec = <-ch:
|
|
}
|
|
}
|
|
}
|
|
// We wait for the first packet in a bundle
|
|
// only. The second and later packets must be
|
|
// immediately available or they will be omitted
|
|
// from this bundle.
|
|
timer.Reset(0)
|
|
|
|
if len(p) == 0 {
|
|
// timer expired or receive on ch, we
|
|
// are done with this response.
|
|
break
|
|
}
|
|
|
|
limit -= 2 + len(p)
|
|
if payload.Len() == 0 {
|
|
// No packet length check for the first
|
|
// packet; if it's too large, we allow
|
|
// it to be truncated and dropped by the
|
|
// receiver.
|
|
} else if limit < 0 {
|
|
// Stash this packet to send in the next
|
|
// response.
|
|
ttConn.Stash(p, rec.ClientID)
|
|
break
|
|
}
|
|
if int(uint16(len(p))) != len(p) {
|
|
panic(len(p))
|
|
}
|
|
binary.Write(&payload, binary.BigEndian, uint16(len(p)))
|
|
payload.Write(p)
|
|
}
|
|
timer.Stop()
|
|
|
|
rec.Resp.Answer[0].Data = dns.EncodeRDataTXT(payload.Bytes())
|
|
}
|
|
|
|
buf, err := rec.Resp.WireFormat()
|
|
if err != nil {
|
|
log.Printf("resp WireFormat: %v", err)
|
|
continue
|
|
}
|
|
// Truncate if necessary.
|
|
// https://tools.ietf.org/html/rfc1035#section-4.1.1
|
|
if len(buf) > maxUDPPayload {
|
|
log.Printf("truncating response of %d bytes to max of %d", len(buf), maxUDPPayload)
|
|
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() {
|
|
log.Printf("WriteTo temporary error: %v", err)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// computeMaxEncodedPayload computes the maximum amount of downstream TXT RR
|
|
// data that keep the overall response size less than maxUDPPayload, in the
|
|
// worst case when the response answers a query that has a maximum-length name
|
|
// in its Question section. Returns 0 in the case that no amount of data makes
|
|
// the overall response size small enough.
|
|
//
|
|
// This function needs to be kept in sync with sendLoop with regard to how it
|
|
// builds candidate responses.
|
|
func computeMaxEncodedPayload(limit int) int {
|
|
// 64+64+64+62 octets, needs to be base32-decodable.
|
|
maxLengthName, err := dns.NewName([][]byte{
|
|
[]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
|
[]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
|
[]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
|
[]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
{
|
|
// Compute the encoded length of maxLengthName and that its
|
|
// length is actually at the maximum of 255 octets.
|
|
n := 0
|
|
for _, label := range maxLengthName {
|
|
n += len(label) + 1
|
|
}
|
|
n += 1 // For the terminating null label.
|
|
if n != 255 {
|
|
panic(fmt.Sprintf("max-length name is %d octets, should be %d %s", n, 255, maxLengthName))
|
|
}
|
|
}
|
|
|
|
queryLimit := uint16(limit)
|
|
if int(queryLimit) != limit {
|
|
queryLimit = 0xffff
|
|
}
|
|
query := &dns.Message{
|
|
Question: []dns.Question{
|
|
{
|
|
Name: maxLengthName,
|
|
Type: dns.RRTypeTXT,
|
|
Class: dns.RRTypeTXT,
|
|
},
|
|
},
|
|
// EDNS(0)
|
|
Additional: []dns.RR{
|
|
{
|
|
Name: dns.Name{},
|
|
Type: dns.RRTypeOPT,
|
|
Class: queryLimit, // requester's UDP payload size
|
|
TTL: 0, // extended RCODE and flags
|
|
Data: []byte{},
|
|
},
|
|
},
|
|
}
|
|
resp, _ := responseFor(query, dns.Name([][]byte{}))
|
|
// As in sendLoop.
|
|
resp.Answer = []dns.RR{
|
|
{
|
|
Name: query.Question[0].Name,
|
|
Type: query.Question[0].Type,
|
|
Class: query.Question[0].Class,
|
|
TTL: responseTTL,
|
|
Data: nil, // will be filled in below
|
|
},
|
|
}
|
|
|
|
// Binary search to find the maximum payload length that does not result
|
|
// in a wire-format message whose length exceeds the limit.
|
|
low := 0
|
|
high := 32768
|
|
for low+1 < high {
|
|
mid := (low + high) / 2
|
|
resp.Answer[0].Data = dns.EncodeRDataTXT(make([]byte, mid))
|
|
buf, err := resp.WireFormat()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(buf) <= limit {
|
|
low = mid
|
|
} else {
|
|
high = mid
|
|
}
|
|
}
|
|
|
|
return low
|
|
}
|
|
|
|
func run(privkey []byte, domain dns.Name, upstream string, dnsConn net.PacketConn) error {
|
|
defer dnsConn.Close()
|
|
|
|
log.Printf("pubkey %x", noise.PubkeyFromPrivkey(privkey))
|
|
|
|
// We have a variable amount of room in which to encode downstream
|
|
// packets in each response, because each response must contain the
|
|
// query's Question section, which is of variable length. But we cannot
|
|
// give dynamic packet size limits to KCP; the best we can do is set a
|
|
// global maximum which no packet will exceed. We choose that maximum to
|
|
// keep the UDP payload size under maxUDPPayload, even in the worst case
|
|
// of a maximum-length name in the query's Question section.
|
|
maxEncodedPayload := computeMaxEncodedPayload(maxUDPPayload)
|
|
// 2 bytes accounts for a packet length prefix.
|
|
mtu := maxEncodedPayload - 2
|
|
if mtu < 80 {
|
|
if mtu < 0 {
|
|
mtu = 0
|
|
}
|
|
return fmt.Errorf("maximum UDP payload size of %d leaves only %d bytes for payload", maxUDPPayload, mtu)
|
|
}
|
|
log.Printf("effective MTU %d", mtu)
|
|
|
|
// Start up the virtual PacketConn for turbotunnel.
|
|
ttConn := turbotunnel.NewQueuePacketConn(turbotunnel.DummyAddr{}, idleTimeout*2)
|
|
ln, err := kcp.ServeConn(nil, 0, 0, ttConn)
|
|
if err != nil {
|
|
return fmt.Errorf("opening KCP listener: %v", err)
|
|
}
|
|
defer ln.Close()
|
|
go func() {
|
|
err := acceptSessions(ln, privkey, mtu, upstream)
|
|
if err != nil {
|
|
log.Printf("acceptSessions: %v", err)
|
|
}
|
|
}()
|
|
|
|
ch := make(chan *record, 100)
|
|
defer close(ch)
|
|
|
|
// We could run multiple copies of sendLoop; that would allow more time
|
|
// for each response to collect downstream data before being evicted by
|
|
// another response that needs to be sent.
|
|
go func() {
|
|
err := sendLoop(dnsConn, ttConn, ch, maxEncodedPayload)
|
|
if err != nil {
|
|
log.Printf("sendLoop: %v", err)
|
|
}
|
|
}()
|
|
|
|
return recvLoop(domain, dnsConn, ttConn, ch)
|
|
}
|
|
|
|
func main() {
|
|
var genKey bool
|
|
var privkeyFilename string
|
|
var privkeyString string
|
|
var pubkeyFilename string
|
|
var udpAddr string
|
|
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(flag.CommandLine.Output(), `Usage:
|
|
%[1]s -gen-key -privkey-file PRIVKEYFILE -pubkey-file PUBKEYFILE
|
|
%[1]s -udp ADDR -privkey-file PRIVKEYFILE DOMAIN UPSTREAMADDR
|
|
|
|
Example:
|
|
%[1]s -gen-key -privkey-file server.key -pubkey-file server.pub
|
|
%[1]s -udp :53 -privkey-file server.key t.example.com 127.0.0.1:8000
|
|
|
|
`, os.Args[0])
|
|
flag.PrintDefaults()
|
|
}
|
|
flag.BoolVar(&genKey, "gen-key", false, "generate a server keypair; print to stdout or save to files")
|
|
flag.IntVar(&maxUDPPayload, "mtu", maxUDPPayload, "maximum size of DNS responses")
|
|
flag.StringVar(&privkeyString, "privkey", "", fmt.Sprintf("server private key (%d hex digits)", noise.KeyLen*2))
|
|
flag.StringVar(&privkeyFilename, "privkey-file", "", "read server private key from file (with -gen-key, write to file)")
|
|
flag.StringVar(&pubkeyFilename, "pubkey-file", "", "with -gen-key, write server public key to file")
|
|
flag.StringVar(&udpAddr, "udp", "", "UDP address to listen on (required)")
|
|
flag.Parse()
|
|
|
|
log.SetFlags(log.LstdFlags | log.LUTC)
|
|
|
|
if genKey {
|
|
// -gen-key mode.
|
|
if flag.NArg() != 0 || privkeyString != "" || udpAddr != "" {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
if err := generateKeypair(privkeyFilename, pubkeyFilename); err != nil {
|
|
fmt.Fprintf(os.Stderr, "cannot generate keypair: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
// Ordinary server mode.
|
|
if flag.NArg() != 2 {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
domain, err := dns.ParseName(flag.Arg(0))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "invalid domain %+q: %v\n", flag.Arg(0), err)
|
|
os.Exit(1)
|
|
}
|
|
upstream := flag.Arg(1)
|
|
// We keep upstream as a string in order to eventually pass it
|
|
// to net.Dial in handleStream. But for the sake of displaying
|
|
// an error or warning at startup, rather than only when the
|
|
// first stream occurs, we apply some parsing and name
|
|
// resolution checks here.
|
|
{
|
|
upstreamHost, _, err := net.SplitHostPort(upstream)
|
|
if err != nil {
|
|
// host:port format is required in all cases, so
|
|
// this is a fatal error.
|
|
fmt.Fprintf(os.Stderr, "cannot parse upstream address %+q: %v\n", upstream, err)
|
|
os.Exit(1)
|
|
}
|
|
upstreamIPAddr, err := net.ResolveIPAddr("ip", upstreamHost)
|
|
if err != nil {
|
|
// Failure to resolve the host portion is only a
|
|
// warning. The name will be re-resolved on each
|
|
// net.Dial in handleStream.
|
|
log.Printf("warning: cannot resolve upstream host %+q: %v", upstreamHost, err)
|
|
} else if upstreamIPAddr.IP == nil {
|
|
// Handle the special case of an empty string
|
|
// for the host portion, which resolves to a nil
|
|
// IP. This is a fatal error as we will not be
|
|
// able to dial this address.
|
|
fmt.Fprintf(os.Stderr, "cannot parse upstream address %+q: missing host in address\n", upstream)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
if udpAddr == "" {
|
|
fmt.Fprintf(os.Stderr, "the -udp option is required\n")
|
|
os.Exit(1)
|
|
}
|
|
dnsConn, err := net.ListenPacket("udp", udpAddr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "opening UDP listener: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if pubkeyFilename != "" {
|
|
fmt.Fprintf(os.Stderr, "-pubkey-file may only be used with -gen-key\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
var privkey []byte
|
|
if privkeyFilename != "" && privkeyString != "" {
|
|
fmt.Fprintf(os.Stderr, "only one of -privkey and -privkey-file may be used\n")
|
|
os.Exit(1)
|
|
} else if privkeyFilename != "" {
|
|
var err error
|
|
privkey, err = readKeyFromFile(privkeyFilename)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "cannot read privkey from file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
} else if privkeyString != "" {
|
|
var err error
|
|
privkey, err = noise.DecodeKey(privkeyString)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "privkey format error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
if len(privkey) == 0 {
|
|
log.Println("generating a temporary one-time keypair")
|
|
log.Println("use the -privkey or -privkey-file option for a persistent server keypair")
|
|
var err error
|
|
privkey, err = noise.GeneratePrivkey()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
err = run(privkey, domain, upstream, dnsConn)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|