forked from cloud/ovgate
OpenVPN client with an authenticated SOCKS5 front door
A userspace VPN gateway: builds an OpenVPN tunnel to a VPNGate node with
the OpenVPN 3 core, terminates it in-process with lwIP, and serves SOCKS5
(RFC 1928/1929, CONNECT and UDP ASSOCIATE) over it. No root, no tun
device, no routing table changes.
Layout follows the module boundaries in docs/ARCHITECTURE.md:
vpngate/ directory fetch + CSV parse (lines run to ~13.5 KB, so the
parser streams rather than splitting on newlines)
selector/ two-phase pick: cheap prior over the whole list, then real
TCP handshake timing of the top K
ovpn/ openvpn3 driven through TunBuilder, packets over a socketpair
netstack/ lwIP: the TCP/IP stack that makes "no root" possible
egress/ the swappable way out, and make-before-break switching
socks5/ the front door
health/ per-window scoring, and the decision to move
app/ wiring, admin HTTP, signals
docs/FEASIBILITY.md is the analysis this was built from, including the
one requirement that is not physically possible -- carrying established
TCP connections across a node switch -- and what is done instead
(zero-progress redial, UDP re-homing, grace-period drain).
Tests: 155 without the tunnel egress, 172 with it. The seam is the egress
factory; selection, scoring, history and probing all run for real.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// 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 <asio.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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<DnsResolver> {
|
||||
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<DnsResolver> create(std::shared_ptr<Netif> 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> 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<Handler> waiters;
|
||||
size_t server_idx = 0;
|
||||
asio::steady_timer timer;
|
||||
Clock::time_point deadline;
|
||||
std::vector<uint8_t> 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<IpAddress> 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<Query> &q);
|
||||
void on_query_timeout(const std::shared_ptr<Query> &q);
|
||||
void finish(const std::shared_ptr<Query> &q, const std::error_code &ec,
|
||||
std::vector<IpAddress> 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<IpAddress> &addrs,
|
||||
uint32_t ttl_seconds);
|
||||
bool cache_get(const std::string &name, std::vector<IpAddress> *out);
|
||||
|
||||
std::shared_ptr<Netif> netif_;
|
||||
DnsConfig cfg_;
|
||||
Strand strand_;
|
||||
|
||||
std::vector<IpAddress> servers_;
|
||||
UdpSocketPtr sock_;
|
||||
bool opening_ = false;
|
||||
std::vector<std::shared_ptr<Query>> pending_open_;
|
||||
std::vector<uint8_t> rx_buf_;
|
||||
bool receiving_ = false;
|
||||
|
||||
std::unordered_map<uint16_t, std::shared_ptr<Query>> by_id_;
|
||||
std::unordered_map<std::string, std::shared_ptr<Query>> by_name_;
|
||||
|
||||
std::unordered_map<std::string, CacheEntry> cache_;
|
||||
std::deque<std::string> 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<uint8_t> *out);
|
||||
|
||||
struct ParseResult {
|
||||
uint16_t id = 0;
|
||||
int rcode = 0;
|
||||
bool truncated = false;
|
||||
std::vector<IpAddress> 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
|
||||
Reference in New Issue
Block a user