This commit is contained in:
swrup 2025-11-11 02:07:51 +01:00
parent aa2ff7b2f0
commit 2f3113f55d
11742 changed files with 1223940 additions and 0 deletions

View file

@ -0,0 +1,11 @@
_build/
*.install
.merlin
*pcap
*out
coverage/
*req
*key
*private
*pem

View file

@ -0,0 +1,830 @@
### v10.2.2 (2025-10-20)
* Expose a module type insotead of a module for Dns_mirage_resolver_shared.S,
and move it to dns-resolver.mirage.shared ocamlfind library (#396 @dinosaure)
* Expose dns-resolver.shared with Dns_root, Dns_metrics, Dns_block as a public
ocamlfind library (#396 @dinosaure)
* Mention RFC9460 in README (#397 @nickbetteridge)
### v10.2.1 (2025-09-25)
* dns-stub: provide a shared interface with dns-resolver.mirage
(named Dns_resolver_mirage_shared) (#394 @hannesm)
### v10.2.0 (2025-08-01)
* dns: add support for extended DNS error (RFC 8914) (#374 @hannesm)
* dns-resolver: instead of querying a single (ip, query), query up to three
(#375 @reynir @hannesm)
* dns-cache: fix metrics (#377 #379 @hannesm @reynir)
* dns-cache: support RFC 8020, if there's a NXDomain, nothing is underneath
(#390 @hannesm)
* dns-trie: canonicalize domain name before insertion, reducing allocations
(#388 @reynir)
* dns-server: make number of tries in cache (for IXFR) configurable
(#387 @reynir)
* dns-resolver: add metrics (#376 #378 @hannesm @reynir)
* dns-resolver: allow update to the primary, and the TLS state (#383 @reynir)
* dns-resolver: heuristics to detect blocked domains (#380 @reynir)
* dns-resolver: report extended DNS error when blocking
(#385 #384 @reynir @hannesm)
* dns-resolver: implement opportunistic TLS (#389 @hannesm)
* dns-resolver: support query name minimisation (RFC 9156) (#391 @hannesm)
* dns-resolver: features via flags (#392 @hannesm)
### v10.1.0 (2025-05-24)
* dns-mirage-resolver: adapt API to prepare for DoH (#373 @dinosaure @PizieDust
@reynir @hannesm)
* resolver: support DNSSEC
- #372 @reynir @hannesm
- #361 @reynir @hannesm
* dns: support SVCB and HTTPS resource records (RFC 9460, #359 @nickbetteridge)
* dns-cli: avoid deprecated cmdliner values (#367 @hannesm, fixes #366),
add app/resolver (#366)
* resolver: add further tests, fix the loop of a cyclic NS (#368 @hannesm)
### v10.0.0 (2025-02-07)
* dns-certify: use a pair instead of two separate arguments for the dns-key
(#357 @hannesm)
* add KSK 2024 (#358 @hannesm)
* update to mirage-crypto 1.2.0 API changes (#362 @hannesm)
* use dune variants, avoid functors (#363 @hannesm)
### v9.1.0 (2024-10-22)
* Dns.Dnskey: provide to_string and name_key_to_string (@hannesm, @dinosaure,
#356 - fixes #355)
* BREAKING: Dns.Dnskey remove pp_name_key (unused, irritating, #356)
* BREAKING Dns_certify_mirage.retrieve_certificate use separate dns_key_name
and dns_key arguments, avoid string decoding in that function (#356)
### v9.0.1 (2024-09-27)
* dns-client-miou: use String.get_uint16_be instead of String.get_int16_be
(#354 @dinosaure)
### v9.0.0 (2024-08-26)
* Remove `Cstruct.t` and use `string`/`bytes` (@hannesm, @dinosaure, @palainp, #351)
* Add a Miou implementation of `ocaml-dns` (@dinosaure, @hannesm, #352)
### v8.0.0 (2024-05-29)
* dns-client (lwt, mirage): depend on happy-eyeballs-{lwt,mirage} instead of
duplicating the code. This requires happy-eyeballs 1.1.0, and now the same
Happy_eyeballs_{lwt,mirage}.t is used for DNS (connecting to the nameserver)
and for the application (connecting to a remote host)
(@dinosaure @hannesm #346)
* server: improve API documentation (@hannesm
1a80bd4080e597687152cf351d035ef5f00c5946
000ae02dfc477d91c05891e3891a447328ae448a)
* server: add a `packet_callback` to `handle_packet` and `handle_buf`
(@RyanGibb #349)
* server: expose `update_data` (@RyanGibb #350)
* resolver: b root name server IP change (@hannesm #348)
* secondary server [mirage]: avoid infinite loop in connect (avoids SYN floods)
(@hannesm @reynir #347)
* resolver, dns_zone: use consistently `Log` instead of `Logs` (@palainp #342)
### v7.0.3 (2023-06-15)
* dns-client-lwt, dns-client-mirage: adapt to happy-eyeballs 0.6 changes,
also avoid unnecessary recursion (#340 @hannesm, @reynir)
### v7.0.2 (2023-06-13)
* dns-server: for secondary servers use the right zone transfers and keys, fixed
in #339 by @hannesm
* dns: add support for null record (arbitrary binary data) (#338 @RyanGibb)
### v7.0.1 (2023-02-27)
* dns-server.zone: fix parsing of zone files that contain tokens such as
`N` `S` `E` `W` `-<number>` `<number>m` `DS` `CAA` `TYPE<number>`.
There was an inconsistency in the Dns_zone_parser.keyword_or_number rule.
Test cases have been added, a comment has been added to the
Dns_zone_lexer.kw_or_cs function. Discovered while updating the primary NS
with an entry of "e.ns", fixed in #336 @hannesm
Broken since the early days of this development
### v7.0.0 (2023-02-16)
* BREAKING: dns-client is split into 3 packages: dns-client-lwt,
dns-client-mirage. If your dune file contains dns-client.lwt, use
dns-client-lwt now. If your dune file contains dns-client.mirage, use
dns-client-mirage now (#331 @hannesm)
* update to mirage-crypto 0.11.0 API changes and tls 0.16.0 packaging changes
(#331 @hannesm)
* dns-client.resolvconf: add line number to parser (#334 @hannesm, inspired by
#328 @bikallem)
* dns-client.resolvconf: allow zone idx (RFC 4007) for IPv6 entries
(#334 @hannesm, inspired by #328 @bikallem)
* dns-server.zone: allow zone files without final newline (add a newline to the
buffer if the last character is not \n) (#333 @hannesm)
* dns-client-{lwt,mirage}: do not log when the resolver closed the connection,
but there are no pending requests (#332 @reynir)
* dns-certify: in Dns_certify_mirage use X509.Private_key.of_string, the
behaviour when both key_data and key_seed is provided changed, and leads to
an exception now (#330 @hannesm)
### v6.4.1 (2022-12-02)
* dns-client: adapt to happy eyeballs 0.4.0 (#329 @reynir @hannesm)
* dns-resolver: dnssec validation is optional via a labeled parameter ~dnssec
passed to Dns_resolver.create (#325 @hannesm)
* upgrade to dune 2 (#327 @reynir)
### v6.4.0 (2022-10-24)
* dns-client: demote log level of response to debug (#317 @hannesm)
* dns-client: use DNS-over-TLS for uncensoreddns.org only (#320 @hannesm)
* API: dns-client: connect returns the protocol (UDP/TCP), allowing mixed UDP
and TCP namerservers being used (#322 @hannesm)
* dns-client-mirage: allow hostname in authenticator, improve error message and
documentation (#319 #322 @hannesm)
* dns-client-mirage: support UDP nameservers as "udp:<IP>" in
nameserver_of_string (#322 @reynir @hannesm)
* API: dns-client, dns-stub, dns-resolver: ?size is now ?cache_size (#322
@hannesm, suggested by @reynir)
### v6.3.0 (2022-07-26)
* dns-server: demote log level for various messages (#309 @hannesm)
* dns-zone: add additional glue: only add if authoritative for nameserver domain
(#309 @hannesm)
* BUGFIX: dns-trie: fix lookup when delegations are present, add tests
(#309 @hannesm)
* ozone: be more explicit when showing errors (#311 @psafont)
* dns: avoid polymorphic comparison (#314 @hannesm, reported by @RyanGibb)
* FEATURE: dns: add LOC resource records (RFC 1876) (#310 @RyanGibb)
### v6.2.2 (2022-04-08)
* BUGFIX dns-trie: the collect_entries function jumped over zone boundaries.
This lead dns-primary-git to detect changes in zones with subdomains delegated
to the same name servers, leading to dropping of zones (#308 @hannesm,
reported by @reynir)
* BUGFIX dns-server.text: add address glue records for name servers out of the
authority of this server. This is crucial since dns-primary-git supports
such glue records to notify these about zone updates (#307 @hannesm)
* New functionality in dns-zone (dns-server.zone): decode_keys, decode_zones,
and decode_zones_keys copied from dns-primary-git for reusing in other
projects (#307 @hannesm)
### v6.2.1 (2022-04-01)
* BUGFIX dns: RTYPE is 16 bit, previously 15 bit were accepted, also check for
being positive (#304, @hannesm @reynir)
* dns-server: dns-trie zone check no longer enforces that the nameserver is in
the nameserve set of the zone, this enables hidden primary setups (fixes #303,
@hannesm)
### v6.2.0 (2022-03-09)
* New opam package "dnssec" implementing dnssec validation (@reynir @hannesm)
* Use custom log sources, not the default one from Logs (@reynir @hannesm)
* BUGFIX dns-resolver: unlisten on the listen port, not the packet src_port
(#290 @hannesm)
* dns-resolver: add IPv6 addresses of root servers (fixes #262, @hannesm)
* dns-resolver: preliminary support for DNSSec (#262 @reynir @hannesm)
* dns-client: when /etc/resolv.conf modifies, update the list of nameservers
(#291 @hannesm @reynir)
* dns-cli: update to cmdliner 1.1.0 (#300 @hannesm)
* dns-client-mirage: add module type and nameserver_of_string and connect to
allow creation of a MirageOS device (#297 @dinosaure)
* dns-cache: add size, capacity, and weight to metrics (#301, fixes #299,
@hannesm)
### v6.1.4 (2022-01-11)
* dns-client-{mirage,lwt}: avoid multiple simultaneous connection attempts
to the same resolver. Now, before a connection is initiated, a Lwt_condition
is put into the client state which subsequent resolution requests wait for
(#285 @hannesm, review by @reynir, reported by @kit-ty-kate in
roburio/http-lwt-client#8)
### v6.1.3 (2021-12-17)
* dns-mirage: use tcpip >= 7.0.0 instead of deprecated mirage-stack and
mirage-protocols (#283 @dinosaure)
### v6.1.2 (2021-11-27)
* dns: avoid exceptions when decoding resource records (#282 @reynir @hannesm)
### v6.1.1 (2021-11-19)
* dns-client: by default, do not send EDNS (#280 @reynir @hannesm)
* BREAKING dns-certify.mirage: treat key_data as base64 encoded data
(#280 @reynir @hannesm)
* mirage: update to mirage-protocols 6.0.0 API (use TCP.listen / UDP.listen)
resolver: unlisten on UDP port after response has been received (#280 @reynir
@hannesm)
### v6.1.0 (2021-11-10)
* BUGFIX dns-client: avoid exception on TLS handshake failure (reported by @reynir)
* FEATURE dns-client: optionally send a edns in the query (reported by @orbitz #276)
* dns-client: combine send and recv, avoid resource leaks on timeout
### v6.0.2 (2021-10-27)
* dns: remove astring dependency (@hannesm)
* dns-client: adapt to happy-eyeballs 0.1.0 API (#274 @hannesm)
* dns-client: avoid List.concat_map, make it available on OCaml 4.08 (@hannesm)
### v6.0.1 (2021-10-25)
* remove rresult dependency (#273 @hannesm, review by @reynir)
### v6.0.0 (2021-10-19)
* use Cstruct.length instead of deprecated Cstruct.len
* avoid deprecated fmt functions
* dns-client: send EDNS tcp keepalive with a timeout of 120 seconds if TCP
is used (@reynir @hannesm)
* BREAKING dns: Rr_map.get_ttl is now ttl, and takes 'a key -> 'a -> int32
(instead of b -> int32), Rr_map.with_ttl now is 'a key -> 'a -> int32 -> 'a
(instead of b -> int32 -> b) (#264 @hannesm)
* BREAKING dns: Rr_map.A now uses Ipaddr.V4.Set.t, Aaaa uses Ipaddr.V6.Set.t
(requires ipaddr 5.2.0) (#268 @hannesm)
* BREAKING dns.cache: type entry now is polymorphic ('a entry = `Entry of 'a ...)
(instead of `Entry of Rr_map.b) (#263 @reynir and @hannesm)
* BREAKING dns.cache: use a LRU.F.t instead of LRU.M.t (#256 @hannesm)
* dns.cache: provide get_or_cname and get_any function (#256 #257 @hannesm)
* BUGFIX dns.cache: update if time to live of cached entry expired
(reported in #259 by @dinosaure, fix by @reynir and @hannesm)
* dns-client support DNS-over-TLS (RFC 7858): the type io_addr is now a variant
of `Plaintext (Ipaddr.t * int) or `Tls (Tls.Config.client * Ipaddr.t * int)
By default, ca-certs (ca-certs-nss for MirageOS) are used as trust anchors,
and the certificate is expected to contain the IP address of the resolver.
The default resolver (anycast.uncensoreddns.org) certificate is verified by
hostname, since the let's encrypt certificate does not include an IP address
in SubjectAlternativeNames (#270 @hannesm)
* BREAKING dns-client.mirage.Make is extended by a Mirage_clock.PCLOCK
(#270 @hannesm)
* BREAKING dns-client, dns-stub: use Dns.proto instead of custom [`TCP|`UDP]
(#266 @hannesm)
* dns-client: use a `mutable timeout_ns : int64` instead of
`timeout_ns : int64 ref` (#269 @hannesm)
* BREAKING dns-client: remove `?nameserver` from
getaddrinfo/gethostbyname/gehostbyname6/get_resource_record - if a custom
nameserver should be queried, a distinct Dns_client.t can be constructed
(#269 @reynir and @hannesm)
* dns-client: multiplex over TCP connections (#269 @reynir and @hannesm)
* dns-client: use happy-eyeballs to connect to all nameservers from
/etc/resolv.conf sequentially (lwt and mirage) (#269 @reynir and @hannesm)
* BREAKING dns-client remove UDP support from lwt (#270 @reynir and @hannesm)
* BREAKING dns-resolver.mirage add DNS-over-TLS support (@reynir @hannesm)
* BREAKING dns-resolver remove "mode" from codebase, default to recursive
(a stub resolver is available as dns-stub) (#260 @hannesm)
* dns-resolver: use dns.cache instead of copy in Dns_resolver_cache
(#256 @hannesm)
* BUGFIX dns-resolver: fix responses to queries (reported in #255 by @dinosaure,
fix in #258 by @reynir and @hannesm)
* dns-resolver: refactor and cleanup code, remove statistics, remove dead code
(#258 #261 @reynir @hannesm)
* dns-stub: reconnect to resolver, resend all outstanding queries
(#269 @hannesm)
### v5.0.1 (2021-04-22)
* dns-certify: adapt to X.509 0.13.0 API changes (#254 @hannesm)
### v5.0.0 (2021-04-14)
* IPv6 support for client and server (Mirage, Unix, Lwt) (#249 #252 @hannesm)
This results in breaking changes, especially in the Mirage boilerplate,
since now a Mirage_stack.V4V6 is needed instead of a Mirage_stack.V4.
* dns-certify: support EC private keys, now that X509 0.12.0 supports them
(#252 @hannesm)
### v4.6.3 (2021-01-11)
* dns-server: wildcard support (#248 @hannesm)
* dns-certify: only dnskey needs to be a valid hostname (#247 @hannesm),
allow [`raw] Domain_name.t in signing requests (#249 @hannesm)
* dns-client.resolvconf provides a parser for /etc/resolv.conf (#240 @hannesm),
used in dns-client.unix and dns-client.lwt (#241 @hannesm)
* BUGFIX dns-cli notify keys are accepted in namekey_c (#242 @hannesm)
* BUGFIX dns: revise TXT resource record encoding and storage (for DKIM usage)
previously RR were cut at 255 characters (fixes #244, #245 @hannesm)
* BUGFIX dns: decoding of TSIG packets (#250 @hannesm)
* BUGFIX ocertify: pem file may contain a certificate chain (#246 @hannesm)
### v4.6.2 (2020-08-07)
* fixes for 32 bit support (OCaml-CI now runs on 32 bit) in test suite and EDNS
* dns: fix EDNS flag decoding and encoding (16 bit only)
reported in #234 by @dinosaure, fix #235 by @hannesm
* dns-server: reply to unsupported EDNS version (not 0) with
rcode=16 (BadVersOrSig), as required by RFC 6891, and tested by DNS flag day
issue #166, fix in #237 by @hannesm
### v4.6.1 (2020-06-20)
* dns-client.lwt, dns-client.unix: initialize RNG (#232 @hannesm)
* dns-cli: compatible with mirage-crypto-rng 0.8 (#232 @hannesm)
### v4.6.0 (2020-06-02)
* dns: bugfix for name compression when encoding names at offset > (2 ^ 14) - 1
(#225 @hannesm)
* dns: allow unknown DNSKEY algorithm, TLSA certificate usage, selector,
matching type, SSHFP algorithm and typ. This makes the DNS library
future-proof for when new values are assigned (#228 @hannesm)
* dns: enforce a max_rdata_length for all resource records. This ensures that
when a resource record is loaded into the server, it can be extracted via a
DNS query and transferred via IXFR/AXFR
(#230 @hannesm, reported in #229 via #225)
* AXFR: encode and decode support for AXFR transfers spanning multiple messages
(#225 @hannesm)
* client: do not initialize the Mirage_crypto_rng in the library, initialize
the RNG in applications (#227 @hannesm)
* certify: provide cert_matches_csr function and use it (cleans up partial
ad-hoc matches which did not verify that all hostnames of the CSR are present
in the certificate) (#226 @hannesm, reported in #224)
### v4.5.0 (2020-04-23)
* client: add timeout for DNS requests (defaults to 5 seconds, as in resolv.h).
* dns-client-mirage functor requires a Mirage_time.S implementation (changes API).
Update your code as in this commit:
https://github.com/roburio/unikernels/commit/201e980f458ebb515298392227294e7b508a1009
#223 @linse @hannesm, review by @cfcs
### v4.4.1 (2020-03-29)
* client: treat '*.localhost' and '*.invalid' special, as specified in RFC 6761
and let-localhost-be-localhost. #221 @hannesm, review by @cfcs (who reported
#220, original report roburio/openvpn#28)
### v4.4.0 (2020-03-13)
* dns-stub, a new opam package, is a stub resolver #209 @hannesm, review by
@cfcs
* embed IP address of recursive resolver only once #214 @hannesm, fixes #210,
review by @cfcs
* Dns_trie.lookup returns NotAuthoritative if no SOA is present #217 @hannesm,
review by @cfcs
* Secondary server is looked up in trie properly (may be in another zone, which
primary is not authoritative for the other zone) #217 @hannesm, review by
@cfcs
* new function Dns.Dnskey.pp_name_key #218 @hannesm, review by @cfcs
* dns-certify uses new ACME protocol (where the intermediate certificate is
part of the issuance process) #219 @hannesm, review by @cfcs
* dns-certify/dns-tsig/dns-cli: use mirage-crypto #219 @hannesm, review by @cfcs
### v4.3.1 (2020-01-21)
* server (#207, @hannesm, review by @cfcs)
- provide return code and request vs reply statistics
- BUGFIX update only increase SOA.serial of zones which changed (including regression tests)
- expose Authentication.access_granted, Authentication.zone_and_operation, Authentication.operation_to_string
* dns (#207, @hannesm, review by @cfcs)
- expose Rcode.to_string for metrics above
### v4.3.0 (2020-01-09)
* dns
- BUGFIX Name_rr_map.remove_sub remove empty maps (#205, @hannesm)
* server (#205, @hannesm)
- authentication refactoring: given a key by its Domain_name.t (name._op.zone),
this is valid for operation `op` for `zone` and subdomains thereof. The
operation may be one of `update`, `transfer`, and `notify`, with an `update`
key being valid for any operation, and a `transfer` key valid for
notifications as well
- Primary.create has a new optional argument `unauthenticated_zone_transfer`
to allow unsigned zone transfer requests
- the type `Authentication.a` and value `Authentication.tsig_auth` are removed
- Primary.create and Secondary.create no longer have the `a` argument
- authentication uniformly uses `Authentication.access`
- handle_update / handle_axfr_request / handle_ixfr_request are provided and
under test
- tests for authentication and handle_question
* client (#204, @hannesm)
- introduce get_resource_record which is the same as getaddrinfo, but returns
the error as variant instead of [ `Msg of string ]
- BUGFIX follow_cname handles replies with a cname and no data for the alias
appropriately (and a regression test has been developed)
### v4.2.0 (2019-11-20)
* dns
relax resource record parsing, don't require the name to be a hostname it
used to be strict on the parser, but that violates RFC 2181 Sec 11
> The DNS itself places only one restriction on the particular labels that can
> be used to identify resource records. That one restriction relates to the
> length of the label and the full name.
previous code had already exceptions for DNSKEY, TXT, CNAME, TLSA (service
name or host name), SRV (service name) (#201 @hannesm)
* dns-certify
BUGFIX provide signing_request to create certificate signing requests,
now including all hostnames in subjectAlternativeName (previously, the
common name was left out which is not what RFC 5280 recommends)
(#198 @hannesm)
* dns-server.mirage
- provide metrics (using the metrics library) of connections and actions (#199 @hannesm)
- BREAKING the `on_update` callback passed to `primary` has more arguments (#200 @hannesm)
`~authenticated_key` : [`raw] Domain_name.t option
`~update_source` : Ipaddr.V4.t
* dns-server
- BREAKING handle_buf: returns Domain_name.t of key used for authentication (#200 @hannesm)
- BUGFIX handle_update: allow modification of multiple zones at once
still, each name must be within the zone given in Query.name (which
is authenticated against), allowing hidden let's encrypt secondary
for multiple zones, using a keys authorized for the root zone (#200 @hannesm)
- BUGFIX Dns_trie.zone returns the zone (Domain_name.t * Soa.t) of a
provided Domain_name.t, it now works for non-existing names, tests
were added (#200 @hannesm)
* dns-mirage: log packets on debug level instead of info (#198 @hannesm)
### v4.1.0 (2019-11-01)
* Client improvements (#191 #192 @olleolleolle @linse @cfcs @hannesm in marrakesh September)
- new sublibrary dns.cache providing an LRU cache, mostly copied from resolver
- it uses a LRU cache now (defaults to 32 entries) from dns.cache
- since #195 a mutable LRU.M.t
- tests were added
- Dns_client_flow has been migrated to Dns_client
- various code cleanups (extracted functions), better naming, improved docstrings
- Uflow is now known as Transport
- requires a monotonic clock on creation
* adapt to x509 0.8.0 API changes (#193 @hannesm)
* adapt to newer MirageOS interfaces (#196 @hannesm)
### v4.0.0 (2019-08-15)
* Switch to uDNS implementation, developed from scratch since 2017, primarily
focusing on a recursive caching resolver. The server part supports dynamic
updates (RFC 2135), transaction authentication with HMAC (RFC 2845), zone
transfer (RFC 5936), incremental zone transfer (RFC 1995), change
notifications (RFC 1996) amongst others.
* The core library uses a GADT for resource record sets, where the key (resource
record type) specifies the value type.
* The API does not leak exceptions, but uses the result type where appropriate.
* TCP transport is well supported and used widely (client uses it by default)
* Naming: client is a DNS client, resolver is the recursive resolver library
* The DNS library is split into the following opam packages and sublibraries:
- `dns` - the core library
- `dns-tsig` - transaction signatures
- `dns-cli` - command line utilities (odig, onotify, ..)
- `dns-client` - pure client implementation
- `.unix` - DNS client using the Unix module for communication
- `.lwt` - DNS client using Lwt_unix for communication
- `.mirage` - DNS client using MirageOS for communication
- `dns-certify` - helpers for let's encrypt provisioning
- `.mirage` - certificate provisioning with MirageOS
- `dns-mirage` - generic MirageOS communication layer
- `dns-server` - pure server implementation
- `.mirage` - MirageOS primary and secondary server
- `.zone` - zone file parser (mostly taken from the 1.x series)
- `dns-resolver` - pure recursive resolver implementation
- `.mirage` - MirageOS recursive resolver
* Only OCaml 4.07.0 and above are supported
* Multicast DNS has been dropped for now
* A client using async from JS has not been implemented yet
* The default recursive resolver, used by the client implementations, is
uncensoreddns.org
### v1.1.3 (2019-07-16)
* Support domain-name.0.3.0 interface, which bumps the minimum
OCaml version supported to 4.04 due to that dependency (@avsm)
* Fix tests with recent OCaml (use mmap/bigarray-compat) (@avsm)
### v1.1.2 (2019-02-28)
* Mirage: adapt to mirage-kv 2.0.0 interface (#156 by @samoht)
### v1.1.1 (2019-01-25)
* Support Base64.3.0.0 interface (@avsm)
### v1.1.0 (2019-01-05)
* Improve parsing robustness with:
- invalid pointers in packets
- taking total packet size limitations into account
- handling unknown opcodes gracefully without an exception
Work done by @Willy-Tan in #154.
* Port build from jbuilder to Dune (#155 #152 #153 by @paurkedal @samoht)
* Update opam metadata to the 2.0 format.
### v1.0.1 (2017-11-06)
* dns-lwt-unix: add missing dependency on cmdliner (#145 by @avsm)
* async: close reader and writer properly (#147 by @1yefuwang1)
* fix build with OCaml 4.06 (and -safe-string) (#148 by @djs55)
### v1.0.0 (2017-06-23)
Rearrange the `opam` packages to eliminate optional dependencies,
and have explicit and separate packages for the Lwt, Async and
Mirage implementations. The `opam` and `ocamlfind` layouts now
have the same names:
- `dns`: the core package
- `dns-lwt`: Lwt implementation
- `dns-lwt-unix`: Lwt Unix, including servers
- `dns-async`: Async implementation (this currently uses Unix)
This layout is not compatible with the older releases which had
ocamlfind subpackages, so an upper bound will be needed in OPAM
for those. However, porting should be relatively straightforward
to the new release, and in return your users will not have to deal
with specifying a myriad of optional dependencies in OPAM.
- This release also ports the build to use Jbuilder, which speeds
it up quite a bit and removes boilerplate files.
- Depend on Lwt 3.0.0 interfaces, including the blocking bind.
### v0.20.2 (2017-06-01)
* Depend explicitly on `Ipaddr_unix` and `Uri_services` modules.
* Add a basic server example which has a static lookup table and
does not use the Trie structure. It is in [examples/server.ml].
### v0.20.1 (2017-05-16)
* Port to lwt >= 3.0 (#136, @djs55)
### v0.20.0 (2017-03-23)
* Remove the `Dns.Buf` module that formerly wrapped Cstruct, now that the
latter is a mature library.
* Add an `?alloc` optional argument to functions that would formerly accept
a `Dns.Buf`. By default, this allocates a single page, but consumers of
this library can override it in order to supply their own allocation
logic (e.g. a pool allocator).
* Remove dependency on `io-page` to follow the `Dns.Buf` removal.
All these changes were part of #132 by @hannesm.
### v0.19.1 (2017-02-15)
* Use topkg instead of oasis (#126
* Do not reverse the order of resource records in the parser (#109 by @djs55)
* Restrict to OCaml 4.03.0+.
* Fix bug parsing pointers to pointers to DNS name labels (#129 by @yeungda-rea)
### v0.19.0 (2017-01-20)
* Port to MirageOS 3 module types.
* Remove runtime dependency on PPX from META file
* Bugfixes and improvements for async backend compilation (#100 by @vbmithr).
### v0.18.1 (2016-04-17)
* Clear AA bit on requests, as some servers will drop these otherwise
### v0.18.0 (2016-03-12)
* Remove dependency on camlp4, switch to ppx
### v0.17.0 (2016-03-11)
* This library now depends on the `hashcons` package rather than
containing a fork of it. Now that there is no LGPL (+ linking exception
code left, clarify that the license is ISC. Previously the `opam`
file claimed a mixture of licenses (#86 via @djs55)).
* Add multi-distro Travis testing support.
* Library now depends on OCaml 4.02+
### v0.16.0 (2015-10-21)
* Change source port randomization to avoid overflow in the port range
(#83 from @yomimono).
Improve mDNS support (#82 from Luke Dunstan):
* Add `Dns.Probe` to implement the unique name probing portion of mDNS.
* Expose the `Dns.Name.Set` construct.
* Added a functor `Mdns_resolver_mirage.Chain` that is intended to compose
an mDNS resolver with a normal DNS resolver such that `*.local` is resolved
via mDNS and everything else is done with DNS.
* Changed `Dns.Query` to not respond to queries for classes other than IN.
* Fixed mDNS legacy responses to use TTL <= 10 sec
* Fixed mDNS responses to use RD=0.
### v0.15.3 (2015-07-30)
* Fix regression in 0.15.2 which prevented `Dns_server_unix.listen` from
answering more than one query (#80 from Magnus Skjegstad)
### v0.15.2 (2015-07-04)
* Fix incorrect mirage dependency on tcpip
* Improve clarity and formatting of Lwt use
* Remove camlp4 dependency
* Now requires lwt >2.4.7
### v0.15.1 (2015-07-02)
* Fix critical DNS resolver timeout bug causing unexpected exceptions
### v0.15.0 (2015-05-14)
* Name.domain_name has been renamed to Name.t and is now abstract
* Name.domain_name_to_string has been renamed to Name.to_string
* Name.string_to_domain_name has been deprecated for Name.of_string
* Name.parse_name has been renamed to Name.parse
* Name.marshal_name has been renamed to Name.marshal
* Name.hashcons_charstring has been renamed to Name.hashcons_string
* Name.hashcons_domainname has been renamed to Name.hashcons
* Name.canon2key has been renamed to Name.to_key
* Name.for_reverse has been replaced by Name.of_ipaddr
* Name.of_ipaddr accepts a Ipaddr.t and produces a name suitable for reverse DNS
* We now require >= ipaddr.2.6.0 to support Name.of_ipaddr
* uri 1.7.0+ is now required for its uri.services service registry
* Named service lookups are now supported in zone files
* Dig string serializations are now in Dns.Dig (#61 from Heidi Howard
### v0.14.1 (2015-03-29)
* Reduce namespace pollution in `name.ml` to avoid breaking with Cstruct 1.6.0+.
* Add a `Dns_server.compose` function to make it easier to build
resolution pipelines (#58).
* Add a `Dns_server_mirage` functor (#55).
* Add `Dns_resolver.resolve_pkt` to support custom query packets (#49).
* Split out the experimental Async_resolver into a `Async_kernel` and
Unix libraries. This introduces the `dns.async-unix` library.
### v0.14.0 (2015-01-29)
* Renamed `Packet.QM` to `Packet.Q_Normal` and `QU` to `Q_mDNS_Unicast` for
clarity and added more detailed doc comments. Added constructor function
`Packet.make_question` for convenience. (#41
* Support `io-page` 1.3.0+ interface. (#40
### v0.13.0 (2015-01-26)
* Add support for multicast DNS (RFC6762) in the trie. (#35 from Luke Dunstan
* mDNS doesn't use SOA nor delegation (RFC 6762 section 12), so some minor changes
to Trie are required to handle this.
* mDNS doesn't echo the questions in the response (RFC 6762 section 6), except
in legacy mode, so a `bool` argument was added to `Query.response_of_answer`.
* `Query.answer` still exists but now `Query.answer_multiple` is also available
for answering multiple questions in one query to produce a single answer
(RFC 6762 section 5.3). One caveat is that responses may exceed the maximum
message length, but that is not really specific to mDNS. Also, in
theory multiple questions might require multiple separate response
messages in unusual cases, but that is complicated and the library
does not deal with that yet.
* `Query.answer_multiple` takes an optional function to allow the caller
to control the `cache-flush` bit. This bit is only set for records
that have been "confirmed as unique". Using a callback requires
minimal changes here but puts the burden of maintaining uniqueness
state elsewhere.
* `Query.answer_multiple` takes an optional function to filter the
answer, in order to support "known answer suppression" (RFC 6762
section 7.1). Again, using a callback requires minimal change to the
core, but later on the mDNS-specific known answer suppression logic
could move into the `Query` module if that turns out to be simpler.
* A query for `PTR` returns additional records for `SRV` and `TXT`, to
support efficient service discovery.
* `Trie.iter` was added to support mDNS announcements.
* Switch to `Bytes` instead of `String` for eventual `-safe-string` support.
* Partially remove some error printing to stderr. (#36
Unit tests were added for some of the changes above, including a test-only
dependency on `pcap-format`.
### v0.12.0 (2014-12-24)
* Parse and marshal the mDNS unicast-response bit (#29).
* Add OUnit tests for `Dns.Packet.parse` using `pcap` files.
* Fix parsing of `SRV` records (#30).
* Use `Bytes` instead of `String` for mutable buffers.
* Switch to `Base64` v2, which uses `B64` as the toplevel module name
to avoid linking conflicts with other community libraries.
### v0.11.0 (2014-11-02)
* Do not depend in Io_page; instead `Dns.Buf.create` now accepts an
optional `alloc` parameter to use a custom allocator such as `Io_page`.
* Add Async DNS resolver modules from @marklrh (#22).
* Add a Dns_resolver_mirage.Static for a static DNS interface.
### v0.10.0 (2014-08-20)
* Add `Dns_resolver_mirage` module for making stub resolution requests
using the Mirage module types.
* `Dns.Resolvconf` parses `/etc/resolv.conf` entries using `Ipaddr.t`
instead of `string` values now.
* Adapt `Dns_resolver` and `Dns_resolver_unix` to use `Ipaddr.t` more.
* Improve `mldig` to use `Ipaddr` more and add more RR printing to
match the conventional `dig` tool behaviour.
* Expose `Dns.Packet.Not_implemented` exception rather than a pattern
match failure.
* Depend on external `Base64` package instead of bundling one inside
the `Dns` packed module.
* Add a local `opam` file for easier pinning.
* Add an `examples/` directory with a DNS forwarder sample (#21).
### v0.9.1 (2014-07-29)
* Fix file descriptor leak in resolver (#15, #16) by expanding `commfn`
with a cleanup function.
### v0.9.0 (2014-06-16)
* Ensure that all `Dns.Buf.t` buffers are page-aligned, via `Io_page`.
* Remove a Unix dependency that snuck into the `Dns_resolver` portable
core, by adding a timeout argument to the `commfn` type.
* Improve ocamldoc in `Dns_resolver_unix`.
### v0.8.1 (2014-04-19)
* Add `process_of_zonebufs` to handle multiple zone files.
* Adapt `Dns_server_unix` to expose multiple zonebuf functions.
### v0.8.0 (2014-02-21)
* Use `Ipaddr.V6` to restore IPv6/AAAA RR support.
* `process_query` now takes an output buffer so it doesn't have to
overwrite the input buffer it just parsed.
* Add Travis continuous integration scripts.
* Regenerate with OASIS 0.4.1
* Split the `dns.lwt` into a portable `dns.lwt-core` that doesn't
require Unix (from which a Mirage version can be built). The only
change to existing applications is that Unix-specific functions
have shifted into `Dns_resolver_unix` or `Dns_server_unix`, with
the module types for `PROCESSOR` and `CLIENT` unchanged.
### v0.7.0 (2013-08-26)
* Add path argument to `Resolv_conf in Dns_resolver.config.
* `Dns_resolver.t` is now a record type rather than a first-class module.
* Fix `mldig` server and port options.
* Change `Zone.load_zone` to `Zone.load` and make it functional over `Loader.db`.
* Use `Ipaddr.V4.t` addresses in favor of Cstruct or Uri_IP representations.
* Fix `RRSIG` signed type to be of the answer rather than the question.
* Fix `ANY` queries.
* Add `Buf` to provide a nickname for `char Bigarray`s.
* Change `Packet.{parse,marshal}` to use Buf.t rather than exposing Cstruct.t
* Change `Packet.parse` to remove name map parameter
* Factor protocol modules into `Protocol` with default DNS implementations
* Add first-class `PROCESSOR` module to `Dns_server` for contextual
protocol extensions
* Change `Dns_server.listen` to accept processor
* Rename `Dns_server.listen_with_zonebuf` and `Dns_server.listen_with_zonefile`
to `Dns_server.serve_with_zonebuf` and `Dns_server.serve_with_zonefile` resp.
* Add `processor_of_process`, `process_of_zonebuf`,
`eventual_process_of_zonefile`, and `serve_with_processor` to `Dns_server`
* Rename `Query.query_answer` to `Query.answer`
* Add `Query.response_of_answer` and `Query.answer_of_response`
* Move `Dns_resolver.build_query` to `Query.create`
* By default, DNS packet IDs are randomly generated with Random
* `Dns_resolver` now supports simultaneous resolver protocol requests
* Fix reversed multiple TXT parse bug
* Move DNSSEC implementation to <//github.com/dsheets/ocaml-dnssec>
### v0.6.2 (2013-02-13)
* Fix Lwt compilation after switch to `Dns.Names.Map` instead of `Hashtbl`.
* Fix Lwt Makefile detection (`Lwt.unix` instead of `Lwt.ssl`
### v0.6.1 (2013-02-12)
* Improve performance of packet marshalling.
* Add a Mirage `Dns_server` subpackage.
### v0.6.0 (2012-12-31)
* (Very) experimental DNSSEC support.
* Use cstruct-0.6.0 API.
* Improve robustness of `Dns_resolver`.
* Add EDNS0 support for larger packet sizes.
### v0.5.2 (2012-11-28)
* Fix the server interface to be fully asynchronous.
* Correct `q_type`/`q_class` arguments being ignored in the Lwt
Dns_resolver (from Pierre Chambart).
### v0.5.1 (2012-10-05)
* Remain compatible with OCaml-3.12.1 with the more verbose
first-class module syntax.
### v0.5.0 (2012-09-29)
* Add mldig as a full(ish)-featured dig clone, with similar
output format.
* Add `Dns.Resolvconf` for parsing `/etc/resolv.conf` files on
POSIX hosts.
* Move the Lwt bits (resolver, server, cmdline client) into
a separate directory, to follow the Cohttp convention.
### v0.4.0 (2012-09-18)
* Initial public release.

View file

@ -0,0 +1,23 @@
Copyright (c) 2017, 2018, Hannes Mehnert
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,13 @@
.PHONY: build clean test doc
build:
dune build
test:
dune runtest
doc:
dune build @doc
clean:
dune clean

View file

@ -0,0 +1,167 @@
# ocaml-dns - a Domain Name System (DNS) library
(c) 2017-2019 Hannes Mehnert (robur.io, Center for the Cultivation of Technology)
v10.2.2
This library supports most of the domain name system used in the wild. It
adheres to strict conventions. Failing early and hard. It is mostly
implemented in the pure fragment of OCaml (no mutation, isolated IO, no
exceptions).
It all started out as an experiment to run a recursive resolver, but after
initial prototypes it turned out that every configurable recursive resolver
needs a fully-fledged authoritative nameserver as well (for overriding various
zones such as `.localhost` and reverse lookups of RFC 1918 IP ranges).
Legacy resource record types are not dealt with, and there is no plan to support
`ISDN`, `MAILA`, `MAILB`, `WKS`, `MB`, `HINFO`, ... . `AXFR`, `IXFR`,
and `UPDATE` is only handled via TCP connections. The only resource class
supported is `IN` (the Internet). Truncated hmac in `TSIG` are not supported
(always the full length of the hash algorithm is used).
Please read [the blog article](https://hannes.robur.coop/Posts/DNS) for a more
detailed overview.
This library is published under the 2 clause BSD license.
## Supported RFCs
* [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities
* [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification
* [RFC 1876](https://tools.ietf.org/html/rfc1876) A Means for Expressing Location Information in the Domain Name System
* [RFC 1912](https://tools.ietf.org/html/rfc1912) Common DNS Operational and Configuration Errors
* [RFC 1995](https://tools.ietf.org/html/rfc1995) Incremental Zone Transfer in DNS
* [RFC 1996](https://tools.ietf.org/html/rfc1996) A Mechanism for Prompt Notification of Zone Changes (DNS NOTIFY)
* [RFC 2136](https://tools.ietf.org/html/rfc2136) Dynamic Updates in the domain name system (DNS UPDATE)
* [RFC 2181](https://tools.ietf.org/html/rfc2181) Clarifications to the DNS Specification
* [RFC 2308](https://tools.ietf.org/html/rfc2308) Negative Caching of DNS Queries (DNS NCACHE)
* [RFC 2782](https://tools.ietf.org/html/rfc2782) A DNS RR for specifying the location of services (DNS SRV)
* [RFC 2845](https://tools.ietf.org/html/rfc2845) Secret Key Transaction Authentication for DNS (TSIG)
* [RFC 3596](https://tools.ietf.org/html/rfc3596) DNS Extensions to Support IP Version 6
* [RFC 4033](https://tools.ietf.org/html/rfc4033) DNS Security Introduction and Requirements
* [RFC 4034](https://tools.ietf.org/html/rfc4034) Resource Records for the DNS Security Extensions
* [RFC 4035](https://tools.ietf.org/html/rfc4035) Protocol Modifications for the DNS Security Extensions
* [RFC 4255](https://tools.ietf.org/html/rfc4255) Using DNS to Securely Publish Secure Shell (SSH) Key Fingerprints
* [RFC 4343](https://tools.ietf.org/html/rfc4343) Domain Name System (DNS) Case Insensitivity Clarification
* [RFC 4509](https://tools.ietf.org/html/rfc4509) Use of SHA-256 in DNSSEC Delegation Signer (DS) Resource Records (RRs)
* [RFC 4592](https://tools.ietf.org/html/rfc4592) The Role of Wildcards in the Domain Name System
* [RFC 4635](https://tools.ietf.org/html/rfc4635) HMAC SHA TSIG Algorithm Identifiers
* `*` [RFC 5001](https://tools.ietf.org/html/rfc5001) DNS Name Server Identifier (NSID) Option
* [RFC 5155](https://tools.ietf.org/html/rfc5155) DNS Security (DNSSEC) Hashed Authenticated Denial of Existence
* [RFC 5358](https://tools.ietf.org/html/rfc5358) Preventing Use of Recursive Nameservers in Reflector Attacks
* [RFC 5452](https://tools.ietf.org/html/rfc5452) Measures for Making DNS More Resilient against Forged Answers
* [RFC 5936](https://tools.ietf.org/html/rfc5936) DNS Zone Transfer Protocol (AXFR)
* [RFC 6594](https://tools.ietf.org/html/rfc6594) Use of the SHA-256 Algorithm with RSA, Digital Signature Algorithm (DSA), and Elliptic Curve DSA (ECDSA) in SSHFP Resource Records
* [RFC 6605](https://tools.ietf.org/html/rfc6605) Elliptic Curve Digital Signature Algorithm (DSA) for DNSSEC
* [RFC 6698](https://tools.ietf.org/html/rfc6698.html) The DNS-Based Authentication of Named Entities (DANE) Transport Layer Security (TLS) Protocol: TLSA
* [RFC 6761](https://tools.ietf.org/html/rfc6761) Special-Use Domain Names
* `*` [RFC 6762](https://tools.ietf.org/html/rfc6762) Multicast DNS
* [RFC 6844](https://tools.ietf.org/html/rfc6844) DNS Certification Authority Authorization (CAA) Resource Record
* [RFC 6890](https://tools.ietf.org/html/rfc6890) Special-Purpose IP Address Registries
* [RFC 6891](https://tools.ietf.org/html/rfc6891) Extension Mechanisms for DNS (EDNS(0))
* [RFC 6895](https://tools.ietf.org/html/rfc6895) Domain Name System (DNS) IANA Considerations (BCP 42)
* [RFC 7129](https://tools.ietf.org/html/rfc7129) Authenticated Denial of Existence in the DNS
* [RFC 7479](https://tools.ietf.org/html/rfc7479) Using Ed25519 in SSHFP Resource Records
* [RFC 7626](https://tools.ietf.org/html/rfc7626) DNS Privacy Considerations
* [RFC 7766](https://tools.ietf.org/html/rfc7766) DNS Transport over TCP - Implementation Requirements
* [RFC 7816](https://tools.ietf.org/html/rfc7816) DNS Query Name Minimisation to Improve Privacy
* [RFC 7828](https://tools.ietf.org/html/rfc7828) The edns-tcp-keepalive EDNS0 Option
* `*` [RFC 7830](https://tools.ietf.org/html/rfc7830) The EDNS(0) Padding Option
* `*` [RFC 7873](https://tools.ietf.org/html/rfc7873) Domain Name System (DNS) Cookies
* [RFC 8020](https://tools.ietf.org/html/rfc8020) NXDOMAIN: There Really Is Nothing Underneath
* [RFC 8080](https://tools.ietf.org/html/rfc8080) Edwards-Curve Digital Security Algorithm (EdDSA) for DNSSEC
* [RFC 8109](https://tools.ietf.org/html/rfc8109) Initializing a DNS Resolver with Priming Queries
* [RFC 8914](https://tools.ietf.org/html/rfc8914) Extended DNS Errors
* [RFC 9156](https://tools.ietf.org/html/rfc8914) DNS Query Name Minimisation to Improve Privacy
* [RFC 9460](https://tools.ietf.org/html/rfc9460) Service Binding and Parameter Specification via the DNS (SVCB and HTTPS Resource Records)
* [RFC 9539](https://tools.ietf.org/html/rfc9539) Unilateral Opportunistic Deployment of Encrypted RecursivetoAuthoritative DNS
* [draft-ietf-dnsop-let-localhost-be-localhost-02](https://tools.ietf.org/html/draft-ietf-dnsop-let-localhost-be-localhost-02) Let 'localhost' be localhost.
`*`: Please note that the RFCs marked with `*` are only partially implemented
(i.e. only wire format, but no logic handling the feature).
## Installation
You first need to install [OCaml](https://ocaml.org) (at least 4.08.2) and
[opam](https://opam.ocaml.org), the OCaml package manager (at least 2.0.0) on
your machine (you can use opam to install an up-to-date OCaml (`opam switch
4.08.2`)).
You may want to follow the [mirage installation
instructions](https://mirage.io/wiki/install) to get `mirage` installed on your
computer.
To minimize the amount of run-time dependencies for each individual
functionality, the library is split into multiple opam packages (core, server,
client, resolver, cli, certify), with multiple ocamlfind libraries for the
different backends (no optional dependencies) -- i.e. `dns-server.mirage`
contains the MirageOS-specific DNS server code.
Now the µDNS library is installed, and you can try out the examples. Find some
examples at the [unikernel repository](https://github.com/roburio/unikernels).
## Documentation
API documentation [is available online](https://mirage.github.io/ocaml-dns/).
## Transition from older versions
The pre-4.0.0 versions of ocaml-dns had a significantly different interface,
and so applications using them will need to be rewritten to follow the
stricter coding style used in the post-4.0.0 branches. The major improvements
from 1.x to the 4.x series are:
- data (rrset) is defined in a single GADT in `Rr_map`
- added support for: notify, dynamic update, zone transfer, tsig (hmac authentication), edns
- no mutable data structures, leading to easier reasoning about library state
- switched to an independent `domain_name` library which uses a faster and more
compact `string array` instead of `string list` for storing domain names
- integration with LetsEncrypt for provisioning valid X.509 certificates
- no use of exceptions, instead preferring explicit result values from API functions
Please get in touch on <mirageos-devel@lists.xenproject.org> or on the Discuss forum
at <https://discuss.ocaml.org> (with the `mirageos` tag) if you have any questions
about migrating (or just general questions).
## Development
To work with the [opam](https://opam.ocaml.org/) packages provided when
developing modifications to DNS, or when pinning a specific version,
you will have to pin the same *version* for all of them:
```csh
: csh syntax
set version=4.99.0
set repo=git+https://github.com/mirage/ocaml-dns.git
# the -y parameter means "force" or
# "do go ahead and register a new package"
# the -n parameter means
# "just register the pin, don't actually install it yet"
foreach pkg ( dns dns-{certify,cli,client,resolver,server,mirage,tsig,stub} )
opam pin add -y -n $pkg.$version --dev $repo
end
```
```bash
: bash syntax
version=4.99.0
repo=git+https://github.com/mirage/ocaml-dns.git
for pkg in dns dns-{certify,cli,client,resolver,server,mirage,tsig,stub}
do
opam pin add -y -n $pkg.$version --dev $repo
done
```
Now you can install the packages you need, for instance:
```shell
opam install dns-client
```
or
```shell
opam install dns-resolver
```

View file

@ -0,0 +1,110 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
let reporter_with_ts ~dst () =
let pp_tags f tags =
let pp tag () =
let (Logs.Tag.V (def, value)) = tag in
Format.fprintf f " %s=%a" (Logs.Tag.name def) (Logs.Tag.printer def) value;
()
in
Logs.Tag.fold pp tags ()
in
let report src level ~over k msgf =
let tz_offset_s = Ptime_clock.current_tz_offset_s () in
let posix_time = Ptime_clock.now () in
let src = Logs.Src.name src in
let k _ =
over ();
k ()
in
msgf @@ fun ?header ?tags fmt ->
Format.kfprintf k dst
("%a:%a %a [%s] @[" ^^ fmt ^^ "@]@.")
(Ptime.pp_rfc3339 ?tz_offset_s ())
posix_time
Fmt.(option ~none:(any "") pp_tags)
tags Logs_fmt.pp_header (level, header) src
in
{ Logs.report }
let setup_log style_renderer level =
Fmt_tty.setup_std_outputs ?style_renderer ();
Logs.set_level level;
Logs.set_reporter (reporter_with_ts ~dst:Format.std_formatter ())
let connect_tcp ip port =
let sa = Unix.ADDR_INET (Ipaddr_unix.to_inet_addr ip, port) in
let fam = match ip with Ipaddr.V4 _ -> Unix.PF_INET | Ipaddr.V6 _ -> Unix.PF_INET6 in
let sock = Unix.(socket fam SOCK_STREAM 0) in
Unix.(setsockopt sock SO_REUSEADDR true) ;
Unix.connect sock sa ;
sock
(* TODO EINTR, SIGPIPE *)
let send_tcp sock buf =
let size = String.length buf in
let size_buf =
let b = Bytes.create 2 in
Bytes.set_int16_be b 0 size ;
b
in
let data = Bytes.cat size_buf (Bytes.of_string buf) in
let whole = size + 2 in
let rec out off =
if off = whole then ()
else
let bytes = Unix.send sock data off (whole - off) [] in
out (bytes + off)
in
out 0
let recv_tcp sock =
let rec read_exactly buf len off =
if off = len then ()
else
let n = Unix.recv sock buf off (len - off) [] in
read_exactly buf len (off + n)
in
let buf = Bytes.create 2 in
read_exactly buf 2 0 ;
let len = Bytes.get_int16_be buf 0 in
let buf' = Bytes.create len in
read_exactly buf' len 0 ;
Bytes.unsafe_to_string buf'
open Cmdliner
let setup_log =
Term.(const setup_log
$ Fmt_cli.style_renderer ()
$ Logs_cli.level ())
let ip_c = Arg.conv (Ipaddr.of_string, Ipaddr.pp)
let namekey_c =
let parse s =
let ( let* ) = Result.bind in
let* (name, key) = Dns.Dnskey.name_key_of_string s in
let is_op s =
Domain_name.(equal_label s "_update" || equal_label s "_transfer" || equal_label s "_notify")
in
let amount = match Domain_name.find_label ~rev:true name is_op with
| None -> 0
| Some x -> succ x
in
let* zone = Domain_name.drop_label ~amount name in
let* zone = Domain_name.host zone in
Ok (name, zone, key)
in
let pp ppf (name, zone, key) =
Fmt.pf ppf "key name %a zone %a dnskey %a"
Domain_name.pp name Domain_name.pp zone Dns.Dnskey.pp key
in
Arg.conv (parse, pp)
let name_c =
Arg.conv
((fun s -> Result.bind (Domain_name.of_string s) Domain_name.host),
Domain_name.pp)
let domain_name_c =
Arg.conv (Domain_name.of_string, Domain_name.pp)

View file

@ -0,0 +1,57 @@
(library
(name dns_cli)
(public_name dns-cli)
(wrapped false)
(modules dns_cli)
(libraries dns cmdliner ptime.clock.os logs.fmt fmt.cli logs.cli fmt.tty ipaddr.unix))
(executable
(name ocertify)
(public_name ocertify)
(package dns-cli)
(modules ocertify)
(libraries dns dns-certify dns-cli bos fpath x509 ptime ptime.clock.os mirage-crypto-pk mirage-crypto-rng mirage-crypto-rng.unix))
(executable
(name oupdate)
(public_name oupdate)
(package dns-cli)
(modules oupdate)
(libraries dns dns-tsig dns-cli ptime ptime.clock.os mirage-crypto-rng mirage-crypto-rng.unix randomconv))
(executable
(name onotify)
(public_name onotify)
(package dns-cli)
(modules onotify)
(libraries dns dns-tsig dns-cli ptime ptime.clock.os mirage-crypto-rng mirage-crypto-rng.unix randomconv))
(executable
(name ozone)
(public_name ozone)
(package dns-cli)
(modules ozone)
(libraries dns dns-cli dns-server.zone dns-server bos))
(executable
(name odns)
(public_name odns)
(modules odns)
(package dns-cli)
(libraries dns dns-client-lwt dns-cli cmdliner mtime.clock.os
lwt.unix ohex bos))
(executable
(name odnssec)
(public_name odnssec)
(modules odnssec)
(package dns-cli)
(libraries dns dns-client-lwt dns-cli cmdliner mtime.clock.os
lwt.unix dnssec))
(executable
(name resolver)
(public_name resolver)
(modules resolver)
(package dns-cli)
(libraries dns-cli dns-resolver dns-resolver.mirage lwt.unix tcpip.stack-socket mirage-mtime.unix logs.fmt mirage-crypto-rng.unix))

View file

@ -0,0 +1,205 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
let ( let* ) = Result.bind
let find_or_generate_key key_filename keytype keydata seed bits =
let* f_exists = Bos.OS.File.exists key_filename in
if f_exists then
let* data = Bos.OS.File.read key_filename in
X509.Private_key.decode_pem data
else
let* key =
match keydata with
| None -> Ok (X509.Private_key.generate ?seed ~bits keytype)
| Some s ->
let* s = Base64.decode s in
X509.Private_key.of_octets s keytype
in
let pem = X509.Private_key.encode_pem key in
let* () = Bos.OS.File.write ~mode:0o600 key_filename pem in
Ok key
let query_certificate sock fqdn csr =
match Dns_certify.query Mirage_crypto_rng.generate (Ptime_clock.now ()) fqdn csr with
| Error e -> Error e
| Ok (out, cb) ->
Dns_cli.send_tcp sock out;
let data = Dns_cli.recv_tcp sock in
cb data
let nsupdate_csr sock host keyname zone dnskey csr =
match Dns_certify.nsupdate Mirage_crypto_rng.generate Ptime_clock.now ~host ~keyname ~zone dnskey csr with
| Error s -> Error s
| Ok (out, cb) ->
Dns_cli.send_tcp sock out;
let data = Dns_cli.recv_tcp sock in
match cb data with
| Ok () -> Ok ()
| Error e -> Error (`Msg (Fmt.str "nsupdate reply error %a" Dns_certify.pp_u_err e))
let jump _ server_ip port hostname more_hostnames dns_key_opt csr key keytype keydata seed bits cert force =
Mirage_crypto_rng_unix.use_default ();
let fn suffix = function
| None -> Fpath.(v (Domain_name.to_string hostname) + suffix)
| Some x -> Fpath.v x
in
let csr_filename = fn "req" csr
and key_filename = fn "key" key
and cert_filename = fn "pem" cert
in
let* csr =
let* f_exists = Bos.OS.File.exists csr_filename in
if f_exists then
let* data = Bos.OS.File.read csr_filename in
X509.Signing_request.decode_pem data
else
let* key = find_or_generate_key key_filename keytype keydata seed bits in
let* csr = Dns_certify.signing_request hostname ~more_hostnames key in
let pem = X509.Signing_request.encode_pem csr in
let* () = Bos.OS.File.write csr_filename pem in
Ok csr
in
(* before doing anything, let's check whether cert_filename is present,
the public key matches, and the certificate is still valid *)
let now = Ptime_clock.now () in
let tomorrow =
let (d, ps) = Ptime.Span.to_d_ps (Ptime.to_span now) in
Ptime.v (succ d, ps)
in
let* cert =
let* f_exists = Bos.OS.File.exists cert_filename in
if f_exists then
let* data = Bos.OS.File.read cert_filename in
let* certs = X509.Certificate.decode_pem_multiple data in
match List.filter (fun c -> X509.Certificate.supports_hostname c hostname) certs with
| [] -> Ok None
| [ cert ] -> Ok (Some cert)
| _ -> Error (`Msg "multiple certificates that match the hostname")
else
Ok None
in
let* () =
match cert with
| Some cert ->
if not force && Dns_certify.cert_matches_csr ~until:tomorrow now csr cert then
Error (`Msg "valid certificate with matching key already present")
else
Ok ()
| None -> Ok ()
in
(* strategy: unless force is provided, we can request DNS, and if a
certificate is present, compare its public key with csr public key *)
let write_certificate certs =
let data = X509.Certificate.encode_pem_multiple certs in
let* () = Bos.OS.File.delete cert_filename in
Bos.OS.File.write cert_filename data
in
let sock = Dns_cli.connect_tcp server_ip port in
let* should_update =
if force then
Ok true
else match query_certificate sock hostname csr with
| Ok (server, chain) ->
Logs.app (fun m -> m "found cached certificate in DNS");
let* () = write_certificate (server :: chain) in
Ok false
| Error `No_tlsa ->
Logs.debug (fun m -> m "no TLSA found, sending update");
Ok true
| Error (`Msg m) -> Error (`Msg m)
| Error ((`Decode _ | `Bad_reply _ | `Unexpected_reply _) as e) ->
Error (`Msg (Fmt.str "error %a while parsing TLSA reply"
Dns_certify.pp_q_err e))
in
if not should_update then
Ok ()
else
let* () =
match dns_key_opt with
| None -> Error (`Msg "no dnskey provided, but required for uploading CSR")
| Some (keyname, zone, dnskey) ->
let* () = nsupdate_csr sock hostname keyname zone dnskey csr in
let rec request retries =
match query_certificate sock hostname csr with
| Error (`Msg msg) -> Error (`Msg msg)
| Error #Dns_certify.q_err when retries = 0 ->
Error (`Msg "failed to retrieve certificate (tried 10 times)")
| Error `No_tlsa ->
Logs.warn (fun m -> m "still no tlsa, sleeping two more seconds");
Unix.sleep 2;
request (pred retries)
| Error (#Dns_certify.q_err as e) ->
Logs.err (fun m -> m "error %a while handling TLSA reply (retrying)"
Dns_certify.pp_q_err e);
request (pred retries)
| Ok (server, chain) -> write_certificate (server :: chain)
in
request 10
in
Logs.app (fun m -> m "success! your certificate is stored in %a (private key %a, csr %a)"
Fpath.pp cert_filename Fpath.pp key_filename Fpath.pp csr_filename);
Ok ()
open Cmdliner
let dns_server =
let doc = "DNS server IP" in
Arg.(required & pos 0 (some Dns_cli.ip_c) None & info [] ~doc ~docv:"IP")
let port =
let doc = "Port to connect to" in
Arg.(value & opt int 53 & info [ "port" ] ~doc)
let dns_key =
let doc = "nsupdate key (name:alg:b64key, where name is YYY._update.zone)" in
Arg.(value & opt (some Dns_cli.namekey_c) None & info [ "dns-key" ] ~doc ~docv:"KEY")
let hostname =
let doc = "Hostname (FQDN) to issue a certificate for" in
Arg.(required & pos 1 (some Dns_cli.name_c) None & info [] ~doc ~docv:"HOSTNAME")
let more_hostnames =
let doc = "Additional hostnames to be included in the certificate as SubjectAlternativeName extension" in
Arg.(value & opt_all Dns_cli.domain_name_c [] & info ["additional"] ~doc ~docv:"HOSTNAME")
let csr =
let doc = "certificate signing request filename (defaults to hostname.req)" in
Arg.(value & opt (some string) None & info [ "csr" ] ~doc)
let key =
let doc = "private key filename (default to hostname.key)" in
Arg.(value & opt (some string) None & info [ "key" ] ~doc)
let seed =
let doc = "private key seed (or full private key if keytype is a EC key)" in
Arg.(value & opt (some string) None & info [ "seed" ] ~doc)
let bits =
let doc = "private key bits" in
Arg.(value & opt int 4096 & info [ "bits" ] ~doc)
let keydata =
let doc = "private key (base64 encoded)" in
Arg.(value & opt (some string) None & info [ "data" ] ~doc)
let keytype =
let doc = "keytype to generate" in
Arg.(value & opt (enum X509.Key_type.strings) `RSA & info [ "type" ] ~doc)
let cert =
let doc = "certificate filename (defaults to hostname.pem)" in
Arg.(value & opt (some string) None & info [ "certificate" ] ~doc)
let force =
let doc = "force signing request to DNS" in
Arg.(value & flag & info [ "force" ] ~doc)
let ocertify =
let doc = "ocertify requests a signed certificate" in
let man = [ `S "BUGS"; `P "Submit bugs to me";] in
let term =
Term.(term_result (const jump $ Dns_cli.setup_log $ dns_server $ port $ hostname $ more_hostnames $ dns_key $ csr $ key $ keytype $ keydata $ seed $ bits $ cert $ force))
and info = Cmd.info "ocertify" ~version:"10.2.2" ~doc ~man
in
Cmd.v info term
let () = exit (Cmd.eval ocertify)

View file

@ -0,0 +1,433 @@
(* odns client utility. *)
(* RFC 768 DNS over UDP *)
(* RFC 7766 DNS over TCP: https://tools.ietf.org/html/rfc7766 *)
(* RFC 6698 DANE: https://tools.ietf.org/html/rfc6698*)
let pp_zone ppf (domain,query_type,query_value) =
(* TODO dig also prints 'IN' after the TTL, we don't... *)
Fmt.string ppf
(Dns.Rr_map.text_b domain (Dns.Rr_map.B (query_type, query_value)))
let pp_zone_tlsa ppf (domain,ttl,(tlsa:Dns.Tlsa.t)) =
(* TODO this implementation differs a bit from Dns_map.text and tries to
follow the `dig` output to make it easier to port existing scripts *)
Fmt.pf ppf "%a.\t%ld\tIN\t%d\t%d\t%d\t%s"
Domain_name.pp domain
ttl
(Dns.Tlsa.cert_usage_to_int tlsa.cert_usage)
(Dns.Tlsa.selector_to_int tlsa.selector)
(Dns.Tlsa.matching_type_to_int tlsa.matching_type)
( (* this produces output similar to `dig`, splitting the hex string
in chunks of 56 chars (28 bytes): *)
let hex = Ohex.decode tlsa.data in
let hlen = String.length hex in
let rec loop acc = function
| n when n + 56 >= hlen ->
String.concat " " (List.rev (String.sub hex n (hlen-n)::acc))
|> String.uppercase_ascii
| n -> loop ((String.sub hex n 56)::acc) (n+56)
in loop [] 0)
let pp_nameserver ppf = function
| `Plaintext (ip, port) -> Fmt.pf ppf "TCP %a:%d" Ipaddr.pp ip port
| `Tls (tls_cfg, ip, port) ->
Fmt.pf ppf "TLS %a:%d%a" Ipaddr.pp ip port
Fmt.(option ~none:(any "") (append (any "#") Domain_name.pp))
((Tls.Config.of_client tls_cfg).Tls.Config.peer_name)
let do_a nameservers domains () =
let happy_eyeballs = Happy_eyeballs_lwt.create () in
let t = Dns_client_lwt.create ?nameservers happy_eyeballs in
let (_, ns) = Dns_client_lwt.nameservers t in
Logs.info (fun m -> m "querying NS %a for A records of %a"
pp_nameserver (List.hd ns) Fmt.(list ~sep:(any ", ") Domain_name.pp) domains);
let job =
Lwt_list.iter_p (fun domain ->
let open Lwt in
Logs.debug (fun m -> m "looking up %a" Domain_name.pp domain);
Dns_client_lwt.(getaddrinfo t A domain)
>|= function
| Ok (_ttl, addrs) when Ipaddr.V4.Set.is_empty addrs ->
(* handle empty response? *)
Logs.app (fun m -> m ";%a. IN %a"
Domain_name.pp domain
Dns.Rr_map.ppk (Dns.Rr_map.K A))
| Ok resp ->
Logs.app (fun m -> m "%a" pp_zone (domain, A, resp))
| Error (`Msg msg) ->
Logs.err (fun m -> m "Failed to lookup %a: %s\n"
Domain_name.pp domain msg)
) domains
in
match Lwt_main.run job with
| () -> Ok () (* TODO handle errors *)
let for_all_domains nameservers ~domains typ f =
(* [for_all_domains] is a utility function that lets us avoid duplicating
this block of code in all the subcommands.
We leave {!do_a} simple to provide a more readable example. *)
let happy_eyeballs = Happy_eyeballs_lwt.create () in
let t = Dns_client_lwt.create ?nameservers happy_eyeballs in
let _, ns = Dns_client_lwt.nameservers t in
Logs.info (fun m -> m "NS: %a" pp_nameserver (List.hd ns));
let open Lwt in
match
Lwt_main.run
(Lwt_list.iter_p
(fun domain ->
Dns_client_lwt.getaddrinfo t typ domain >|= function
| Error `Msg msg ->
Logs.err (fun m ->
m "Failed to lookup %a for %a: %s\n%!"
Dns.Rr_map.ppk (Dns.Rr_map.K typ)
Domain_name.pp domain msg) ;
()
| Ok x -> f domain x)
domains)
with
| () -> Ok () (* TODO catch failed jobs *)
let output_response typ domain resp =
Logs.app (fun m -> m "%a" pp_zone (domain, typ, resp))
let do_aaaa nameserver domains () =
for_all_domains nameserver ~domains Dns.Rr_map.Aaaa
(output_response Dns.Rr_map.Aaaa)
let do_mx nameserver domains () =
for_all_domains nameserver ~domains Dns.Rr_map.Mx
(output_response Dns.Rr_map.Mx)
let do_tlsa nameserver domains () =
for_all_domains nameserver ~domains Dns.Rr_map.Tlsa
(fun domain (ttl, tlsa_resp) ->
Dns.Rr_map.Tlsa_set.iter (fun tlsa ->
Logs.app (fun m -> m "%a" pp_zone_tlsa (domain, ttl, tlsa))
) tlsa_resp)
let do_txt nameserver domains () =
for_all_domains nameserver ~domains Dns.Rr_map.Txt
(fun _domain (ttl, txtset) ->
Dns.Rr_map.Txt_set.iter (fun txtrr ->
Logs.app (fun m -> m "%ld: @[<v>%s@]" ttl txtrr)
) txtset)
let do_any _nameserver _domains () =
(* TODO *)
Error (`Msg "ANY functionality is not present atm due to refactorings, come back later")
let do_dkim nameserver (selector:string) domains () =
let domains = List.map (fun original_domain ->
Domain_name.prepend_label_exn
(Domain_name.prepend_label_exn
(original_domain) "_domainkey") selector
) domains in
for_all_domains nameserver ~domains Dns.Rr_map.Txt
(fun _domain (_ttl, txtset) ->
Dns.Rr_map.Txt_set.iter (fun txt ->
Logs.app (fun m -> m "%s" txt)
) txtset)
let do_type nameserver typ domains () =
match Dns.Rr_map.of_int typ with
| Ok K k ->
for_all_domains nameserver ~domains k
(fun domain resp ->
Logs.app (fun m -> m "%a" pp_zone (domain, k, resp)))
| _ -> Error (`Msg "bad argument")
let do_loc nameserver domains () =
for_all_domains nameserver ~domains Dns.Rr_map.Loc
(output_response Dns.Rr_map.Loc)
open Cmdliner
let sdocs = Manpage.s_common_options
let setup_log =
let setup_log (style_renderer:Fmt.style_renderer option) level : unit =
Fmt_tty.setup_std_outputs ?style_renderer () ;
Logs.set_level level ;
Logs.set_reporter (Logs_fmt.reporter ())
in
Term.(const setup_log $ Fmt_cli.style_renderer ~docs:sdocs ()
$ Logs_cli.level ~docs:sdocs ())
let arg_ns : 'a Term.t =
let doc = "IP of nameserver to use" in
Arg.(value & opt (some Dns_cli.ip_c) None & info ~docv:"NS-IP" ~doc ["ns"])
let arg_port : 'a Term.t =
let doc = "Port of nameserver" in
Arg.(value & opt int 53 & info ~docv:"NS-PORT" ~doc ["ns-port"])
let tls_hostname =
let doc = "Hostname to use for TLS authentication" in
Arg.(value & opt (some Dns_cli.name_c) None &
info ~docv:"HOSTNAME" ~doc ["tls-hostname"])
let tls_ca_file =
let doc = "TLS trust anchor file" in
Arg.(value & opt (some file) None &
info ~docv:"CAs" ~doc ["tls-ca-file"])
let tls_ca_dir =
let doc = "TLS trust anchor directory" in
Arg.(value & opt (some dir) None &
info ~docv:"CAs" ~doc ["tls-ca-directory"])
let tls_cert_fp =
let doc = "TLS certificate fingerprint" in
Arg.(value & opt (some string) None &
info ~docv:"FP" ~doc ["tls-cert-fingerprint"])
let tls_key_fp =
let doc = "TLS public key fingerprint" in
Arg.(value & opt (some string) None &
info ~docv:"FP" ~doc ["tls-key-fingerprint"])
let no_tls =
let doc = "Disable DNS-over-TLS" in
Arg.(value & flag & info ~docv:"no-tls" ~doc ["no-tls"])
let nameserver =
let ( let* ) = Result.bind in
let ns no_tls ca_file ca_dir cert_fp key_fp hostname ip port =
if no_tls then
Option.map (fun ip -> `Tcp, [ `Plaintext (ip, port)]) ip
else
match ip with
| None -> None
| Some ip ->
let auth peer_name ip =
let cfg auth =
Result.join
(Result.map
(fun authenticator -> Tls.Config.client ~authenticator ?peer_name ?ip ()) auth)
in
let time () = Some (Ptime_clock.now ()) in
let of_fp data =
let hash, fp =
let h_of_string = function
| "md5" -> Some `MD5
| "sha" | "sha1" -> Some `SHA1
| "sha224" -> Some `SHA224
| "sha256" -> Some `SHA256
| "sha384" -> Some `SHA384
| "sha512" -> Some `SHA512
| _ -> None
in
match String.split_on_char ':' data with
| [] -> invalid_arg "empty fingerprint"
| [ fp ] -> `SHA256, fp
| hash :: rt -> match h_of_string (String.lowercase_ascii hash) with
| Some h -> h, String.concat "" rt
| None -> invalid_arg ("unknown hash: " ^ hash)
in
let hex = Ohex.encode fp in
hash, hex
in
match ca_file, ca_dir, cert_fp, key_fp with
| None, None, None, None -> cfg (Ca_certs.authenticator ())
| Some f, None, None, None ->
let* data = Bos.OS.File.read (Fpath.v f) in
let* certs = X509.Certificate.decode_pem_multiple data in
cfg (Ok (X509.Authenticator.chain_of_trust ~time certs))
| None, Some d, None, None ->
let* files = Bos.OS.Dir.contents (Fpath.v d) in
let* certs =
List.fold_left (fun r f ->
let* acc = r in
let* data = Bos.OS.File.read f in
let* cert = X509.Certificate.decode_pem data in
Ok (cert :: acc))
(Ok []) files
in
cfg (Ok (X509.Authenticator.chain_of_trust ~time certs))
| None, None, Some fp, None ->
let hash, fingerprint = of_fp fp in
cfg (Ok (X509.Authenticator.cert_fingerprint ~time ~hash ~fingerprint))
| None, None, None, Some fp ->
let hash, fingerprint = of_fp fp in
cfg (Ok (X509.Authenticator.key_fingerprint ~time ~hash ~fingerprint))
| _ -> invalid_arg "only one of cert-file, cert-dir, key-fingerprint, cert-fingerprint is supported"
in
let ip' = match hostname with None -> Some ip | Some _ -> None in
let tls = match auth hostname ip' with
| Ok a -> a
| Error `Msg msg -> invalid_arg msg
in
Some (`Tcp, [ `Tls (tls, ip, if port = 53 then 853 else port);
`Plaintext (ip, port) ])
in
Term.(const ns $ no_tls $ tls_ca_file $ tls_ca_dir $ tls_cert_fp $ tls_key_fp $ tls_hostname $ arg_ns $ arg_port)
let arg_domains : [ `raw ] Domain_name.t list Term.t =
let doc = "Domain names to operate on" in
Arg.(non_empty & pos_all Dns_cli.domain_name_c []
& info [] ~docv:"DOMAIN(s)" ~doc)
let arg_selector : string Term.t =
let doc = "DKIM selector string" in
Arg.(required & opt (some string) None
& info ["selector"] ~docv:"SELECTOR" ~doc)
let cmd_a : unit Cmd.t =
let doc = "Query a NS for A records" in
let man = [
`P {| Output mimics that of $(b,dig A )$(i,DOMAIN)|}
] in
let term =
Term.(term_result (const do_a $ nameserver $ arg_domains $ setup_log))
and info =
Cmd.info "a" ~version:(Manpage.escape "v10.2.2") ~man ~doc ~sdocs
in
Cmd.v info term
let cmd_aaaa : unit Cmd.t =
let doc = "Query a NS for AAAA records" in
let man = [
`P {| Output mimics that of $(b,dig AAAA )$(i,DOMAIN)|}
] in
let term =
Term.(term_result (const do_aaaa $ nameserver $ arg_domains $ setup_log))
and info =
Cmd.info "aaaa" ~version:(Manpage.escape "v10.2.2") ~man ~doc ~sdocs
in
Cmd.v info term
let cmd_mx : unit Cmd.t =
let doc = "Query a NS for mailserver (MX) records" in
let man = [
`P {| Output mimics that of $(b,dig MX )$(i,DOMAIN)|}
] in
let term =
Term.(term_result (const do_mx $ nameserver $ arg_domains $ setup_log))
and info =
Cmd.info "mx" ~version:(Manpage.escape "v10.2.2") ~man ~doc ~sdocs
in
Cmd.v info term
let cmd_tlsa : unit Cmd.t =
let doc = "Query a NS for TLSA records (see DANE / RFC 7671)" in
let man = [
`S Manpage.s_arguments ;
`S Manpage.s_description ;
`P {|Note that you must specify which $(b,service name)
you want to retrieve the key(s) of.
To retrieve the $(b,HTTPS) cert of $(i,www.example.com),
you would query the NS:
$(mname) $(tname) $(b,_443._tcp.)$(i,www.example.com)
|} ;
`P {|Brief list of other handy service name prefixes:|};
`P {| $(b,_5222._tcp) (XMPP); |} ;
`P {| $(b,_853._tcp) (DNS-over-TLS); |} ;
`P {| $(b,_25._tcp) (SMTP with STARTTLS); |} ;
`P {| $(b,_465._tcp)(SMTP); |} ;
`P {| $(b,_993._tcp) (IMAP) |} ;
`S Manpage.s_options ;
] in
let term =
Term.(term_result (const do_tlsa $ nameserver $ arg_domains $ setup_log))
and info =
Cmd.info "tlsa" ~version:(Manpage.escape "v10.2.2") ~man ~doc ~sdocs
in
Cmd.v info term
let cmd_txt : unit Cmd.t =
let doc = "Query a NS for TXT records" in
let man = [
`S Manpage.s_arguments ;
`S Manpage.s_description ;
`P {| Output format is currently: $(i,{TTL}: {text escaped in OCaml format})
It would be nice to mirror `dig` output here.|} ;
`S Manpage.s_options ;
] in
let term =
Term.(term_result (const do_txt $ nameserver $ arg_domains $ setup_log))
and info =
Cmd.info "txt" ~version:(Manpage.escape "v10.2.2") ~man ~doc ~sdocs
in
Cmd.v info term
let cmd_any : unit Cmd.t =
let doc = "Query a NS for ANY records" in
let man = [
`S Manpage.s_arguments ;
`S Manpage.s_description ;
`P {| The output will be fairly similar to $(b,dig ANY )$(i,example.com)|} ;
`S Manpage.s_options ;
] in
let term =
Term.(term_result (const do_any $ nameserver $ arg_domains $ setup_log))
and info =
Cmd.info "any" ~version:(Manpage.escape "v10.2.2") ~man ~doc ~sdocs
in
Cmd.v info term
let cmd_dkim : unit Cmd.t =
let doc = "Query a NS for DKIM (RFC 6376) records for a given selector" in
let man = [
`S Manpage.s_arguments ;
`S Manpage.s_description ;
`S {| Looks up DKIM (DomainKeys Identified Mail) Signatures in
accordance with RFC 6376.
Basically it's a recursive TXT lookup on
$(i,SELECTOR)._domainkeys.$(i,DOMAIN).
Each key is printed on its own concatenated line.
|} ;
`S Manpage.s_options ;
] in
let term =
Term.(term_result (const do_dkim $ nameserver $ arg_selector
$ arg_domains $ setup_log))
and info =
Cmd.info "dkim" ~version:(Manpage.escape "v10.2.2") ~man ~doc ~sdocs
in
Cmd.v info term
let arg_typ : int Term.t =
let doc = "Type to query" in
Arg.(required & opt (some int) None
& info ["type"] ~docv:"TYPE" ~doc)
let cmd_type : unit Cmd.t =
let doc = "Query a NS for a type, providing its integer number" in
let term =
Term.(term_result (const do_type $ nameserver $ arg_typ
$ arg_domains $ setup_log))
and info =
Cmd.info "type" ~version:(Manpage.escape "v10.2.2") ~doc ~sdocs
in
Cmd.v info term
let cmd_loc : unit Cmd.t =
let doc = "Query a NS for LOC records" in
let term =
Term.(term_result (const do_loc $ nameserver $ arg_domains $ setup_log))
and info =
Cmd.info "loc" ~version:(Manpage.escape "v10.2.2") ~doc ~sdocs
in
Cmd.v info term
let cmd_help : 'a Term.t =
let help _ = `Help (`Pager, None) in
Term.(ret (const help $ setup_log))
let cmds =
[ cmd_a ; cmd_tlsa; cmd_txt ; cmd_any; cmd_dkim ; cmd_aaaa ; cmd_mx ; cmd_type ; cmd_loc ]
let () =
let doc = "OCaml uDns alternative to `dig`" in
let man = [
`P {|For more information about the available subcommands,
run them while passing the help flag: $(tname) $(i,SUBCOMMAND) $(b,--help)
|}
] in
let info =
Cmd.info "odns" ~version:(Manpage.escape "v10.2.2") ~man ~doc ~sdocs
in
let group = Cmd.group ~default:cmd_help info cmds in
exit (Cmd.eval group)

View file

@ -0,0 +1,201 @@
open Lwt.Infix
open Dns
let ( let* ) = Result.bind
let pp_zone ppf (domain, query_type, query_value) =
Fmt.string ppf
(Rr_map.text_b domain (Rr_map.B (query_type, query_value)))
let pp_nameserver ppf = function
| `Plaintext (ip, port) -> Fmt.pf ppf "TCP %a:%d" Ipaddr.pp ip port
| `Tls (tls_cfg, ip, port) ->
Fmt.pf ppf "TLS %a:%d%a" Ipaddr.pp ip port
Fmt.(option ~none:(any "") (append (any "#") Domain_name.pp))
((Tls.Config.of_client tls_cfg).Tls.Config.peer_name)
let jump () hostname typ ns =
match Dns.Rr_map.of_string typ with
| Ok K k ->
Lwt_main.run (
let edns = Edns.create ~dnssec_ok:true ~payload_size:4096 () in
let nameservers = match ns with
| None -> None
| Some ip -> Some (`Tcp, [ `Plaintext (ip, 53) ])
in
let happy_eyeballs = Happy_eyeballs_lwt.create () in
let t = Dns_client_lwt.create ?nameservers ~edns:(`Manual edns) happy_eyeballs in
let (_, ns) = Dns_client_lwt.nameservers t in
Logs.info (fun m -> m "querying NS %a for A records of %a"
pp_nameserver (List.hd ns) Domain_name.pp hostname);
let log_err = function
| `Msg msg ->
Logs.err (fun m -> m "error from resolver %s" msg);
Error (`Msg "bad request")
| `Partial ->
Logs.err (fun m -> m "partial from resolver");
Error (`Msg "partial")
| #Dnssec.err as e ->
Logs.err (fun m -> m "dnssec error %a" Dnssec.pp_err e);
Error (`Msg "error")
in
let now = Ptime_clock.now () in
let retrieve_dnskey dnskeys ds_set requested_domain =
Dns_client_lwt.(get_raw_reply t Dnskey requested_domain) >|= function
| Error e -> log_err e
| Ok reply ->
let keys =
match reply with
| `Answer (answer, _) ->
Option.map
(fun (_, keys) ->
let valid_keys =
Rr_map.Ds_set.fold (fun ds acc ->
match Dnssec.validate_ds requested_domain keys ds with
| Ok key -> Rr_map.Dnskey_set.add key acc
| Error `Msg msg ->
Logs.warn (fun m -> m "couldn't validate DS (for %a): %s"
Domain_name.pp requested_domain msg);
acc
| Error `Extended e ->
Logs.warn (fun m -> m "couldn't validate DS (for %a): %a"
Domain_name.pp requested_domain
Extended_error.pp e);
acc)
ds_set Rr_map.Dnskey_set.empty
in
Logs.debug (fun m -> m "found %d DNSKEYS with matching DS"
(Rr_map.Dnskey_set.cardinal valid_keys));
valid_keys)
(Name_rr_map.find requested_domain Dnskey answer)
| _ -> None
in
let keys = Option.value ~default:dnskeys keys in
match Dnssec.verify_reply now keys requested_domain Dnskey reply with
| Error (`No_domain _ | `No_data _) ->
Logs.warn (fun m -> m "no DNSKEY for %a"
Domain_name.pp requested_domain);
Error (`Msg (Fmt.str "missing DNSKEY for %a"
Domain_name.pp requested_domain))
| Error e -> log_err e
| Ok (_, keys) ->
Logs.info (fun m -> m "verified RRSIG for DNSKEYS");
let keys =
Rr_map.Dnskey_set.filter
(fun k -> Dnskey.F.mem `Zone k.Dnskey.flags)
keys
in
Ok keys
in
let retrieve_ds dnskeys name =
Dns_client_lwt.(get_raw_reply t Ds name) >|= function
| Error e -> log_err e
| Ok reply ->
match Dnssec.verify_reply ~follow_cname:false now dnskeys name Ds reply with
| Ok (_, ds) -> Ok (Some ds)
| Error (`No_domain _ | `No_data _) ->
Logs.warn (fun m -> m "no data or no domain for DS in %a"
Domain_name.pp name);
Ok None
| Error (`Cname a) ->
Logs.warn (fun m -> m "cname alias for %a (DS) to %a"
Domain_name.pp name
Domain_name.pp a);
Ok None
| Error e->
log_err e
in
let rec retrieve_validated_dnskeys hostname =
Logs.info (fun m -> m "validating and retrieving DNSKEYS for %a" Domain_name.pp hostname);
if Domain_name.equal hostname Domain_name.root then begin
Logs.info (fun m -> m "retrieving DNSKEYS for %a" Domain_name.pp hostname);
retrieve_dnskey Rr_map.Dnskey_set.empty Dnssec.root_ds hostname
end else
let open Lwt_result.Infix in
retrieve_validated_dnskeys Domain_name.(drop_label_exn hostname) >>= fun parent_dnskeys ->
Logs.info (fun m -> m "retrieving DS for %a" Domain_name.pp hostname);
retrieve_ds parent_dnskeys hostname >>= function
| Some ds_set ->
(* following 4509 - if there's a sha2 DS, drop sha1 ones *)
let ds_set' =
if
Rr_map.Ds_set.exists
(fun ds ->
match ds.Ds.digest_type with
| Ds.SHA256 | Ds.SHA384 -> true
| _ -> false)
ds_set
then
Rr_map.Ds_set.filter
(fun ds -> not (ds.Ds.digest_type = Ds.SHA1))
ds_set
else
ds_set
in
if Rr_map.Ds_set.cardinal ds_set > Rr_map.Ds_set.cardinal ds_set' then
Logs.warn (fun m -> m "dropped %d DS records (SHA1)"
(Rr_map.Ds_set.cardinal ds_set' - Rr_map.Ds_set.cardinal ds_set));
Logs.info (fun m -> m "retrieving DNSKEYS for %a" Domain_name.pp hostname);
retrieve_dnskey parent_dnskeys ds_set' hostname
| None ->
Logs.info (fun m -> m "no DS for %a, continuing with old keys" Domain_name.pp hostname);
Lwt.return (Ok parent_dnskeys)
in
retrieve_validated_dnskeys hostname >>= function
| Error _ as e -> Lwt.return e
| Ok dnskeys ->
Dns_client_lwt.(get_raw_reply t k hostname) >|= function
| Error e -> log_err e
| Ok reply ->
match Dnssec.verify_reply now dnskeys hostname k reply with
| Ok rrs ->
Logs.app (fun m -> m "%a" pp_zone (hostname, k, rrs));
Ok ()
| Error (`No_domain _ | `No_data _) ->
Logs.warn (fun m -> m "no data or no domain for %a (%a)"
Domain_name.pp hostname Rr_map.ppk (K k));
Ok ()
| Error e -> log_err e
)
| _ -> Error (`Msg "couldn't decode type")
open Cmdliner
let parse_domain : [ `raw ] Domain_name.t Arg.conv =
Arg.conv'
((fun name ->
Result.map_error
(function `Msg m -> Fmt.str "Invalid domain: %S: %s" name m)
(Domain_name.of_string name)),
Domain_name.pp)
let arg_domain : [ `raw ] Domain_name.t Term.t =
let doc = "Host to operate on" in
Arg.(value & opt parse_domain (Domain_name.of_string_exn "cloudflare.com")
& info [ "host" ] ~docv:"HOST" ~doc)
let parse_ip =
Arg.conv'
((fun s ->
match Ipaddr.of_string s with
| Ok ip -> Ok ip
| Error (`Msg m) -> Error ("failed to parse IP address: " ^ m)),
Ipaddr.pp)
let nameserver : Ipaddr.t option Term.t =
let doc = "Nameserver to use" in
Arg.(value & opt (some parse_ip) None & info [ "nameserver" ] ~docv:"NAMESERVER" ~doc)
let arg_typ : string Term.t =
let doc = "Type to query" in
Arg.(value & opt string "A" & info ["type"] ~docv:"TYPE" ~doc)
let cmd =
let term =
Term.(term_result (const jump $ Dns_cli.setup_log $ arg_domain $ arg_typ $ nameserver))
and info = Cmd.info "odnssec" ~version:"10.2.2"
in
Cmd.v info term
let () = exit (Cmd.eval cmd)

View file

@ -0,0 +1,95 @@
(* (c) 2019 Hannes Mehnert, all rights reserved *)
open Dns
let notify zone serial key now =
let raw_zone = Domain_name.raw zone in
let question = Packet.Question.create raw_zone Soa
and soa =
{ Soa.nameserver = raw_zone ; hostmaster = raw_zone ; serial ;
refresh = 0l; retry = 0l ; expiry = 0l ; minimum = 0l }
and header = Randomconv.int16 Mirage_crypto_rng.generate, Packet.Flags.singleton `Authoritative
in
let p = Packet.create header question (`Notify (Some soa)) in
match key with
| None -> Ok (p, fst (Packet.encode `Tcp p), None)
| Some (keyname, _, dnskey) ->
Logs.debug (fun m -> m "signing with key %a: %a" Domain_name.pp keyname Dnskey.pp dnskey) ;
match Dns_tsig.encode_and_sign ~proto:`Tcp p now dnskey keyname with
| Ok (cs, mac) -> Ok (p, cs, Some mac)
| Error e -> Error e
let jump _ serverip port zone key serial =
Mirage_crypto_rng_unix.use_default ();
let now = Ptime_clock.now () in
Logs.app (fun m -> m "notifying to %a:%d zone %a serial %lu"
Ipaddr.pp serverip port Domain_name.pp zone serial) ;
match notify zone serial key now with
| Error s -> Error (`Msg (Fmt.str "signing %a" Dns_tsig.pp_s s))
| Ok (request, data, mac) ->
let data_len = String.length data in
Logs.debug (fun m -> m "built data %d" data_len) ;
let socket = Dns_cli.connect_tcp serverip port in
Dns_cli.send_tcp socket data ;
let read_data = Dns_cli.recv_tcp socket in
Unix.close socket ;
match key with
| None ->
begin match Packet.decode read_data with
| Ok reply ->
begin match Packet.reply_matches_request ~request reply with
| Ok `Notify_ack ->
Logs.app (fun m -> m "successful notify!") ;
Ok ()
| Ok r -> Error (`Msg (Fmt.str "expected notify ack, got %a" Packet.pp_reply r))
| Error e -> Error (`Msg (Fmt.str "notify reply %a is not ok %a"
Packet.pp reply Packet.pp_mismatch e))
end
| Error e ->
Error (`Msg (Fmt.str "failed to decode notify reply! %a" Packet.pp_err e))
end
| Some (keyname, _, dnskey) ->
match Dns_tsig.decode_and_verify now dnskey keyname ?mac read_data with
| Error e ->
Error (`Msg (Fmt.str "failed to decode TSIG signed notify reply! %a" Dns_tsig.pp_e e))
| Ok (reply, _, _) ->
match Packet.reply_matches_request ~request reply with
| Ok `Notify_ack ->
Logs.app (fun m -> m "successful TSIG signed notify!") ;
Ok ()
| Ok r -> Error (`Msg (Fmt.str "expected notify ack, got %a" Packet.pp_reply r))
| Error e ->
Error (`Msg (Fmt.str "expected reply to %a %a, got %a!"
Packet.pp_mismatch e
Packet.pp request Packet.pp reply))
open Cmdliner
let serverip =
let doc = "IP address of DNS server" in
Arg.(required & pos 0 (some Dns_cli.ip_c) None & info [] ~doc ~docv:"SERVERIP")
let port =
let doc = "Port to connect to" in
Arg.(value & opt int 53 & info [ "port" ] ~doc)
let serial =
let doc = "Serial number" in
Arg.(value & opt int32 1l & info [ "serial" ] ~doc)
let key =
let doc = "DNS HMAC secret (name:alg:b64key)" in
Arg.(value & opt (some Dns_cli.namekey_c) None & info [ "key" ] ~doc ~docv:"KEY")
let zone =
let doc = "Zone to notify" in
Arg.(required & pos 1 (some Dns_cli.name_c) None & info [] ~doc ~docv:"ZONE")
let cmd =
let info = Cmd.info "onotify" ~version:"10.2.2"
and term =
Term.(term_result (const jump $ Dns_cli.setup_log $ serverip $ port $ zone $ key $ serial))
in
Cmd.v info term
let () = exit (Cmd.eval cmd)

View file

@ -0,0 +1,91 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
open Dns
let create_update zone hostname ip_address =
let zone = Packet.Question.create zone Soa
and update =
let up =
Domain_name.Map.singleton hostname
[
Packet.Update.Remove (Rr_map.K A) ;
Packet.Update.Add Rr_map.(B (A, (60l, Ipaddr.V4.Set.singleton ip_address)))
]
in
(Domain_name.Map.empty, up)
and header = Randomconv.int16 Mirage_crypto_rng.generate, Packet.Flags.empty
in
Packet.create header zone (`Update update)
let jump _ serverip port (keyname, zone, dnskey) hostname ip_address =
Mirage_crypto_rng_unix.use_default ();
let now = Ptime_clock.now () in
Logs.app (fun m -> m "updating to %a:%d zone %a A 600 %a %a"
Ipaddr.pp serverip port
Domain_name.pp zone
Domain_name.pp hostname
Ipaddr.V4.pp ip_address) ;
Logs.debug (fun m -> m "using key %a: %a" Domain_name.pp keyname Dns.Dnskey.pp dnskey) ;
let p = create_update zone hostname ip_address in
match Dns_tsig.encode_and_sign ~proto:`Tcp p now dnskey keyname with
| Error s ->
Error (`Msg (Fmt.str "tsig sign error %a" Dns_tsig.pp_s s))
| Ok (data, mac) ->
let data_len = String.length data in
Logs.debug (fun m -> m "built data %d" data_len) ;
let socket = Dns_cli.connect_tcp serverip port in
Dns_cli.send_tcp socket data ;
let read_data = Dns_cli.recv_tcp socket in
(try (Unix.close socket) with _ -> ()) ;
match Dns_tsig.decode_and_verify now dnskey keyname ~mac read_data with
| Error e ->
Error (`Msg (Fmt.str "nsupdate error %a" Dns_tsig.pp_e e))
| Ok (reply, _, _) ->
match Packet.reply_matches_request ~request:p reply with
| Ok `Update_ack ->
Logs.app (fun m -> m "successful and signed update!") ;
Ok ()
| Ok r ->
Error (`Msg (Fmt.str "nsupdate expected update ack, received %a" Packet.pp_reply r))
| Error e ->
Error (`Msg (Fmt.str "nsupdate error %a (reply %a does not match request %a)"
Packet.pp_mismatch e Packet.pp reply Packet.pp p))
open Cmdliner
let serverip =
let doc = "IP address of DNS server" in
Arg.(required & pos 0 (some Dns_cli.ip_c) None & info [] ~doc ~docv:"SERVERIP")
let port =
let doc = "Port to connect to" in
Arg.(value & opt int 53 & info [ "port" ] ~doc)
let key =
let doc = "DNS HMAC secret (name:alg:b64key where name is yyy._update.zone)" in
Arg.(required & pos 1 (some Dns_cli.namekey_c) None & info [] ~doc ~docv:"KEY")
let hostname =
let doc = "Hostname to modify" in
Arg.(required & pos 2 (some Dns_cli.domain_name_c) None & info [] ~doc ~docv:"HOSTNAME")
let ipv4_c =
Arg.conv'
((fun s ->
match Ipaddr.V4.of_string s with
| Ok ip -> Ok ip
| Error (`Msg m) -> Error ("failed to parse IP address: " ^ m)),
Ipaddr.V4.pp)
let ip_address =
let doc = "New IP address" in
Arg.(required & pos 3 (some ipv4_c) None & info [] ~doc ~docv:"IP")
let cmd =
let term =
Term.(term_result (const jump $ Dns_cli.setup_log $ serverip $ port $ key $ hostname $ ip_address))
and info = Cmd.info "oupdate" ~version:"10.2.2"
in
Cmd.v info term
let () = exit (Cmd.eval cmd)

View file

@ -0,0 +1,72 @@
(* (c) 2019 Hannes Mehnert, all rights reserved *)
(* goal is to check a given zonefile whether it is valid (and to-be-used
by an authoritative NS - i.e. there must be a SOA record, TTL are good)
if a NS/MX name is within the zone, it needs an address record
the name of the file is taken as the domain name *)
open Dns
let ( let* ) = Result.bind
let load_zone zone =
let* data = Bos.OS.File.read Fpath.(v zone) in
let* rrs = Dns_zone.parse data in
let domain = Domain_name.of_string_exn Fpath.(basename (v zone)) in
let bad = Domain_name.Map.filter
(fun name _ -> not (Domain_name.is_subdomain ~domain ~subdomain:name))
rrs
in
if not (Domain_name.Map.is_empty bad) then
Error (`Msg (Fmt.str "Entries of domain '%a' are not in its zone, won't handle this:@.%a"
Domain_name.pp domain Dns.Name_rr_map.pp bad))
else
Ok (Dns_trie.insert_map rrs Dns_trie.empty)
let jump _ zone old =
let* trie = load_zone zone in
let* () =
Result.map_error
(fun e -> `Msg (Fmt.to_to_string Dns_trie.pp_zone_check e))
(Dns_trie.check trie)
in
Logs.app (fun m -> m "successfully checked zone") ;
let zones =
Dns_trie.fold Soa trie
(fun name _ acc -> Domain_name.Set.add name acc)
Domain_name.Set.empty
in
if Domain_name.Set.cardinal zones = 1 then
let zone = Domain_name.Set.choose zones in
let* zone_data = Dns_server.text zone trie in
Logs.debug (fun m -> m "assembled zone data %s" zone_data) ;
(match old with
| None -> Ok ()
| Some fn ->
let* old = load_zone fn in
match Dns_trie.lookup zone Soa trie, Dns_trie.lookup zone Soa old with
| Ok fresh, Ok old when Soa.newer ~old fresh ->
Logs.debug (fun m -> m "zone %a newer than old" Domain_name.pp zone) ;
Ok ()
| _ ->
Error (`Msg "SOA comparison wrong"))
else
Error (`Msg "expected exactly one zone")
open Cmdliner
let newzone =
let doc = "New zone file" in
Arg.(required & pos 0 (some file) None & info [] ~doc ~docv:"ZONE")
let oldzone =
let doc = "Old zone file" in
Arg.(value & opt (some file) None & info [ "old" ] ~doc ~docv:"ZONE")
let cmd =
let term =
Term.(term_result (const jump $ Dns_cli.setup_log $ newzone $ oldzone))
and info = Cmd.info "ozone" ~version:"10.2.2"
in
Cmd.v info term
let () = exit (Cmd.eval cmd)

View file

@ -0,0 +1,97 @@
module Resolver = Dns_resolver_mirage.Make(Tcpip_stack_socket.V4V6)
open Lwt.Infix
let pp_val ppf f =
let open Metrics in
match value f with
| V (String, s) -> Fmt.pf ppf "%S" s
| V (Int, i) -> Fmt.pf ppf "%d" i
| V (Int32, i32) -> Fmt.pf ppf "%ld" i32
| V (Int64, i64) -> Fmt.pf ppf "%Ld" i64
| V (Uint, u) -> Fmt.pf ppf "%u" u
| V (Uint32, u32) -> Fmt.pf ppf "%lu" u32
| V (Uint64, u64) -> Fmt.pf ppf "%Lu" u64
| _ -> pp_value ppf f
let print_resolver_stats () =
let map = Metrics.get_cache () in
let dns_resolver_src =
List.find (fun src -> Metrics.Src.name src = "dns-resolver") (Metrics.Src.list ())
in
let dns_resolver_metrics =
match Metrics.SM.find_opt dns_resolver_src map with
| None ->
print_endline "no dns-resolver found";
[]
| Some ms ->
List.concat_map (fun (_tags, data) -> Metrics.Data.fields data) ms
in
List.iter (fun field ->
Logs.app (fun m -> m "%s %a" (Metrics.key field) pp_val field))
dns_resolver_metrics;
exit 130
let main dnssec qname_min opportunistic =
Mirage_crypto_rng_unix.use_default ();
let reporter = Metrics.cache_reporter () in
Metrics.set_reporter reporter;
Metrics.enable_all ();
Udpv4v6_socket.connect ~ipv4_only:true ~ipv6_only:false Ipaddr.V4.Prefix.global None >>= fun udp ->
Tcpv4v6_socket.connect ~ipv4_only:true ~ipv6_only:false Ipaddr.V4.Prefix.global None >>= fun tcp ->
Tcpip_stack_socket.V4V6.connect udp tcp >>= fun stack ->
let resolver =
let primary_t =
(* setup DNS server state: *)
Dns_server.Primary.create ~rng:Mirage_crypto_rng.generate Dns_trie.empty
in
let features =
(if dnssec then [ `Dnssec ] else []) @
(if qname_min then [ `Qname_minimisation ] else []) @
(if opportunistic then [ `Opportunistic_tls_authoritative ] else [])
in
Dns_resolver.create features ~ip_protocol:`Ipv4_only
(Mirage_mtime.elapsed_ns ()) Mirage_crypto_rng.generate primary_t
in
let _resolver = Resolver.resolver ~port:53530 stack resolver in
let _ : Sys.signal_behavior =
Sys.signal Sys.sigint
(Signal_handle
(fun _ -> print_resolver_stats ()))
in
Tcpip_stack_socket.V4V6.listen stack >|= fun () ->
Ok ()
let jump () dnssec qname_min opportunistic =
Lwt_main.run (main dnssec qname_min opportunistic)
open Cmdliner
let dnssec =
let doc =
Arg.info ~doc:"Validate DNS replies and cache DNSSEC data." [ "dnssec" ]
in
Arg.(value & flag doc)
let qname_minimisation =
let doc =
Arg.info ~doc:"Use qname minimisation (RFC 9156)." [ "qname-minimisation" ]
in
Arg.(value & flag doc)
let opportunistic_tls =
let doc =
Arg.info ~doc:"Use opportunistic TLS from recursive resolver to authoriative (RFC 9539)."
[ "opportunistic-tls-authoritative" ]
in
Arg.(value & flag doc)
let cmd =
let term =
Term.(term_result (const jump $ Dns_cli.setup_log $ dnssec $ qname_minimisation $ opportunistic_tls))
and info = Cmd.info "resolver" ~version:"10.2.2"
in
Cmd.v info term
let () = exit (Cmd.eval cmd)

View file

@ -0,0 +1,378 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Dns
let src = Logs.Src.create "dns_cache" ~doc:"DNS cache"
module Log = (val Logs.src_log src : Logs.LOG)
type rank =
| ZoneFile
| ZoneTransfer
| AuthoritativeAnswer of Rrsig.t option
| AuthoritativeAuthority of Rrsig.t option
| ZoneGlue
| NonAuthoritativeAnswer
| Additional
let compare_rrsig_opt a b =
match a, b with
| None, None -> 0
| Some _, None -> 1
| None, Some _ -> -1
| Some a, Some b ->
Ptime.compare a.Rrsig.signature_expiration b.Rrsig.signature_expiration
let compare_rank a b = match a, b with
| ZoneFile, ZoneFile -> 0
| ZoneFile, _ -> 1
| _, ZoneFile -> -1
| ZoneTransfer, ZoneTransfer -> 0
| ZoneTransfer, _ -> 1
| _, ZoneTransfer -> -1
| AuthoritativeAnswer signed, AuthoritativeAnswer signed' ->
compare_rrsig_opt signed signed'
| AuthoritativeAnswer _, _ -> 1
| _, AuthoritativeAnswer _ -> -1
| AuthoritativeAuthority signed, AuthoritativeAuthority signed' ->
compare_rrsig_opt signed signed'
| AuthoritativeAuthority _, _ -> 1
| _, AuthoritativeAuthority _ -> -1
| ZoneGlue, ZoneGlue -> 0
| ZoneGlue, _ -> 1
| _, ZoneGlue -> -1
| NonAuthoritativeAnswer, NonAuthoritativeAnswer -> 0
| NonAuthoritativeAnswer, _ -> 1
| _, NonAuthoritativeAnswer -> -1
| Additional, Additional -> 0
let pp_rank ppf = function
| ZoneFile -> Fmt.string ppf "zone file data"
| ZoneTransfer -> Fmt.string ppf "zone transfer data"
| AuthoritativeAnswer signed ->
Fmt.pf ppf "authoritative answer data (signed: %a)"
Fmt.(option ~none:(any "no") Rrsig.pp) signed
| AuthoritativeAuthority signed ->
Fmt.pf ppf "authoritative authority data (signed: %a)"
Fmt.(option ~none:(any "no") Rrsig.pp) signed
| ZoneGlue -> Fmt.string ppf "zone file glue"
| NonAuthoritativeAnswer -> Fmt.string ppf "non-authoritative answer"
| Additional -> Fmt.string ppf "additional data"
type 'a entry = [
| `Entry of 'a
| `No_data of [ `raw ] Domain_name.t * Soa.t
| `No_domain of [ `raw ] Domain_name.t * Soa.t
| `Serv_fail of [ `raw ] Domain_name.t * Soa.t
]
module RRMap = Map.Make(struct
type t = Rr_map.k
let compare = Rr_map.comparek
end)
module Entry = struct
type meta = int64 * rank
let pp_meta ppf (ts, rank) =
Fmt.pf ppf "%a created %Lu" pp_rank rank ts
type rr_map_entry =
| Entry of Rr_map.b
| No_data of [ `raw ] Domain_name.t * Soa.t
| Serv_fail of [ `raw ] Domain_name.t * Soa.t
let pp_map_entry ppf entry = match entry with
| Entry b -> Fmt.pf ppf "entry %a" Rr_map.pp_b b
| No_data (name, soa) -> Fmt.pf ppf "no data %a SOA %a" Domain_name.pp name Soa.pp soa
| Serv_fail (name, soa) -> Fmt.pf ppf "server fail %a SOA %a" Domain_name.pp name Soa.pp soa
let to_entry : type a. a Rr_map.key -> rr_map_entry -> a entry = fun typ r -> match r with
| Entry (B (k, v)) -> begin match Rr_map.K.compare typ k with Gmap.Order.Eq -> `Entry v | _ -> assert false end
| No_data (name, soa) -> `No_data (name, soa)
| Serv_fail (name, soa) -> `Serv_fail (name, soa)
let of_entry typ = function
| `Entry v -> Entry (B (typ, v))
| `No_data (name, soa) -> No_data (name, soa)
| `Serv_fail (name, soa) -> Serv_fail (name, soa)
| _ -> assert false
type t =
| No_domain of meta * [ `raw ] Domain_name.t * Soa.t
| Rr_map of (meta * rr_map_entry) RRMap.t
(* Part of the LRU.Weighted interface *)
let weight = function
| No_domain _ -> 1
| Rr_map tm -> RRMap.cardinal tm
let pp_entry ppf (meta, entry) = Fmt.pf ppf "e (%a) %a" pp_meta meta pp_map_entry entry
let pp ppf = function
| No_domain (meta, name, soa) ->
Fmt.pf ppf "no domain (%a) %a SOA %a" pp_meta meta Domain_name.pp name Soa.pp soa
| Rr_map rr ->
Fmt.pf ppf "entries: %a"
Fmt.(list ~sep:(any ";@,") (pair Rr_map.ppk pp_entry))
(RRMap.bindings rr)
end
module Key = struct
type t = [ `raw ] Domain_name.t
let compare = Domain_name.compare
end
module LRU = Lru.F.Make(Key)(Entry)
type t = LRU.t
let metrics =
let f = function
| `Lookup -> "lookups"
| `Hit -> "hits"
| `Miss -> "misses"
| `Drop -> "drops"
| `Insert -> "insertions"
in
let incr, get = create_counter ~f in
let data (cache, thing) =
incr thing;
Metrics.Data.v
(Metrics.uint "size" (LRU.size cache) ::
Metrics.uint "weight" (LRU.weight cache) ::
Metrics.uint "capacity" (LRU.capacity cache) ::
get ())
in
let src = Metrics.Src.v ~tags:Metrics.Tags.[] ~data "dns-cache" in
(fun cache r -> Metrics.add src (fun x -> x) (fun d -> d (cache, r)))
let empty = LRU.empty
let size = LRU.size
let capacity = LRU.capacity
let pp = LRU.pp Fmt.(pair ~sep:(any ": ") Domain_name.pp Entry.pp)
module N = Domain_name.Set
let compute_updated_ttl ~created ~now ttl =
Int32.sub ttl (Int32.of_int (Duration.to_sec (Int64.sub now created)))
let pp_entry key ppf entry =
let pp_ns ppf (name, soa) = Fmt.pf ppf "%a SOA %a" Domain_name.pp name Soa.pp soa in
match entry with
| `Entry v -> Fmt.pf ppf "entry %a" Rr_map.pp_b (B (key, v))
| `No_data ns -> Fmt.(append (any "no data ") pp_ns) ppf ns
| `No_domain ns -> Fmt.(append (any "no domain ") pp_ns) ppf ns
| `Serv_fail ns -> Fmt.(append (any "serv fail ") pp_ns) ppf ns
let get_ttl k = function
| `Entry v -> Rr_map.ttl k v
| `No_data (_, soa) -> soa.Soa.minimum
| `No_domain (_, soa) -> soa.Soa.minimum
| `Serv_fail (_, soa) -> soa.Soa.minimum
let with_ttl : type a . a Rr_map.key -> int32 -> a entry -> a entry = fun k ttl r -> match r with
| `Entry v ->
let v' = Rr_map.with_ttl k v ttl in
`Entry v'
| `No_data (name, soa) -> `No_data (name, { soa with Soa.minimum = ttl })
| `No_domain (name, soa) -> `No_domain (name, { soa with Soa.minimum = ttl })
| `Serv_fail (name, soa) -> `Serv_fail (name, { soa with Soa.minimum = ttl })
let rec no_dom_upwards cache name =
if Domain_name.count_labels name > 1 then
let name' = Domain_name.drop_label_exn name in
match LRU.find name' cache with
| None -> no_dom_upwards cache name'
| Some No_domain (meta, name, soa) -> None, Ok (meta, `No_domain (name, soa))
| Some _ -> None, Error `Cache_miss
else
None, Error `Cache_miss
let find cache name query_type =
match LRU.find name cache with
| None -> no_dom_upwards cache name
| Some No_domain (meta, name, soa) -> None, Ok (meta, `No_domain (name, soa))
| Some Rr_map resource_records ->
Some resource_records,
match RRMap.find_opt (K query_type) resource_records with
| Some (meta, entry) -> Ok (meta, Entry.to_entry query_type entry)
| None -> Error `Cache_miss
let insert cache ?map ts name query_type rank entry =
let meta = ts, rank in
let cache = match entry with
| `No_domain (name', soa) -> LRU.add name (No_domain (meta, name', soa)) cache
| `Entry _ | `No_data _ | `Serv_fail _ ->
let map = match map with None -> RRMap.empty | Some x -> x in
let map' = RRMap.add (K query_type) (meta, Entry.of_entry query_type entry) map in
LRU.add name (Rr_map map') cache
in
(* Make sure we are within memory bounds *)
LRU.trim cache
let update_ttl typ entry ~created ~now =
let ttl = get_ttl typ entry in
let updated_ttl = compute_updated_ttl ~created ~now ttl in
if updated_ttl < 0l then Error `Cache_drop else Ok (with_ttl typ updated_ttl entry)
let get cache ts name query_type =
metrics cache `Lookup;
match snd (find cache name query_type) with
| Error e -> metrics cache `Miss; cache, Error e
| Ok ((created, rank), entry) ->
match update_ttl query_type entry ~created ~now:ts with
| Ok entry' -> metrics cache `Hit; LRU.promote name cache, Ok (entry', rank)
| Error e -> metrics cache `Drop; cache, Error e
let find_any cache name =
match LRU.find name cache with
| None -> Error `Cache_miss
| Some No_domain (meta, name, soa) -> Ok (`No_domain (meta, name, soa))
| Some Rr_map rrs -> Ok (`Entries rrs)
let get_any cache ts name =
metrics cache `Lookup;
match find_any cache name with
| Error e -> metrics cache `Miss; cache, Error e
| Ok r ->
let ttl created curr =
let ttl = compute_updated_ttl ~created ~now:ts curr in
if ttl < 0l then Error `Cache_drop else Ok ttl
in
LRU.promote name cache,
match r with
| `No_domain ((created, rank), name, soa) ->
begin match ttl created soa.Soa.minimum with
| Error _ as e -> metrics cache `Drop; e
| Ok minimum ->
metrics cache `Hit;
Ok (`No_domain (name, { soa with Soa.minimum }), rank)
end
| `Entries rrs ->
let rrs, r =
RRMap.fold (fun _k ((created, rank), v) (acc, r) ->
match v with
| Entry.Entry B (k, v) ->
begin match ttl created (Rr_map.ttl k v) with
| Ok ttl ->
let v' = Rr_map.with_ttl k v ttl in
Rr_map.add k v' acc, rank
| Error _ -> acc, r
end
| _ -> acc, r) rrs (Rr_map.empty, Additional)
in
match Rr_map.is_empty rrs with
| true -> metrics cache `Drop; Error `Cache_drop
| false -> metrics cache `Hit; Ok (`Entries rrs, r)
let get_or_cname : type a . t -> int64 -> [`raw] Domain_name.t -> a Rr_map.key ->
t * ([ a entry | `Alias of int32 * [`raw] Domain_name.t] * rank,
[ `Cache_drop | `Cache_miss ]) result =
fun cache ts name query_type ->
metrics cache `Lookup;
let map_result : _ -> t * ([ a entry | `Alias of int32 * [`raw] Domain_name.t] * rank, [ `Cache_drop | `Cache_miss ]) result = function
| Error e -> metrics cache `Miss; cache, Error e
| Ok ((created, rank), entry) ->
match update_ttl query_type entry ~created ~now:ts with
| Ok entry' -> metrics cache `Hit; LRU.promote name cache, Ok ((entry', rank) :> [ _ entry | `Alias of int32 * [`raw] Domain_name.t ] * rank)
| Error e -> metrics cache `Drop; cache, Error e
in
match find cache name query_type with
| Some map, r ->
begin match RRMap.find_opt (K Cname) map with
| Some ((created, rank), Entry.Entry (B (Cname, (ttl, name)))) ->
let ttl = compute_updated_ttl ~created ~now:ts ttl in
if ttl < 0l then
map_result r
else begin
metrics cache `Hit;
LRU.promote name cache, Ok (`Alias (ttl, name), rank)
end
| _ -> map_result r
end
| _, e -> map_result e
let get_nsec3 cache ts name =
metrics cache `Lookup;
let zone_labels = Domain_name.count_labels name in
let nsec3_rrs =
LRU.fold (fun ename entry acc ->
if
Domain_name.is_subdomain ~domain:name ~subdomain:ename &&
Domain_name.count_labels ename - 1 = zone_labels
then
match entry with
| Rr_map rrs ->
begin
match RRMap.find_opt (K Nsec3) rrs with
| Some ((created, r), (Entry (B (Nsec3, v)) as e)) ->
begin match update_ttl Nsec3 (Entry.to_entry Nsec3 e) ~created ~now:ts with
| Ok `Entry (ttl, _) -> (ename, ttl, snd v, r) :: acc
| Ok _ -> acc
| Error _ -> acc
end
| _ -> acc
end
| _ -> acc
else
acc)
[] cache
in
match nsec3_rrs with
| [] ->
metrics cache `Miss;
cache, Error `Cache_miss
| xs ->
metrics cache `Hit;
List.fold_right LRU.promote (List.map (fun (a, _, _, _) -> a) xs) cache,
Ok xs
(* XXX: we may want to define a minimum as well (5 minutes? 30 minutes?
use SOA expiry?) MS used to use 24 hours in internet explorer
from RFC1034 on this topic:
The idea is that if cached data is known to come from a particular zone,
and if an authoritative copy of the zone's SOA is obtained, and if the
zone's SERIAL has not changed since the data was cached, then the TTL of
the cached data can be reset to the zone MINIMUM value if it is smaller.
This usage is mentioned for planning purposes only, and is not
recommended as yet.
and 2308, Sec 4:
Despite being the original defined meaning, the first of these, the
minimum TTL value of all RRs in a zone, has never in practice been
used and is hereby deprecated.
and 1035 6.2:
The MINIMUM value in the SOA should be used to set a floor on the TTL of data
distributed from a zone. This floor function should be done when the data is
copied into a response. This will allow future dynamic update protocols to
change the SOA MINIMUM field without ambiguous semantics.
*)
(* according to RFC1035, section 7.3, a TTL of a week is a good
maximum value! *)
let week = Int32.of_int Duration.(to_sec (of_day 7))
let clip_ttl_to_week query_type entry =
let ttl = get_ttl query_type entry in
if ttl < week then entry else with_ttl query_type week entry
let pp_query ppf (name, query_type) =
Fmt.pf ppf "%a (%a)" Domain_name.pp name Packet.Question.pp_qtype query_type
let set cache ts name query_type rank entry =
let entry' = clip_ttl_to_week query_type entry in
let cache' map = insert cache ?map ts name query_type rank entry' in
match find cache name query_type with
| map, Error _ ->
Log.debug (fun m -> m "set: %a nothing found, adding: %a"
pp_query (name, `K (K query_type)) (pp_entry query_type) entry');
metrics cache `Insert; cache' map
| map, Ok ((created, rank'), entry) ->
Log.debug (fun m -> m "set: %a found rank %a insert rank %a: %d"
pp_query (name, `K (K query_type)) pp_rank rank' pp_rank rank (compare_rank rank' rank));
match update_ttl query_type entry ~created ~now:ts, compare_rank rank' rank with
| Ok _, 1 -> cache
| _ -> metrics cache `Insert; cache' map
let remove cache name =
LRU.remove name cache

View file

@ -0,0 +1,90 @@
(** DNS cache - a least recently used cache of DNS responses
This data structure allows to insert and retrieve entries into a least
recently used data structure. An [`Entry] weights the cardinality of the
resource record map, all other entries have a weight of 1.
The time to live is preserved, and when it is exceeded the entry is no
longer returned.
*)
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Dns
(** The variant of the rank in the cache. *)
type rank =
| ZoneFile
| ZoneTransfer
| AuthoritativeAnswer of Rrsig.t option
| AuthoritativeAuthority of Rrsig.t option
| ZoneGlue
| NonAuthoritativeAnswer
| Additional
val pp_rank : rank Fmt.t
(** [pp_rank ppf rank] pretty-prints the [rank] on [ppf]. *)
val compare_rank : rank -> rank -> int
(** [compare_rank a b] compares the ranks [a] with [b]. *)
(** The type of a DNS cache. *)
type t
val empty : int -> t
(** [empty maximum_size] is an empty DNS cache with the maximum size as
capacity. *)
val size : t -> int
(** [size cache] is the number of bindings currently in the [cache]. *)
val capacity : t -> int
(** [capacity cache] is the used weight. *)
val pp : t Fmt.t
(** [pp ppf t] pretty prints the cache [t] on [ppf]. *)
(** The polymorphic variant of an entry: a resource record, or no data,
no domain, or a server failure. *)
type 'a entry = [
| `Entry of 'a
| `No_data of [ `raw ] Domain_name.t * Soa.t
| `No_domain of [ `raw ] Domain_name.t * Soa.t
| `Serv_fail of [ `raw ] Domain_name.t * Soa.t
]
val pp_entry : 'a Rr_map.key -> 'a entry Fmt.t
(** [pp_entry ppf entry] pretty-prints [entry] on [ppf]. *)
val get : t -> int64 -> [ `raw ] Domain_name.t -> 'a Rr_map.key ->
t * ('a entry * rank, [ `Cache_miss | `Cache_drop ]) result
(** [get cache timestamp type name] retrieves the query [type, name] from the
[cache] using [timestamp]. If the time to live is exceeded, a [`Cache_drop]
is returned. If there is no entry in the cache, a [`Cache_miss] is
returned. *)
val get_or_cname : t -> int64 -> [ `raw ] Domain_name.t -> 'a Rr_map.key ->
t * ([ 'a entry | `Alias of int32 * [`raw] Domain_name.t] * rank,
[ `Cache_miss | `Cache_drop ]) result
(** [get_or_cname cache timestamp type name] is the same as [get], but if a
[`Cache_miss] is encountered, a lookup for an alias (CNAME) is done. *)
val get_any : t -> int64 -> [ `raw ] Domain_name.t ->
t * ([ `Entries of Rr_map.t
| `No_domain of [ `raw ] Domain_name.t * Soa.t ] * rank,
[ `Cache_miss | `Cache_drop ]) result
(** [get_any cache timestamp name] retrieves all resource records for [name]
in [cache]. *)
val get_nsec3 : t -> int64 -> [ `raw ] Domain_name.t ->
t * (([`raw] Domain_name.t * int32 * Nsec3.t * rank) list, [ `Cache_miss | `Cache_drop ]) result
(** [get_nsec3 cache timestamp name] retrieves all nsec3 resource records for
the zone [name]. *)
val set : t -> int64 -> [ `raw ] Domain_name.t -> 'a Rr_map.key -> rank ->
'a entry -> t
(** [set cache timestamp type name rank value] attempts to insert
[type, name, value] into the [cache] using the [timestamp] and [rank]. If
an entry already exists with a higher [rank], the [cache] is unchanged. *)
val remove : t -> [ `raw ] Domain_name.t -> t
(** [remove cache name] removes [name] from [cache]. *)

View file

@ -0,0 +1,6 @@
(library
(name dns_cache)
(public_name dns.cache)
(modules dns_cache)
(libraries domain-name dns duration lru metrics)
(wrapped false))

View file

@ -0,0 +1,207 @@
open Dns
let src = Logs.Src.create "dns_certify" ~doc:"DNS certify"
module Log = (val Logs.src_log src : Logs.LOG)
let tlsa_is usage sel typ t =
t.Tlsa.cert_usage = usage &&
t.Tlsa.selector = sel &&
t.Tlsa.matching_type = typ
let is_csr t =
tlsa_is Tlsa.Domain_issued_certificate Tlsa.Private Tlsa.No_hash t
let csr req =
let data = X509.Signing_request.encode_der req in
{
Tlsa.matching_type = Tlsa.No_hash ;
cert_usage = Tlsa.Domain_issued_certificate ;
selector = Tlsa.Private ;
data
}
let is_certificate t =
tlsa_is Tlsa.Domain_issued_certificate Tlsa.Full_certificate Tlsa.No_hash t
let certificate cert =
let data = X509.Certificate.encode_der cert in
{
Tlsa.matching_type = Tlsa.No_hash ;
cert_usage = Tlsa.Domain_issued_certificate ;
selector = Tlsa.Full_certificate ;
data
}
let is_ca_certificate t =
tlsa_is Tlsa.CA_constraint Tlsa.Full_certificate Tlsa.No_hash t
let ca_certificate data = {
Tlsa.matching_type = Tlsa.No_hash ;
cert_usage = Tlsa.CA_constraint ;
selector = Tlsa.Full_certificate ;
data
}
let signing_request hostname ?(more_hostnames = []) key =
let host = Domain_name.to_string hostname in
let extensions =
match more_hostnames with
| [] -> X509.Signing_request.Ext.empty
| _ ->
let ext =
let additional = List.map Domain_name.to_string more_hostnames in
let gn = X509.General_name.(singleton DNS (host :: additional)) in
X509.Extension.(singleton Subject_alt_name (false, gn))
in
X509.Signing_request.Ext.(singleton Extensions ext)
in
X509.(Signing_request.create
[Distinguished_name.(Relative_distinguished_name.singleton (CN host))]
~extensions key)
let dns_header rng =
let id = Randomconv.int16 rng in
(id, Packet.Flags.empty)
let le_label = "_letsencrypt"
and p_label = "_tcp"
let is_name name =
if Domain_name.count_labels name < 2 then
false
else
Domain_name.(equal_label le_label (get_label_exn name 0) &&
equal_label p_label (get_label_exn name 1))
let letsencrypt_name name =
match Domain_name.(prepend_label (raw name) p_label) with
| Ok name' -> Domain_name.prepend_label name' le_label
| Error e -> Error e
type u_err = [ `Tsig of Dns_tsig.e | `Bad_reply of Packet.mismatch * Packet.t | `Unexpected_reply of Packet.reply ]
let pp_u_err ppf = function
| `Tsig e -> Fmt.pf ppf "tsig error %a" Dns_tsig.pp_e e
| `Bad_reply (e, res) -> Fmt.pf ppf "bad reply %a: %a" Packet.pp_mismatch e Packet.pp res
| `Unexpected_reply r -> Fmt.pf ppf "unexpected reply %a" Packet.pp_reply r
let nsupdate rng now ~host ~keyname ~zone dnskey request =
match letsencrypt_name host with
| Error e -> Error e
| Ok host ->
let tlsa = csr request in
let zone = Packet.Question.create zone Soa
and update =
let up =
Domain_name.Map.singleton host
[
Packet.Update.Remove (K Tlsa) ;
Packet.Update.Add (B (Tlsa, (3600l, Rr_map.Tlsa_set.singleton tlsa)))
]
in
(Domain_name.Map.empty, up)
and header = dns_header rng
in
let packet = Packet.create header zone (`Update update) in
let now = now () in
match Dns_tsig.encode_and_sign ~proto:`Tcp packet now dnskey keyname with
| Error e -> Error (`Msg (Fmt.to_to_string Dns_tsig.pp_s e))
| Ok (data, mac) ->
Ok (data, (fun data ->
match Dns_tsig.decode_and_verify now dnskey keyname ~mac data with
| Error e -> Error (`Tsig e)
| Ok (res, _, _) ->
match Packet.reply_matches_request ~request:packet res with
| Ok `Update_ack -> Ok ()
| Ok r -> Error (`Unexpected_reply r)
| Error e -> Error (`Bad_reply (e, res))))
type q_err = [
| `Decode of Packet.err
| `Bad_reply of Packet.mismatch * Packet.t
| `Unexpected_reply of Packet.reply
| `No_tlsa
]
let pp_q_err ppf = function
| `Decode err -> Fmt.pf ppf "decoding failed %a" Packet.pp_err err
| `Bad_reply (e, res) -> Fmt.pf ppf "bad reply %a: %a" Packet.pp_mismatch e Packet.pp res
| `Unexpected_reply r -> Fmt.pf ppf "unexpected reply %a" Packet.pp_reply r
| `No_tlsa -> Fmt.pf ppf "No TLSA record found"
(* may be better suited in X509? *)
let cert_matches_csr ?until now csr cert =
let until = match until with None -> now | Some x -> x in
let csr_key = X509.Signing_request.((info csr).public_key)
and csr_hostnames = X509.Signing_request.hostnames csr
and cert_key = X509.Certificate.public_key cert
and cert_hostnames = X509.Certificate.hostnames cert
and (st, en) = X509.Certificate.validity cert
in
let valid = Ptime.is_later ~than:st now && Ptime.is_later ~than:until en in
if not (String.equal (X509.Public_key.fingerprint cert_key) (X509.Public_key.fingerprint csr_key)) then begin
Log.info (fun m -> m "public key of CSR and certificate %a do not match"
X509.Certificate.pp cert);
false
end else if not (X509.Host.Set.equal cert_hostnames csr_hostnames) then begin
Log.info (fun m -> m "hostnames of CSR %a and certificate %a do not match"
X509.Host.Set.pp csr_hostnames X509.Host.Set.pp cert_hostnames);
false
end else if not valid then begin
let pp_pt = Ptime.pp_rfc3339 () in
Log.info (fun m -> m "Certificate is not valid now %a (until %a), it is \
valid from %a until %a)"
pp_pt now pp_pt until pp_pt st pp_pt en);
false
end else
true
let tlsas_to_certchain host now csr tlsas =
let certificates, ca_certificates =
Rr_map.Tlsa_set.fold (fun tlsa (certs, cacerts as acc) ->
if is_certificate tlsa || is_ca_certificate tlsa then
match X509.Certificate.decode_der tlsa.Tlsa.data with
| Error (`Msg msg) ->
Log.warn (fun m -> m "couldn't decode tlsa record %a: %s (%a)"
Domain_name.pp host msg
Ohex.pp tlsa.Tlsa.data);
acc
| Ok cert ->
match is_certificate tlsa, is_ca_certificate tlsa with
| true, _ -> (cert :: certs, cacerts)
| _, true -> (certs, cert :: cacerts)
| _ -> acc
else acc)
tlsas ([], [])
in
match List.find_opt (cert_matches_csr now csr) certificates with
| None -> Error `No_tlsa
| Some server_cert ->
match List.rev (X509.Validation.build_paths server_cert ca_certificates) with
| (_server :: chain) :: _ -> Ok (server_cert, chain)
| _ -> Ok (server_cert, []) (* build_paths always returns the server_cert *)
let query rng now host csr =
match letsencrypt_name host with
| Error e -> Error e
| Ok host ->
let header = dns_header rng
and question = Packet.Question.create host Tlsa
in
let request = Packet.create header question `Query in
let out, _ = Packet.encode `Tcp request
and react data =
match Packet.decode data with
| Error e -> Error (`Decode e)
| Ok reply ->
match Packet.reply_matches_request ~request reply with
| Ok (`Answer (answer, _)) ->
begin match Name_rr_map.find host Tlsa answer with
| None -> Error `No_tlsa
| Some (_, tlsas) -> tlsas_to_certchain host now csr tlsas
end
| Ok (`Rcode_error (Rcode.NXDomain, Opcode.Query, _)) -> Error `No_tlsa
| Ok reply -> Error (`Unexpected_reply reply)
| Error e -> Error (`Bad_reply (e, reply))
in
Ok (out, react)

View file

@ -0,0 +1,93 @@
open Dns
val signing_request : [`host] Domain_name.t ->
?more_hostnames:([`raw] Domain_name.t list) ->
X509.Private_key.t -> (X509.Signing_request.t, [> `Msg of string ]) result
(** [signing_request name ~more_hostnames key] creates a X509 signing request
where [name] will be the common name in its subject, and if [more_hostnames]
is provided and non-empty, [name :: more_hostnames] will be the value of a
subjectAlternativeName extension. *)
val letsencrypt_name : 'a Domain_name.t ->
([ `raw ] Domain_name.t, [> `Msg of string ]) result
(** [letsencrypt_name host] is the service name at which we store let's encrypt
certificates for the [host]. *)
val is_csr : Dns.Tlsa.t -> bool
(** [is_csr tlsa] is true if [tlsa] is a certificate signing request (cert_usage
is Domain_issued_certificate, selector is Private, and matching_type is
No_hash). *)
val csr : X509.Signing_request.t -> Dns.Tlsa.t
(** [csr req] is the signing request [req] encoded as TLSA record. *)
val is_certificate : Dns.Tlsa.t -> bool
(** [is_certificate tlsa] is true if [tlsa] is a certificate (cert_usage is
Domain_issued_certificate, selector is Full_certificate, and matching_type
is No_hash). *)
val certificate : X509.Certificate.t -> Dns.Tlsa.t
(** [certificate crt] is the certificate [crt] encoded as TLSA record. *)
val is_ca_certificate : Dns.Tlsa.t -> bool
(** [is_ca_certificate tlsa] is true if [tlsa] is a CA certificate (cert_usage
is CA_constraint, selector is Full_certificate, and matching_type is
No_hash). *)
val ca_certificate : string -> Dns.Tlsa.t
(** [ca_certificate data] is the CA certificate [data] encoded as TLSA record. *)
val is_name : 'a Domain_name.t -> bool
(** [is_name domain_name] is true if it contains the prefix used in this
library ("_letsencrypt._tcp"). *)
type u_err = [
| `Tsig of Dns_tsig.e
| `Bad_reply of Packet.mismatch * Packet.t
| `Unexpected_reply of Packet.reply
]
(** The type of update errors. *)
val pp_u_err : u_err Fmt.t
(** [pp_u_err ppf u] pretty-prints [u] on [ppf]. *)
val nsupdate : (int -> string) -> (unit -> Ptime.t) ->
host:[ `host ] Domain_name.t -> keyname:'b Domain_name.t ->
zone:[ `host ] Domain_name.t -> Dns.Dnskey.t -> X509.Signing_request.t ->
(string * (string -> (unit, [> u_err ]) result),
[> `Msg of string ]) result
(** [nsupdate rng now ~host ~keyname ~zone dnskey csr] is a buffer with a DNS
update that removes all TLSA records from the given [host], and adds a single
TLSA record containing the certificate signing request. It also returns a
function which decodes a given answer, checks it to be a valid reply, and
returns either unit or an error. The outgoing packet is signed with the
provided [dnskey], the answer is checked to be signed by the same key. If
the sign operation fails, [nsupdate] returns an error. *)
type q_err = [
| `Decode of Packet.err
| `Bad_reply of Packet.mismatch * Packet.t
| `Unexpected_reply of Packet.reply
| `No_tlsa
]
(** The type for query errors. *)
val pp_q_err : q_err Fmt.t
(** [pp_q_err ppf q] pretty-prints [q] on [ppf]. *)
val cert_matches_csr : ?until:Ptime.t -> Ptime.t -> X509.Signing_request.t ->
X509.Certificate.t -> bool
(** [cert_matches_csr ~until now csr cert] is [true] if [cert] matches the
signing request [csr], and is valid from [now] until [until] (defaults to
[now]). The matching is [true] if the public key matches, and the set of
hostnames in [csr] and [cert] are equal. A log message on the info level
is emitted if the return value if [false]. *)
val query : (int -> string) -> Ptime.t -> [ `host ] Domain_name.t ->
X509.Signing_request.t ->
(string *
(string -> (X509.Certificate.t * X509.Certificate.t list, [> q_err ]) result),
[> `Msg of string ]) result
(** [query rng now csr] is a [buffer] with a DNS TLSA query for the name of
[csr], and a function that decodes a given answer, either returning a X.509
certificate valid [now] and matching [csr], and a CA chain, or an error. *)

View file

@ -0,0 +1,5 @@
(library
(name dns_certify)
(public_name dns-certify)
(wrapped false)
(libraries dns dns-tsig x509 randomconv logs mirage-crypto-ec mirage-crypto-pk))

View file

@ -0,0 +1,340 @@
open Dns
let src = Logs.Src.create "dns_client" ~doc:"DNS client"
module Log = (val Logs.src_log src : Logs.LOG)
module Pure = struct
type 'key query_state =
{ protocol : Dns.proto ;
key: 'key ;
query : Packet.t ;
} constraint 'key = 'a Rr_map.key
let make_query rng protocol ?(dnssec = false) edns hostname
: 'xy ->
string * 'xy query_state =
(* SRV records: Service + Protocol are case-insensitive, see RFC2728 pg2. *)
fun record_type ->
let edns = match edns with
| `None -> None
| `Manual e -> Some e
| `Auto -> match protocol with
| `Udp -> None
| `Tcp -> Some (Edns.create ~extensions:[Edns.Tcp_keepalive (Some 1200)] ())
in
let question = Packet.Question.create hostname record_type in
let header =
let flags = Packet.Flags.singleton `Recursion_desired in
let flags =
if dnssec then Packet.Flags.add `Authentic_data flags else flags
in
Randomconv.int16 rng, flags
in
let query = Packet.create ?edns header question `Query in
Log.debug (fun m -> m "sending %a" Dns.Packet.pp query);
let cs , _ = Packet.encode protocol query in
begin match protocol with
| `Udp -> cs
| `Tcp ->
let len_field = Bytes.create 2 in
Bytes.set_uint16_be len_field 0 (String.length cs) ;
String.concat "" [Bytes.unsafe_to_string len_field ; cs]
end, { protocol ; query ; key = record_type }
(* name: the originally requested domain name. *)
(* NOTE that this function compresses answers:
foo.example CNAME 500 bar.example
bar.example A 300 1.2.3.4
is compressed to:
foo.example A 300 1.2.3.4
-> which is fine for applications (i think so)
-> which is struggling for the cache (not entirely sure about this tbh)
-> it is not clear whether it meets the DNS specifications nicely *)
let rec follow_cname name ~iterations:iterations_left ~answer ~state =
if iterations_left <= 0
then Error (`Msg "CNAME recursion too deep")
else
match Domain_name.Map.find_opt name answer with
| None -> Ok (`Need_soa name)
| Some relevant_map ->
match Rr_map.find state.key relevant_map with
| Some response -> Ok (`Data response)
| None ->
match Rr_map.(find Cname relevant_map) with
| None -> Error (`Msg "Invalid DNS response")
| Some (_ttl, redirected_host) ->
let iterations = pred iterations_left in
follow_cname redirected_host ~iterations ~answer ~state
let consume_protocol_prefix buf =
function (* consume TCP two-byte length prefix: *)
| `Udp -> Ok buf
| `Tcp ->
match String.get_uint16_be buf 0 with
| exception Invalid_argument _ -> Error () (* TODO *)
| pkt_len when pkt_len > String.length buf -2 ->
Log.debug (fun m -> m "Partial: %d >= %d-2"
pkt_len (String.length buf));
Error () (* TODO return remaining # *)
| pkt_len ->
if 2 + pkt_len < String.length buf then
Log.warn (fun m -> m "Extraneous data in DNS response");
Ok (String.sub buf 2 pkt_len)
let find_soa authority =
Domain_name.Map.fold (fun k rr_map acc ->
match Rr_map.(find Soa rr_map) with
| Some soa -> Some (Domain_name.raw k, soa)
| None -> acc)
authority None
let distinguish_answer state =
let ( let* ) = Result.bind in
function
| `Answer (answer, authority) when not (Domain_name.Map.is_empty answer) ->
begin
let q = fst state.query.question in
let* o = follow_cname q ~iterations:20 ~answer ~state in
match o with
| `Data x -> Ok (`Data x)
| `Need_soa _name ->
(* should we retain CNAMEs (and send them to the client)? *)
(* should we 'adjust' the SOA name to be _name? *)
match find_soa authority with
| Some soa -> Ok (`No_data soa)
| None -> Error (`Msg "invalid reply, couldn't find SOA")
end
| `Answer (_, authority) ->
begin match find_soa authority with
| Some soa -> Ok (`No_data soa)
| None -> Error (`Msg "invalid reply, no SOA in no data")
end
| `Rcode_error (Rcode.NXDomain, Opcode.Query, Some (_answer, authority)) ->
begin match find_soa authority with
| Some soa -> Ok (`No_domain soa)
| None -> Error (`Msg "invalid reply, no SOA in nodomain")
end
| r ->
Error (`Msg (Fmt.str "Ok %a, expected answer" Packet.pp_reply r))
let consume_rest_of_buffer state buf =
let to_msg t =
Result.map_error (fun e ->
`Msg
(Fmt.str
"QUERY: @[<v>hdr:%a (id: %d = %d) (q=q: %B)@ query:%a%a \
opt:%a tsig:%B@,failed: %a@,@]"
Packet.pp_header t
(fst t.header) (fst state.query.header)
(Packet.Question.compare t.question state.query.question = 0)
Packet.Question.pp t.question
Packet.pp_data t.data
(Fmt.option Dns.Edns.pp) t.edns
(match t.tsig with None -> false | Some _ -> true)
Packet.pp_mismatch e))
in
match Packet.decode buf with
| Error `Partial as e -> e
| Error err ->
Error (`Msg (Fmt.str "Error parsing response: %a" Packet.pp_err err))
| Ok t ->
Log.debug (fun m -> m "received %a" Dns.Packet.pp t);
to_msg t (Packet.reply_matches_request ~request:state.query t)
let parse_response (type requested)
: requested Rr_map.key query_state -> string ->
(Packet.reply,
[> `Partial
| `Msg of string]) result =
fun state buf ->
match consume_protocol_prefix buf state.protocol with
| Ok buf -> consume_rest_of_buffer state buf
| Error () -> Error `Partial
let handle_response (type requested)
: requested Rr_map.key query_state -> string ->
( [ `Data of requested
| `Partial
| `No_data of [`raw] Domain_name.t * Soa.t
| `No_domain of [`raw] Domain_name.t * Soa.t ],
[`Msg of string]) result =
fun state buf ->
match parse_response state buf with
| Error `Partial -> Ok `Partial
| Error `Msg _ as e -> e
| Ok reply -> distinguish_answer state reply
end
(* Anycast address of uncensoreddns.org *)
let default_resolver_hostname = Domain_name.(host_exn (of_string_exn "anycast.uncensoreddns.org"))
let default_resolvers = [
Ipaddr.of_string_exn "2001:67c:28a4::" ;
Ipaddr.of_string_exn "91.239.100.100" ;
]
module type S = sig
type context
type +'a io
type io_addr
type stack
type t
val create : ?nameservers:(Dns.proto * io_addr list) -> timeout:int64 -> stack -> t
val nameservers : t -> Dns.proto * io_addr list
val rng : int -> string
val clock : unit -> int64
val connect : t -> (Dns.proto * context, [> `Msg of string ]) result io
val send_recv : context -> string -> (string, [> `Msg of string ]) result io
val close : context -> unit io
val bind : 'a io -> ('a -> 'b io) -> 'b io
val lift : 'a -> 'a io
end
let localhost = Domain_name.of_string_exn "localhost"
let localsoa = Soa.create (Domain_name.prepend_label_exn localhost "ns")
let invalid = Domain_name.of_string_exn "invalid"
let invalidsoa = Soa.create (Domain_name.prepend_label_exn invalid "ns")
let rfc6761_special (type req) q_name (q_typ : req Dns.Rr_map.key) : (req Dns_cache.entry, unit) result =
if Domain_name.is_subdomain ~domain:localhost ~subdomain:q_name then
let open Dns.Rr_map in
match q_typ with
| A -> Ok (`Entry (300l, Ipaddr.V4.Set.singleton Ipaddr.V4.localhost))
| Aaaa ->
Ok (`Entry (300l, Ipaddr.V6.Set.singleton Ipaddr.V6.localhost))
| _ -> Ok (`No_domain (localhost, localsoa))
else if Domain_name.is_subdomain ~domain:invalid ~subdomain:q_name then
Ok (`No_domain (invalid, invalidsoa))
else
Error ()
module Make = functor (Transport:S) ->
struct
type t = {
mutable cache : Dns_cache.t ;
transport : Transport.t ;
edns : [ `None | `Auto | `Manual of Dns.Edns.t ] ;
}
let transport { transport ; _ } = transport
(* TODO eventually use Auto, and retry without on FormErr *)
let create ?(cache_size = 32) ?(edns = `None) ?nameservers ?(timeout = Duration.of_sec 5) stack =
{ cache = Dns_cache.empty cache_size ;
transport = Transport.create ?nameservers ~timeout stack ;
edns ;
}
let nameservers { transport; _ } = Transport.nameservers transport
let (>>=) = Transport.bind
(* result-bind *)
let (>>|) a b =
a >>= function
| Ok a' -> b a'
| Error e -> Transport.lift (Error e)
(* result-bind-and-lift *)
let (>>|=) a f = a >>| fun b -> Transport.lift (f b)
let lift_ok (type req) :
(req Dns_cache.entry, 'a) result ->
(req, [> `Msg of string
| `No_data of [ `raw ] Domain_name.t * Dns.Soa.t
| `No_domain of [ `raw ] Domain_name.t * Dns.Soa.t ]) result
= function
| Ok `Entry value -> Ok value
| Ok (`No_data _ as nodata) -> Error nodata
| Ok (`No_domain _ as nodom) -> Error nodom
| Ok (`Serv_fail _)
| Error _ -> Error (`Msg "")
let get_raw_reply t query_type name =
Transport.connect t.transport >>| fun (proto, socket) ->
Log.debug (fun m -> m "Connected to NS.");
let tx, state =
Pure.make_query Transport.rng proto ~dnssec:true t.edns name query_type
in
(Transport.send_recv socket tx >>| fun recv_buffer ->
Log.debug (fun m -> m "Read @[<v>%d bytes@]"
(String.length recv_buffer)) ;
Log.debug (fun m -> m "received: %a" (Ohex.pp_hexdump ()) recv_buffer);
Transport.lift (Pure.parse_response state recv_buffer)) >>= fun r ->
Transport.close socket >>= fun () ->
Transport.lift r
let get_resource_record (type requested) t (query_type:requested Dns.Rr_map.key) name
: (requested, [> `Msg of string
| `No_data of [ `raw ] Domain_name.t * Dns.Soa.t
| `No_domain of [ `raw ] Domain_name.t * Dns.Soa.t ]) result Transport.io =
let domain_name = Domain_name.raw name in
match rfc6761_special domain_name query_type |> lift_ok with
| Ok _ as ok -> Transport.lift ok
| Error ((`No_data _ | `No_domain _) as nod) -> Error nod |> Transport.lift
| Error `Msg _ ->
let cache', r =
Dns_cache.get t.cache (Transport.clock ()) domain_name query_type
in
t.cache <- cache';
match lift_ok (Result.map fst r) with
| Ok _ as ok -> Transport.lift ok
| Error ((`No_data _ | `No_domain _) as nod) -> Error nod |> Transport.lift
| Error `Msg _ ->
Transport.connect t.transport >>| fun (proto, socket) ->
Log.debug (fun m -> m "Connected to NS.");
let tx, state =
Pure.make_query Transport.rng proto t.edns name query_type
in
(Transport.send_recv socket tx >>| fun recv_buffer ->
Log.debug (fun m -> m "Read @[<v>%d bytes@]"
(String.length recv_buffer)) ;
let update_cache entry =
let rank = Dns_cache.NonAuthoritativeAnswer in
let cache =
Dns_cache.set t.cache (Transport.clock ()) domain_name query_type rank entry
in
t.cache <- cache
in
Transport.lift
(match Pure.handle_response state recv_buffer with
| Ok `Data x ->
update_cache (`Entry x);
Ok x
| Ok ((`No_data _ | `No_domain _) as nodom) ->
update_cache nodom;
Error nodom
| Error `Msg xxx -> Error (`Msg xxx)
| Ok `Partial -> Error (`Msg "Truncated UDP response"))) >>= fun r ->
Transport.close socket >>= fun () ->
Transport.lift r
let lift_cache_error query_type m =
(match m with
| Ok a -> Ok a
| Error `Msg msg -> Error (`Msg msg)
| Error (#Dns_cache.entry as e) ->
Error (`Msg (Fmt.str "DNS cache error @[%a@]" (Dns_cache.pp_entry query_type) e)))
|> Transport.lift
let getaddrinfo (type requested) t (query_type:requested Dns.Rr_map.key) name
: (requested, [> `Msg of string ]) result Transport.io =
get_resource_record t query_type name >>= lift_cache_error query_type
let gethostbyname stack domain =
getaddrinfo stack Dns.Rr_map.A domain >>|= fun (_ttl, resp) ->
match Ipaddr.V4.Set.choose_opt resp with
| None -> Error (`Msg "No A record found")
| Some ip -> Ok ip
let gethostbyname6 stack domain =
getaddrinfo stack Dns.Rr_map.Aaaa domain >>|= fun (_ttl, res) ->
match Ipaddr.V6.Set.choose_opt res with
| None -> Error (`Msg "No AAAA record found")
| Some ip -> Ok ip
end

View file

@ -0,0 +1,196 @@
(* TODO ideally there'd be something like mirage-flow-lwt that didn't depend
on lwt and a ton of other things, and still provided [map]
and [connect] and so on. leaving this stuff here for now until a
better solution presents itself. *)
val default_resolver_hostname : [`host] Domain_name.t
val default_resolvers : Ipaddr.t list
(** [default_resolver] is a list of IPv6 and IPv4 address of the default
resolver. Currently it is the IP address of the UncensoredDNS.org
anycast service. *)
module type S = sig
type context
(** A context is a network connection initialized by {!connect} *)
type +'a io
(** [io] is the type of an effect. ['err] is a polymorphic variant. *)
type io_addr
(** An address for a given context type, usually this will consist of
IP address + a TCP/IP or UDP/IP port number, but for some context types
it can carry additional information for purposes of cryptographic
verification. *)
type stack
(** A stack with which to connect. *)
type t
(** The abstract state of a DNS client. *)
val create : ?nameservers:(Dns.proto * io_addr list) -> timeout:int64 ->
stack -> t
(** [create ~nameservers ~timeout stack] creates the state record of
the DNS client. We use [timeout] (ns) as a cumulative time budget for
connect and request timeouts. *)
val nameservers : t -> Dns.proto * io_addr list
(** The address of a nameservers that is supposed to work with
the underlying context, can be used if the user does not want to
bother with configuring their own.*)
val rng : int -> string
(** [rng t] is a random number generator. *)
val clock : unit -> int64
(** [clock t] is the monotonic clock. *)
val connect : t -> (Dns.proto * context, [> `Msg of string ]) result io
(** [connect t] is a new connection ([context]) to [t], or an error. *)
val send_recv : context -> string -> (string, [> `Msg of string ]) result io
(** [send_recv context buffer] sends [buffer] to the [context] upstream, and
then reads a buffer. *)
val close : context -> unit io
(** [close context] closes the [context], freeing up resources. *)
val bind : 'a io -> ('a -> 'b io) -> 'b io
(** a.k.a. [>>=] *)
val lift : 'a -> 'a io
end
module Make : functor (T : S) ->
sig
type t
(** The abstract type of a DNS client. *)
val transport : t -> T.t
(** [transport t] is the transport of [t]. *)
val create : ?cache_size:int ->
?edns:[ `None | `Auto | `Manual of Dns.Edns.t ] ->
?nameservers:(Dns.proto * T.io_addr list) -> ?timeout:int64 ->
T.stack -> t
(** [create ~cache_size ~edns ~nameservers ~timeout stack] creates the state
of the DNS client. We use [timeout] (ns, default 5s) as a time budget for
connect and request timeouts. To specify a timeout, use
[create ~timeout:(Duration.of_sec 3)]. Whether or not to use
{{:https://tools.ietf.org/html/rfc6891}EDNS} in queries is controlled
by [~edns] (defaults to [`None]): if [None], no EDNS will be present,
[`Auto] adds TCP Keepalive if protocol is TCP, [`Manual edns] adds the
EDNS data specified. *)
val nameservers : t -> Dns.proto * T.io_addr list
(** [nameservers state] returns the list of nameservers to be used. *)
val getaddrinfo : t -> 'response Dns.Rr_map.key ->
'a Domain_name.t ->
('response, [> `Msg of string ]) result T.io
(** [getaddrinfo state query_type name] is the
[query_type]-dependent response regarding [name], or
an [Error _] message. See {!Dns_client.query_state} for more information
about the result types. *)
val gethostbyname : t -> [ `host ] Domain_name.t ->
(Ipaddr.V4.t, [> `Msg of string ]) result T.io
(** [gethostbyname state hostname] is the IPv4 address of
[hostname] resolved via the [state] specified.
If the query fails, or if the [domain] does not have any IPv4 addresses,
an [Error _] message is returned. Any extraneous IPv4 addresses are
ignored. For an example of using this API, see [unix/ohost.ml] in the
distribution of this package. *)
val gethostbyname6 : t -> [ `host ] Domain_name.t ->
(Ipaddr.V6.t, [> `Msg of string ]) result T.io
(** [gethostbyname6 state hostname] is the IPv6 address of
[hostname] resolved via the [state] specified.
It is the IPv6 equivalent of {!gethostbyname}. *)
val get_resource_record : t -> 'response Dns.Rr_map.key -> 'a Domain_name.t ->
('response,
[> `Msg of string
| `No_data of [ `raw ] Domain_name.t * Dns.Soa.t
| `No_domain of [ `raw ] Domain_name.t * Dns.Soa.t ]) result T.io
(** [get_resource_record state query_type name] resolves
[query_type, name] via the [state] specified. The
behaviour is equivalent to {!getaddrinfo}, apart from the error return
value - [get_resource_record] distinguishes some errors, at the moment
[No_data] if the [name] exists, but not the [query_type], and
[No_domain] if the [name] does not exist. This allows clients to treat
these error conditions explicitly. *)
val get_raw_reply : t -> 'response Dns.Rr_map.key ->
'a Domain_name.t ->
(Dns.Packet.reply, [> `Partial | `Msg of string ]) result T.io
(** [get_raw_reply state query_type name] resolves [query_type, name] via the
[state] specified. The complete DNS reply is returned. CNAME records
are not followed. This allows DNSSec to process the entire reply. *)
end
module Pure : sig
(** The pure interface to the client part of uDns.
Various helper modules to do with side effects are available from
{!Dns_client_lwt}, {!Dns_client_unix} and so forth. *)
type 'key query_state constraint 'key = 'a Dns.Rr_map.key
(** [query_state] is parameterized over the query type, so the type of the
representation of the answer depends on what the name server was asked to
provide. See {!Dns.Rr_map.k} for a list of response types. The first
element (the [int32]) in most of the tuples is the Time-To-Live (TTL)
field returned from the server, which you can use to calculate when you
should request fresh information in case you are writing a long-running
application. *)
val make_query :
(int -> string) -> Dns.proto -> ?dnssec:bool ->
[ `None | `Auto | `Manual of Dns.Edns.t ] ->
'a Domain_name.t ->
'query_type Dns.Rr_map.key ->
string * 'query_type Dns.Rr_map.key query_state
(** [make_query rng protocol name query_type] is [query, query_state]
where [query] is the serialized DNS query to send to the name server,
and [query_state] is the information required to validate the response. *)
val parse_response : 'query_type Dns.Rr_map.key query_state -> string ->
(Dns.Packet.reply, [ `Partial | `Msg of string]) result
(** [parse_response query_state response] is the information contained in
[response] parsed using [query_state] when the query was successful, or
an [`Msg message] if the [response] did not match the [query_state]
(or if the query failed).
In a TCP usage context the [`Partial] means there are more bytes to be
read in order to parse correctly. This can happen due to short reads or if
the server (or something along the route) chunks its responses into
multiple individual packets. In that case you should concatenate
[response] and the next received data and call this function again.
In a UDP usage context the [`Partial] means information was lost, due to
an incomplete packet. *)
val handle_response : 'query_type Dns.Rr_map.key query_state -> string ->
( [ `Data of 'query_type
| `Partial
| `No_data of [`raw] Domain_name.t * Dns.Soa.t
| `No_domain of [`raw] Domain_name.t * Dns.Soa.t ],
[`Msg of string]) result
(** [handle_response query_state response] is the information contained in
[response] parsed using [query_state] when the query was successful, or
an [`Msg message] if the [response] did not match the [query_state]
(or if the query failed).
In a TCP usage context the [`Partial] means there are more bytes to be
read in order to parse correctly. This can happen due to short reads or if
the server (or something along the route) chunks its responses into
multiple individual packets. In that case you should concatenate
[response] and the next received data and call this function again.
In a UDP usage context the [`Partial] means information was lost, due to
an incomplete packet. *)
end

View file

@ -0,0 +1,6 @@
(library
(name dns_client)
(public_name dns-client)
(modules dns_client)
(libraries dns.cache domain-name dns randomconv)
(wrapped false))

View file

@ -0,0 +1,41 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Hannes Mehnert <hannes@mehnert.org>"]
homepage: "https://github.com/mirage/ocaml-dns"
doc: "https://mirage.github.io/ocaml-dns/"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
license: "BSD-2-Clause"
depends: [
"dune" {>= "2.8.0"}
"ocaml" {>= "4.13.0"}
"dns" {= version}
"dns-tsig" {= version}
"dns-mirage" {= version}
"randomconv" {>= "0.2.0"}
"duration" {>= "0.1.2"}
"x509" {>= "1.0.0"}
"lwt" {>= "4.2.1"}
"mirage-sleep" {>= "4.0.0"}
"mirage-ptime" {>= "5.0.0"}
"tcpip" {>= "8.2.0"}
"logs"
"mirage-crypto-ec"
"mirage-crypto-pk" {>= "1.0.0"}
"mirage-crypto-rng" {>= "1.0.0"}
]
build: [
["dune" "subst"] {dev}
["dune" "build" "-p" name "-j" jobs]
["dune" "runtest" "-p" name "-j" jobs] {with-test}
]
synopsis: "MirageOS let's encrypt certificate retrieval"
description: """
A function to retrieve a certificate when providing a hostname, TSIG key, server
IP, and an optional key seed. Best used with an letsencrypt unikernel.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,56 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Hannes Mehnert <hannes@mehnert.org>"]
homepage: "https://github.com/mirage/ocaml-dns"
doc: "https://mirage.github.io/ocaml-dns/"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
license: "BSD-2-Clause"
depends: [
"dune" {>= "2.8.0"}
"ocaml" {>= "4.13.0"}
"dns" {= version}
"dnssec" {= version}
"dns-tsig" {= version}
"dns-client-lwt" {= version}
"dns-server" {= version}
"dns-certify" {= version}
"dns-resolver" {= version}
"bos" {>= "0.2.0"}
"cmdliner" {>= "1.1.0"}
"fpath" {>= "0.7.2"}
"x509" {>= "1.0.0"}
"mirage-crypto" {>= "1.0.0"}
"mirage-crypto-pk" {>= "1.0.0"}
"mirage-crypto-rng" {>= "2.0.0"}
"mirage-mtime" {>= "5.0.0"}
"mtime" {>= "2.1.0"}
"ptime" {>= "1.2.0"}
"tcpip" {>= "8.2.0"}
"ohex" {>= "0.2.0"}
"logs" {>= "0.6.3"}
"fmt" {>= "0.8.8"}
"ipaddr" {>= "4.0.0"}
"lwt" {>= "4.0.0"}
"randomconv" {>= "0.2.0"}
"metrics" {>= "0.5.0"}
"alcotest" {with-test}
]
build: [
["dune" "subst"] {dev}
["dune" "build" "-p" name "-j" jobs]
["dune" "runtest" "-p" name "-j" jobs] {with-test}
]
synopsis: "Unix command line utilities using uDNS"
description: """
'oupdate' sends a DNS update frome to a DNS server that sets 'hostname A ip'.
For authentication via TSIG, a hmac secret needs to be provided.
'ocertify' updates DNS with a certificate signing request, and polls a matching
certificate. Best used with an letsencrypt unikernel.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,34 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Joe Hill"]
homepage: "https://github.com/mirage/ocaml-dns"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
license: "BSD-2-Clause"
build: [
[ "dune" "subst"] {dev}
[ "dune" "build" "-p" name "-j" jobs ]
[ "dune" "runtest" "-p" name "-j" jobs ] {with-test}
]
depends: [
"dune" {>="2.8.0"}
"ocaml" {>= "4.13.0"}
"dns-client" {= version}
"dns" {= version}
"ipaddr" {>= "5.3.0"}
"lwt" {>= "4.2.1"}
"mtime" {>= "1.2.0"}
"mirage-crypto-rng" {>= "1.2.0"}
"happy-eyeballs-lwt" {>= "2.0.0"}
"happy-eyeballs" {>= "2.0.0"}
"tls-lwt" {>= "2.0.0"}
"ca-certs" {>= "1.0.0"}
]
synopsis: "DNS client API using lwt"
description: """
A client implementation using uDNS and lwt for side effects.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,31 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Robur <team@robur.coop>"]
homepage: "https://github.com/mirage/ocaml-dns"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
license: "BSD-2-Clause"
build: [
[ "dune" "subst"] {dev}
[ "dune" "build" "-p" name "-j" jobs ]
[ "dune" "runtest" "-p" name "-j" jobs ] {with-test}
]
depends: [
"dune" {>="2.8.0"}
"ocaml" {>= "5.0.0"}
"dns-client" {= version}
"domain-name" {>= "0.4.0"}
"ipaddr" {>= "5.3.0"}
"miou" {>= "0.1.0"}
"tls-miou-unix" {>= "2.0.0"}
"happy-eyeballs" {>= "2.0.0"}
"happy-eyeballs-miou-unix" {>= "2.0.0"}
]
synopsis: "DNS client API for Miou"
description: """
A client implementation using uDNS using Miou.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,38 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Joe Hill"]
homepage: "https://github.com/mirage/ocaml-dns"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
license: "BSD-2-Clause"
build: [
[ "dune" "subst"] {dev}
[ "dune" "build" "-p" name "-j" jobs ]
[ "dune" "runtest" "-p" name "-j" jobs ] {with-test}
]
depends: [
"dune" {>="2.8.0"}
"ocaml" {>= "4.13.0"}
"dns-client" {= version}
"domain-name" {>= "0.4.0"}
"ipaddr" {>= "5.3.0"}
"lwt" {>= "4.2.1"}
"tcpip" {>= "8.2.0"}
"mirage-sleep" {>= "4.0.0"}
"mirage-mtime" {>= "5.0.0"}
"mirage-ptime" {>= "5.0.0"}
"happy-eyeballs-mirage" {>= "2.0.0"}
"happy-eyeballs" {>= "2.0.0"}
"tls-mirage" {>= "2.0.0"}
"x509" {>= "1.0.0"}
"ca-certs-nss" {>= "3.108-1"}
"mirage-crypto-rng" {>= "1.0.0"}
]
synopsis: "DNS client API for MirageOS"
description: """
A client implementation using uDNS using MirageOS.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,32 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Joe Hill"]
homepage: "https://github.com/mirage/ocaml-dns"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
license: "BSD-2-Clause"
build: [
[ "dune" "subst"] {dev}
[ "dune" "build" "-p" name "-j" jobs ]
[ "dune" "runtest" "-p" name "-j" jobs ] {with-test}
]
depends: [
"dune" {>="2.8.0"}
"ocaml" {>= "4.13.0"}
"dns" {= version}
"randomconv" {>= "0.2.0"}
"domain-name" {>= "0.4.0"}
"mtime" {>= "1.2.0"}
"mirage-crypto-rng" {>= "1.2.0"}
"fmt" {>= "0.9.0"}
"ipaddr" {>= "5.5.0"}
"alcotest" {with-test}
]
synopsis: "DNS client API"
description: """
A client implementation using uDNS.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,42 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Hannes Mehnert <hannes@mehnert.org>"]
homepage: "https://github.com/mirage/ocaml-dns"
doc: "https://mirage.github.io/ocaml-dns/"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
license: "BSD-2-Clause"
depends: [
"dune" {>= "2.8.0"}
"ocaml" {>= "4.13.0"}
"cstruct" {>= "6.0.0"}
"dns" {= version}
"ipaddr" {>= "5.2.0"}
"lwt" {>= "4.2.1"}
"tcpip" {>= "8.2.0"}
]
build: [
["dune" "subst"] {dev}
["dune" "build" "-p" name "-j" jobs]
["dune" "runtest" "-p" name "-j" jobs] {with-test}
]
synopsis: "An opinionated Domain Name System (DNS) library"
description: """
µDNS supports most of the domain name system used in the wild. It adheres to
strict conventions. Failing early and hard. It is mostly implemented in the
pure fragment of OCaml (no mutation, isolated IO, no exceptions).
Legacy resource record types are not dealt with, and there is no plan to support
`ISDN`, `MAILA`, `MAILB`, `WKS`, `MB`, `NULL`, `HINFO`, ... . `AXFR` is only
handled via TCP connections. The only resource class supported is `IN` (the
Internet). Truncated hmac in `TSIG` are not supported (always the full length
of the hash algorithm is used).
Please read [the blog article](https://hannes.robur.coop/Posts/DNS) for a more
detailed overview.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,47 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Hannes Mehnert <hannes@mehnert.org>"]
homepage: "https://github.com/mirage/ocaml-dns"
doc: "https://mirage.github.io/ocaml-dns/"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
license: "BSD-2-Clause"
depends: [
"dune" {>= "2.8.0"}
"ocaml" {>= "4.13.0"}
"dns" {= version}
"dns-server" {= version}
"dns-mirage" {= version}
"dnssec" {= version}
"lru" {>= "0.3.0"}
"duration" {>= "0.1.2"}
"randomconv" {>= "0.2.0"}
"lwt" {>= "4.2.1"}
"mirage-sleep" {>= "4.0.0"}
"mirage-mtime" {>= "5.0.0"}
"mirage-ptime" {>= "5.0.0"}
"tcpip" {>= "8.2.0"}
"alcotest" {with-test}
"logs"
"tls" {>= "1.0.0"}
"tls-mirage" {>= "1.0.0"}
"mirage-crypto-rng" {>= "1.0.0"}
"ca-certs-nss" {>= "3.113.1"}
"ipaddr" {>= "5.6.1"}
"metrics" {>= "0.5.0"}
]
build: [
["dune" "subst"] {dev}
["dune" "build" "-p" name "-j" jobs]
["dune" "runtest" "-p" name "-j" jobs] {with-test}
]
synopsis: "DNS resolver business logic"
description: """
Forwarding and recursive resolvers as value-passing functions. To be used with
an effectful layer.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,43 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Hannes Mehnert <hannes@mehnert.org>"]
homepage: "https://github.com/mirage/ocaml-dns"
doc: "https://mirage.github.io/ocaml-dns/"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
license: "BSD-2-Clause"
depends: [
"dune" {>= "2.8.0"}
"ocaml" {>= "4.13.0"}
"cstruct" {>= "6.0.0"}
"dns" {= version}
"dns-mirage" {= version}
"randomconv" {>= "0.2.0"}
"duration" {>= "0.1.2"}
"lwt" {>= "4.2.1"}
"mirage-sleep" {>= "4.0.0"}
"mirage-mtime" {>= "5.0.0"}
"mirage-ptime" {>= "5.0.0"}
"tcpip" {>= "8.2.0"}
"mirage-crypto-rng" {with-test & >= "1.2.0"}
"alcotest" {with-test}
"dns-tsig" {with-test}
"base64" {with-test & >= "3.0.0"}
"metrics"
"logs" {>= "0.7.0"}
]
build: [
["dune" "subst"] {dev}
["dune" "build" "-p" name "-j" jobs]
["dune" "runtest" "-p" name "-j" jobs] {with-test}
]
synopsis: "DNS server, primary and secondary"
description: """
Primary and secondary DNS server implemented in value-passing style. Needs an
effectful layer to be useful.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,42 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Hannes Mehnert <hannes@mehnert.org>"]
homepage: "https://github.com/mirage/ocaml-dns"
doc: "https://mirage.github.io/ocaml-dns/"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
license: "BSD-2-Clause"
depends: [
"dune" {>= "2.8.0"}
"ocaml" {>= "4.13.0"}
"cstruct" {>= "6.0.0"}
"dns" {= version}
"dns-client-mirage" {= version}
"dns-mirage" {= version}
"dns-resolver" {= version}
"dns-tsig" {= version}
"dns-server" {= version}
"duration" {>= "0.1.2"}
"randomconv" {>= "0.2.0"}
"lwt" {>= "4.2.1"}
"mirage-ptime" {>= "5.0.0"}
"tcpip" {>= "8.2.0"}
"metrics"
"mirage-crypto-rng" {>= "1.0.0"}
"tls-mirage" {>= "2.0.2"}
]
build: [
["dune" "subst"] {dev}
["dune" "build" "-p" name "-j" jobs]
["dune" "runtest" "-p" name "-j" jobs] {with-test}
]
synopsis: "DNS stub resolver"
description: """
Forwarding and recursive resolvers as value-passing functions. To be used with
an effectful layer.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,30 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Hannes Mehnert <hannes@mehnert.org>"]
homepage: "https://github.com/mirage/ocaml-dns"
doc: "https://mirage.github.io/ocaml-dns/"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
license: "BSD-2-Clause"
depends: [
"dune" {>= "2.8.0"}
"ocaml" {>= "4.13.0"}
"dns" {= version}
"digestif" {>= "1.2.0"}
"base64" {>= "3.0.0"}
"alcotest" {with-test}
]
build: [
["dune" "subst"] {dev}
["dune" "build" "-p" name "-j" jobs]
["dune" "runtest" "-p" name "-j" jobs] {with-test}
]
synopsis: "TSIG support for DNS"
description: """
TSIG is used to authenticate nsupdate frames using a HMAC.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,48 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Hannes Mehnert <hannes@mehnert.org>" "Reynir Björnsson <reynir@reynir.dk>"]
homepage: "https://github.com/mirage/ocaml-dns"
doc: "https://mirage.github.io/ocaml-dns/"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
license: "BSD-2-Clause"
depends: [
"dune" {>= "2.8.0"}
"ocaml" {>= "4.13.0"}
"logs" "ptime"
"fmt" {>= "0.8.8"}
"domain-name" {>= "0.4.0"}
"gmap" {>= "0.3.0"}
"ipaddr" {>= "5.2.0"}
"alcotest" {with-test}
"lru" {>= "0.3.0"}
"duration" {>= "0.1.2"}
"metrics"
"ohex" {>= "0.2.0"}
"base64" {>= "3.3.0"}
]
conflicts: [ "result" {< "1.5"} ]
build: [
["dune" "subst"] {dev}
["dune" "build" "-p" name "-j" jobs]
["dune" "runtest" "-p" name "-j" jobs] {with-test}
]
synopsis: "An opinionated Domain Name System (DNS) library"
description: """
µDNS supports most of the domain name system used in the wild. It adheres to
strict conventions. Failing early and hard. It is mostly implemented in the
pure fragment of OCaml (no mutation, isolated IO, no exceptions).
Legacy resource record types are not dealt with, and there is no plan to support
`ISDN`, `MAILA`, `MAILB`, `WKS`, `MB`, `NULL`, `HINFO`, ... . `AXFR` is only
handled via TCP connections. The only resource class supported is `IN` (the
Internet). Truncated hmac in `TSIG` are not supported (always the full length
of the hash algorithm is used).
Please read [the blog article](https://hannes.robur.coop/Posts/DNS) for a more
detailed overview.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,34 @@
version: "10.2.2"
opam-version: "2.0"
maintainer: "team AT robur dot coop"
authors: ["Hannes Mehnert <hannes@mehnert.org>" "Reynir Björnsson <reynir@reynir.dk>"]
homepage: "https://github.com/mirage/ocaml-dns"
doc: "https://mirage.github.io/ocaml-dns/"
dev-repo: "git+https://github.com/mirage/ocaml-dns.git"
bug-reports: "https://github.com/mirage/ocaml-dns/issues"
license: "BSD-2-Clause"
depends: [
"dune" {>= "2.8.0"}
"ocaml" {>= "4.13.0"}
"dns" {= version}
"alcotest" {with-test}
"mirage-crypto" {>= "1.0.0"}
"mirage-crypto-pk" {>= "1.0.0"}
"mirage-crypto-ec" {>= "1.0.0"}
"domain-name" {>= "0.4.0"}
"base64" {with-test & >= "3.0.0"}
"logs" {>= "0.7.0"}
]
build: [
["dune" "subst"] {dev}
["dune" "build" "-p" name "-j" jobs]
["dune" "runtest" "-p" name "-j" jobs] {with-test}
]
synopsis: "DNSSec support for OCaml-DNS"
description: """
DNSSec (DNS security extensions) for OCaml-DNS, including
signing and verifying of RRSIG records.
"""
x-maintenance-intent: [ "(latest)" ]

View file

@ -0,0 +1,236 @@
(* RFC 4648, Section 7 - Base 32 with extended hex alphabet *)
let make_alphabet alphabet =
if String.length alphabet <> 32
then invalid_arg "Length of alphabet must be 32" ;
if String.contains alphabet '='
then invalid_arg "Alphabet can not contain padding character" ;
let emap =
Array.init (String.length alphabet) (fun i -> Char.code alphabet.[i]) in
let dmap = Array.make 256 (-1) in
String.iteri (fun idx chr -> dmap.(Char.code chr) <- idx) alphabet ;
(emap, dmap)
let alphabet = make_alphabet "0123456789ABCDEFGHIJKLMNOPQRSTUV"
let pad_char = '='
let pad_int = int_of_char pad_char
let encode ?(pad = true) str =
let len = String.length str in
(* since String.get_uint8 is OCaml >= 4.13 only *)
let str = Bytes.unsafe_of_string str in
let out_len = (len + 4) / 5 * 8 in
let out = Bytes.make out_len pad_char in
let o1 b1 = b1 lsr 3
and o2 b1 b2 = (b1 land 0x07) lsl 2 + b2 lsr 6
and o3 b2 = (b2 land 0x3E) lsr 1
and o4 b2 b3 = ((b2 land 0x01) lsl 4) + b3 lsr 4
and o5 b3 b4 = (b3 land 0x0F) lsl 1 + b4 lsr 7
and o6 b4 = (b4 land 0x7c) lsr 2
and o7 b4 b5 = (b4 land 0x03) lsl 3 + b5 lsr 5
and o8 b5 = b5 land 0x1F
in
let emit b1 b2 b3 b4 b5 off =
List.iteri (fun idx v -> Bytes.set_uint8 out (off + idx) ((fst alphabet).(v)))
[ o1 b1; o2 b1 b2; o3 b2; o4 b2 b3; o5 b3 b4; o6 b4; o7 b4 b5; o8 b5 ]
in
let rec enc s_off d_off =
if s_off = len then
(* case 1 *) 0
else if s_off = len - 1 then
(* case 2 - 6 padding = *)
let b1 = Bytes.get_uint8 str s_off in
let p1 = o1 b1 and p2 = o2 b1 0 in
Bytes.set_uint8 out d_off ((fst alphabet).(p1));
Bytes.set_uint8 out (d_off + 1) ((fst alphabet).(p2));
6
else if s_off = len - 2 then
(* case 3 - 4 padding = *)
let b1 = Bytes.get_uint8 str s_off
and b2 = Bytes.get_uint8 str (s_off + 1)
in
let p1 = o1 b1 and p2 = o2 b1 b2 and p3 = o3 b2 and p4 = o4 b2 0 in
Bytes.set_uint8 out d_off ((fst alphabet).(p1));
Bytes.set_uint8 out (d_off + 1) ((fst alphabet).(p2));
Bytes.set_uint8 out (d_off + 2) ((fst alphabet).(p3));
Bytes.set_uint8 out (d_off + 3) ((fst alphabet).(p4));
4
else if s_off = len - 3 then
(* case 4 - 3 padding = *)
let b1 = Bytes.get_uint8 str s_off
and b2 = Bytes.get_uint8 str (s_off + 1)
and b3 = Bytes.get_uint8 str (s_off + 2)
in
let p1 = o1 b1 and p2 = o2 b1 b2 and p3 = o3 b2 and p4 = o4 b2 b3 and p5 = o5 b3 0 in
Bytes.set_uint8 out d_off ((fst alphabet).(p1));
Bytes.set_uint8 out (d_off + 1) ((fst alphabet).(p2));
Bytes.set_uint8 out (d_off + 2) ((fst alphabet).(p3));
Bytes.set_uint8 out (d_off + 3) ((fst alphabet).(p4));
Bytes.set_uint8 out (d_off + 4) ((fst alphabet).(p5));
3
else if s_off = len - 4 then
(* case 5 - 1 padding = *)
let b1 = Bytes.get_uint8 str s_off
and b2 = Bytes.get_uint8 str (s_off + 1)
and b3 = Bytes.get_uint8 str (s_off + 2)
and b4 = Bytes.get_uint8 str (s_off + 3)
in
let p1 = o1 b1 and p2 = o2 b1 b2 and p3 = o3 b2 and p4 = o4 b2 b3 and p5 = o5 b3 b4 and p6 = o6 b4 and p7 = o7 b4 0 in
Bytes.set_uint8 out d_off ((fst alphabet).(p1));
Bytes.set_uint8 out (d_off + 1) ((fst alphabet).(p2));
Bytes.set_uint8 out (d_off + 2) ((fst alphabet).(p3));
Bytes.set_uint8 out (d_off + 3) ((fst alphabet).(p4));
Bytes.set_uint8 out (d_off + 4) ((fst alphabet).(p5));
Bytes.set_uint8 out (d_off + 5) ((fst alphabet).(p6));
Bytes.set_uint8 out (d_off + 6) ((fst alphabet).(p7));
1
else
let b1 = Bytes.get_uint8 str s_off in
let b2 = Bytes.get_uint8 str (s_off + 1) in
let b3 = Bytes.get_uint8 str (s_off + 2) in
let b4 = Bytes.get_uint8 str (s_off + 3) in
let b5 = Bytes.get_uint8 str (s_off + 4) in
emit b1 b2 b3 b4 b5 d_off;
enc (s_off + 5) (d_off + 8)
in
let padding_bytes = enc 0 0 in
let out_s = Bytes.unsafe_to_string out in
if pad then out_s else String.sub out_s 0 (out_len - padding_bytes)
(* RFC 4648 section 10
BASE32-HEX("") = ""
BASE32-HEX("f") = "CO======"
BASE32-HEX("fo") = "CPNG===="
BASE32-HEX("foo") = "CPNMU==="
BASE32-HEX("foob") = "CPNMUOG="
BASE32-HEX("fooba") = "CPNMUOJ1"
BASE32-HEX("foobar") = "CPNMUOJ1E8======"
*)
let decode ?(unpadded = false) str =
let ( let* ) = Result.bind in
let* str =
let lmod8 = String.length str mod 8 in
if lmod8 > 0 then
if unpadded then
Ok (str ^ String.make (8 - lmod8) pad_char)
else
Error (`Msg "invalid input length (not divisible by 8)")
else
Ok str
in
let len = String.length str in
let str = Bytes.unsafe_of_string str in
let out_len = len / 8 * 5 in (* max length *)
let out = Bytes.create out_len in
let o1 b1 b2 = b1 lsl 3 + b2 lsr 2
and o2 b2 b3 b4 = (b2 land 0x03) lsl 6 + b3 lsl 1 + b4 lsr 4
and o3 b4 b5 = (b4 land 0x0F) lsl 4 + b5 lsr 1
and o4 b5 b6 b7 = (b5 land 0x01) lsl 7 + b6 lsl 2 + b7 lsr 3
and o5 b7 b8 = (b7 land 0x07) lsl 5 + b8
in
let c ~off idx =
let r = (snd alphabet).(idx) in
if r = -1 then
Error (`Msg ("bad encoding at " ^ string_of_int off))
else
Ok r
in
let emit s_off v1 v2 v3 v4 v5 v6 v7 v8 off =
let* b1 = c ~off:s_off v1 in
let* b2 = c ~off:(s_off + 1) v2 in
let* b3 = c ~off:(s_off + 2) v3 in
let* b4 = c ~off:(s_off + 3) v4 in
let* b5 = c ~off:(s_off + 4) v5 in
let* b6 = c ~off:(s_off + 5) v6 in
let* b7 = c ~off:(s_off + 6) v7 in
let* b8 = c ~off:(s_off + 7) v8 in
Bytes.set_uint8 out off (o1 b1 b2);
Bytes.set_uint8 out (off + 1) (o2 b2 b3 b4);
Bytes.set_uint8 out (off + 2) (o3 b4 b5);
Bytes.set_uint8 out (off + 3) (o4 b5 b6 b7);
Bytes.set_uint8 out (off + 4) (o5 b7 b8);
Ok ()
in
let rec dec s_off d_off =
if s_off = len then
Ok (0, 0)
else
let v1 = Bytes.get_uint8 str s_off
and v2 = Bytes.get_uint8 str (s_off + 1)
and v3 = Bytes.get_uint8 str (s_off + 2)
and v4 = Bytes.get_uint8 str (s_off + 3)
and v5 = Bytes.get_uint8 str (s_off + 4)
and v6 = Bytes.get_uint8 str (s_off + 5)
and v7 = Bytes.get_uint8 str (s_off + 6)
and v8 = Bytes.get_uint8 str (s_off + 7)
in
if v3 = pad_int then
let* b1 = c ~off:s_off v1 in
let* b2 = c ~off:(s_off + 1) v2 in
let p1 = o1 b1 b2 in
Bytes.set_uint8 out d_off p1;
Ok (6, 4)
else if v5 = pad_int then
let* b1 = c ~off:s_off v1 in
let* b2 = c ~off:(s_off + 1) v2 in
let* b3 = c ~off:(s_off + 2) v3 in
let* b4 = c ~off:(s_off + 3) v4 in
let p1 = o1 b1 b2
and p2 = o2 b2 b3 b4
in
Bytes.set_uint8 out d_off p1;
Bytes.set_uint8 out (d_off + 1) p2;
Ok (4, 3)
else if v6 = pad_int then
let* b1 = c ~off:s_off v1 in
let* b2 = c ~off:(s_off + 1) v2 in
let* b3 = c ~off:(s_off + 2) v3 in
let* b4 = c ~off:(s_off + 3) v4 in
let* b5 = c ~off:(s_off + 4) v5 in
let p1 = o1 b1 b2
and p2 = o2 b2 b3 b4
and p3 = o3 b4 b5
in
Bytes.set_uint8 out d_off p1;
Bytes.set_uint8 out (d_off + 1) p2;
Bytes.set_uint8 out (d_off + 2) p3;
Ok (3, 2)
else if v8 = pad_int then
let* b1 = c ~off:s_off v1 in
let* b2 = c ~off:(s_off + 1) v2 in
let* b3 = c ~off:(s_off + 2) v3 in
let* b4 = c ~off:(s_off + 3) v4 in
let* b5 = c ~off:(s_off + 4) v5 in
let* b6 = c ~off:(s_off + 5) v6 in
let* b7 = c ~off:(s_off + 6) v7 in
let p1 = o1 b1 b2
and p2 = o2 b2 b3 b4
and p3 = o3 b4 b5
and p4 = o4 b5 b6 b7
in
Bytes.set_uint8 out d_off p1;
Bytes.set_uint8 out (d_off + 1) p2;
Bytes.set_uint8 out (d_off + 2) p3;
Bytes.set_uint8 out (d_off + 3) p4;
Ok (1, 1)
else
let* () = emit s_off v1 v2 v3 v4 v5 v6 v7 v8 d_off in
dec (s_off + 8) (d_off + 5)
in
let* (pad_bytes, to_remove) = dec 0 0 in
let rec check_pad = function
| 0 -> Ok ()
| n ->
if Bytes.get_uint8 str (len - n) = pad_int then
check_pad (n - 1)
else
Error (`Msg ("expected pad character at " ^ (string_of_int (len - n))))
in
let* () = check_pad pad_bytes in
let out_str = Bytes.unsafe_to_string out in
if to_remove > 0 then
Ok (String.sub out_str 0 (out_len - to_remove))
else
Ok out_str

View file

@ -0,0 +1,952 @@
open Dns
let src = Logs.Src.create "dnssec" ~doc:"DNS Security"
module Log = (val Logs.src_log src : Logs.LOG)
let ( let* ) = Result.bind
module KM = Map.Make(struct type t = Rr_map.k let compare = Rr_map.comparek end)
let pp_km_name_rr_map ppf rrs =
List.iter (fun (name, (rr_map, _)) ->
Fmt.(list ~sep:(any "@.") string) ppf
(List.map (Rr_map.text_b name) (Rr_map.bindings rr_map)))
(Domain_name.Map.bindings rrs)
let guard a e = if a then Ok () else Error e
let root_ds =
(* <KeyDigest id="Klajeyz" validFrom="2017-02-02T00:00:00+00:00">
<KeyTag>20326</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>
E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D
</Digest>
<PublicKey>
AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU=
</PublicKey>
<Flags>257</Flags>
</KeyDigest>
<KeyDigest id="Kmyv6jo" validFrom="2024-07-18T00:00:00+00:00">
<KeyTag>38696</KeyTag>
<Algorithm>8</Algorithm>
<DigestType>2</DigestType>
<Digest>
683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16
</Digest>
<PublicKey>
AwEAAa96jeuknZlaeSrvyAJj6ZHv28hhOKkx3rLGXVaC6rXTsDc449/cidltpkyGwCJNnOAlFNKF2jBosZBU5eeHspaQWOmOElZsjICMQMC3aeHbGiShvZsx4wMYSjH8e7Vrhbu6irwCzVBApESjbUdpWWmEnhathWu1jo+siFUiRAAxm9qyJNg/wOZqqzL/dL/q8PkcRU5oUKEpUge71M3ej2/7CPqpdVwuMoTvoB+ZOT4YeGyxMvHmbrxlFzGOHOijtzN+u1TQNatX2XBuzZNQ1K+s2CXkPIZo7s6JgZyvaBevYtxPvYLw4z9mR7K2vaF18UYH9Z9GNUUeayffKC73PYc=
</PublicKey>
<Flags>257</Flags>
</KeyDigest>
*)
let ds2017 = {
Ds.key_tag = 20326 ;
algorithm = Dnskey.RSA_SHA256 ;
digest_type = SHA256 ;
digest = Ohex.decode "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D" ;
}
and ds2024 = {
Ds.key_tag = 38696 ;
algorithm = Dnskey.RSA_SHA256 ;
digest_type = SHA256 ;
digest = Ohex.decode "683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16" ;
}
in
Rr_map.Ds_set.(add ds2024 (singleton ds2017))
type pub = [
| `P256 of Mirage_crypto_ec.P256.Dsa.pub
| `P384 of Mirage_crypto_ec.P384.Dsa.pub
| `ED25519 of Mirage_crypto_ec.Ed25519.pub
| `RSA of Mirage_crypto_pk.Rsa.pub
]
let pp_pub ppf = function
| `P256 _ -> Fmt.string ppf "P256"
| `P384 _ -> Fmt.string ppf "P384"
| `ED25519 _ -> Fmt.string ppf "ED25519"
| `RSA k -> Fmt.pf ppf "RSA %d bits" (Mirage_crypto_pk.Rsa.pub_bits k)
(* used by DS, RFC 4034 section 5.1.4 *)
let digest algorithm owner dnskey =
let digest : type a. a Digestif.hash -> (string, _) result = fun h ->
let res = Digestif.digest_string h (Dnskey.digest_prep owner dnskey) in
Ok (Digestif.to_raw_string h res) in
match algorithm with
| Ds.SHA1 -> digest Digestif.SHA1
| Ds.SHA256 -> digest Digestif.SHA256
| Ds.SHA384 -> digest Digestif.SHA384
| dt ->
Error (`Extended (`Unsupported_Ds_digest,
Some (Fmt.str "DS %a: unkown digest type: %a"
Domain_name.pp owner Ds.pp_digest_type dt)))
let dnskey_to_pk req_dom { Dnskey.algorithm ; key ; _ } =
let map_ec_err r =
Result.map_error (fun e -> `Msg (Fmt.to_to_string Mirage_crypto_ec.pp_error e)) r
in
match algorithm with
| Dnskey.RSA_SHA1 | Dnskey.RSASHA1_NSEC3_SHA1 | Dnskey.RSA_SHA256 | Dnskey.RSA_SHA512 ->
(* described in RFC 3110 *)
let* () = if String.length key > 0 then Ok () else Error (`Msg "key data too short") in
let e_len = String.get_int8 key 0 in
let data = String.sub key 1 (String.length key - 1) in
let* () = if String.length key > (e_len + 1) then Ok () else Error (`Msg "key data too short") in
let e = String.sub data 0 e_len
and n = String.sub data e_len (String.length data - e_len) in
let e = Mirage_crypto_pk.Z_extra.of_octets_be e
and n = Mirage_crypto_pk.Z_extra.of_octets_be n
in
let* pub = Mirage_crypto_pk.Rsa.pub ~e ~n in
Ok (`RSA pub)
| Dnskey.P256_SHA256 ->
let four = String.make 1 '\004' in
let* pub = map_ec_err (Mirage_crypto_ec.P256.Dsa.pub_of_octets (four ^ key)) in
Ok (`P256 pub)
| Dnskey.P384_SHA384 ->
let four = String.make 1 '\004' in
let* pub = map_ec_err (Mirage_crypto_ec.P384.Dsa.pub_of_octets (four ^ key)) in
Ok (`P384 pub)
| Dnskey.ED25519 ->
let* pub = map_ec_err (Mirage_crypto_ec.Ed25519.pub_of_octets key) in
Ok (`ED25519 pub)
| MD5 | SHA1 | SHA224 | SHA256 | SHA384 | SHA512 | Unknown _ ->
Error (`Extended (`Unsupported_Dnskey_algorithm,
Some (Fmt.str "%a DNSKEY unsupported algorithm: %a"
Domain_name.pp req_dom Dnskey.pp_algorithm algorithm)))
let verify : type a . Ptime.t -> pub -> [`raw] Domain_name.t -> Rrsig.t ->
a Rr_map.key -> a ->
([`raw] Domain_name.t * [`raw] Domain_name.t, [> `Msg of string | `Extended of Extended_error.t ]) result =
fun now key name rrsig t v ->
(* from RFC 4034 section 3.1.8.1 *)
Log.debug (fun m -> m "verifying for %a (with %a / %a)" Domain_name.pp name
pp_pub key
Dnskey.pp_algorithm rrsig.Rrsig.algorithm);
let* algorithm =
match rrsig.Rrsig.algorithm with
| Dnskey.RSA_SHA1 -> Ok `SHA1
| Dnskey.RSASHA1_NSEC3_SHA1 -> Ok `SHA1
| Dnskey.RSA_SHA256 -> Ok `SHA256
| Dnskey.RSA_SHA512 -> Ok `SHA512
| Dnskey.P256_SHA256 -> Ok `SHA256
| Dnskey.P384_SHA384 -> Ok `SHA384
| Dnskey.ED25519 -> Ok `SHA512
| a ->
let msg =
Fmt.str "unsupported signature algorithm %a" Dnskey.pp_algorithm a
in
Error (`Extended (`Other, Some msg))
in
let digest data =
match rrsig.Rrsig.algorithm with
| Dnskey.RSA_SHA1 -> Digestif.SHA1.(digest_string data |> to_raw_string)
| Dnskey.RSASHA1_NSEC3_SHA1 -> Digestif.SHA1.(digest_string data |> to_raw_string)
| Dnskey.RSA_SHA256 -> Digestif.SHA256.(digest_string data |> to_raw_string)
| Dnskey.RSA_SHA512 -> Digestif.SHA512.(digest_string data |> to_raw_string)
| Dnskey.P256_SHA256 -> Digestif.SHA256.(digest_string data |> to_raw_string)
| Dnskey.P384_SHA384 -> Digestif.SHA384.(digest_string data |> to_raw_string)
| Dnskey.ED25519 -> Digestif.SHA512.(digest_string data |> to_raw_string)
| _ -> assert false (* NOTE(dinosaure): prevent by [algorithm] and [let*]. *)
in
let* () =
guard (Ptime.is_later ~than:now rrsig.Rrsig.signature_expiration)
(`Extended (`Signature_expired, None))
in
let* () =
guard (Ptime.is_later ~than:rrsig.Rrsig.signature_inception now)
(`Extended (`Signature_not_yet_valid, None))
in
let* (used_name, data) = Rr_map.prep_for_sig name rrsig t v in
let hashed () = digest data in
let ok_if_true p =
if p then
Ok (used_name, rrsig.Rrsig.signer_name)
else
Error (`Msg "signature verification failed")
in
match key with
| `P256 key ->
let signature =
String.sub rrsig.Rrsig.signature 0 32,
String.sub rrsig.Rrsig.signature 32 (String.length rrsig.Rrsig.signature - 32) in
ok_if_true (Mirage_crypto_ec.P256.Dsa.verify ~key signature (hashed ()))
| `P384 key ->
let signature =
String.sub rrsig.Rrsig.signature 0 48,
String.sub rrsig.Rrsig.signature 48 (String.length rrsig.Rrsig.signature - 48) in
ok_if_true (Mirage_crypto_ec.P384.Dsa.verify ~key signature (hashed ()))
| `ED25519 key ->
let msg = data in
ok_if_true (Mirage_crypto_ec.Ed25519.verify ~key rrsig.Rrsig.signature ~msg)
| `RSA key ->
let hashp = ( = ) algorithm
and msg = `Message data
and signature = rrsig.Rrsig.signature
in
ok_if_true (Mirage_crypto_pk.Rsa.PKCS1.verify ~hashp ~key ~signature msg)
let filter_ds_if_sha2_present ds_set =
(* RFC 4509 - drop SHA1 DS if SHA2 DS are present *)
if Rr_map.Ds_set.exists (fun ds ->
match ds.Ds.digest_type with
| Ds.SHA256 | Ds.SHA384 -> true | _ -> false)
ds_set
then
Rr_map.Ds_set.filter
(fun ds -> not (ds.Ds.digest_type = SHA1))
ds_set
else
ds_set
let validate_ds zone dnskeys ds =
let* used_dnskey =
let key_signing_keys =
Rr_map.Dnskey_set.filter (fun dnsk ->
Dnskey.F.mem `Secure_entry_point dnsk.Dnskey.flags &&
Dnskey.key_tag dnsk = ds.Ds.key_tag)
dnskeys
in
if Rr_map.Dnskey_set.cardinal key_signing_keys = 1 then
Ok (Rr_map.Dnskey_set.choose key_signing_keys)
else
Error (`Msg (string_of_int (Rr_map.Dnskey_set.cardinal key_signing_keys) ^ " key signing keys for " ^ string_of_int ds.key_tag))
in
let* dgst = digest ds.Ds.digest_type zone used_dnskey in
if String.equal ds.Ds.digest dgst then begin
Log.debug (fun m -> m "Found DNSKEY for DS for zone %a (key tag %u)"
Domain_name.pp zone ds.Ds.key_tag);
Ok used_dnskey
end else
Error (`Msg "key signing key couldn't be validated")
let validate_rrsig_keys now dnskeys rrsigs requested_domain t v =
Log.debug (fun m -> m "validating for %a typ %a"
Domain_name.pp requested_domain
Rr_map.ppk (K t));
let keys_rrsigs =
Rr_map.Dnskey_set.fold (fun key acc ->
let key_tag = Dnskey.key_tag key in
let matching =
Rr_map.Rrsig_set.filter (fun rr -> rr.Rrsig.key_tag = key_tag) rrsigs
in
Rr_map.Rrsig_set.fold (fun rr acc -> (key, rr) :: acc) matching acc)
dnskeys []
in
Log.debug (fun m -> m "found %d key-rrsig pairs" (List.length keys_rrsigs));
let verify_signature (key, rrsig) =
let* pkey = dnskey_to_pk requested_domain key in
verify now pkey requested_domain rrsig t v
in
match List.partition Result.is_ok (List.map verify_signature keys_rrsigs) with
| r :: _, _ -> r
| [], e :: _ -> e
| [], [] -> Error (`Msg "no key-rrsig pair found")
let find_soa auth =
match
Domain_name.Map.fold (fun k (rr_map, kms) acc ->
match Rr_map.(find Soa rr_map) with
| Some soa -> Some (Domain_name.raw k, soa, KM.find (K Soa) kms)
| None -> acc)
auth None
with
| None -> Error (`Msg "no SOA in authority")
| Some (name, soa, used_name) ->
if Domain_name.equal name used_name then
Ok (name, soa)
else
Error (`Msg (Fmt.str "SOA owner %a differs from used name %a"
Domain_name.pp name Domain_name.pp used_name))
let is_name_in_chain ~soa_name ~name ~owner nsec =
(* for the last NSEC entry, next_domain is zone itself (thus = soa_name) *)
let next_owner = (snd nsec).Nsec.next_domain in
Domain_name.(compare owner name < 0 &&
(compare name next_owner < 0 ||
compare soa_name next_owner = 0))
let name_in_chain ~soa_name ~name ~owner nsec =
if is_name_in_chain ~soa_name ~name ~owner nsec then
Ok ()
else
Error (`Msg (Fmt.str "name not in chain: owner %a, name %a, next owner %a (soa %a)"
Domain_name.pp owner
Domain_name.pp name
Domain_name.pp (snd nsec).Nsec.next_domain
Domain_name.pp soa_name))
let nsec_chain ~soa_name name auth =
let matches =
Domain_name.Map.filter (fun owner rr_map ->
match Rr_map.find Nsec (fst rr_map) with
| Some nsec ->
Log.debug (fun m -> m "is domain name %a in chain %a (to %a)?"
Domain_name.pp name Domain_name.pp owner
Domain_name.pp (snd nsec).Nsec.next_domain);
is_name_in_chain ~soa_name ~name ~owner nsec
| None -> false)
auth
in
if Domain_name.Map.cardinal matches = 1 then
let owner, rrs = Domain_name.Map.choose matches in
let nsec = Rr_map.get Nsec (fst rrs) in
let used_name = KM.find (K Nsec) (snd rrs) in
if Domain_name.equal used_name owner then
Ok (owner, nsec)
else
Error (`Msg (Fmt.str "used_name %a is not owner %a in NSEC %a"
Domain_name.pp used_name Domain_name.pp owner
Nsec.pp (snd nsec)))
else
Error (`Msg (Fmt.str "couldn't find nsec chain record covering %a in %a"
Domain_name.pp name pp_km_name_rr_map auth))
let is_ent name ~owner nsec =
Domain_name.is_subdomain ~domain:name ~subdomain:(snd nsec).Nsec.next_domain &&
Domain_name.compare owner name < 0
let wildcard_non_existence ~soa_name name auth =
Log.debug (fun m -> m "wildcard non-existence %a (soa %a)"
Domain_name.pp name Domain_name.pp soa_name);
(* for non-existing wildcard NSEC: its owner must be between
<name> and <soa_name> *)
let rec proof_wildcard_absence name =
Log.debug (fun m -> m "proof_wildcards with %a" Domain_name.pp name);
if Domain_name.equal soa_name name then
Ok ()
else
match nsec_chain ~soa_name name auth with
| Ok (owner, nsec) when is_ent name ~owner nsec -> Ok ()
| _ ->
let wc_name = Domain_name.(prepend_label_exn (drop_label_exn name) "*") in
Log.debug (fun m -> m "proof_wildcard_absence %a, wc_name %a"
Domain_name.pp name
Domain_name.pp wc_name);
if Domain_name.Map.exists (fun _owner (rr_map, kms) ->
match Rr_map.find Nsec rr_map with
| Some nsec ->
let owner = KM.find (K Nsec) kms in
is_name_in_chain ~soa_name ~name:wc_name ~owner nsec
| None -> false)
auth
then
proof_wildcard_absence (Domain_name.drop_label_exn wc_name)
else
Error (`Msg (Fmt.str "no denial of existence for %a found"
Domain_name.pp wc_name))
in
proof_wildcard_absence name
let nsec3_hash salt iterations name =
let cs_name = Rr_map.canonical_encoded_name name in
let rec more = function
| 0 -> Digestif.SHA1.(digest_string (cs_name ^ salt) |> to_raw_string)
| k -> Digestif.SHA1.(digest_string ((more (k - 1)) ^ salt) |> to_raw_string)
in
more iterations
let nsec3_hashed_name salt iterations ~soa_name name =
let h = nsec3_hash salt iterations name in
Domain_name.prepend_label_exn soa_name (Base32.encode h)
let nsec3_rrs auth =
let nsec3_map =
(* filter out any non-nsec3 rrs and those where label_count doesn't match *)
Domain_name.Map.filter (fun name (rr_map, kms) ->
Rr_map.exists (function
| B (Nsec3, (_, nsec3)) ->
begin match nsec3.Nsec3.flags with
| Some `Opt_out | None -> true
| Some `Unknown _ -> false
end
| _ -> false) rr_map &&
Domain_name.equal name (KM.find (K Nsec3) kms))
auth
in
if Domain_name.Map.is_empty nsec3_map then
Error (`Msg "no NSEC3 resource record found")
else begin
Log.debug (fun m -> m "nsec3 non-existence %d" (Domain_name.Map.cardinal nsec3_map));
let Nsec3.{ iterations ; salt ; _ } =
let _, (rrs, _) = Domain_name.Map.choose nsec3_map in
snd (Rr_map.get Nsec3 rrs)
in
if iterations > 150 then
Error (`Msg "NSEC3 iterations greater than 150, ignoring")
else
Ok (nsec3_map, salt, iterations)
end
let nsec3_closest_encloser nsec3_map salt iterations ~soa_name name =
let rec find_it chop name =
let hashed_name = nsec3_hashed_name ~soa_name salt iterations name in
match Domain_name.Map.find hashed_name nsec3_map with
| Some (rrs, _) -> Ok (chop, name, Rr_map.get Nsec3 rrs)
| None ->
let* parent = Domain_name.drop_label name in
let chopped = Domain_name.get_label_exn name 0 in
find_it chopped parent
in
let* (last_chop, closest_encloser, closest_encloser_nsec) = find_it "" name in
Log.debug (fun m -> m "last chop %s closest encloser %a (hashed %a)"
last_chop Domain_name.pp closest_encloser
Domain_name.pp (nsec3_hashed_name ~soa_name salt iterations closest_encloser));
(* 8.3: DNAME bit must not be set, and NS may only be set if SOA bit is set *)
(* TODO DNAME *)
let* () =
let types = (snd closest_encloser_nsec).Nsec3.types in
if Bit_map.mem (Rr_map.to_int Ns) types then
if not (Bit_map.mem (Rr_map.to_int Soa) types) then
Error (`Msg (Fmt.str "nsec3 with NS but not SOA %a %a"
Domain_name.pp closest_encloser
Nsec3.pp (snd closest_encloser_nsec)))
else
(* RFC 5155 8.9: presence of NS implies absence of DNAME *)
Ok ()
else if Bit_map.mem (*DNAME*)39 types then
Error (`Msg (Fmt.str "nsec3 with DNAME %a %a"
Domain_name.pp closest_encloser
Nsec3.pp (snd closest_encloser_nsec)))
else
Ok ()
in
(* verify existence of nsec3 where owner < next_closer < next_owner_hashed *)
let next_closer = Domain_name.prepend_label_exn closest_encloser last_chop in
let next_closer_hashed = nsec3_hashed_name ~soa_name salt iterations next_closer in
Ok (closest_encloser, next_closer, next_closer_hashed)
let nsec3_between nsec3_map ~soa_name hashed_name =
Log.debug (fun m -> m "nsec3 between %a" Domain_name.pp hashed_name);
let m =
Domain_name.Map.filter (fun name (rrs, _) ->
if Domain_name.compare name hashed_name < 0 then begin
Log.debug (fun m -> m "(%a) yes %a" Domain_name.pp hashed_name
Domain_name.pp name);
let _, nsec3 = Rr_map.get Nsec3 rrs in
let hashed_next_owner =
Domain_name.prepend_label_exn soa_name
(Base32.encode nsec3.Nsec3.next_owner_hashed)
in
Log.debug (fun m -> m "(%a) comparing with %a: %d"
Domain_name.pp hashed_name
Domain_name.pp hashed_next_owner
(Domain_name.compare hashed_name hashed_next_owner));
Domain_name.compare hashed_name hashed_next_owner < 0
end else
false)
nsec3_map
in
if Domain_name.Map.cardinal m = 1 then
Ok (Domain_name.Map.choose m)
else begin
Log.debug (fun m -> m "nsec3 between %a no" Domain_name.pp hashed_name);
Error (`Msg (Fmt.str "no NSEC3 with owner < %a < next_owner_hashed"
Domain_name.pp hashed_name))
end
let nsec3_non_existence name ~soa_name auth =
Log.debug (fun m -> m "nsec3 non-existence %a (zone %a)"
Domain_name.pp name Domain_name.pp soa_name);
let* (nsec3_map, salt, iterations) = nsec3_rrs auth in
let* (closest_encloser, _next_closer, hashed_next_closer) =
nsec3_closest_encloser nsec3_map salt iterations ~soa_name name
in
let* (_, (rrs, _)) = nsec3_between nsec3_map ~soa_name hashed_next_closer in
let nsec_next_closer = Rr_map.get Nsec3 rrs in
let opt_out =
match (snd nsec_next_closer).Nsec3.flags with
| Some `Opt_out -> true
| Some `Unknown _ | None -> false
in
Log.debug (fun m -> m "next_closer %a proved, opt out %B"
Domain_name.pp hashed_next_closer opt_out);
(* TODO 8.5 and 8.6!? *)
if opt_out then
Ok nsec_next_closer
else
(* verify existence of nsec3 where owner < wc < next_owner_hashed *)
let wc = Domain_name.prepend_label_exn closest_encloser "*" in
let hashed_wc = nsec3_hashed_name ~soa_name salt iterations wc in
let* _ = nsec3_between nsec3_map ~soa_name hashed_wc in
Ok nsec_next_closer
let nsec3_chain ~soa_name ~wc_name ~name auth =
Log.debug (fun m -> m "nsec3 chain soa %a wc %a name %a"
Domain_name.pp soa_name Domain_name.pp wc_name
Domain_name.pp name);
let closest_encloser = Domain_name.drop_label_exn wc_name in
let next_closer =
let lbl_idx = Domain_name.count_labels closest_encloser in
let lbl = Domain_name.get_label_exn ~rev:true name lbl_idx in
Domain_name.prepend_label_exn closest_encloser lbl
in
Log.debug (fun m -> m "next_closer %a" Domain_name.pp next_closer);
let* (nsec3_map, salt, iterations) = nsec3_rrs auth in
let hashed_next_closer =
nsec3_hashed_name ~soa_name salt iterations next_closer
in
nsec3_between nsec3_map ~soa_name hashed_next_closer
let nsec_non_existence name ~soa_name auth =
let* _ = nsec_chain ~soa_name name auth in
wildcard_non_existence ~soa_name name auth
let no_domain name auth =
(* no domain:
- a SOA from a parent (zone), plus RRSIG
- an NSEC for non-existing wildcard, plus rrsig
- a NSEC <prev domain> .. <next-domain>, plus rrsig
-> ensure requested_domain is between these domains *)
let* (soa_name, soa) = find_soa auth in
let* () =
if Domain_name.is_subdomain ~subdomain:name ~domain:soa_name then
Ok ()
else
Error (`Msg (Fmt.str "question %a is not subdomain of SOA %a"
Domain_name.pp name Domain_name.pp soa_name))
in
match
nsec_non_existence name ~soa_name auth,
nsec3_non_existence name ~soa_name auth
with
| Ok (), _ | _, Ok _ -> Ok (soa_name, soa)
| Error _ as e, _ -> e
let nsec_no_data ~soa_name name k auth =
match Domain_name.Map.find name auth with
| Some (rr_map, kms) when Rr_map.mem Nsec rr_map ->
let nsec = Rr_map.get Nsec rr_map
and nsec_owner = KM.find (K Nsec) kms
in
let* () =
if Domain_name.equal nsec_owner name then
Ok ()
else
Error (`Msg (Fmt.str "nsec owner %a is not name %a"
Domain_name.pp nsec_owner
Domain_name.pp name))
in
if Bit_map.mem (Rr_map.to_int k) (snd nsec).Nsec.types then
Error (`Msg (Fmt.str "nsec claims type %a to be present" Rr_map.ppk (K k)))
else if Bit_map.mem (Rr_map.to_int Cname) (snd nsec).Nsec.types then
Error (`Msg (Fmt.str "nsec claims CNAME to be present"))
else
Ok ()
| _ ->
(* nsec in chain ++ wildcard nsec *)
let* _ = nsec_chain ~soa_name name auth in
let rec find_wc name =
if Domain_name.is_subdomain ~domain:soa_name ~subdomain:name then
let wc_name = Domain_name.prepend_label_exn name "*" in
Log.debug (fun m -> m "looking for %a" Domain_name.pp wc_name);
match Domain_name.Map.find wc_name auth with
| Some (rr_map, kms) when Rr_map.mem Nsec rr_map ->
let nsec = Rr_map.get Nsec rr_map
and nsec_owner = KM.find (K Nsec) kms
in
Ok (wc_name, nsec, nsec_owner)
| _ ->
let* name = Domain_name.drop_label name in
find_wc name
else
Error (`Msg "no wildcard nsec found")
in
match find_wc name with
| Ok (wc_name, wc_nsec, wc_nsec_owner) ->
let* () =
if Domain_name.equal wc_nsec_owner wc_name then
Ok ()
else
Error (`Msg (Fmt.str "bad wildcard nsec, wc_name %a nsec_owner %a"
Domain_name.pp wc_name Domain_name.pp wc_nsec_owner))
in
if Bit_map.mem (Rr_map.to_int k) (snd wc_nsec).Nsec.types then
Error (`Msg (Fmt.str "nsec claims type %a to be present" Rr_map.ppk (K k)))
else
Ok ()
| Error _ ->
wildcard_non_existence ~soa_name name auth
let nsec3_no_data ~soa_name name k auth =
Log.debug (fun m -> m "nsec3 no data %a (zone %a)"
Domain_name.pp name Domain_name.pp soa_name);
let* (nsec3_map, salt, iterations) = nsec3_rrs auth in
let hashed_name = nsec3_hashed_name ~soa_name salt iterations name in
match Domain_name.Map.find hashed_name nsec3_map with
| Some (rr_map, _) ->
let _, nsec3 = Rr_map.get Nsec3 rr_map in
if Bit_map.mem (Rr_map.to_int k) nsec3.Nsec3.types then
Error (`Msg (Fmt.str "nsec3 claims type %a to be present" Rr_map.ppk (K k)))
else if Bit_map.mem (Rr_map.to_int Cname) nsec3.Nsec3.types then
Error (`Msg (Fmt.str "nsec3 claims type Cname to be present"))
else
Ok ()
| None ->
let* (_closest_encloser, _next_closer, hashed_next_closer) =
nsec3_closest_encloser nsec3_map salt iterations ~soa_name name
in
let* (_, (rrs, _)) = nsec3_between nsec3_map ~soa_name hashed_next_closer in
let nsec_next_closer = Rr_map.get Nsec3 rrs in
let opt_out =
match (snd nsec_next_closer).Nsec3.flags with
| Some `Opt_out -> true
| Some `Unknown _ | None -> false
in
Log.debug (fun m -> m "next_closer %a proved, opt out %B"
Domain_name.pp hashed_next_closer opt_out);
if opt_out then
Ok ()
else
Error (`Msg "no NSEC3, and next_closer has no opt-out")
let no_data name k auth =
(* no data:
- SOA + RRSIG
- (NSEC for name (and not for type = k) OR wildcard NSEC) + RRSIG
*)
let* (soa_name, soa) = find_soa auth in
let* () =
if Domain_name.is_subdomain ~subdomain:name ~domain:soa_name then
Ok ()
else
Error (`Msg (Fmt.str "name %a is not a subdomain of soa %a"
Domain_name.pp name Domain_name.pp soa_name))
in
match
nsec_no_data ~soa_name name k auth,
nsec3_no_data ~soa_name name k auth
with
| Ok (), _ | _, Ok () -> Ok (soa_name, soa)
| Error _ as e, _ -> e
let has_delegation name_rr_map name =
let rrs =
Domain_name.Map.filter (fun owner_name rrs ->
Domain_name.is_subdomain ~domain:owner_name ~subdomain:name &&
Rr_map.mem Ns rrs) name_rr_map
in
Log.debug (fun m -> m "has_delegation with %d in %a"
(Domain_name.Map.cardinal rrs)
Name_rr_map.pp name_rr_map);
if Domain_name.Map.cardinal rrs = 1 then
Some (Domain_name.Map.choose rrs)
else
None
let validate_delegation signer_name auth (zname, rrs) =
let _, ns = Rr_map.get Ns rrs in
match Domain_name.Map.find zname auth with
| Some (rrs, kms) when Rr_map.mem Ds rrs ->
let ds = snd (Rr_map.get Ds rrs) in
let used_name = KM.find (K Ds) kms in
if not (Domain_name.equal used_name zname) then
Error (`Msg (Fmt.str "owner %a of DS %a does not match used name %a"
Domain_name.pp zname
Fmt.(list ~sep:(any ", ") Ds.pp)
(Rr_map.Ds_set.elements ds)
Domain_name.pp used_name))
else
Ok (`Signed_delegation (zname, ns, ds))
| Some (rrs, kms) when Rr_map.mem Nsec rrs ->
let nsec = snd (Rr_map.get Nsec rrs) in
let used_name = KM.find (K Nsec) kms in
if not (Domain_name.equal used_name zname) then
Error (`Msg (Fmt.str "owner %a of Nsec %a does not match used name %a"
Domain_name.pp zname
Nsec.pp nsec
Domain_name.pp used_name))
else if
(not (Bit_map.mem (Rr_map.to_int Ds) nsec.Nsec.types)) &&
Bit_map.mem (Rr_map.to_int Ns) nsec.Nsec.types
then
Ok (`Unsigned_delegation (zname, ns))
else
Error (`Msg (Fmt.str "NSEC present for %a (%a), but either has DS or no NS bits"
Domain_name.pp zname Nsec.pp nsec))
| _ ->
let soa_name = Option.value ~default:Domain_name.root signer_name in
let* nsec3 = nsec3_non_existence zname ~soa_name auth in
if (snd nsec3).Nsec3.flags = Some `Opt_out then
Ok (`Unsigned_delegation (zname, ns))
else
Error (`Msg (Fmt.str "NSEC3 for closest encloser %a present %a, but not opt-out"
Domain_name.pp zname
Nsec3.pp (snd nsec3)))
let maybe_validate_wildcard_answer signer_name auth kms name k =
let used_name = KM.find (K k) kms in
if Domain_name.equal used_name name then
Ok ()
else begin
(* RFC 4035 5.3.4 - verify in authority the wildcard-expanded
positive reply (no direct match) *)
(* RFC 5155 8.8 - there's a candidate closest encloser for qname
(the used_name without "*") - need to verify existence of a nsec3
covering next_closer name to qname *)
(match signer_name with
| None -> Log.warn (fun m -> m "no signer name provided")
| Some _ -> ());
let soa_name = Option.value ~default:Domain_name.root signer_name in
match
nsec_chain ~soa_name name auth,
nsec3_chain ~soa_name ~wc_name:used_name ~name auth
with
| Ok _, _ | _, Ok _ -> Ok ()
| Error _ as e, _ -> e
end
let validate_answer :
type a. ?signer_name:[`raw] Domain_name.t ->
[`raw] Domain_name.t -> a Rr_map.rr ->
(Rr_map.t * [`raw] Domain_name.t KM.t) Domain_name.Map.t ->
(Rr_map.t * [`raw] Domain_name.t KM.t) Domain_name.Map.t ->
Name_rr_map.t ->
(a,
[> `Cname of [`raw] Domain_name.t
| `Unsigned_delegation of [`raw] Domain_name.t * Domain_name.Host_set.t
| `Signed_delegation of [`raw] Domain_name.t * Domain_name.Host_set.t * Rr_map.Ds_set.t
| `No_data of [`raw] Domain_name.t * Soa.t
| `Msg of string ]) result =
fun ?signer_name name k answer auth raw_auth ->
Log.debug (fun m -> m "validating %a (%a)"
Domain_name.pp name Rr_map.ppk (K k));
match Domain_name.Map.find name answer with
| None ->
(* left are two options: no data OR delegation *)
Option.fold
~none:(
let* (soa_name, soa) = no_data name k auth in
Log.debug (fun m -> m "validated no data");
Error (`No_data (soa_name, soa)))
~some:(fun x ->
let* r = validate_delegation signer_name auth x in
Error r)
(has_delegation raw_auth name)
| Some (rr_map, kms) ->
match Rr_map.find k rr_map with
| Some rrs ->
let* () = maybe_validate_wildcard_answer signer_name auth kms name k in
Ok rrs
| None ->
match Rr_map.find Cname rr_map with
| None ->
let* (soa_name, soa) = no_data name k auth in
Log.debug (fun m -> m "validated no data");
Error (`No_data (soa_name, soa))
| Some rr ->
let* () = maybe_validate_wildcard_answer signer_name auth kms name Cname in
Log.info (fun m -> m "verified CNAME to %a" Domain_name.pp (snd rr));
Error (`Cname (snd rr))
type err = [
| `Cname of [ `raw ] Domain_name.t
| `Unsigned_delegation of [`raw] Domain_name.t * Domain_name.Host_set.t
| `Signed_delegation of [`raw] Domain_name.t * Domain_name.Host_set.t * Rr_map.Ds_set.t
| `No_data of [ `raw ] Domain_name.t * Dns.Soa.t
| `No_domain of [ `raw ] Domain_name.t * Dns.Soa.t
| `Msg of string
]
let pp_err ppf = function
| `Cname alias -> Fmt.pf ppf "cname %a" Domain_name.pp alias
| `Unsigned_delegation (owner, ns) ->
Fmt.pf ppf "unsigned delegation of %a to %a"
Domain_name.pp owner
Fmt.(list ~sep:(any ", ") Domain_name.pp)
(Domain_name.Host_set.elements ns)
| `Signed_delegation (owner, ns, ds) ->
Fmt.pf ppf "signed delegation of %a to %a (DS %a)"
Domain_name.pp owner
Fmt.(list ~sep:(any ", ") Domain_name.pp)
(Domain_name.Host_set.elements ns)
Fmt.(list ~sep:(any ", ") Ds.pp)
(Rr_map.Ds_set.elements ds)
| `No_data (name, soa) ->
Fmt.pf ppf "no data %a %a" Domain_name.pp name Soa.pp soa
| `No_domain (name, soa) ->
Fmt.pf ppf "no domain %a %a" Domain_name.pp name Soa.pp soa
| `Msg m -> Fmt.pf ppf "error %s" m
let fold_option a b =
match a, b with
| None, None -> None
| Some a, None -> Some a
| None, Some b -> Some b
| Some a, Some b ->
if not (Domain_name.equal a b) then
Log.warn (fun m -> m "different signer names %a and %a"
Domain_name.pp a Domain_name.pp b);
Some a
(* to avoid missing a signature check, and also checking the signature
multiple times, first verify all signatures in the map *)
let check_signatures now dnskeys map =
(* the result is again a map, but with an additional nesting to track the
used name (wildcard signatures) *)
Domain_name.Map.fold (fun name rr_map (signer_name, acc) ->
let rrsig_ttl, rrsigs =
Option.value ~default:(0l, Rr_map.Rrsig_set.empty)
(Rr_map.find Rrsig rr_map)
in
let signer_name, rrs = Rr_map.fold (fun b ((signer_name, (rrs, names)) as acc) ->
match b with
| B (Rr_map.Rrsig, _) -> acc
| B (k, v) ->
let int = Rr_map.to_int k in
let rrsigs =
Rr_map.Rrsig_set.filter
(fun rrsig -> rrsig.Rrsig.type_covered = int)
rrsigs
in
if Rr_map.Rrsig_set.is_empty rrsigs then
Log.warn (fun m -> m "couldn't find RRSIG for %a %a"
Domain_name.pp name Rr_map.pp_b b);
match validate_rrsig_keys now dnskeys rrsigs name k v with
| Ok (used_name, signer_name') ->
let signer = fold_option signer_name (Some signer_name') in
let rrs = Rr_map.add k v rrs in
let rrs =
Rr_map.update Rrsig (function
| None -> Some (rrsig_ttl, rrsigs)
| Some (_, s) ->
Some (rrsig_ttl, Rr_map.Rrsig_set.union s rrsigs))
rrs
in
signer, (rrs, KM.add (Rr_map.K k) used_name names)
| Error `Msg msg ->
Log.warn (fun m -> m "RRSIG verification for %a %a failed: %s"
Domain_name.pp name Rr_map.pp_b b msg);
acc
| Error `Extended e ->
Log.warn (fun m -> m "RRSIG verification for %a %a failed: %a"
Domain_name.pp name Rr_map.pp_b b
Extended_error.pp e);
acc)
rr_map (signer_name, (Rr_map.empty, KM.empty))
in
signer_name,
if Rr_map.is_empty (fst rrs) then
acc
else
Domain_name.Map.add name rrs acc)
map (None, Domain_name.Map.empty)
let verify_reply : type a. ?fuel:int -> ?follow_cname:bool ->
Ptime.t -> Rr_map.Dnskey_set.t -> [`raw] Domain_name.t -> a Rr_map.rr ->
Packet.reply ->
(a,
[> `Cname of [ `raw ] Domain_name.t
| `Unsigned_delegation of [`raw] Domain_name.t * Domain_name.Host_set.t
| `Signed_delegation of [`raw] Domain_name.t * Domain_name.Host_set.t * Rr_map.Ds_set.t
| `No_data of [ `raw ] Domain_name.t * Dns.Soa.t
| `No_domain of [ `raw ] Domain_name.t * Dns.Soa.t
| `Msg of string ]) result =
fun ?(fuel = 20) ?(follow_cname = true) now dnskeys name k reply ->
Log.debug (fun m -> m "verifying %a (%a)"
Domain_name.pp name Rr_map.ppk (K k));
match reply with
| `Answer (answer, authority) ->
let signer_name, signed_answer = check_signatures now dnskeys answer
and signer_name2, signed_authority = check_signatures now dnskeys authority
in
let signer_name = fold_option signer_name signer_name2 in
begin
let rec more ~fuel name =
if fuel = 0 then
Error (`Msg "too many CNAME redirections")
else
match validate_answer ?signer_name name k signed_answer signed_authority authority with
| Error `Cname other when follow_cname ->
more ~fuel:(fuel - 1) other
| r -> r
in
more ~fuel name
end
| `Rcode_error (NXDomain, Query, Some (answer, authority)) ->
let signer_name, _answer = check_signatures now dnskeys answer
and signer_name2, authority = check_signatures now dnskeys authority
in
let _signer_name = fold_option signer_name signer_name2 in
let* (soa_name, soa) = no_domain name authority in
Error (`No_domain (soa_name, soa))
| r ->
Error (`Msg (Fmt.str "unexpected reply: %a" Packet.pp_reply r))
let remove_km map =
Domain_name.Map.fold
(fun name (rrs, _) acc -> Domain_name.Map.add name rrs acc)
map Domain_name.Map.empty
let verify_packet now dnskeys packet =
let qname = fst packet.Packet.question in
let* data =
match packet.Packet.data with
| `Answer (answer, authority) ->
let signer_name, signed_answer = check_signatures now dnskeys answer
and signer_name2, signed_auth = check_signatures now dnskeys authority
in
let signer_name = fold_option signer_name signer_name2 in
let ans = remove_km signed_answer and auth = remove_km signed_auth in
begin match Domain_name.Map.find qname signed_answer with
| None ->
Option.fold
~none:(
match snd packet.question with
| `K K k ->
let* _ = no_data qname k signed_auth in
Ok (`Answer (ans, auth))
| _ -> Error (`Msg "qtype is not a valid typ"))
~some:(fun (zname, rrs) ->
(* add (unsigned!) rrs back into auth *)
let* _ = validate_delegation signer_name signed_auth (zname, rrs) in
let ns_rr = Rr_map.get Ns rrs in
let auth =
Domain_name.Map.update zname
(function
| None -> Some Rr_map.(singleton Ns ns_rr)
| Some rrs -> Some Rr_map.(add Ns ns_rr rrs))
auth
in
Ok (`Answer (ans, auth))
)
(has_delegation authority qname)
| Some (rrs, kms) ->
begin match snd packet.question with
| `K K k ->
begin match Rr_map.find k rrs, Rr_map.find Cname rrs with
| None, None ->
let* _ = no_data qname k signed_auth in
Ok (`Answer (ans, auth))
| Some _, _ ->
let* _ = maybe_validate_wildcard_answer signer_name signed_auth kms qname k in
Ok (`Answer (ans, auth))
| _, Some _ ->
let* _ = maybe_validate_wildcard_answer signer_name signed_auth kms qname Cname in
Ok (`Answer (ans, auth))
end
| _ -> Error (`Msg "qtype is not a valid typ")
end
end
| `Rcode_error (Rcode.NXDomain, Query, Some (answer, authority)) ->
let signer_name, signed_answer = check_signatures now dnskeys answer
and signer_name2, signed_authority = check_signatures now dnskeys authority
in
let _signer_name = fold_option signer_name signer_name2 in
let* _ = no_domain qname signed_authority in
let answer = remove_km signed_answer and auth = remove_km signed_authority in
Ok (`Rcode_error (Rcode.NXDomain, Opcode.Query, Some (answer, auth)))
| `Rcode_error (rc, op, Some (ans, aut)) ->
let signer_name, signed_answer = check_signatures now dnskeys ans
and signer_name2, signed_authority = check_signatures now dnskeys aut
in
let _signer_name = fold_option signer_name signer_name2 in
let answer = remove_km signed_answer and auth = remove_km signed_authority in
Ok (`Rcode_error (rc, op, Some (answer, auth)))
| `Rcode_error (rc, op, None) -> Ok (`Rcode_error (rc, op, None))
| x -> Ok x
in
Ok (Packet.create ~additional:packet.additional ?edns:packet.edns
?tsig:packet.tsig packet.header packet.question data)

View file

@ -0,0 +1,5 @@
(library
(name dnssec)
(public_name dnssec)
(wrapped false)
(libraries mirage-crypto mirage-crypto-pk mirage-crypto-ec dns logs domain-name))

View file

@ -0,0 +1,4 @@
(lang dune 2.8)
(name dns)
(version v10.2.2)
(formatting disabled)

View file

@ -0,0 +1,368 @@
open Lwt.Infix
module IM = Map.Make(Int)
let src = Logs.Src.create "dns_client_lwt" ~doc:"effectful DNS lwt layer"
module Log = (val Logs.src_log src : Logs.LOG)
module Transport : Dns_client.S
with type io_addr = [ `Plaintext of Ipaddr.t * int | `Tls of Tls.Config.client * Ipaddr.t * int ]
and type +'a io = 'a Lwt.t
and type stack = Happy_eyeballs_lwt.t
= struct
type io_addr = [ `Plaintext of Ipaddr.t * int | `Tls of Tls.Config.client * Ipaddr.t * int ]
type +'a io = 'a Lwt.t
type stack = Happy_eyeballs_lwt.t
type nameservers =
| Static of io_addr list
| Resolv_conf of {
mutable nameservers : io_addr list;
mutable digest : Digest.t option
}
type t = {
nameservers : nameservers;
timeout_ns : int64 ;
(* TODO: avoid race, use a mvar instead of condition *)
mutable fd : [ `Plain of Lwt_unix.file_descr | `Tls of Tls_lwt.Unix.t ] option ;
mutable connected_condition : (unit, [ `Msg of string ]) result Lwt_condition.t option ;
mutable requests : (string * (string, [ `Msg of string ]) result Lwt_condition.t) IM.t ;
he : Happy_eyeballs_lwt.t ;
}
type context = t
let nameserver_ips = function
| Static nameservers -> nameservers
| Resolv_conf { nameservers; _ } -> nameservers
let read_file file =
try
let fh = open_in file in
try
let content = really_input_string fh (in_channel_length fh) in
close_in_noerr fh ;
Ok content
with _ ->
close_in_noerr fh;
Error (`Msg ("Error reading file: " ^ file))
with _ -> Error (`Msg ("Error opening file " ^ file))
let clock = Mtime_clock.elapsed_ns
let close_socket fd =
Lwt.catch (fun () -> Lwt_unix.close fd) (fun _ -> Lwt.return_unit)
let authenticator =
let authenticator_ref = ref None in
fun () ->
match !authenticator_ref with
| Some x -> x
| None -> match Ca_certs.authenticator () with
| Ok a -> authenticator_ref := Some a ; a
| Error `Msg m -> invalid_arg ("failed to load trust anchors: " ^ m)
let decode_resolv_conf data =
let ( let* ) = Result.bind in
let authenticator = authenticator () in
let* ns = Dns_resolvconf.parse data in
match
List.flatten
(List.map
(fun (`Nameserver ip) ->
match Tls.Config.client ~authenticator ~ip () with
| Ok tls -> [ `Tls (tls, ip, 853) ; `Plaintext (ip, 53) ]
| Error `Msg msg ->
Log.err (fun m -> m "creating TLS configuratio for %a: %s"
Ipaddr.pp ip msg);
[ `Plaintext (ip, 53) ])
ns)
with
| [] -> Error (`Msg "no nameservers in resolv.conf")
| ns -> Ok ns
let resolv_conf () =
let ( let* ) = Result.bind in
let* data = read_file "/etc/resolv.conf" in
let* ns =
Result.map_error
(function `Msg msg ->
Log.warn (fun m -> m "error %s decoding resolv.conf %S" msg data);
`Msg msg)
(decode_resolv_conf data)
in
Ok (ns, Digest.string data)
let default_resolver () =
let authenticator = authenticator () in
let peer_name = Dns_client.default_resolver_hostname in
let tls_config =
match Tls.Config.client ~authenticator ~peer_name () with
| Ok cfg -> cfg
| Error `Msg msg -> invalid_arg msg
in
List.map (fun ip -> `Tls (tls_config, ip, 853)) Dns_client.default_resolvers
let maybe_resolv_conf t =
match t.nameservers with
| Static _ -> ()
| Resolv_conf resolv_conf ->
let needs_update =
match read_file "/etc/resolv.conf", resolv_conf.digest with
| Ok data, Some dgst ->
let dgst' = Digest.string data in
if Digest.equal dgst' dgst then
`No
else
`Data (data, dgst')
| Ok data, None ->
let digest = Digest.string data in
`Data (data, digest)
| Error _, None ->
`No
| Error `Msg msg, Some _ ->
Log.warn (fun m -> m "error reading /etc/resolv.conf: %s" msg);
`Default
in
match needs_update with
| `No -> ()
| `Default ->
resolv_conf.digest <- None;
resolv_conf.nameservers <- default_resolver ()
| `Data (data, dgst) ->
match decode_resolv_conf data with
| Ok ns ->
resolv_conf.digest <- Some dgst;
resolv_conf.nameservers <- ns
| Error `Msg msg ->
Log.warn (fun m -> m "error %s decoding resolv.conf: %S" msg data);
resolv_conf.digest <- None;
resolv_conf.nameservers <- default_resolver ()
let create ?nameservers ~timeout happy_eyeballs =
let nameservers =
match nameservers with
| Some (`Udp, _) -> invalid_arg "UDP is not supported"
| Some (`Tcp, ns) -> Static ns
| None ->
match resolv_conf () with
| Error _ -> Resolv_conf { nameservers = default_resolver (); digest = None }
| Ok (ips, digest) -> Resolv_conf { nameservers = ips; digest = Some digest }
in
{
nameservers ;
timeout_ns = timeout ;
fd = None ;
connected_condition = None ;
requests = IM.empty ;
he = happy_eyeballs ;
}
let nameservers { nameservers; _ } = `Tcp, nameserver_ips nameservers
let rng = Mirage_crypto_rng.generate ?g:None
let with_timeout timeout f =
let timeout =
Lwt_unix.sleep (Duration.to_f timeout) >|= fun () ->
Error (`Msg "DNS request timeout")
in
Lwt.pick [ f ; timeout ]
let close _ = Lwt.return_unit
let send_query fd tx =
Lwt.catch (fun () ->
match fd with
| `Plain fd ->
Lwt_unix.send fd (Bytes.unsafe_of_string tx) 0
(String.length tx) [] >>= fun res ->
if res <> String.length tx then
Lwt_result.fail (`Msg ("oops" ^ (string_of_int res)))
else
Lwt_result.return ()
| `Tls fd ->
Lwt_result.ok (Tls_lwt.Unix.write fd tx))
(fun e -> Lwt.return (Error (`Msg (Printexc.to_string e))))
let send_recv (t : context) tx =
if String.length tx > 4 then
match t.fd with
| None -> Lwt.return (Error (`Msg "no connection to the nameserver established"))
| Some fd ->
let id = String.get_uint16_be tx 2 in
with_timeout t.timeout_ns
(let open Lwt_result.Infix in
send_query fd tx >>= fun () ->
let cond = Lwt_condition.create () in
t.requests <- IM.add id (tx, cond) t.requests;
let open Lwt.Infix in
Lwt_condition.wait cond >|= fun data ->
match data with Ok _ | Error `Msg _ as r -> r) >|= fun r ->
t.requests <- IM.remove id t.requests;
r
else
Lwt.return (Error (`Msg "invalid DNS packet (data length <= 4)"))
let bind = Lwt.bind
let lift = Lwt.return
let rec read_loop ?(linger = "") (t : t) fd =
Lwt.catch (fun () ->
match fd with
| `Plain fd ->
let recv_buffer = Bytes.create 2048 in
Lwt_unix.recv fd recv_buffer 0 (Bytes.length recv_buffer) [] >|= fun r ->
(r, recv_buffer)
| `Tls fd ->
let recv_buffer = Bytes.create 2048 in
Tls_lwt.Unix.read fd recv_buffer >|= fun r ->
(r, recv_buffer))
(fun e ->
Log.err (fun m -> m "error %s reading from resolver" (Printexc.to_string e));
Lwt.return (0, Bytes.empty)) >>= function
| (0, _) ->
(match fd with
| `Plain fd -> close_socket fd
| `Tls fd -> Tls_lwt.Unix.close fd) >|= fun () ->
t.fd <- None;
if not (IM.is_empty t.requests) then
Log.info (fun m -> m "end of file reading from resolver")
| (read_len, cs) ->
let rec handle_data data =
let cs_len = String.length data in
if cs_len > 2 then
let len = String.get_uint16_be data 0 in
if cs_len - 2 >= len then
let packet, rest =
if cs_len - 2 = len
then data, ""
else String.sub data 0 (len + 2), String.sub data (len + 2) (String.length data - len - 2)
in
let id = String.get_uint16_be packet 2 in
(match IM.find_opt id t.requests with
| None -> Log.warn (fun m -> m "received unsolicited data, ignoring")
| Some (_, cond) ->
Lwt_condition.broadcast cond (Ok packet));
handle_data rest
else
read_loop ~linger:data t fd
else
read_loop ~linger:data t fd
in
let cs = String.sub (Bytes.unsafe_to_string cs) 0 read_len in
handle_data (if String.length linger = 0 then cs else linger ^ cs)
let req_all fd t =
IM.fold (fun _id (data, _) r ->
r >>= function
| Error _ as e -> Lwt.return e
| Ok () -> send_query fd data)
t.requests (Lwt.return (Ok ()))
let to_pairs =
List.map (function `Plaintext (ip, port) | `Tls (_, ip, port) -> ip, port)
let find_ns ns (addr, port) =
List.find (function `Plaintext (ip, p) | `Tls (_, ip, p) ->
Ipaddr.compare ip addr = 0 && p = port)
ns
let rec connect_to_ns_list (t : t) connected_condition nameservers =
let ns = to_pairs nameservers in
(* The connect_timeout given here is a bit too much, since it should
be (a) connect to the remote NS (b) send query, receive answer.
At the moment, how this is done, is that we use the connect_timeout
for (a) and another separate one for (b). Since we do connection
pooling, it is slightly tricky to use only a single connect_timeout. *)
Happy_eyeballs_lwt.connect_ip ~connect_timeout:t.timeout_ns t.he ns >>= function
| Error `Msg msg ->
let err =
Error (`Msg (Fmt.str "error %s connecting to resolver %a"
msg
Fmt.(list ~sep:(any ", ") (pair ~sep:(any ":") Ipaddr.pp int))
(to_pairs (nameserver_ips t.nameservers))))
in
Lwt_condition.broadcast connected_condition err;
t.connected_condition <- None;
Lwt.return err
| Ok (addr, socket) ->
let continue socket =
t.fd <- Some socket;
Lwt.async (fun () ->
read_loop t socket >>= fun () ->
if IM.is_empty t.requests then
Lwt.return_unit
else
connect_via_tcp_to_ns t >|= function
| Error (`Msg msg) ->
Log.err (fun m -> m "error while connecting to resolver: %s" msg)
| Ok () -> ());
Lwt_condition.broadcast connected_condition (Ok ());
t.connected_condition <- None;
req_all socket t
in
let config = find_ns (nameserver_ips t.nameservers) addr in
match config with
| `Plaintext _ -> continue (`Plain socket)
| `Tls (tls_cfg, _, _) ->
Lwt.catch (fun () ->
Tls_lwt.Unix.client_of_fd tls_cfg socket >>= fun f ->
continue (`Tls f))
(fun e ->
Log.warn (fun m -> m "TLS handshake with %a:%d failed: %s"
Ipaddr.pp (fst addr) (snd addr) (Printexc.to_string e));
let ns' =
List.filter
(function
| `Tls (_, ip, port) ->
not (Ipaddr.compare ip (fst addr) = 0 && port = snd addr)
| _ -> true)
nameservers
in
if ns' = [] then begin
let err = Error (`Msg "no further nameservers configured") in
Lwt_condition.broadcast connected_condition err;
t.connected_condition <- None;
Lwt.return err
end else
connect_to_ns_list t connected_condition ns')
and connect_via_tcp_to_ns (t : t) =
match t.fd, t.connected_condition with
| Some _, _ -> Lwt.return (Ok ())
| None, Some w -> Lwt_condition.wait w
| None, None ->
let connected_condition = Lwt_condition.create () in
t.connected_condition <- Some connected_condition ;
maybe_resolv_conf t;
connect_to_ns_list t connected_condition (nameserver_ips t.nameservers)
let connect t =
connect_via_tcp_to_ns t >|= function
| Ok () -> Ok (`Tcp, t)
| Error `Msg msg -> Error (`Msg msg)
end
(* Now that we have our {!Transport} implementation we can include the logic
that goes on top of it: *)
include Dns_client.Make(Transport)
let create ?cache_size ?edns ?nameservers ?timeout happy_eyeballs =
let dns = create ?cache_size ?edns ?nameservers ?timeout happy_eyeballs in
let getaddrinfo record domain_name =
let open Lwt_result.Infix in
match record with
| `A ->
getaddrinfo dns Dns.Rr_map.A domain_name >|= fun (_ttl, set) ->
Ipaddr.V4.Set.fold (fun ipv4 -> Ipaddr.Set.add (Ipaddr.V4 ipv4))
set Ipaddr.Set.empty
| `AAAA ->
getaddrinfo dns Dns.Rr_map.Aaaa domain_name >|= fun (_ttl, set) ->
Ipaddr.V6.Set.fold (fun ipv6 -> Ipaddr.Set.add (Ipaddr.V6 ipv6))
set Ipaddr.Set.empty
in
Happy_eyeballs_lwt.inject happy_eyeballs getaddrinfo;
dns
(* initialize the RNG *)
let () = Mirage_crypto_rng_unix.use_default ()

View file

@ -0,0 +1,18 @@
(** {!Lwt_unix} helper module for {!Dns_client}.
For more information see the {!Dns_client.Make} functor.
The {!Dns_client} is available as Dns_client_lwt after
linking to dns-client.lwt in your dune file.
It initializes the RNG (using Mirage_crypto_rng_lwt.initialize ()).
*)
(** A flow module based on non-blocking I/O on top of the
Lwt_unix socket API. *)
module Transport : Dns_client.S
with type io_addr = [ `Plaintext of Ipaddr.t * int | `Tls of Tls.Config.client * Ipaddr.t * int ]
and type +'a io = 'a Lwt.t
and type stack = Happy_eyeballs_lwt.t
include module type of Dns_client.Make(Transport)

View file

@ -0,0 +1,6 @@
(library
(name dns_client_lwt)
(modules dns_client_lwt)
(public_name dns-client-lwt)
(libraries lwt lwt.unix dns dns-client dns-client.resolvconf mtime.clock.os mirage-crypto-rng.unix ipaddr.unix happy-eyeballs happy-eyeballs-lwt tls-lwt ca-certs)
(wrapped false))

View file

@ -0,0 +1,189 @@
let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt
let src = Logs.Src.create "dns-client-miou-unix"
module Log = (val Logs.src_log src : Logs.LOG)
module Transport = struct
open Happy_eyeballs_miou_unix
type +'a io = 'a
type io_addr =
[ `Plaintext of Ipaddr.t * int | `Tls of Tls.Config.client * Ipaddr.t * int ]
type t = {
nameservers: io_addr list
; proto: Dns.proto
; timeout: float
; happy: stack
}
and stack = Happy_eyeballs_miou_unix.t
type context =
{ fd : [ `Udp of Miou_unix.file_descr
| `Tcp of Miou_unix.file_descr
| `Tls of Tls_miou_unix.t ]
; timeout : float }
let clock = Mtime_clock.elapsed_ns
let same_address ipaddr' port' = function
| `Plaintext (ipaddr, port) -> Ipaddr.compare ipaddr ipaddr' = 0 && port = port'
| `Tls (_, ipaddr, port) -> Ipaddr.compare ipaddr ipaddr' = 0 && port = port'
exception Timeout
let with_timeout ~timeout:ts fn =
let timeout () = Miou_unix.sleep ts; raise Timeout in
let prm1 = Miou.async timeout in
let prm0 = Miou.async fn in
Miou.await_first [ prm0; prm1 ]
let connect_to_nameservers t =
let ( let* ) = Result.bind in
match t.proto with
| `Tcp ->
let ip_of_nameserver = function
| `Plaintext (ipaddr, port) -> (ipaddr, port)
| `Tls (_, ipaddr, port) -> (ipaddr, port) in
let ips = List.map ip_of_nameserver t.nameservers in
let* ((ipaddr, port) as addr), fd = connect_ip t.happy ips in
begin match List.find (same_address ipaddr port) t.nameservers with
| `Plaintext _ -> Ok (addr, `Tcp fd)
| `Tls (config, _, _) ->
try let fd = Tls_miou_unix.client_of_fd config fd in
Ok (addr, `Tls fd)
with End_of_file ->
Miou_unix.close fd;
error_msgf "Connection to nameservers (via TLS) impossible" end
| `Udp ->
let is_plaintext = function `Plaintext v -> Either.Left v | _ -> Either.Right () in
let[@warning "-8"] (ipaddr, port) :: _, _ = List.partition_map is_plaintext t.nameservers in
let proto_number, socket_type = Unix.((getprotobyname "udp").p_proto, SOCK_DGRAM) in
let domain = match ipaddr with
| Ipaddr.V4 _ -> Unix.PF_INET
| Ipaddr.V6 _ -> Unix.PF_INET6 in
let fd = Unix.socket domain socket_type proto_number in
let addr = Unix.ADDR_INET (Ipaddr_unix.to_inet_addr ipaddr, port) in
let connect () =
Unix.connect fd addr;
((ipaddr, port), `Udp (Miou_unix.of_file_descr fd)) in
match with_timeout ~timeout:t.timeout connect with
| Ok value -> Ok value
| Error Timeout ->
Unix.close fd;
error_msgf "Connection to nameservers (via UDP) timeout"
| Error exn ->
Unix.close fd;
error_msgf "Unexpected error: %S" (Printexc.to_string exn)
let nameservers { nameservers; proto; _ } = (proto, nameservers)
let bind x f = f x
let lift = Fun.id
let rng = Mirage_crypto_rng.generate ?g:None
let connect t =
let ( let* ) = Result.bind in
let* ((addr, port), fd) = connect_to_nameservers t in
Log.debug (fun m -> m "Connected to a nameserver %a:%d" Ipaddr.pp addr port);
match fd with
| `Tcp _ | `Tls _ -> Ok (`Tcp, { fd; timeout= t.timeout })
| `Udp _ -> Ok (`Udp, { fd; timeout= t.timeout })
let send_recv_tls ~timeout ~id fd str =
let send () = Tls_miou_unix.write fd str in
let recv () =
let rec go buf rx_len =
let expected_len =
if rx_len >= 2 then Some (Bytes.get_uint16_be buf 0) else None in
match expected_len with
| None ->
let len = Tls_miou_unix.read fd buf ~off:rx_len in
if rx_len + len >= 2 && len > 0 then go buf (rx_len + len)
else failwith "TLS connection closed by nameserver"
| Some expected_len when rx_len >= expected_len + 2 ->
let id' = Bytes.get_uint16_be buf 2 in
if id = id'
then Bytes.sub_string buf 0 (expected_len + 2)
else
let buf' = Bytes.make 2048 '\000' in
let rx_len' = rx_len - (expected_len + 2) in
Bytes.blit buf (expected_len + 2) buf' 0 rx_len';
go buf' rx_len'
| Some expected_len when Bytes.length buf >= expected_len + 2 ->
let len = (expected_len + 2) - rx_len in
Tls_miou_unix.really_read fd buf ~off:rx_len ~len;
go buf (rx_len + len)
| Some expected_len ->
(* NOTE(dinosaure): in this branch, [buf] is not large enough to store
the DNS packet. We allocate a new buffer which can store the actual
DNS packet and use it for the next [go] iteration. *)
let buf' = Bytes.make (expected_len + 2) '\000' in
Bytes.blit buf 0 buf' 0 rx_len;
go buf' rx_len in
go (Bytes.make 2048 '\000') 0 in
let ( >>= ) = Result.bind in
match with_timeout ~timeout send >>= fun () ->
with_timeout ~timeout recv with
| Ok _ as rx -> rx
| Error Timeout -> error_msgf "DNS request timeout"
| Error (Failure msg) -> Error (`Msg msg)
| Error (End_of_file | Tls_miou_unix.Closed_by_peer) ->
error_msgf "End of file reading from nameserver"
| Error exn ->
error_msgf "Got an unexpected exception: %s"
(Printexc.to_string exn)
let send_recv { fd; timeout } str =
if String.length str > 4 then begin
match fd with
| `Tls fd ->
let id = String.get_uint16_be str 2 in
send_recv_tls ~timeout ~id fd str
| `Udp fd | `Tcp fd ->
let fd = Miou_unix.to_file_descr fd in
Unix.clear_nonblock fd;
let send () =
Log.debug (fun m -> m "sending a dns packet to resolver");
Unix.setsockopt_float fd Unix.SO_SNDTIMEO timeout;
let len = Unix.send_substring fd str 0 (String.length str) [] in
if len <> String.length str
then failwith "Broken write to upstream nameserver" in
let recv () =
let buffer = Bytes.make 2048 '\000' in
Unix.setsockopt_float fd Unix.SO_RCVTIMEO timeout;
let len = Unix.recv fd buffer 0 (Bytes.length buffer) [] in
(* TODO(dinosaure): should we check rx_len and continue until we got
the full packet (only for tcp/ip)? *)
if len > 0 && len <= Bytes.length buffer
then Bytes.sub_string buffer 0 len
else failwith "Reading from nameserver socket failed" in
let ( >>= ) = Result.bind in
match with_timeout ~timeout send >>= fun () ->
with_timeout ~timeout recv with
| Ok _ as rx -> rx
| Error Timeout -> error_msgf "DNS request timeout"
| Error (Failure msg) -> Error (`Msg msg)
| Error exn ->
error_msgf "Got an unexpected exception: %s"
(Printexc.to_string exn)
end
else error_msgf "Invalid context (data length <= 4)"
let close { fd; _ } = match fd with
| `Tcp fd | `Udp fd -> Miou_unix.close fd
| `Tls fd -> Tls_miou_unix.close fd
let of_ns ns = Int64.to_float ns /. 1_000_000_000.
let create ?nameservers ~timeout happy =
let proto, nameservers =
match nameservers with
| None -> (`Udp, [ `Plaintext (Ipaddr.of_string_exn "8.8.8.8", 53) ])
| Some (a, nss) -> (a, nss)
in
{ nameservers; proto; timeout= of_ns timeout; happy }
end
include Dns_client.Make (Transport)

View file

@ -0,0 +1,6 @@
module Transport : Dns_client.S
with type io_addr = [ `Plaintext of Ipaddr.t * int | `Tls of Tls.Config.client * Ipaddr.t * int ]
and type +'a io = 'a
and type stack = Happy_eyeballs_miou_unix.t
include module type of Dns_client.Make (Transport)

View file

@ -0,0 +1,5 @@
(library
(name dns_client_miou_unix)
(modules dns_client_miou_unix)
(public_name dns-client-miou-unix)
(libraries dns-client tls-miou-unix happy-eyeballs-miou-unix))

View file

@ -0,0 +1,111 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
open Lwt.Infix
let src = Logs.Src.create "dns_certify_mirage" ~doc:"effectful DNS certify"
module Log = (val Logs.src_log src : Logs.LOG)
module Make (S : Tcpip.Stack.V4V6) = struct
module D = Dns_mirage.Make(S)
let nsupdate_csr flow host keyname zone dnskey csr =
match
Dns_certify.nsupdate Mirage_crypto_rng.generate Mirage_ptime.now
~host ~keyname ~zone dnskey csr
with
| Error s -> Lwt.return (Error s)
| Ok (out, cb) ->
D.send_tcp (D.flow flow) (Cstruct.of_string out) >>= function
| Error () -> Lwt.return (Error (`Msg "tcp sending error"))
| Ok () -> D.read_tcp flow >|= function
| Error () -> Error (`Msg "tcp receive err")
| Ok data -> match cb (Cstruct.to_string data) with
| Error e -> Error (`Msg (Fmt.str "nsupdate reply error %a" Dns_certify.pp_u_err e))
| Ok () -> Ok ()
let query_certificate flow name csr =
match Dns_certify.query Mirage_crypto_rng.generate (Mirage_ptime.now ()) name csr with
| Error e -> Lwt.return (Error e)
| Ok (out, cb) ->
D.send_tcp (D.flow flow) (Cstruct.of_string out) >>= function
| Error () -> Lwt.return (Error (`Msg "couldn't send tcp"))
| Ok () ->
D.read_tcp flow >|= function
| Error () -> Error (`Msg "error while reading answer")
| Ok data -> match cb (Cstruct.to_string data) with
| Error e -> Error e
| Ok cert -> Ok cert
let query_certificate_or_csr flow hostname keyname zone dnskey csr =
query_certificate flow hostname csr >>= function
| Ok certificate ->
Log.info (fun m -> m "found certificate in DNS") ;
Lwt.return (Ok certificate)
| Error (`Msg msg) ->
Log.err (fun m -> m "error %s" msg) ;
Lwt.return (Error (`Msg msg))
| Error ((`Decode _ | `Bad_reply _ | `Unexpected_reply _) as e) ->
Log.err (fun m -> m "query error %a, giving up" Dns_certify.pp_q_err e);
Lwt.return (Error (`Msg "query error"))
| Error `No_tlsa ->
Log.info (fun m -> m "no certificate in DNS, need to transmit the CSR") ;
nsupdate_csr flow hostname keyname zone dnskey csr >>= function
| Error (`Msg msg) ->
Log.err (fun m -> m "failed to nsupdate TLSA %s" msg) ;
Lwt.fail_with "nsupdate issue"
| Ok () ->
let rec wait_for_cert ?(retry = 10) () =
if retry = 0 then
Lwt.return (Error (`Msg "too many retries, giving up"))
else
query_certificate flow hostname csr >>= function
| Ok certificate ->
Log.info (fun m -> m "finally found a certificate") ;
Lwt.return (Ok certificate)
| Error (`Msg msg) ->
Log.err (fun m -> m "error while querying certificate %s" msg) ;
Lwt.return (Error (`Msg msg))
| Error (#Dns_certify.q_err as q) ->
Log.info (fun m -> m "still waiting for certificate, got error %a" Dns_certify.pp_q_err q) ;
Mirage_sleep.ns (Duration.of_sec 2) >>= fun () ->
wait_for_cert ~retry:(pred retry) ()
in
wait_for_cert ()
let retrieve_certificate stack (dns_key_name, dns_key) ~hostname ?(additional_hostnames = []) ?(key_type = `RSA) ?key_data ?key_seed ?bits dns port =
let zone = Domain_name.(host_exn (drop_label_exn ~amount:2 dns_key_name)) in
let not_sub subdomain = not (Domain_name.is_subdomain ~subdomain ~domain:zone) in
if not_sub hostname then
invalid_arg "hostname not a subdomain of zone provided by dns_key"
else
let key =
let seed_or_data, data = match key_data, key_seed with
| None, None -> invalid_arg "neither key_data nor key_seed is supplied"
| Some data, _ -> Some `Data, data
| None, Some seed -> Some `Seed, seed
in
Result.fold
~ok:Fun.id
~error:(function `Msg msg -> invalid_arg ("key generation failed: " ^ msg))
(X509.Private_key.of_string ?seed_or_data ?bits key_type data)
in
match
let more_hostnames = additional_hostnames in
Dns_certify.signing_request hostname ~more_hostnames key
with
| Error (`Msg m) -> invalid_arg ("create signing request failed: " ^ m)
| Ok csr ->
S.TCP.create_connection (S.tcp stack) (dns, port) >>= function
| Error e ->
Log.err (fun m -> m "error %a while connecting to name server"
S.TCP.pp_error e);
Lwt.return (Error (`Msg "couldn't connect to name server"))
| Ok flow ->
let flow = D.of_flow flow in
query_certificate_or_csr flow hostname dns_key_name zone dns_key csr >>= fun certificate ->
S.TCP.close (D.flow flow) >|= fun () ->
match certificate with
| Error e -> Error e
| Ok (cert, chain) -> Ok (cert :: chain, key)
end

View file

@ -0,0 +1,21 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module Make (S : Tcpip.Stack.V4V6) : sig
val retrieve_certificate :
S.t -> ([`raw ] Domain_name.t * Dns.Dnskey.t) ->
hostname:[ `host ] Domain_name.t ->
?additional_hostnames:[ `raw ] Domain_name.t list ->
?key_type:X509.Key_type.t -> ?key_data:string -> ?key_seed:string ->
?bits:int -> S.TCP.ipaddr -> int ->
(X509.Certificate.t list * X509.Private_key.t, [ `Msg of string ]) result Lwt.t
(** [retrieve_certificate stack dns_key ~hostname ~key_type ~key_data ~key_seed ~bits server_ip port]
generates a private key (using [key_type], [key_data], [key_seed], and
[bits]), a certificate signing request for the given [hostname] and
[additional_hostnames], and sends [server_ip] an nsupdate (DNS-TSIG with
[dns_key]) with the csr as TLSA record, awaiting for a matching
certificate as TLSA record. Requires a service that interacts with let's
encrypt to transform the CSR into a signed certificate. If something
fails, an exception (via [Lwt.fail]) is raised. This is meant for
unikernels that require a valid TLS certificate before they can start
their service (i.e. most web servers, mail servers). *)
end

View file

@ -0,0 +1,5 @@
(library
(name dns_certify_mirage)
(public_name dns-certify.mirage)
(wrapped false)
(libraries dns dns-mirage dns-certify mirage-crypto-rng mirage-crypto-pk lwt duration mirage-sleep mirage-ptime tcpip))

View file

@ -0,0 +1,482 @@
open Lwt.Infix
let src = Logs.Src.create "dns_client_mirage" ~doc:"effectful DNS client layer"
module Log = (val Logs.src_log src : Logs.LOG)
module IM = Map.Make(Int)
module type S = sig
type happy_eyeballs
module Transport :
sig
include Dns_client.S
with type +'a io = 'a Lwt.t
and type io_addr = [
| `Plaintext of Ipaddr.t * int
| `Tls of Tls.Config.client * Ipaddr.t * int
]
val happy_eyeballs : t -> happy_eyeballs
end
include module type of Dns_client.Make(Transport)
val nameserver_of_string : string ->
(Dns.proto * Transport.io_addr, [> `Msg of string ]) result
val connect :
?cache_size:int ->
?edns:[ `None | `Auto | `Manual of Dns.Edns.t ] ->
?nameservers:string list ->
?timeout:int64 -> Transport.stack ->
t Lwt.t
end
module Make
(S : Tcpip.Stack.V4V6)
(H : Happy_eyeballs_mirage.S with type stack = S.t
and type flow = S.TCP.flow) = struct
type happy_eyeballs = H.t
module TLS = Tls_mirage.Make(S.TCP)
let auth_err = match X509.Authenticator.of_string "" with
| Ok _ -> "should not happen"
| Error `Msg m -> m
let format = {|
The format of an IP address and optional port is:
- '[::1]:port' for an IPv6 address, or
- '127.0.0.1:port' for an IPv4 address.
The format of a nameserver is:
- 'udp:IP' where the first element is the string "udp" and the [IP] as described
above (port defaults to 53): UDP packets to the provided IP address will be
sent from a random source port;
- 'tcp:IP' where the first element is the string "tcp" and the [IP] as described
above (port defaults to 53): a TCP connection to the provided IP address will
be established;
- 'tls:IP' where the first element is the string "tls", the [IP] as described
above (port defaults to 853): a TCP connection will be established, on top of
which a TLS handshake with the authenticator
(https://github.com/mirage/ca-certs-nss) will be done (which checks for the
IP address being in the certificate as SubjectAlternativeName);
- 'tls:IP!hostname' where the first element is the string "tls",
the [IP] as described above (port defaults to 853), the [hostname] a host name
used for the TLS authentication: a TCP connection will be established, on top
of which a TLS handshake with the authenticator
(https://github.com/mirage/ca-certs-nss) will be done;
- 'tls:IP!hostname!authenticator' where the first element is the string "tls",
the [IP] as described above (port defaults to 853), the [hostname] a host name
used for the TLS authentication, and the [authenticator] an X509
authenticator: a TCP connection will be established, on top of which a TLS
handshake with the authenticator will be done.
|} ^ auth_err
let nameserver_of_string str =
let ( let* ) = Result.bind in
begin match String.split_on_char ':' str with
| "tls" :: rest ->
let str = String.concat ":" rest in
( match String.split_on_char '!' str with
| [ nameserver ] ->
let* ipaddr, port = Ipaddr.with_port_of_string ~default:853 nameserver in
let* authenticator = Ca_certs_nss.authenticator () in
let* tls = Tls.Config.client ~authenticator () in
Ok (`Tcp, `Tls (tls, ipaddr, port))
| nameserver :: opt_hostname :: authenticator ->
let* ipaddr, port = Ipaddr.with_port_of_string ~default:853 nameserver in
let peer_name, data =
match
let* dn = Domain_name.of_string opt_hostname in
Domain_name.host dn
with
| Ok hostname -> Some hostname, String.concat "!" authenticator
| Error _ -> None, String.concat "!" (opt_hostname :: authenticator)
in
let* authenticator =
if data = "" then
Ca_certs_nss.authenticator ()
else
let* a = X509.Authenticator.of_string data in
Ok (a (fun () -> Some (Mirage_ptime.now ())))
in
let* tls = Tls.Config.client ~authenticator ?peer_name () in
Ok (`Tcp, `Tls (tls, ipaddr, port))
| [] -> assert false )
| "tcp" :: nameserver ->
let str = String.concat ":" nameserver in
let* ipaddr, port = Ipaddr.with_port_of_string ~default:53 str in
Ok (`Tcp, `Plaintext (ipaddr, port))
| "udp" :: nameserver ->
let str = String.concat ":" nameserver in
let* ipaddr, port = Ipaddr.with_port_of_string ~default:53 str in
Ok (`Udp, `Plaintext (ipaddr, port))
| _ ->
Error (`Msg ("Unable to decode nameserver " ^ str))
end |> Result.map_error (function `Msg e -> `Msg (e ^ format))
module Transport :
sig
include Dns_client.S
with type stack = S.t * happy_eyeballs
and type +'a io = 'a Lwt.t
and type io_addr = [
| `Plaintext of Ipaddr.t * int
| `Tls of Tls.Config.client * Ipaddr.t * int
]
val happy_eyeballs : t -> happy_eyeballs
end = struct
type stack = S.t * happy_eyeballs
type io_addr = [
| `Plaintext of Ipaddr.t * int
| `Tls of Tls.Config.client * Ipaddr.t * int
]
type +'a io = 'a Lwt.t
module IS = Set.Make(Int)
type t = {
nameservers : io_addr list ;
proto : Dns.proto ;
timeout_ns : int64 ;
stack : S.t ;
mutable udp_ports : IS.t ;
mutable flow : [`Plain of S.TCP.flow | `Tls of TLS.flow ] option ;
mutable connected_condition : (unit, [ `Msg of string ]) result Lwt_condition.t option ;
mutable requests : (Cstruct.t * (Cstruct.t, [ `Msg of string ]) result Lwt_condition.t) IM.t ;
he : H.t ;
}
type context = t
let clock = Mirage_mtime.elapsed_ns
let happy_eyeballs { he ; _ } = he
let read_udp t ip ip_us ~src ~dst ~src_port:_ data =
if Ipaddr.compare ip_us dst = 0 && Ipaddr.compare ip src = 0 &&
Cstruct.length data > 12 (* minimum DNS length (header length) *)
then
(let id = Cstruct.BE.get_uint16 data 0 in
(match IM.find_opt id t.requests with
| None -> Log.warn (fun m -> m "received unsolicited data, ignoring")
| Some (_, cond) -> Lwt_condition.broadcast cond (Ok data)));
Lwt.return_unit
let generate_udp_port t =
let rec go retries =
if retries = 0 then
Error (`Msg "couldn't find a free UDP port")
else
let port = 1024 + ((String.get_uint16_be (Mirage_crypto_rng.generate 2) 0) mod (65536 - 1024)) in
if IS.mem port t.udp_ports then
go (retries - 1)
else
(t.udp_ports <- IS.add port t.udp_ports;
Ok port)
in
go 32
let create ?nameservers ~timeout (stack, he) =
let proto, nameservers = match nameservers with
| None ->
let authenticator = match Ca_certs_nss.authenticator () with
| Ok a -> a
| Error `Msg m -> invalid_arg ("bad CA certificates " ^ m)
in
let tls_cfg =
let peer_name = Dns_client.default_resolver_hostname in
match Tls.Config.client ~authenticator ~peer_name () with
| Ok a -> a
| Error `Msg m -> invalid_arg ("invalid TLS configuration: " ^ m)
in
let ns =
List.map (fun ip -> `Tls (tls_cfg, ip, 853))
Dns_client.default_resolvers
in
`Tcp, ns
| Some (a, ns) -> a, ns
in
{
nameservers ;
proto ;
timeout_ns = timeout ;
stack ;
udp_ports = IS.empty ;
flow = None ;
connected_condition = None ;
requests = IM.empty ;
he ;
}
let nameservers { proto ; nameservers ; _ } = proto, nameservers
let rng n = Mirage_crypto_rng.generate ?g:None n
let with_timeout time_left f =
let timeout =
Mirage_sleep.ns time_left >|= fun () ->
Error (`Msg "DNS request timeout")
in
Lwt.pick [ f ; timeout ]
let bind = Lwt.bind
let lift = Lwt.return
let rec read_loop ?(linger = Cstruct.empty) t flow =
let process cs =
let rec handle_data data =
let cs_len = Cstruct.length data in
if cs_len > 2 then
let len = Cstruct.BE.get_uint16 data 0 in
if cs_len - 2 >= len then
let packet, rest =
if cs_len - 2 = len
then data, Cstruct.empty
else Cstruct.split data (len + 2)
in
let id = Cstruct.BE.get_uint16 packet 2 in
(match IM.find_opt id t.requests with
| None -> Log.warn (fun m -> m "received unsolicited data, ignoring")
| Some (_, cond) -> Lwt_condition.broadcast cond (Ok packet));
handle_data rest
else
read_loop ~linger:data t flow
else
read_loop ~linger:data t flow
in
handle_data (if Cstruct.length linger = 0 then cs else Cstruct.append linger cs)
in
match flow with
| `Plain flow ->
begin
S.TCP.read flow >>= function
| Error e ->
t.flow <- None;
Log.err (fun m -> m "error %a reading from resolver" S.TCP.pp_error e);
Lwt.return_unit
| Ok `Eof ->
t.flow <- None;
if not (IM.is_empty t.requests) then
Log.info (fun m -> m "end of file reading from resolver");
Lwt.return_unit
| Ok (`Data cs) ->
process cs
end
| `Tls flow ->
begin
TLS.read flow >>= function
| Error e ->
t.flow <- None;
Log.err (fun m -> m "error %a reading from resolver" TLS.pp_error e);
Lwt.return_unit
| Ok `Eof ->
t.flow <- None;
if not (IM.is_empty t.requests) then
Log.info (fun m -> m "end of file reading from resolver");
Lwt.return_unit
| Ok (`Data cs) ->
process cs
end
let query_one flow data =
match flow with
| `Plain flow ->
begin
S.TCP.write flow data >>= function
| Error e ->
Lwt.return (Error (`Msg (Fmt.to_to_string S.TCP.pp_write_error e)))
| Ok () -> Lwt.return (Ok ())
end
| `Tls flow ->
begin
TLS.write flow data >>= function
| Error e ->
Lwt.return (Error (`Msg (Fmt.to_to_string TLS.pp_write_error e)))
| Ok () -> Lwt.return (Ok ())
end
let req_all flow t =
IM.fold (fun _id (data, _) r ->
r >>= function
| Error _ as e -> Lwt.return e
| Ok () -> query_one flow data)
t.requests (Lwt.return (Ok ()))
let to_pairs =
List.map (function `Plaintext (ip, port)
| `Tls (_, ip, port) -> (ip, port))
let find_ns ns (addr, port) =
List.find (function `Plaintext (ip, p) | `Tls (_, ip, p) ->
Ipaddr.compare ip addr = 0 && p = port)
ns
let rec connect_ns t nameservers =
let connected_condition = Lwt_condition.create () in
t.connected_condition <- Some connected_condition ;
let ns = to_pairs nameservers in
(* The connect_timeout given here is a bit too much, since it should
be (a) connect to the remote NS (b) send query, receive answer.
At the moment, how this is done, is that we use the connect_timeout
for (a) and another separate one for (b). Since we do connection
pooling, it is slightly tricky to use only a single connect_timeout. *)
H.connect_ip ~connect_timeout:t.timeout_ns t.he ns >>= function
| Error `Msg msg ->
let err = Error (`Msg (Fmt.str "error %s connecting to resolver %a"
msg
Fmt.(list ~sep:(any ", ") (pair ~sep:(any ":") Ipaddr.pp int))
(to_pairs t.nameservers)))
in
Lwt_condition.broadcast connected_condition err;
t.connected_condition <- None;
Log.err (fun m -> m "error connecting to resolver %s" msg);
Lwt.return err
| Ok (addr, flow) ->
let continue flow =
t.flow <- Some flow;
Lwt.async (fun () ->
read_loop t flow >>= fun () ->
if not (IM.is_empty t.requests) then
connect_ns t t.nameservers >|= function
| Error `Msg msg ->
Log.err (fun m -> m "error while connecting to resolver: %s" msg)
| Ok () -> ()
else
Lwt.return_unit);
Lwt_condition.broadcast connected_condition (Ok ());
t.connected_condition <- None;
req_all flow t
in
let config = find_ns t.nameservers addr in
match config with
| `Plaintext _ -> continue (`Plain flow)
| `Tls (tls_cfg, _ip, _port) ->
TLS.client_of_flow tls_cfg flow >>= function
| Ok tls -> continue (`Tls tls)
| Error e ->
Log.warn (fun m -> m "error establishing TLS connection to %a:%d: %a"
Ipaddr.pp (fst addr) (snd addr) TLS.pp_write_error e);
let ns' =
List.filter (function
| `Tls (_, ip, port) ->
not (Ipaddr.compare ip (fst addr) = 0 && port = snd addr)
| _ -> true)
nameservers
in
if ns' = [] then begin
let err = Error (`Msg "no further nameservers configured") in
Lwt_condition.broadcast connected_condition err;
t.connected_condition <- None;
Lwt.return err
end else
connect_ns t ns'
let connect t =
let to_tcp = function
| Ok () -> Ok (`Tcp, t)
| Error `Msg msg -> Error (`Msg msg)
in
match t.proto with
| `Udp -> Lwt.return (Ok (`Udp, t))
| `Tcp -> match t.flow, t.connected_condition with
| Some _, _ -> Lwt.return (Ok (`Tcp, t))
| None, Some w -> Lwt_condition.wait w >|= to_tcp
| None, None -> connect_ns t t.nameservers >|= to_tcp
let close _f =
(* ignoring this here *)
Lwt.return_unit
let send_recv t tx =
let ( >>>= ) = Lwt_result.bind in
if Cstruct.length tx > 4 then
match t.proto, t.flow with
| `Udp, _ ->
let dst, dst_port = match t.nameservers with
| `Plaintext (ip, port) :: _ -> ip, port
| _ -> assert false
in
let src = S.IP.src (S.ip t.stack) ~dst in
let id = Cstruct.BE.get_uint16 tx 0 in
Lwt.return (generate_udp_port t) >>>= fun udp_port ->
with_timeout t.timeout_ns
(S.UDP.listen (S.udp t.stack) ~port:udp_port (read_udp t dst src);
(S.UDP.write ~src_port:udp_port ~dst ~dst_port (S.udp t.stack) tx >|= function
| Error e -> Error (`Msg (Fmt.to_to_string S.UDP.pp_error e))
| Ok () -> Ok ()) >>>= fun () ->
let cond = Lwt_condition.create () in
t.requests <- IM.add id (tx, cond) t.requests;
let open Lwt.Infix in
Lwt_condition.wait cond >|= fun data ->
match data with Ok _ | Error `Msg _ as r -> r) >|= fun r ->
S.UDP.unlisten (S.udp t.stack) ~port:udp_port;
t.udp_ports <- IS.remove udp_port t.udp_ports;
t.requests <- IM.remove id t.requests;
r
| `Tcp, None -> Lwt.return (Error (`Msg "no connection to resolver"))
| `Tcp, Some flow ->
let id = Cstruct.BE.get_uint16 tx 2 in
with_timeout t.timeout_ns
(let open Lwt_result.Infix in
query_one flow tx >>= fun () ->
let cond = Lwt_condition.create () in
t.requests <- IM.add id (tx, cond) t.requests;
let open Lwt.Infix in
Lwt_condition.wait cond >|= fun data ->
match data with Ok _ | Error `Msg _ as r -> r) >|= fun r ->
t.requests <- IM.remove id t.requests;
r
else
Lwt.return (Error (`Msg "invalid context (data length <= 4)"))
let send_recv t tx =
Lwt_result.map Cstruct.to_string (send_recv t (Cstruct.of_string tx))
end
include Dns_client.Make(Transport)
let decode_nameservers ?(nameservers= []) () =
let nameservers =
List.map
(fun nameserver -> match nameserver_of_string nameserver with
| Ok nameserver -> nameserver
| Error (`Msg err) -> invalid_arg err)
nameservers
in
let tcp, udp =
List.fold_left (fun (tcp, udp) -> function
| `Tcp, a -> a :: tcp, udp
| `Udp, a -> tcp, a :: udp)
([], []) nameservers
in
match tcp, udp with
| [], [] -> None
| [], _::_ -> Some (`Udp, udp)
| _::_, [] -> Some (`Tcp, tcp)
| _::_, udps ->
let pp_io_addr ppf = function
|`Plaintext (ip, port) -> Fmt.pf ppf "%a:%u" Ipaddr.pp ip port
| `Tls (_, ip, port) -> Fmt.pf ppf "TLS: %a:%u" Ipaddr.pp ip port
in
Log.warn (fun m -> m "ignoring UDP nameservers %a, using TCP nameservers %a"
Fmt.(list ~sep:(any ", ") pp_io_addr) udps
Fmt.(list ~sep:(any ", ") pp_io_addr) tcp);
Some (`Tcp, tcp)
let connect ?cache_size ?edns ?nameservers ?timeout (stack, he) =
let nameservers = decode_nameservers ?nameservers () in
let t = create ?cache_size ?edns ?nameservers ?timeout (stack, he) in
let getaddrinfo record domain_name =
let open Lwt_result.Infix in
match record with
| `A ->
getaddrinfo t Dns.Rr_map.A domain_name >|= fun (_ttl, set) ->
Ipaddr.V4.Set.fold (fun ipv4 -> Ipaddr.Set.add (Ipaddr.V4 ipv4))
set Ipaddr.Set.empty
| `AAAA ->
getaddrinfo t Dns.Rr_map.Aaaa domain_name >|= fun (_ttl, set) ->
Ipaddr.V6.Set.fold (fun ipv6 -> Ipaddr.Set.add (Ipaddr.V6 ipv6))
set Ipaddr.Set.empty
in
H.inject (Transport.happy_eyeballs (transport t)) getaddrinfo;
Lwt.return t
end

View file

@ -0,0 +1,59 @@
module type S = sig
type happy_eyeballs
module Transport :
sig
include Dns_client.S
with type +'a io = 'a Lwt.t
and type io_addr = [
| `Plaintext of Ipaddr.t * int
| `Tls of Tls.Config.client * Ipaddr.t * int
]
val happy_eyeballs : t -> happy_eyeballs
end
include module type of Dns_client.Make(Transport)
val nameserver_of_string : string ->
(Dns.proto * Transport.io_addr, [> `Msg of string ]) result
(** [nameserver_of_string authenticators str] returns a {!Transport.io_addr}
from the given string. The format is:
- [udp:<ipaddr>(:port)?] for a plain nameserver and we will communicate
with it {i via} the UDP protocol
- [tcp:<ipaddr>(:port)?] for a plain nameserver and we will communicate
with it {i via} the TCP protocol
- [tls:<ipaddr>(:port)?((!hostname)?!authenticator)?] for a nameserver and
we will communicate with it {i via} the TCP protocol plus the TLS
encrypted layer. The user can verify the nameserver {i via} an
{i authenticator} (see {!X509.Authenticator.of_string} for the format
of it). The {i hostname} can be provided to be used as peer name by the
authenticator. By default, {!Ca_certs_nss.authenticator} is used.
*)
val connect :
?cache_size:int ->
?edns:[ `None | `Auto | `Manual of Dns.Edns.t ] ->
?nameservers:string list ->
?timeout:int64 ->
Transport.stack -> t Lwt.t
(** [connect ?cache_size ?edns ?nameservers ?timeout (stack, happy_eyeballs)]
creates a DNS entity which is able to resolve domain-name. It expects
few optional arguments:
- [cache_size] the size of the LRU cache,
- [edns] the behaviour of whether or not to send edns in queries,
- [nameservers] a list of {i nameservers} used to resolve domain-names,
- [timeout] (in nanoseconds), passed to {create}.
The provided [happy_eyeballs] will use [t] for resolving hostnames.
@raise [Invalid_argument] if given strings don't respect formats explained
by {!nameserver_of_string}.
*)
end
module Make
(S : Tcpip.Stack.V4V6)
(H : Happy_eyeballs_mirage.S with type stack = S.t
and type flow = S.TCP.flow)
: S with type Transport.stack = S.t * H.t
and type happy_eyeballs = H.t

View file

@ -0,0 +1,5 @@
(library
(name dns_client_mirage)
(public_name dns-client-mirage)
(libraries domain-name ipaddr mirage-crypto-rng mirage-sleep tcpip mirage-ptime mirage-mtime dns-client happy-eyeballs happy-eyeballs-mirage tls-mirage ca-certs-nss)
(wrapped false))

View file

@ -0,0 +1,85 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Lwt.Infix
let src = Logs.Src.create "dns_mirage" ~doc:"effectful DNS layer"
module Log = (val Logs.src_log src : Logs.LOG)
module Make (S : Tcpip.Stack.V4V6) = struct
module IPM = struct
include Map.Make(struct
type t = Ipaddr.t * int
let compare (ip, p) (ip', p') = match Ipaddr.compare ip ip' with
| 0 -> compare p p'
| x -> x
end)
let find k t = try Some (find k t) with Not_found -> None
end
module U = S.UDP
module T = S.TCP
type f = {
flow : T.flow ;
mutable linger : Cstruct.t ;
}
let of_flow flow = { flow ; linger = Cstruct.empty }
let flow { flow ; _ } = flow
let rec read_exactly f length =
let dst_ip, dst_port = T.dst f.flow in
if Cstruct.length f.linger >= length then
let a, b = Cstruct.split f.linger length in
f.linger <- b ;
Lwt.return (Ok a)
else
T.read f.flow >>= function
| Ok `Eof ->
Log.debug (fun m -> m "end of file on flow %a:%d" Ipaddr.pp dst_ip dst_port) ;
T.close f.flow >>= fun () ->
Lwt.return (Error ())
| Error e ->
Log.err (fun m -> m "error %a reading flow %a:%d" T.pp_error e Ipaddr.pp dst_ip dst_port) ;
T.close f.flow >>= fun () ->
Lwt.return (Error ())
| Ok (`Data b) ->
f.linger <- Cstruct.append f.linger b ;
read_exactly f length
let send_udp stack src_port dst dst_port data =
Log.debug (fun m -> m "udp: sending %d bytes from %d to %a:%d"
(Cstruct.length data) src_port Ipaddr.pp dst dst_port) ;
U.write ~src_port ~dst ~dst_port (S.udp stack) data >|= function
| Error e -> Log.warn (fun m -> m "udp: failure %a while sending from %d to %a:%d"
U.pp_error e src_port Ipaddr.pp dst dst_port)
| Ok () -> ()
let send_tcp flow answer =
let dst_ip, dst_port = T.dst flow in
Log.debug (fun m -> m "tcp: sending %d bytes to %a:%d" (Cstruct.length answer) Ipaddr.pp dst_ip dst_port) ;
let len = Cstruct.create 2 in
Cstruct.BE.set_uint16 len 0 (Cstruct.length answer) ;
T.write flow (Cstruct.append len answer) >>= function
| Ok () -> Lwt.return (Ok ())
| Error e ->
Log.err (fun m -> m "tcp: error %a while writing to %a:%d" T.pp_write_error e Ipaddr.pp dst_ip dst_port) ;
T.close flow >|= fun () ->
Error ()
let send_tcp_multiple flow datas =
Lwt_list.fold_left_s (fun acc d ->
match acc with
| Error () -> Lwt.return (Error ())
| Ok () -> send_tcp flow d)
(Ok ()) datas
let read_tcp flow =
read_exactly flow 2 >>= function
| Error () -> Lwt.return (Error ())
| Ok l ->
let len = Cstruct.BE.get_uint16 l 0 in
read_exactly flow len
end

View file

@ -0,0 +1,36 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module Make (S : Tcpip.Stack.V4V6) : sig
module IPM : sig
include Map.S with type key = Ipaddr.t * int
val find : Ipaddr.t * int -> 'a t -> 'a option
end
(** [IPM] is a map using [ip * port] as key. *)
type f
(** A 2byte-length per message flow abstraction, the embedding of DNS frames
via TCP. *)
val of_flow : S.TCP.flow -> f
(** [of_flow flow] is [f]. *)
val flow : f -> S.TCP.flow
(** [flow f] is the underlying flow. *)
val read_tcp : f -> (Cstruct.t, unit) result Lwt.t
(** [read_tcp f] returns either a buffer or an error (logs actual error). *)
val send_tcp : S.TCP.flow -> Cstruct.t -> (unit, unit) result Lwt.t
(** [send_tcp flow buf] sends the buffer, either succeeds or fails (logs
actual error). *)
val send_tcp_multiple : S.TCP.flow -> Cstruct.t list ->
(unit, unit) result Lwt.t
(** [send_tcp_multiple flow bufs] sends the buffers, either succeeds or fails
(logs actual error). *)
val send_udp : S.t -> int -> Ipaddr.t -> int -> Cstruct.t -> unit Lwt.t
(** [send_udp stack source_port dst dst_port buf] sends the [buf] as UDP
packet to [dst] on [dst_port]. *)
end

View file

@ -0,0 +1,5 @@
(library
(name dns_mirage)
(public_name dns-mirage)
(wrapped false)
(libraries dns tcpip ipaddr lwt))

View file

@ -0,0 +1,437 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
open Lwt.Infix
let src = Logs.Src.create "dns_resolver_mirage" ~doc:"effectful DNS resolver"
module Log = (val Logs.src_log src : Logs.LOG)
module Make (S : Tcpip.Stack.V4V6) = struct
module Dns = Dns_mirage.Make(S)
module T = S.TCP
module TLS = Tls_mirage.Make(T)
type t = {
push : (Ipaddr.t * int * string * (int32 * string) Lwt.u) option -> unit ;
primary_data : unit -> Dns_trie.t ;
with_primary_data : Dns_trie.t -> unit ;
update_tls : Tls.Config.server -> unit ;
}
type tls_flow = { tls_flow : TLS.flow ; mutable linger : Cstruct.t }
module FM = Map.Make(struct
type t = Ipaddr.t * int
let compare (ip, p) (ip', p') =
match Ipaddr.compare ip ip' with
| 0 -> compare p p'
| x -> x
end)
let resolver stack ?(root = false) ?(timer = 500) ?(udp = true) ?(tcp = true) ?tls ?(port = 53) ?(tls_port = 853) t =
let server_port = 53 in
let server_tls_port = 853 in
let state = ref t in
(* according to RFC5452 4.5, we can chose source port between 1024-49152 *)
let sport () = 1024 + Randomconv.int ~bound:48128 Mirage_crypto_rng.generate in
(* TODO limit these maps so we don't use too much memory *)
let tcp_in = ref FM.empty in
let ocaml_in = ref FM.empty in
let auth = ref Ipaddr.Map.empty in
let tls_auth = ref Ipaddr.Map.empty in
let stream, push = Lwt_stream.create () in
let opportunistic = List.mem `Opportunistic_tls_authoritative (Dns_resolver.features t) in
let send_tls flow data =
let len = Cstruct.create 2 in
Cstruct.BE.set_uint16 len 0 (Cstruct.length data);
TLS.writev flow [len; data] >>= function
| Ok () -> Lwt.return (Ok ())
| Error e ->
Log.err (fun m -> m "tls error %a while writing" TLS.pp_write_error e);
TLS.close flow >|= fun () ->
Error ()
in
let rec read_tls ({ tls_flow ; linger } as f) length =
if Cstruct.length linger >= length then
let a, b = Cstruct.split linger length in
f.linger <- b;
Lwt.return (Ok a)
else
TLS.read tls_flow >>= function
| Ok `Eof -> Log.debug (fun m -> m "end of file while reading"); TLS.close tls_flow >|= fun () -> Error ()
| Error e -> Log.warn (fun m -> m "error reading TLS: %a" TLS.pp_error e); TLS.close tls_flow >|= fun () -> Error ()
| Ok (`Data d) ->
f.linger <- Cstruct.append linger d;
read_tls f length
in
let read_tls_packet f =
read_tls f 2 >>= function
| Error () -> Lwt.return (Error ())
| Ok k ->
let len = Cstruct.BE.get_uint16 k 0 in
read_tls f len
in
let retry_tls = Duration.of_day 1 in (* from RFC 9539, 4.3 "damping" *)
let tls_timeout = Duration.of_sec 2 in (* RFC 9539, 4.3 "timeout" (4s), we use 2s *)
let rec client_tls_out cfg dst port =
tls_auth := Ipaddr.Map.add dst (`Tls_tried (Mirage_mtime.elapsed_ns ())) !tls_auth;
T.create_connection (S.tcp stack) (dst, port) >>= function
| Error e ->
(* do i need to report this back into the resolver? what are their options then? *)
Log.err (fun m -> m "error %a while establishing tcp connection to %a:%d"
T.pp_error e Ipaddr.pp dst port) ;
Lwt.return (Error ())
| Ok flow ->
Log.debug (fun m -> m "established new outgoing TCP connection to %a:%d"
Ipaddr.pp dst port);
TLS.client_of_flow cfg flow >|= function
| Error e ->
Log.warn (fun m -> m "TLS error (to %a:%d): %a" Ipaddr.pp dst port
TLS.pp_write_error e);
Error ()
| Ok tls ->
let cfg =
match TLS.epoch tls with
| Error () -> cfg
| Ok ed ->
let anchors = Result.get_ok Ca_certs_nss.trust_anchors in
let authenticator =
let time () = Some (Mirage_ptime.now ()) in
match X509.Validation.verify_chain_of_trust ~host:None ~ip:dst
~time ~anchors ed.Tls.Core.peer_certificate_chain
with
| Ok _ ->
Log.info (fun m -> m "NS %a using ca-certs-nss authenticator"
Ipaddr.pp dst);
Result.get_ok (Ca_certs_nss.authenticator ())
| Error _ ->
match ed.peer_certificate with
| None ->
Log.info (fun m -> m "NS %a no certificate provided"
Ipaddr.pp dst);
fun ?ip:_ ~host:_ _certs -> Ok None
| Some cert ->
let fingerprint =
X509.(Public_key.fingerprint (Certificate.public_key cert))
in
Log.info (fun m -> m "NS %a using key-fingerprint %a authenticator"
Ohex.pp fingerprint Ipaddr.pp dst);
X509.Authenticator.key_fingerprint ~time ~hash:`SHA256 ~fingerprint
in
Result.get_ok (Tls.Config.client ~authenticator ())
in
tls_auth := Ipaddr.Map.add dst (`Tls_succeeded cfg) !tls_auth;
Log.debug (fun m -> m "tls connection to %a:%d" Ipaddr.pp dst port);
auth := Ipaddr.Map.add dst (`Tls tls) !auth ;
Lwt.async (fun () ->
let tls_and_linger = { tls_flow = tls ; linger = Cstruct.empty } in
let rec loop () =
read_tls_packet tls_and_linger >>= function
| Error () ->
Log.debug (fun m -> m "removing %a from auth" Ipaddr.pp dst) ;
auth := Ipaddr.Map.remove dst !auth ;
Lwt.return_unit
| Ok data ->
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let new_state, answers, queries =
let data = Cstruct.to_string data in
Dns_resolver.handle_buf !state now ts false `Tcp dst port data
in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
loop ()
in
loop ()) ;
Ok ()
and client_tcp_out dst port =
T.create_connection (S.tcp stack) (dst, port) >|= function
| Error e ->
(* do i need to report this back into the resolver? what are their options then? *)
Log.err (fun m -> m "error %a while establishing tcp connection to %a:%d"
T.pp_error e Ipaddr.pp dst port) ;
Error ()
| Ok flow ->
Log.debug (fun m -> m "established new outgoing TCP connection to %a:%d"
Ipaddr.pp dst port);
auth := Ipaddr.Map.add dst (`Tcp flow) !auth ;
Lwt.async (fun () ->
let f = Dns.of_flow flow in
let rec loop () =
Dns.read_tcp f >>= function
| Error () ->
Log.debug (fun m -> m "removing %a from auth" Ipaddr.pp dst) ;
auth := Ipaddr.Map.remove dst !auth ;
Lwt.return_unit
| Ok data ->
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let new_state, answers, queries =
let data = Cstruct.to_string data in
Dns_resolver.handle_buf !state now ts false `Tcp dst port data
in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
loop ()
in
loop ()) ;
Ok ()
and client_tcp dst port ~tls_port data =
match Ipaddr.Map.find_opt dst !auth with
| None ->
begin
let try_it = match Ipaddr.Map.find_opt dst !tls_auth with
| None -> Some None
| Some `Tls_succeeded cfg -> Some (Some cfg)
| Some `Tls_tried ts ->
if Int64.(ts >= sub (Mirage_mtime.elapsed_ns ()) retry_tls) then
Some None
else
None
in
(match try_it with
| Some cfg when opportunistic ->
let cfg =
match cfg with
| None ->
let authenticator ?ip:_ ~host:_ _certs = Ok None in
Result.get_ok (Tls.Config.client ~authenticator ())
| Some cfg -> cfg
in
client_tls_out cfg dst tls_port
| _ ->
client_tcp_out dst port) >>= function
| Error () ->
let sport = sport () in
S.UDP.listen (S.udp stack) ~port:sport (udp_cb sport false) ;
Dns.send_udp stack sport dst port (Cstruct.of_string data)
| Ok () -> client_tcp dst port ~tls_port data
end
| Some `Tcp x ->
begin
Dns.send_tcp x (Cstruct.of_string data) >>= function
| Ok () -> Lwt.return_unit
| Error () ->
auth := Ipaddr.Map.remove dst !auth ;
client_tcp dst port ~tls_port data
end
| Some `Tls tls ->
begin
send_tls tls (Cstruct.of_string data) >>= function
| Ok () -> Lwt.return_unit
| Error () ->
auth := Ipaddr.Map.remove dst !auth ;
client_tcp dst port ~tls_port data
end
and maybe_tcp dst port data =
(match Ipaddr.Map.find_opt dst !auth with
| Some `Tcp flow -> Dns.send_tcp flow (Cstruct.of_string data)
| Some `Tls tls -> send_tls tls (Cstruct.of_string data)
| None -> Lwt.return (Error ())) >>= function
| Ok () -> Lwt.return_unit
| Error () ->
let try_tls =
match Ipaddr.Map.find_opt dst !tls_auth with
| None -> true
| Some `Tls_succeeded _ -> true
| Some `Tls_tried ts ->
Int64.(ts >= sub (Mirage_mtime.elapsed_ns ()) retry_tls)
in
(if try_tls then
Lwt.pick [
(Mirage_sleep.ns tls_timeout >|= fun () -> `Timeout);
(client_tcp dst port ~tls_port:server_tls_port data >|= fun () -> `Used_tls)
]
else
Lwt.return `Timeout) >>= function
| `Timeout ->
let sport = sport () in
S.UDP.listen (S.udp stack) ~port:sport (udp_cb sport false) ;
Dns.send_udp stack sport dst port (Cstruct.of_string data)
| `Used_tls -> Lwt.return_unit
and handle_query (proto, dst, data) = match proto with
| `Udp -> maybe_tcp dst server_port data
| `Tcp -> client_tcp dst server_port ~tls_port:server_tls_port data
and handle_answer (proto, dst, dst_port, ttl, data) = match proto with
| `Udp -> Dns.send_udp stack port dst dst_port (Cstruct.of_string data)
| `Tcp ->
let from_tcp = FM.find_opt (dst, dst_port) !tcp_in in
let from_ocaml = FM.find_opt (dst, dst_port) !ocaml_in in
match from_tcp, from_ocaml with
| None, None ->
Log.err (fun m -> m "wanted to answer %a:%d via TCP, but couldn't find a flow"
Ipaddr.pp dst dst_port) ;
Lwt.return_unit
| Some (`Tcp flow), None ->
(Dns.send_tcp flow (Cstruct.of_string data) >|= function
| Ok () -> ()
| Error () -> tcp_in := FM.remove (dst, dst_port) !tcp_in)
| Some (`Tls flow), None ->
(send_tls flow (Cstruct.of_string data) >|= function
| Ok () -> ()
| Error () -> tcp_in := FM.remove (dst, dst_port) !tcp_in)
| None, Some wk -> begin
ocaml_in := FM.remove (dst, dst_port) !ocaml_in;
Lwt.wakeup wk (ttl, data);
Lwt.return_unit end
| Some _, Some _ -> assert false
and udp_cb lport req ~src ~dst:_ ~src_port buf =
let buf = Cstruct.to_string buf in
let now = Mirage_ptime.now ()
and ts = Mirage_mtime.elapsed_ns ()
in
let new_state, answers, queries =
Dns_resolver.handle_buf !state now ts req `Udp src src_port buf
in
if not req then
S.UDP.unlisten (S.udp stack) ~port:lport;
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries
in
if udp then begin
S.UDP.listen (S.udp stack) ~port (udp_cb port true);
Log.info (fun f -> f "DNS resolver listening on UDP port %d" port);
end;
let rec ocaml_cb () =
Lwt_stream.get stream >>= function
| Some (dst_ip, dst_port, data, wk) ->
ocaml_in := FM.add (dst_ip, dst_port) wk !ocaml_in;
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let new_state, answers, queries =
Dns_resolver.handle_buf !state now ts true `Tcp dst_ip dst_port data in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
ocaml_cb ()
| None -> Lwt.return_unit in
Lwt.async ocaml_cb;
let tcp_cb query flow =
let dst_ip, dst_port = T.dst flow in
Log.debug (fun m -> m "tcp connection from %a:%d" Ipaddr.pp dst_ip dst_port) ;
tcp_in := FM.add (dst_ip, dst_port) (`Tcp flow) !tcp_in ;
let f = Dns.of_flow flow in
let rec loop () =
Dns.read_tcp f >>= function
| Error () ->
tcp_in := FM.remove (dst_ip, dst_port) !tcp_in ;
Lwt.return_unit
| Ok data ->
let data = Cstruct.to_string data in
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let new_state, answers, queries =
Dns_resolver.handle_buf !state now ts query `Tcp dst_ip dst_port data
in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
loop ()
in
loop ()
in
if tcp then begin
S.TCP.listen (S.tcp stack) ~port (tcp_cb true);
Log.info (fun m -> m "DNS resolver listening on TCP port %d" port);
end;
let tls_cb cfg flow =
let dst_ip, dst_port = T.dst flow in
TLS.server_of_flow cfg flow >>= function
| Error e ->
Log.warn (fun m -> m "TLS error (from %a:%d): %a" Ipaddr.pp dst_ip dst_port
TLS.pp_write_error e);
Lwt.return_unit
| Ok tls ->
Log.debug (fun m -> m "tls connection from %a:%d" Ipaddr.pp dst_ip dst_port);
tcp_in := FM.add (dst_ip, dst_port) (`Tls tls) !tcp_in ;
let tls_and_linger = { tls_flow = tls ; linger = Cstruct.empty } in
let rec loop () =
read_tls_packet tls_and_linger >>= function
| Error () ->
tcp_in := FM.remove (dst_ip, dst_port) !tcp_in ;
Lwt.return_unit
| Ok data ->
let data = Cstruct.to_string data in
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let new_state, answers, queries =
Dns_resolver.handle_buf !state now ts true `Tcp dst_ip dst_port data
in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
loop ()
in
loop ()
in
let update_tls tls_cfg =
S.TCP.listen (S.tcp stack) ~port:tls_port (tls_cb tls_cfg);
in
(match tls with
| None -> ()
| Some cfg ->
update_tls cfg;
Log.info (fun m -> m "DNS resolver listening on TLS port %d" tls_port));
let rec time () =
let new_state, answers, queries =
Dns_resolver.timer !state (Mirage_mtime.elapsed_ns ())
in
state := new_state ;
Lwt_list.iter_p handle_answer answers >>= fun () ->
Lwt_list.iter_p handle_query queries >>= fun () ->
Mirage_sleep.ns (Duration.of_ms timer) >>= fun () ->
time ()
in
Lwt.async time ;
let primary_data () =
Dns_resolver.primary_data !state
in
let with_primary_data data =
let (t, outs) =
Dns_resolver.with_primary_data !state
(Mirage_ptime.now ())
(Mirage_mtime.elapsed_ns ())
data
in
state := t;
if outs <> [] then
Log.warn (fun m -> m "Updating resolver's primary name server resulted
in 'notify's. Secondaries in the resolver's primary DNS is *not*
supported. The 'notify's are discarded.")
in
if root then begin
let rec root () =
let new_state, q = Dns_resolver.query_root !state (Mirage_mtime.elapsed_ns ()) `Tcp in
state := new_state ;
handle_query q >>= fun () ->
Mirage_sleep.ns (Duration.of_day 6) >>= fun () ->
root ()
in
Lwt.async root end ;
{ push; primary_data; with_primary_data; update_tls }
let resolve_external { push; _ } (dst_ip, dst_port) data =
let th, wk = Lwt.wait () in
push (Some (dst_ip, dst_port, data, wk));
th
let primary_data { primary_data; _ } = primary_data ()
let update_primary_data { with_primary_data; _ } data = with_primary_data data
let update_tls { update_tls; _ } tls_config = update_tls tls_config
end

View file

@ -0,0 +1,16 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module Make (S : Tcpip.Stack.V4V6) : sig
type t
val resolver
: S.t -> ?root:bool -> ?timer:int -> ?udp:bool -> ?tcp:bool -> ?tls:Tls.Config.server -> ?port:int -> ?tls_port:int
-> Dns_resolver.t -> t
(** [resolver stack ~root ~timer ~udp ~tcp ~tls ~port ~tls_port resolver]
registers a caching resolver on the provided protocols [udp], [tcp], [tls]
using [port] for udp and tcp (defaults to 53), [tls_port] for tls (defaults
to 853) using the [resolver] configuration. The [timer] is in milliseconds
and defaults to 500 milliseconds.*)
include Dns_resolver_mirage_shared.S with type t := t
end

View file

@ -0,0 +1,10 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module type S = sig
type t
val resolve_external : t -> Ipaddr.t * int -> string -> (int32 * string) Lwt.t
val primary_data : t -> Dns_trie.t
val update_primary_data : t -> Dns_trie.t -> unit
val update_tls : t -> Tls.Config.server -> unit
end

View file

@ -0,0 +1,22 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module type S = sig
type t
val resolve_external : t -> Ipaddr.t * int -> string -> (int32 * string) Lwt.t
(** [resolve_external t (ip, port) data] resolves for [(ip, port)] the query
[data] and returns a pair of the minimum TTL and a response. *)
val primary_data : t -> Dns_trie.t
(** [primary_data t] is the DNS trie of the primary for the resolver [t]. *)
val update_primary_data : t -> Dns_trie.t -> unit
(** [update_primary_data t data] updates the primary for the resolver [t]
with the DNS trie [data]. Any 'notify's to secondaries are discarded -
secondary name servers are not supported in this setup. *)
val update_tls : t -> Tls.Config.server -> unit
(** [update_tls t tls_config] updates the tls configuration to [tls_config].
If the resolver wasn't already listening for TLS connections it will
start listening. *)
end

View file

@ -0,0 +1,13 @@
(library
(name dns_resolver_mirage)
(public_name dns-resolver.mirage)
(wrapped false)
(modules dns_resolver_mirage)
(libraries dns dns-resolver dns-server dns-mirage lwt duration mirage-sleep mirage-ptime mirage-mtime tcpip mirage-crypto-rng tls tls-mirage ca-certs-nss dns-resolver.mirage.shared))
(library
(name dns_resolver_mirage_shared)
(public_name dns-resolver.mirage.shared)
(wrapped false)
(modules dns_resolver_mirage_shared)
(libraries ipaddr dns-server tcpip tls))

View file

@ -0,0 +1,334 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
open Lwt.Infix
let src = Logs.Src.create "dns_server_mirage" ~doc:"effectful DNS server"
module Log = (val Logs.src_log src : Logs.LOG)
module Make (S : Tcpip.Stack.V4V6) = struct
let inc =
let f = function
| `Udp_query -> "udp queries"
| `Udp_answer -> "udp answers"
| `Tcp_query -> "tcp queries"
| `Tcp_answer -> "tcp answers"
| `Tcp -> "tcp-server"
| `Tcp_client -> "tcp-client"
| `Tcp_keep -> "keep tcp flow"
| `Notify -> "request"
| `On_update -> "on update"
| `On_notify -> "on notify"
| `Tcp_cache_add -> "tcp cache add"
| `Tcp_cache_drop -> "tcp cache drop"
in
let src = Dns.counter_metrics ~f "dns-server-mirage" in
(fun x -> Metrics.add src (fun x -> x) (fun d -> d x))
module Dns = Dns_mirage.Make(S)
module T = S.TCP
let primary ?(on_update = fun ~old:_ ~authenticated_key:_ ~update_source:_ _ -> Lwt.return_unit) ?(on_notify = fun _ _ -> Lwt.return None) ?(timer = 2) ?(port = 53) stack t =
let state = ref t in
let tcp_out = ref Ipaddr.Map.empty in
let drop ip =
if Ipaddr.Map.mem ip !tcp_out then begin
inc `Tcp_cache_drop;
tcp_out := Ipaddr.Map.remove ip !tcp_out ;
state := Dns_server.Primary.closed !state ip
end
in
let connect recv_task ip =
inc `Tcp_client;
let dport = 53 in
Log.debug (fun m -> m "creating connection to %a:%d" Ipaddr.pp ip dport) ;
T.create_connection (S.tcp stack) (ip, dport) >>= function
| Error e ->
Log.err (fun m -> m "error %a while establishing tcp connection to %a:%d"
T.pp_error e Ipaddr.pp ip port) ;
Lwt.return (Error ())
| Ok flow ->
inc `Tcp_cache_add;
tcp_out := Ipaddr.Map.add ip flow !tcp_out ;
Lwt.async (recv_task ip dport flow);
Lwt.return (Ok flow)
in
let send_notify recv_task (ip, data) =
inc `Notify;
let data = List.map Cstruct.of_string data in
let connect_and_send ip =
connect recv_task ip >>= function
| Ok flow -> Dns.send_tcp_multiple flow data
| Error () -> Lwt.return (Error ())
in
(match Ipaddr.Map.find_opt ip !tcp_out with
| None -> connect_and_send ip
| Some f -> Dns.send_tcp_multiple f data >>= function
| Ok () -> Lwt.return (Ok ())
| Error () -> drop ip ; connect_and_send ip) >>= function
| Ok () -> Lwt.return_unit
| Error () ->
drop ip;
Lwt_list.iter_p (Dns.send_udp stack port ip 53) data
in
let maybe_update_state key ip t =
let old = !state in
let trie server = Dns_server.Primary.data server in
state := t;
if Dns_trie.equal (trie t) (trie old) then
Lwt.return_unit
else begin
inc `On_update ; on_update ~old:(trie old) ~authenticated_key:key ~update_source:ip t
end
and maybe_notify recv_task t now ts = function
| None -> Lwt.return_unit
| Some n -> inc `On_notify ; on_notify n t >>= function
| None -> Lwt.return_unit
| Some (trie, keys) ->
let state', outs = Dns_server.Primary.with_keys t now ts keys in
let state'', outs' = Dns_server.Primary.with_data state' now ts trie in
state := state'';
Lwt_list.iter_p (send_notify recv_task) (outs @ outs')
in
let rec recv_task ip port flow () =
let f = Dns.of_flow flow in
let rec loop () =
Dns.read_tcp f >>= function
| Error () -> drop ip ; Lwt.return_unit
| Ok data ->
inc `Tcp_query;
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let t, answers, notify, n, key =
Dns_server.Primary.handle_buf !state now ts `Tcp ip port (Cstruct.to_string data)
in
let n' = match n with
| Some `Keep -> inc `Tcp_cache_add ; inc `Tcp_keep ; tcp_out := Ipaddr.Map.add ip flow !tcp_out ; None
| Some `Notify soa -> Some (`Notify soa)
| Some `Signed_notify soa -> Some (`Signed_notify soa)
| None -> None
in
maybe_update_state key ip t >>= fun () ->
maybe_notify recv_task t now ts n' >>= fun () ->
if answers <> [] then inc `Tcp_answer;
let answers = List.map Cstruct.of_string answers in
(Dns.send_tcp_multiple flow answers >|= function
| Ok () -> ()
| Error () -> drop ip) >>= fun () ->
Lwt_list.iter_p (send_notify recv_task) notify >>= fun () ->
loop ()
in
loop ()
in
let tcp_cb flow =
inc `Tcp;
let dst_ip, dst_port = T.dst flow in
recv_task dst_ip dst_port flow ()
in
S.TCP.listen (S.tcp stack) ~port tcp_cb ;
Log.info (fun m -> m "DNS server listening on TCP port %d" port) ;
let udp_cb ~src ~dst:_ ~src_port buf =
inc `Udp_query;
let buf = Cstruct.to_string buf in
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let t, answers, notify, n, key =
Dns_server.Primary.handle_buf !state now ts `Udp src src_port buf
in
let n' = match n with
| None | Some `Keep -> None
| Some `Notify soa -> Some (`Notify soa)
| Some `Signed_notify soa -> Some (`Signed_notify soa)
in
maybe_update_state key src t >>= fun () ->
maybe_notify recv_task t now ts n' >>= fun () ->
if answers <> [] then inc `Udp_answer;
let answers = List.map Cstruct.of_string answers in
(Lwt_list.iter_s (Dns.send_udp stack port src src_port) answers) >>= fun () ->
Lwt_list.iter_p (send_notify recv_task) notify
in
S.UDP.listen (S.udp stack) ~port udp_cb ;
Log.info (fun m -> m "DNS server listening on UDP port %d" port) ;
let rec time () =
let now = Mirage_ptime.now () in
let ts = Mirage_mtime.elapsed_ns () in
let t, notifies = Dns_server.Primary.timer !state now ts in
maybe_update_state None Ipaddr.(V4 V4.localhost) t >>= fun () ->
Lwt_list.iter_p (send_notify recv_task) notifies >>= fun () ->
Mirage_sleep.ns (Duration.of_sec timer) >>= fun () ->
time ()
in
Lwt.async time
let secondary ?(on_update = fun ~old:_ _trie -> Lwt.return_unit) ?(timer = 5) ?(port = 53) stack t =
let state = ref t in
let tcp_out = ref Ipaddr.Map.empty in
let maybe_update_state t =
let old = !state in
let trie server = Dns_server.Secondary.data server in
state := t ;
if Dns_trie.equal (trie t) (trie old) then
Lwt.return_unit
else begin
inc `On_update ; on_update ~old:(trie old) t
end
in
let rec close ~timer ip =
(match Ipaddr.Map.find_opt ip !tcp_out with
| None -> Lwt.return_unit
| Some f -> T.close f) >>= fun () ->
tcp_out := Ipaddr.Map.remove ip !tcp_out ;
let now = Mirage_ptime.now () in
let elapsed = Mirage_mtime.elapsed_ns () in
let state', out = Dns_server.Secondary.closed !state now elapsed ip in
state := state' ;
if not timer then
request ~timer (ip, out)
else
Lwt.return_unit
and read_and_handle ~timer ip f =
Dns.read_tcp f >>= function
| Error () ->
Log.debug (fun m -> m "removing %a from tcp_out" Ipaddr.pp ip) ;
close ~timer ip
| Ok data ->
inc `Tcp_query;
let now = Mirage_ptime.now () in
let elapsed = Mirage_mtime.elapsed_ns () in
let t, answer, out =
Dns_server.Secondary.handle_buf !state now elapsed `Tcp ip (Cstruct.to_string data)
in
maybe_update_state t >>= fun () ->
(match answer with
| None -> Lwt.return (Ok ())
| Some x ->
inc `Tcp_answer;
let x = Cstruct.of_string x in
Dns.send_tcp (Dns.flow f) x >>= function
| Error () ->
Log.debug (fun m -> m "removing %a from tcp_out" Ipaddr.pp ip) ;
close ~timer ip >|= fun () -> Error ()
| Ok () -> Lwt.return (Ok ())) >>= fun r ->
(match out with
| None -> Lwt.return_unit
| Some (ip, data) -> request_one ~timer (ip, data)) >>= fun () ->
match r with
| Ok () -> read_and_handle ~timer ip f
| Error () -> Lwt.return_unit
and request ~timer (ip, data) =
inc `Notify;
let dport = 53 in
match Ipaddr.Map.find_opt ip !tcp_out with
| None ->
begin
Log.debug (fun m -> m "creating connection to %a:%d" Ipaddr.pp ip dport) ;
inc `Tcp_client;
T.create_connection (S.tcp stack) (ip, dport) >>= function
| Error e ->
Log.err (fun m -> m "error %a while establishing tcp connection to %a:%d"
T.pp_error e Ipaddr.pp ip dport) ;
close ~timer ip
| Ok flow ->
tcp_out := Ipaddr.Map.add ip flow !tcp_out ;
let data = List.map Cstruct.of_string data in
Dns.send_tcp_multiple flow data >>= function
| Error () -> close ~timer ip
| Ok () ->
Lwt.async (fun () -> read_and_handle ~timer ip (Dns.of_flow flow)) ;
Lwt.return_unit
end
| Some flow ->
let data = List.map Cstruct.of_string data in
Dns.send_tcp_multiple flow data >>= function
| Ok () -> Lwt.return_unit
| Error () ->
Log.warn (fun m -> m "closing tcp flow to %a:%d, retrying request"
Ipaddr.pp ip dport) ;
T.close flow >>= fun () ->
tcp_out := Ipaddr.Map.remove ip !tcp_out ;
let data = List.map Cstruct.to_string data in
request ~timer (ip, data)
and request_one ~timer (ip, d) = request ~timer (ip, [ d ])
in
let udp_cb ~src ~dst:_ ~src_port buf =
Log.debug (fun m -> m "udp frame from %a:%d" Ipaddr.pp src src_port) ;
inc `Udp_query;
let buf = Cstruct.to_string buf in
let now = Mirage_ptime.now () in
let elapsed = Mirage_mtime.elapsed_ns () in
let t, answer, out =
Dns_server.Secondary.handle_buf !state now elapsed `Udp src buf
in
maybe_update_state t >>= fun () ->
(match out with
| None -> ()
| Some (ip, cs) -> Lwt.async (fun () -> request_one ~timer:false (ip, cs))) ;
match answer with
| None -> Lwt.return_unit
| Some out ->
inc `Udp_answer;
let out = Cstruct.of_string out in
Dns.send_udp stack port src src_port out
in
S.UDP.listen (S.udp stack) ~port udp_cb ;
Log.info (fun m -> m "secondary DNS listening on UDP port %d" port) ;
let tcp_cb flow =
inc `Tcp;
let dst_ip, dst_port = T.dst flow in
tcp_out := Ipaddr.Map.add dst_ip flow !tcp_out ;
Log.debug (fun m -> m "tcp connection from %a:%d" Ipaddr.pp dst_ip dst_port) ;
let f = Dns.of_flow flow in
let rec loop () =
Dns.read_tcp f >>= function
| Error () -> tcp_out := Ipaddr.Map.remove dst_ip !tcp_out ; Lwt.return_unit
| Ok data ->
inc `Tcp_query;
let data = Cstruct.to_string data in
let now = Mirage_ptime.now () in
let elapsed = Mirage_mtime.elapsed_ns () in
let t, answer, out =
Dns_server.Secondary.handle_buf !state now elapsed `Tcp dst_ip data
in
maybe_update_state t >>= fun () ->
(match out with
| None -> ()
| Some (ip, cs) -> Lwt.async (fun () -> request_one ~timer:false (ip, cs)));
match answer with
| None ->
Log.warn (fun m -> m "no TCP output") ;
loop ()
| Some data ->
inc `Tcp_answer;
let data = Cstruct.of_string data in
Dns.send_tcp flow data >>= function
| Ok () -> loop ()
| Error () -> tcp_out := Ipaddr.Map.remove dst_ip !tcp_out ; Lwt.return_unit
in
loop ()
in
S.TCP.listen (S.tcp stack) ~port tcp_cb ;
Log.info (fun m -> m "secondary DNS listening on TCP port %d" port) ;
let rec time () =
let now = Mirage_ptime.now () in
let elapsed = Mirage_mtime.elapsed_ns () in
let t, out = Dns_server.Secondary.timer !state now elapsed in
maybe_update_state t >>= fun () ->
List.iter (fun (ip, cs) ->
Lwt.async (fun () -> request ~timer:true (ip, cs))) out ;
Mirage_sleep.ns (Duration.of_sec timer) >>= fun () ->
time ()
in
Lwt.async time
end

View file

@ -0,0 +1,30 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
module Make (S : Tcpip.Stack.V4V6) : sig
val primary :
?on_update:(old:Dns_trie.t -> authenticated_key:[`raw] Domain_name.t option -> update_source:Ipaddr.t -> Dns_server.Primary.s -> unit Lwt.t) ->
?on_notify:([ `Notify of Dns.Soa.t option | `Signed_notify of Dns.Soa.t option ] ->
Dns_server.Primary.s ->
(Dns_trie.t * ([ `raw ] Domain_name.t * Dns.Dnskey.t) list) option Lwt.t) ->
?timer:int -> ?port:int -> S.t -> Dns_server.Primary.s -> unit
(** [primary ~on_update ~timer ~port stack primary] starts a primary server on
[port] (default 53, both TCP and UDP) with the given [primary]
configuration. [timer] is the DNS notify timer in seconds, and defaults to
2 seconds. [on_update ~old ~authenticated_key ~update_source s] is a
callback if the data served by the primary server [s] got updated by a
potentially authenticated nsupdate packet, the used [authenticated_key]
and source [update_source] are passed to the callback. The
[on_notify notify s] callback is executed when a notify request is received
by the primary DNS server (may be used for signaling of a (hidden) DNS
secondary server). *)
val secondary :
?on_update:(old:Dns_trie.t -> Dns_server.Secondary.s -> unit Lwt.t) ->
?timer:int -> ?port:int -> S.t -> Dns_server.Secondary.s ->
unit
(** [secondary ~on_update ~timer ~port stack secondary] starts a secondary
server on [port] (default 53). The [on_update] callback is executed when
the zone changes. The [timer] (in seconds, defaults to 5 seconds) is used
for refreshing zones. *)
end

View file

@ -0,0 +1,5 @@
(library
(name dns_server_mirage)
(public_name dns-server.mirage)
(wrapped false)
(libraries dns dns-server dns-mirage lwt duration randomconv mirage-sleep mirage-ptime mirage-mtime tcpip metrics))

View file

@ -0,0 +1,402 @@
(* mirage stub resolver *)
open Lwt.Infix
open Dns
let src = Logs.Src.create "dns_stub_mirage" ~doc:"effectful DNS stub layer"
module Log = (val Logs.src_log src : Logs.LOG)
module Make (S : Tcpip.Stack.V4V6) = struct
(* data in the wild:
- a request comes in hdr, q
- q to be found in cache
- q not found in cache (to be forwarded to the recursive resolver)
- unless q in transit (this to-be-done if it is worth it (is it?))
- a fresh hdr, q is generated and sent to the recursive resolver
- now hdr, q is registered to be awaited for
-- we can either signal the request task once we found something,
or preserve the original hdr, q together with ip and port
- a reply goes out hdr, q, answer
the "Client" is only concerned about the connection to the resolver, with
multiplexing.
the current API is:
dns_client calls connect .. -> flow
send flow data
recv flow (* potentially multiple times *)
i.e. our flow being (int * _):
connect <nothing>
send (id, _) data <- id <- data[2..3]
recv (id, _) <- registers condition in N[id] ; waits ; removes condition
or phrased differently:
a recv_loop reads continously, whenever a full packet is received,
N[id] is woken up with the packet
*)
let metrics =
let f = function
| `Udp_queries -> "udp-queries"
| `Tcp_queries -> "tcp-queries"
| `Ocaml_queries -> "ocaml-queries"
| `Tcp_connections -> "tcp-connections"
| `Authoritative_answers -> "authoritative-answers"
| `Authoritative_errors -> "authoritative-errors"
| `Reserved_answers -> "reserved-answers"
| `On_update -> "on-update"
| `Resolver_queries -> "resolver-queries"
| `Resolver_answers -> "resolver-answers"
| `Resolver_nodata -> "resolver-nodata"
| `Resolver_nodomain -> "resolver-nodomain"
| `Resolver_servfail -> "resolver-servfail"
| `Resolver_notimp -> "resolver-notimplemented"
in
let metrics = Dns.counter_metrics ~f "stub-resolver" in
(fun x -> Metrics.add metrics (fun x -> x) (fun d -> d x))
module H = Happy_eyeballs_mirage.Make(S)
module Client = Dns_client_mirage.Make(S)(H)
module TLS = Tls_mirage.Make(S.TCP)
(* likely this should contain:
- a primary server (handling updates)
- a client on steroids: multiplexing on connections
- listening for DNS requests from clients:
first find them in primary server
if not authoritative, use the client
*)
(* task management
- multiple requests for the same name, type can be done at the same "time"
-> need to remember outstanding requests and signal to clients
*)
(* take multiple resolver IPs and round-robin / ask both (take first answer,
ignoring ServFail etc.) *)
(* timeout of resolver, retransmission (to another resolver / another flow) *)
module Dns_flow = Dns_mirage.Make(S)
type t = {
client : Client.t ;
reserved : Dns_server.t ;
mutable server : Dns_server.t ;
on_update : old:Dns_trie.t -> ?authenticated_key:[`raw] Domain_name.t -> update_source:Ipaddr.t -> Dns_trie.t -> unit Lwt.t ;
push : (Ipaddr.t * int * string * (int32 * string) Lwt.u) option -> unit ;
mutable update_tls : Tls.Config.server -> unit ;
}
let primary_data { server ; _ } =
server.Dns_server.data
let update_primary_data t trie =
let server = Dns_server.with_data t.server trie in
t.server <- server
let resolve_external { push ; _ } (ip, port) data =
let th, wk = Lwt.wait () in
push (Some (ip, port, data, wk));
th
let update_tls { update_tls ; _ } tls = update_tls tls
let build_reply header question proto ?additional data =
let ttl = Packet.minimum_ttl data in
let packet = Packet.create ?additional header question data in
ttl, fst (Packet.encode proto packet)
let query_server trie question data header proto =
match Dns_server.handle_question trie question with
| Ok (_flags, answer, additional) ->
(* TODO do sth with flags *)
metrics `Authoritative_answers;
let data = `Answer answer in
let ttl = Packet.minimum_ttl data in
let packet = Packet.create ?additional header question data in
let packet =
match Dns_block.edns packet with
| None -> packet
| Some edns ->
Dns_resolver_metrics.resolver_stats `Blocked;
Dns.Packet.with_edns packet (Some edns)
in
let reply = ttl, fst (Packet.encode proto packet) in
Some reply
| Error (Rcode.NotAuth, _) -> None
| Error (rcode, answer) ->
metrics `Authoritative_errors;
let data = `Rcode_error (rcode, Packet.opcode_data data, answer) in
let reply = build_reply header question proto data in
Some reply
let tsig_decode_sign server proto packet buf header question =
let now = Mirage_ptime.now () in
match Dns_server.handle_tsig server now packet buf with
| Error _ ->
let data =
`Rcode_error (Rcode.Refused, Packet.opcode_data packet.Packet.data, None)
in
let reply = build_reply header question proto data in
Error reply
| Ok k ->
let key =
match k with None -> None | Some (keyname, _, _, _) -> Some keyname
in
let sign data =
let ttl = Packet.minimum_ttl data in
let packet = Packet.create header question data in
match k with
| None -> Some (ttl, fst (Packet.encode proto packet))
| Some (keyname, _tsig, mac, dnskey) ->
match Dns_tsig.encode_and_sign ~proto ~mac packet now dnskey keyname with
| Error s -> Log.err (fun m -> m "error %a while signing answer" Dns_tsig.pp_s s); None
| Ok (cs, _) -> Some (ttl, cs)
in
Ok (key, sign)
let axfr_server server proto packet question buf header =
match tsig_decode_sign server proto packet buf header question with
| Error e -> Some e
| Ok (key, sign) ->
match Dns_server.handle_axfr_request server proto key question with
| Error rcode ->
let err = `Rcode_error (rcode, Packet.opcode_data packet.Packet.data, None) in
let reply = build_reply header question proto err in
Some reply
| Ok axfr ->
sign (`Axfr_reply axfr)
let update_server t proto ip packet question u buf header =
let server = t.server in
match tsig_decode_sign server proto packet buf header question with
| Error e -> Lwt.return (Some e)
| Ok (key, sign) ->
match Dns_server.handle_update server proto key question u with
| Ok (trie, _) ->
let old = server.data in
let server' = Dns_server.with_data server trie in
t.server <- server';
metrics `On_update;
t.on_update ~old ?authenticated_key:key ~update_source:ip trie >|= fun () ->
sign `Update_ack
| Error rcode ->
Lwt.return (sign (`Rcode_error (rcode, Opcode.Update, None)))
let server t proto ip packet header question data buf =
match data with
| `Query -> Lwt.return (query_server t.server question data header proto)
| `Axfr_request ->
Lwt.return (axfr_server t.server proto packet question buf header)
| `Update u ->
update_server t proto ip packet question u buf header
| _ ->
let data =
`Rcode_error (Rcode.NotImp, Packet.opcode_data packet.Packet.data, None)
in
let pkt = build_reply header question proto data in
Lwt.return (Some pkt)
let resolve t question data header proto =
metrics `Resolver_queries;
let name = fst question in
match data, snd question with
| `Query, `K Rr_map.K key ->
begin Client.get_resource_record t.client key name >|= function
| Error `Msg msg ->
Log.err (fun m -> m "couldn't resolve %s" msg);
let data = `Rcode_error (Rcode.ServFail, Opcode.Query, None) in
metrics `Resolver_servfail;
let reply = build_reply header question proto data in
Some reply
| Error `No_data (domain, soa) ->
let answer = (Name_rr_map.empty, Name_rr_map.singleton domain Soa soa) in
let data = `Answer answer in
metrics `Resolver_nodata;
let reply = build_reply header question proto data in
Some reply
| Error `No_domain (domain, soa) ->
let answer = (Name_rr_map.empty, Name_rr_map.singleton domain Soa soa) in
let data = `Rcode_error (Rcode.NXDomain, Opcode.Query, Some answer) in
metrics `Resolver_nodomain;
let reply = build_reply header question proto data in
Some reply
| Ok reply ->
let answer = (Name_rr_map.singleton name key reply, Name_rr_map.empty) in
let data = `Answer answer in
metrics `Resolver_answers;
let reply = build_reply header question proto data in
Some reply
end
| _ ->
Log.err (fun m -> m "not implemented %a, data %a"
Dns.Packet.Question.pp question
Dns.Packet.pp_data data);
let data = `Rcode_error (Rcode.NotImp, Packet.opcode_data data, None) in
metrics `Resolver_notimp;
let reply = build_reply header question proto data in
Lwt.return (Some reply)
(* we're now doing up to three lookups for each request:
- in authoritative server (Dns_trie)
- in reserved trie (Dns_trie)
- in resolver cache (Dns_cache)
- asking a remote resolver
instead, on startup authoritative (from external) could be merged with
reserved (but that makes data store very big and not easy to understand
(lots of files for the reserved zones)) *)
let handle t proto ip buf =
match Packet.decode buf with
| Error err ->
Log.err (fun m -> m "couldn't decode %a" Packet.pp_err err);
Dns_resolver_metrics.response_metric 0L;
Dns_resolver_metrics.resolver_stats `Error;
let answer = Packet.raw_error buf Rcode.FormErr in
Lwt.return (Option.map (fun r -> 0l, r) answer)
| Ok packet ->
Dns_resolver_metrics.resolver_stats `Queries;
let start = Mirage_mtime.elapsed_ns () in
let header, question, data = packet.Packet.header, packet.question, packet.data in
(* check header flags: recursion desired (and send recursion available) *)
(server t proto ip packet header question data buf >>= function
| Some data -> Lwt.return (Some data)
| None ->
(* next look in reserved trie! *)
match query_server t.reserved question data header proto with
| Some data -> metrics `Reserved_answers ; Lwt.return (Some data)
| None -> resolve t question data header proto) >|= fun reply ->
let stop = Mirage_mtime.elapsed_ns () in
Dns_resolver_metrics.response_metric (Int64.sub stop start);
reply
let send_tls flow data =
let len = Cstruct.create 2 in
Cstruct.BE.set_uint16 len 0 (Cstruct.length data);
TLS.writev flow [len; data] >>= function
| Ok () -> Lwt.return (Ok ())
| Error e ->
Log.err (fun m -> m "tls error %a while writing" TLS.pp_write_error e);
TLS.close flow >|= fun () ->
Error ()
type tls_flow = { tls_flow : TLS.flow ; mutable linger : Cstruct.t }
let rec read_tls ({ tls_flow ; linger } as f) length =
if Cstruct.length linger >= length then
let a, b = Cstruct.split linger length in
f.linger <- b;
Lwt.return (Ok a)
else
TLS.read tls_flow >>= function
| Ok `Eof -> Log.debug (fun m -> m "end of file while reading"); TLS.close tls_flow >|= fun () -> Error ()
| Error e -> Log.warn (fun m -> m "error reading TLS: %a" TLS.pp_error e); TLS.close tls_flow >|= fun () -> Error ()
| Ok (`Data d) ->
f.linger <- Cstruct.append linger d;
read_tls f length
let read_tls_packet f =
read_tls f 2 >>= function
| Error () -> Lwt.return (Error ())
| Ok k ->
let len = Cstruct.BE.get_uint16 k 0 in
read_tls f len
let create ?(cache_size = 10000) ?(udp = true) ?(tcp = true) ?(port = 53) ?tls ?(tls_port = 853) ?edns ?nameservers ?timeout ?(on_update = fun ~old:_ ?authenticated_key:_ ~update_source:_ _trie -> Lwt.return_unit) primary ~happy_eyeballs stack : t Lwt.t =
Client.connect ~cache_size ?edns ?nameservers ?timeout (stack, happy_eyeballs) >|= fun client ->
let server = Dns_server.Primary.server primary in
let stream, push = Lwt_stream.create () in
let reserved = Dns_server.create Dns_resolver_root.reserved Mirage_crypto_rng.generate in
let update_tls _ = () in
let t = { client ; reserved ; server ; on_update ; push ; update_tls } in
let udp_cb ~src ~dst:_ ~src_port buf =
let buf = Cstruct.to_string buf in
metrics `Udp_queries;
handle t `Udp src buf >>= function
| None -> Lwt.return_unit
| Some (_ttl, data) ->
let data = Cstruct.of_string data in
S.UDP.write ~src_port:port ~dst:src ~dst_port:src_port (S.udp stack) data >|= function
| Error e -> Log.warn (fun m -> m "udp: failure %a while sending to %a:%d"
S.UDP.pp_error e Ipaddr.pp src src_port)
| Ok () -> ()
in
if udp then
S.UDP.listen (S.udp stack) ~port udp_cb ;
let tcp_cb flow =
metrics `Tcp_connections;
let dst_ip, dst_port = S.TCP.dst flow in
Log.debug (fun m -> m "tcp connection from %a:%d" Ipaddr.pp dst_ip dst_port) ;
let f = Dns_flow.of_flow flow in
let rec loop () =
Dns_flow.read_tcp f >>= function
| Error () -> Lwt.return_unit
| Ok data ->
metrics `Tcp_queries;
let data = Cstruct.to_string data in
handle t `Tcp dst_ip data >>= function
| None ->
Log.warn (fun m -> m "no TCP output") ;
loop ()
| Some (_ttl, data) ->
let data = Cstruct.of_string data in
Dns_flow.send_tcp flow data >>= function
| Ok () -> loop ()
| Error () -> Lwt.return_unit
in
loop ()
in
if tcp then
S.TCP.listen (S.tcp stack) ~port tcp_cb;
let rec ocaml_cb () =
Lwt_stream.get stream >>= function
| Some (dst_ip, _dst_port, data, wk) ->
metrics `Ocaml_queries;
begin
handle t `Tcp dst_ip data >|= function
| None ->
Log.warn (fun m -> m "no TCP output")
| Some (ttl, data) ->
Lwt.wakeup wk (ttl, data);
end >>= fun () ->
ocaml_cb ()
| None -> Lwt.return_unit in
Lwt.async ocaml_cb;
let tls_cb cfg flow =
let dst_ip, dst_port = S.TCP.dst flow in
TLS.server_of_flow cfg flow >>= function
| Error e ->
Log.warn (fun m -> m "TLS error (from %a:%d): %a" Ipaddr.pp dst_ip dst_port
TLS.pp_write_error e);
Lwt.return_unit
| Ok tls ->
Log.debug (fun m -> m "tls connection from %a:%d" Ipaddr.pp dst_ip dst_port);
let tls_and_linger = { tls_flow = tls ; linger = Cstruct.empty } in
let rec loop () =
read_tls_packet tls_and_linger >>= function
| Error () ->
Lwt.return_unit
| Ok data ->
let data = Cstruct.to_string data in
handle t `Tcp dst_ip data >>= function
| None ->
Log.warn (fun m -> m "no TLS output") ;
loop ()
| Some (_ttl, data) ->
let data = Cstruct.of_string data in
send_tls tls data >>= function
| Ok () -> loop ()
| Error () -> Lwt.return_unit
in
loop ()
in
let update_tls tls_cfg =
S.TCP.listen (S.tcp stack) ~port:tls_port (tls_cb tls_cfg);
in
t.update_tls <- update_tls;
(match tls with None -> () | Some cfg -> update_tls cfg);
t
end

View file

@ -0,0 +1,28 @@
(* (c) 2025 Hannes Mehnert, all rights reserved *)
module Make (S : Tcpip.Stack.V4V6) : sig
type t
module H : sig
include Happy_eyeballs_mirage.S with type stack = S.t and type flow = S.TCP.flow
val connect_device : ?aaaa_timeout:int64 -> ?connect_delay:int64 ->
?connect_timeout:int64 -> ?resolve_timeout:int64 -> ?resolve_retries:int ->
?timer_interval:int64 -> ?getaddrinfo:getaddrinfo -> stack -> t Lwt.t
end
val create : ?cache_size:int -> ?udp:bool -> ?tcp:bool -> ?port:int ->
?tls:Tls.Config.server -> ?tls_port:int ->
?edns:[ `Auto | `Manual of Dns.Edns.t | `None ] ->
?nameservers:string list ->
?timeout:int64 ->
?on_update:(old:Dns_trie.t -> ?authenticated_key:[ `raw ] Domain_name.t ->
update_source:Ipaddr.t -> Dns_trie.t -> unit Lwt.t) ->
Dns_server.Primary.s -> happy_eyeballs:H.t -> S.t -> t Lwt.t
(** [create ~cache_size ~edns ~nameservers ~timeout ~on_update server ~happy_eyeballs stack]
registers a stub resolver on the provided protocols [udp], [tcp], [tls]
using [port] for udp and tcp (defaults to 53), [tls_port] for tls (defaults
to 853) using the [resolver] configuration. The [timer] is in milliseconds
and defaults to 500 milliseconds.*)
include Dns_resolver_mirage_shared.S with type t := t
end

View file

@ -0,0 +1,5 @@
(library
(name dns_stub_mirage)
(public_name dns-stub.mirage)
(wrapped false)
(libraries dns dns-server dns-tsig metrics dns_resolver_shared dns-resolver.mirage.shared dns-mirage dns-client-mirage lwt mirage-ptime tcpip mirage-crypto-rng tls-mirage))

View file

@ -0,0 +1,14 @@
let parse buf =
try
Resolvconf_state.reset ();
let buf =
if String.(get buf (pred (length buf))) = '\n' then buf else buf ^ "\n"
in
let lexbuf = Lexing.from_string buf in
Ok (Resolvconf_parser.resolvconf Resolvconf_lexer.lex lexbuf)
with
| Parsing.Parse_error ->
Error (`Msg (Fmt.str "parse error at line %d" Resolvconf_state.(state.lineno)))
| exn ->
Error (`Msg (Fmt.str "error at line %d: %s" Resolvconf_state.(state.lineno)
(Printexc.to_string exn)))

View file

@ -0,0 +1 @@
val parse : string -> ([ `Nameserver of Ipaddr.t ] list, [> `Msg of string ]) result

View file

@ -0,0 +1,9 @@
(library
(name dns_resolvconv)
(public_name dns-client.resolvconf)
(private_modules resolvconf_lexer resolvconf_parser resolvconf_state)
(libraries ipaddr fmt)
(wrapped false))
(ocamlyacc resolvconf_parser)
(ocamllex resolvconf_lexer)

View file

@ -0,0 +1,30 @@
{
open Resolvconf_state
open Resolvconf_parser
}
let ipv4 = (['0'-'9']+ '.' ['0'-'9']+ '.' ['0'-'9']+ '.' ['0'-'9']+) as contents
let ipv6 = (['0'-'9' 'a'-'f' 'A'-'F' ':']+) as contents
let zone_id = (['0'-'9' 'a'-'z' 'A'-'Z' '.' ]+) as contents
(* inspired by https://github.com/tailhook/resolv-conf/blob/master/src/grammar.rs *)
rule lex = parse
| "nameserver" { SNAMESERVER }
| "options" ([^'\n']*) '\n' { state.lineno <- state.lineno + 1 ; EOL }
| "search" ([^'\n']*) '\n' { state.lineno <- state.lineno + 1 ; EOL }
| "domain" ([^'\n']*) '\n' { state.lineno <- state.lineno + 1 ; EOL }
| "sortlist" ([^'\n']*) '\n' { state.lineno <- state.lineno + 1 ; EOL }
| "lookup" ([^'\n']*) '\n' { state.lineno <- state.lineno + 1 ; EOL }
| "family" ([^'\n']*) '\n' { state.lineno <- state.lineno + 1 ; EOL }
| [' ' '\t']+ { SPACE }
| ipv4 { IPV4 contents }
| ipv6 { IPV6 contents }
| '.' { DOT }
| ':' { COLON }
| '%' { PERCENT }
| [' ' '\t']* ('#' [^'\n']*)? '\n' { state.lineno <- state.lineno + 1 ; EOL }
| [' ' '\t']* (';' [^'\n']*)? '\n' { state.lineno <- state.lineno + 1 ; EOL }
| zone_id { ZONE_ID contents }
| eof { EOF }

View file

@ -0,0 +1,37 @@
%{
%}
%token EOF
%token EOL
%token SPACE
%token SNAMESERVER
%token DOT
%token COLON
%token PERCENT
%token <string> IPV4
%token <string> IPV6
%token <string> ZONE_ID
%start resolvconf
%type <[ `Nameserver of Ipaddr.t ] list> resolvconf
%%
resolvconf: lines EOF { List.rev $1 }
lines:
/* nothing */ { [] }
| lines EOL { $1 }
| lines nameserver EOL { $2 :: $1 }
s: SPACE {} | s SPACE {}
ipv4: IPV4 { Ipaddr.V4.of_string_exn $1 }
ipv6:
IPV6 { Ipaddr.V6.of_string_exn $1 }
| IPV6 PERCENT ZONE_ID { Ipaddr.V6.of_string_exn $1 }
nameserver:
SNAMESERVER s ipv4 { `Nameserver (Ipaddr.V4 $3) }
| SNAMESERVER s ipv6 { `Nameserver (Ipaddr.V6 $3) }

View file

@ -0,0 +1,11 @@
(* State variables for the parser & lexer *)
type parserstate = {
mutable lineno : int ;
}
let state = {
lineno = 1 ;
}
let reset () =
state.lineno <- 1

View file

@ -0,0 +1,100 @@
open Dns
let src = Logs.Src.create "dns_block" ~doc:"DNS block"
module Log = (val Logs.src_log src : Logs.LOG)
let nameserver =
let lh = Domain_name.of_string_exn "localhost"
and bl = Domain_name.of_string_exn "blocked"
in
fun ns -> Domain_name.equal ns lh || Domain_name.equal ns bl
let ipv4 =
let lh = Ipaddr.V4.(Set.singleton localhost)
and any = Ipaddr.V4.(Set.singleton any)
in
fun ipv4s -> Ipaddr.V4.Set.equal ipv4s lh || Ipaddr.V4.Set.equal ipv4s any
let ipv6 =
let lh = Ipaddr.V6.(Set.singleton localhost)
and un = Ipaddr.V6.(Set.singleton unspecified)
in
fun ipv6s -> Ipaddr.V6.Set.equal ipv6s lh || Ipaddr.V6.Set.equal ipv6s un
let likely reply =
(* HACK! We assume blocked domains have a certain shape. *)
let blocked_soa auth =
Domain_name.Map.cardinal auth > 0 &&
Domain_name.Map.for_all (fun _domain rr ->
match Rr_map.find Rr_map.Soa rr with
| None -> false
| Some soa -> nameserver soa.nameserver)
auth
in
match reply.Packet.data with
| `Answer (answ, _auth) ->
Domain_name.Map.for_all
(fun _domain rr ->
Rr_map.for_all
(function
| Rr_map.B (Rr_map.A, (_, ips)) -> ipv4 ips
| Rr_map.B (Rr_map.Aaaa, (_, ips)) -> ipv6 ips
| _ -> false)
rr)
answ
| `Rcode_error (Rcode.NXDomain, _, Some (_answ, auth)) -> blocked_soa auth
| _ -> false
let reason reply =
let find_soa_hostmaster rr =
match Rr_map.find Rr_map.Soa rr with
| None -> None
| Some soa ->
if nameserver soa.Soa.nameserver then
Some (Domain_name.to_string soa.Soa.hostmaster)
else
None
in
let find_soa_hostmaster_in_domain_map map =
Domain_name.Map.fold (fun _domain rr acc ->
match acc, find_soa_hostmaster rr with
| None, x -> x
| Some x, None -> Some x
| Some x, Some y ->
if not (String.equal x y) then
Log.info (fun m -> m "finding blocklist resulted in %S and %S, using the first" x y);
Some x) map None
in
let find_soa_hostmaster_in_reply answer authority =
match find_soa_hostmaster_in_domain_map answer, find_soa_hostmaster_in_domain_map authority with
| None, x -> x
| Some x, None -> Some x
| Some x, Some y ->
if not (String.equal x y) then
Log.info (fun m -> m "finding blocklist resulted in %S (answer) and %S (authority), using the first" x y);
Some x
in
let r =
match reply.Packet.data with
| `Answer (answ, auth) -> find_soa_hostmaster_in_reply answ auth
| `Rcode_error (Rcode.NXDomain, _, Some (answ, auth)) ->
find_soa_hostmaster_in_reply answ auth
| _ -> None
in
Option.map (fun reason -> "appears in blocklist " ^ reason) r
let edns reply =
if likely reply then
(* After guessing that a domain is blocked we add [`Filtered] extended error
code and emit a [`Blocked] metrics event. *)
let reason = reason reply in
match reply.edns with
| None ->
Some (Edns.create ~extended_error:(`Blocked, reason) ())
| Some ({ Edns.extensions = []; extended_rcode; version; dnssec_ok; payload_size }) ->
Some (Edns.create ~extended_error:(`Blocked, reason) ~extended_rcode ~version ~dnssec_ok ~payload_size ())
| Some edns ->
Log.warn (fun m -> m "don't know how to extend edns to add extended error; not doing anything:@ %a" Edns.pp edns);
Some edns
else
None

View file

@ -0,0 +1,667 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Dns
type key = [ `raw ] Domain_name.t * Packet.Question.qtype
let pp_key = Dns_resolver_cache.pp_question
let src = Logs.Src.create "dns_resolver" ~doc:"DNS resolver"
module Log = (val Logs.src_log src : Logs.LOG)
(* The cache (a Map!?) for answers: once a specific type/name comes in, we know
which questions can progress now *)
module QM = Map.Make(struct
type t = key
let compare (n, t) (n', t') =
match Domain_name.compare n n' with
| 0 -> Packet.Question.compare_qtype t t'
| x -> x
end)
type awaiting = {
ts : int64;
retry : int;
proto : proto;
zone : [ `raw ] Domain_name.t;
edns : Edns.t option;
ip : Ipaddr.t;
port : int;
question : key;
id : int;
checking_disabled : bool;
dnssec_ok : bool;
}
let awaiting_eq a b =
Ipaddr.compare a.ip b.ip = 0 &&
Int.equal a.port b.port &&
Domain_name.equal (fst a.question) (fst b.question) &&
Packet.Question.compare_qtype (snd a.question) (snd b.question) = 0 &&
Int.equal a.id b.id
module TM = Map.Make(struct
type t = key * Ipaddr.t * int * int
let compare ((n, t), ip, port, id) ((n', t'), ip', port', id') =
let andThen v f = match v with 0 -> f () | x -> x in
andThen (Domain_name.compare n n')
(fun () -> andThen (Packet.Question.compare_qtype t t')
(fun () -> andThen (Ipaddr.compare ip ip')
(fun () -> andThen (Int.compare port port')
(fun () -> Int.compare id id'))))
end)
let retry_interval = Duration.of_ms 500
type feature =
[ `Dnssec | `Qname_minimisation | `Opportunistic_tls_authoritative ]
module FS = Set.Make(struct
type t = feature
let compare a b = match a, b with
| `Dnssec, `Dnssec -> 0 | `Dnssec, _ -> 1 | _, `Dnssec -> -1
| `Qname_minimisation, `Qname_minimisation -> 0
| `Qname_minimisation, _ -> 1
| _, `Qname_minimisation -> -1
| `Opportunistic_tls_authoritative, `Opportunistic_tls_authoritative -> 0
(* | `Opportunistic_tls_authoritative, _ -> 1
| _, `Opportunistic_tls_authoritative -> -1 *)
end)
type t = {
ip_protocol : [ `Both | `Ipv4_only | `Ipv6_only ];
features : FS.t ;
rng : int -> string ;
primary : Dns_server.Primary.s ;
cache : Dns_cache.t ;
transit : awaiting TM.t ;
queried : awaiting list QM.t ;
mutable clients : Ipaddr.Set.t ;
record_clients : bool ;
}
let create ?(record_clients = true) ?(cache_size = 10000) ?(ip_protocol = `Both) features now rng primary =
let cache = Dns_cache.empty cache_size in
let cache =
List.fold_left (fun cache (name, b) ->
Dns_cache.set cache now
name A Dns_cache.Additional
(`Entry b))
cache Dns_resolver_root.a_records
in
let cache =
List.fold_left (fun cache (name, b) ->
Dns_cache.set cache now
name Aaaa Dns_cache.Additional
(`Entry b))
cache Dns_resolver_root.aaaa_records
in
let cache =
Dns_cache.set cache now
Domain_name.root Ns Dns_cache.Additional
(`Entry Dns_resolver_root.ns_records)
in
let cache =
Dns_cache.set cache now
Domain_name.root Ds Dns_cache.Additional
(`Entry (Int32.max_int, Dnssec.root_ds))
in
let features = FS.of_list features in
{ ip_protocol ; features ; rng ; cache ; primary ; transit = TM.empty ; queried = QM.empty ;
clients = Ipaddr.Set.empty ; record_clients }
let features t = FS.elements t.features
let pick rng = function
| [] -> None
| [ x ] -> Some x
| xs -> Some (List.nth xs (Randomconv.int ~bound:(List.length xs) rng))
let build_query ?id ?(recursion_desired = false) ?(checking_disabled = false) ?(dnssec_ok = true) t ts proto question retry zone edns ip =
let id = match id with Some id -> id | None -> Randomconv.int16 t.rng in
let header =
let flags =
(* tell the NS we will do the checking.
See: https://www.rfc-editor.org/rfc/rfc4035#section-4.6 *)
let flags =
if FS.mem `Dnssec t.features then
Packet.Flags.singleton `Checking_disabled
else Packet.Flags.empty
in
if recursion_desired then
Packet.Flags.add `Recursion_desired flags
else
flags
in
id, flags
in
let el = { ts; retry; proto; zone; edns; ip; port = 53; question; id; checking_disabled; dnssec_ok } in
let key = question, ip, el.port, id in
let transit =
if TM.mem key t.transit then
Log.warn (fun m -> m "overwriting transit of %a" pp_key question) ;
TM.add key el t.transit
in
let packet = Packet.create ?edns header (question :> Packet.Question.t) `Query in
let cs, _ = Packet.encode proto packet in
{ t with transit }, cs
let query ?recursion_desired t ts await retry ip name typ =
let k = (name, typ) in
let await = { await with retry = succ await.retry } in
(* TODO here we may want to use the _default protocol_ (and edns settings) instead of `Udp *)
let payload_size = if FS.mem `Dnssec t.features then Some 1220 (* from RFC 4035 4.1 *) else None in
let edns = Some (Edns.create ~dnssec_ok:(FS.mem `Dnssec t.features) ?payload_size ()) in
let t, packet = build_query ?recursion_desired ~checking_disabled:await.checking_disabled ~dnssec_ok:await.dnssec_ok t ts `Udp k retry await.zone edns ip in
let queried =
let q = Option.value ~default:[] (QM.find_opt k t.queried) in
if List.exists (awaiting_eq await) q then q else await :: q
in
let t = { t with queried = QM.add k queried t.queried } in
Log.debug (fun m -> m "query: query %a %a" Ipaddr.pp ip pp_key k) ;
(packet, ip), t
let was_in_transit t key id sender sport =
let tm_key = key, sender, sport, id in
match TM.find tm_key t with
| exception Not_found ->
Log.warn (fun m -> m "key %a not present in set (likely retransmitted)"
pp_key key);
None, t
| awaiting ->
if Ipaddr.compare sender awaiting.ip = 0 && id = awaiting.id then
Some (awaiting.zone, awaiting.edns), TM.remove tm_key t
else
(Log.warn (fun m -> m "unsolicited reply for %a (id %04X vs o_id %04X, sender %a vs o_sender %a)"
pp_key key id awaiting.id Ipaddr.pp sender Ipaddr.pp awaiting.ip);
None, t)
let find_queries t k =
match QM.find k t with
| exception Not_found ->
Log.warn (fun m -> m "couldn't find entry %a in map" pp_key k) ;
t, []
| vals ->
QM.remove k t, vals
let handle_query ?(retry = 0) t ts awaiting =
if Int64.sub ts awaiting.ts > Int64.shift_left retry_interval 2 then begin
Log.warn (fun m -> m "dropping q %a from %a:%d (timed out)"
pp_key awaiting.question Ipaddr.pp awaiting.ip awaiting.port);
`Nothing, t
end else
let dnssec = FS.mem `Dnssec t.features && not awaiting.checking_disabled in
let qname_minimisation = FS.mem `Qname_minimisation t.features in
let r, cache = Dns_resolver_cache.handle_query t.cache ~qname_minimisation ~dnssec ~dnssec_ok:awaiting.dnssec_ok ~rng:t.rng t.ip_protocol ts awaiting.question in
let t = { t with cache } in
match r with
| `Queries _ when awaiting.retry >= 10 ->
Log.warn (fun m -> m "dropping q %a from %a:%d (already sent 10 packets)"
pp_key awaiting.question Ipaddr.pp awaiting.ip awaiting.port);
(* TODO reply with error! *)
`Nothing, t
| `Queries [] ->
Log.warn (fun m -> m "dropping q %a from %a:%d (queries is empty)"
pp_key awaiting.question Ipaddr.pp awaiting.ip awaiting.port);
`Nothing, t
| `Queries qs ->
let query_one (acc, t) (zone, (nam, types), ip) =
Log.debug (fun m -> m "have to query (zone %a) %a using ip %a"
Domain_name.pp zone
Fmt.(list ~sep:(any ", ") pp_key)
(List.map (fun t -> (nam, t)) types)
Ipaddr.pp ip);
let await = { awaiting with zone } in
List.fold_left (fun (acc, t) typ ->
let r, t = query t ts await retry ip nam typ in
r :: acc, t)
(acc, t) types
in
let r, t = List.fold_left query_one ([], t) qs in
`Query r, t
| `Reply (flags, answer, additional) ->
let time = Int64.sub ts awaiting.ts in
let max_size, edns = Edns.reply awaiting.edns in
let packet = Packet.create ?edns ?additional (awaiting.id, flags) (awaiting.question :> Packet.Question.t) (answer :> Packet.data) in
Log.debug (fun m -> m "answering %a after %a %d out packets: %a"
pp_key awaiting.question Duration.pp time awaiting.retry
Packet.pp packet) ;
Dns_resolver_metrics.response_metric time;
let cs, _ = Packet.encode ?max_size awaiting.proto packet in
let ttl = Packet.minimum_ttl (answer :> Packet.data) in
`Answer (ttl, cs), t
let scrub_it t proto zone edns ts ~signed qtype p =
match Dns_resolver_utils.scrub zone ~signed qtype p, edns with
| Ok xs, _ ->
let cache =
List.fold_left
(fun t (n, Dns_resolver_utils.E (ty, e), r) ->
(*Log.debug (fun m -> m "Dns_cache.set %a %a %a"
Rr_map.ppk (K ty) Domain_name.pp n (Dns_cache.pp_entry ty) e) ;*)
Dns_cache.set t ts n ty r e)
t xs
in
if Packet.Flags.mem `Truncation (snd p.header) && proto = `Udp then
(Log.warn (fun m -> m "NS truncated reply, using TCP now") ;
`Upgrade_to_tcp cache)
else
`Cache cache
| Error Rcode.FormErr, Some _ ->
Log.warn (fun m -> m "NS sent FormErr, retrying without edns!") ;
`Query_without_edns
| Error e, _ ->
Log.warn (fun m -> m "NS didn't like us %a" Rcode.pp e) ;
`Try_another_ns
let handle_primary t now ts proto sender sport packet _request buf =
(* makes only sense to ask primary for query=true since we'll never issue questions from primary *)
let handle_inner name =
let t, answer, _, _ = Dns_server.Primary.handle_packet t now ts proto sender sport packet name in
match answer with
| None -> `None (* TODO incoming ??? are never replied to - should be revised!? *)
| Some reply ->
(* delegation if authoritative is not set! *)
if Packet.Flags.mem `Authoritative (snd reply.header) then begin
Log.debug (fun m -> m "authoritative reply %a" Packet.pp reply) ;
let reply =
match Dns_block.edns reply with
| None -> reply
| Some edns ->
Dns_resolver_metrics.resolver_stats `Blocked;
Dns.Packet.with_edns reply (Some edns)
in
let r = Packet.encode proto reply in
let ttl = Packet.minimum_ttl reply.data in
`Reply (t, (reply, ttl, r))
end else match reply.data with
| `Answer data -> `Delegation (data, reply.additional)
| _ -> `None (* not authoritative, error!! *)
in
match Dns_server.(handle_tsig (Primary.server t) now packet buf) with
| Error (e, data) ->
Log.err (fun m -> m "tsig failed %a" Tsig_op.pp_e e);
begin match data with
| Some data -> `Reply (t, 0l, data)
| None -> `None
end
| Ok None ->
begin match handle_inner None with
| `Reply (t, (_, ttl, (out, _))) -> `Reply (t, ttl, out)
| `None -> `None
| `Delegation d -> `Delegation d
end
| Ok (Some (name, tsig, mac, key)) ->
match handle_inner (Some name) with
| `Reply (t, (reply, ttl, (buf, max_size))) ->
begin match Dns_server.((Primary.server t).tsig_sign) ~max_size ~mac name tsig ~key reply buf with
| None ->
Log.warn (fun m -> m "couldn't use %a to tsig sign, using unsigned reply" Domain_name.pp name) ;
`Reply (t, ttl, buf)
| Some (buf, _) -> `Reply (t, 0l, buf)
end
| `None -> `None
| `Delegation x -> `Delegation x
let handle_awaiting_queries ?retry t ts (name, typ) =
let queried, values = find_queries t.queried (name, typ) in
let t = { t with queried } in
List.fold_left (fun (t, out_a, out_q) awaiting ->
Log.debug (fun m -> m "now querying %a" pp_key awaiting.question) ;
match handle_query ?retry t ts awaiting with
| `Nothing, t -> t, out_a, out_q
| `Query pkts, t -> t, out_a, (List.map (fun (pkt, dst) -> (`Udp, dst, pkt)) pkts) @ out_q
| `Answer (ttl, pkt), t -> t, (awaiting.proto, awaiting.ip, awaiting.port, ttl, pkt) :: out_a, out_q)
(t, [], []) values
let resolve t ts proto sender sport req =
match req.Packet.data, Packet.Question.qtype req.Packet.question with
| `Query, Some q_type ->
Log.debug (fun m -> m "resolving %a" Packet.Question.pp req.question) ;
if not (Packet.Flags.mem `Recursion_desired (snd req.Packet.header)) then
Log.warn (fun m -> m "recursion not desired") ;
(* ask the cache *)
let checking_disabled = Packet.Flags.mem `Checking_disabled (snd req.header)
and dnssec_ok = match req.edns with None -> false | Some edns -> edns.Edns.dnssec_ok in
let awaiting = { ts; retry = 0; proto; zone = Domain_name.root ; edns = req.edns; ip = sender; port = sport; question = (fst req.question, q_type); id = fst req.header; checking_disabled; dnssec_ok } in
begin match handle_query t ts awaiting with
| `Answer (ttl, pkt), t ->
Log.debug (fun m -> m "answer %a" Packet.Question.pp req.question) ;
t, [ (proto, sender, sport, ttl, pkt) ], []
| `Nothing, t ->
Log.debug (fun m -> m "nothing %a" Packet.Question.pp req.question) ;
t, [], [] (* TODO: send a reply!? *)
| `Query pkts, t ->
Log.debug (fun m -> m "query %d %a" (List.length pkts) Packet.Question.pp req.question) ;
t, [], List.map (fun (packet, dst) -> `Udp, dst, packet) pkts
end
| _ ->
Log.err (fun m -> m "ignoring %a" Packet.pp req);
let pkt = Packet.create
(fst req.header, Packet.Flags.empty) req.question
(`Rcode_error (Rcode.NotImp, Packet.opcode_data req.data, None))
in
let buf, _ = Packet.encode proto pkt in
t, [ proto, sender, sport, 0l, buf ], []
let handle_reply t now ts proto sender sport packet reply =
match reply, Packet.Question.qtype packet.Packet.question with
| `Answer _, Some qtype
| `Rcode_error (Rcode.NXDomain, Opcode.Query, _), Some qtype
| `Rcode_error (Rcode.ServFail, Opcode.Query, _), Some qtype ->
begin
Log.debug (fun m -> m "handling reply to %a" Packet.Question.pp packet.question);
(* (a) first check whether frame was in transit! *)
let key = fst packet.question, qtype in
let r, transit = was_in_transit t.transit key (fst packet.header) sender sport in
let t = { t with transit } in
match r with
| None -> Ok (t, [], [])
| Some (zone, edns) ->
(* (b) DNSSec verification of RRs *)
let t, packet, signed =
if FS.mem `Dnssec t.features then
let t, dnskeys =
match qtype with
| `K K Rr_map.Dnskey ->
let cache, ds = Dns_cache.get t.cache ts zone Rr_map.Ds in
{ t with cache },
begin match ds with
| Ok (`Entry (_, ds_set), _) ->
let keys = match reply with
| `Answer (a, _) -> Name_rr_map.find zone Rr_map.Dnskey a
| _ -> None
in
let ds_set = Dnssec.filter_ds_if_sha2_present ds_set in
Option.map (fun (_, dnskeys) ->
Rr_map.Ds_set.fold (fun ds acc ->
match Dnssec.validate_ds zone dnskeys ds with
| Ok key -> Rr_map.Dnskey_set.add key acc
| Error `Msg msg ->
Log.debug (fun m -> m "couldn't validate DS (for %a): %s"
Domain_name.pp zone msg);
acc
| Error `Extended e ->
Log.debug (fun m -> m "couldn't validate DS (for %a): %a"
Domain_name.pp zone
Extended_error.pp e);
acc)
ds_set Rr_map.Dnskey_set.empty)
keys
| _ ->
Log.warn (fun m -> m "no DS in cache for %a" Domain_name.pp zone);
None
end
| _ ->
let cache, dnskeys = Dns_cache.get t.cache ts zone Rr_map.Dnskey in
{ t with cache },
match dnskeys with
| Ok (`Entry (_, dnskey_set), _) -> Some dnskey_set
| _ ->
Log.warn (fun m -> m "no DNSKEYS in cache for %a" Domain_name.pp zone);
None
in
let packet, signed =
match dnskeys with
| None ->
Log.warn (fun m -> m "no DNSKEY present, couldn't validate packet");
packet, false
| Some dnskeys ->
match Dnssec.verify_packet now dnskeys packet with
| Ok packet -> packet, true
| Error `Msg msg ->
Log.err (fun m -> m "error %s verifying reply %a"
msg Packet.pp_reply reply);
packet, false
in
t, packet, signed
else
t, packet, false
in
(* (c) now we scrub and either *)
match scrub_it t.cache proto zone edns ts ~signed qtype packet with
| `Query_without_edns ->
let t, cs = build_query t ts proto key 1 zone None sender in
Log.debug (fun m -> m "resolve: requery without edns %a %a"
Ipaddr.pp sender pp_key key) ;
Ok (t, [], [ `Udp, sender, cs ])
| `Upgrade_to_tcp cache ->
(* RFC 2181 Sec 9: correct would be to drop entire frame, and retry with tcp *)
(* but we're happy to retrieve the partial information, it may be useful *)
let t = { t with cache } in
(* this may provoke the very same question again -
but since tcp is first, that should trigger the TCP connection,
which is then reused... ok, we may send the same query twice
with different ids *)
(* TODO we may want to get rid of the handle_awaiting_queries
entirely here!? *)
let (t, out_a, out_q), recursion_desired =
handle_awaiting_queries t ts key, false
in
let edns = Some (Edns.create ~dnssec_ok:(FS.mem `Dnssec t.features) ()) in
let t, cs = build_query ~recursion_desired t ts `Tcp key 1 zone edns sender in
Log.debug (fun m -> m "resolve: upgrade to tcp %a %a"
Ipaddr.pp sender pp_key key) ;
Ok (t, out_a, (`Tcp, sender, cs) :: out_q)
| `Try_another_ns ->
(* is this the right behaviour? by luck we'll use another path *)
Ok (handle_awaiting_queries t ts key)
| `Cache cache ->
let t = { t with cache } in
Ok (handle_awaiting_queries t ts key)
end
| v, _ ->
Log.err (fun m -> m "ignoring reply %a" Packet.pp_reply v);
Error ()
let handle_delegation t ts proto sender sport req (delegation, add_data) =
Log.debug (fun m -> m "handling delegation %a (for %a)" Packet.Answer.pp delegation Packet.pp req) ;
match req.Packet.data, Packet.Question.qtype req.question with
| `Query, Some qtype ->
let dnssec = FS.mem `Dnssec t.features && not (Packet.Flags.mem `Checking_disabled (snd req.header))
and dnssec_ok = match req.edns with None -> false | Some edns -> edns.Edns.dnssec_ok
in
let r, cache = Dns_resolver_cache.answer ~dnssec ~dnssec_ok t.cache ts (fst req.question) qtype in
let t = { t with cache } in
begin match r with
| `Query name ->
(* we should look into delegation for the actual delegation name,
but instead we're looking for any glue (A) in additional *)
let ips =
let ip4s, ip6s =
Domain_name.Map.fold (fun _ rrmap (ip4s, ip6s) ->
(match Rr_map.(find A rrmap) with
| None -> ip4s
| Some (_, ip4s') -> Ipaddr.V4.Set.union ip4s ip4s'),
(match Rr_map.(find Aaaa rrmap) with
| None -> ip6s
| Some (_, ip6s') -> Ipaddr.V6.Set.union ip6s ip6s'))
add_data (Ipaddr.V4.Set.empty, Ipaddr.V6.Set.empty)
in
let ip4s = List.map (fun ip -> Ipaddr.V4 ip) (Ipaddr.V4.Set.elements ip4s)
and ip6s = List.map (fun ip -> Ipaddr.V6 ip) (Ipaddr.V6.Set.elements ip6s)
in
match t.ip_protocol with
| `Both -> ip4s @ ip6s
| `Ipv4_only -> ip4s
| `Ipv6_only -> ip6s
in
begin match pick t.rng ips with
| None ->
Log.err (fun m -> m "something is wrong, delegation but no IP");
t, [], []
| Some ip ->
Log.debug (fun m -> m "found ip %a, maybe querying %a"
Ipaddr.pp ip pp_key (name, qtype)) ;
(* TODO is Domain_name.root correct here? *)
let checking_disabled = Packet.Flags.mem `Checking_disabled (snd req.header)
and dnssec_ok = match req.edns with None -> false | Some edns -> edns.Edns.dnssec_ok
in
let await = { ts; retry = 0; proto; zone = Domain_name.root; edns = req.edns; ip = sender; port = sport; question = (fst req.question, qtype); id = fst req.header; checking_disabled; dnssec_ok } in
let (cs, ip), t = query ~recursion_desired:true t ts await 0 ip name qtype in
t, [], [ `Udp, ip, cs ]
end
| `Packet (flags, reply, additional) ->
let max_size, edns = Edns.reply req.edns in
Log.debug (fun m -> m "delegation reply for %a from cache: %a"
Packet.pp req Packet.pp_reply reply) ;
let packet = Packet.create ?edns ?additional (fst req.header, flags) req.question (reply :> Packet.data) in
let ttl = Packet.minimum_ttl (reply :> Packet.data) in
let pkt, _ = Packet.encode ?max_size proto packet in
Dns_resolver_metrics.response_metric 0L;
t, [ proto, sender, sport, ttl, pkt ], []
(* send it out! we've a cache hit here! *)
end
| _ ->
Log.err (fun m -> m "ignoring %a" Packet.pp req) ;
let pkt =
Packet.create (fst req.header, Packet.Flags.empty)
req.question (`Rcode_error (Rcode.NotImp, Packet.opcode_data req.data, None))
in
t, [ proto, sender, sport, 0l, fst (Packet.encode proto pkt) ], []
let handle_buf t now ts query_allowed proto sender sport buf =
match Packet.decode buf with
(* | Error (`Bad_edns_version v) ->
Log.err (fun m -> m "bad edns version (from %a:%d) %u for@.%a"
Ipaddr.pp sender sport
v Cstruct.hexdump_pp buf) ;
t, handle_error ~error:Dns_enum.BadVersOrSig proto sender sport buf, [] *)
| Error e ->
Dns_resolver_metrics.resolver_stats `Error;
Log.err (fun m -> m "decode error (from %a:%d) %a for@.%a"
Ipaddr.pp sender sport
Packet.pp_err e Ohex.pp buf) ;
let answer = match Packet.raw_error buf Rcode.FormErr with
| None -> []
| Some data -> [ proto, sender, sport, 0l, data ]
in
t, answer, []
| Ok res ->
Log.debug (fun m -> m "reacting to packet from %a:%d"
Ipaddr.pp sender sport) ;
match res.Packet.data with
| #Packet.reply as reply ->
begin
match handle_reply t now ts proto sender sport res reply with
| Ok a ->
Log.debug (fun m -> m "handled reply %a:%d"
Ipaddr.pp sender sport) ;
a
| Error () -> t, [], []
end
| #Packet.request as req when query_allowed ->
Dns_resolver_metrics.resolver_stats `Queries;
if t.record_clients then
if not (Ipaddr.Set.mem sender t.clients) then begin
t.clients <- Ipaddr.Set.add sender t.clients;
Dns_resolver_metrics.resolver_stats `Clients
end;
begin
match handle_primary t.primary now ts proto sender sport res req buf with
| `Reply (primary, ttl, pkt) ->
Dns_resolver_metrics.response_metric 0L;
Log.debug (fun m -> m "handled primary %a:%d" Ipaddr.pp sender sport) ;
{ t with primary }, [ proto, sender, sport, ttl, pkt ], []
| `Delegation dele ->
Log.debug (fun m -> m "handled delegation %a:%d" Ipaddr.pp sender sport) ;
handle_delegation t ts proto sender sport res dele
| `None ->
Log.debug (fun m -> m "resolving %a:%d" Ipaddr.pp sender sport) ;
(* DNSSEC request DS / DNSKEY / NS from auth *)
resolve t ts proto sender sport res
end
| _ ->
Log.err (fun m -> m "ignoring unsolicited packet (query allowed? %b) %a" query_allowed Packet.pp res);
t, [], []
let query_root t now proto =
let root_ip () =
match pick t.rng (Dns_resolver_root.ips t.ip_protocol) with
| None -> assert false
| Some x -> x
in
let ip =
match Dns_cache.get t.cache now Domain_name.root Ns with
| _, Ok (`Entry (_, names), _) ->
let ip4s, ip6s =
Domain_name.Host_set.fold (fun name (v4s, v6s) ->
(match snd (Dns_cache.get t.cache now (Domain_name.raw name) A) with
| Ok (`Entry (_, ips), _) -> Ipaddr.V4.Set.union ips v4s
| _ -> v4s),
(match snd (Dns_cache.get t.cache now (Domain_name.raw name) Aaaa) with
| Ok (`Entry (_, ips), _) -> Ipaddr.V6.Set.union ips v6s
| _ -> v6s))
names (Ipaddr.V4.Set.empty, Ipaddr.V6.Set.empty)
in
let ip4s = List.map (fun ip -> Ipaddr.V4 ip) (Ipaddr.V4.Set.elements ip4s)
and ip6s = List.map (fun ip -> Ipaddr.V6 ip) (Ipaddr.V6.Set.elements ip6s)
in
let ips = match t.ip_protocol with
| `Both -> ip4s @ ip6s
| `Ipv4_only -> ip4s
| `Ipv6_only -> ip6s
in
begin match pick t.rng ips with
| Some ip -> ip
| None -> root_ip ()
end
| _ -> root_ip ()
in
let question = Domain_name.root, `K (Rr_map.K Ns)
and id = Randomconv.int16 t.rng
and edns = Some (Edns.create ())
and checking_disabled = false
and dnssec_ok = true
in
let el =
{ ts = now; retry = 0; proto; zone = Domain_name.root; edns; ip; port = 53; question; id; checking_disabled; dnssec_ok }
in
let key = question, ip, el.port, id in
let t = { t with transit = TM.add key el t.transit } in
let packet = Packet.create ?edns (id, Packet.Flags.empty) question `Query in
let cs, _ = Packet.encode proto packet in
t, (proto, ip, cs)
let max_retries = 5
let err_retries t question =
let t, reqs = find_queries t question in
t, List.fold_left (fun acc awaiting ->
Log.debug (fun m -> m "now erroring to %a" pp_key awaiting.question) ;
let packet = Packet.create (awaiting.id, Packet.Flags.empty)
(awaiting.question :> Packet.Question.t)
(`Rcode_error (Rcode.ServFail, Opcode.Query, None))
in
let buf, _ = Packet.encode awaiting.proto packet in
(awaiting.proto, awaiting.ip, awaiting.port, 0l, buf) :: acc)
[] reqs
let timer t ts =
let transit, rem =
TM.partition
(fun _ awaiting -> Int64.sub ts awaiting.ts < retry_interval)
t.transit
in
let t = { t with transit } in
if not (TM.is_empty transit && TM.is_empty rem) then
Log.debug (fun m -> m "try_other timer wheel -- keeping %d, running over %d"
(TM.cardinal transit) (TM.cardinal rem)) ;
TM.fold (fun ((name, typ), _ip, _port, _id) awaiting (t, out_a, out_q) ->
let retry = succ awaiting.retry in
if retry < max_retries then begin
let t, outa, outq = handle_awaiting_queries ~retry t ts (name, typ) in
(t, outa @ out_a, outq @ out_q)
end else begin
Log.info (fun m -> m "retry limit exceeded for %a at %a!"
pp_key (name, typ) Ipaddr.pp awaiting.ip) ;
let queried, out_as = err_retries t.queried (name, typ) in
({ t with queried }, out_as @ out_a, out_q)
end)
rem (t, [], [])
let primary_data t = Dns_server.Primary.data t.primary
let with_primary_data t now ts data =
let primary, outs = Dns_server.Primary.with_data t.primary now ts data in
{ t with primary }, outs

View file

@ -0,0 +1,56 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
type t
(** The type of a DNS resolver. *)
type feature =
[ `Dnssec | `Qname_minimisation | `Opportunistic_tls_authoritative ]
val create : ?record_clients:bool -> ?cache_size:int ->
?ip_protocol:[ `Both | `Ipv4_only | `Ipv6_only ] ->
feature list ->
int64 -> (int -> string) -> Dns_server.Primary.s -> t
(** [create ~record_clients ~cache_size ~ip_protocol features now rng primary]
creates the value of a resolver, pre-filled with root NS and their IP
addresses. If [ip_protocol] is provided, and set to [`V4_only], only IPv4
packets will be emitted. If [`V6_only] is set, only IPv6 packets will be
emitted. If [`Both] (the default), either IPv4 and IPv6 packets are
emitted. If [record_clients] is true (the default), the metrics of
the resolver will include the amount of clients. This keeps a set of
Ipaddr.t of all clients around, which may use some memory if it is a public
resolver.
Some features can be specified, whether DNSSec validation should be done,
whether query name minimisation should be done, and whether opportunistic
encryption using TLS to the authoritative should be done.
*)
val features : t -> feature list
val handle_buf : t -> Ptime.t -> int64 -> bool -> Dns.proto -> Ipaddr.t ->
int -> string ->
t * (Dns.proto * Ipaddr.t * int * int32 * string) list
* (Dns.proto * Ipaddr.t * string) list
(** [handle_buf t now ts query_or_reply proto sender source-port buf] handles
resolution of [buf], which leads to a new [t], a list of answers to be
transmitted (quintuple of protocol, ip address, port, minimum ttl, buffer),
and a list of queries (triple of protocol, ip address, buffer). *)
val query_root : t -> int64 -> Dns.proto ->
t * (Dns.proto * Ipaddr.t * string)
(** [query_root t now proto] potentially requests an update of the root
zone. Best invoked by a regular timer. *)
val timer : t -> int64 ->
t * (Dns.proto * Ipaddr.t * int * int32 * string) list
* (Dns.proto * Ipaddr.t * string) list
(** [timer t now] potentially retransmits DNS requests and/or sends NXDomain
answers. *)
val primary_data : t -> Dns_trie.t
(** [primary_data t] is the DNS trie of the primary. *)
val with_primary_data : t -> Ptime.t -> int64 -> Dns_trie.t -> t * (Ipaddr.t * string list) list
(** [with_primary_data t now ts data] is a pair [(t', outs)] where [t'] is [t]
updated with the [data] DNS trie, and [outs] is the data to send out (if
any). *)

View file

@ -0,0 +1,496 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Dns
module N = Domain_name.Set
let src = Logs.Src.create "dns_resolver_cache" ~doc:"DNS resolver cache"
module Log = (val Logs.src_log src : Logs.LOG)
let _pp_err ppf = function
| `Cache_miss -> Fmt.string ppf "cache miss"
| `Cache_drop -> Fmt.string ppf "cache drop"
let pp_question ppf (name, typ) =
Fmt.pf ppf "%a (%a)" Domain_name.pp name Packet.Question.pp_qtype typ
let is_signed = function
| Dns_cache.AuthoritativeAnswer signed
| AuthoritativeAuthority signed -> signed
| _ -> None
let find_nsec t ts typ name =
let rec up name =
match snd (Dns_cache.get t ts name Nsec) with
| Ok (`Entry (ttl, nsec), rank) ->
if Bit_map.mem (Rr_map.to_int typ) nsec.Nsec.types then
Some (name, (ttl, nsec), rank)
else
None
| _ ->
if Domain_name.count_labels name >= 1 then
up (Domain_name.drop_label_exn name)
else
None
in
up name
let nsec_no t ts typ name =
match find_nsec t ts typ name with
| Some _ -> true
| None -> false
let find_nsec3 t ts typ name =
let rec up name =
match snd (Dns_cache.get_nsec3 t ts name) with
| Ok nsec3 ->
let (_, _, Nsec3.{ iterations ; salt ; _ }, _) = List.hd nsec3 in
let soa_name = Domain_name.drop_label_exn name in
let hashed_name = Dnssec.nsec3_hashed_name salt iterations ~soa_name name in
List.find_opt (fun (name, _, nsec3, _) ->
let name = Domain_name.drop_label_exn ~rev:true name in
let hashed_next_owner =
Domain_name.prepend_label_exn soa_name
(Base32.encode nsec3.Nsec3.next_owner_hashed)
in
(* TODO non-wc-expanded nsec3 only?? *)
(Domain_name.compare name hashed_name < 0 &&
Domain_name.compare hashed_name hashed_next_owner < 0) ||
(* TODO wc nsec3 as well? *)
(Domain_name.compare name hashed_name = 0 &&
not (Bit_map.mem (Rr_map.to_int typ) nsec3.types))
)
nsec3
| Error _ ->
if Domain_name.count_labels name > 1 then
up (Domain_name.drop_label_exn name)
else
None
in
up name
let nsec3_covering t ts typ name =
match find_nsec3 t ts typ name with
| None -> false
| Some _ -> true
let upwards_ds_nonexisting t ts name =
let rec go name =
if nsec_no t ts Ds name || nsec3_covering t ts Ds name then
true
else
match Domain_name.drop_label name with
| Error _ -> false
| Ok name -> go name
in
go name
let find_nearest_ns ip_proto dnssec ts t name =
let find_ns name = match snd (Dns_cache.get t ts name Ns) with
| Ok (`Entry (_, names), r) -> Domain_name.Host_set.elements names, is_signed r
| _ -> [], None
and find_dnskey name = match snd (Dns_cache.get t ts name Dnskey) with
| Ok _ -> true
| _ -> false
and dnskey_nonexisting name = match snd (Dns_cache.get t ts name Dnskey) with
| Ok _ -> false
| Error _ ->
(* no need to check for Ds nonexistance upwards, since we're only called
if we have a Ds *)
nsec_no t ts Dnskey name || nsec3_covering t ts Dnskey name
and need_to_query_for_ds name = match snd (Dns_cache.get t ts name Ds) with
| Ok _ -> false
| Error _ -> not (upwards_ds_nonexisting t ts name)
and have_ds name =
match snd (Dns_cache.get t ts name Ds) with
| Ok (`Entry _, _) -> true
| _ -> false
and find_address name =
let ip4s =
Result.fold
~ok:(function
| `Entry (_, ips), _ ->
List.map (fun ip -> Ipaddr.V4 ip) (Ipaddr.V4.Set.elements ips)
| _ -> [])
~error:(fun _ -> [])
(snd (Dns_cache.get t ts name A))
and ip6s =
Result.fold
~ok:(function
| `Entry (_, ips), _ ->
List.map (fun ip -> Ipaddr.V6 ip) (Ipaddr.V6.Set.elements ips)
| _ -> [])
~error:(fun _ -> [])
(snd (Dns_cache.get t ts name Aaaa))
in
match ip_proto with
| `Both -> ip4s @ ip6s
| `Ipv4_only -> ip4s
| `Ipv6_only -> ip6s
in
let have_ips_or_dnskey name ips =
if dnssec && not (find_dnskey name) && have_ds name then
if dnskey_nonexisting name then (
(* this is tricky, and likely bad - we have a DS but no DNSKEY *)
Log.warn (fun m -> m "DS present for %a, but nonexisting DNSKEY (NSEC/NSEC3)"
Domain_name.pp name);
`HaveIPs (name, ips))
else
(* if dnssec is enabled, and have a DS record, and we don't have a dnskey,
request it -- avoiding loops by only asking for dnskey if there's DS *)
`NeedDnskey (name, ips)
else
`HaveIPs (name, ips)
in
let rec go nam =
(* Log.info (fun m -> m "go %a" Domain_name.pp nam); *)
let ns, signed_ns = find_ns nam in
match ns with
| [] ->
(* Log.warn (fun m -> m "go no NS for %a" Domain_name.pp nam); *)
if Domain_name.(equal root nam) then
[ have_ips_or_dnskey nam (Dns_resolver_root.ips ip_proto) ]
else
go (Domain_name.drop_label_exn nam)
| _ when dnssec && need_to_query_for_ds nam ->
(* dnssec enabled, and no DS (and no nonexistance proof for DS) ->
query for DS (which is always provided by the domain above:
"." has it for ".coop" / ".com" for "example/com"
-> this also avoids loops, if we get a negative reply for DS, we move
on (and run into the case below)
*)
(* Log.info (fun m -> m "need to query for DS %a" Domain_name.pp nam); *)
List.map (function
| `HaveIPs (_name, ips) -> `NeedDs (nam, ips)
| `NeedDnskey _ | `NeedAddress _ | `NeedDs _
| `NeedSignedNs _ as r -> r)
(if Domain_name.(equal root nam) then
[ have_ips_or_dnskey name (Dns_resolver_root.ips ip_proto) ]
else
go (Domain_name.drop_label_exn nam))
| name_servers ->
List.fold_left (fun acc ns ->
let host = Domain_name.raw ns in
match find_address host with
| [] ->
(* Log.info (fun m -> m "go no address for NS %a (for %a)"
Domain_name.pp host
Domain_name.pp nam); *)
if Domain_name.is_subdomain ~subdomain:ns ~domain:nam then
(* we actually need glue *)
if Domain_name.(equal root nam) then
have_ips_or_dnskey nam (Dns_resolver_root.ips ip_proto) :: acc
else
(go (Domain_name.drop_label_exn nam)) @ acc
else
`NeedAddress (nam, host) :: acc
| ips ->
(* Log.info (fun m -> m "go address for NS %a (for %a): %a (dnssec %B signed_ns %B have_ds %B find_dnskey %B)"
Domain_name.pp host
Domain_name.pp nam
Ipaddr.pp ip
dnssec (Option.is_some signed_ns) (have_ds nam)
(find_dnskey nam)); *)
if dnssec && Option.is_none signed_ns && have_ds nam then
if find_dnskey nam then
`NeedSignedNs (nam, ips) :: acc
else if dnskey_nonexisting nam then (
(* Log.warn (fun m -> m "DS present for %a, but NSEC/NSEC3 for DNSKEY"
Domain_name.pp nam); *)
have_ips_or_dnskey nam ips :: acc)
else
`NeedDnskey (nam, ips) :: acc
else
have_ips_or_dnskey nam ips :: acc)
[] name_servers
in
go name
let resolve t ~qname_minimisation ~dnssec ip_proto ts name typ =
(* the standard recursive algorithm *)
let addresses = match ip_proto with
| `Both -> [`K (Rr_map.K A); `K (Rr_map.K Aaaa)]
| `Ipv4_only -> [`K (Rr_map.K A)]
| `Ipv6_only -> [`K (Rr_map.K Aaaa)]
in
(* with DNSSec:
- input is qname and qtyp
- (a) we have (validated) NS record (+DNSKEY) for zone -> move along
- (b) we miss a NS entry -> drop label and find one
---> we also want to collect DS and DNSKEY entries (or non-existence of DS)
---> we get DS by dnssec ok in EDNS
---> we may have unsigned NS (+ glue), and need to ask the NS for NS (+dnssec)
---> we may have unsigned glue, and need to go down for signed A/AAAA
*)
let rec go t visited types zone name =
Log.debug (fun m -> m "go %a (zone %a)" Domain_name.pp name Domain_name.pp zone) ;
let t =
if N.mem zone visited then
(* we need to break the cycle if there's one domain pointing to NS in
another domain, and this other domain NS pointing to one domain. *)
(* if we lack glue here, we should query .. for NS again with the hope
to get some glue *)
Dns_cache.remove t zone
else
t
in
List.concat_map (function
| `NeedAddress (zone, ns) -> go t (N.add zone visited) addresses zone ns
| `NeedDnskey (zone, ips) -> [ zone, zone, [`K (Rr_map.K Dnskey)], ips, t ]
| `NeedDs (zone, ips) -> [ zone, zone, [`K (Rr_map.K Ds)], ips, t ]
| `HaveIPs (zone, ips) ->
(* qname minimisation: if we can, query minimal qname (and NS)
this is possible as long as we haven't received a negative reply on
the NS query -- that's why we have another Dns_cache.get NS below *)
let name, types =
if qname_minimisation then
let n = Domain_name.count_labels name
and z = Domain_name.count_labels zone
in
let n' =
if succ z < n then
Domain_name.drop_label_exn ~amount:(n - succ z) name
else
name
in
let name' =
match snd (Dns_cache.get t ts n' Ns) with
| Ok (`Entry _, _) -> n'
| _ -> name
in
name', if Domain_name.equal name' name then types else [ `K (Rr_map.K Ns) ]
else
name, types
in
[ zone, name, types, ips, t ]
| `NeedSignedNs (domain, ips) -> [ domain, domain, [ `K (Rr_map.K Ns) ], ips, t ])
(find_nearest_ns ip_proto dnssec ts t (Domain_name.raw name))
in
go t N.empty [typ] Domain_name.root name
let to_map (name, soa) = Name_rr_map.singleton name Soa soa
let follow_cname t ts typ ~name ttl ~alias =
let rec follow t acc name =
let t, r = Dns_cache.get_or_cname t ts name typ in
match r with
| Error _ ->
Log.debug (fun m -> m "follow_cname: cache miss, need to query %a"
Domain_name.pp name);
`Query name, t
| Ok (`Alias (_, alias), r) ->
let acc' = Domain_name.Map.add name (Rr_map.singleton Cname (ttl, alias)) acc in
if Domain_name.Map.mem alias acc then begin
Log.warn (fun m -> m "follow_cname: cycle detected") ;
`Out (Rcode.NoError, is_signed r, acc', Name_rr_map.empty), t
end else begin
Log.debug (fun m -> m "follow_cname: alias to %a, follow again"
Domain_name.pp alias);
follow t acc' alias
end
| Ok (`Entry v, r) ->
let acc' = Domain_name.Map.add name Rr_map.(singleton typ v) acc in
Log.debug (fun m -> m "follow_cname: entry found, returning");
`Out (Rcode.NoError, is_signed r, acc', Name_rr_map.empty), t
| Ok (`No_domain res, r) ->
Log.debug (fun m -> m "follow_cname: nodom");
`Out (Rcode.NXDomain, is_signed r, acc, to_map res), t
| Ok (`No_data res, r) ->
Log.debug (fun m -> m "follow_cname: nodata");
`Out (Rcode.NoError, is_signed r, acc, to_map res), t
| Ok (`Serv_fail res, r) ->
Log.debug (fun m -> m "follow_cname: servfail") ;
`Out (Rcode.ServFail, is_signed r, acc, to_map res), t
in
let initial = Name_rr_map.singleton name Cname (ttl, alias) in
follow t initial alias
let signed_or_nonexisting ~dnssec t ts ty name r =
if dnssec then
Option.is_some (is_signed r) || nsec_no t ts ty name || nsec3_covering t ts ty name ||
upwards_ds_nonexisting t ts name
else
true
let ttl k = function
| Ok (`Entry v, _) -> Rr_map.ttl k v
| Ok ((`No_data (_, soa), _) | (`No_domain (_, soa), _) | (`Serv_fail (_, soa), _)) ->
soa.Soa.minimum
| Ok (`Alias (ttl, _), _) -> ttl
| Error _ -> 0l
let answer ~dnssec ~dnssec_ok t ts name (typ : Packet.Question.qtype) =
let packet _t _add ty rcode ~ttl ~rrsig answer authority =
let answer =
if dnssec_ok then
if Domain_name.Map.cardinal answer > 0 then
match rrsig with
| Some rrsig -> Name_rr_map.add name Rrsig (ttl, Rr_map.Rrsig_set.singleton rrsig) answer
| None -> answer
else
answer
else
answer
in
let authority =
if dnssec_ok then
if Domain_name.Map.cardinal authority = 1 then
let name, rr_map = Domain_name.Map.choose authority in
match Rr_map.find Soa rr_map with
| None -> authority
| Some _soa ->
let authority =
match rrsig with
| None -> authority
| Some rrsig ->
Name_rr_map.add name Rrsig (ttl, Rr_map.Rrsig_set.singleton rrsig) authority
in
match ty with
| None -> authority
| Some ty ->
match find_nsec t ts ty name, find_nsec3 t ts ty name with
| Some (name, (ttl, nsec), rank), _ ->
let authority = Name_rr_map.add name Nsec (ttl, nsec) authority in
(match is_signed rank with
| Some rrsig -> Name_rr_map.add name Rrsig (ttl, Rr_map.Rrsig_set.singleton rrsig) authority
| None -> authority)
| _, Some (name, ttl, nsec3, rank) ->
let authority = Name_rr_map.add name Nsec3 (ttl, nsec3) authority in
(match is_signed rank with
| Some rrsig -> Name_rr_map.add name Rrsig (ttl, Rr_map.Rrsig_set.singleton rrsig) authority
| None -> authority)
| None, _ -> authority
else
authority
else
authority
in
let data = (answer, authority) in
let flags =
let f = Packet.Flags.(add `Recursion_available (singleton `Recursion_desired)) in
if dnssec && match rrsig with Some _ -> true | None -> false then
Packet.Flags.add `Authentic_data f
else
f
(* XXX: we should look for a fixpoint here ;) *)
(* and additional, t = if add then additionals t ts answer else [], t *)
and data = match rcode with
| Rcode.NoError -> `Answer data
| x ->
let data = if Packet.Answer.is_empty data then None else Some data in
`Rcode_error (x, Opcode.Query, data)
in
flags, data, None
in
match typ with
| `Any ->
let t, r = Dns_cache.get_any t ts name in
let ttl = match r with
| Ok (`No_domain (_, soa), _) -> soa.Soa.minimum
| Ok (`Entries _rrs, _) -> 0l
| Error _ -> 0l
in
begin match r with
| Error _e ->
(* Log.warn (fun m -> m "error %a while looking up %a, query"
pp_err e pp_question (name, typ)); *)
`Query name, t
| Ok (`No_domain res, r) ->
Log.debug (fun m -> m "no domain while looking up %a, query" pp_question (name, typ));
`Packet (packet t false None Rcode.NXDomain ~ttl ~rrsig:(is_signed r) Domain_name.Map.empty (to_map res)), t
| Ok (`Entries rr_map, r) ->
Log.debug (fun m -> m "entries while looking up %a" pp_question (name, typ));
let data = Domain_name.Map.singleton name rr_map in
`Packet (packet t true None Rcode.NoError ~ttl ~rrsig:(is_signed r) data Domain_name.Map.empty), t
end
| `K (Rr_map.K ty) ->
let t, r = Dns_cache.get_or_cname t ts name ty in
let ttl = ttl ty r in
match r with
| Error _e ->
(* Log.warn (fun m -> m "error %a while looking up %a, query"
_pp_err _e pp_question (name, typ)); *)
`Query name, t
| Ok (`No_domain res, r) ->
if not (signed_or_nonexisting ~dnssec t ts ty name r) then `Query name, t else (
Log.debug (fun m -> m "no domain while looking up %a" pp_question (name, typ));
`Packet (packet t false (Some ty) Rcode.NXDomain ~ttl ~rrsig:(is_signed r) Domain_name.Map.empty (to_map res)), t)
| Ok (`No_data res, r) ->
if not (signed_or_nonexisting ~dnssec t ts ty name r) then `Query name, t else (
Log.debug (fun m -> m "no data while looking up %a" pp_question (name, typ));
`Packet (packet t false (Some ty) Rcode.NoError ~ttl ~rrsig:(is_signed r) Domain_name.Map.empty (to_map res)), t)
| Ok (`Serv_fail res, r) ->
if not (signed_or_nonexisting ~dnssec t ts ty name r) then `Query name, t else (
Log.debug (fun m -> m "serv fail while looking up %a" pp_question (name, typ));
`Packet (packet t false (Some ty) Rcode.ServFail ~ttl ~rrsig:None Domain_name.Map.empty (to_map res)), t)
| Ok (`Alias (ttl, alias), r) ->
if not (signed_or_nonexisting ~dnssec t ts ty name r) then `Query name, t else
begin
Log.debug (fun m -> m "alias while looking up %a" pp_question (name, typ));
match ty with
| Cname ->
let data = Name_rr_map.singleton name Cname (ttl, alias) in
`Packet (packet t false (Some ty) Rcode.NoError ~ttl ~rrsig:(is_signed r) data Domain_name.Map.empty), t
| ty ->
match follow_cname t ts ty ~name ttl ~alias with
| `Out (rcode, rrsig, an, au), t -> `Packet (packet t true (Some ty) rcode ~ttl ~rrsig an au), t
| `Query n, t -> `Query n, t
end
| Ok (`Entry v, r) ->
if not (signed_or_nonexisting ~dnssec t ts ty name r) then `Query name, t else
(Log.debug (fun m -> m "entry while looking up %a" pp_question (name, typ));
let data = Name_rr_map.singleton name ty v in
`Packet (packet t true (Some ty) Rcode.NoError ~ttl ~rrsig:(is_signed r) data Domain_name.Map.empty), t)
let pick_n rng n xs =
let l = List.length xs in
if n >= l then
xs
else
let rec pick amount bound =
if amount = 0 then
[]
else
let e = Randomconv.int ~bound rng in
let ips'' = pick (amount - 1) (bound - 1) in
e :: List.map (fun idx -> if idx < e then idx else succ idx) ips''
in
let idx = pick n l in
List.map (List.nth xs) idx
let handle_query t ~qname_minimisation ~dnssec ~dnssec_ok ~rng ip_proto ts (qname, qtype) =
match answer ~dnssec ~dnssec_ok t ts qname qtype with
| `Packet (flags, data, additional), t ->
Log.debug (fun m -> m "handle_query: reply %a (%a)" Domain_name.pp qname
Packet.Question.pp_qtype qtype);
`Reply (flags, data, additional), t
| `Query name, t ->
(* DS should be requested at the parent *)
let name', recover =
if Domain_name.count_labels name > 1 && qtype = `K (Rr_map.K Ds) then
let n' = Domain_name.drop_label_exn name in
n', fun n -> if Domain_name.equal n n' then name else n
else
name, Fun.id
in
let actions = resolve t ~qname_minimisation ~dnssec ip_proto ts name' qtype in
let up_to_three = pick_n rng 3 actions in
let ip1 = 4 - List.length up_to_three in
let ip2 = max 1 (3 - List.length up_to_three) in
let _i, queries, t' =
List.fold_left (fun (i, acc, _t) (zone, name'', types, ips, t) ->
let name'' = recover name'' in
let number_of_ips = if i = 0 then ip1 else ip2 in
let ips = pick_n rng number_of_ips ips in
Log.debug (fun m -> m "handle_query %a (%a) query %a, resolve zone %a query %a (%a), ips %a"
Domain_name.pp qname Packet.Question.pp_qtype qtype
Domain_name.pp name Domain_name.pp zone Domain_name.pp name''
Fmt.(list ~sep:(any ", ") Packet.Question.pp_qtype) types
Fmt.(list ~sep:(any ", ") Ipaddr.pp) ips);
let actions =
List.map (fun ip -> (zone, (name'', types), ip)) ips
in
succ i, acc @ actions, Some t)
(0, [], None) up_to_three
in
`Queries queries, Option.value ~default:t t'

View file

@ -0,0 +1,25 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Dns
val pp_question : ([ `raw ] Domain_name.t * Packet.Question.qtype) Fmt.t
val follow_cname : Dns_cache.t -> int64 -> 'a Rr_map.key -> name:[ `raw ] Domain_name.t -> int32 ->
alias:[ `raw ] Domain_name.t ->
[ `Out of Rcode.t * Rrsig.t option * Name_rr_map.t * Name_rr_map.t
| `Query of [ `raw ] Domain_name.t ] * Dns_cache.t
val answer : dnssec:bool -> dnssec_ok:bool -> Dns_cache.t -> int64 -> [ `raw ] Domain_name.t -> Packet.Question.qtype ->
[ `Query of [ `raw ] Domain_name.t
| `Packet of Packet.Flags.t * Packet.reply * Name_rr_map.t option ] * Dns_cache.t
val resolve : Dns_cache.t -> qname_minimisation:bool -> dnssec:bool ->
[`Both | `Ipv4_only | `Ipv6_only] -> int64 -> [ `raw ] Domain_name.t ->
Packet.Question.qtype ->
([ `raw ] Domain_name.t * [ `raw ] Domain_name.t * Packet.Question.qtype list * Ipaddr.t list * Dns_cache.t) list
val handle_query : Dns_cache.t -> qname_minimisation:bool -> dnssec:bool -> dnssec_ok:bool ->
rng:(int -> string) -> [`Both | `Ipv4_only | `Ipv6_only ] ->
int64 ->
[ `raw ] Domain_name.t * Packet.Question.qtype ->
[ `Reply of Packet.Flags.t * Packet.reply * Name_rr_map.t option
| `Queries of ([ `raw ] Domain_name.t * ([ `raw ] Domain_name.t * Packet.Question.qtype list) * Ipaddr.t) list ] * Dns_cache.t

View file

@ -0,0 +1,18 @@
let resolver_stats =
let f = function
| `Error -> "error"
| `Queries -> "queries"
| `Blocked -> "blocked"
| `Clients -> "clients"
in
let src = Dns.counter_metrics ~f "dns-resolver" in
(fun r -> Metrics.add src (fun x -> x) (fun d -> d r))
let response_metric =
let store = ref (0L, 0L) in
let data dp =
store := (Int64.succ (fst !store), Int64.add dp (snd !store));
Metrics.Data.v [ Metrics.uint "mean response" (Duration.to_ms (Int64.div (snd !store) (fst !store))) ]
in
let src = Metrics.Src.v ~tags:Metrics.Tags.[] ~data "dns-resolver-timings" in
(fun dp -> Metrics.add src (fun x -> x) (fun d -> d dp))

View file

@ -0,0 +1,3 @@
val resolver_stats : [ `Blocked | `Clients | `Error | `Queries ] -> unit
val response_metric : int64 -> unit

View file

@ -0,0 +1,82 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
open Dns
let root_servers =
List.map (fun (n, ip4, ip6) ->
Domain_name.(host_exn (of_string_exn n)),
Ipaddr.V4.of_string_exn ip4,
Ipaddr.V6.of_string_exn ip6)
[
"a.root-servers.net", "198.41.0.4", "2001:503:ba3e::2:30" ; (* VeriSign, Inc. *)
"b.root-servers.net", "170.247.170.2", "2801:1b8:10::b" ; (* University of Southern California (ISI) *)
"c.root-servers.net", "192.33.4.12", "2001:500:2::c" ; (* Cogent Communications *)
"d.root-servers.net", "199.7.91.13", "2001:500:2d::d" ; (* University of Maryland *)
"e.root-servers.net", "192.203.230.10", "2001:500:a8::e" ; (* NASA (Ames Research Center) *)
"f.root-servers.net", "192.5.5.241", "2001:500:2f::f" ; (* Internet Systems Consortium, Inc. *)
"g.root-servers.net", "192.112.36.4", "2001:500:12::d0d" ; (* US Department of Defense (NIC) *)
"h.root-servers.net", "198.97.190.53", "2001:500:1::53" ; (* US Army (Research Lab) *)
"i.root-servers.net", "192.36.148.17", "2001:7fe::53" ; (* Netnod *)
"j.root-servers.net", "192.58.128.30", "2001:503:c27::2:30" ; (* VeriSign, Inc. *)
"k.root-servers.net", "193.0.14.129", "2001:7fd::1" ; (* RIPE NCC *)
"l.root-servers.net", "199.7.83.42", "2001:500:9f::42" ; (* ICANN *)
"m.root-servers.net", "202.12.27.33", "2001:dc3::35" ; (* WIDE Project *)
]
let a_ttl = 3600000l
let ns_ttl = 518400l
let ns_records =
let ns =
let add_to_set set (name, _, _) = Domain_name.Host_set.add name set in
List.fold_left add_to_set Domain_name.Host_set.empty root_servers
in
(ns_ttl, ns)
let a_records =
List.map (fun (name, ip, _) ->
Domain_name.raw name, (a_ttl, Ipaddr.V4.Set.singleton ip))
root_servers
let aaaa_records =
List.map (fun (name, _, ip) ->
Domain_name.raw name, (a_ttl, Ipaddr.V6.Set.singleton ip))
root_servers
let ips protocol =
List.fold_left (fun acc (_, ip4, ip6) ->
match protocol with
| `Both -> Ipaddr.V4 ip4 :: Ipaddr.V6 ip6 :: acc
| `Ipv4_only -> Ipaddr.V4 ip4 :: acc
| `Ipv6_only -> Ipaddr.V6 ip6 :: acc)
[] root_servers
let reserved_zone_records =
let n = Domain_name.of_string_exn in
(* RFC 6761, avoid them to get out of here + multicast DNS 6762 *)
let zones =
Domain_name.Set.(add (n "local") (* multicast dns, RFC 6762 *)
(add (n "test") (add (n "invalid") (* RFC 6761 *)
(add (n "localhost") (* RFC 6761, draft let-localhost-be-localhost *)
empty))))
in
let local_net_name = "127.in-addr.arpa" in
Domain_name.Set.add (n local_net_name) zones
let stub_soa s =
let nameserver = Domain_name.prepend_label_exn s "ns"
and hostmaster = Domain_name.prepend_label_exn s "hostmaster"
in
{ Soa.nameserver ; hostmaster ; serial = 0l ; refresh = 300l ; retry = 300l ;
expiry = 300l ; minimum = 300l }
let reserved_zones =
let inv s = Rr_map.(B (Soa, stub_soa s)) in
Domain_name.Set.fold (fun n acc -> (n, inv n) :: acc) reserved_zone_records []
let reserved =
Domain_name.Set.fold (fun name trie ->
Dns_trie.insert name Rr_map.Soa (stub_soa name) trie)
reserved_zone_records Dns_trie.empty
let root_servers =
List.map (fun (n, ip4, ip6) -> Domain_name.raw n, ip4, ip6) root_servers

View file

@ -0,0 +1,27 @@
(* (c) 2018 Hannes Mehnert, all rights reserved *)
open Dns
val root_servers : ([ `raw ] Domain_name.t * Ipaddr.V4.t * Ipaddr.V6.t) list
(** [root_servers] are the root servers. *)
val ns_records : (int32 * Domain_name.Host_set.t)
(** [ns_records] is the root nameserver binding. *)
val a_records : ([ `raw ] Domain_name.t * (int32 * Ipaddr.V4.Set.t)) list
(** [a_records] is a list of names and bindings (A records) for the root
servers. *)
val aaaa_records : ([ `raw ] Domain_name.t * (int32 * Ipaddr.V6.Set.t)) list
(** [aaaa_records] is a list of names and bindings (AAAA records) for the root
servers. *)
val ips : [ `Both | `Ipv4_only | `Ipv6_only ] -> Ipaddr.t list
(** [ips ip_proto] is a list of ip addresses of the root servers. *)
val reserved_zones : ([ `raw ] Domain_name.t * Rr_map.b) list
(** [reserved_zones] is a list of names and bindings for reserved zones
specified by RFCs (private network address ranges, private domains) *)
val reserved : Dns_trie.t
(** [reserved] is a trie with all [reserved_zones]. *)

View file

@ -0,0 +1,3 @@
module Root = Dns_resolver_root
module Metrics = Dns_resolver_metrics
module Block = Dns_block

View file

@ -0,0 +1,281 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Dns
open Dns_resolver_cache
let src = Logs.Src.create "dns_resolver_util" ~doc:"DNS resolver util"
module Log = (val Logs.src_log src : Logs.LOG)
type e = E : 'a Rr_map.key * 'a Dns_cache.entry -> e
let invalid_soa name =
let p pre =
Result.value ~default:name
(Result.bind
(Domain_name.prepend_label name "invalid")
(fun n -> Domain_name.prepend_label n pre))
in
{
Soa.nameserver = p "ns" ; hostmaster = p "hostmaster" ;
serial = 1l ; refresh = 16384l ; retry = 2048l ;
expiry = 1048576l ; minimum = 300l
}
let rrsig rr_map ty =
match Rr_map.find Rrsig rr_map with
| Some v ->
Rr_map.Rrsig_set.find_first_opt (fun rrsig ->
rrsig.Rrsig.type_covered = Rr_map.to_int ty)
(snd v)
| None -> None
let noerror bailiwick (_, flags) ~signed q_name q_type (answer, authority) additional =
(* maybe should be passed explicitly (when we don't do qname minimisation) *)
let in_bailiwick name = Domain_name.is_subdomain ~domain:bailiwick ~subdomain:name in
(* ANSWER *)
let answers, anames =
match Domain_name.Map.find q_name answer with
| None ->
(* NODATA (no answer, but SOA (or not) in authority) *)
begin
(* RFC2308, Sec 2.2 "No data":
- answer is empty
- authority has a) SOA + NS, b) SOA, or c) nothing *)
(* an example for this behaviour is NS:
asking for AAAA www.soup.io, get empty answer + SOA in authority
asking for AAAA coffee.soup.io, get empty answer + authority *)
(* the "sub" should be relaxed - for dig ns mail.mehnert.org I get soa in mehnert.org!
--> but how to discover SOA/zone boundaries? *)
let rank rrsig =
if Packet.Flags.mem `Authoritative flags then
Dns_cache.AuthoritativeAuthority (if signed then rrsig else None)
else
Dns_cache.Additional
in
match
Domain_name.Map.fold (fun name rr_map acc ->
if Domain_name.is_subdomain ~subdomain:q_name ~domain:name then
match Rr_map.find Soa rr_map with
| Some soa -> (name, soa, rr_map) :: acc
| None -> acc
else
acc)
authority []
with
| (name, soa, rr_map)::_ ->
begin match q_type with
| `Any -> [] (* i really don't know how to handle ANY NoDATA*)
| `K Rr_map.K k -> [ q_name, E (k, `No_data (name, soa)), rank (rrsig rr_map Soa) ]
(* this is wrong for the normal iterative algorithm:
it asks for foo.com @root, and get .com NS in AU and A in AD
| [] when not (Packet.Header.FS.mem `Truncation flags) ->
Log.warn (fun m -> m "noerror answer, but nothing in authority whose sub is %a in %a, invalid_soa!"
pp_question (q_name, q_type) Name_rr_map.pp authority) ;
[ q_type, q_name, Additional, `No_data (q_name, invalid_soa q_name) ] *)
end
| [] -> [] (* general case when we get an answer from root server *)
end, Domain_name.Set.empty
| Some rr_map ->
let rank rrsig =
if Packet.Flags.mem `Authoritative flags then
Dns_cache.AuthoritativeAnswer (if signed then rrsig else None)
else
Dns_cache.NonAuthoritativeAnswer
in
(* collect those rrsets which are of interest depending on q_type! *)
match q_type with
| `Any ->
Rr_map.fold (fun (B (k, v)) (acc, names) ->
(q_name, E (k, `Entry v), rank (rrsig rr_map k)) :: acc,
Domain_name.Host_set.fold (fun n acc ->
Domain_name.Set.add (Domain_name.raw n) acc)
(Rr_map.names k v) names)
rr_map ([], Domain_name.Set.empty)
| `K (Rr_map.K Cname) ->
begin match Rr_map.find Cname rr_map with
| Some v -> [ q_name, E (Cname, `Entry v), rank (rrsig rr_map Cname) ],
Domain_name.Host_set.fold (fun n acc ->
Domain_name.Set.add (Domain_name.raw n) acc)
(Rr_map.names Cname v) Domain_name.Set.empty
| None ->
(* case no cname *)
Log.warn (fun m -> m "noerror answer with right name, but no cname in %a, invalid soa for %a"
Name_rr_map.pp answer pp_question (q_name, q_type));
[ q_name, E (Cname, `No_data (q_name, invalid_soa q_name)), rank None ],
Domain_name.Set.empty
end
| `K (Rr_map.K k) -> match Rr_map.find k rr_map with
| Some v ->
[ q_name, E (k, `Entry v), rank (rrsig rr_map k) ],
Domain_name.Host_set.fold (fun n acc ->
Domain_name.Set.add (Domain_name.raw n) acc)
(Rr_map.names k v) Domain_name.Set.empty
| None -> match Rr_map.find Cname rr_map with
| None ->
(* case neither TYP nor cname *)
Log.warn (fun m -> m "noerror answer with right name, but not TYP nor cname in %a, invalid soa for %a"
Name_rr_map.pp answer pp_question (q_name, q_type));
[ q_name, E (k, `No_data (q_name, invalid_soa q_name)), rank None ],
Domain_name.Set.empty
| Some cname ->
(* explicitly register as CNAME so it'll be found *)
(* should we try to find further records for the new alias? *)
[ q_name, E (Cname, `Entry cname), rank (rrsig rr_map Cname) ],
Domain_name.Set.singleton (snd cname)
in
(* AUTHORITY - NS and DS records, also nsec and nsec3 *)
let ns, nsnames =
(* authority points us to NS of q_name! *)
(* we collect a list of NS records and the ns names *)
(* TODO need to be more careful, q: foo.com a: foo.com a 1.2.3.4 au: foo.com ns blablubb.com ad: blablubb.com A 1.2.3.4 *)
let rank s =
if Packet.Flags.mem `Authoritative flags then
Dns_cache.AuthoritativeAuthority (if signed then s else None)
else
Dns_cache.Additional
in
let ns, others, names =
Domain_name.Map.fold (fun name map (ns_acc, other_acc, s) ->
if in_bailiwick name then
let ns, s =
match Rr_map.find Ns map with
| None -> ns_acc, s
| Some (ns : int32 * Domain_name.Host_set.t) ->
(name, ns) :: ns_acc, Domain_name.Host_set.fold (fun n acc ->
Domain_name.Set.add (Domain_name.raw n) acc)
(snd ns) s
in
let others = match Rr_map.find Nsec map with
| None -> other_acc
| Some n -> (name, E (Nsec, `Entry n), rank (rrsig map Nsec)) :: other_acc
in
let others = match Rr_map.find Nsec3 map with
| None -> others
| Some n -> (name, E (Nsec3, `Entry n), rank (rrsig map Nsec3)) :: others
in
let others = match Rr_map.find Ds map with
| None -> others
| Some n -> (name, E (Ds, `Entry n), rank (rrsig map Ds)) :: others
in
ns, others, s
else
ns_acc, other_acc, s)
authority
([], [], Domain_name.Set.empty)
in
List.fold_left (fun acc (name, ns) ->
(name, E (Ns, `Entry ns), rank None) :: acc)
others ns, names
in
(* ADDITIONAL *)
(* maybe only these thingies which are subdomains of q_name? *)
(* preserve A/AAAA records only for NS lookups? *)
(* now we have processed:
- answer (filtered to where name = q_name)
- authority with SOA and NS entries
- names from these answers, and authority
- additional section can contain glue records if needed
- only A and AAAA records are of interest for glue *)
let glues =
let names = Domain_name.Set.union anames nsnames in
let names = Domain_name.Set.filter in_bailiwick names in
Domain_name.Set.fold (fun name acc ->
match Domain_name.Map.find name additional with
| None -> acc
| Some map ->
let a = match Rr_map.find A map with
| None -> acc
| Some v -> (name, E (A, `Entry v), Dns_cache.Additional) :: acc
in
match Rr_map.find Aaaa map with
| None -> a
| Some v -> (name, E (Aaaa, `Entry v), Dns_cache.Additional) :: a)
names []
in
(* This is defined in RFC2181, Sec9 -- answer is unique if authority or
additional is non-empty *)
let answer_complete =
not (Domain_name.Map.is_empty authority && Domain_name.Map.is_empty additional)
in
match answers, ns with
| [], [] when not answer_complete && Packet.Flags.mem `Truncation flags ->
(* special handling for truncated replies.. better not add anything *)
Log.warn (fun m -> m "truncated reply for %a, ignoring completely"
pp_question (q_name, q_type));
[]
| [], [] ->
(* not sure if this can happen, maybe discard everything? *)
Log.warn (fun m -> m "reply without answers or ns invalid so for %a"
pp_question (q_name, q_type));
begin match q_type with
| `Any -> []
| `K Rr_map.K k -> [ q_name, E (k,`No_data (q_name, invalid_soa q_name)), Dns_cache.Additional ]
end
| _, _ -> answers @ ns @ glues
let find_soa name authority =
let rec go name =
match Domain_name.Map.find name authority with
| None -> go (Domain_name.drop_label_exn name)
| Some rrmap -> match Rr_map.(find Soa rrmap) with
| None -> go (Domain_name.drop_label_exn name)
| Some soa -> name, soa, rrsig rrmap Soa
in
try Some (go name) with Invalid_argument _ -> None
let nxdomain (_, flags) ~signed name data =
(* we can't do much if authoritiative is not set (some auth dns do so) *)
(* There are cases where answer is non-empty, but contains a CNAME *)
(* RFC 2308 Sec 1 + 2.1 show that NXDomain is for the last QNAME! *)
(* -> need to potentially extract CNAME(s) *)
let answer, authority = match data with
| None -> Name_rr_map.empty, Name_rr_map.empty
| Some x -> x
in
let cnames =
let rec go acc name =
match Domain_name.Map.find name answer with
| None -> acc
| Some rrmap -> match Rr_map.(find Cname rrmap) with
| None -> acc
| Some (ttl, alias) -> go ((name, (ttl, alias), rrsig rrmap Cname) :: acc) alias
in
go [] name
in
let soa = find_soa name authority in
(* since NXDomain have CNAME semantics, we store them as CNAME *)
let rank rrsig =
if Packet.Flags.mem `Authoritative flags then
Dns_cache.AuthoritativeAnswer (if signed then rrsig else None)
else
Dns_cache.NonAuthoritativeAnswer
in
(* we conclude NXDomain, there are 3 cases we care about:
no soa in authority and no cname answer -> inject an invalid_soa (avoid loops)
a matching soa, no cname -> NoDom q_name
_, a matching cname -> NoErr q_name with cname
*)
let entries =
let soa_name, soa, rrsig = match soa with
| None -> name, invalid_soa name, None
| Some x -> x
in
match cnames with
| [] -> [ name, E (Cname, `No_domain (soa_name, soa)), rrsig ]
| rrs -> List.map (fun (name, cname, rrsig) -> (name, E (Cname, `Entry cname), rrsig)) rrs
in
(* the cname does not matter *)
List.map (fun (name, res, rrsig) -> name, res, rank rrsig) entries
let scrub zone ~signed qtype p =
Log.debug (fun m -> m "scrubbing (bailiwick %a) data %a"
Domain_name.pp zone Packet.pp p);
let qname = fst p.question in
match p.Packet.data with
| `Answer data ->
Ok (noerror zone p.header ~signed qname qtype data p.additional)
| `Rcode_error (Rcode.NXDomain, _, data) ->
Ok (nxdomain p.Packet.header ~signed qname data)
| e -> Error (Packet.rcode_data e)

View file

@ -0,0 +1,16 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Dns
type e = E : 'a Rr_map.key * 'a Dns_cache.entry -> e
val scrub : [ `raw ] Domain_name.t -> signed:bool -> Packet.Question.qtype ->
Packet.t ->
(([ `raw ] Domain_name.t * e * Dns_cache.rank) list, Rcode.t) result
(** [scrub bailiwick packet] returns a list of entries to-be-added to the
cache. This respects only in-bailiwick resources records, and qualifies the
[packet]. The purpose is to avoid cache poisoning by not accepting all
resource records. *)
val invalid_soa : [ `raw ] Domain_name.t -> Soa.t
(** [invalid_soa name] returns a stub SOA for [name]. *)

View file

@ -0,0 +1,15 @@
(library
(name dns_resolver)
(public_name dns-resolver)
(instrumentation
(backend bisect_ppx))
(wrapped false)
(modules dns_resolver dns_resolver_utils dns_resolver_cache)
(libraries dns dns.cache dns-server lru duration randomconv dnssec logs dns_resolver_shared))
(library
(name dns_resolver_shared)
(public_name dns-resolver.shared)
(wrapped false)
(modules dns_resolver_shared dns_resolver_root dns_block dns_resolver_metrics)
(libraries dns dns-server metrics))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,238 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Dns
(** DNS Server implementation *)
(** Authentication, stored in a Dns_trie with privileges to operations embedded in the name. *)
module Authentication : sig
(** A key is a pair of a [`raw Domain_name.t] and a [Dnskey.t]. In the name,
operation privileges and potentially IP addresses are encoded, e.g.
[foo._transfer.example.com] may do AXFR on [example.com] and any
subdomain, e.g. [foo.example.com]. *)
type operation = [
| `Update
| `Transfer
| `Notify
]
(** The type of operations, sorted by highest ot lowest privileges, an
[`Update] may as well carry out a [`Transfer]. *)
val operation_to_string : operation -> string
(** [operation_to_string op] is the string representation of [op]. *)
val all_ops : operation list
(** [all_ops] is a list of all operations. *)
val access_granted : required:operation -> operation -> bool
(** [access_granted ~required key_operation] is [true] if [key_operation] is
authorised for [required] operation. *)
val zone_and_operation : 'a Domain_name.t -> ([`host] Domain_name.t * operation) option
(** [zone_and_operation key] is [Some (zone, op)], the [zone] of the [key],
and its operation [op]. If the [key] is not in the expected format, [None]
is returned. *)
val access : ?key:'a Domain_name.t -> zone:'b Domain_name.t -> operation -> bool
(** [access op ~key ~zone] checks whether [key] is authorised for [op] on
[zone]. *)
type t
(** Opaque type for storing authentication keys. *)
end
type t = private {
data : Dns_trie.t ;
auth : Authentication.t ;
unauthenticated_zone_transfer : bool ;
rng : int -> string ;
tsig_verify : Tsig_op.verify ;
tsig_sign : Tsig_op.sign ;
}
(** The state of a DNS server. *)
val create : ?unauthenticated_zone_transfer:bool ->
?tsig_verify:Tsig_op.verify ->
?tsig_sign:Tsig_op.sign ->
?auth:Authentication.t ->
Dns_trie.t ->
(int -> string) ->
t
(** [create ~unauthenticated_zone_transfer ~tsig_verify ~tsig_sign ~auth data rng]
constructs a [t]. See {!Primary.create} and {!Secondary.create} for the
logic running a primary or secondary server. *)
val with_data : t -> Dns_trie.t -> t
(** [with_data t data] is [t'] where the [data] field is updated with the
provided value. Be aware that this function breaks the semantics of a
primary server with secondaries, since secondaries won't be notified and
will be out of sync. Use if you know what you do. The data of a secondary
will usually come via zone transfer from the primary name services. *)
val text : 'a Domain_name.t -> Dns_trie.t -> (string, [> `Msg of string ]) result
(** [text name trie] results in a string representation (zonefile) of the trie. *)
val handle_question : t -> Packet.Question.t ->
(Packet.Flags.t * Packet.Answer.t * Name_rr_map.t option,
Rcode.t * Packet.Answer.t option) result
(** [handle_question t question] handles the DNS query [question] by looking
it up in the trie of [t]. The result is either an answer or an error. *)
val update_data : Dns_trie.t -> 'a Domain_name.t ->
Dns.Packet.Update.prereq list Domain_name.Map.t
* Dns.Packet.Update.update list Domain_name.Map.t ->
( Dns_trie.t * (Domain_name.Set.elt * Dns.Soa.t) list,
Dns.Rcode.t )
result
(** [update_data data domain update_content] applies the [update_content] to
the [data] for [domain]. This function breaks the semantics of a primary
server with secondaries, since the secondaries won't be notified of the
update and will be out of sync. Use if you know what you are doing. *)
val handle_update : t -> proto -> [ `raw ] Domain_name.t option ->
Packet.Question.t -> Packet.Update.t ->
(Dns_trie.t * ([`raw] Domain_name.t * Soa.t) list, Rcode.t) result
(** [handle_update t proto keyname question update] authenticates the update
request and processes the update. This function breaks the semantics of a
primary server with secondaries, since the secondaries won't be notified.
Use if you know what you are doing. *)
val handle_axfr_request : t -> proto -> [ `raw ] Domain_name.t option ->
Packet.Question.t -> (Packet.Axfr.t, Rcode.t) result
(** [handle_axfr_request t proto keyname question] authenticates the zone
transfer request and processes it. If the request is valid, and the zone
available, a zone transfer is returned. *)
type trie_cache
val handle_ixfr_request : t -> trie_cache -> proto -> [ `raw ] Domain_name.t option ->
Packet.Question.t -> Soa.t -> (Packet.Ixfr.t, Rcode.t) result
(** [handle_ixfr_request t cache proto keyname question soa] authenticates the
incremental zone transfer request and processes it. If valid, an incremental
zone transfer is returned. *)
val handle_tsig : ?mac:string -> t -> Ptime.t -> Packet.t ->
string -> (([ `raw ] Domain_name.t * Tsig.t * string * Dnskey.t) option,
Tsig_op.e * string option) result
(** [handle_tsig ~mac t now packet buffer] verifies the tsig
signature if present, returning the keyname, tsig, mac, and used key. *)
type packet_callback = Packet.Question.t -> Packet.reply option
(** [packet_callback question] either returns a reply to a DNS question [Some reply] or [None]. *)
module Primary : sig
type s
(** The state of a primary DNS server. *)
val server : s -> t
(** [server s] is the server of the primary. *)
val data : s -> Dns_trie.t
(** [data s] is the data store of [s]. *)
val with_data : s -> Ptime.t -> int64 -> Dns_trie.t ->
s * (Ipaddr.t * string list) list
(** [with_data s now ts trie] replaces the current data with [trie] in [s].
The returned notifications should be send out. *)
val with_keys : s -> Ptime.t -> int64 -> ('a Domain_name.t * Dnskey.t) list ->
s * (Ipaddr.t * string list) list
(** [with_keys s now ts keys] replaces the current keys with [keys] in [s],
and generates notifications. *)
val trie_cache : s -> trie_cache
(** [trie_cache s] is the trie cache of the server. *)
val create : ?trie_cache_entries:int -> ?keys:('a Domain_name.t * Dnskey.t) list ->
?unauthenticated_zone_transfer:bool ->
?tsig_verify:Tsig_op.verify -> ?tsig_sign:Tsig_op.sign ->
rng:(int -> string) -> Dns_trie.t -> s
(** [create ~trie_cache_entries ~keys ~unauthenticated_zone_transfer
~tsig_verify ~tsig_sign ~rng data] creates a primary server. If
[unauthenticated_zone_transfer] is provided and [true] (defaults to
[false]), anyone can transfer the zones. [trie_cache_entries] is the
backlog to keep in memory for incremental zone transfers (IXFR, default is 5). This
affects memory usage. *)
val handle_packet : ?packet_callback:packet_callback -> s -> Ptime.t -> int64
-> proto -> Ipaddr.t -> int -> Packet.t -> 'a Domain_name.t option ->
s * Packet.t option * (Ipaddr.t * string list) list *
[> `Notify of Soa.t option | `Keep ] option
(** [handle_packet ~packet_callback s now ts src src_port proto key packet]
handles the given [packet], returning new state, an answer, and
potentially notify packets to secondary name servers. If [packet_callback]
is specified, it is called for each incoming query. If it returns
[Some reply], this reply is used instead of the usual lookup in the
zone data. It can be used for custom query processing, such as for load
balancing or transporting data. *)
val handle_buf : ?packet_callback:packet_callback -> s -> Ptime.t -> int64
-> proto -> Ipaddr.t -> int -> string ->
s * string list * (Ipaddr.t * string list) list *
[ `Notify of Soa.t option | `Signed_notify of Soa.t option | `Keep ] option *
[ `raw ] Domain_name.t option
(** [handle_buf ~packet_callback s now ts proto src src_port buffer] decodes
the [buffer], processes the DNS frame using {!handle_packet}, and encodes
the reply. The result is a new state, potentially a list of answers to the
requestor, a list of notifications to send out, information whether a
notify (or signed notify) was received, and the hmac key used for
authentication. If [packet_callback] is specified, it is called for each
incoming query. If it returns [Some reply], this reply is used instead of
the usual lookup in the zone data. This can be used for custom query
processing, such as for load balancing or transporting data. *)
val closed : s -> Ipaddr.t -> s
(** [closed s ip] marks the connection to [ip] closed. *)
val timer : s -> Ptime.t -> int64 ->
s * (Ipaddr.t * string list) list
(** [timer s now ts] may encode some notifications to secondary name servers
if previous ones were not acknowledged. *)
val to_be_notified : s -> [ `host ] Domain_name.t ->
(Ipaddr.t * [ `raw ] Domain_name.t option) list
(** [to_be_notified s zone] returns a list of pairs of IP address and optional
tsig key name of the servers to be notified for a zone change. This list
is based on (a) NS entries for the zone, (b) registered TSIG transfer keys,
and (c) active connection (which transmitted a signed SOA). *)
end
module Secondary : sig
type s
(** The state of a secondary DNS server. *)
val data : s -> Dns_trie.t
(** [data s] is the zone data of [s]. *)
val with_data : s -> Dns_trie.t -> s
(** [with_data s trie] is [s] with its data replaced by [trie]. *)
val create : ?primary:Ipaddr.t ->
tsig_verify:Tsig_op.verify -> tsig_sign:Tsig_op.sign ->
rng:(int -> string) -> ('a Domain_name.t * Dnskey.t) list -> s
(** [create ~primary ~tsig_verify ~tsig_sign ~rng keys] creates a secondary
DNS server state. *)
val handle_packet : ?packet_callback:packet_callback -> s -> Ptime.t -> int64 ->
Ipaddr.t -> Packet.t -> 'a Domain_name.t option ->
s * Packet.t option * (Ipaddr.t * string) option
(** [handle_packet s now ts ip proto key t] handles the incoming packet. *)
val handle_buf : ?packet_callback:packet_callback -> s -> Ptime.t -> int64 ->
proto -> Ipaddr.t -> string ->
s * string option * (Ipaddr.t * string) option
(** [handle_buf ~packet_callback s now ts proto src buf] decodes [buf], processes with
{!handle_packet}, and encodes the results. *)
val timer : s -> Ptime.t -> int64 ->
s * (Ipaddr.t * string list) list
(** [timer s now ts] may request SOA or retransmit AXFR. *)
val closed : s -> Ptime.t -> int64 -> Ipaddr.t ->
s * string list
(** [closed s now ts ip] marks [ip] as closed, the returned buffers (SOA
requests) should be sent to [ip]. *)
end

View file

@ -0,0 +1,551 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
open Dns
module Canonical : sig
type 'a t
val v : 'a Domain_name.t -> 'a t
val d : 'a t -> 'a Domain_name.t
end = struct
type 'a t = 'a Domain_name.t
let v n = Domain_name.canonical n
let d n = n
end
module O = struct
type t = string
let compare = String.compare
end
module M = Map.Make(O)
type t = N of t M.t * Rr_map.t
let empty = N (M.empty, Rr_map.empty)
let is_empty (N (sub, map)) = M.is_empty sub && Rr_map.is_empty map
let bindings t =
let rec go pre (N (sub, e)) =
let subs = M.bindings sub in
(pre, e) ::
List.fold_left
(fun acc (pre', va) ->
acc @ go (Domain_name.prepend_label_exn pre pre') va) [] subs
in
go Domain_name.root t
let pp_map name ppf map =
Fmt.(list ~sep:(any "@.") string) ppf
(List.map (Rr_map.text_b name) (Rr_map.bindings map))
let pp ppf t = List.iter (fun (name, map) -> pp_map name ppf map) (bindings t)
let rec equal (N (sub, map)) (N (sub', map')) =
Rr_map.equal { f = Rr_map.equal_rr } map map' && M.equal equal sub sub'
type e = [ `Delegation of [ `raw ] Domain_name.t * (int32 * Domain_name.Host_set.t)
| `EmptyNonTerminal of [ `raw ] Domain_name.t * Soa.t
| `NotAuthoritative
| `NotFound of [ `raw ] Domain_name.t * Soa.t ]
let pp_e ppf = function
| `Delegation (name, (ttl, ns)) ->
Fmt.pf ppf "delegation %a to TTL %lu %a" Domain_name.pp name ttl
Fmt.(list ~sep:(any ",@,") Domain_name.pp) (Domain_name.Host_set.elements ns)
| `EmptyNonTerminal (name, soa) ->
Fmt.pf ppf "empty non terminal %a SOA %a" Domain_name.pp name Soa.pp soa
| `NotAuthoritative -> Fmt.string ppf "not authoritative"
| `NotFound (name, soa) -> Fmt.pf ppf "not found %a soa %a" Domain_name.pp name Soa.pp soa
let ( let* ) = Result.bind
let guard p err = if p then Ok () else Error err
let ent name map =
let soa = Rr_map.get Soa map in
`EmptyNonTerminal (name, soa)
let to_ns name map =
let ttl, ns =
match Rr_map.find Ns map with
| None -> 0l, Domain_name.Host_set.empty
| Some (ttl, ns) -> ttl, ns
in
(name, ttl, ns)
let check_zone = function
| None -> Error `NotAuthoritative
| Some (`Delegation (name, (ttl, ns))) -> Error (`Delegation (name, (ttl, ns)))
| Some (`Soa (z, zmap)) -> Ok (z, zmap)
let lookup_res zone ty m =
let* z, zmap = check_zone zone in
match Rr_map.find ty m with
| Some v -> Ok (Rr_map.B (ty, v), to_ns z zmap)
| None -> match Rr_map.find Cname m with
| None when Rr_map.cardinal m = 1 && Rr_map.(mem Soa m) ->
(* this is primary a hack for localhost, which must be NXDomain,
but there's a SOA for localhost (to handle it authoritatively) *)
(* TODO should we check that the label-node map is empty?
well, if we have a proper authoritative zone, there'll be a NS *)
let soa = Rr_map.get Rr_map.Soa zmap in
Error (`NotFound (z, soa))
| None -> Error (ent z zmap)
| Some cname -> Ok (B (Cname, cname), to_ns z zmap)
let lookup_aux name t =
let name = Canonical.d name in
let k = Domain_name.to_array name in
let l = Array.length k in
let fzone idx map =
let name = Domain_name.(of_array (Array.sub (to_array name) 0 idx)) in
match Rr_map.mem Soa map, Rr_map.find Ns map with
| true, _ -> Some (`Soa (name, map))
| false, Some ns -> Some (`Delegation (name, ns))
| false, None -> None
in
let rec go idx zone = function
| N (sub, map) ->
let zone = match fzone idx map with None -> zone | Some x -> Some x in
if idx = l then Ok (zone, sub, map)
else match M.find (Array.get k idx) sub with
| exception Not_found ->
begin match zone with
| None -> Error `NotAuthoritative
| Some (`Delegation (name, (ttl, ns))) ->
Error (`Delegation (name, (ttl, ns)))
| Some (`Soa (name, map)) ->
(* may still be a wildcard *)
match M.find "*" sub with
| exception Not_found ->
let soa = Rr_map.get Soa map in
Error (`NotFound (name, soa))
| N (sub, map) -> Ok (zone, sub, map)
end
| x -> go (succ idx) zone x
in
go 0 None t
let lookup_with_cname name ty t =
let* zone, _sub, map = lookup_aux (Canonical.v name) t in
lookup_res zone ty map
let lookup name key t =
let* zone, _sub, map = lookup_aux (Canonical.v name) t in
let* z, zmap = check_zone zone in
Option.to_result ~none:(ent z zmap) (Rr_map.find key map)
let lookup_any name t =
match lookup_aux (Canonical.v name) t with
| Error e -> Error e
| Ok (zone, _sub, m) ->
let* z, zmap = check_zone zone in
Ok (m, to_ns z zmap)
let lookup_glue name t =
match lookup_aux (Canonical.v name) t with
| Error _ -> None, None
| Ok (_zone, _sub, map) -> Rr_map.find A map, Rr_map.find Aaaa map
let zone name t =
match lookup_aux (Canonical.v name) t with
| Error (`NotFound (zone, soa)) -> Ok (zone, soa)
| Error e -> Error e
| Ok (zone, _, _) ->
match check_zone zone with
| Error e -> Error e
| Ok (name, map) ->
(* we ended with `Soa, which checked that map contains a Soa *)
Ok (name, Rr_map.get Soa map)
let fold key (N (sub, map)) f s =
let get name map acc =
match Rr_map.find key map with
| Some a -> f name a acc
| None -> acc
in
let rec collect name sub acc =
List.fold_left (fun acc (pre, N (sub, map)) ->
let n' = Domain_name.prepend_label_exn name pre in
let keys = get n' map acc in
collect n' sub keys)
acc (M.bindings sub)
in
let name = Domain_name.root in
collect name sub (get name map s)
let collect_rrs name sub map =
let collect_map top name rrmap =
if not top && Rr_map.mem Ns rrmap then
(* delegation *)
let ns_entries =
Option.fold ~none:[]
~some:(fun ns -> [ name, Rr_map.B (Ns, ns) ])
(Rr_map.find Ns rrmap)
and ds_entries =
Option.fold ~none:[]
~some:(fun ds -> [ name, Rr_map.B (Ds, ds) ])
(Rr_map.find Ds rrmap)
and rrsig_entries =
Option.fold ~none:[]
~some:(fun rrsig -> [ name, Rr_map.B (Rrsig, rrsig) ])
(Rr_map.find Rrsig rrmap)
in
ns_entries @ ds_entries @ rrsig_entries, false
else
Rr_map.fold (fun v acc -> (name, v) :: acc) rrmap [], true
in
let rec go top name sub map =
let entries, recurse = collect_map top name map in
if recurse then
List.fold_left
(fun acc (pre, N (sub, map)) ->
acc @ go false (Domain_name.prepend_label_exn name pre) sub map)
entries (M.bindings sub)
else
entries
in
go true name sub map
let collect_entries name sub map =
let ttlsoa =
match Rr_map.find Soa map with
| Some v -> Some v
| None when Domain_name.count_labels name = 0 ->
Some { Soa.nameserver = Domain_name.root ;
hostmaster = Domain_name.root ;
serial = 0l ; refresh = 0l ; retry = 0l ;
expiry = 0l ; minimum = 0l }
| None -> None
in
match ttlsoa with
| None -> Error `NotAuthoritative
| Some soa ->
let entries = collect_rrs name sub (Rr_map.remove Soa map) in
let res =
List.fold_left (fun acc (name, (Rr_map.B (k, v))) ->
Name_rr_map.add name k v acc) Domain_name.Map.empty entries
in
Ok (soa, res)
let entries name t =
let name = Domain_name.raw name in
let* zone, sub, map = lookup_aux (Canonical.v name) t in
match zone with
| None -> Error `NotAuthoritative
| Some (`Delegation (name, (ttl, ns))) ->
Error (`Delegation (name, (ttl, ns)))
| Some (`Soa (name', _)) when Domain_name.equal name name' ->
collect_entries name sub map
| Some (`Soa (_, _)) -> Error `NotAuthoritative
type zone_check = [ `Missing_soa of [ `raw ] Domain_name.t
| `Cname_other of [ `raw ] Domain_name.t
| `Bad_ttl of [ `raw ] Domain_name.t * Rr_map.b
| `Empty of [ `raw ] Domain_name.t * Rr_map.k
| `Missing_address of [ `host ] Domain_name.t
| `Soa_not_a_host of [ `raw ] Domain_name.t * string ]
let pp_zone_check ppf = function
| `Missing_soa name -> Fmt.pf ppf "missing soa for %a" Domain_name.pp name
| `Cname_other name -> Fmt.pf ppf "%a contains a cname record, and also other entries" Domain_name.pp name
| `Bad_ttl (name, v) -> Fmt.pf ppf "bad TTL for %a %a" Domain_name.pp name Rr_map.pp_b v
| `Empty (name, typ) -> Fmt.pf ppf "%a empty %a" Domain_name.pp name Rr_map.ppk typ
| `Missing_address name -> Fmt.pf ppf "missing address record for %a" Domain_name.pp name
| `Soa_not_a_host (name, msg) -> Fmt.pf ppf "%a the SOA nameserver is not a hostname: %s" Domain_name.pp name msg
(* TODO: check for no cname loops? and dangling cname!? *)
let check trie =
let has_address name =
match lookup name Rr_map.A trie with
| Ok _ -> true
| Error (`Delegation _) -> true
| _ -> match lookup name Rr_map.Aaaa trie with
| Ok _ -> true
| _ -> false
in
let rec check_sub names state sub map =
let name = Domain_name.of_strings_exn names in
let state' =
match Rr_map.find Soa map with
| None -> begin match Rr_map.find Ns map with
| None -> state
| Some _ -> `None
end
| Some _ -> `Soa name
in
let* () =
guard ((Rr_map.mem Cname map && Rr_map.cardinal map = 1) ||
not (Rr_map.mem Cname map)) (`Cname_other name)
in
let* () =
Rr_map.fold (fun v r ->
let* () = r in
match v with
| B (Dnskey, (ttl, keys)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Dnskey_set.is_empty keys then
Error (`Empty (name, Rr_map.K Dnskey))
else Ok ()
| B (Ns, (ttl, names)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Domain_name.Host_set.is_empty names then
Error (`Empty (name, K Ns))
else
let domain = match state' with `None -> name | `Soa zone -> zone in
Domain_name.Host_set.fold (fun name r ->
let* () = r in
if Domain_name.is_subdomain ~subdomain:name ~domain then
guard (has_address name) (`Missing_address name)
else
Ok ()) names (Ok ())
| B (Cname, (ttl, _)) ->
if ttl < 0l then Error (`Bad_ttl (name, v)) else Ok ()
| B (Mx, (ttl, mxs)) ->
if ttl < 0l then
Error (`Bad_ttl (name, v))
else if Rr_map.Mx_set.is_empty mxs then
Error (`Empty (name, K Mx))
else
let domain = match state' with `None -> name | `Soa zone -> zone in
Rr_map.Mx_set.fold (fun { mail_exchange ; _ } r ->
let* () = r in
if Domain_name.is_subdomain ~subdomain:mail_exchange ~domain then
guard (has_address mail_exchange) (`Missing_address mail_exchange)
else
Ok ())
mxs (Ok ())
| B (Ptr, (ttl, name)) ->
if ttl < 0l then Error (`Bad_ttl (Domain_name.raw name, v)) else Ok ()
| B (Soa, soa) ->
begin match Domain_name.host soa.nameserver with
| Error (`Msg m) -> Error (`Soa_not_a_host (soa.nameserver, m))
| Ok _ -> Ok ()
end
| B (Txt, (ttl, txts)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Txt_set.is_empty txts then
Error (`Empty (name, K Txt))
else if
Rr_map.Txt_set.exists (fun s -> String.length s > 0) txts
then
Ok ()
else
Error (`Empty (name, K Txt))
| B (A, (ttl, a)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Ipaddr.V4.Set.is_empty a then
Error (`Empty (name, K A))
else Ok ()
| B (Aaaa, (ttl, aaaa)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Ipaddr.V6.Set.is_empty aaaa then
Error (`Empty (name, K Aaaa))
else Ok ()
| B (Srv, (ttl, srvs)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Srv_set.is_empty srvs then
Error (`Empty (name, K Srv))
else Ok ()
| B (Svcb, (ttl, svcbs)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Svcb_set.is_empty svcbs then
Error (`Empty (name, K Svcb))
else Ok ()
| B (Https, (ttl, httpss)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Https_set.is_empty httpss then
Error (`Empty (name, K Https))
else Ok ()
| B (Caa, (ttl, caas)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Caa_set.is_empty caas then
Error (`Empty (name, K Caa))
else Ok ()
| B (Tlsa, (ttl, tlsas)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Tlsa_set.is_empty tlsas then
Error (`Empty (name, K Tlsa))
else Ok ()
| B (Sshfp, (ttl, sshfps)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Sshfp_set.is_empty sshfps then
Error (`Empty (name, K Sshfp))
else Ok ()
| B (Ds, (ttl, ds)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Ds_set.is_empty ds then
Error (`Empty (name, K Ds))
else Ok ()
| B (Rrsig, (ttl, rrs)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Rrsig_set.is_empty rrs then
Error (`Empty (name, K Rrsig))
else Ok ()
| B (Nsec, (ttl, _rr)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else Ok ()
| B (Nsec3, (ttl, _rr)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else Ok ()
| B (Loc, (ttl, locs)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Loc_set.is_empty locs then
Error (`Empty (name, Rr_map.K Loc))
else Ok ()
| B (Null, (ttl, nulls)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Null_set.is_empty nulls then
Error (`Empty (name, Rr_map.K Null))
else Ok ()
| B (Unknown x, (ttl, datas)) ->
if ttl < 0l then Error (`Bad_ttl (name, v))
else if Rr_map.Txt_set.is_empty datas then
Error (`Empty (name, K (Unknown x)))
else Ok ())
map (Ok ())
in
M.fold (fun lbl (N (sub, map)) r ->
let* () = r in
check_sub (lbl :: names) state' sub map) sub (Ok ())
in
let (N (sub, map)) = trie in
check_sub [] `None sub map
let find f name t =
let name = Canonical.d name in
let lbls = Domain_name.to_array name in
let l = Array.length lbls in
let rec go idx (N (sub, map)) =
if idx = l then
let sub', map' = f sub map in
N (sub', map')
else
let lbl = Array.get lbls idx in
let node = match M.find lbl sub with
| exception Not_found -> empty
| x -> x
in
let node' = go (succ idx) node in
if is_empty node' then
N (M.remove lbl sub, map)
else
N (M.add lbl node' sub, map)
in
go 0 t
let replace name k v t =
find (fun sub map -> sub, Rr_map.add k v map) (Canonical.v name) t
let insert name k v t =
let name = Canonical.v name in
let merge sub map =
let new_v = match Rr_map.find k map with
| None -> v
| Some v' -> Rr_map.union_rr k v' v
in
sub, Rr_map.add k new_v map
in
find merge name t
let replace_map m t =
Domain_name.Map.fold (fun name map trie ->
find (fun sub _ -> sub, map) (Canonical.v name) trie) m t
let insert_map m t =
Domain_name.Map.fold (fun name map trie ->
let union sub old = sub, Rr_map.union { f = Rr_map.unionee } old map in
find union (Canonical.v name) trie)
m t
let remove k ty v t =
let remove sub map =
let map' = match Rr_map.find ty map with
| None -> map
| Some old -> match Rr_map.remove_rr ty old v with
| None -> Rr_map.remove ty map
| Some v' -> Rr_map.add ty v' map
in
sub, map'
in
find remove (Canonical.v k) t
let remove_ty k ty t =
let remove sub map = sub, Rr_map.remove ty map in
find remove (Canonical.v k) t
let remove_all k t =
let remove sub _ = sub, Rr_map.empty in
find remove (Canonical.v k) t
let remove_map m t =
let merge k present remove = match present, remove with
| None, None -> None
| Some x, None -> Some x
| None, Some _ -> None
| Some x, Some y -> Rr_map.remove_rr k x y
in
Domain_name.Map.fold (fun name map trie ->
let remove sub old = sub, Rr_map.merge { f = merge } old map in
find remove (Canonical.v name) trie)
m t
let remove_zone name t =
let remove sub _ =
(* go through all of sub, and retain those subtrees with Soa *)
let rec fold_sub sub =
M.fold (fun lbl node acc ->
match go node with None -> acc | Some n -> M.add lbl n acc)
sub M.empty
and go (N (sub, map)) =
match Rr_map.find Soa map with
| None ->
(* no SOA, continue search *)
let sub' = fold_sub sub in
if M.is_empty sub' then None else Some (N (sub', Rr_map.empty))
| Some _ ->
(* SOA, retain this submap *)
Some (N (sub, map))
in
fold_sub sub, Rr_map.empty (* drop the initial RRmap in any case! *)
in
find remove (Canonical.v name) t
let diff zone req_soa ~old current =
match entries zone current with
| Error _ -> Error (`Msg "couldn't find zone in current trie")
| Ok (soa, map) ->
if not (Soa.newer ~old:req_soa soa) then
Ok (soa, `Empty)
else
match entries zone old with
| Error _ -> Ok (soa, `Full map)
| Ok (oldsoa, oldmap) ->
(* first, we fold over old map and collect the differences in two maps *)
let (to_remove, to_add), names =
Domain_name.Map.fold (fun name old ((to_remove, to_add), names) ->
let newmap =
match Domain_name.Map.find name map with
| None -> Rr_map.empty | Some x -> x
in
(match Rr_map.diff ~old newmap with
| None, None -> to_remove, to_add
| Some rm, None -> Domain_name.Map.add name rm to_remove, to_add
| None, Some add -> to_remove, Domain_name.Map.add name add to_add
| Some rm, Some add ->
Domain_name.Map.add name rm to_remove,
Domain_name.Map.add name add to_add),
Domain_name.Set.add name names)
oldmap Domain_name.((Map.empty, Map.empty), Set.empty)
in
(* now we fold over newmap and add then unless already handled *)
let to_add =
Domain_name.Map.fold (fun name newmap to_add ->
if Domain_name.Set.mem name names then
to_add
else
Domain_name.Map.add name newmap to_add)
map to_add
in
Ok (soa, `Difference (oldsoa, to_remove, to_add))

View file

@ -0,0 +1,140 @@
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
(** Prefix tree data structure for domain names
The key is a {!Domain_name}, whereas the value may be any resource record.
The representation is a tree, where the edges are domain name labels, and
the nodes carry a {{!Dns.Rr_map.t}resource map}.
Some special treatment is applied for zones, which must have a start of
authority entry and a set of name servers. End of authority, also known as
delegation, is supported. Aliases (canonical names, CNAME records) are also
supported.
The data structure tries to preserve invariants recommended by the domain
name system, such as that for any name there may either be an alias or any
other record, there must be a SOA record, and multiple NS records for an
authoritative zone, a resource type must have entries of the given type (no
NS record for A type, the ttl for all resource records of a rrset is the
same.
*)
open Dns
(** {2 Abstract trie type} *)
type t
(** The type of the trie. *)
val pp : t Fmt.t
(** [pp ppf t] pretty prints [t] to [ppf]. *)
val empty : t
(** [empty] is the empty trie. *)
val equal : t -> t -> bool
(** [equal a b] compares [a] with [b]. *)
(** {2 Operations to modify the trie} *)
val insert_map : Rr_map.t Domain_name.Map.t -> t -> t
(** [insert_map m t] inserts all elements of the domain name map [m] into
[t], potentially existing are unioned with {!Dns.Rr_map.unionee}. *)
val replace_map : Rr_map.t Domain_name.Map.t -> t -> t
(** [replace_map m t] replaces in the trie [t] all existing bindings of the
domain name map [m] with the provided map. *)
val remove_map : Rr_map.t Domain_name.Map.t -> t -> t
(** [remove_map m t] removes all elements of the domain name map [m] from
[t]. *)
val insert : 'a Domain_name.t -> 'b Rr_map.key -> 'b -> t -> t
(** [insert n k v t] inserts [k, v] under [n] in [t]. Existing entries are
unioneed with {!Dns.Rr_map.union_rr}. *)
val replace : 'a Domain_name.t -> 'b Rr_map.key -> 'b -> t -> t
(** [replace n k v t] inserts [k, v] under [n] in [t]. Existing entries are
replaced. *)
val remove : 'a Domain_name.t -> 'b Rr_map.key -> 'b -> t -> t
(** [remove k ty v t] removes [ty, v] from [t] at [k]. Beware, this may lead
to a [t] where the initially mentioned invariants are violated. *)
val remove_ty : 'a Domain_name.t -> 'b Rr_map.key -> t -> t
(** [remove_ty k ty t] removes [ty] from [t] at [k]. Beware, this may lead to a
[t] where the initially mentioned invariants are violated. *)
val remove_all : 'a Domain_name.t -> t -> t
(** [remove_all k t] removes all entries of [k] in [t]. Beware, this may lead to
a [t] where the initially mentioned invariants are violated. *)
val remove_zone : 'a Domain_name.t -> t -> t
(** [remove_zone name t] remove the zone [name] from [t], retaining subzones
(entries with [Soa] records). This removes as well any delegations. *)
(** {2 Checking invariants} *)
type zone_check = [ `Missing_soa of [ `raw ] Domain_name.t
| `Cname_other of [ `raw ] Domain_name.t
| `Bad_ttl of [ `raw ] Domain_name.t * Rr_map.b
| `Empty of [ `raw ] Domain_name.t * Rr_map.k
| `Missing_address of [ `host ] Domain_name.t
| `Soa_not_a_host of [ `raw ] Domain_name.t * string ]
val pp_zone_check : zone_check Fmt.t
(** [pp_err ppf err] pretty prints the error [err]. *)
val check : t -> (unit, zone_check) result
(** [check t] checks all invariants. *)
(** {2 Lookup} *)
type e = [ `Delegation of [ `raw ] Domain_name.t * (int32 * Domain_name.Host_set.t)
| `EmptyNonTerminal of [ `raw ] Domain_name.t * Soa.t
| `NotAuthoritative
| `NotFound of [ `raw ] Domain_name.t * Soa.t ]
(** The type of lookup errors. *)
val pp_e : e Fmt.t
(** [pp_e ppf e] pretty-prints [e] on [ppf]. *)
val zone : 'a Domain_name.t -> t ->
([ `raw ] Domain_name.t * Soa.t, e) result
(** [zone k t] returns either the zone and soa for [k] in [t], or an error. *)
val lookup_with_cname : 'a Domain_name.t -> 'b Rr_map.key -> t ->
(Rr_map.b * ([ `raw ] Domain_name.t * int32 * Domain_name.Host_set.t), e) result
(** [lookup_with_cname k ty t] finds [k, ty] in [t]. It either returns the found
resource record set and authority information, a cname alias and authority
information, or an error. *)
val lookup : 'a Domain_name.t -> 'b Rr_map.key -> t -> ('b, e) result
(** [lookup k ty t] finds [k, ty] in [t], which may lead to an error. *)
val lookup_any : 'a Domain_name.t -> t ->
(Rr_map.t * ([ `raw ] Domain_name.t * int32 * Domain_name.Host_set.t), e) result
(** [lookup_any k t] looks up all resource records of [k] in [t], and returns
that and the authority information. *)
val lookup_glue : 'a Domain_name.t -> t ->
(int32 * Ipaddr.V4.Set.t) option * (int32 * Ipaddr.V6.Set.t) option
(** [lookup_glue k t] finds glue records (A, AAAA) for [k] in [t]. It ignores
potential DNS invariants, e.g. that there is no surrounding zone. *)
val entries : 'a Domain_name.t -> t ->
(Dns.Soa.t * Rr_map.t Domain_name.Map.t, e) result
(** [entries name t] returns either the SOA and all entries for the requested
[name], or an error. *)
val fold : 'a Rr_map.key -> t -> ([ `raw ] Domain_name.t -> 'a -> 'b -> 'b) -> 'b -> 'b
(** [fold key t f acc] calls [f] with [dname value acc] element in [t]. *)
val diff : 'a Domain_name.t -> Soa.t -> old:t -> t ->
(Soa.t * [ `Empty | `Full of Name_rr_map.t | `Difference of Soa.t * Name_rr_map.t * Name_rr_map.t ],
[> `Msg of string ]) result
(** [diff zone soa ~old trie] computes the difference of [zone] in [old] and
[trie], and returns either [`Empty] if [soa] is equal or newer than the one
in [trie], [`Full] (the same as [entries]) if [zone] is not present in [old],
or [`Difference (old_soa, deleted, added)]. Best used with IXFR. An error
occurs if [zone] is not present in [trie]. *)

View file

@ -0,0 +1,5 @@
(library
(name dns_server)
(public_name dns-server)
(wrapped false)
(libraries dns randomconv duration metrics))

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
(library
(name dns)
(public_name dns)
(wrapped false)
(libraries fmt ipaddr logs ptime gmap domain-name metrics base64 ohex)
; (preprocess (pps ppx_expect)) ; once https://github.com/ocaml/dune/issues/897 is resolved
; (inline_tests)
)

View file

@ -0,0 +1,201 @@
open Dns
let ip = Ipaddr.V4.of_string_exn
let name = Domain_name.of_string_exn
let invalid_soa name =
let p pre =
Domain_name.(prepend_label_exn name "invalid" |> fun n -> prepend_label_exn n pre)
in
{
Soa.nameserver = p "ns" ; hostmaster = p "hostmaster" ;
serial = 1l ; refresh = 16384l ; retry = 2048l ;
expiry = 1048576l ; minimum = 300l
}
let cached_err =
let module M = struct
type t = [ `Cache_miss | `Cache_drop ]
let pp ppf = function
| `Cache_miss -> Fmt.string ppf "cache miss"
| `Cache_drop -> Fmt.string ppf "cache drop"
let equal a b = match a, b with
| `Cache_miss, `Cache_miss -> true
| `Cache_drop, `Cache_drop -> true
| _ -> false
end in
(module M: Alcotest.TESTABLE with type t = M.t)
let entry_eq t a b =
match a, b with
| `Entry b, `Entry b' -> Rr_map.equal_rr t b b'
| `No_data (name, soa), `No_data (name', soa') -> Domain_name.equal name name' && Dns.Soa.compare soa soa' = 0
| `No_domain (name, soa), `No_domain (name', soa') -> Domain_name.equal name name' && Dns.Soa.compare soa soa' = 0
| `Serv_fail (name, soa), `Serv_fail (name', soa') -> Domain_name.equal name name' && Dns.Soa.compare soa soa' = 0
| _, _ -> false
let cached_ok (t : 'a Rr_map.key) =
let pp ppf res = Dns_cache.pp_entry t ppf (fst res)
and equal (r, rank) (r', rank') =
entry_eq t r r' && Dns_cache.compare_rank rank rank' = 0
in
Alcotest.testable pp equal
let cached_r t = Alcotest.(result (cached_ok t) cached_err)
let empty_cache () =
let cache = Dns_cache.empty 100 in
Alcotest.check (cached_r Rr_map.A) "empty cache results in Cache_miss"
(Error `Cache_miss)
(snd (Dns_cache.get cache 0L (name "an-actual-website.com") A))
let cache_a () =
let cache = Dns_cache.empty 100 in
let name = name "an-actual-website.com" in
let a = 250l, Ipaddr.V4.Set.singleton (ip "1.2.3.4") in
let cache = Dns_cache.set cache 0L name A (AuthoritativeAnswer None) (`Entry a) in
Alcotest.check (cached_r Rr_map.A) "cache with A results in res"
(Ok (`Entry a, AuthoritativeAnswer None))
(snd (Dns_cache.get cache 0L name A)) ;
Alcotest.check (cached_r Rr_map.Cname) "cache with A results in CacheMiss"
(Error `Cache_miss) (snd (Dns_cache.get cache 0L name Cname))
let cache_nodata () =
let cache = Dns_cache.empty 100 in
let name = name "an-alias.com"
and subname = name "another-domain.an-alias.com"
in
let soa = invalid_soa name in
let nodata = `No_data (subname, soa) in
let a = 250l, Ipaddr.V4.Set.singleton (ip "1.2.3.4") in
let cache = Dns_cache.set cache 0L name A (AuthoritativeAnswer None) (`Entry a) in
let cache = Dns_cache.set cache 0L subname A (AuthoritativeAnswer None) nodata in
Alcotest.check (cached_r Rr_map.A) "cache with A nodata results in nodata"
(Ok (nodata, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subname A)) ;
Alcotest.check (cached_r Rr_map.Ns) "cache with A nodata results in cache miss for NS"
(Error `Cache_miss) (snd (Dns_cache.get cache 0L subname Ns)) ;
Alcotest.check (cached_r Rr_map.A) "cache with A nodata results in a record"
(Ok (`Entry a, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L name A)) ;
Alcotest.check (cached_r Rr_map.Ns) "cache with A nodata results in cache miss for NS'"
(Error `Cache_miss) (snd (Dns_cache.get cache 0L name Ns)) ;
let cache = Dns_cache.set cache 0L subname A (AuthoritativeAnswer None) (`Entry a) in
Alcotest.check (cached_r Rr_map.A) "cache with A nodata results in nodata"
(Ok (`Entry a, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subname A))
let cache_nodom () =
let cache = Dns_cache.empty 100 in
let name = name "an-alias.com"
and subname = name "another-domain.an-alias.com"
and subsubname = name "fo.another-domain.an-alias.com"
in
let soa = invalid_soa name in
let nodom = `No_domain (subname, soa) in
let a = 250l, Ipaddr.V4.Set.singleton (ip "1.2.3.4") in
let cache = Dns_cache.set cache 0L name A (AuthoritativeAnswer None) (`Entry a) in
let cache = Dns_cache.set cache 0L subname A (AuthoritativeAnswer None) nodom in
Alcotest.check (cached_r Rr_map.A) "cache with A nodom results in nodom"
(Ok (nodom, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subname A)) ;
Alcotest.check (cached_r Rr_map.Ns) "cache with A nodom results in nodom for NS"
(Ok (nodom, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subname Ns)) ;
Alcotest.check (cached_r Rr_map.Ns) "cache with A nodom results in nodom for Ns and subsub"
(Ok (nodom, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subsubname Ns)) ;
Alcotest.check (cached_r Rr_map.A) "cache with A nodom results in nodom for A and subsub"
(Ok (nodom, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subsubname A)) ;
Alcotest.check (cached_r Rr_map.A) "cache with A nodom results in a record"
(Ok (`Entry a, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L name A)) ;
Alcotest.check (cached_r Rr_map.Ns) "cache with A nodom results in cache miss for NS'"
(Error `Cache_miss) (snd (Dns_cache.get cache 0L name Ns)) ;
let cache = Dns_cache.set cache 0L subname A (AuthoritativeAnswer None) (`Entry a) in
Alcotest.check (cached_r Rr_map.A) "cache with A nodata results in nodom"
(Ok (`Entry a, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subname A))
let cache_tests = [
"empty cache", `Quick, empty_cache ;
"cache with A", `Quick, cache_a ;
"cache nodata", `Quick, cache_nodata ;
"cache nodom", `Quick, cache_nodom ;
]
let entry_or_cname t a b = match a, b with
| (`Alias (ttl, name), r),
(`Alias (ttl', name'), r') ->
ttl = ttl' && Domain_name.equal name name' && Dns_cache.compare_rank r r' = 0
| (#Dns_cache.entry as e1, r1), (#Dns_cache.entry as e2, r2) ->
entry_eq t e1 e2 && Dns_cache.compare_rank r1 r2 = 0
| _ -> false
let pp_or_cname t ppf = function
| `Alias (ttl, name), _ -> Fmt.pf ppf "alias %lu %a" ttl Domain_name.pp name
| #Dns_cache.entry as e, _ -> Dns_cache.pp_entry t ppf e
let cname_or_cached t =
Alcotest.testable (pp_or_cname t) (entry_or_cname t)
let cached_cname_r t = Alcotest.result (cname_or_cached t) cached_err
let empty = Dns_cache.empty 100
let cname_empty_cache () =
Alcotest.check (cached_cname_r Rr_map.A) "empty cache results in Cache_miss"
(Error `Cache_miss)
(snd (Dns_cache.get_or_cname empty 0L (name "foo.com") A))
let cname_cache_a () =
let name = name "foo.com" in
let a = 250l, Ipaddr.V4.Set.singleton (ip "1.2.3.4") in
let cache = Dns_cache.set empty 0L name A (AuthoritativeAnswer None) (`Entry a) in
Alcotest.check (cached_cname_r Rr_map.A) "cache with A results in res"
(Ok (`Entry a, AuthoritativeAnswer None))
(snd (Dns_cache.get_or_cname cache 0L name A)) ;
Alcotest.check (cached_cname_r Rr_map.Cname) "cache with A results in CacheMiss"
(Error `Cache_miss)
(snd (Dns_cache.get_or_cname cache 0L name Cname))
let cname_cache_cname () =
let rel = name "bar.com" in
let name = name "foo.com" in
let cname = 250l, rel in
let cache = Dns_cache.set empty 0L name Cname (AuthoritativeAnswer None) (`Entry cname) in
Alcotest.check (cached_cname_r Rr_map.Cname) "cache with CNAME results in res"
(Ok (`Alias cname, AuthoritativeAnswer None))
(snd (Dns_cache.get_or_cname cache 0L name Cname)) ;
Alcotest.check (cached_cname_r Rr_map.A) "cache with CNAME results in res for A"
(Ok (`Alias cname, AuthoritativeAnswer None))
(snd (Dns_cache.get_or_cname cache 0L name A)) ;
Alcotest.check (cached_cname_r Rr_map.Ns) "cache with CNAME results in res for NS"
(Ok (`Alias cname, AuthoritativeAnswer None))
(snd (Dns_cache.get_or_cname cache 0L name Ns))
let cname_cache_cname_nodata () =
let rel = name "bar.com" in
let name = name "foo.com" in
let cname = 250l, rel in
let bad_soa = invalid_soa name in
let cache =
Dns_cache.set
(Dns_cache.set empty 0L name Cname (AuthoritativeAnswer None) (`Entry cname))
0L name Ns (AuthoritativeAnswer None) (`No_data (name, bad_soa))
in
Alcotest.check (cached_cname_r Rr_map.Cname) "cache with CNAME results in res"
(Ok (`Alias cname, AuthoritativeAnswer None))
(snd (Dns_cache.get_or_cname cache 0L name Cname)) ;
Alcotest.check (cached_cname_r Rr_map.Ns) "cache with CNAME results in res for NS"
(Ok (`Alias cname, AuthoritativeAnswer None))
(snd (Dns_cache.get_or_cname cache 0L name Ns)) ;
Alcotest.check (cached_cname_r Rr_map.A) "cache with CNAME results in res for A"
(Ok (`Alias cname, AuthoritativeAnswer None))
(snd (Dns_cache.get_or_cname cache 0L name A))
let cname_cache_tests = [
"empty cache", `Quick, cname_empty_cache ;
"cache with A", `Quick, cname_cache_a ;
"cache with CNAME", `Quick, cname_cache_cname ;
"cache with another cname", `Quick, cname_cache_cname_nodata ;
]
let tests = [
"cache tests", cache_tests;
"cname cache tests", cname_cache_tests;
]
let () = Alcotest.run "DNS cache tests" tests

View file

@ -0,0 +1,328 @@
let ip =
let module M = struct
type t = Ipaddr.V4.t
let pp = Ipaddr.V4.pp
let equal a b = Ipaddr.V4.compare a b = 0
end in
(module M : Alcotest.TESTABLE with type t = M.t)
let ipset = Alcotest.(slist ip Ipaddr.V4.compare)
let p_cs = Alcotest.testable (Ohex.pp_hexdump ()) String.equal
module Make_query_tests = struct
let produces_same_output () =
let rng n = String.make n '\x00' in
let name:'a Domain_name.t = Domain_name.of_string_exn "example.com" in
let actual, _state = Dns_client.Pure.make_query rng `Tcp `Auto name Dns.Rr_map.A in
let expected = Ohex.decode
"00 2e 00 00 01 00 00 01 00 00 00 00 00 01 07 65
78 61 6d 70 6c 65 03 63 6f 6d 00 00 01 00 01 00
00 29 02 00 00 00 00 00 00 06 00 0b 00 02 04 b0" in
Alcotest.check p_cs "produces cool stuff" expected actual
let tests = [
"produces same output", `Quick, produces_same_output;
"fails on unspported query_type", `Quick, produces_same_output;
]
end
module Parse_response_tests = struct
let unpacks_response () =
(* Bytes 3, 4 are set to `00 00` - these represent query ID *)
let ipv4_buf = Ohex.decode
"00 77 00 00 81 80 00 01 00 01 00 02 00 02 03 66
6f 6f 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 00
01 00 00 02 2a 00 04 17 17 56 2c c0 0c 00 02 00
01 00 01 87 ae 00 10 03 6e 73 31 09 64 69 67 69
6d 65 64 69 61 c0 10 c0 0c 00 02 00 01 00 01 87
ae 00 06 03 6e 73 32 c0 39 c0 35 00 01 00 01 00
02 40 8a 00 04 17 15 f2 58 c0 51 00 01 00 01 00
02 40 8a 00 04 17 15 f3 77" in
(* This `rng` generates zeros, used for the query ID above *)
let rng n = String.make n '\x00' in
let name:'a Domain_name.t = Domain_name.of_string_exn "foo.com" in
let _actual, state = Dns_client.Pure.make_query rng `Tcp `Auto name Dns.Rr_map.A in
match Dns_client.Pure.handle_response state ipv4_buf with
| Ok `Data _ -> () (* TODO: Alcotest TESTABLE for this return value *)
| _ -> ignore(failwith "error")
let fails_to_unpack_mismatched () =
(* TODO: It is possible to use crowbar here, to generate the ipv4_buf *)
(* Bytes 3, 4 are set to `aa aa` - these represent query ID *)
let ipv4_buf = Ohex.decode
"00 77 aa aa 81 80 00 01 00 01 00 02 00 02 03 66
6f 6f 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 00
01 00 00 02 2a 00 04 17 17 56 2c c0 0c 00 02 00
01 00 01 87 ae 00 10 03 6e 73 31 09 64 69 67 69
6d 65 64 69 61 c0 10 c0 0c 00 02 00 01 00 01 87
ae 00 06 03 6e 73 32 c0 39 c0 35 00 01 00 01 00
02 40 8a 00 04 17 15 f2 58 c0 51 00 01 00 01 00
02 40 8a 00 04 17 15 f3 77" in
(* This `rng` generates zeros, used for the query ID above *)
let rng n = String.make n '\x00' in
let name:'a Domain_name.t = Domain_name.of_string_exn "foo.com" in
let _actual, state = Dns_client.Pure.make_query rng `Tcp `Auto name Dns.Rr_map.A in
match Dns_client.Pure.parse_response state ipv4_buf with
| Error `Msg _ -> ()
| __ -> failwith "should have rejected mismatched input"
(* TODO: Alcotest TESTABLE for this return value *)
let tests = [
"unpacks some kind of response", `Quick, unpacks_response;
"fails to unpack response with mismatching query ID", `Quick, fails_to_unpack_mismatched;
]
end
(* {!Transport} provides a mock implementation of the transport used by
Dns_client.Make. The mock data uses the default_debug_info reference cell.
*)
type debug_info = string
let default_debug_info = ref []
module Transport (*: Dns_client.S
with type io_addr = debug_info
and type stack = unit
and type +'a io = 'a *)
= struct
type io_addr = debug_info
type stack = Dns.proto
type context = debug_info list ref
type t = Dns.proto
type +'a io = 'a
let create ?nameservers:_ ~timeout:_ proto = proto
let nameservers proto = proto, !default_debug_info
let rng n = String.make n '\x00'
let clock () = 0L
let bind a b = b a
let lift v = v
let close _ = ()
let connect a = Ok (a, default_debug_info)
let send_recv (mock_responses : context) _ =
match !mock_responses with
| [] -> failwith("nothing to recv from the wire")
| hd::tail -> mock_responses := tail; Ok hd
end
(* Now that we have our {!Transport} implementation we can include the logic
that goes on top of it: *)
include Dns_client.Make(Transport)
module Transport_with_time_machine = struct
include Transport
(* the timestamps are for: cache lookup1, cache upate, cache lookup2, cache update2 *)
let timestamps = ref [0L; 0L; Duration.of_sec 601 ; Duration.of_sec 601]
let clock () =
match !timestamps with
| [] -> assert false
| head::tail -> timestamps := tail; head
end
module Dns_client_with_time_machine = Dns_client.Make(Transport_with_time_machine)
module Gethostbyname_tests = struct
let foo_com_is_valid () =
let domain_name = Domain_name.(of_string_exn "foo.com" |> host_exn) in
(* Bytes 3, 4 are set to `00 00` - these represent query ID *)
let ipv4_buf = Ohex.decode
"00 77 00 00 81 80 00 01 00 01 00 02 00 02 03 66
6f 6f 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 00
01 00 00 02 2a 00 04 17 17 56 2c c0 0c 00 02 00
01 00 01 87 ae 00 10 03 6e 73 31 09 64 69 67 69
6d 65 64 69 61 c0 10 c0 0c 00 02 00 01 00 01 87
ae 00 06 03 6e 73 32 c0 39 c0 35 00 01 00 01 00
02 40 8a 00 04 17 15 f2 58 c0 51 00 01 00 01 00
02 40 8a 00 04 17 15 f3 77" in
let t = create `Tcp in
default_debug_info := [ipv4_buf];
match gethostbyname t domain_name with
| Ok _ip -> ()
| Error _ -> failwith "foo.com should have been returned"
let returns_from_the_cache () =
let domain_name = Domain_name.(of_string_exn "foo.com" |> host_exn) in
(* Bytes 3, 4 are set to `00 00` - these represent query ID *)
let ipv4_buf = Ohex.decode
"00 77 00 00 81 80 00 01 00 01 00 02 00 02 03 66
6f 6f 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 00
01 00 00 02 2a 00 04 17 17 56 2c c0 0c 00 02 00
01 00 01 87 ae 00 10 03 6e 73 31 09 64 69 67 69
6d 65 64 69 61 c0 10 c0 0c 00 02 00 01 00 01 87
ae 00 06 03 6e 73 32 c0 39 c0 35 00 01 00 01 00
02 40 8a 00 04 17 15 f2 58 c0 51 00 01 00 01 00
02 40 8a 00 04 17 15 f3 77" in
let t = create `Tcp in
default_debug_info := [ipv4_buf];
match gethostbyname t domain_name with
| Error _ -> failwith "foo.com should have been returned"
| Ok _ip ->
default_debug_info := [];
match gethostbyname t domain_name with
| Error _ -> failwith "should have been cached"
| Ok _ -> () (* we returned content, but the wire stayed silent *)
let uses_network_when_cache_evicted () =
let domain_name = Domain_name.(of_string_exn "foo.com" |> host_exn) in
(* Bytes 3, 4 are set to `00 00` - these represent query ID *)
let ipv4_buf = Ohex.decode
"00 77 00 00 81 80 00 01 00 01 00 02 00 02 03 66
6f 6f 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 00
01 00 00 02 2a 00 04 17 17 56 2c c0 0c 00 02 00
01 00 01 87 ae 00 10 03 6e 73 31 09 64 69 67 69
6d 65 64 69 61 c0 10 c0 0c 00 02 00 01 00 01 87
ae 00 06 03 6e 73 32 c0 39 c0 35 00 01 00 01 00
02 40 8a 00 04 17 15 f2 58 c0 51 00 01 00 01 00
02 40 8a 00 04 17 15 f3 77" in
let t = Dns_client_with_time_machine.create `Tcp in
default_debug_info := [ipv4_buf];
match Dns_client_with_time_machine.gethostbyname t domain_name with
| Error _ -> failwith "foo.com should have been returned"
| Ok _ip ->
default_debug_info := [ipv4_buf];
match Dns_client_with_time_machine.gethostbyname t domain_name with
| Error _ -> failwith "should have been cached"
| Ok _ -> (* we returned content, AND the wire was used *)
assert (!default_debug_info = [])
let tests = [
"foo.com is valid", `Quick, foo_com_is_valid;
"when cache is populated, return from cache", `Quick, returns_from_the_cache;
"when content evicted, use network", `Quick, uses_network_when_cache_evicted;
]
end
module Getaddrinfo_tests = struct
let supports_mx_packets () =
let domain_name = Domain_name.(of_string_exn "google.com" |> host_exn) in
(* a google.com MX record - bytes 3,4 are set to the query ID 00 00 *)
let ipv4_buf = Ohex.decode
"02 1e 00 00 81 80 00 01 00 05 00 04 00 0f 06 67
6f 6f 67 6c 65 03 63 6f 6d 00 00 0f 00 01 c0 0c
00 0f 00 01 00 00 02 58 00 11 00 1e 04 61 6c 74
32 05 61 73 70 6d 78 01 6c c0 0c c0 0c 00 0f 00
01 00 00 02 58 00 04 00 0a c0 2f c0 0c 00 0f 00
01 00 00 02 58 00 09 00 28 04 61 6c 74 33 c0 2f
c0 0c 00 0f 00 01 00 00 02 58 00 09 00 14 04 61
6c 74 31 c0 2f c0 0c 00 0f 00 01 00 00 02 58 00
09 00 32 04 61 6c 74 34 c0 2f c0 0c 00 02 00 01
00 00 ad 8c 00 06 03 6e 73 31 c0 0c c0 0c 00 02
00 01 00 00 ad 8c 00 06 03 6e 73 34 c0 0c c0 0c
00 02 00 01 00 00 ad 8c 00 06 03 6e 73 33 c0 0c
c0 0c 00 02 00 01 00 00 ad 8c 00 06 03 6e 73 32
c0 0c c0 2f 00 01 00 01 00 00 00 e0 00 04 6c b1
77 1b c0 6c 00 01 00 01 00 00 00 0d 00 04 ac d9
c2 1b 04 41 4c 54 32 c0 2f 00 01 00 01 00 00 00
0d 00 04 6c b1 61 1b c0 81 00 01 00 01 00 00 00
e0 00 04 6c b1 08 1a c0 94 00 01 00 01 00 03 8b
b6 00 04 d8 ef 20 0a c0 ca 00 01 00 01 00 03 8f
54 00 04 d8 ef 22 0a c0 b8 00 01 00 01 00 03 8b
b6 00 04 d8 ef 24 0a c0 a6 00 01 00 01 00 03 97
5d 00 04 d8 ef 26 0a c0 2f 00 1c 00 01 00 00 00
f9 00 10 2a 00 14 50 40 13 0c 01 00 00 00 00 00
00 00 1b c0 6c 00 1c 00 01 00 00 00 f9 00 10 24
04 68 00 40 03 0c 04 00 00 00 00 00 00 00 1b 04
41 4c 54 33 c0 2f 00 1c 00 01 00 00 01 1b 00 10
26 07 f8 b0 40 0e 0c 00 00 00 00 00 00 00 00 1b
c0 94 00 1c 00 01 00 02 64 96 00 10 20 01 48 60
48 02 00 32 00 00 00 00 00 00 00 0a c0 ca 00 1c
00 01 00 04 54 3a 00 10 20 01 48 60 48 02 00 34
00 00 00 00 00 00 00 0a c0 b8 00 1c 00 01 00 01
79 ea 00 10 20 01 48 60 48 02 00 36 00 00 00 00
00 00 00 0a c0 a6 00 1c 00 01 00 01 d1 ba 00 10
20 01 48 60 48 02 00 38 00 00 00 00 00 00 00 0a" in
let mock_state = create `Tcp in
default_debug_info := [ipv4_buf];
match getaddrinfo mock_state Dns.Rr_map.Mx domain_name with
| Ok (_ttl, mx_set) ->
let make_mx_record (preference, domain_name) =
Dns.Mx.{
preference;
mail_exchange = Domain_name.host_exn (Domain_name.of_string_exn domain_name)
} in
(* assert this is Google MX *)
Alcotest.(check bool __LOC__ true (Dns.Rr_map.Mx_set.equal mx_set
(Dns.Rr_map.Mx_set.of_list
(List.map make_mx_record [
(10, "aspmx.l.google.com");
(20, "alt1.aspmx.l.google.com");
(30, "alt2.aspmx.l.google.com");
(40, "alt3.aspmx.l.google.com");
(50, "alt4.aspmx.l.google.com")
]))))
| Error _ -> failwith "foo.com should have been returned"
let fails_on_partial_udp_packet () =
let domain_name = Domain_name.(of_string_exn "google.com" |> host_exn) in
(* A partial google.com MX record
first two bytes identify this as a TCP packet - dropped here
bytes 3,4 are set to the query ID 00 00
*)
let udp_buf = Ohex.decode
" 00 00 81 80 00 01 00 05 00 04 00 0f 06 67
6f 6f 67 6c 65 03 63 6f " in
let mock_state = create `Udp in
default_debug_info := [udp_buf];
match getaddrinfo mock_state Dns.Rr_map.Mx domain_name with
| Error `Msg actual ->
let expected = "Truncated UDP response" in
Alcotest.(check string "reports the truncated UDP packet failure" expected actual)
| Ok (_, _) -> failwith "Should have reported the Truncated UDP packet"
let cname_and_nodata_packet () =
(* we request a non-existing record type of existing domain name, which is
an alias - the reply is a CNAME with NoData *)
(* concretely, requesting AAAA raw.githubusercontent.com, reply is
AN: raw.githubusercontent.com CNAME github.map.fastly.net
AU: SOA fastly.net *)
let domain_name =
Domain_name.(of_string_exn "raw.githubusercontent.com" |> host_exn)
in
let udp_buf = Ohex.decode {|
00 00 81 80 00 01 00 01 00 01 00 00 03 72
61 77 11 67 69 74 68 75 62 75 73 65 72 63 6f 6e
74 65 6e 74 03 63 6f 6d 00 00 1c 00 01 c0 0c 00
05 00 01 00 00 00 16 00 17 06 67 69 74 68 75 62
03 6d 61 70 06 66 61 73 74 6c 79 03 6e 65 74 00
c0 42 00 06 00 01 00 00 00 14 00 2e 03 6e 73 31
c0 42 0a 68 6f 73 74 6d 61 73 74 65 72 06 66 61
73 74 6c 79 c0 22 78 39 c6 29 00 00 0e 10 00 00
02 58 00 09 3a 80 00 00 00 1e|}
in
let mock_state = create `Udp in
default_debug_info := [udp_buf];
match getaddrinfo mock_state Dns.Rr_map.Aaaa domain_name with
| Error `Msg actual ->
let expected = "DNS cache error no data fastly.net" in
let len = String.length expected in
Alcotest.(check string __LOC__ expected (String.sub actual 0 len))
| Ok (_, _) -> Alcotest.fail "Should have returned nodata"
let tests = [
"supports_mx_packets", `Quick, supports_mx_packets;
"a partial UDP response packet fails", `Quick, fails_on_partial_udp_packet;
"cname and nodata in packet", `Quick, cname_and_nodata_packet;
]
end
let tests = [
"make_query tests", Make_query_tests.tests;
"parse_response tests", Parse_response_tests.tests;
"gethostbyname tests", Gethostbyname_tests.tests;
"getaddrinfo tests", Getaddrinfo_tests.tests;
]
let () = Alcotest.run "DNS client tests" tests

View file

@ -0,0 +1,53 @@
(test
(name tests)
(package dns)
(libraries dns alcotest)
(modules tests))
(test
(name server)
(package dns-server)
(libraries base64 dns-server dns-server.zone dns-tsig alcotest mirage-crypto-rng.unix)
(modules server))
(test
(name tsig)
(package dns-tsig)
(libraries dns-tsig alcotest)
(modules tsig))
(test
(name resolver)
(package dns-resolver)
(libraries dns-resolver alcotest logs.fmt)
(modules resolver))
(test
(name client)
(package dns-client)
(libraries dns-client alcotest)
(modules client))
(test
(name cache)
(package dns)
(libraries dns.cache alcotest)
(modules cache))
(test
(name resolvconf)
(package dns-client)
(libraries dns-client.resolvconf ipaddr alcotest)
(modules resolvconf))
(test
(name test_dnssec)
(package dnssec)
(libraries dns dnssec mirage-crypto-pk base64 logs.fmt alcotest)
(modules test_dnssec))
(test
(name test_rfc9460)
(package dns-server)
(libraries dns-server dns-server.zone logs.fmt alcotest)
(modules test_rfc9460))

View file

@ -0,0 +1,118 @@
let ok =
let module M = struct
type t = [ `Nameserver of Ipaddr.t ] list
let pp =
let pp_one ppf = function
| `Nameserver ip -> Fmt.pf ppf "nameserver %a" Ipaddr.pp ip
in
Fmt.(list ~sep:(any "\n") pp_one)
let equal a b = compare a b = 0 (* TODO polymorphic equality *)
end in
(module M: Alcotest.TESTABLE with type t = M.t)
let err =
let module M = struct
type t = [ `Msg of string ]
let pp ppf = function
| `Msg m -> Fmt.string ppf m
let equal _ _ = true
end in
(module M: Alcotest.TESTABLE with type t = M.t)
let test_one test_name (data, expected) () =
Alcotest.(check (result ok err)
("resolvconf " ^ test_name) expected (Dns_resolvconf.parse data))
let v4_ns = [ "8.8.8.8" ; "8.8.4.4" ]
and v6_ns = [ "2001:4860:4860::8888" ; "2001:4860:4860::8844" ]
let ok_result ns =
Ok (List.map (fun s -> `Nameserver (Ipaddr.of_string_exn s)) ns)
let linux =
{|
# Not all of these are supported by TRust-DNS
# They are testing that they don't break parsing
options ndots:8 timeout:8 attempts:8
domain example.com
search example.com sub.example.com
nameserver 2001:4860:4860::8888
nameserver 2001:4860:4860::8844
nameserver 8.8.8.8
nameserver 8.8.4.4
# some options not supported by TRust-DNS
options rotate
options inet6 no-tld-query
# A basic option not supported
sortlist 130.155.160.0/255.255.240.0 130.155.0.0
|}
let macos =
{|
#
# Mac OS X Notice
#
# This file is not used by the host name and address resolution
# or the DNS query routing mechanisms used by most processes on
# this Mac OS X system.
#
# This file is automatically generated.
#
options ndots:8 timeout:8 attempts:8
domain example.com.
search example.com. sub.example.com.
nameserver 2001:4860:4860::8888
nameserver 2001:4860:4860::8844
nameserver 8.8.8.8
nameserver 8.8.4.4
|}
let openbsd =
{|
# Generated by em0 dhclient
nameserver 8.8.8.8
nameserver 8.8.4.4
lookup file bind
|}
let simple =
{|
nameserver 8.8.8.8
nameserver 8.8.4.4
|}
let nixos =
{|
nameserver fe80::c2d7:aaff:fe96:8d82%wlp3s0
|}
let nixos2 =
{|
nameserver 8.8.8.8
nameserver 8.8.4.4
nameserver fe80::c2d7:aaff:fe96:8d82%wlp3s0
nameserver 8.8.8.8
nameserver 8.8.4.4
|}
let local_ns = [ "fe80::c2d7:aaff:fe96:8d82" ]
let tests = [
"linux", `Quick, test_one "linux" (linux, ok_result (v6_ns @ v4_ns)) ;
"macos", `Quick, test_one "macos" (macos, ok_result (v6_ns @ v4_ns)) ;
"openbsd", `Quick, test_one "openbsd" (openbsd, ok_result v4_ns) ;
"simple", `Quick, test_one "simple" (simple, ok_result v4_ns) ;
"nixos", `Quick, test_one "nixos (with zone index)"
(nixos, ok_result local_ns) ;
"nixos 2", `Quick, test_one "nixos 2 (with zone index)"
(nixos2, ok_result (v4_ns @ local_ns @ v4_ns)) ;
]
let () = Alcotest.run "DNS resolvconf tests" [ "resolvconf tests", tests ]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more