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>
104 lines
4.5 KiB
CMake
104 lines
4.5 KiB
CMake
# Third-party dependency resolution.
|
|
#
|
|
# System packages (asio, OpenSSL, lz4, fmt) are expected to be installed.
|
|
# openvpn3 and lwIP are source dependencies: point OVG_OPENVPN3_DIR / OVG_LWIP_DIR
|
|
# at an existing checkout, or let FetchContent pull them.
|
|
|
|
include(FetchContent)
|
|
find_package(PkgConfig REQUIRED)
|
|
find_package(Threads REQUIRED)
|
|
|
|
pkg_search_module(OPENSSL REQUIRED IMPORTED_TARGET openssl)
|
|
pkg_search_module(LZ4 REQUIRED IMPORTED_TARGET liblz4)
|
|
|
|
find_package(fmt REQUIRED)
|
|
|
|
find_path(ASIO_INCLUDE_DIR asio.hpp REQUIRED)
|
|
message(STATUS "asio headers: ${ASIO_INCLUDE_DIR}")
|
|
|
|
set(OVG_OPENVPN3_DIR "" CACHE PATH "Existing openvpn3 checkout (skips download)")
|
|
set(OVG_LWIP_DIR "" CACHE PATH "Existing lwIP checkout (skips download)")
|
|
|
|
if(OVG_WITH_TUNNEL)
|
|
# ---- openvpn3 core -------------------------------------------------------
|
|
if(OVG_OPENVPN3_DIR)
|
|
set(OPENVPN3_SOURCE_DIR ${OVG_OPENVPN3_DIR})
|
|
else()
|
|
FetchContent_Declare(openvpn3
|
|
GIT_REPOSITORY https://github.com/OpenVPN/openvpn3.git
|
|
GIT_TAG master
|
|
GIT_SHALLOW TRUE)
|
|
FetchContent_Populate(openvpn3) # populate only: we compile it into our own target
|
|
set(OPENVPN3_SOURCE_DIR ${openvpn3_SOURCE_DIR})
|
|
endif()
|
|
|
|
if(NOT EXISTS ${OPENVPN3_SOURCE_DIR}/client/ovpncli.cpp)
|
|
message(FATAL_ERROR "openvpn3 not found at ${OPENVPN3_SOURCE_DIR}")
|
|
endif()
|
|
message(STATUS "openvpn3 source: ${OPENVPN3_SOURCE_DIR}")
|
|
|
|
# The openvpn3 core is header-only apart from these two units. It cannot be
|
|
# built as a standalone library because callers must supply OPENVPN_LOG and
|
|
# friends at compile time, so we compile the sources into our own target.
|
|
add_library(ovg_openvpn3 STATIC
|
|
${OPENVPN3_SOURCE_DIR}/client/ovpncli.cpp
|
|
${OPENVPN3_SOURCE_DIR}/openvpn/crypto/data_epoch.cpp)
|
|
|
|
target_include_directories(ovg_openvpn3 PUBLIC ${OPENVPN3_SOURCE_DIR} ${ASIO_INCLUDE_DIR})
|
|
# -isystem for consumers. The core's headers are not clean under our warning
|
|
# set and never will be; suppressing per-warning here would mean chasing a
|
|
# new flag on every upstream bump, and blanket-disabling warnings on the file
|
|
# that includes them would blind us to problems in our own code in that same
|
|
# file. This draws the line exactly where it belongs.
|
|
set_target_properties(ovg_openvpn3 PROPERTIES
|
|
INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${OPENVPN3_SOURCE_DIR}")
|
|
target_compile_definitions(ovg_openvpn3 PUBLIC
|
|
ASIO_STANDALONE
|
|
USE_ASIO
|
|
HAVE_LZ4
|
|
USE_OPENSSL
|
|
USE_TUN_BUILDER) # <-- the whole design hinges on this: see docs/FEASIBILITY.md §2.2
|
|
target_link_libraries(ovg_openvpn3 PUBLIC
|
|
PkgConfig::OPENSSL PkgConfig::LZ4 fmt::fmt Threads::Threads)
|
|
target_compile_options(ovg_openvpn3 PRIVATE -Wno-deprecated-declarations)
|
|
|
|
# ---- lwIP ----------------------------------------------------------------
|
|
if(OVG_LWIP_DIR)
|
|
set(LWIP_SOURCE_DIR ${OVG_LWIP_DIR})
|
|
else()
|
|
FetchContent_Declare(lwip
|
|
GIT_REPOSITORY https://github.com/lwip-tcpip/lwip.git
|
|
GIT_TAG STABLE-2_2_1_RELEASE
|
|
GIT_SHALLOW TRUE)
|
|
FetchContent_Populate(lwip)
|
|
set(LWIP_SOURCE_DIR ${lwip_SOURCE_DIR})
|
|
endif()
|
|
|
|
if(NOT EXISTS ${LWIP_SOURCE_DIR}/src/Filelists.cmake)
|
|
message(FATAL_ERROR "lwIP not found at ${LWIP_SOURCE_DIR}")
|
|
endif()
|
|
message(STATUS "lwIP source: ${LWIP_SOURCE_DIR}")
|
|
|
|
set(LWIP_DIR ${LWIP_SOURCE_DIR})
|
|
# Our lwipopts.h and the arch/ shim live in src/netstack/lwip_port.
|
|
set(LWIP_INCLUDE_DIRS
|
|
${LWIP_SOURCE_DIR}/src/include
|
|
${CMAKE_CURRENT_SOURCE_DIR}/src/netstack/lwip_port)
|
|
include(${LWIP_SOURCE_DIR}/src/Filelists.cmake)
|
|
|
|
# Core + IPv4 only. Deliberately NOT lwipapi_SRCS: api_lib/api_msg/netbuf/
|
|
# sockets are the sequential and BSD-socket APIs, which require NO_SYS=0 and
|
|
# a real threading layer. We drive the raw callback API from a strand instead
|
|
# (see src/netstack/lwip_stack.h), so those files cannot compile and would not
|
|
# be used if they did.
|
|
add_library(ovg_lwip STATIC ${lwipcore_SRCS} ${lwipcore4_SRCS})
|
|
target_include_directories(ovg_lwip PUBLIC ${LWIP_INCLUDE_DIRS})
|
|
# -isystem for consumers: lwIP's headers are not clean under our warning set.
|
|
set_target_properties(ovg_lwip PROPERTIES
|
|
INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${LWIP_SOURCE_DIR}/src/include")
|
|
# No LWIP_DEBUG define here, on purpose. lwIP tests it with #ifdef, not #if,
|
|
# so defining it to 0 *enables* the debug machinery -- the opposite of what it
|
|
# reads like. lwip_port/lwipopts.h says the same thing where it #undefs it.
|
|
target_compile_options(ovg_lwip PRIVATE -Wno-unused-parameter -Wno-address)
|
|
endif()
|