holic/redirect.name

Wildcard fallback?

Opened this issue · 7 comments

@holic fantastic tool, thanks. The power:simplicity here is wonderful.

How do you feel about a PR to handle catch-all wildcards before the fallback function?

e.g.

             IN  ALIAS  alias.redirect.name ; domain apex using a non-standard record
_redirect    IN  TXT    "Redirects permanently to https://example.com/*"

*            IN  CNAME  alias.redirect.name
_redirect.*  IN  TXT    "Redirects permanently to https://example.com/*"

My use-case is extremely lazily redirecting miss-spelled domain names. This functionality would allow for a standard set of records to be added for each without care to past or future sub-domain changes.

Currently this would be handled by defining a _redirect TXT record for every possible subdomain.

(Also, if it's relevant, I'm self hosting via the published docker image - swift and stateless is great)

Interestingly, this is non-terminal wildcard notation is a bit contentious.

https://tools.ietf.org/html/rfc4592#section-2.1.3 allows it. AWS's Route53 treats it as a single record, which is what I based my proposal on.

Golang's net package doesn't recognise it as a valid domain.

In lieu of discovering if Go's net package has a bug, an alternative that makes this explicit is probably a better interface - i.e. catch-all or default subdomain at each level.

A PoC to illustrate is below.

; example.net

*                  IN  CNAME  alias.redirect.name
_redirect.default  IN  TXT    "Redirects to https://example.com/*"

In this example request is made to foo.example.net so _redirect.foo.example.net TXT is looked up.
This does not exist so _redirect.default.example.net TXT is looked up, a result is found and the redirection occurs. The order of execution allows explicit redirections to occur and the default only happens for the subdomains of the same level.

diff --git a/server.go b/server.go
index 846ee27..f39feac 100644
--- a/server.go
+++ b/server.go
@@ -7,6 +7,7 @@ import (
    "net/http"
    "net/url"
    "os"
+   "regexp"
    "strings"
    "time"
 )
@@ -19,12 +20,22 @@ func fallback(w http.ResponseWriter, r *http.Request, reason string) {
    http.Redirect(w, r, location, 302)
 }

+func hostnameLookup(host string) ([]string, error) {
+   hostname := fmt.Sprintf("_redirect.%s", host)
+   return net.LookupTXT(hostname)
+}
+
 func handler(w http.ResponseWriter, r *http.Request) {
    parts := strings.Split(r.Host, ":")
    host := parts[0]

-   hostname := fmt.Sprintf("_redirect.%s", host)
-   txt, err := net.LookupTXT(hostname)
+   txt, err := hostnameLookup(host)
+   if err != nil {
+       pattern := regexp.MustCompile("^(.*?)\\.(.*)$")
+       recursiveHost := pattern.ReplaceAllString(host, "default.$2")
+       txt, err = hostnameLookup(recursiveHost)
+   }
+
    if err != nil {
        fallback(w, r, fmt.Sprintf("Could not resolve hostname (%v)", err))
        return

I'm keen to hear your thoughts before moving further ahead. If/when appropriate I'll send this over as a proper PR with the relevant tests and documentation.

holic commented

Interesting idea! I think it would make sense to just fall back to the next level's _redirect configuration instead of a special name. For example:

*                IN  CNAME  alias.redirect.name
_redirect.zombo  IN  TXT    "Redirects to http://zombo.com/"
_redirect        IN  TXT    "Redirects to https://example.com/*"

Requests to zombo.yourdomain.com would redirect to http://zombo.com/ while all other requests (e.g. any.yourdomain.com or www.yourdomain.com) would redirect to https://example.com/.

That said, I'm not sure I actually want to implement this for a couple of reasons:

  • I would have to traverse the domain by level until it finds a match or runs out of hostnames. This would at least triple the number of DNS queries made for every request.
  • Alternatively, I would need to maintain a list of top-level domains (e.g. .com and .co.nz) to know when to "stop" looking to save DNS queries (since we know that _redirect.com and _redirect.co.nz will never resolve).

Both seem like a large burden on the server for a feature/side-effect that is unlikely to see significant usage.

I think the main blocker of a simple implementation is the responding server doesn't know if it was an explicit domain request or a wildcard that got it there.

If we did know, then when a wildcard was requested respond with the explicit _redirect if it exists, or the _redirect on the same level.
This could be achieved by an additional lookup to r.Host ala dig +short *.example.com to see if this record exists.

The negative of this is the 3 requests - first to explicit, second to dig-ish and third to default.

A named default mode has the advantage of only 2 requests - first to explicit and second to default.

I don't think traversal is a good idea is you could have unintended redirects with no way of opting out.

I don't think a list of domains needs to be maintained (though publicsuffix could work) because the failure mode of net.LookupTXT is enough - com or co.nz is never going to pass domain validation in net.LookupTXT so will return err and fail out.

I'm going to stick a named default behind a flag in a fork for my usage. If this or some remix of the feature is useful I'm always happy to PR :)

holic commented

Sounds good - curious to hear how well it works in practice.

One worry I have with relying on publicsuffix is that the current TLD landscape is wildly in flux (new TLDs released all the time). I would assume that each time I want to pick up new TLD changes, I would need to recompile and rerelease the Go binary?

D1yt commented

So I guess we don't see wildcard fallback anymore? Thought I could simplify adding CNAME entries to your site. But I guess I have to manually add them anyway

@rjocoleman You made a fork with a catch-all?
This is exactly what I was looking for and I'm trying to see if its possible
I want to redirect *.blog.domain.com to blog.domain.com