diff --git a/traversal.go b/traversal.go index eef7df1..6a6638e 100644 --- a/traversal.go +++ b/traversal.go @@ -36,3 +36,17 @@ func RegisteredToFQDN(registered, fqdn string, callback func(domain string) bool } } } + +// SplitRegisteredToFQDN executes the provided callback routine for domain names, splitting +// the registered domain into a prefix and suffix part and omitting the part in between. +// The process stops if the callback routine returns true, indicating completion. +func SplitRegisteredToFQDN(registered, fqdn string, callback func(prefix, suffix string) bool) { + base := len(strings.Split(registered, ".")) + labels := strings.Split(fqdn, ".") + + for i := 1; i <= len(labels)-base-1; i++ { + if callback(strings.Join(labels[:i], "."), strings.Join(labels[i+1:], ".")) { + break + } + } +} diff --git a/traversal_test.go b/traversal_test.go index 43cdaff..a404bd4 100644 --- a/traversal_test.go +++ b/traversal_test.go @@ -106,3 +106,84 @@ func TestRegisteredToFQDN(t *testing.T) { }) } } + +func TestSplitRegisteredToFQDN(t *testing.T) { + type Result struct { + prefix string + suffix string + } + + var got []Result + tests := []struct { + name string + fqdn string + registered string + expected []Result + callback func(prefix, suffix string) bool + }{ + { + name: "Full traversal", + fqdn: "www.accessphysiotherapy.com.ezproxy.utica.edu", + registered: "utica.edu", + expected: []Result{ + { + "www", + "com.ezproxy.utica.edu", + }, + { + "www.accessphysiotherapy", + "ezproxy.utica.edu", + }, + { + "www.accessphysiotherapy.com", + "utica.edu", + }, + }, + callback: func(prefix, suffix string) bool { + got = append(got, Result{ + prefix, + suffix, + }) + return false + }, + }, + { + name: "Only TLD+1", + fqdn: "ezproxy.utica.edu", + registered: "utica.edu", + expected: []Result{}, + callback: func(prefix, suffix string) bool { + got = append(got, Result{ + prefix, + suffix, + }) + return true + }, + }, + + { + name: "Only subdomain", + fqdn: "utica.edu", + registered: "utica.edu", + expected: []Result{}, + callback: func(prefix, suffix string) bool { + got = append(got, Result{ + prefix, + suffix, + }) + return true + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got = []Result{} + + SplitRegisteredToFQDN(tt.registered, tt.fqdn, tt.callback) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("Unexpected Result, expected %v, got %v", tt.expected, got) + } + }) + } +} diff --git a/wildcards.go b/wildcards.go index eccfd64..095a4c2 100644 --- a/wildcards.go +++ b/wildcards.go @@ -43,11 +43,19 @@ type wildcard struct { // UnlikelyName takes a subdomain name and returns an unlikely DNS name within that subdomain. func UnlikelyName(sub string) string { + return UnlikelyNameFromSplit("", sub) +} + +// UnlikelyNameFromSplit takes a subdomain prefix and suffix and returns an unlikely DNS name in the middle of those. +func UnlikelyNameFromSplit(prefix, suffix string) string { ldh := []rune(LDHChars) ldhLen := len(ldh) // Determine the max label length - l := MaxDNSNameLen - (len(sub) + 1) + l := MaxDNSNameLen - (len(suffix) + 1) + if len(prefix) > 0 { + l = l - (len(prefix) + 1) + } if l > MaxLabelLen { l = MaxLabelLen } else if l < MinLabelLen { @@ -69,7 +77,13 @@ func UnlikelyName(sub string) string { if newlabel == "" { return newlabel } - return newlabel + "." + sub + + sub := newlabel + "." + suffix + if len(prefix) > 0 { + sub = prefix + "." + sub + } + + return sub } // WildcardDetected returns true when the provided DNS response could be a wildcard match. @@ -78,7 +92,8 @@ func (r *Resolvers) WildcardDetected(ctx context.Context, resp *dns.Msg, domain return false } - name := strings.ToLower(RemoveLastDot(resp.Question[0].Name)) + question := RemoveLastDot(resp.Question[0].Name) + name := strings.ToLower(question) domain = strings.ToLower(RemoveLastDot(domain)) if labels := strings.Split(name, "."); len(labels) > len(strings.Split(domain, ".")) { name = strings.Join(labels[1:], ".") @@ -87,12 +102,26 @@ func (r *Resolvers) WildcardDetected(ctx context.Context, resp *dns.Msg, domain var found bool // Check for a DNS wildcard at each label starting with the registered domain RegisteredToFQDN(domain, name, func(sub string) bool { - if w := r.getWildcard(ctx, sub); w.respMatchesWildcard(resp) { + if w := r.getWildcard(ctx, "", sub); w.respMatchesWildcard(resp) { found = true return true } return false }) + + if found { + return true + } + + // Check for a DNS wildcard between levels + SplitRegisteredToFQDN(domain, question, func(prefix, suffix string) bool { + if w := r.getWildcard(ctx, prefix, suffix); w.respMatchesWildcard(resp) { + found = true + return true + } + return false + }) + return found } @@ -143,18 +172,18 @@ func (r *Resolvers) goodDetector() bool { return success } -func (r *Resolvers) getWildcard(ctx context.Context, sub string) *wildcard { +func (r *Resolvers) getWildcard(ctx context.Context, prefix, sub string) *wildcard { r.Lock() - w, found := r.wildcards[sub] + w, found := r.wildcards[prefix+".."+sub] if !found { w = &wildcard{} - r.wildcards[sub] = w + r.wildcards[prefix+".."+sub] = w } r.Unlock() if !found { w.Lock() - w.Detected, w.Answers = r.wildcardTest(ctx, sub) + w.Detected, w.Answers = r.wildcardTest(ctx, prefix, sub) w.Unlock() } return w @@ -182,7 +211,7 @@ func (w *wildcard) respMatchesWildcard(resp *dns.Msg) bool { } // Determines if the provided subdomain has a DNS wildcard. -func (r *Resolvers) wildcardTest(ctx context.Context, sub string) (bool, []*ExtractedAnswer) { +func (r *Resolvers) wildcardTest(ctx context.Context, prefix, suffix string) (bool, []*ExtractedAnswer) { var detected bool var answers []*ExtractedAnswer @@ -192,7 +221,7 @@ func (r *Resolvers) wildcardTest(ctx context.Context, sub string) (bool, []*Extr for i := 0; i < numOfWildcardTests; i++ { var name string for { - name = UnlikelyName(sub) + name = UnlikelyNameFromSplit(prefix, suffix) if name != "" { break } @@ -228,7 +257,7 @@ func (r *Resolvers) wildcardTest(ctx context.Context, sub string) (bool, []*Extr } } if detected { - r.log.Printf("DNS wildcard detected: Resolver %s: %s", r.detector.address, "*."+sub) + r.log.Printf("DNS wildcard detected: Resolver %s: %s", r.detector.address, "*."+suffix) } return detected, final } diff --git a/wildcards_test.go b/wildcards_test.go index 2eab5d0..131a8df 100644 --- a/wildcards_test.go +++ b/wildcards_test.go @@ -13,6 +13,17 @@ import ( "github.com/miekg/dns" ) +func TestUnlikelyNameFromSplit(t *testing.T) { + name := UnlikelyNameFromSplit("proxy", "example.com") + if !strings.HasPrefix(name, "proxy.") { + t.Fatalf("Unlikely name `%s` does not have `%s` prefix", name, "proxy.") + } + if !strings.HasSuffix(name, ".example.com") { + t.Fatalf("Unlikely name `%s` does not have `%s` suffix", name, ".example.com") + } + t.Log(name) +} + func TestSetDetectionResolver(t *testing.T) { r := NewResolvers() defer r.Stop() @@ -58,6 +69,16 @@ func TestWildcardDetected(t *testing.T) { input: "ns.wildcard.domain.com", want: false, }, + { + label: "invalid name within a middle-wildcard", + input: "wildcard.jeff_foley.domain.com", + want: true, + }, + { + label: "valid name within a middle-wildcard", + input: "wildcard.app.domain.com", + want: false, + }, } for _, c := range cases { @@ -83,6 +104,10 @@ func wildcardHandler(w dns.ResponseWriter, req *dns.Msg) { addr = "192.168.1.2" } else if strings.HasSuffix(name, ".wildcard.domain.com.") { addr = "192.168.1.64" + } else if name == "wildcard.app.domain.com." { + addr = "192.168.1.65" + } else if strings.HasPrefix(name, "wildcard.") && strings.HasSuffix(name, ".domain.com.") { + addr = "192.168.1.66" } if addr == "" {