// The way out. Everything above this line -- SOCKS5, health, admin -- programs // against this interface and nothing else. // // --------------------------------------------------------------------------- // Why the whole design hangs on this being a shared_ptr // --------------------------------------------------------------------------- // A SOCKS5 session takes a reference at accept time and holds it until it ends. // That single rule buys three things that would otherwise each need their own // machinery: // // * A session can never be re-homed underneath itself. The egress it started // on is the egress it finishes on, so there is no window where a half-sent // request goes out one tunnel and its continuation out another. // * Draining needs no session table. The reference count *is* the number of // sessions still using this egress; when it hits zero the destructor closes // the tunnel. Nothing to iterate, nothing to leak. // * The switch is a pointer swap. Promote = store a new shared_ptr in the // manager; every acquire() after that gets the new one, every session // before it is untouched. // // docs/ARCHITECTURE.md ยง5.2. // // --------------------------------------------------------------------------- // Threading // --------------------------------------------------------------------------- // Every method is safe to call from any thread. Each async method takes the // executor its handler must run on, and so does everything the handler hands // back: a stream created with a session's strand delivers all of its own // completions on that strand too. // // Passing the executor explicitly rather than fixing it at construction is what // lets one egress serve a thousand sessions on four io threads while each // session still sees a single-threaded world. Implementations that need // internal serialization -- the tunnel one does, because lwIP does -- arrange // that privately and never leak it to the caller. #pragma once #include #include #include #include #include #include #include "common/config.h" #include "common/endpoint.h" #include "netstack/stream.h" namespace ovg::egress { using netstack::TcpStreamPtr; using netstack::UdpSocketPtr; enum class EgressState { Idle, // constructed, start() not called Connecting, // bringing the tunnel up; no traffic yet Ready, // usable Draining, // still serving existing sessions, refusing new ones Down, // unusable; `detail()` says why }; const char *egress_state_name(EgressState s); struct EgressStats { // Identity. std::string node_id; std::string node_country; std::string server_ip; std::string local_address; // our address inside the tunnel std::string proto; // "udp" / "tcp" / "direct" // Age and use. int64_t uptime_ms = 0; int64_t sessions = 0; // shared_ptr use_count minus the manager's own refs // Traffic, as seen at the tunnel. Zero for the direct egress, which has no // single aggregate to report. uint64_t tun_bytes_in = 0; uint64_t tun_bytes_out = 0; uint64_t transport_bytes_in = 0; uint64_t transport_bytes_out = 0; int last_packet_received_ms = -1; // -1 = never // Connection outcomes, since construction. uint64_t tcp_opened = 0; uint64_t tcp_failed = 0; int64_t tcp_active = 0; int64_t udp_active = 0; // Packet-level health, from the netstack. All zero for direct. uint64_t rx_packets = 0; uint64_t tx_packets = 0; uint64_t tx_dropped = 0; uint64_t rx_malformed = 0; uint64_t rx_dropped = 0; double connect_failure_rate() const { const uint64_t total = tcp_opened + tcp_failed; return total == 0 ? 0.0 : static_cast(tcp_failed) / total; } }; class Egress { public: using ConnectHandler = std::function; using UdpBindHandler = std::function; using ResolveHandler = std::function)>; virtual ~Egress() = default; // `target` may be a domain: resolving it here rather than at the caller is // what keeps DNS inside the tunnel. A literal address skips the lookup. virtual void async_connect_tcp(const asio::any_io_executor &ex, const Endpoint &target, Millis timeout, ConnectHandler h) = 0; // An outbound datagram socket on this egress, for SOCKS5 UDP ASSOCIATE. virtual void async_bind_udp(const asio::any_io_executor &ex, UdpBindHandler h) = 0; // Exposed separately from connect because SOCKS5 needs the resolved address // for the BND field in the reply, and the health monitor times a lookup as // its in-tunnel RTT probe. virtual void async_resolve(const asio::any_io_executor &ex, const std::string &host, ResolveHandler h) = 0; virtual EgressState state() const = 0; virtual EgressStats stats() const = 0; // Human-readable reason for Down, or an empty string. virtual std::string detail() const = 0; // Stops accepting new work. Existing streams keep running: this is the state // an egress sits in between being replaced and its last session ending. // Idempotent. virtual void begin_drain() = 0; // Tears everything down now, aborting live streams with Error::EgressGone. // `on_done` runs on the egress's executor once the tunnel is gone. virtual void shutdown(std::function on_done = {}) = 0; // Stable label for logs: node id for a tunnel, "direct" otherwise. virtual const std::string &label() const = 0; bool usable() const { return state() == EgressState::Ready; } }; using EgressPtr = std::shared_ptr; } // namespace ovg::egress