folbricht / routedns

DNS stub resolver, proxy and router with support for DoT, DoH, DoQ, and DTLS
BSD 3-Clause "New" or "Revised" License
479 stars 63 forks source link

Include caller arguments and introduce new `query` type #366

Open Anuskuss opened 8 months ago

Anuskuss commented 8 months ago

When a group calls another group it would be great to have access to the arguments, e.g.

[groups.blocklist]
type               = "response-blocklist-ip"
resolver           = "myresolver"
blocklist-resolver = "blocklist-resolver"
blocklist          = [ "1.2.3.4" ]

[groups.blocklist-resolver]
type      = "static-responder"
edns0-ede = { code = 15, text = "IP $1 is in the blocklist" } # $1 = "1.2.3.4"

And I would also like to propose the idea of a new query group which allows you to construct your own query (this also needs support for arguments). Could be useful for changing the query type or question, e.g.

[routers.router]
routes = [
  { resolver = "any-emulator", type = "ANY" },
  { resolver = "myresolver" }
]

[groups.any-emulator]
type     = "query"
resolver = "myresolver"
query    = [ "A $2", "AAAA $2" ] # $1 = "ANY", $2 = "example.com"
folbricht commented 8 months ago

This isn't possible with the current implementation unfortunately. It's mostly a limitation of the configuration language chosen (TOML). This requires a rewrite of significant parts to support a more flexible way to configure it, possibly using a higher-level language for config. I've been considering doing that but not sure I'll have time for it in the foreseeable future.

Anuskuss commented 8 months ago

These are mostly gimmicks so it's understandable that it's not a priority. Anyway, I have another example (for people that can't disable their routers' DNS rebind protection):

[routers.router]
routes = [
  { resolver = "dns-rebind-emulator", type = "A", name = '\.rebind\.$' },
  { resolver = "myresolver" }
]

[groups.dns-rebind-emulator]
type      = "replace"
resolvers = [ "dns-rebind-emulator-helper" ]
replace   = [
  { from = '^(\d+)-(\d+)-(\d+)-(\d+)\.rebind\.$', to = '${1}.${2}.${3}.${4}.' }
]

[groups.dns-rebind-emulator-helper]
type   = "static-responder"
answer = [ "IN A $1" ]

Regarding the 2nd suggestion, do you think that's possible with current tools?

folbricht commented 8 months ago

If I understand it correctly you need a responder that can build a response based on the query name, right?

This should be possible, and in fact there's a lot of potential here. Let's just make this even more generic. What if we had an element that would allow some simple scripting language to be defined, that could eval the input and either build the output or forward to another group? That'd be extremely powerful and could probably even do what you were asking in part 1 and more.

Just a mockup, but perhaps it could look like

[groups.my-script]
type      = "script-modifier"
resolvers = [ "resolver1", "resolver2" ]
script   = '
  <some script language that can take the input, then either produce a response or forward to any resolver>
'

Would have to find a script interpreter that integrates well, perhaps https://github.com/Shopify/go-lua or https://github.com/yuin/gopher-lua (not used either one yet).

Anuskuss commented 8 months ago

What does the resolver do in this example? The group takes an input, gives it to the script, the script returns something and that gets forwarded to the resolver? I don't think that's gonna be too useful. I guess preparing something for another group could be useful, but we already got replace and routers, what else could one want?

