Commit Graph
72 Commits
Author SHA1 Message Date
David Fifield 2eb03bb746 Escape DNS names that appear in logs.
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
2021-08-12 13:20:41 -06:00
David Fifield e4dc2883ef Use errors.Is to compare against ErrClosedPipe.
I was still getting "io: read/write on closed pipe" errors in the logs,
even after comparing errors against io.ErrClosedPipe to skip logging
them. It turns out that kcp-go wraps many of its errors in another type.
The actual type of the errors was *errors.withStack, where errors is
https://github.com/pkg/errors. We can use the go1.13 errors interface
(https://blog.golang.org/go1.13-errors) to get at the value inside.
2021-08-03 21:00:45 -06:00
David Fifield 1f73f6f5b6 Ignore ErrClosedPipe in "copy stream←upstream" as well.
Saw this happen on the server during the 2021-08-02 performance tests.
Doing on the client, too, for uniformity.
2021-08-03 20:58:20 -06:00
David Fifield de15c5a512 Performance tuning: MaxStreamBuffer, SetWindowSize, QueueSize.
This enlarges a few buffers and windows, with the goal of improving
download performance. kcp's SetWindowSize controls the number of
unacknowledged packets that are allowed. smux's MaxStreamBuffer is
another kind of "receive window" that is advertised to the peer of how
much we are willing to receive at once. The default MaxStreamBuffer is
64 KB, but kcptun overrides the default to 2 MB. turbotunnel's QueueSize
is the size of internal buffers in QueuePacketConn and RemoteMap;
empirically I found that the server would sometimes fill its outgoing
buffer if SetWindowSize and QueueSize were equal, so I set QueueSize to
be twice SetWindowSize.

https://lists.torproject.org/pipermail/anti-censorship-team/2021-July/000178.html
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/merge_requests/48

The changes have a large effect on a direct -udp connection without a
recursive resolver—which, however, is a discouraged configuration.
Through a recursive resolver, the improvements are more modest. If I
really crank up the buffer sizes, I can get surprisingly fast downloads
over a direct -udp connection (over 1 MB/s), but a connection through a
resolver doesn't keep getting faster and may even get slower. I want to
avoid a bufferbloat situation with oversized buffers, too. I manually
explored a small neighborhood of parameter values and picked some
settings that looked reasonable.

The tables below show the test results. The test is downloading 10 MiB
between two servers with 100 ms RTT between them. Server:
	dnstt-server -udp :53 -privkey-file server.key t.example.com 127.0.0.1:9321
	ncat -l -k -v 9321 --send-only --sh-exec 'dd bs=1M count=10 if=/dev/urandom'
Client:
	dnstt-client -pubkey-file server.pub t.example.com 127.0.0.1:7000
	ncat --recv-only 127.0.0.1 7000 | pv -t -r -a -b -i 0.2 > /dev/null
I did the download under every treatment twice and recorded the download
rate in KiB/s. "Server drops" comes from hacking some log messages to
turbotunnel.QueuePacketConn to track how often the "Drop the incoming
packet" (QueueIncoming method) and "Drop the outgoing packet" (WriteTo)
cases happen.

resolver	method	QueueSize	MaxStreamBuffer	SetWindowSize	KiB/s	KiB/s
--------	------	---------	---------------	-------------	-----	-----
direct		udp	64		64*1024		(32, 32)	 169	 173	(status before this commit)
dns.google	udp	64		64*1024		(32, 32)	  63.8	  64.3	(status before this commit)
dns.google	doh	64		64*1024		(32, 32)	 125	 122	(status before this commit)

resolver	method	QueueSize	MaxStreamBuffer	SetWindowSize	KiB/s	KiB/s
--------	------	---------	---------------	-------------	-----	-----
direct		udp	64		1*1024*1024	(32, 32)	 172	 174
dns.google	udp	64		1*1024*1024	(32, 32)	  57.3	  58.4	server drops
dns.google	doh	64		1*1024*1024	(32, 32)	 128	 128

resolver	method	QueueSize	MaxStreamBuffer	SetWindowSize	KiB/s	KiB/s
--------	------	---------	---------------	-------------	-----	-----
direct		udp	64		1*1024*1024	(64, 64)	 322	 305
dns.google	udp	64		1*1024*1024	(64, 64)	  72.5	  70.9	server drops
dns.google	doh	64		1*1024*1024	(64, 64)	 136	 139	server drops

resolver	method	QueueSize	MaxStreamBuffer	SetWindowSize	KiB/s	KiB/s
--------	------	---------	---------------	-------------	-----	-----
direct		udp	128		1*1024*1024	(64, 64)	 321	 325	(this commit)
dns.google	udp	128		1*1024*1024	(64, 64)	  82.5	  78.5	(this commit)
dns.google	doh	128		1*1024*1024	(64, 64)	 129	 131	(this commit)

resolver	method	QueueSize	MaxStreamBuffer	SetWindowSize	KiB/s	KiB/s
--------	------	---------	---------------	-------------	-----	-----
direct		udp	2048		4*1024*1024	(1024, 1024)	1240	1060	server drops
dns.google	udp	2048		4*1024*1024	(1024, 1024)	  73.5	 81.4
dns.google	doh	2048		4*1024*1024	(1024, 1024)	 115	 129
2021-08-02 15:25:19 -06:00
David Fifield 12c59bf6f5 Don't report io.ErrClosedPipe from Session.AcceptStream. 2021-08-02 01:17:39 -06:00
David Fifield 6cfd91839f Don't consider timer and nextReq until stash and outgoing are empty.
When the timer is expired, we want to continue packing as long as
additional packets are available with zero waiting. I'm not sure about
deferring the nextReq break, but we expect packet packing to be a quick
operation.
2021-08-01 23:22:30 -06:00
David Fifield 0e7ea57efb Simplify break from the packing loop in sendLoop. 2021-08-01 22:59:45 -06:00
David Fifield c7613b89e1 Reduce smux idle timeout from 10 minutes to 2 minutes. 2021-08-01 22:32:57 -06:00
David Fifield 706c66544e Change noise.GenerateKeypair to noise.GeneratePrivkey. 2021-08-01 22:31:15 -06:00
David Fifield 6b3e1a32ae Make noise.NewServer take only the private key. 2021-08-01 22:30:49 -06:00
David Fifield 23759e203f Apply a timeout to upstream dials in the server.
In my testing locally, these dials would time out after about 30 seconds
anyway:
	2021/04/20 23:26:46 begin session 54cafb53
	2021/04/20 23:26:47 begin stream 54cafb53:3
	2021/04/20 23:27:19 stream 54cafb53:3 handleStream: stream 54cafb53:3 connect upstream: dial tcp X.X.X.X:YYYY: connect: connection timed out
	2021/04/20 23:27:19 end stream 54cafb53:3
Which is in line with the documentation for net.Dialer:
	https://golang.org/pkg/net/#Dialer
	With or without a timeout, the operating system may impose its
	own earlier timeout. For instance, TCP timeouts are often around
	3 minutes.
But may as well be explicit.

This commit has the side effect of changing the error message from
"connection timed out" to "i/o timeout".
	2021/04/20 23:28:08 begin session 05b0a46e
	2021/04/20 23:28:09 begin stream 05b0a46e:3
	2021/04/20 23:28:39 stream 05b0a46e:3 handleStream: stream 05b0a46e:3 connect upstream: dial tcp X.X.X.X:YYYY: i/o timeout
	2021/04/20 23:28:39 end stream 05b0a46e:3
2021-04-20 17:32:15 -06:00
David Fifield 6e5ba30abf Use net.Dial, rather than net.DialTCP, to dial upstream.
The usual use case for upstream is that it is a localhost IP address and
port, but it may also be a hostname and port. net.DialTCP resolves the
hostname once and for all, and only uses one of the hostname's IP
addresses if there are more than one. net.Dial will try all the IP
addresses in turn until it is able to establish a connection.

Now upstream is kept as a string variable all the way through the call
chain. For the sake of usability, we try resolving the address with
net.ResolveTCPAddr in main, to emit an error or warning right away,
rather than deferring it to the first stream.
2021-04-20 17:32:15 -06:00
David Fifield 064c53e3d1 Be uniform about not ending log calls with "\n". 2021-04-20 15:14:10 -06:00
David Fifield b6b803986c Close smux session in acceptStreams of server.
This was a memory leak.

Compare to the `sess.Close()` in the client:
https://repo.or.cz/dnstt.git/blob/2fe067548848f7dd1acb527a20699d7d2358d150:/dnstt-client/main.go#l174

This is issue UCB-02-002 from the 2021 security audit of Turbo Tunnel by
Cure53.
2021-04-20 13:51:38 -06:00
David Fifield a6602a871b Log "too few or too many" questions.
This log line would formerly be emitted for a query with 0 questions:
	FORMERR: too many questions (0)

The Nmap DNSStatusRequest probe is a query with 0 questions.
2021-03-19 23:01:37 -06:00
David Fifield 459bf7fff8 Use an external port in dnstt-server example. 2020-08-30 19:51:44 -06:00
David Fifield 58c01e740f requestor → requester
RFC 1035 uses "requester" and RFC 6891 uses "requestor". I think I
prefer "requester", and aspell agrees.
2020-08-30 19:50:23 -06:00
David Fifield 15c272edc4 Note to self about multiple sendLoop. 2020-04-29 23:29:39 -06:00
David Fifield 8f965fe37b Fix sending of leftover packets.
There was a logic error in the code. The nextP variable was used to
store the packet that was too big to pack into the most recent DNS
response. But nextP was not tied to any particular ClientID; instead it
would be sent to whatever client happened to be the recipient of the
next response.

The confusion didn't cause connections to fail completely; any
misdirected packets were treated as out-of-sequence garbage by KCP and
dropped. But it hurt performance a lot: I saw a download go from 300
KB/s to 50 KB/s just by connecting a second client with a different
ClientID (not even sending or receiving with the second client). The
reason is that a fraction of the packets intended for the downloading
client were instead sent to the idle client, which to the downloading
client looks like a packet drop, requiring a retransmission by the
server.

We fix it by placing the leftover packet in a per-ClientID stash, rather
than a variable shared by all ClientIDs.
2020-04-29 23:29:39 -06:00
David Fifield 8526369e65 Give next-response/timer-expired priority over packing downstream.
I don't know what I was thinking in
f1ee951fd6. The way it was written, if
there were not immediately additional packets to pack into the
downstream, it would stop trying to pack and would instead wait until
the maxResponseDelay or another response to send. What I meant is that
the timer and the next-response channel should have priority, if either
of those is true *and* there is additional downstream available to pack.
Only when both of those are false should we try to pack downstream data.
2020-04-29 20:56:33 -06:00
David Fifield 2371fb4558 Attempt to extract packets only if we got a ClientID. 2020-04-29 12:57:06 -06:00
David Fifield 24d7fd82b2 Log "too short for ClientID" on when it's a non-error response.
The server would log "NXDOMAIN: 0 bytes are too short to contain a
ClientID" even in the common cases where it got an A or NS query from
the resolver (possibly from QNAME minimization).
2020-04-29 12:56:41 -06:00
David Fifield 05444dcb22 smux Stream.Write may also return EOF. 2020-04-25 21:27:14 -06:00
David Fifield 241225df1d Add -mtu option to server. 2020-04-25 20:54:09 -06:00
David Fifield f7e028a697 Log when truncating a response.
We don't expect this to happen often. It probably indicates an error.
2020-04-25 20:28:37 -06:00
David Fifield e328c57b21 Log pubkey before MTU. 2020-04-25 20:28:37 -06:00
David Fifield a00ef8f9ea Compute maxEncodedPayload automatically from maxUDPPayload.
Previously I had maxEncodedPayload hard-coded as a separate constant,
but it is completely dependent on maxUDPPayload. We compute it by
actually constructing wire-format packets and taking their length, which
should be less fragile than the formula I previously had commented,
though it requires some synchronization between the sendLoop and
computeMaxEncodedPayload functions.
2020-04-25 20:28:37 -06:00
David Fifield e5efc55f23 Extract the ClientID outside of responseFor. 2020-04-25 19:51:15 -06:00
David Fifield 9ee6bf8abf Use wg.Add(2) instead of 2 × wg.Add(1). 2020-04-23 15:51:53 -06:00
David Fifield 9f430df8aa Make the privkey file only readable by the user. 2020-04-19 17:31:16 -06:00
David Fifield a650238f1e Don't log QTYPE != TXT errors. 2020-04-19 17:16:27 -06:00
David Fifield d14deab12b Documentation and light refactoring. 2020-04-19 17:16:27 -06:00
David Fifield a6af2f1df1 Make -udp required, resolve in main. 2020-04-19 16:20:50 -06:00
David Fifield f168777a13 dns Message.Opcode method. 2020-04-19 10:33:40 -06:00
David Fifield 4b0b144257 Consolidate the authoritative domain check.
Formerly this was split into two pieces: one that did the domain check
and set the AA bit, and another that checked the AA bit and returned an
error if it was not set. It was done in two parts because the check for
UDP payload size occurred in the middle. Since 59f03791 the payload
check is moved to the end, so we can do the authoritative domain check
in one piece.
2020-04-19 10:28:56 -06:00
David Fifield 0567fa9abb Remove addr fro "cannot parse DNS query" log message. 2020-04-19 09:44:05 -06:00
David Fifield e9a98c3aef Avoid logging EOF and ErrClosedPipe errors.
smux Stream.WriteTo may return io.EOF, which breaks the contract of
io.Copy that says it should not return io.EOF. smux.Stream doesn't have
a unidirectional shutdown, so we always end up slamming it shut in both
directions and leave the other direction with a broken pipe.
2020-04-19 02:13:48 -06:00
David Fifield 34b7e82af4 More logging of query validation errors in server. 2020-04-19 01:17:33 -06:00
David Fifield 98bf401738 Use NXDOMAIN rather than FORMERR for QTYPE != TXT. 2020-04-19 01:13:35 -06:00
David Fifield 59f03791df Do the payload size check after the TXT and base32 checks.
That way we can respond with NXDOMAIN rather than FORMERR to more of the
random queries that arrive.
2020-04-19 00:59:04 -06:00
David Fifield 04e1295e52 Infer a payload size of 512 even when there is no OPT RR.
Before we were inferring a size of 0. It would still be rejected for
being too small, but was confusing with the log messages I am adding.
2020-04-19 00:53:34 -06:00
David Fifield 294f1acc7d Fix second AA check. 2020-04-19 00:53:34 -06:00
David Fifield 1e57ed8d42 Add missing return for EDNS version != 0. 2020-04-19 00:53:34 -06:00
David Fifield 7fad23ceca Reduce maxUDPPayload to 1232 for compatibility with Quad9. 2020-04-19 00:53:34 -06:00
David Fifield 0a630f91c5 Log stream begin/end in server, log conv in client. 2020-04-18 19:05:23 -06:00
David Fifield b1bf9164a7 -privkey-file and -pubkey-file options. 2020-04-18 17:56:45 -06:00
David Fifield 505db3ec23 Server -privkey option and client -pubkey option. 2020-04-18 17:35:32 -06:00
David Fifield cd03021417 Note -udp is required on server. 2020-04-18 16:02:14 -06:00
David Fifield b98eb2c75e Move dummyAddr to turbotunnel.DummyAddr. 2020-04-18 16:02:14 -06:00
David Fifield f69e4e0d71 Truncation and TC bit in responses. 2020-04-18 16:02:14 -06:00