Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/routedns/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type group struct {
Resolvers []string
Type string
Replace []rdns.ReplaceOperation // only used by "replace" type
Subdomain []rdns.SubDomainReplaceOperation // only used by "SubDomainReplace" type
GCPeriod int `toml:"gc-period"` // Time-period (seconds) used to expire cached items in the "cache" type
ECSOp string `toml:"ecs-op"` // ECS modifier operation, "add", "delete", "privacy"
ECSAddress net.IP `toml:"ecs-address"` // ECS address. If empty for "add", uses the client IP. Ignored for "privacy" and "delete"
Expand Down
29 changes: 29 additions & 0 deletions cmd/routedns/example-config/subdomain-replace.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Basic DNS proxy. Translate all plain DNS queries received on port 53
Comment thread
charlieporth1 marked this conversation as resolved.
Outdated
# into DNS-over-TLS queries to Cloudflare's DNS server.
[bootstrap-resolver]
address = "1.1.1.1:853"
protocol = "dot"

[resolvers.cloudflare-dot]
address = "gcp.ctptech.dev:853"
protocol = "dot"

[listeners.local-udp]
address = ":53"
protocol = "udp"
resolver = "sub-domain-replace"

[listeners.local-tcp]
address = ":53"
protocol = "tcp"
resolver = "sub-domain-replace"

[groups.sub-domain-replace]
type = "subdomain-replace"
resolvers = [ "cloudflare-dot" ]
subdomain = [
{ from = '{}.ctptech.dev.', to = '{}.whiskey.software.' },
{ from = '{}.whiskey.software.', to = '{}.charlesp.tech.' },
{ from = '{}.whisky.software.', to = '{}.whiskey.dev.' },
{ from = '{}.local.', to = '{}.' },
]
9 changes: 9 additions & 0 deletions cmd/routedns/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,16 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
if err != nil {
return err
}
case "subdomain-replace":
if len(gr) != 1 {
return fmt.Errorf("type replace only supports one resolver in '%s'", id)
}
resolvers[id], err = rdns.NewSubDomainReplace(id, gr[0], g.Subdomain...)
if err != nil {
return err
}
case "ttl-modifier":

if len(gr) != 1 {
return fmt.Errorf("type ttl-modifier only supports one resolver in '%s'", id)
}
Expand Down
115 changes: 115 additions & 0 deletions subdomain-replace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package rdns

import (
"errors"
"strings"

"github.com/miekg/dns"
)

// Replace is a resolver that modifies queries according to regular expressions
// and forwards the modified queries to another resolver. Responses are then
// mapped back to the original query string.
type SubDomainReplace struct {
id string
resolver Resolver
exp subDomainReplaceExpressions
}

var _ Resolver = &SubDomainReplace{}

type subDomainReplaceExp struct {
from string
to string
}

type subDomainReplaceExpressions []subDomainReplaceExp
func (r subDomainReplaceExp) substitute (qname string) (string, bool) {
fromDomain := r.from
toDomain := r.to
// from {}.ctptech.dev to {}.charlesp.tech

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// from {}.ctptech.dev to {}.charlesp.tech
// from .ctptech.dev to .charlesp.tech

// from test.ctptech.dev to test.charlesp.tech
fromDomain = strings.Replace(fromDomain, "{}", "", 1)
Comment thread
charlieporth1 marked this conversation as resolved.
Outdated
toDomain = strings.Replace(toDomain, "{}", "", 1)
if strings.HasSuffix(qname,fromDomain ) {
str := strings.Replace(qname, fromDomain, toDomain, 1)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a small issue with just doing a simple replace since the "fromDomain" string can show up more than once in the domain. Let's say you have fromDomain = .local. and the domain name being looked up is example.local.something.local. then it'd replace the first occurrance. It might be best to simply strip the fromDomain with strings.TrimSuffix(), then add the toDomain value to the result with + like so:

sub := strings.TrimSuffix(qname, fromDomain)
str := sub+toDomain
return str, true

Log.WithField("qname", qname).Debug("matches: modifying query")
return str, true
} else {
return "", false
}
Comment on lines +37 to +39

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else {
return "", false
}
}
return "", false

}
func (r subDomainReplaceExpressions) apply(qname string) string {
for _, e := range r {
s, result := e.substitute(qname)
if result {
return s
} else {
Comment thread
charlieporth1 marked this conversation as resolved.
Outdated
}
}
return qname
}

type SubDomainReplaceOperation struct {
From string
To string
}

// NewReplace returns a new instance of a Replace resolver.
func NewSubDomainReplace(id string, resolver Resolver, list ...SubDomainReplaceOperation) (*SubDomainReplace, error) {
var exp subDomainReplaceExpressions
for _, o := range list {
if strings.Contains(o.From, "{}") && strings.Contains(o.To, "{}") {
exp = append(exp, subDomainReplaceExp{o.From, o.To})
} else {
return nil, errors.New("{} not found")
Comment thread
charlieporth1 marked this conversation as resolved.
Outdated
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you still need this since {} isn't necessary in the config anymore?

}
Log.WithField("exp", exp).Debug("exp query")
return &SubDomainReplace{id: id, resolver: resolver, exp: exp}, nil
}

// Resolve a DNS query by first replacing the query string with another
// sending the query upstream and replace the name in the response with
// the original query string again.
func (r *SubDomainReplace) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
if len(q.Question) < 1 {
return nil, errors.New("no question in query")
}

oldName := q.Question[0].Name
newName := r.exp.apply(oldName)
log := logger(r.id, q, ci)

// if nothing needs modifying, we can stop here and use the original query
if newName == oldName {
log.Debug("forwarding unmodified query to resolver")
return r.resolver.Resolve(q, ci)
}

// Modify the query string
q.Question[0].Name = newName

// Send the query upstream
log.WithField("new-qname", newName).WithField("resolver", r.resolver).Debug("forwarding modified query to resolver")
a, err := r.resolver.Resolve(q, ci)
if err != nil || a == nil {
return nil, err
}

// Set the question back to the original name
a.Question[0].Name = oldName

// Now put the original name in all answer records that have the
// new name
for _, answer := range a.Answer {
if answer.Header().Name == newName {
answer.Header().Name = oldName
}
}
return a, nil
}

func (r *SubDomainReplace) String() string {
return r.id
}