If scripting were to be introduced (which I don't think is a good idea anyway), I would make it a resolver. So emulating ANY queries would look something like this:

[resolvers.myscript]
type    = "script"
address = "script.sh"

[routers.router]
routes = [
  { resolver = "myscript", type = "ANY" },
  { resolver = "myresolver" }
]
#!/bin/bash

for t in A AAAA CAA HTTPS MX NS SOA TXT; do
  dig +noall +answer $2 $t
done

But then again, I feel like this could almost be done exclusively with the tools already provided.

cbuijs commented 8 months ago

Maybe some of this can be done similar as with replace, but then make it possible to modify the the data/answer instead only the query (or even combine)? The variables can be generated using parentheses (round brackets) in regex?

folbricht commented 8 months ago

It's definitely possible to implement something like that, a group that takes the query-name, applies a regex, and generates the response IP for it. It's just that this would be very specific to one use-case with a lot of limitations:

With the scripting element I was thinking it'd add a low-level way to do custom modifications. It wouldn't be a shell script of course, but some other language that can inspect and modify queries and responses. In the example above the "resolvers" would be optional. The script would receive a query and could then decide to either answer it directly, or forward to one of the defined resolvers. It'd allow implementing custom routing, load-balancing, etc. Not sure how complex that'd be to implement yet. Perhaps a basic "derive-answer-from-query-name" element would be the right approach for now.

Anuskuss commented 8 months ago

It's just that scripting is a can of worms. Or rather executing any external file is. Maybe inline scripting is enough, possible in a language that can't do much damage (e.g. Lua)?

Generally speaking, I think a query merger makes sense: Send your query to multiple resolvers (e.g. Cloudflare and Google), collect the answers, remove duplicates and return. That would be the first (technically last) step to emulate ANY queries and would be useful just by itself. I guess changing the query type really isn't useful except for that single use case.

folbricht commented 8 months ago

It'd definitely be inline Lua with a small set of pre-defined functions to manipulate DNS queries and responses, no sub-processes or shell access. With that one could then implement pretty much any behavior that may be missing from the standard elements (though a little slower). Implementing this requires a bit more time than I can currently spend.

In the mean time, perhaps I could implement what you asked like so:

[routers.router]
routes = [
  { resolver = "dns-rebind-emulator", type = "A", name = '\.rebind\.$' },
  { resolver = "myresolver" }
]

[groups.dns-rebind-emulator-helper]
type   = "static-responder"
query-name-regex = '^(\d+)-(\d+)-(\d+)-(\d+)\.rebind\.$'
answer = [ "IN A ${1}.${2}.${3}.${4}." ]

This would allow you to use the content of the query name to build a static response. It should solve the original ask. Not a fan of that there being some dynamic behavior in a group named "static-responder" but it's not too bad. This is relatively easy to add. Any thoughts on doing this?

cbuijs commented 8 months ago

Doesn't win the beauty contest, but works. :-)

I think for the cases mentioned it should be fine and should be sufficient.

Anuskuss commented 8 months ago

Looks okay but I don't really like query-name-regex. How about format or question (question being the counterpart to answer)? Also would be nice to be able use this to implement the original request:

[groups.blocklist-resolver]
type      = "static-responder"
question  = "^(.+)\.$"
answer    = [ "IN A 0.0.0.0" ]
edns0-ede = { code = 15, text = "IP ${1} is in the blocklist" }
folbricht commented 8 months ago

Would you be able to try out the issue-366 branch? It should let you use a question regex like this:

[groups.static]
type   = "static-responder"
question = '^(\d+)-(\d+)-(\d+)-(\d+)\.rebind\.$'
answer = ["IN A $1.$2.$3.$4"]
[groups.static]
type      = "static-responder"
question  = '^(.+)\.$'
answer    = [ "IN A 0.0.0.0" ]
edns0-ede = { code = 15, text = "IP $1 is in the blocklist" }
folbricht commented 8 months ago

https://github.com/folbricht/routedns/pull/373 lets you specify an extended error message from the blocklist. And it lets you customize the whole message. Would you be able to try that out?

folbricht commented 7 months ago

https://github.com/folbricht/routedns/pull/378 adds a new static-template that should be able to handle the rebind protection. There's an example of it in the PR. These templates can also use a number of string-manipulation functions that might come in handy. The documentation has been updated.

I think this solution is better than using regexes, and it should be more powerful as it allows customizing all response records, not just the answers.

Anuskuss commented 7 months ago

The base case works but it breaks when blocklist-resolver is used in response-blocklist-ip (but that may be by design). Also it seems like there's still no ability to get the filtered IP address instead.

folbricht commented 7 months ago

Not sure I understand what exactly fails. Do you have an example config I can try?

Anuskuss commented 7 months ago
[groups.blocklist-resolver]
type      = "static-responder"
answer    = [ "IN A 0.0.0.0" ]

[groups.blocklist]
type               = "blocklist-v2"
resolvers          = [ "cloudflare" ]
#blocklist-resolver = "blocklist-resolver"
blocklist-format   = "domain"
blocklist          = [ "evil.com" ]
edns0-ede          = { code = 15, text = "{{ .Question }}" }

This example will have EDE: 15 (Blocked): (evil.com.) in the response. Uncommenting blocklist-resolver results in no message.

folbricht commented 7 months ago

Yes, that is actually correct/expected. edns0-ede is only used when the blocklist actually blocks something itself. If blocklist-resolver is used, it doesn't actually block but forward somewhere for a response.

Anuskuss commented 3 months ago

Ok so the first (block reason in EDE text) and third (rebind emulation) suggestions are possible now. Do you think the second (ANY emulation) is something that's worth implementing? I think that it sounds interesting (mostly for getting A and AAAA in a single call) but I don't know how feasible this is. I'll close this if you think it's not worth the effort.