mirror of
https://gitlab.com/shadow_contributor/ntptun.git
synced 2026-10-03 03:46:50 +00:00
udp mode
This commit is contained in:
parent
f00f2cd3ce
commit
3fa4d8a8a7
22 changed files with 938 additions and 221 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
# Build output
|
||||
/build/
|
||||
/build-*/
|
||||
/out/
|
||||
*.o
|
||||
*.obj
|
||||
|
|
@ -8,6 +9,9 @@
|
|||
# Local logs / scratch
|
||||
*.log
|
||||
|
||||
# Design spec — kept locally, not shipped
|
||||
tunneling-over-ntp.md
|
||||
|
||||
# Editor
|
||||
.cache/
|
||||
compile_commands.json
|
||||
|
|
|
|||
|
|
@ -10,28 +10,48 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
|||
set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Build type" FORCE)
|
||||
endif()
|
||||
|
||||
# The app uses POSIX / Linux system APIs (sockets, TUN, clock_gettime,
|
||||
# sigaction). Expose them explicitly while keeping a strict language standard.
|
||||
add_compile_definitions(_GNU_SOURCE)
|
||||
# The app uses POSIX / Linux system APIs (sockets, TUN, sigaction) on Linux and
|
||||
# Winsock on Windows. Expose the POSIX feature set explicitly on non-MSVC
|
||||
# toolchains while keeping a strict language standard.
|
||||
if(NOT MSVC)
|
||||
add_compile_definitions(_GNU_SOURCE)
|
||||
endif()
|
||||
|
||||
# ---- Warnings --------------------------------------------------------------
|
||||
add_library(ntptun_warnings INTERFACE)
|
||||
target_compile_options(ntptun_warnings INTERFACE
|
||||
-Wall -Wextra -Wpedantic -Wshadow -Wnon-virtual-dtor -Wcast-align
|
||||
-Wunused -Wnull-dereference -Wdouble-promotion -Wformat=2)
|
||||
if(MSVC)
|
||||
target_compile_options(ntptun_warnings INTERFACE /W4 /permissive-)
|
||||
# Winsock deprecation/secure-CRT noise is not relevant here.
|
||||
target_compile_definitions(ntptun_warnings INTERFACE
|
||||
_CRT_SECURE_NO_WARNINGS _WINSOCK_DEPRECATED_NO_WARNINGS)
|
||||
else()
|
||||
target_compile_options(ntptun_warnings INTERFACE
|
||||
-Wall -Wextra -Wpedantic -Wshadow -Wnon-virtual-dtor -Wcast-align
|
||||
-Wunused -Wnull-dereference -Wdouble-promotion -Wformat=2)
|
||||
endif()
|
||||
|
||||
# ---- Core library ----------------------------------------------------------
|
||||
add_library(ntptun_core STATIC
|
||||
set(ntptun_core_sources
|
||||
src/Ntp.cpp
|
||||
src/Carrier.cpp
|
||||
src/Endpoint.cpp
|
||||
src/Platform.cpp
|
||||
src/UdpSocket.cpp
|
||||
src/TunDevice.cpp
|
||||
src/Config.cpp
|
||||
src/Client.cpp
|
||||
src/Server.cpp)
|
||||
|
||||
# The TUN transport is Linux-only; compile it only there.
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
list(APPEND ntptun_core_sources src/TunDevice.cpp)
|
||||
endif()
|
||||
|
||||
add_library(ntptun_core STATIC ${ntptun_core_sources})
|
||||
target_include_directories(ntptun_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
target_link_libraries(ntptun_core PRIVATE ntptun_warnings)
|
||||
if(WIN32)
|
||||
target_link_libraries(ntptun_core PUBLIC ws2_32)
|
||||
endif()
|
||||
|
||||
# ---- Executable ------------------------------------------------------------
|
||||
add_executable(ntptun src/main.cpp)
|
||||
|
|
|
|||
181
README.md
181
README.md
|
|
@ -1,7 +1,8 @@
|
|||
# ntptun — IP datagrams over NTP
|
||||
# ntptun — IP over NTP
|
||||
(and UDP too)
|
||||
|
||||
A small, dependency-free C++17 implementation of the IP-over-NTP tunnel: it carries one complete inner
|
||||
IPv4/IPv6 datagram inside one NTPv4 packet, using an NTP extension field with a
|
||||
A small, dependency-free C++17 implementation of the IP-over-NTP tunnel and UDP-over-NTP: it carries one complete inner
|
||||
IPv4/IPv6 or UDP datagram inside one NTPv4 packet, using an NTP extension field with a
|
||||
lightweight SHA‑256‑based XOR obfuscation layer for traffic classification and
|
||||
admission filtering.
|
||||
|
||||
|
|
@ -10,14 +11,78 @@ admission filtering.
|
|||
> no authentication**. If you need those properties, run WireGuard or IPsec
|
||||
> *inside* the tunnel.
|
||||
|
||||
- **Target:** Linux (uses `/dev/net/tun`, `poll`, BSD sockets).
|
||||
- **Targets:** Linux (full features) and Windows/MSVC (UDP transport only — see
|
||||
below). The TUN transport uses `/dev/net/tun` and is Linux-only; sockets and
|
||||
the event loop are portable over Winsock and BSD sockets.
|
||||
- **Dependencies:** none. Only the C++17 standard library. SHA‑256 is a
|
||||
self-contained header ([include/ntptun/Sha256.hpp](include/ntptun/Sha256.hpp),
|
||||
FIPS 180‑4).
|
||||
- **Interface model:** the app attaches to an **existing** TUN interface that
|
||||
you create and configure out of band (see below).
|
||||
you create and configure out of band (see below), or — in UDP transport — it
|
||||
needs no interface at all.
|
||||
|
||||
## What it does
|
||||
## Transports
|
||||
|
||||
`ntptun` carries one of two inner payload types inside the NTP carrier, selected
|
||||
by `transport =` in the config:
|
||||
|
||||
- **`tun`** (default, Linux only): full inner IPv4/IPv6 datagrams read from and
|
||||
written to a TUN interface. This is the original IP-over-NTP mode.
|
||||
- **`udp`** (portable, incl. Windows/MSVC): raw **UDP port tunneling**. The
|
||||
client listens on a local UDP port (`udp_listen`, e.g. `127.0.0.1:9000`) and
|
||||
tunnels whatever it receives; point WireGuard or any UDP client at that
|
||||
address. The server releases each client's payloads to a single real target
|
||||
(`udp_target`) using a **distinct source port per user**: `udp_base_port +
|
||||
client_id`, so multiple clients are demultiplexed by their outgoing port.
|
||||
Downstream replies from the target are returned to the originating client.
|
||||
|
||||
Because the source port is `udp_base_port + client_id`, the sum must stay
|
||||
within the 16-bit port space: **`udp_base_port + max(client_id) <= 65535`**.
|
||||
The server logs the usable `client_id` range at startup and rejects (with an
|
||||
error) any request whose computed relay port would exceed 65535.
|
||||
|
||||
A UDP payload that will not fit one NTP carrier (larger than `inner_mtu`) is
|
||||
**discarded with an error logged** — keep the inner app's MTU low enough.
|
||||
|
||||
See [examples/client_udp.conf](examples/client_udp.conf) and
|
||||
[examples/server_udp.conf](examples/server_udp.conf).
|
||||
|
||||
### Sizing the inner protocol's MTU
|
||||
|
||||
Every datagram the inner app sends must fit one NTP carrier: its UDP payload
|
||||
must be ≤ `inner_mtu`, or ntptun drops it (with an error logged). The carrier's
|
||||
own overhead is a fixed **64 bytes** (`48` NTP header + `4` ext type/len + `12`
|
||||
carrier header), so the outer packet is `inner_mtu + 64`; keep
|
||||
`ntp_payload_cap ≥ inner_mtu + 64`. Configure each protocol so its packets stay
|
||||
under `inner_mtu`:
|
||||
|
||||
| Protocol | Hard min packet? | Path MTU discovery | What you configure |
|
||||
|---|---|---|---|
|
||||
| WireGuard | no | no | interface `MTU = inner_mtu − 32` (e.g. `1104` at default) |
|
||||
| KCP / kcptun | no | no | KCP `mtu ≤ inner_mtu` (e.g. `--mtu 1136`; header is *inside* the MTU) |
|
||||
| DTLS | no (fragments handshake) | app-driven, no auto PMTUD | DTLS link MTU ≤ `inner_mtu` |
|
||||
|
||||
Notes: WireGuard's 32-byte overhead is added *on top* of the inner packet, so
|
||||
you subtract it; KCP and DTLS account for their own header inside the MTU, so you
|
||||
don't.
|
||||
|
||||
> **Recommended: pair ntptun with [GOST](https://github.com/go-gost/gost).**
|
||||
> GOST speaks KCP and DTLS natively over UDP, so you get a proven proxy/tunnel
|
||||
> (HTTP, SOCKS5, relay, port-forwarding, TLS) *inside* the NTP carrier with one
|
||||
> tool on both ends. Point GOST's KCP/DTLS dialer at the ntptun client's
|
||||
> `udp_listen` and its listener at the ntptun server's `udp_target`:
|
||||
>
|
||||
> ```bash
|
||||
> # server: gost KCP/DTLS listener that ntptun releases traffic to
|
||||
> gost -L 'http+kcp://:8443?kcp.mtu=1250' # (or http+dtls://:8443)
|
||||
> # client: gost dials the ntptun client's local UDP port
|
||||
> gost -L http://127.0.0.1:18080 -F 'http+kcp://127.0.0.1:9000?kcp.mtu=1250'
|
||||
> ```
|
||||
>
|
||||
> KCP and DTLS have both been tested end-to-end through the tunnel. DTLS runs at
|
||||
> the default `inner_mtu = 1136`; KCP just needs `kcp.mtu ≤ inner_mtu`.
|
||||
|
||||
## What it does (example in TUN mode)
|
||||
|
||||
```
|
||||
client host server host
|
||||
|
|
@ -40,21 +105,10 @@ admission filtering.
|
|||
- **source‑learns** routes (inner source IP → Client ID) so it knows which
|
||||
client a downstream datagram belongs to — no extra routing config needed;
|
||||
- queues downstream datagrams per client and returns exactly one per request;
|
||||
- relays ordinary (non‑tunnel) NTP requests to a real upstream server so a
|
||||
plain NTP probe still gets a genuine answer, with amplification
|
||||
and rate‑limit guards.
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
include/ntptun/ public headers (Bytes, Sha256, Ntp, Carrier, KeyStore,
|
||||
IpPacket, Endpoint, UdpSocket, TunDevice, Config, Client,
|
||||
Server, Logging)
|
||||
src/ implementation (.cpp) + main.cpp
|
||||
tests/ dependency-free unit tests (SHA-256 vectors, carrier
|
||||
round-trip/validation, NTP header/extension parsing)
|
||||
examples/ sample server.conf / client.conf
|
||||
```
|
||||
- relays anything that isn't one of its own valid carriers — ordinary
|
||||
non‑tunnel NTP *and* packets that fail the carrier's authentication check —
|
||||
to a real upstream server, so an active probe still gets a genuine answer
|
||||
(with amplification and rate‑limit guards).
|
||||
|
||||
## Build (GCC or Clang)
|
||||
|
||||
|
|
@ -89,7 +143,14 @@ ctest --test-dir build --output-on-failure
|
|||
|
||||
The binary is `build/ntptun`.
|
||||
|
||||
## Create and configure a TUN interface
|
||||
### Windows (Visual Studio, UDP transport)
|
||||
|
||||
On Windows, install the **Desktop development with C++** workload, then open the
|
||||
repo folder in Visual Studio (**File ▸ Open ▸ Folder**) — it auto-detects the
|
||||
CMake project and builds `ntptun.exe` with MSVC. Only the `udp` transport is
|
||||
available (the TUN transport is Linux-only).
|
||||
|
||||
## Create and configure a TUN interface (for TUN mode)
|
||||
|
||||
`ntptun` does **not** create the interface; it attaches to an existing one by
|
||||
name (`IFF_TUN | IFF_NO_PI`). Create a persistent TUN owned by your user so the
|
||||
|
|
@ -149,38 +210,23 @@ the tunnel:
|
|||
ping 10.9.0.1
|
||||
```
|
||||
|
||||
### Quick single-host smoke test with network namespaces
|
||||
|
||||
You can exercise both ends on one machine using two network namespaces, each
|
||||
with its own `tun0`, connected by a veth pair. Outline:
|
||||
|
||||
```bash
|
||||
sudo ip netns add srv
|
||||
sudo ip netns add cli
|
||||
sudo ip link add veth-s type veth peer name veth-c
|
||||
sudo ip link set veth-s netns srv
|
||||
sudo ip link set veth-c netns cli
|
||||
sudo ip -n srv addr add 192.0.2.1/24 dev veth-s
|
||||
sudo ip -n cli addr add 192.0.2.2/24 dev veth-c
|
||||
sudo ip -n srv link set veth-s up
|
||||
sudo ip -n cli link set veth-c up
|
||||
|
||||
# In each namespace: create tun0, assign 10.9.0.1/.2, set mtu 1136, run ntptun
|
||||
# (server listen on 0.0.0.0:12300; client server=192.0.2.1:12300).
|
||||
```
|
||||
|
||||
## Configuration reference
|
||||
|
||||
Line-based `key = value`; `#` starts a comment. Keys are 32 bytes, given as 64
|
||||
hex characters.
|
||||
hex characters. `mode` selects the endpoint role, while `transport` selects
|
||||
what is carried through the tunnel. In the default `tun` transport, `tun` is
|
||||
required on both endpoints. In the `udp` transport, no TUN interface is used;
|
||||
the client instead requires `udp_listen`, and the server requires `udp_target`
|
||||
and `udp_base_port`.
|
||||
|
||||
| Key | Mode | Default | Meaning |
|
||||
|-----|------|---------|---------|
|
||||
| `mode` | both | — | `client` or `server` (required) |
|
||||
| `tun` | both | — | existing TUN interface name (required) |
|
||||
| Key | Applies to | Default | Meaning |
|
||||
|-----|------------|---------|---------|
|
||||
| `mode` | both | — | endpoint role: `client` or `server` (required) |
|
||||
| `transport` | both | `tun` | inner transport: `tun` (Linux only) or `udp`; must match on both ends |
|
||||
| `tun` | TUN, both | — | existing TUN interface name (required when `transport = tun`) |
|
||||
| `ext_type` | both | `0x0F4E` | NTP extension type used for the tunnel |
|
||||
| `inner_mtu` | both | `1136` | max inner IP datagram size |
|
||||
| `ntp_payload_cap` | both | `1200` | max NTP UDP payload |
|
||||
| `inner_mtu` | both | `1136` | max inner IP datagram (`tun`) or UDP payload (`udp`) size |
|
||||
| `ntp_payload_cap` | both | `1200` | max NTP UDP payload; must accommodate `inner_mtu + 64` bytes of framing |
|
||||
| `log_level` | both | `info` | `error`/`warn`/`info`/`debug` |
|
||||
| `default_key` | both | — | shared 32‑byte key (hex) |
|
||||
| `client_key` | both | — | `id:hex` per‑client override (repeatable) |
|
||||
|
|
@ -190,11 +236,14 @@ hex characters.
|
|||
| `poll_interval_ms` | client | `250` | poll mode: idle empty‑poll interval / loop wakeup |
|
||||
| `poll_window` | client | `1` | poll mode: empty polls kept outstanding (downstream throughput lever) |
|
||||
| `keepalive_ms` | client | `1000` | push mode: keepalive interval to keep the server's address/NAT fresh |
|
||||
| `listen` | server | — | `host:port` to bind (required) |
|
||||
| `upstream` | server | — | real NTP server for the relay path (optional) |
|
||||
| `relay_timeout_ms` | server | `1000` | how long a relayed request awaits a reply |
|
||||
| `relay_rate_per_sec` | server | `10` | per‑source relay rate limit |
|
||||
| `udp_listen` | UDP client | — | local `host:port` that receives inner UDP datagrams (required for `udp`) |
|
||||
| `listen` | server | — | outer NTP `host:port` to bind (required) |
|
||||
| `upstream` | server | — | real NTP server for the non-tunnel relay path (optional) |
|
||||
| `relay_timeout_ms` | server | `1000` | how long a relayed ordinary NTP request awaits a reply |
|
||||
| `relay_rate_per_sec` | server | `10` | per-source ordinary NTP relay rate limit |
|
||||
| `max_queue_per_client` | server | `64` | max queued downstream datagrams per client |
|
||||
| `udp_target` | UDP server | — | real `host:port` that receives decapsulated UDP payloads (required for `udp`) |
|
||||
| `udp_base_port` | UDP server | — | base relay source port (required for `udp`); client `I` uses `udp_base_port + I` |
|
||||
|
||||
At least one of `default_key` / `client_key` must be set. IPv6 literals in
|
||||
`host:port` must be bracketed, e.g. `[2001:db8::1]:123`.
|
||||
|
|
@ -253,6 +302,32 @@ send unsolicited mode‑4 packets, so a stateful observer can distinguish it.
|
|||
cannot hijack that association.
|
||||
- **Hybrid:** a `default_key` plus `client_key` overrides for selected IDs.
|
||||
|
||||
### Non‑tunnel relay (anti‑active‑probing)
|
||||
|
||||
An adversary who suspects your `listen` address hides a tunnel can *actively
|
||||
probe* it with an ordinary NTP mode‑3 request: a real time server answers, a
|
||||
bespoke tunnel stays silent — and that difference is the fingerprint. Set
|
||||
`upstream` to a real NTP server and the box forwards **anything that isn't one
|
||||
of its own valid carriers** upstream, relaying the genuine reply back. That
|
||||
covers both non‑tunnel packets and **failed‑authentication carriers** (wrong
|
||||
key, tampered, etc.), so a probe can't tell it apart from a stock time source.
|
||||
The path is conservative: it only fires for plausible NTPv3/v4 requests, is
|
||||
rate‑limited per source (`relay_rate_per_sec`), expires after
|
||||
`relay_timeout_ms`, and caps replies to the request size so it can't be used as
|
||||
a reflection amplifier.
|
||||
|
||||
**Recommendations**
|
||||
|
||||
- **Turn it on for any internet‑facing server.** With `upstream` unset, non‑tunnel
|
||||
and failed‑auth requests are *silently dropped* — the exact "speaks nothing
|
||||
back" tell a prober wants. Off only makes sense on a private/allow‑listed path.
|
||||
- **Pick a plausible, reachable upstream** — a public pool server or the box's own
|
||||
time source, believable for where your server appears to live; tune
|
||||
`relay_rate_per_sec`/`relay_timeout_ms` to the probe volume and upstream RTT.
|
||||
- **Know the residual tell.** Relayed answers are genuine (real stratum/refid),
|
||||
tunnel answers are synthesized (stratum 2). This defeats naive probing, not an
|
||||
analyst comparing both — for that, layer WireGuard/IPsec inside the tunnel.
|
||||
|
||||
## Live integration test (RU → US)
|
||||
|
||||
End-to-end test carrying real internet traffic over the tunnel: a client in
|
||||
|
|
|
|||
34
examples/client_udp.conf
Normal file
34
examples/client_udp.conf
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Example ntptun CLIENT configuration — UDP port-tunneling transport.
|
||||
#
|
||||
# In this mode the client does NOT use a TUN interface. It listens on a local
|
||||
# UDP port and tunnels whatever it receives (e.g. a WireGuard client pointed at
|
||||
# 127.0.0.1:9000). This transport builds and runs on Windows/MSVC too.
|
||||
|
||||
mode = client
|
||||
transport = udp
|
||||
client_id = 42
|
||||
server = 203.0.113.10:12300
|
||||
|
||||
# Local UDP address the app connects to. Point WireGuard (or any UDP client)
|
||||
# here; the client remembers the last local peer and returns downstream
|
||||
# datagrams to it.
|
||||
udp_listen = 127.0.0.1:9000
|
||||
|
||||
ext_type = 0x0F4E
|
||||
# inner_mtu is the largest UDP payload that fits one NTP carrier. Payloads
|
||||
# larger than this are discarded with an error in the log.
|
||||
#
|
||||
# WireGuard adds a fixed 32-byte overhead to each encapsulated IP packet, so set
|
||||
# WireGuard's interface MTU to at most inner_mtu - 32. With inner_mtu = 1136
|
||||
# that is MTU = 1104 (put `MTU = 1104` under [Interface] in the WireGuard conf).
|
||||
# The client also logs this recommended maximum at startup.
|
||||
inner_mtu = 1136
|
||||
ntp_payload_cap = 1200
|
||||
|
||||
# push = false
|
||||
poll_interval_ms = 250
|
||||
poll_window = 8
|
||||
keepalive_ms = 1000
|
||||
log_level = info
|
||||
|
||||
default_key = 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f
|
||||
35
examples/server_udp.conf
Normal file
35
examples/server_udp.conf
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Example ntptun SERVER configuration — UDP port-tunneling transport.
|
||||
#
|
||||
# In this mode the server does NOT use a TUN interface. It forwards each
|
||||
# client's decapsulated UDP payloads to a single real target, using a distinct
|
||||
# source port per client so multiple users are demultiplexed by their outgoing
|
||||
# port: source_port = udp_base_port + client_id.
|
||||
#
|
||||
# Example: udp_base_port = 40000 means client_id 42 sends to the target from
|
||||
# source port 40042. Ensure udp_base_port + max(client_id) stays <= 65535.
|
||||
|
||||
mode = server
|
||||
transport = udp
|
||||
listen = 0.0.0.0:12300
|
||||
|
||||
# The real destination that every client's payloads are relayed to (e.g. a
|
||||
# WireGuard server endpoint).
|
||||
udp_target = 198.51.100.7:51820
|
||||
udp_base_port = 40000
|
||||
|
||||
# Optional: relay ordinary (non-tunnel) NTP requests to a real upstream server.
|
||||
upstream = 192.168.1.1:123
|
||||
|
||||
ext_type = 0x0F4E
|
||||
# Largest UDP payload (each direction) that fits one NTP carrier. Larger
|
||||
# datagrams are discarded with an error in the log.
|
||||
inner_mtu = 1136
|
||||
ntp_payload_cap = 1200
|
||||
relay_timeout_ms = 1000
|
||||
relay_rate_per_sec = 10
|
||||
log_level = info
|
||||
|
||||
# push = false
|
||||
|
||||
default_key = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
|
||||
client_key = 42:202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f
|
||||
|
|
@ -19,6 +19,15 @@ constexpr std::uint8_t kCarrierVersion = 1;
|
|||
// Carrier kinds.
|
||||
constexpr std::uint8_t kKindIpDatagram = 1;
|
||||
constexpr std::uint8_t kKindEmptyPoll = 2;
|
||||
// Raw UDP payload (port-tunneling transport). Carries an opaque application
|
||||
// datagram rather than a full inner IP packet.
|
||||
constexpr std::uint8_t kKindUdpDatagram = 3;
|
||||
|
||||
// Fixed per-packet overhead a WireGuard data packet adds over the encapsulated
|
||||
// inner IP packet (4 B type+reserved, 4 B receiver index, 8 B counter, 16 B
|
||||
// Poly1305 tag). Used to derive the largest WireGuard interface MTU that still
|
||||
// fits one NTP carrier: MTU_wg <= inner_mtu - kWireGuardOverhead.
|
||||
constexpr std::size_t kWireGuardOverhead = 32;
|
||||
|
||||
// Carrier layout sizes.
|
||||
constexpr std::size_t kClientIdSize = 2; // clear
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
// Tunnel client: reads inner IP datagrams from the TUN interface, encapsulates
|
||||
// them in NTP mode-3 requests, and delivers decapsulated downstream datagrams
|
||||
// received in mode-4 responses. Sends periodic empty polls to drain the
|
||||
// server's downstream queue.
|
||||
// Tunnel client. In TUN transport it reads inner IP datagrams from a TUN
|
||||
// interface; in UDP transport it listens on a local UDP port (e.g. a WireGuard
|
||||
// client pointed at 127.0.0.1:9000) and tunnels raw UDP payloads. Either way it
|
||||
// encapsulates datagrams in NTP mode-3 requests and delivers decapsulated
|
||||
// downstream datagrams received in mode-4 responses. Sends periodic empty polls
|
||||
// to drain the server's downstream queue.
|
||||
#ifndef NTPTUN_CLIENT_HPP
|
||||
#define NTPTUN_CLIENT_HPP
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
|
||||
#include "ntptun/Bytes.hpp"
|
||||
#include "ntptun/Config.hpp"
|
||||
#include "ntptun/Endpoint.hpp"
|
||||
#include "ntptun/Platform.hpp"
|
||||
#include "ntptun/TunDevice.hpp"
|
||||
#include "ntptun/UdpSocket.hpp"
|
||||
|
||||
|
|
@ -24,18 +29,24 @@ public:
|
|||
void run(const std::atomic<bool>& stop);
|
||||
|
||||
private:
|
||||
void drain_tun();
|
||||
void drain_inner(); // read from the local side (TUN or UDP listener)
|
||||
void drain_socket();
|
||||
void top_up_polls();
|
||||
void maybe_keepalive();
|
||||
bool send_carrier(std::uint8_t kind, ByteSpan inner_ip);
|
||||
bool send_carrier(std::uint8_t kind, ByteSpan inner);
|
||||
void handle_response(ByteSpan packet);
|
||||
void deliver_inner(std::uint8_t kind, ByteSpan payload);
|
||||
std::uint64_t next_transmit_ts();
|
||||
bool take_pending(std::uint64_t origin_ts);
|
||||
|
||||
ClientConfig cfg_;
|
||||
Key key_;
|
||||
TunDevice tun_;
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
std::optional<TunDevice> tun_;
|
||||
#endif
|
||||
std::optional<UdpSocket> local_; // UDP transport: local app listener
|
||||
Endpoint local_peer_; // UDP transport: last app source address
|
||||
bool have_local_peer_ = false;
|
||||
UdpSocket sock_;
|
||||
|
||||
std::uint64_t last_transmit_ts_ = 0;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,13 @@ namespace ntptun {
|
|||
|
||||
enum class Mode { Client, Server };
|
||||
|
||||
// Inner transport carried inside the NTP tunnel.
|
||||
// Tun: full inner IPv4/IPv6 datagrams via a TUN interface (Linux only).
|
||||
// Udp: raw UDP port tunneling (no TUN; builds and runs on Windows/MSVC).
|
||||
enum class Transport { Tun, Udp };
|
||||
|
||||
struct CommonConfig {
|
||||
Transport transport = Transport::Tun;
|
||||
std::string tun;
|
||||
std::uint16_t ext_type = 0x0F4E;
|
||||
std::size_t inner_mtu = 1136;
|
||||
|
|
@ -38,6 +44,9 @@ struct ClientConfig {
|
|||
// In push mode, how often to send a keepalive so the server keeps the
|
||||
// client's address/NAT mapping fresh.
|
||||
unsigned keepalive_ms = 1000;
|
||||
// UDP transport only: local address the client listens on for the app
|
||||
// (e.g. a WireGuard client pointed at 127.0.0.1:9000).
|
||||
Endpoint udp_listen;
|
||||
};
|
||||
|
||||
struct ServerConfig {
|
||||
|
|
@ -47,6 +56,11 @@ struct ServerConfig {
|
|||
unsigned relay_timeout_ms = 1000;
|
||||
unsigned relay_rate_per_sec = 10;
|
||||
std::size_t max_queue_per_client = 64;
|
||||
// UDP transport only: real target the decapsulated payloads are sent to,
|
||||
// and the base source port. Each client uses source port udp_base_port +
|
||||
// client_id so multiple users are demultiplexed by their outgoing port.
|
||||
Endpoint udp_target;
|
||||
std::uint16_t udp_base_port = 0;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@
|
|||
#ifndef NTPTUN_ENDPOINT_HPP
|
||||
#define NTPTUN_ENDPOINT_HPP
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "ntptun/IpPacket.hpp"
|
||||
#include "ntptun/Platform.hpp"
|
||||
|
||||
namespace ntptun {
|
||||
|
||||
|
|
@ -22,6 +20,10 @@ public:
|
|||
// When `passive` is true an empty/"*" host resolves to the wildcard address.
|
||||
static std::optional<Endpoint> parse(const std::string& hostport, bool passive);
|
||||
|
||||
// Build a wildcard (INADDR_ANY / in6addr_any) endpoint on a fixed local
|
||||
// port for the given address family. Used for per-client relay bind ports.
|
||||
static Endpoint wildcard(int family, std::uint16_t port);
|
||||
|
||||
const sockaddr* sockaddr_ptr() const noexcept {
|
||||
return reinterpret_cast<const sockaddr*>(&storage_);
|
||||
}
|
||||
|
|
|
|||
95
include/ntptun/Platform.hpp
Normal file
95
include/ntptun/Platform.hpp
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Cross-platform networking shims. Centralizes the differences between Winsock
|
||||
// (MSVC/Windows) and POSIX (Linux) so the rest of the code can use one socket
|
||||
// type, one poll wrapper and one set of error helpers.
|
||||
//
|
||||
// TUN support is Linux-only; NTPTUN_HAVE_TUN is defined there and nowhere else.
|
||||
#ifndef NTPTUN_PLATFORM_HPP
|
||||
#define NTPTUN_PLATFORM_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
# ifndef WIN32_LEAN_AND_MEAN
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# endif
|
||||
# ifndef NOMINMAX
|
||||
# define NOMINMAX
|
||||
# endif
|
||||
// WSAPoll, AI_NUMERICSERV and getaddrinfo require Windows Vista (0x0600) or
|
||||
// later. Raise the target if the toolchain (e.g. MinGW) defaults lower.
|
||||
# if !defined(_WIN32_WINNT) || (_WIN32_WINNT < 0x0600)
|
||||
# undef _WIN32_WINNT
|
||||
# define _WIN32_WINNT 0x0600
|
||||
# endif
|
||||
# include <winsock2.h>
|
||||
# include <ws2tcpip.h>
|
||||
# include <basetsd.h>
|
||||
|
||||
# if !defined(_SSIZE_T_DEFINED) && !defined(NTPTUN_SSIZE_T_DEFINED)
|
||||
# define NTPTUN_SSIZE_T_DEFINED
|
||||
using ssize_t = SSIZE_T;
|
||||
# endif
|
||||
|
||||
#else // POSIX
|
||||
|
||||
# include <sys/types.h>
|
||||
# include <sys/socket.h>
|
||||
# include <netinet/in.h>
|
||||
# include <arpa/inet.h>
|
||||
# include <netdb.h>
|
||||
# include <poll.h>
|
||||
# include <unistd.h>
|
||||
|
||||
# if defined(__linux__)
|
||||
# define NTPTUN_HAVE_TUN 1
|
||||
# endif
|
||||
|
||||
#endif
|
||||
|
||||
namespace ntptun {
|
||||
|
||||
#if defined(_WIN32)
|
||||
using socket_t = SOCKET;
|
||||
inline constexpr socket_t kInvalidSocket = INVALID_SOCKET;
|
||||
#else
|
||||
using socket_t = int;
|
||||
inline constexpr socket_t kInvalidSocket = -1;
|
||||
#endif
|
||||
|
||||
// Poll flag for "readable"; translated to the native value inside net_poll.
|
||||
inline constexpr short kPollIn = 0x0001;
|
||||
|
||||
// A portable pollfd equivalent used with net_poll.
|
||||
struct PollFd {
|
||||
socket_t fd = kInvalidSocket;
|
||||
short events = 0;
|
||||
short revents = 0;
|
||||
};
|
||||
|
||||
// Initialize the networking stack (WSAStartup on Windows; a no-op elsewhere).
|
||||
// Idempotent and safe to call from any entry point.
|
||||
void net_startup();
|
||||
|
||||
// Close a socket handle (closesocket / close).
|
||||
void socket_close(socket_t s) noexcept;
|
||||
|
||||
// True when the most recent socket operation failed only because it would have
|
||||
// blocked (EAGAIN/EWOULDBLOCK / WSAEWOULDBLOCK).
|
||||
bool socket_would_block() noexcept;
|
||||
|
||||
// Throw std::system_error describing the most recent socket error.
|
||||
[[noreturn]] void socket_throw(const char* what);
|
||||
|
||||
// Put a socket into (non)blocking mode.
|
||||
void socket_set_nonblocking(socket_t s, bool enabled);
|
||||
|
||||
// Wait for readability/writability on the given descriptors. Semantics match
|
||||
// ::poll: returns the number of ready descriptors, 0 on timeout, <0 on error
|
||||
// (with errno/WSAGetLastError set, EINTR possible on POSIX).
|
||||
int net_poll(PollFd* fds, std::size_t n, int timeout_ms);
|
||||
|
||||
} // namespace ntptun
|
||||
|
||||
#endif // NTPTUN_PLATFORM_HPP
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
// Tunnel server: classifies incoming NTP packets into the tunnel path or the
|
||||
// real-NTP relay path, forwards decapsulated client datagrams onto its TUN
|
||||
// interface, queues downstream datagrams per client, and answers each request
|
||||
// with exactly one response.
|
||||
// real-NTP relay path. In TUN transport it forwards decapsulated client
|
||||
// datagrams onto its TUN interface and source-learns downstream routes; in UDP
|
||||
// transport it forwards raw payloads to a configured target from a per-client
|
||||
// source port (udp_base_port + client_id). Queues downstream datagrams per
|
||||
// client and answers each request with exactly one response.
|
||||
#ifndef NTPTUN_SERVER_HPP
|
||||
#define NTPTUN_SERVER_HPP
|
||||
|
||||
|
|
@ -17,6 +19,7 @@
|
|||
#include "ntptun/Config.hpp"
|
||||
#include "ntptun/IpPacket.hpp"
|
||||
#include "ntptun/Ntp.hpp"
|
||||
#include "ntptun/Platform.hpp"
|
||||
#include "ntptun/TunDevice.hpp"
|
||||
#include "ntptun/UdpSocket.hpp"
|
||||
|
||||
|
|
@ -51,10 +54,14 @@ private:
|
|||
void drain_udp();
|
||||
void drain_tun();
|
||||
void drain_upstream();
|
||||
void drain_relay(std::uint16_t client_id, UdpSocket& sock);
|
||||
|
||||
void process_request(ByteSpan packet, const Endpoint& from);
|
||||
void handle_tunnel(std::uint64_t request_transmit_ts, const DecodedCarrier& carrier,
|
||||
const Endpoint& from);
|
||||
void queue_downstream(std::uint16_t client_id, std::uint8_t kind, ByteSpan payload);
|
||||
// UDP transport: get (creating if needed) the relay socket for a client.
|
||||
UdpSocket* relay_for(std::uint16_t client_id);
|
||||
void send_response(std::uint16_t client_id, std::uint64_t request_transmit_ts,
|
||||
std::uint8_t kind, ByteSpan payload, const Endpoint& from);
|
||||
void relay(ByteSpan packet, const NtpHeader& header, const Endpoint& from);
|
||||
|
|
@ -63,12 +70,18 @@ private:
|
|||
std::uint64_t next_transmit_ts();
|
||||
|
||||
ServerConfig cfg_;
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
TunDevice tun_;
|
||||
std::map<ip::Address, std::uint16_t> routes_; // learned inner dst -> client id
|
||||
#endif
|
||||
UdpSocket sock_;
|
||||
std::optional<UdpSocket> upstream_;
|
||||
|
||||
std::map<std::uint16_t, ClientState> clients_;
|
||||
std::map<ip::Address, std::uint16_t> routes_; // learned inner dst -> client id
|
||||
// UDP transport: per-client relay sockets to the target and their last-use
|
||||
// time (for LRU eviction when the table is full).
|
||||
std::map<std::uint16_t, UdpSocket> relay_socks_;
|
||||
std::map<std::uint16_t, std::chrono::steady_clock::time_point> relay_last_;
|
||||
std::deque<PendingRelay> relays_;
|
||||
std::map<ip::Address, RateBucket> rate_;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@
|
|||
#ifndef NTPTUN_TUNDEVICE_HPP
|
||||
#define NTPTUN_TUNDEVICE_HPP
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
#include "ntptun/Bytes.hpp"
|
||||
#include "ntptun/Platform.hpp"
|
||||
|
||||
namespace ntptun {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
// RAII UDP socket wrapper. Throws std::system_error on fatal errors; recv/send
|
||||
// return -1 on EAGAIN so callers can drive them from a poll() loop.
|
||||
// return -1 when the operation would block so callers can drive them from a
|
||||
// poll() loop. Cross-platform over Winsock and POSIX (see Platform.hpp).
|
||||
#ifndef NTPTUN_UDPSOCKET_HPP
|
||||
#define NTPTUN_UDPSOCKET_HPP
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "ntptun/Bytes.hpp"
|
||||
#include "ntptun/Endpoint.hpp"
|
||||
#include "ntptun/Platform.hpp"
|
||||
|
||||
namespace ntptun {
|
||||
|
||||
|
|
@ -31,20 +31,20 @@ public:
|
|||
void connect(const Endpoint& remote);
|
||||
void set_nonblocking(bool enabled);
|
||||
|
||||
int fd() const noexcept { return fd_; }
|
||||
bool valid() const noexcept { return fd_ >= 0; }
|
||||
socket_t fd() const noexcept { return fd_; }
|
||||
bool valid() const noexcept { return fd_ != kInvalidSocket; }
|
||||
|
||||
// Return bytes transferred, or -1 on EAGAIN/EWOULDBLOCK.
|
||||
// Return bytes transferred, or -1 when the operation would block.
|
||||
ssize_t recv_from(Byte* buf, std::size_t len, Endpoint& from);
|
||||
ssize_t send_to(const Byte* buf, std::size_t len, const Endpoint& to);
|
||||
ssize_t recv(Byte* buf, std::size_t len);
|
||||
ssize_t send(const Byte* buf, std::size_t len);
|
||||
|
||||
private:
|
||||
explicit UdpSocket(int fd) noexcept : fd_(fd) {}
|
||||
explicit UdpSocket(socket_t fd) noexcept : fd_(fd) {}
|
||||
void close_fd() noexcept;
|
||||
|
||||
int fd_ = -1;
|
||||
socket_t fd_ = kInvalidSocket;
|
||||
};
|
||||
|
||||
} // namespace ntptun
|
||||
|
|
|
|||
|
|
@ -121,7 +121,8 @@ CarrierStatus decode_carrier_value(ByteSpan value, const KeyStore& keys,
|
|||
if (flags != 0) {
|
||||
return CarrierStatus::Invalid;
|
||||
}
|
||||
if (kind != kKindIpDatagram && kind != kKindEmptyPoll) {
|
||||
if (kind != kKindIpDatagram && kind != kKindEmptyPoll &&
|
||||
kind != kKindUdpDatagram) {
|
||||
return CarrierStatus::Invalid;
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +147,19 @@ CarrierStatus decode_carrier_value(ByteSpan value, const KeyStore& keys,
|
|||
return CarrierStatus::Ok;
|
||||
}
|
||||
|
||||
if (kind == kKindUdpDatagram) {
|
||||
// Raw UDP port-tunneling payload: opaque application bytes, not an IP
|
||||
// datagram, so no inner-IP length validation applies.
|
||||
if (payload_len == 0) {
|
||||
return CarrierStatus::Invalid;
|
||||
}
|
||||
out.client_id = client_id;
|
||||
out.kind = kind;
|
||||
out.inner_ip.assign(buf.data() + kProtectedHeaderSize,
|
||||
buf.data() + kProtectedHeaderSize + payload_len);
|
||||
return CarrierStatus::Ok;
|
||||
}
|
||||
|
||||
// kind == kKindIpDatagram
|
||||
if (payload_len == 0) {
|
||||
return CarrierStatus::Invalid;
|
||||
|
|
|
|||
135
src/Client.cpp
135
src/Client.cpp
|
|
@ -1,18 +1,16 @@
|
|||
#include "ntptun/Client.hpp"
|
||||
|
||||
#include <poll.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cerrno>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
|
||||
#include "ntptun/Carrier.hpp"
|
||||
#include "ntptun/IpPacket.hpp"
|
||||
#include "ntptun/Logging.hpp"
|
||||
#include "ntptun/Ntp.hpp"
|
||||
#include "ntptun/Platform.hpp"
|
||||
|
||||
namespace ntptun {
|
||||
namespace {
|
||||
|
|
@ -30,53 +28,94 @@ std::uint64_t now_ms() {
|
|||
|
||||
Client::Client(ClientConfig config)
|
||||
: cfg_(std::move(config)),
|
||||
tun_(TunDevice::open(cfg_.common.tun)),
|
||||
sock_(UdpSocket::create(cfg_.server.family())) {
|
||||
const auto key = cfg_.common.keys.resolve(cfg_.client_id);
|
||||
if (!key) {
|
||||
throw std::runtime_error("no key resolves for configured client_id");
|
||||
}
|
||||
key_ = *key;
|
||||
|
||||
if (cfg_.common.transport == Transport::Udp) {
|
||||
local_ = UdpSocket::bind(cfg_.udp_listen);
|
||||
} else {
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
tun_ = TunDevice::open(cfg_.common.tun);
|
||||
#else
|
||||
throw std::runtime_error("tun transport is not supported on this platform");
|
||||
#endif
|
||||
}
|
||||
sock_.connect(cfg_.server);
|
||||
}
|
||||
|
||||
void Client::run(const std::atomic<bool>& stop) {
|
||||
tun_.set_nonblocking(true);
|
||||
sock_.set_nonblocking(true);
|
||||
|
||||
std::array<pollfd, 2> fds{};
|
||||
fds[0].fd = tun_.fd();
|
||||
fds[0].events = POLLIN;
|
||||
socket_t inner_fd = kInvalidSocket;
|
||||
if (cfg_.common.transport == Transport::Udp) {
|
||||
local_->set_nonblocking(true);
|
||||
inner_fd = local_->fd();
|
||||
} else {
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
tun_->set_nonblocking(true);
|
||||
inner_fd = tun_->fd();
|
||||
#endif
|
||||
}
|
||||
|
||||
PollFd fds[2]{};
|
||||
fds[0].fd = inner_fd;
|
||||
fds[0].events = kPollIn;
|
||||
fds[1].fd = sock_.fd();
|
||||
fds[1].events = POLLIN;
|
||||
fds[1].events = kPollIn;
|
||||
|
||||
last_transmit_ts_ = 0;
|
||||
const bool push = cfg_.common.push_mode;
|
||||
const int timeout_ms =
|
||||
static_cast<int>(push ? cfg_.keepalive_ms : cfg_.poll_interval_ms);
|
||||
const char* xport = cfg_.common.transport == Transport::Udp ? "udp" : "tun";
|
||||
const std::string inner_desc =
|
||||
cfg_.common.transport == Transport::Udp
|
||||
? ("listen=" + cfg_.udp_listen.to_string())
|
||||
: ("tun=" + cfg_.common.tun);
|
||||
if (push) {
|
||||
log_info("client ", cfg_.client_id, " up: tun=", tun_.name(),
|
||||
log_info("client ", cfg_.client_id, " up: transport=", xport, " ", inner_desc,
|
||||
" server=", cfg_.server.to_string(), " mode=push keepalive_ms=",
|
||||
cfg_.keepalive_ms);
|
||||
} else {
|
||||
log_info("client ", cfg_.client_id, " up: tun=", tun_.name(),
|
||||
log_info("client ", cfg_.client_id, " up: transport=", xport, " ", inner_desc,
|
||||
" server=", cfg_.server.to_string(), " mode=poll poll_window=",
|
||||
cfg_.poll_window);
|
||||
}
|
||||
|
||||
if (cfg_.common.transport == Transport::Udp) {
|
||||
// Largest inner app payload is inner_mtu; a WireGuard data packet adds a
|
||||
// fixed overhead on top of its encapsulated IP packet, so the WireGuard
|
||||
// interface MTU must be at most inner_mtu - kWireGuardOverhead.
|
||||
if (cfg_.common.inner_mtu > kWireGuardOverhead) {
|
||||
log_info("udp: max inner UDP payload ", cfg_.common.inner_mtu,
|
||||
" bytes; set WireGuard MTU <= ",
|
||||
cfg_.common.inner_mtu - kWireGuardOverhead);
|
||||
} else {
|
||||
log_warn("udp: inner_mtu ", cfg_.common.inner_mtu,
|
||||
" is too small to carry WireGuard (needs > ",
|
||||
kWireGuardOverhead, " bytes)");
|
||||
}
|
||||
}
|
||||
|
||||
while (!stop.load()) {
|
||||
const int rc = ::poll(fds.data(), fds.size(), timeout_ms);
|
||||
const int rc = net_poll(fds, 2, timeout_ms);
|
||||
if (rc < 0) {
|
||||
#if !defined(_WIN32)
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
throw std::system_error(errno, std::generic_category(), "poll");
|
||||
#endif
|
||||
socket_throw("poll");
|
||||
}
|
||||
if (rc > 0) {
|
||||
if (fds[0].revents & POLLIN) {
|
||||
drain_tun();
|
||||
if (fds[0].revents & kPollIn) {
|
||||
drain_inner();
|
||||
}
|
||||
if (fds[1].revents & POLLIN) {
|
||||
if (fds[1].revents & kPollIn) {
|
||||
drain_socket();
|
||||
}
|
||||
}
|
||||
|
|
@ -89,10 +128,33 @@ void Client::run(const std::atomic<bool>& stop) {
|
|||
log_info("client shutting down");
|
||||
}
|
||||
|
||||
void Client::drain_tun() {
|
||||
void Client::drain_inner() {
|
||||
std::array<Byte, kBufSize> buf{};
|
||||
if (cfg_.common.transport == Transport::Udp) {
|
||||
Endpoint peer;
|
||||
for (;;) {
|
||||
const ssize_t n = local_->recv_from(buf.data(), buf.size(), peer);
|
||||
if (n < 0) {
|
||||
break; // no more packets
|
||||
}
|
||||
local_peer_ = peer;
|
||||
have_local_peer_ = true;
|
||||
if (static_cast<std::size_t>(n) > cfg_.common.inner_mtu) {
|
||||
log_error("udp: dropping oversized datagram of ", n,
|
||||
" bytes (max payload is ", cfg_.common.inner_mtu,
|
||||
"); lower the inner app MTU, or for QUIC/Hysteria2 "
|
||||
"raise inner_mtu >= 1200 and ntp_payload_cap >= "
|
||||
"inner_mtu+64");
|
||||
continue;
|
||||
}
|
||||
send_carrier(kKindUdpDatagram,
|
||||
ByteSpan(buf.data(), static_cast<std::size_t>(n)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
for (;;) {
|
||||
const ssize_t n = tun_.read_packet(buf.data(), buf.size());
|
||||
const ssize_t n = tun_->read_packet(buf.data(), buf.size());
|
||||
if (n < 0) {
|
||||
break; // EAGAIN: no more packets
|
||||
}
|
||||
|
|
@ -118,9 +180,10 @@ void Client::drain_tun() {
|
|||
}
|
||||
send_carrier(kKindIpDatagram, pkt);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Client::send_carrier(std::uint8_t kind, ByteSpan inner_ip) {
|
||||
bool Client::send_carrier(std::uint8_t kind, ByteSpan inner) {
|
||||
const std::uint64_t ts = next_transmit_ts();
|
||||
|
||||
NtpHeader header;
|
||||
|
|
@ -129,14 +192,14 @@ bool Client::send_carrier(std::uint8_t kind, ByteSpan inner_ip) {
|
|||
header.precision = -20;
|
||||
header.transmit_ts = ts;
|
||||
|
||||
const ByteVector value = encode_carrier_value(cfg_.client_id, kind, inner_ip,
|
||||
const ByteVector value = encode_carrier_value(cfg_.client_id, kind, inner,
|
||||
key_, Direction::ClientToServer, ts);
|
||||
const ByteVector packet =
|
||||
build_packet_with_extension(header, cfg_.common.ext_type, value);
|
||||
|
||||
if (packet.size() > cfg_.common.ntp_payload_cap) {
|
||||
log_warn("dropping datagram: encoded size ", packet.size(),
|
||||
" exceeds NTP payload cap ", cfg_.common.ntp_payload_cap);
|
||||
log_error("dropping datagram: encoded size ", packet.size(),
|
||||
" exceeds NTP payload cap ", cfg_.common.ntp_payload_cap);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -226,11 +289,35 @@ void Client::handle_response(ByteSpan packet) {
|
|||
if (decoded.client_id != cfg_.client_id) {
|
||||
return; // belongs to another logical association
|
||||
}
|
||||
if (decoded.kind == kKindIpDatagram && !decoded.inner_ip.empty()) {
|
||||
tun_.write_packet(decoded.inner_ip.data(), decoded.inner_ip.size());
|
||||
if ((decoded.kind == kKindIpDatagram || decoded.kind == kKindUdpDatagram) &&
|
||||
!decoded.inner_ip.empty()) {
|
||||
deliver_inner(decoded.kind, decoded.inner_ip);
|
||||
}
|
||||
}
|
||||
|
||||
void Client::deliver_inner(std::uint8_t kind, ByteSpan payload) {
|
||||
if (payload.empty()) {
|
||||
return;
|
||||
}
|
||||
if (cfg_.common.transport == Transport::Udp) {
|
||||
if (kind != kKindUdpDatagram) {
|
||||
return;
|
||||
}
|
||||
if (!have_local_peer_) {
|
||||
log_debug("udp: no local peer yet; dropping downstream datagram");
|
||||
return;
|
||||
}
|
||||
local_->send_to(payload.data(), payload.size(), local_peer_);
|
||||
return;
|
||||
}
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
if (kind != kKindIpDatagram) {
|
||||
return;
|
||||
}
|
||||
tun_->write_packet(payload.data(), payload.size());
|
||||
#endif
|
||||
}
|
||||
|
||||
std::uint64_t Client::next_transmit_ts() {
|
||||
std::uint64_t ts = ntp_now();
|
||||
if (ts <= last_transmit_ts_) {
|
||||
|
|
|
|||
|
|
@ -144,7 +144,25 @@ Config load_config(const std::string& path) {
|
|||
Config cfg;
|
||||
|
||||
CommonConfig common;
|
||||
common.tun = require("tun");
|
||||
if (const auto* v = get("transport")) {
|
||||
if (*v == "tun") {
|
||||
common.transport = Transport::Tun;
|
||||
} else if (*v == "udp") {
|
||||
common.transport = Transport::Udp;
|
||||
} else {
|
||||
fail("transport must be 'tun' or 'udp', got: " + *v);
|
||||
}
|
||||
}
|
||||
#if !defined(NTPTUN_HAVE_TUN)
|
||||
if (common.transport == Transport::Tun) {
|
||||
fail("transport 'tun' is not supported on this platform; use 'udp'");
|
||||
}
|
||||
#endif
|
||||
if (common.transport == Transport::Tun) {
|
||||
common.tun = require("tun");
|
||||
} else if (const auto* v = get("tun")) {
|
||||
common.tun = *v; // optional/ignored in udp transport
|
||||
}
|
||||
if (const auto* v = get("ext_type")) {
|
||||
common.ext_type = static_cast<std::uint16_t>(parse_uint("ext_type", *v, 65535));
|
||||
}
|
||||
|
|
@ -198,6 +216,10 @@ Config load_config(const std::string& path) {
|
|||
cc.keepalive_ms =
|
||||
static_cast<unsigned>(parse_uint("keepalive_ms", *v, 3600000));
|
||||
}
|
||||
if (cc.common.transport == Transport::Udp) {
|
||||
cc.udp_listen =
|
||||
parse_endpoint("udp_listen", require("udp_listen"), true);
|
||||
}
|
||||
cfg.client = std::move(cc);
|
||||
} else if (mode_str == "server") {
|
||||
cfg.mode = Mode::Server;
|
||||
|
|
@ -218,6 +240,14 @@ Config load_config(const std::string& path) {
|
|||
if (const auto* v = get("max_queue_per_client")) {
|
||||
sc.max_queue_per_client = parse_uint("max_queue_per_client", *v, 1000000);
|
||||
}
|
||||
if (sc.common.transport == Transport::Udp) {
|
||||
sc.udp_target = parse_endpoint("udp_target", require("udp_target"), false);
|
||||
sc.udp_base_port = static_cast<std::uint16_t>(
|
||||
parse_uint("udp_base_port", require("udp_base_port"), 65535));
|
||||
if (sc.udp_base_port == 0) {
|
||||
fail("udp_base_port must be between 1 and 65535");
|
||||
}
|
||||
}
|
||||
cfg.server = std::move(sc);
|
||||
} else {
|
||||
fail("mode must be 'client' or 'server', got: " + mode_str);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
#include "ntptun/Endpoint.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "ntptun/Platform.hpp"
|
||||
|
||||
namespace ntptun {
|
||||
|
||||
std::optional<Endpoint> Endpoint::parse(const std::string& hostport, bool passive) {
|
||||
net_startup();
|
||||
std::string host;
|
||||
std::string port;
|
||||
|
||||
|
|
@ -50,11 +50,29 @@ std::optional<Endpoint> Endpoint::parse(const std::string& hostport, bool passiv
|
|||
|
||||
Endpoint ep;
|
||||
std::memcpy(&ep.storage_, result->ai_addr, result->ai_addrlen);
|
||||
ep.length_ = result->ai_addrlen;
|
||||
ep.length_ = static_cast<socklen_t>(result->ai_addrlen);
|
||||
freeaddrinfo(result);
|
||||
return ep;
|
||||
}
|
||||
|
||||
Endpoint Endpoint::wildcard(int family, std::uint16_t port) {
|
||||
Endpoint ep;
|
||||
if (family == AF_INET6) {
|
||||
auto* in6 = reinterpret_cast<sockaddr_in6*>(&ep.storage_);
|
||||
in6->sin6_family = AF_INET6;
|
||||
in6->sin6_addr = in6addr_any;
|
||||
in6->sin6_port = htons(port);
|
||||
ep.length_ = sizeof(sockaddr_in6);
|
||||
} else {
|
||||
auto* in4 = reinterpret_cast<sockaddr_in*>(&ep.storage_);
|
||||
in4->sin_family = AF_INET;
|
||||
in4->sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
in4->sin_port = htons(port);
|
||||
ep.length_ = sizeof(sockaddr_in);
|
||||
}
|
||||
return ep;
|
||||
}
|
||||
|
||||
ip::Address Endpoint::ip_key() const noexcept {
|
||||
ip::Address key;
|
||||
if (storage_.ss_family == AF_INET) {
|
||||
|
|
|
|||
14
src/Ntp.cpp
14
src/Ntp.cpp
|
|
@ -1,7 +1,7 @@
|
|||
#include "ntptun/Ntp.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
|
||||
namespace ntptun {
|
||||
|
||||
|
|
@ -86,12 +86,12 @@ bool parse_ntp_extensions(ByteSpan packet, std::vector<NtpExtension>& out) {
|
|||
}
|
||||
|
||||
std::uint64_t ntp_now() {
|
||||
timespec ts{};
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
const std::uint64_t seconds =
|
||||
static_cast<std::uint64_t>(ts.tv_sec) + kNtpUnixEpochOffset;
|
||||
const std::uint64_t frac =
|
||||
(static_cast<std::uint64_t>(ts.tv_nsec) << 32) / 1000000000ULL;
|
||||
using namespace std::chrono;
|
||||
const auto now = system_clock::now().time_since_epoch();
|
||||
const std::uint64_t whole_ns =
|
||||
static_cast<std::uint64_t>(duration_cast<nanoseconds>(now).count());
|
||||
const std::uint64_t seconds = whole_ns / 1000000000ULL + kNtpUnixEpochOffset;
|
||||
const std::uint64_t frac = ((whole_ns % 1000000000ULL) << 32) / 1000000000ULL;
|
||||
return (seconds << 32) | (frac & 0xFFFFFFFFULL);
|
||||
}
|
||||
|
||||
|
|
|
|||
134
src/Platform.cpp
Normal file
134
src/Platform.cpp
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#include "ntptun/Platform.hpp"
|
||||
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
# include <cerrno>
|
||||
# include <fcntl.h>
|
||||
#endif
|
||||
|
||||
namespace ntptun {
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
void net_startup() {
|
||||
static const bool ok = [] {
|
||||
WSADATA data{};
|
||||
const int rc = WSAStartup(MAKEWORD(2, 2), &data);
|
||||
if (rc != 0) {
|
||||
throw std::system_error(rc, std::system_category(), "WSAStartup");
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
(void)ok;
|
||||
}
|
||||
|
||||
void socket_close(socket_t s) noexcept {
|
||||
if (s != kInvalidSocket) {
|
||||
::closesocket(s);
|
||||
}
|
||||
}
|
||||
|
||||
bool socket_would_block() noexcept {
|
||||
const int e = WSAGetLastError();
|
||||
return e == WSAEWOULDBLOCK;
|
||||
}
|
||||
|
||||
void socket_throw(const char* what) {
|
||||
throw std::system_error(WSAGetLastError(), std::system_category(), what);
|
||||
}
|
||||
|
||||
void socket_set_nonblocking(socket_t s, bool enabled) {
|
||||
u_long mode = enabled ? 1u : 0u;
|
||||
if (::ioctlsocket(s, FIONBIO, &mode) != 0) {
|
||||
socket_throw("ioctlsocket(FIONBIO)");
|
||||
}
|
||||
}
|
||||
|
||||
int net_poll(PollFd* fds, std::size_t n, int timeout_ms) {
|
||||
std::vector<WSAPOLLFD> native(n);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
native[i].fd = fds[i].fd;
|
||||
native[i].events = (fds[i].events & kPollIn) ? POLLRDNORM : 0;
|
||||
native[i].revents = 0;
|
||||
}
|
||||
const int rc = ::WSAPoll(native.empty() ? nullptr : native.data(),
|
||||
static_cast<ULONG>(n), timeout_ms);
|
||||
if (rc > 0) {
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
short r = 0;
|
||||
if (native[i].revents & (POLLRDNORM | POLLHUP | POLLERR)) {
|
||||
r |= kPollIn;
|
||||
}
|
||||
fds[i].revents = r;
|
||||
}
|
||||
} else {
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
fds[i].revents = 0;
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
#else // POSIX
|
||||
|
||||
void net_startup() {}
|
||||
|
||||
void socket_close(socket_t s) noexcept {
|
||||
if (s != kInvalidSocket) {
|
||||
::close(s);
|
||||
}
|
||||
}
|
||||
|
||||
bool socket_would_block() noexcept {
|
||||
return errno == EAGAIN || errno == EWOULDBLOCK;
|
||||
}
|
||||
|
||||
void socket_throw(const char* what) {
|
||||
throw std::system_error(errno, std::generic_category(), what);
|
||||
}
|
||||
|
||||
void socket_set_nonblocking(socket_t s, bool enabled) {
|
||||
int flags = ::fcntl(s, F_GETFL, 0);
|
||||
if (flags < 0) {
|
||||
socket_throw("fcntl(F_GETFL)");
|
||||
}
|
||||
if (enabled) {
|
||||
flags |= O_NONBLOCK;
|
||||
} else {
|
||||
flags &= ~O_NONBLOCK;
|
||||
}
|
||||
if (::fcntl(s, F_SETFL, flags) < 0) {
|
||||
socket_throw("fcntl(F_SETFL)");
|
||||
}
|
||||
}
|
||||
|
||||
int net_poll(PollFd* fds, std::size_t n, int timeout_ms) {
|
||||
std::vector<pollfd> native(n);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
native[i].fd = fds[i].fd;
|
||||
native[i].events = (fds[i].events & kPollIn) ? POLLIN : 0;
|
||||
native[i].revents = 0;
|
||||
}
|
||||
const int rc = ::poll(native.empty() ? nullptr : native.data(),
|
||||
static_cast<nfds_t>(n), timeout_ms);
|
||||
if (rc > 0) {
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
short r = 0;
|
||||
if (native[i].revents & (POLLIN | POLLHUP | POLLERR)) {
|
||||
r |= kPollIn;
|
||||
}
|
||||
fds[i].revents = r;
|
||||
}
|
||||
} else {
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
fds[i].revents = 0;
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace ntptun
|
||||
233
src/Server.cpp
233
src/Server.cpp
|
|
@ -1,16 +1,15 @@
|
|||
#include "ntptun/Server.hpp"
|
||||
|
||||
#include <poll.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ntptun/Logging.hpp"
|
||||
#include "ntptun/Platform.hpp"
|
||||
|
||||
namespace ntptun {
|
||||
namespace {
|
||||
|
|
@ -19,7 +18,9 @@ constexpr std::size_t kBufSize = 65536;
|
|||
constexpr std::size_t kMaxPendingRelays = 4096;
|
||||
constexpr std::size_t kMaxRateEntries = 8192;
|
||||
constexpr std::size_t kMaxRoutes = 100000;
|
||||
constexpr std::size_t kMaxRelaySockets = 4096;
|
||||
constexpr int kPollTimeoutMs = 200;
|
||||
constexpr std::size_t kNoIndex = static_cast<std::size_t>(-1);
|
||||
|
||||
bool plausible_request(const NtpHeader& header) {
|
||||
const std::uint8_t version = header.version();
|
||||
|
|
@ -30,9 +31,14 @@ bool plausible_request(const NtpHeader& header) {
|
|||
} // namespace
|
||||
|
||||
Server::Server(ServerConfig config)
|
||||
: cfg_(std::move(config)),
|
||||
tun_(TunDevice::open(cfg_.common.tun)),
|
||||
sock_(UdpSocket::bind(cfg_.listen)) {
|
||||
: cfg_(std::move(config)), sock_(UdpSocket::bind(cfg_.listen)) {
|
||||
if (cfg_.common.transport == Transport::Tun) {
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
tun_ = TunDevice::open(cfg_.common.tun);
|
||||
#else
|
||||
throw std::runtime_error("tun transport is not supported on this platform");
|
||||
#endif
|
||||
}
|
||||
if (cfg_.upstream) {
|
||||
UdpSocket up = UdpSocket::create(cfg_.upstream->family());
|
||||
up.connect(*cfg_.upstream);
|
||||
|
|
@ -41,49 +47,98 @@ Server::Server(ServerConfig config)
|
|||
}
|
||||
|
||||
void Server::run(const std::atomic<bool>& stop) {
|
||||
tun_.set_nonblocking(true);
|
||||
const bool tun_xport = cfg_.common.transport == Transport::Tun;
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
if (tun_xport) {
|
||||
tun_.set_nonblocking(true);
|
||||
}
|
||||
#endif
|
||||
sock_.set_nonblocking(true);
|
||||
if (upstream_) {
|
||||
upstream_->set_nonblocking(true);
|
||||
}
|
||||
|
||||
log_info("server up: tun=", tun_.name(), " listen=", cfg_.listen.to_string(),
|
||||
upstream_ ? " relay=on" : " relay=off",
|
||||
const char* xport = tun_xport ? "tun" : "udp";
|
||||
const std::string inner_desc =
|
||||
tun_xport ? ("tun=" + cfg_.common.tun)
|
||||
: ("target=" + cfg_.udp_target.to_string() + " base_port=" +
|
||||
std::to_string(cfg_.udp_base_port));
|
||||
log_info("server up: transport=", xport, " ", inner_desc,
|
||||
" listen=", cfg_.listen.to_string(), upstream_ ? " relay=on" : " relay=off",
|
||||
cfg_.common.push_mode ? " mode=push" : " mode=poll");
|
||||
|
||||
if (!tun_xport) {
|
||||
// In UDP transport each client relays from source port
|
||||
// udp_base_port + client_id, so the largest usable client_id is
|
||||
// (65535 - udp_base_port). Clients above that cannot be served and are
|
||||
// rejected per-request in relay_for().
|
||||
const unsigned max_client_id = 65535u - cfg_.udp_base_port;
|
||||
log_info("udp relay: source ports ", cfg_.udp_base_port, "..65535; ",
|
||||
"usable client_id range 1..", max_client_id,
|
||||
" (udp_base_port + client_id must be <= 65535)");
|
||||
if (max_client_id < 65535u) {
|
||||
log_warn("udp relay: client_id > ", max_client_id,
|
||||
" cannot be served with udp_base_port=", cfg_.udp_base_port);
|
||||
}
|
||||
}
|
||||
|
||||
while (!stop.load()) {
|
||||
std::array<pollfd, 3> fds{};
|
||||
nfds_t nfds = 0;
|
||||
fds[nfds].fd = tun_.fd();
|
||||
fds[nfds].events = POLLIN;
|
||||
const std::size_t tun_idx = nfds++;
|
||||
fds[nfds].fd = sock_.fd();
|
||||
fds[nfds].events = POLLIN;
|
||||
const std::size_t udp_idx = nfds++;
|
||||
std::size_t up_idx = 0;
|
||||
std::vector<PollFd> fds;
|
||||
std::size_t up_idx = kNoIndex;
|
||||
|
||||
fds.push_back(PollFd{sock_.fd(), kPollIn, 0});
|
||||
const std::size_t udp_idx = 0;
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
std::size_t tun_idx = kNoIndex;
|
||||
if (tun_xport) {
|
||||
tun_idx = fds.size();
|
||||
fds.push_back(PollFd{tun_.fd(), kPollIn, 0});
|
||||
}
|
||||
#endif
|
||||
if (upstream_) {
|
||||
fds[nfds].fd = upstream_->fd();
|
||||
fds[nfds].events = POLLIN;
|
||||
up_idx = nfds++;
|
||||
up_idx = fds.size();
|
||||
fds.push_back(PollFd{upstream_->fd(), kPollIn, 0});
|
||||
}
|
||||
|
||||
const int rc = ::poll(fds.data(), nfds, kPollTimeoutMs);
|
||||
std::vector<std::uint16_t> relay_ids;
|
||||
if (!tun_xport) {
|
||||
relay_ids.reserve(relay_socks_.size());
|
||||
for (auto& entry : relay_socks_) {
|
||||
relay_ids.push_back(entry.first);
|
||||
fds.push_back(PollFd{entry.second.fd(), kPollIn, 0});
|
||||
}
|
||||
}
|
||||
const std::size_t relay_base = fds.size() - relay_ids.size();
|
||||
|
||||
const int rc = net_poll(fds.data(), fds.size(), kPollTimeoutMs);
|
||||
if (rc < 0) {
|
||||
#if !defined(_WIN32)
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
throw std::system_error(errno, std::generic_category(), "poll");
|
||||
#endif
|
||||
socket_throw("poll");
|
||||
}
|
||||
if (rc > 0) {
|
||||
if (fds[udp_idx].revents & POLLIN) {
|
||||
if (fds[udp_idx].revents & kPollIn) {
|
||||
drain_udp();
|
||||
}
|
||||
if (fds[tun_idx].revents & POLLIN) {
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
if (tun_idx != kNoIndex && (fds[tun_idx].revents & kPollIn)) {
|
||||
drain_tun();
|
||||
}
|
||||
if (upstream_ && (fds[up_idx].revents & POLLIN)) {
|
||||
#endif
|
||||
if (up_idx != kNoIndex && (fds[up_idx].revents & kPollIn)) {
|
||||
drain_upstream();
|
||||
}
|
||||
for (std::size_t i = 0; i < relay_ids.size(); ++i) {
|
||||
if (fds[relay_base + i].revents & kPollIn) {
|
||||
const auto it = relay_socks_.find(relay_ids[i]);
|
||||
if (it != relay_socks_.end()) {
|
||||
drain_relay(relay_ids[i], it->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
expire_relays();
|
||||
}
|
||||
|
|
@ -148,32 +203,119 @@ void Server::handle_tunnel(std::uint64_t request_transmit_ts,
|
|||
state.has_src = true;
|
||||
state.last_request_ts = request_transmit_ts;
|
||||
|
||||
if (carrier.kind == kKindIpDatagram) {
|
||||
if (tun_.write_packet(carrier.inner_ip.data(), carrier.inner_ip.size()) < 0) {
|
||||
log_debug("tun write would block; inner datagram dropped");
|
||||
}
|
||||
// Source-learn a route back to this client for downstream traffic.
|
||||
const auto src = ip::source(carrier.inner_ip);
|
||||
if (src) {
|
||||
if (routes_.size() >= kMaxRoutes && routes_.find(*src) == routes_.end()) {
|
||||
log_warn("route table full; not learning new route");
|
||||
} else {
|
||||
routes_[*src] = carrier.client_id;
|
||||
if (cfg_.common.transport == Transport::Udp) {
|
||||
if (carrier.kind == kKindUdpDatagram && !carrier.inner_ip.empty()) {
|
||||
UdpSocket* r = relay_for(carrier.client_id);
|
||||
if (r && r->send(carrier.inner_ip.data(), carrier.inner_ip.size()) < 0) {
|
||||
log_debug("relay send would block; payload dropped");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
if (carrier.kind == kKindIpDatagram) {
|
||||
if (tun_.write_packet(carrier.inner_ip.data(), carrier.inner_ip.size()) < 0) {
|
||||
log_debug("tun write would block; inner datagram dropped");
|
||||
}
|
||||
// Source-learn a route back to this client for downstream traffic.
|
||||
const auto src = ip::source(carrier.inner_ip);
|
||||
if (src) {
|
||||
if (routes_.size() >= kMaxRoutes && routes_.find(*src) == routes_.end()) {
|
||||
log_warn("route table full; not learning new route");
|
||||
} else {
|
||||
routes_[*src] = carrier.client_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Answer with exactly one response, carrying a queued datagram if available.
|
||||
std::uint8_t kind = kKindEmptyPoll;
|
||||
ByteVector payload;
|
||||
if (!state.downstream.empty()) {
|
||||
kind = kKindIpDatagram;
|
||||
kind = (cfg_.common.transport == Transport::Udp) ? kKindUdpDatagram
|
||||
: kKindIpDatagram;
|
||||
payload = std::move(state.downstream.front());
|
||||
state.downstream.pop_front();
|
||||
}
|
||||
send_response(carrier.client_id, request_transmit_ts, kind, payload, from);
|
||||
}
|
||||
|
||||
UdpSocket* Server::relay_for(std::uint16_t client_id) {
|
||||
const auto existing = relay_socks_.find(client_id);
|
||||
if (existing != relay_socks_.end()) {
|
||||
relay_last_[client_id] = std::chrono::steady_clock::now();
|
||||
return &existing->second;
|
||||
}
|
||||
|
||||
const std::uint32_t port =
|
||||
static_cast<std::uint32_t>(cfg_.udp_base_port) + client_id;
|
||||
if (port > 65535u) {
|
||||
log_error("relay: source port ", port, " out of range for client ", client_id);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Evict the least-recently-used relay socket when the table is full.
|
||||
if (relay_socks_.size() >= kMaxRelaySockets && !relay_last_.empty()) {
|
||||
const auto lru = std::min_element(
|
||||
relay_last_.begin(), relay_last_.end(),
|
||||
[](const auto& a, const auto& b) { return a.second < b.second; });
|
||||
const std::uint16_t victim = lru->first;
|
||||
relay_socks_.erase(victim);
|
||||
relay_last_.erase(victim);
|
||||
log_debug("relay: evicted LRU socket for client ", victim);
|
||||
}
|
||||
|
||||
try {
|
||||
UdpSocket s = UdpSocket::bind(
|
||||
Endpoint::wildcard(cfg_.udp_target.family(), static_cast<std::uint16_t>(port)));
|
||||
s.connect(cfg_.udp_target);
|
||||
s.set_nonblocking(true);
|
||||
const auto inserted = relay_socks_.emplace(client_id, std::move(s));
|
||||
relay_last_[client_id] = std::chrono::steady_clock::now();
|
||||
return &inserted.first->second;
|
||||
} catch (const std::exception& e) {
|
||||
log_error("relay: cannot open socket for client ", client_id, " on port ",
|
||||
port, ": ", e.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Server::drain_relay(std::uint16_t client_id, UdpSocket& sock) {
|
||||
std::array<Byte, kBufSize> buf{};
|
||||
for (;;) {
|
||||
const ssize_t n = sock.recv(buf.data(), buf.size());
|
||||
if (n < 0) {
|
||||
break;
|
||||
}
|
||||
relay_last_[client_id] = std::chrono::steady_clock::now();
|
||||
if (static_cast<std::size_t>(n) > cfg_.common.inner_mtu) {
|
||||
log_error("relay: dropping oversized reply of ", n, " bytes for client ",
|
||||
client_id, " (max payload is ", cfg_.common.inner_mtu,
|
||||
"); raise inner_mtu (and ntp_payload_cap >= inner_mtu+64), "
|
||||
"e.g. inner_mtu >= 1200 for QUIC/Hysteria2");
|
||||
continue;
|
||||
}
|
||||
queue_downstream(client_id, kKindUdpDatagram,
|
||||
ByteSpan(buf.data(), static_cast<std::size_t>(n)));
|
||||
}
|
||||
}
|
||||
|
||||
void Server::queue_downstream(std::uint16_t client_id, std::uint8_t kind,
|
||||
ByteSpan payload) {
|
||||
ClientState& state = clients_[client_id];
|
||||
if (cfg_.common.push_mode && state.has_src) {
|
||||
// Push immediately to the client's last known address instead of waiting
|
||||
// for a poll (breaks strict NTP request/response semantics).
|
||||
send_response(client_id, state.last_request_ts, kind, payload, state.last_src);
|
||||
return;
|
||||
}
|
||||
state.downstream.emplace_back(payload.begin(), payload.end());
|
||||
while (state.downstream.size() > cfg_.max_queue_per_client) {
|
||||
state.downstream.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
void Server::send_response(std::uint16_t client_id, std::uint64_t request_transmit_ts,
|
||||
std::uint8_t kind, ByteSpan payload, const Endpoint& from) {
|
||||
const auto key = cfg_.common.keys.resolve(client_id);
|
||||
|
|
@ -207,6 +349,7 @@ void Server::send_response(std::uint16_t client_id, std::uint64_t request_transm
|
|||
}
|
||||
|
||||
void Server::drain_tun() {
|
||||
#ifdef NTPTUN_HAVE_TUN
|
||||
std::array<Byte, kBufSize> buf{};
|
||||
for (;;) {
|
||||
const ssize_t n = tun_.read_packet(buf.data(), buf.size());
|
||||
|
|
@ -240,19 +383,9 @@ void Server::drain_tun() {
|
|||
log_debug("no route for inner destination; dropping downstream datagram");
|
||||
continue;
|
||||
}
|
||||
ClientState& state = clients_[route->second];
|
||||
if (cfg_.common.push_mode && state.has_src) {
|
||||
// Push immediately to the client's last known address instead of
|
||||
// waiting for a poll (breaks strict NTP request/response semantics).
|
||||
send_response(route->second, state.last_request_ts, kKindIpDatagram,
|
||||
pkt, state.last_src);
|
||||
continue;
|
||||
}
|
||||
state.downstream.emplace_back(pkt.begin(), pkt.end());
|
||||
while (state.downstream.size() > cfg_.max_queue_per_client) {
|
||||
state.downstream.pop_front();
|
||||
}
|
||||
queue_downstream(route->second, kKindIpDatagram, pkt);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Server::relay(ByteSpan packet, const NtpHeader& header, const Endpoint& from) {
|
||||
|
|
|
|||
|
|
@ -1,48 +1,38 @@
|
|||
#include "ntptun/UdpSocket.hpp"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <system_error>
|
||||
|
||||
#include "ntptun/Platform.hpp"
|
||||
|
||||
namespace ntptun {
|
||||
namespace {
|
||||
|
||||
[[noreturn]] void throw_errno(const char* what) {
|
||||
throw std::system_error(errno, std::generic_category(), what);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UdpSocket::~UdpSocket() { close_fd(); }
|
||||
|
||||
UdpSocket::UdpSocket(UdpSocket&& other) noexcept : fd_(other.fd_) {
|
||||
other.fd_ = -1;
|
||||
other.fd_ = kInvalidSocket;
|
||||
}
|
||||
|
||||
UdpSocket& UdpSocket::operator=(UdpSocket&& other) noexcept {
|
||||
if (this != &other) {
|
||||
close_fd();
|
||||
fd_ = other.fd_;
|
||||
other.fd_ = -1;
|
||||
other.fd_ = kInvalidSocket;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void UdpSocket::close_fd() noexcept {
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
if (fd_ != kInvalidSocket) {
|
||||
socket_close(fd_);
|
||||
fd_ = kInvalidSocket;
|
||||
}
|
||||
}
|
||||
|
||||
UdpSocket UdpSocket::create(int family) {
|
||||
const int fd = ::socket(family, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (fd < 0) {
|
||||
throw_errno("socket");
|
||||
net_startup();
|
||||
const socket_t fd = ::socket(family, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (fd == kInvalidSocket) {
|
||||
socket_throw("socket");
|
||||
}
|
||||
return UdpSocket(fd);
|
||||
}
|
||||
|
|
@ -50,44 +40,35 @@ UdpSocket UdpSocket::create(int family) {
|
|||
UdpSocket UdpSocket::bind(const Endpoint& local) {
|
||||
UdpSocket sock = create(local.family());
|
||||
const int one = 1;
|
||||
::setsockopt(sock.fd_, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
::setsockopt(sock.fd_, SOL_SOCKET, SO_REUSEADDR,
|
||||
reinterpret_cast<const char*>(&one), sizeof(one));
|
||||
if (::bind(sock.fd_, local.sockaddr_ptr(), local.length()) != 0) {
|
||||
throw_errno("bind");
|
||||
socket_throw("bind");
|
||||
}
|
||||
return sock;
|
||||
}
|
||||
|
||||
void UdpSocket::connect(const Endpoint& remote) {
|
||||
if (::connect(fd_, remote.sockaddr_ptr(), remote.length()) != 0) {
|
||||
throw_errno("connect");
|
||||
socket_throw("connect");
|
||||
}
|
||||
}
|
||||
|
||||
void UdpSocket::set_nonblocking(bool enabled) {
|
||||
int flags = ::fcntl(fd_, F_GETFL, 0);
|
||||
if (flags < 0) {
|
||||
throw_errno("fcntl(F_GETFL)");
|
||||
}
|
||||
if (enabled) {
|
||||
flags |= O_NONBLOCK;
|
||||
} else {
|
||||
flags &= ~O_NONBLOCK;
|
||||
}
|
||||
if (::fcntl(fd_, F_SETFL, flags) < 0) {
|
||||
throw_errno("fcntl(F_SETFL)");
|
||||
}
|
||||
socket_set_nonblocking(fd_, enabled);
|
||||
}
|
||||
|
||||
ssize_t UdpSocket::recv_from(Byte* buf, std::size_t len, Endpoint& from) {
|
||||
sockaddr_storage src{};
|
||||
socklen_t src_len = sizeof(src);
|
||||
const ssize_t n = ::recvfrom(fd_, buf, len, 0,
|
||||
reinterpret_cast<sockaddr*>(&src), &src_len);
|
||||
const ssize_t n =
|
||||
::recvfrom(fd_, reinterpret_cast<char*>(buf), static_cast<int>(len), 0,
|
||||
reinterpret_cast<sockaddr*>(&src), &src_len);
|
||||
if (n < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
if (socket_would_block()) {
|
||||
return -1;
|
||||
}
|
||||
throw_errno("recvfrom");
|
||||
socket_throw("recvfrom");
|
||||
}
|
||||
std::memcpy(from.sockaddr_ptr(), &src, src_len);
|
||||
from.set_length(src_len);
|
||||
|
|
@ -95,34 +76,38 @@ ssize_t UdpSocket::recv_from(Byte* buf, std::size_t len, Endpoint& from) {
|
|||
}
|
||||
|
||||
ssize_t UdpSocket::send_to(const Byte* buf, std::size_t len, const Endpoint& to) {
|
||||
const ssize_t n = ::sendto(fd_, buf, len, 0, to.sockaddr_ptr(), to.length());
|
||||
const ssize_t n =
|
||||
::sendto(fd_, reinterpret_cast<const char*>(buf), static_cast<int>(len), 0,
|
||||
to.sockaddr_ptr(), to.length());
|
||||
if (n < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
if (socket_would_block()) {
|
||||
return -1;
|
||||
}
|
||||
throw_errno("sendto");
|
||||
socket_throw("sendto");
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
ssize_t UdpSocket::recv(Byte* buf, std::size_t len) {
|
||||
const ssize_t n = ::recv(fd_, buf, len, 0);
|
||||
const ssize_t n =
|
||||
::recv(fd_, reinterpret_cast<char*>(buf), static_cast<int>(len), 0);
|
||||
if (n < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
if (socket_would_block()) {
|
||||
return -1;
|
||||
}
|
||||
throw_errno("recv");
|
||||
socket_throw("recv");
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
ssize_t UdpSocket::send(const Byte* buf, std::size_t len) {
|
||||
const ssize_t n = ::send(fd_, buf, len, 0);
|
||||
const ssize_t n =
|
||||
::send(fd_, reinterpret_cast<const char*>(buf), static_cast<int>(len), 0);
|
||||
if (n < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
if (socket_would_block()) {
|
||||
return -1;
|
||||
}
|
||||
throw_errno("send");
|
||||
socket_throw("send");
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,17 @@ int main(int argc, char** argv) {
|
|||
: cfg.server->common.log_level;
|
||||
log_level() = level;
|
||||
|
||||
#if defined(_WIN32)
|
||||
std::signal(SIGINT, on_signal);
|
||||
std::signal(SIGTERM, on_signal);
|
||||
#else
|
||||
struct sigaction sa {};
|
||||
sa.sa_handler = on_signal;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(SIGINT, &sa, nullptr);
|
||||
sigaction(SIGTERM, &sa, nullptr);
|
||||
std::signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
if (cfg.mode == Mode::Client) {
|
||||
Client client(std::move(*cfg.client));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue