Quickstart🔗
Installation🔗
Install via pip🔗
Minimal Server🔗
Preparing our server🔗
from nserver import NameServer, Query, A
server = NameServer("example")
@server.rule("example.com", ["A"])
def example_a_records(query: Query):
return A(query.name, "1.2.3.4")
Here's what this code does:
-
To start we import:
NameServer- an instance of this class will contain our applicationQuery- instances of this class will be passed to our rule functions so that we can inspect the incoming DNS queryA- the class used to create DNSArecords
-
Next we create a
NameServerinstance for our application to use. The name we give the server will be used to help distinguish it from others that are also running. -
We then use the
ruledecorator to tell our server when to trigger our function. In this case we will trigger forAqueries that exactly match the nameexample.com. -
When triggered our function will then return a single
Arecord as a response.
Running our server🔗
With our server written we can now run it using the nserver CLI:
We can access it using dig.
; <<>> DiG 9.18.12-0ubuntu0.22.04.3-Ubuntu <<>> -p 5300 @localhost A example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20379
;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;example.com. IN A
;; ANSWER SECTION:
example.com. 300 IN A 1.2.3.4
;; Query time: 324 msec
;; SERVER: 127.0.0.1#5300(localhost) (UDP)
;; WHEN: Thu Nov 02 21:27:12 AEDT 2023
;; MSG SIZE rcvd: 45
Rules🔗
Rules tell our server which queries to send to which functions. NServer ships with a number of rule types.
StaticRulematches on an exact string.ZoneRulematches the given domain and all subdomains.WildcardStringRulewhich allows writing rules using a shorthand syntax.RegexRulewhich uses regular expressions for matching.
The NameServer.rule decorator uses smart_make_rule to automatically select the "best" matching rule type based on the input. This will result in string (str) rules will be used to create either a WildcardStringRule or a StaticRule, whilst regular expression (typing.Pattern) rules will create a RegexRule. This decorator also return the original function unchanged meaning it is possible to decorate the same function with multiple rules.
@saerver.rule("google-dns", ["A"])
def this_will_be_a_static_rule(query):
return A(query.name, "8.8.8.8")
@server.rule("{base_name}", ["A"])
@server.rule("www.{base_name}", ["A"])
@server.rule("mail.{base_name}", ["A"])
def we_only_have_three_servers_for_everything(query):
return list(A(query.name, f"1.1.1.{i+1}") for i in range(3))
Rules can also be added to a server by calling the register_rule method with an exiting rule.
from nserver import RegexRule
server.register_rule(
RegexRule(
re.compile(r"[0-9a-f]{1-4}\.com"),
["A"],
lambda q: return A(q.name, "1.2.3.4"),
)
)
By default all rules match in a case-insensitive manner. This is the expected behaviour for name servers operating on the internet. You can override this by setting case_sensitive=True in the constructors or rule decorator.
The WildcardStringRule🔗
The WildcardStringRule allows using a shorthand notation for matching DNS names.
*will match a single label in the query domain**will match one or more labels in the query domain (in a greedy manner){base_name}will match the "base" of the query name using the Public Suffix List. In general this means the "registered" domain for public TLDs or the last label for non-TLDs (e.g..local,.internal).
For example:
*.example.com.auwill match all first level subdomains ofexample.com.au, but will not matchexample.com.auorfoo.bar.example.com.au.**.example.com.auwill match all subdomains ofexample.com.aubut will not matchexample.com.au.www.{base_name}will matchwwwon all registered and internal domains (www.example.com.au,www.au) but will not match on other subdomains, or as a registered name (www.com.au,www.foo.au)
Responses🔗
Rule functions are expected to return only the following types:
None- A single record instance (of any type)
- A list of record instances (of any record type, including mixed)
- A
Responseinstance
When records are returned, these will automatically be added to a Response instance as answer records. For simple responses this is usually enough. When None is returned it will be converted to an empty response.
However if you wish to return Additional or Authority Records, or change the Error Code you will need to return a Response instance.
For example a typical NS lookup when our application is the authoritive server for the domain may look like this:
# ... server setup exlcuded
from nserver import Response, NS, A, SOA
MY_SERVERS = {
"ns1.example.com": "1.2.3.4",
"ns2.example.com": "1.2.3.5",
"ns-backup.example.com": "9.8.7.6",
}
@server.rule("example.com", ["NS"])
def name_servers(query: Query) -> Response:
response = Response()
for ns, ip in MY_SERVERS.items():
response.answers.append(NS(query.name, ns))
response.additional.append(A(ns, ip))
response.authority.append(SOA(
"example.com",
list(MY_SERVERS.keys())[0],
"admin.example.com",
1,
))
return response