Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dns: short read lock #619

Closed
MikeSchroll opened this issue Jan 8, 2018 · 19 comments
Closed

dns: short read lock #619

MikeSchroll opened this issue Jan 8, 2018 · 19 comments

Comments

@MikeSchroll
Copy link

This morning, having done a new release yesterday with the latest miekg/dns, we began receiving traffic (even traffic we don't consider authorized, and drop) to a dev instance which caused our server code to lock up, and not process any additional incoming queries.

This is the error message:
level=error msg=“unable to listen” address=“REDACTED:53" error=“dns: short read” log_message=“unable to listen” protocol=udp region=FRA serverID=71302

Initially looking into it, we suspected the commit ac8cd78 tied to issue:
#613

We've noticed that this appears to produce errors and lock only on each IP/port we're listening on; and so for instance one server which listens on 20+ IPs/ports had 5 locked from this error. The remaining continued to function.
We've seen this only on UDP, but that may be a factor of it's where most of our traffic comes.

We've initially confirmed this suspicion by rolling back miekg/dns versions, and have not had the error recur. We do not have a way to replicate the error, but we'd like to open this new issue with hopes that others will have some thoughts on the change which is causing this.

@tmthrgd
Copy link
Collaborator

tmthrgd commented Jan 9, 2018

@MikeSchroll ac8cd78 almost certainly exposed the error you're seeing, but that error has always existed, it's simply that before ac8cd78 it was ignored.

The error is coming from

dns/server.go

Line 636 in dcdbddd

return nil, nil, ErrShortRead
which is caused by

dns/udp.go

Line 24 in dcdbddd

func ReadFromSessionUDP(conn *net.UDPConn, b []byte) (int, *SessionUDP, error) {
returning n == 0 && err == nil.

I'm not familiar enough to say what the correct behaviour here is, or whether this error is safe to ignore, but a simple (untested) patch to address this issue is:

From 58d0c781273a9fe2e10303c91eb5b4b5e31071fc Mon Sep 17 00:00:00 2001
From: Tom Thorogood <me+github@tomthorogood.co.uk>
Date: Tue, 9 Jan 2018 12:29:43 +1030
Subject: [PATCH] Ignore ErrShortRead (*Server).serveUDP

Fixes #619
---
 server.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/server.go b/server.go
index 59135b0f53..171b66ed8b 100644
--- a/server.go
+++ b/server.go
@@ -512,6 +512,9 @@ func (srv *Server) serveUDP(l *net.UDPConn) error {
 			if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
 				continue
 			}
+			if err == ErrShortRead {
+				continue
+			}
 			return err
 		}
 		go srv.serve(s.RemoteAddr(), handler, m, l, s, nil)
-- 
2.14.3

@miekg
Copy link
Owner

miekg commented Jan 9, 2018

I'm not familiar enough to say what the correct behaviour here is, or whether this error is safe to ignore, but a simple (untested) patch to address this issue is:

This is I think the main issue here, as I'm also not 100% what is correct; of course #613 is somewhat more theoretical than stuff not working in prod, i.e. rollback? OTOH we should stop on some errors.

Add a bunch of other error conditions sounds a bit like whack a mole. So I'm included to rollback ac8cd78 (and do a similar thing the tcp handler) and try again.

The main error we want to break on is: socket is closed (i.e. the server one)

@twitchyliquid64
Copy link
Contributor

twitchyliquid64 commented Jan 9, 2018

Agree rollback is safest till we work out the path forward.

If doing !opErr.Temporary() is unsafe (for reasons above) and we dont want to risk further unexpected cases, we may want to fallback to the ugly (but effective) version I had originally:

if err != nil {
	if strings.Contains(err.Error(), "use of closed network connection") {
		return err
	}
	continue
}

Other thoughts:

  1. It is valid for a UDP datagram to have a zero length (though ofc not valid for DNS). As such, ErrShortRead represents a temporary failure, and IMHO should implement net.OpError to return .Temporary() == true, and re-instate the error handling we rolled back.
  2. @MikeSchroll does your server code retry after it recieves an error from ActivateAndServe() or ListenAndServe() ?
  3. As previously mentioned, the referenced change exposes the error you are experiencing but it has always been there. I'm also quite curious to know why your dev instance is getting 0-length datagrams! :D

@miekg
Copy link
Owner

miekg commented Jan 9, 2018 via email

@twitchyliquid64
Copy link
Contributor

Re 2: that was never part of the implicit API, i.e. the error returned would be on startup, not while running.

If the implicit intention of ActivateAndServe() or ListenAndServe() is to only return an error if startup failed, then where is the error returned if listening or request servicing fails? Seems to make the most sense (and mirror the standard library) for these methods to return errors on both startup and run errors.

Depending on the application and environment, listener sockets failing/closing is not uncommon. Consider one of my use cases for this library: A container engine runs a DNS server on a bridge interface IP (specific to each container) for the life of the container, but when the interfaces are brought down, linux cascades the failures to the sockets bound to the interfaces' IPs, which manifests as a use of closed connection when the listener socket is used. The only place it makes sense to bubble this error is to return from ActivateAndServe() with the error.

Recommend you change the definition of an error returned from ActivateAndServe() To mean The Server struct and it's underlying listener are no longer usable.

@tmthrgd
Copy link
Collaborator

tmthrgd commented Jan 9, 2018

Following the code from ReadFromSessionUDP down, we end up at the recvmsg syscall. According to the man page:

The return value will be 0 when the peer has performed an orderly shutdown.

Apparently this only applies to TCP sockets though. In this case it would seem that the zero return value literally means a zero-byte datagram was received.

I would say that the code in #617 was actually correct and that readUDP was wrong to return ErrShortRead in the first place. If readUDP stopped returning ErrShortRead, then serve will "send a FormatError back" instead of being skipped. This seems like the correct behaviour to me.

I'll open a pull request with my suggested fix for both #619 (this issue) and #613.

@twitchyliquid64
Copy link
Contributor

twitchyliquid64 commented Jan 9, 2018

If readUDP stopped returning ErrShortRead, then serve will "send a FormatError back" instead of being skipped. This seems like the correct behaviour to me.

I'm not sure if I've read this correctly, but are you suggesting we send back a reply to a an empty UDP packet?

If this is the case, I strongly disagree. That will make all such servers planetwide vulnerable to being abused as part of a DNS amplification attack (for DDOS), albeit with a low multiplier. This would be a risky code change.

@tmthrgd
Copy link
Collaborator

tmthrgd commented Jan 9, 2018

@twitchyliquid64 That's exactly what I'm suggesting. While I do agree it's a potential for amplification, it's exactly one-byte worse than the existing code (i.e. it's not any worse). If you send a 1-byte invalid datagram, you'll already receive the exact same "FormatError" response.

@tmthrgd
Copy link
Collaborator

tmthrgd commented Jan 9, 2018

@twitchyliquid64 Basically the current code is wrong, n == 0 does not indicate an error. If you think returning a "FormatError" response to small datagrams is a problem, that is a separate matter. #621 simply makes the code correct.

@twitchyliquid64
Copy link
Contributor

twitchyliquid64 commented Jan 9, 2018 via email

@tmthrgd
Copy link
Collaborator

tmthrgd commented Jan 9, 2018

@twitchyliquid64 (FYI with a really quick reading of the code, I believe the "FormatError" response should be exactly 12-bytes. I don't know whether that changes your assessment at all).

@twitchyliquid64
Copy link
Contributor

twitchyliquid64 commented Jan 9, 2018 via email

@miekg
Copy link
Owner

miekg commented Jan 9, 2018

An udp datagram smaller than 12 bytes, is not a correct dns package. The header is a dns packet is 12 bytes and then you still need a question section (domain, type, class), which (I think) the shortest can be 2 +2 +2, is 6 bytes.

Not sure what other nameserver do in this case

@miekg
Copy link
Owner

miekg commented Jan 9, 2018

CoreDNS, NSD and BIND return FormErr for the case where you only send the header:

% ./q @deb.atoom.net mx miek.nl
;; opcode: QUERY, status: FORMERR, id: 61964
;; flags: qr; QUERY: 0, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0

;; query time: 256 µs, server: deb.atoom.net.:53(udp), size: 12 bytes
% ./q @open.nlnetlabs.nl mx miek.nl 
;; opcode: QUERY, status: FORMERR, id: 55915
;; flags: qr rd; QUERY: 0, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0

;; query time: 18524 µs, server: open.nlnetlabs.nl.:53(udp), size: 12 bytes

% ./q @open.nlnetlabs.nl mx miek.nl
;; opcode: QUERY, status: FORMERR, id: 2426
;; flags: qr rd; QUERY: 0, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0

% ./q @ns1.dns.nl mx miek.nl 
;; opcode: QUERY, status: FORMERR, id: 30081
;; flags: qr rd; QUERY: 0, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0

;; query time: 11456 µs, server: ns1.dns.nl.:53(udp), size: 12 bytes

@miekg
Copy link
Owner

miekg commented Jan 9, 2018

Seems smaller write (<12) are not responded to. We should probably do the same.

@tmthrgd
Copy link
Collaborator

tmthrgd commented Jan 9, 2018

@miekg I have a four-line patch that will do exactly that. I'll open a pull-request, should I base it on master or #621?

@miekg
Copy link
Owner

miekg commented Jan 9, 2018

if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
is still the right thing todo? I.e. it was the ShortRead crap that killed the server? If yes, I'll just merge #621

It would also be good to have some exposure to a prob env for these changes. (I can recompile CoreDNS locally).

What about TCP?

@miekg miekg closed this as completed in #621 Jan 9, 2018
miekg pushed a commit that referenced this issue Jan 9, 2018
* Do not reutrn ErrShortRead in readUDP

A read of zero bytes indicates a peer shutdown for TCP sockets -- and
thus returning ErrShortRead is fine in readTCP -- but not for UDP
sockets. For UDP sockets a read of zero bytes literally indicates a
zero-byte datagram, and is a valid return value not indicating an error.

Removing this case will cause readUDP to correctly return a zero-byte
message.

* Return non-temporary error from serveUDP loop

Fixes #613
@tmthrgd
Copy link
Collaborator

tmthrgd commented Jan 9, 2018

@miekg #619 (this issue) is definitely caused by the misplaced ErrShortRead. (That code has existed since you refactored reading in 5eca59c). I haven't looked at the TCP code in detail, but I'm pretty sure it's correct (at least I'm not aware of any other/similar issues with it?).

Merging #621 removes the ErrShortRead so should conclusively solve this issue and #613 again.

It would definitely be good to have some sort of test case covering this and #613.

@twitchyliquid64
Copy link
Contributor

To mirror what I said on the PR for completeness:

Because of the 3 way handshake and sequence numbers, its not possible to setup a TCP session with a spoofed source IP. You don't need to consider TCP a problem for DNS amplification.

A empty UDP packet is 28 bytes. If an error response is 12 bytes, that means the amplification ratio is 1.42.

Therefore, we should avoid responding to packets that small, as the PR does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants