// A DNS resolver that queries through a specific tunnel. // // --------------------------------------------------------------------------- // Why not lwIP's resolver // --------------------------------------------------------------------------- // lwIP ships one (LWIP_DNS), and it is disabled in lwip_port/lwipopts.h. It // keeps a single global server list, a single global cache and a fixed table of // in-flight queries -- all file-scope state, exactly like the rest of lwIP. That // is fine with one interface and wrong with two: during a make-before-break // switch both tunnels are up, each pushed its own resolvers, and a name looked // up for a session on the old tunnel must be answered by the old tunnel's // servers over the old tunnel's socket. A per-netif resolver is the only way to // keep that straight, so this is roughly 400 lines we own instead of a global // we would have to serialize access to and still get wrong. // // It also buys three things lwIP's does not offer: a TTL-clamped cache sized // from config, coalescing of concurrent lookups for the same name, and failover // across the pushed servers plus the configured fallbacks. // // --------------------------------------------------------------------------- // Scope // --------------------------------------------------------------------------- // A records only. The stack is IPv4-only (see lwip_stack.h), so an AAAA answer // could not be connected to even if we asked for it. No EDNS0, no DNSSEC, no // TCP fallback: a truncated A-record answer that still carries one address is // usable, and one that carries none fails over to the next server. // // --------------------------------------------------------------------------- // Threading // --------------------------------------------------------------------------- // Its own strand, not the lwIP one. Parsing responses and walking the cache has // no business running where every TCP segment in the process is also processed. // Public methods are safe to call from any thread; handlers run on the strand. #pragma once #include #include #include #include #include #include #include #include #include #include #include "common/config.h" #include "netstack/lwip_stack.h" #include "netstack/stream.h" namespace ovg::netstack { class DnsResolver final : public Resolver, public std::enable_shared_from_this { public: // The server list is the netif's pushed resolvers followed by // cfg.fallback_servers. Both are queried *through the tunnel*: a fallback is // a different address, not a different path. static std::shared_ptr create(std::shared_ptr netif, DnsConfig cfg); ~DnsResolver() override; DnsResolver(const DnsResolver &) = delete; DnsResolver &operator=(const DnsResolver &) = delete; // Resolver void async_resolve(const std::string &host, Handler h) override; void clear_cache() override; struct Stats { uint64_t queries = 0; uint64_t cache_hits = 0; uint64_t coalesced = 0; uint64_t timeouts = 0; uint64_t failures = 0; size_t cached = 0; size_t in_flight = 0; }; Stats stats() const; private: DnsResolver(std::shared_ptr netif, DnsConfig cfg); using Clock = std::chrono::steady_clock; struct Query { explicit Query(const Strand &s) : timer(s) {} std::string name; // lowercased uint16_t id = 0; std::vector waiters; size_t server_idx = 0; asio::steady_timer timer; Clock::time_point deadline; std::vector wire; // kept so a retry does not rebuild it int last_rcode = -1; // for the error message when all fail bool done = false; }; struct CacheEntry { std::vector addrs; Clock::time_point expires; }; // Strand-only. void start(const std::string &name, Handler h); void open_socket(); void arm_receive(); void on_datagram(const std::error_code &ec, size_t n, const Endpoint &from); void send_query(const std::shared_ptr &q); void on_query_timeout(const std::shared_ptr &q); void finish(const std::shared_ptr &q, const std::error_code &ec, std::vector addrs, uint32_t ttl_seconds); void fail_all(const std::error_code &ec); uint16_t allocate_id(); void cache_put(const std::string &name, const std::vector &addrs, uint32_t ttl_seconds); bool cache_get(const std::string &name, std::vector *out); std::shared_ptr netif_; DnsConfig cfg_; Strand strand_; std::vector servers_; UdpSocketPtr sock_; bool opening_ = false; std::vector> pending_open_; std::vector rx_buf_; bool receiving_ = false; std::unordered_map> by_id_; std::unordered_map> by_name_; std::unordered_map cache_; std::deque cache_order_; // insertion order, for eviction std::mt19937 rng_; mutable std::mutex stats_mu_; Stats stats_; }; // Exposed for tests: build an A-record query and parse a response. Pure // functions over byte buffers, which is the only part of DNS worth unit-testing // in isolation. namespace dns { // Returns false if `name` is not a legal DNS name (label > 63, total > 255). bool build_query(const std::string &name, uint16_t id, std::vector *out); struct ParseResult { uint16_t id = 0; int rcode = 0; bool truncated = false; std::vector addrs; uint32_t min_ttl = 0; }; // Returns false only for a response that is malformed at the wire level. An // answer that is well-formed but empty or an error comes back true with rcode // and addrs telling the caller what happened. bool parse_response(const uint8_t *data, size_t len, ParseResult *out); } // namespace dns } // namespace ovg::netstack