From 3543bbc0b0004de85a7698c1d5834ccf2916e89a Mon Sep 17 00:00:00 2001 From: Cottand Date: Mon, 12 Feb 2024 21:41:59 +0000 Subject: [PATCH 1/5] implement wildcard DNS --- serve_mux.go | 31 ++++++++++++++++++++++- serve_mux_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/serve_mux.go b/serve_mux.go index e7f36e221..c59f8a286 100644 --- a/serve_mux.go +++ b/serve_mux.go @@ -4,6 +4,8 @@ import ( "sync" ) +var wildcard []byte = []byte{'*', '.'} + // ServeMux is an DNS request multiplexer. It matches the zone name of // each incoming request against a list of registered patterns add calls // the handler for the pattern that most closely matches the zone name. @@ -28,6 +30,15 @@ func NewServeMux() *ServeMux { // DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = NewServeMux() +func replaceWithAsteriskLabel(qname string) (wildcard string) { + i, shot := NextLabel(qname, 0) + if shot { + return "" + } + + return "*." + qname[i:] +} + func (mux *ServeMux) match(q string, t uint16) Handler { mux.m.RLock() defer mux.m.RUnlock() @@ -37,8 +48,22 @@ func (mux *ServeMux) match(q string, t uint16) Handler { q = CanonicalName(q) + var matchedWildcard Handler + var handler Handler for off, end := 0, false; !end; off, end = NextLabel(q, off) { + // Exact match was not found in the previous label - try matching a wildcard + withWildcard := replaceWithAsteriskLabel(q) + if h, ok := mux.z[withWildcard]; ok { + if t != TypeDS { + matchedWildcard = h + //return h + //continue + } else { + // Continue for DS to see if we have a parent too, if so delegate to the parent + handler = h + } + } if h, ok := mux.z[q[off:]]; ok { if t != TypeDS { return h @@ -46,9 +71,13 @@ func (mux *ServeMux) match(q string, t uint16) Handler { // Continue for DS to see if we have a parent too, if so delegate to the parent handler = h } + // We did not find an exact match, but if we matched the wildcard we can use that + if matchedWildcard != nil { + return matchedWildcard + } } - // Wildcard match, if we have found nothing try the root zone as a last resort. + // If we have found nothing try the root zone as a last resort. if h, ok := mux.z["."]; ok { return h } diff --git a/serve_mux_test.go b/serve_mux_test.go index 3d990ce52..457ca3b17 100644 --- a/serve_mux_test.go +++ b/serve_mux_test.go @@ -28,6 +28,70 @@ func TestDotAsCatchAllWildcard(t *testing.T) { } } +type mockHandler string + +func (mockHandler) ServeDNS(w ResponseWriter, r *Msg) { + panic("implement me") +} + +func TestWildcardMatch(t *testing.T) { + mux := NewServeMux() + mux.Handle("example.com.", mockHandler("example.com")) + mux.Handle("*.example.com.", mockHandler("*.example.com")) + mux.Handle("a.example.com.", mockHandler("a.example.com")) + + handler := mux.match("www.example.com.", TypeTXT) + if handler == nil { + t.Error("example.com match failed") + } + if string(handler.(mockHandler)) != "*.example.com" { + t.Error("www.example.com did not match *.example.com wildcard") + } + + handler = mux.match("a.example.com.", TypeTXT) + if handler == nil { + t.Error("a.example.com match failed") + } + if string(handler.(mockHandler)) != "a.example.com" { + t.Error("a.example.com did not match subdomain a") + } + + handler = mux.match("example.com", TypeTXT) + if handler == nil { + t.Error("example.com match failed") + } + if string(handler.(mockHandler)) != "example.com" { + t.Error("example.com did not match example.com, but with", handler) + } + + handler = mux.match("foo.bar.example.com", TypeTXT) + // see https://datatracker.ietf.org/doc/html/rfc4592#section-2.2.1 + // a wildcard does not match names below its zone + if handler != nil && string(handler.(mockHandler)) == "*.example.com" { + t.Error("foo.bar.example.com matched unexpectedly with non terminal") + } +} + +func TestTwoWildcardMatch(t *testing.T) { + mux := NewServeMux() + mux.Handle(".", mockHandler("root")) + mux.Handle("example.com.", mockHandler("example")) + mux.Handle("*.*.example.com.", mockHandler("2wildcard")) + + handler := mux.match("foo.bar.example.com.", TypeTXT) + if handler == nil { + t.Error("example.com match failed") + } + if string(handler.(mockHandler)) != "wildcard" { + t.Error("foo.bar.example.com did not match *.*.example.com wildcard") + } + + handler = mux.match("www.example.com.", TypeTXT) + if handler != nil { + t.Error("www.example.com matched *.*.example.com") + } +} + func TestCaseFolding(t *testing.T) { mux := NewServeMux() mux.Handle("_udp.example.com.", HandlerFunc(HelloServer)) From 4753ce46a657ccd5c9271a8a78552fe4ab0e703a Mon Sep 17 00:00:00 2001 From: Cottand Date: Mon, 12 Feb 2024 22:17:52 +0000 Subject: [PATCH 2/5] disregard double wildcard tests --- serve_mux.go | 29 ++++++++++------------------- serve_mux_test.go | 20 -------------------- 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/serve_mux.go b/serve_mux.go index c59f8a286..006c9a4e5 100644 --- a/serve_mux.go +++ b/serve_mux.go @@ -4,8 +4,6 @@ import ( "sync" ) -var wildcard []byte = []byte{'*', '.'} - // ServeMux is an DNS request multiplexer. It matches the zone name of // each incoming request against a list of registered patterns add calls // the handler for the pattern that most closely matches the zone name. @@ -48,22 +46,8 @@ func (mux *ServeMux) match(q string, t uint16) Handler { q = CanonicalName(q) - var matchedWildcard Handler - var handler Handler for off, end := 0, false; !end; off, end = NextLabel(q, off) { - // Exact match was not found in the previous label - try matching a wildcard - withWildcard := replaceWithAsteriskLabel(q) - if h, ok := mux.z[withWildcard]; ok { - if t != TypeDS { - matchedWildcard = h - //return h - //continue - } else { - // Continue for DS to see if we have a parent too, if so delegate to the parent - handler = h - } - } if h, ok := mux.z[q[off:]]; ok { if t != TypeDS { return h @@ -71,9 +55,16 @@ func (mux *ServeMux) match(q string, t uint16) Handler { // Continue for DS to see if we have a parent too, if so delegate to the parent handler = h } - // We did not find an exact match, but if we matched the wildcard we can use that - if matchedWildcard != nil { - return matchedWildcard + + // Exact match was not found - try matching a wildcard + withWildcard := replaceWithAsteriskLabel(q) + if h, ok := mux.z[withWildcard]; ok { + if t != TypeDS { + return h + } else { + // Continue for DS to see if we have a parent too, if so delegate to the parent + handler = h + } } } diff --git a/serve_mux_test.go b/serve_mux_test.go index 457ca3b17..bfa6065dd 100644 --- a/serve_mux_test.go +++ b/serve_mux_test.go @@ -72,26 +72,6 @@ func TestWildcardMatch(t *testing.T) { } } -func TestTwoWildcardMatch(t *testing.T) { - mux := NewServeMux() - mux.Handle(".", mockHandler("root")) - mux.Handle("example.com.", mockHandler("example")) - mux.Handle("*.*.example.com.", mockHandler("2wildcard")) - - handler := mux.match("foo.bar.example.com.", TypeTXT) - if handler == nil { - t.Error("example.com match failed") - } - if string(handler.(mockHandler)) != "wildcard" { - t.Error("foo.bar.example.com did not match *.*.example.com wildcard") - } - - handler = mux.match("www.example.com.", TypeTXT) - if handler != nil { - t.Error("www.example.com matched *.*.example.com") - } -} - func TestCaseFolding(t *testing.T) { mux := NewServeMux() mux.Handle("_udp.example.com.", HandlerFunc(HelloServer)) From 4be4a236880e5003ce866f354cd08b5b59f2b9e4 Mon Sep 17 00:00:00 2001 From: Cottand Date: Sat, 17 Feb 2024 18:32:00 +0000 Subject: [PATCH 3/5] support nested wildcards --- .idea/.gitignore | 8 ++++++++ .idea/dns.iml | 9 +++++++++ .idea/modules.xml | 8 ++++++++ .idea/vcs.xml | 6 ++++++ serve_mux.go | 36 +++++++++++++++++++++++------------- serve_mux_test.go | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/dns.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dns.iml b/.idea/dns.iml new file mode 100644 index 000000000..5e764c4f0 --- /dev/null +++ b/.idea/dns.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..b8975db75 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/serve_mux.go b/serve_mux.go index 006c9a4e5..17837fd1f 100644 --- a/serve_mux.go +++ b/serve_mux.go @@ -28,13 +28,23 @@ func NewServeMux() *ServeMux { // DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = NewServeMux() -func replaceWithAsteriskLabel(qname string) (wildcard string) { - i, shot := NextLabel(qname, 0) - if shot { - return "" +func (mux *ServeMux) matchWildcard(q string) Handler { + + wildcards := "*." + // replace the labels of q with wildcard labels until we get a match + for off, end := 0, false; !end; off, end = NextLabel(q, off) { + // skip to removing the first label + if off == 0 { + continue + } + if h, ok := mux.z[wildcards+q[off:]]; ok { + return h + } + wildcards += "*." } - return "*." + qname[i:] + // we found nothing + return nil } func (mux *ServeMux) match(q string, t uint16) Handler { @@ -55,14 +65,14 @@ func (mux *ServeMux) match(q string, t uint16) Handler { // Continue for DS to see if we have a parent too, if so delegate to the parent handler = h } - - // Exact match was not found - try matching a wildcard - withWildcard := replaceWithAsteriskLabel(q) - if h, ok := mux.z[withWildcard]; ok { - if t != TypeDS { - return h - } else { - // Continue for DS to see if we have a parent too, if so delegate to the parent + + // we did not find a match - try wildcards if this is the first iteration only, + // as otherwise we will be attempting the same match on every iteration + if off == 0 { + if h := mux.matchWildcard(q); h != nil { + if t != TypeDS { + return h + } handler = h } } diff --git a/serve_mux_test.go b/serve_mux_test.go index bfa6065dd..28f9ed119 100644 --- a/serve_mux_test.go +++ b/serve_mux_test.go @@ -72,6 +72,41 @@ func TestWildcardMatch(t *testing.T) { } } +func TestTwoWildcardMatch(t *testing.T) { + mux := NewServeMux() + mux.Handle(".", mockHandler("root")) + mux.Handle("example.com.", mockHandler("example")) + mux.Handle("*.*.example.com.", mockHandler("2wildcard")) + + handler := mux.match("foo.bar.example.com.", TypeTXT) + if handler == nil { + t.Error("foo.bar.example.com match failed") + } + if string(handler.(mockHandler)) != "2wildcard" { + t.Error("foo.bar.example.com did not match *.*.example.com wildcard") + } + + handler = mux.match("www.example.com.", TypeTXT) + if handler != nil && string(handler.(mockHandler)) != "example" { + t.Error("www.example.com unexpectedly matched", string(handler.(mockHandler))) + } +} + +func TestWildcardMustNotMatchEntireZone(t *testing.T) { + mux := NewServeMux() + mux.Handle(".", mockHandler("root")) + mux.Handle("*.example.com.", mockHandler("2wildcard")) + + handler := mux.match("foo.bar.example.com.", TypeTXT) + if handler == nil { + t.Error("match failed") + } + + if handler != nil && string(handler.(mockHandler)) != "root" { + t.Error("foo.bar.example.com unexpectedly matched", string(handler.(mockHandler))) + } +} + func TestCaseFolding(t *testing.T) { mux := NewServeMux() mux.Handle("_udp.example.com.", HandlerFunc(HelloServer)) From f1a5fc0bf72f6f9b167382fd67b0a287e0e304b4 Mon Sep 17 00:00:00 2001 From: Cottand Date: Sat, 17 Feb 2024 18:34:15 +0000 Subject: [PATCH 4/5] add comment about empty non-terminals --- serve_mux_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/serve_mux_test.go b/serve_mux_test.go index 28f9ed119..0eb660940 100644 --- a/serve_mux_test.go +++ b/serve_mux_test.go @@ -86,6 +86,8 @@ func TestTwoWildcardMatch(t *testing.T) { t.Error("foo.bar.example.com did not match *.*.example.com wildcard") } + // this tests wildcards as empty non-terminals + // (`*.example.com` is empty in this example) handler = mux.match("www.example.com.", TypeTXT) if handler != nil && string(handler.(mockHandler)) != "example" { t.Error("www.example.com unexpectedly matched", string(handler.(mockHandler))) From d6c70c737a5a6462fdfdf520a71f8887419e2bbe Mon Sep 17 00:00:00 2001 From: Cottand Date: Sat, 17 Feb 2024 18:34:53 +0000 Subject: [PATCH 5/5] remove .idea files --- .idea/.gitignore | 8 -------- .idea/dns.iml | 9 --------- .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ 4 files changed, 31 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/dns.iml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b81b..000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/dns.iml b/.idea/dns.iml deleted file mode 100644 index 5e764c4f0..000000000 --- a/.idea/dns.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index b8975db75..000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddfb..000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file