From 553a98b7bc85dfacad3d7d43c7e853047338ba0b Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Thu, 22 Oct 2026 20:42:27 +0000 Subject: [PATCH 1/5] Always wait for a default route before starting (#73) --- .github/workflows/build.yml | 3 +++ bench/bench.sh | 38 +++++++++++++++++++++++++++ cli/vtesse_cli.c | 51 +++++++++++++++++++++++++------------ docs/usage.md | 15 ++++++++--- 4 files changed, 87 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7cb868..53cba63 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -152,6 +152,9 @@ jobs: - name: Failure scenarios (link loss, server restart, no option 121, lease renewal) run: bench/bench.sh fault-test + - name: Default-route wait before the start (#73) + run: bench/bench.sh route-wait-test + - name: Server log if: always() run: docker logs vtesse-bench-server 2>&1 | tail -100 || true diff --git a/bench/bench.sh b/bench/bench.sh index 84245f1..6b5f616 100755 --- a/bench/bench.sh +++ b/bench/bench.sh @@ -14,6 +14,7 @@ # bench.sh tun-test type tun session (#8): manual IPv4, lwIP and --tap (TUN interface) # bench.sh udp-test UDP relay (#28): datagrams from the device to the tunnel address -> 127.0.0.1 # bench.sh fault-test failures (#9): link loss, server restart, no option 121, lease renewal (~6 min) +# bench.sh route-wait-test the client waits for a default route before it starts (#73) set -eu IMAGE=${BENCH_IMAGE:-vtesse-bench} @@ -233,6 +234,42 @@ cmd_tun_test() { [ "$fails" -eq 0 ] } +# Default-route wait (#73): in a container without a network the client must wait, not +# connect, and go on as soon as a default route appears; SIGTERM during the wait is exit 0. +cmd_route_wait_test() { + repo=$(cd "$HERE/.." && pwd) + c=vtesse-bench-routewait + fails=0 + check() { if eval "$2"; then echo "PASS $1"; else echo "FAIL $1"; fails=$((fails + 1)); fi; } + # built for the bench image (Debian bookworm), like tun-test + docker build -q -t vtesse-ci-native -f "$repo/ci/Dockerfile.native" "$repo/ci" >/dev/null + docker run --rm --user "$(id -u):$(id -g)" -v "$repo:/src" -w /src vtesse-ci-native \ + make -s BUILD=build-bench-wait all >/dev/null + conf='printf "host=10.9.9.9\nport=5000\nsession=02BE0C000001\npassword=x\n" >/tmp/c.conf' + run="exec /src/build-bench-wait/vtesse-cli --no-env --config /tmp/c.conf" + + docker rm -f "$c" >/dev/null 2>&1 || true + docker run -d --name "$c" --network none -v "$repo:/src:ro" "$IMAGE" sh -c "$conf; $run" >/dev/null + sleep 3 + check "no default route: waiting, still running, not connecting" \ + 'docker logs "$c" 2>&1 | grep -q "^waiting for a default route" && + [ "$(docker inspect -f "{{.State.Running}}" "$c")" = true ] && ! docker logs "$c" 2>&1 | grep -q connecting' + docker stop -t 5 "$c" >/dev/null + check "SIGTERM during the wait: exit 0" '[ "$(docker inspect -f "{{.State.ExitCode}}" "$c")" = 0 ]' + docker rm -f "$c" >/dev/null 2>&1 + + docker run -d --name "$c" --privileged --network none -v "$repo:/src:ro" "$IMAGE" sh -c "$conf; $run" >/dev/null + sleep 2 + docker exec "$c" sh -c "ip link set lo up; ip route add default dev lo" + sleep 3 + check "a default route appears: the wait ends and the client connects" \ + 'docker logs "$c" 2>&1 | grep -q "^default route present after" && docker logs "$c" 2>&1 | grep -q connecting' + echo "--- client log"; docker logs "$c" 2>&1 | tail -5 + docker rm -f "$c" >/dev/null 2>&1 + echo "$fails failure(s)" + [ "$fails" -eq 0 ] +} + # UDP relay (#28): the "device" behind the server pushes numbered RTP-like # datagrams to the client's tunnel address; vtesse-cli --udp-relay relays them to # a listener on 127.0.0.1. Checks count, order, content and a fragmented datagram. @@ -386,6 +423,7 @@ case "${1:-}" in tun-test) cmd_tun_test ;; udp-test) cmd_udp_test ;; fault-test) cmd_fault_test ;; + route-wait-test) cmd_route_wait_test ;; proxy-test) python3 "$HERE/proxy_test.py" "$HERE/../build/vtesse-cli" "$BENCH_PORT" "$SERVER" ;; selftest) python3 "$HERE/selftest.py" 127.0.0.1 "$BENCH_PORT" ;; *) sed -n '2,16p' "$0"; exit 1 ;; diff --git a/cli/vtesse_cli.c b/cli/vtesse_cli.c index 6f5335c..bbbc99e 100644 --- a/cli/vtesse_cli.c +++ b/cli/vtesse_cli.c @@ -86,7 +86,7 @@ void nc_udp_relay_destroy(nc_udp_relay_t *r) #endif #define DEFAULT_CONFIG "~/.config/vtesse/probe.conf" -#define ENV_ROUTE_WAIT 60 /* seconds: environment mode waits for the uplink at boot */ +#define ROUTE_WAIT_LOG_S 60 /* seconds between the "still waiting" lines (#73) */ typedef struct cfg_file { char host[256]; @@ -113,6 +113,27 @@ static void on_signal(int sig) loop.stop = 1; } +/* 0 when there is a default route; -1 when a signal stopped the wait (#73) */ +static int wait_default_route(void) +{ + unsigned waited = 0; + + while (!ve_has_default_route()) { + if (loop.stop) + return -1; + if (!waited) + printf("waiting for a default route\n"); + else if (waited % ROUTE_WAIT_LOG_S == 0) + printf("still waiting for a default route (%u min)\n", waited / 60); + fflush(stdout); + sleep(1); + waited++; + } + if (waited) + printf("default route present after %u s\n", waited); + return 0; +} + static void usage(void) { fprintf(stderr, @@ -747,7 +768,7 @@ int main(int argc, char **argv) uint8_t mac[6]; tunnel_t t; nc_source_t src; - int i, err, rc, use_tap = !VTESSE_WITH_LWIP, lwip_opts = 0; + int i, err, rc, use_tap = !VTESSE_WITH_LWIP, lwip_opts = 0, stop_early; #if VTESSE_WITH_LWIP nc_stack_cfg_t sc; nc_proxy_cfg_t pc; @@ -888,24 +909,20 @@ int main(int argc, char **argv) } if (!no_env && env_any(&env)) apply_env(&c, &env); + /* without a default route there is nothing to connect to (#73): wait for one in every + * start mode, with no time limit. At boot the uplink (wlan0 with a background udhcpc) + * may get it after the service starts; the MAC is then the uplink's, not eth0's. Any + * default route ends the wait: through PPPoE or tun (no MAC) eth0 is taken as before. */ + if (!show_config) { + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); + if (wait_default_route() < 0) + return 0; /* stopped while waiting */ + } if (env_mode) { /* only the server given: the VTun defaults of OpenIPC's tunnel */ if (!c.port) c.port = 5000; - /* at boot the uplink (wlan0 with a background udhcpc) may get its default - * route after the service starts: the MAC must be the uplink's, not eth0's */ - if (!show_config && (!c.password[0] || (!c.session[0] && !c.id_from[0] && !id_from))) { - int waited; - - /* any default route ends the wait: through PPPoE or tun (no MAC) eth0 is - * taken at once */ - for (waited = 0; waited < ENV_ROUTE_WAIT && !ve_has_default_route(); waited++) { - if (!waited) - printf("waiting up to %d s for a default route (the MAC for the VTun defaults)\n", - ENV_ROUTE_WAIT); - sleep(1); - } - } if (!c.session[0] && !c.id_from[0] && !id_from && ve_default_iface(c.id_from, sizeof(c.id_from)) < 0) { fprintf(stderr, "no interface with a MAC for the device ID: set vtesse_session or vtesse_id_from\n"); @@ -1036,7 +1053,9 @@ int main(int argc, char **argv) signal(SIGINT, on_signal); signal(SIGTERM, on_signal); signal(SIGPIPE, SIG_IGN); + stop_early = loop.stop; /* a signal after the route wait (#73) is kept */ nc_loop_init(&loop); + loop.stop = stop_early; src = nc_source_from_tunnel(&t); nc_loop_add(&loop, &src); diff --git a/docs/usage.md b/docs/usage.md index e13827b..2e2dd73 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -100,6 +100,14 @@ The CLI brings the tunnel and the userspace stack up, prints link events, the DH address and routes, runs the local proxy and prints proxy statistics on exit. Exit code 0 means the tunnel was up at the end (and `--fetch`, if given, succeeded). +Before it starts, the client waits for a default route (any interface): without one +there is nothing to connect to. The wait has no time limit and cannot be turned off; +it prints `waiting for a default route`, then a line every minute, and +`default route present after N s` when one appears. SIGINT/SIGTERM end the wait +(exit code 0). `--show-config` and `--install` do not wait. At boot this covers an +uplink that gets its route after the service starts (Wi-Fi with a background DHCP +client). + ```sh vtesse-cli --socks 1080 --forward 8554:10.78.0.10:554 curl --socks5 127.0.0.1:1080 http://10.78.0.10/ @@ -228,10 +236,9 @@ so it reaches the same server with the same credentials: - session = that MAC without colons (the [device ID](#device-id)); - password = `echo "$MAC" | sha1sum`: SHA-1 of the MAC with colons **and a newline**. -At boot the uplink may get its default route after the service starts (Wi-Fi with a -background DHCP client): the client then waits up to 60 s for it before it falls back -to `eth0`. `vtesse_session` and `vtesse_id_from` exclude each other; with both set the -client refuses to start. +The client first [waits for a default route](#vtesse-cli); a route through an interface +without a MAC (PPPoE, a tunnel) means `eth0`. `vtesse_session` and `vtesse_id_from` +exclude each other; with both set the client refuses to start. Optional variables override these and the file, one key at a time: From 77b9b649e00c4591c13ce7ba129e707e3783bd13 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Thu, 22 Oct 2026 21:01:20 +0000 Subject: [PATCH 2/5] Userspace only: inbound forwards, ip=learn, TAP backend removed (#67, #68, #72) --- .github/workflows/build.yml | 15 +- Makefile | 28 +- README.md | 86 ++--- README.ru.md | 86 ++--- README.zh-CN.md | 86 ++--- android/app/src/main/cpp/CMakeLists.txt | 1 + bench/README.md | 24 +- bench/bench.sh | 150 ++++---- ci/build-musl.sh | 6 +- cli/install.c | 245 ++++-------- cli/install.h | 10 +- cli/vtesse_cli.c | 490 +++++++----------------- docs/architecture.md | 85 ++-- docs/protocol.md | 3 +- docs/security.md | 10 +- docs/testing.md | 27 +- docs/usage.md | 227 +++++------ include/netcore/hostaddr.h | 14 + include/netcore/proxy.h | 26 +- include/netcore/stack.h | 4 +- include/netcore/tapdev.h | 50 --- netcore/hostaddr.c | 44 +++ netcore/proxy.c | 167 +++++++- netcore/stack.c | 37 ++ netcore/tapdev.c | 257 ------------- port/lwip/lwipopts.h | 2 +- scripts/S99vtesse | 9 +- scripts/vtesse-hook.sh | 102 ----- scripts/vtesse-udhcpc.script | 63 --- scripts/vtesse.init | 8 +- scripts/vtesse.service | 8 +- tests/test_install.sh | 72 ++-- tools/embed-scripts.sh | 2 +- 33 files changed, 966 insertions(+), 1478 deletions(-) create mode 100644 include/netcore/hostaddr.h delete mode 100644 include/netcore/tapdev.h create mode 100644 netcore/hostaddr.c delete mode 100644 netcore/tapdev.c delete mode 100755 scripts/vtesse-hook.sh delete mode 100755 scripts/vtesse-udhcpc.script diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53cba63..8865df0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,11 +64,8 @@ jobs: CFLAGS="-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer" \ LDFLAGS="-fsanitize=address,undefined" test - - name: TAP-only build (LWIP=0) - run: make -j"$(nproc)" BUILD=build-tap LWIP=0 all - - - name: Minimal lwIP build (SOCKS=0 UDP_RELAY=0 TAP=0) and its tests - run: make -j"$(nproc)" BUILD=build-min SOCKS=0 UDP_RELAY=0 TAP=0 test + - name: Minimal build (SOCKS=0 UDP_RELAY=0) and its tests + run: make -j"$(nproc)" BUILD=build-min SOCKS=0 UDP_RELAY=0 test # ------------------------------------------------------------------ cross build: @@ -140,10 +137,10 @@ jobs: - name: Stage 2 acceptance (SOCKS5, port forwards) run: bench/bench.sh proxy-test - - name: TAP backend acceptance - run: bench/bench.sh tap-test + - name: Inbound forwards and ip=learn acceptance (#67, #68) + run: bench/bench.sh accept-test - - name: type tun acceptance (manual IPv4, lwIP and TAP) + - name: type tun acceptance (manual IPv4) run: bench/bench.sh tun-test - name: UDP relay acceptance (RTP from cameras) @@ -234,7 +231,7 @@ jobs: apk=$(find artifacts -type f -name app-release.apk | head -1) [ -n "$apk" ] || { echo "::error::no signed release APK"; exit 1; } cp "$apk" release/vtesse-android.apk - cp scripts/vtesse-hook.sh scripts/vtesse-udhcpc.script scripts/S99vtesse scripts/vtesse.init release/ + cp scripts/S99vtesse scripts/vtesse.init release/ ls -l release # Only the head of main is published: a re-run of an older run, or a run that diff --git a/Makefile b/Makefile index b59aa9f..21453d5 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,8 @@ # VTesse build. # make CROSS_COMPILE=mipsel-linux-musl- STATIC=1 cross compile, static binary -# make LWIP=0 TAP backend only (no stack, no proxy) # make LWIP_PROFILE=medium lwIP memory profile: large (default), # medium (cameras, routers), small -# make SOCKS=0 UDP_RELAY=0 TAP=0 leave out optional modules (default 1) +# make SOCKS=0 UDP_RELAY=0 leave out optional modules (default 1) # (use a separate BUILD=... directory per variant: objects are not rebuilt on a change) CROSS_COMPILE ?= ifneq ($(CROSS_COMPILE),) @@ -28,22 +27,16 @@ LWIP_CFLAGS := -std=gnu99 -Wall -Wno-unused-parameter -Wno-address \ -ffunction-sections -fdata-sections $(INCLUDES) LDFLAGS ?= BUILD ?= build -LWIP ?= 1 # lwIP memory profile (port/lwip/lwipopts.h): large (PCs, Android), medium (cameras, # routers), small LWIP_PROFILE ?= large -# optional modules of the lwIP build (1 = in, 0 = out): SOCKS5 server, UDP relay, -# TAP/TUN backend (kernel interface; Linux only) +# optional modules (1 = in, 0 = out): SOCKS5 server, UDP relay. The TAP backend +# (kernel interface) and LWIP=0 were removed in 2.0.0 (#72). SOCKS ?= 1 UDP_RELAY ?= 1 -TAP ?= 1 -NETCORE_SRC := netcore/tunnel.c netcore/loop.c netcore/route.c -ifeq ($(TAP),1) -NETCORE_SRC += netcore/tapdev.c -else -BASE_CFLAGS += -DVTESSE_WITH_TAP=0 -endif +NETCORE_SRC := netcore/tunnel.c netcore/loop.c netcore/route.c netcore/hostaddr.c \ + netcore/stack.c netcore/proxy.c VTUN_SRC := vtun/vtun_proto.c vtun/vtun_client.c LWIP_SRC := $(addprefix $(LWIP_DIR)/src/core/, def.c inet_chksum.c init.c ip.c mem.c \ memp.c netif.c pbuf.c stats.c sys.c tcp.c tcp_in.c tcp_out.c timeouts.c udp.c) \ @@ -54,8 +47,6 @@ LWIP_PROFILE_NUM := $(if $(filter small,$(LWIP_PROFILE)),0,$(if $(filter medium, BASE_CFLAGS += -DVTESSE_LWIP_PROFILE=$(LWIP_PROFILE_NUM) LWIP_CFLAGS += -DVTESSE_LWIP_PROFILE=$(LWIP_PROFILE_NUM) -ifeq ($(LWIP),1) -NETCORE_SRC += netcore/stack.c netcore/proxy.c BASE_CFLAGS += -DNC_WITH_SOCKS=$(SOCKS) ifeq ($(UDP_RELAY),1) NETCORE_SRC += netcore/udprelay.c @@ -63,15 +54,10 @@ else BASE_CFLAGS += -DVTESSE_WITH_UDP_RELAY=0 endif NETCORE_OBJ := $(NETCORE_SRC:%.c=$(BUILD)/%.o) $(LWIP_SRC:%.c=$(BUILD)/%.o) -else -BASE_CFLAGS += -DVTESSE_WITH_LWIP=0 -NETCORE_OBJ := $(NETCORE_SRC:%.c=$(BUILD)/%.o) -endif VTUN_OBJ := $(VTUN_SRC:%.c=$(BUILD)/%.o) -# scripts/ built into the binary: the default hook and "vtesse --install" (#52) -SCRIPTS := scripts/vtesse-hook.sh scripts/vtesse-udhcpc.script scripts/S99vtesse \ - scripts/vtesse.init scripts/vtesse.service +# scripts/ built into the binary: the service scripts of "vtesse --install" (#52) +SCRIPTS := scripts/S99vtesse scripts/vtesse.init scripts/vtesse.service SCRIPTS_H := $(BUILD)/gen/vtesse_scripts.h CLI_OBJ := $(BUILD)/cli/vtesse_cli.o $(BUILD)/cli/install.o $(BUILD)/cli/env.o diff --git a/README.md b/README.md index 2f7adcb..55b4450 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ A resource access microsystem: a client for existing **VTun** servers (`vtund -s - **No VPN:** no root, no `/dev/net/tun`, no Android `VpnService` (no VPN icon) - **VTun:** 3.0.x servers, TCP transport, `type ether` and `type tun` sessions, XOR challenge response, keepalive, automatic reconnection with exponential back-off -- **Network:** address and routes via DHCP through the tunnel (option 121, RFC 3442), IPv4, outbound connections only +- **Network:** address and routes via DHCP through the tunnel (option 121, RFC 3442), or on `type tun` set by hand or learnt from the first packet the server sends (`ip=learn`); IPv4 +- **Inbound access:** connections from the server to the tunnel address reach services of the device itself (`accept=80`): a camera's web interface, ssh and RTSP, still without a kernel interface - **UDP relay:** RTP pushed by cameras to the tunnel address reaches a local player (`--udp-relay`) - **Local proxy:** SOCKS5 `CONNECT` with optional username/password, static TCP port forwards (e.g. RTSP over TCP for video players) - **Targets:** embedded Linux (IP cameras, routers; ARM and MIPS, including big-endian, uClibc/musl), Android, and later an embeddable communication module @@ -37,7 +38,7 @@ Static binaries (no dependencies) are on the [releases page](https://github.com/ | 64-bit ARM: Raspberry Pi with a 64-bit OS, Rockchip, other ARM64 boards | `arm64` | | PCs and servers | `x86_64` | -One binary per platform, `vtesse-ARCH`, with both backends: lwIP (SOCKS5 / port forwards without root) and TAP (a kernel interface with `--tap`). The TAP backend needs root and the `tun` driver (`/dev/net/tun`); the hook scripts `vtesse-hook.sh` and `vtesse-udhcpc.script` are built into the binary. Until 1.12 a TAP-only `vtesse-tap-ARCH` was published too; its links now return 404, use `vtesse-ARCH --tap` instead. +One binary per platform, `vtesse-ARCH`: the userspace stack with the local proxy and inbound forwards, no root needed. The TAP backend (a kernel interface with `--tap`) was removed in 2.0.0 ([#72](https://github.com/OpenIPC/vtesse/issues/72)): `--tap` is refused, and the hook scripts `vtesse-hook.sh` and `vtesse-udhcpc.script` are no longer published (their links return 404). The releases also carry the VTun server for PCs and servers, `vtund-3.0.2-x86_64`: the original VTun 3.0.2 with our patches (a refused login is logged with the device ID and the reason), see [server/README.md](server/README.md). For cameras and routers vtund is built in the OpenIPC environment when needed. @@ -45,19 +46,17 @@ The releases also carry the VTun server for PCs and servers, `vtund-3.0.2-x86_64 There is one release, [`latest`](https://github.com/OpenIPC/vtesse/releases/latest), titled `VTesse `; every push to `main` rebuilds it, and no versioned releases are kept. File names carry no version (only the VTun server's follows the VTun version), so these links never change: -| File | What it is | Permanent link | -|------------------------|--------------------------------------------------------------|-----------------------------------------------------------------------------------| -| `vtesse-arm32` | client, 32-bit ARM | | -| `vtesse-mipsel` | client, MIPS little-endian | | -| `vtesse-mips` | client, MIPS big-endian | | -| `vtesse-arm64` | client, 64-bit ARM | | -| `vtesse-x86_64` | client, PCs and servers | | -| `vtesse-android.apk` | Android app | | -| `vtund-3.0.2-x86_64` | VTun server with our patches, PCs (follows the VTun version) | | -| `vtesse-hook.sh` | TAP hook script (built into the binary; for `--hook FILE`) | | -| `vtesse-udhcpc.script` | udhcpc script for the TAP interface (built in) | | -| `S99vtesse` | init script, OpenIPC (BusyBox; `--install` writes it) | | -| `vtesse.init` | init script, OpenWrt (procd; `--install` writes it) | | +| File | What it is | Permanent link | +|----------------------|--------------------------------------------------------------|---------------------------------------------------------------------------------| +| `vtesse-arm32` | client, 32-bit ARM | | +| `vtesse-mipsel` | client, MIPS little-endian | | +| `vtesse-mips` | client, MIPS big-endian | | +| `vtesse-arm64` | client, 64-bit ARM | | +| `vtesse-x86_64` | client, PCs and servers | | +| `vtesse-android.apk` | Android app | | +| `vtund-3.0.2-x86_64` | VTun server with our patches, PCs (follows the VTun version) | | +| `S99vtesse` | init script, OpenIPC (BusyBox; `--install` writes it) | | +| `vtesse.init` | init script, OpenWrt (procd; `--install` writes it) | | ```sh curl -fsSLO https://github.com/OpenIPC/vtesse/releases/latest/download/vtesse-arm32 && @@ -66,7 +65,7 @@ curl -fsSLO https://github.com/OpenIPC/vtesse/releases/latest/download/vtesse-ar ### Install as a service: OpenIPC camera, OpenWrt router, Linux PC -As root: download the binary for your `ARCH` (see the table) and let it install itself. It detects the system, copies itself to `/usr/bin/vtesse` (`/usr/local/bin` on a PC), writes `/etc/vtesse.conf` (mode 600) from the keys and the init script (OpenIPC `S99vtesse`, OpenWrt procd `vtesse.init`, systemd `vtesse.service`), and starts the service at once and at every boot. The hook scripts are built in. On OpenWrt the installation survives `sysupgrade`. +As root: download the binary for your `ARCH` (see the table) and let it install itself. It detects the system, copies itself to `/usr/bin/vtesse` (`/usr/local/bin` on a PC), writes `/etc/vtesse.conf` (mode 600) from the keys and the init script (OpenIPC `S99vtesse`, OpenWrt procd `vtesse.init`, systemd `vtesse.service`), and starts the service at once and at every boot. On OpenWrt the installation survives `sysupgrade`. The service runs the userspace stack; the server reaches the device's web interface, ssh and RTSP on its tunnel address: `--install` writes `accept=80`, `accept=22` and `accept=554` to `/etc/vtesse.conf` when it has no `accept` line ([inbound forwards](docs/usage.md#inbound-forwards)), and the connections go to the device's own LAN address, so its web interface asks for the password as on the LAN. ```sh ARCH=arm32 # arm32 or mipsel on cameras, mipsel / mips / arm64 on routers, x86_64 on a PC @@ -79,10 +78,10 @@ curl -fsSL -o /tmp/vtesse https://github.com/OpenIPC/vtesse/releases/latest/down - `--password -` reads the password from standard input instead of the command line. Without `--port` the port is 5000. - An existing `/etc/vtesse.conf` is kept: `vtesse --install` without keys only updates the binary and the init script, keys given again replace their lines. The file is plain `key=value` ([configuration file](docs/usage.md#configuration-file)). - `vtesse --uninstall` stops the service and removes everything, `/etc/vtesse.conf` included. -- `--scripts` also writes the hook scripts to `/usr/libexec/vtesse` to be edited; `--no-start` installs without starting. All options: [Installation](docs/usage.md#installation). -- OpenWrt needs the `tun` driver for the TAP backend: `opkg update && opkg install kmod-tun` (`apk add kmod-tun` on OpenWrt 25+). +- `--no-start` installs without starting. All options: [Installation](docs/usage.md#installation). +- Updating a 1.x installation removes its hook scripts; an `ARGS="--tap ..."` in `/etc/default/vtesse` is reported and must be removed by hand, otherwise the service does not start. -**Helpdesk access** on an OpenIPC camera with vtesse installed: `fw_setenv vtesse vtun.example.org` and a reboot are enough, as with `fw_setenv vtun` for VTun. With only the server set, the port is 5000, the session is the MAC of the default interface and the password is derived from it the way OpenIPC's VTun client does; `vtesse_port`, `vtesse_password` and other variables override that ([U-Boot environment](docs/usage.md#u-boot-environment-helpdesk-access)). `fw_setenv vtesse` turns it off. Anyone with root or a flash dump can read these variables, `vtesse_password` included, and the derived password is no secret (the MAC is visible on the LAN): this mode is for helpdesk access and is no stronger than VTun on OpenIPC ([security](docs/security.md)). +**Helpdesk access** on an OpenIPC camera with vtesse installed: `fw_setenv vtesse vtun.example.org` and a reboot are enough, as with `fw_setenv vtun` for VTun. With only the server set, the port is 5000, the session is the MAC of the default interface and the password is derived from it the way OpenIPC's VTun client does; `vtesse_port`, `vtesse_password` and other variables override that ([U-Boot environment](docs/usage.md#u-boot-environment-helpdesk-access)). The server then reaches the camera's web interface, ssh and RTSP on its tunnel address (ports 80, 22, 554, unless `accept=` lines in `/etc/vtesse.conf` say otherwise). `fw_setenv vtesse` turns it off. Anyone with root or a flash dump can read these variables, `vtesse_password` included, and the derived password is no secret (the MAC is visible on the LAN): this mode is for helpdesk access and is no stronger than VTun on OpenIPC ([security](docs/security.md)). Service control: `/etc/init.d/S99vtesse start|stop|restart|status` on OpenIPC (overrides in `/etc/default/vtesse`), `/etc/init.d/vtesse start|stop` on OpenWrt, `systemctl status vtesse` on a PC. @@ -90,7 +89,7 @@ Service control: `/etc/init.d/S99vtesse start|stop|restart|status` on OpenIPC (o Not supported yet. KeeneticOS (NDMS) is closed firmware: SSH gives only the NDMS command line, no Linux shell. Own software runs only through the **OPKG** component, which installs Entware into `/opt` on a storage volume (`opkg disk `), so the router needs a USB drive or an internal `storage:` partition. -Tried on a Netcraze Explorer NC-1621 (Keenetic rebrand, NDMS 5.2 beta, MIPS, 128 MB RAM, `small_flash`): the `opkg` component is available and installs (`components install opkg`, `components commit`, firmware rebuilt and the router restarts), but the model has no USB and no `storage:` volume, and Entware on the RAM disk `temp:` is refused (`unsuitable disk type`). Next: a Keenetic with a USB port (MT7621 or arm64) and an `.ipk` package with `vtesse-mipsel`/`vtesse-arm64`, an Entware init script and NDMS hooks for the TAP backend. +Tried on a Netcraze Explorer NC-1621 (Keenetic rebrand, NDMS 5.2 beta, MIPS, 128 MB RAM, `small_flash`): the `opkg` component is available and installs (`components install opkg`, `components commit`, firmware rebuilt and the router restarts), but the model has no USB and no `storage:` volume, and Entware on the RAM disk `temp:` is refused (`unsuitable disk type`). Next: a Keenetic with a USB port (MT7621 or arm64) and an `.ipk` package with `vtesse-mipsel`/`vtesse-arm64` and an Entware init script. ### Linux PC, Raspberry Pi @@ -102,7 +101,7 @@ curl -fsSL -o ~/.local/bin/vtesse https://github.com/OpenIPC/vtesse/releases/lat ~/.local/bin/vtesse --config ~/.config/vtesse/vtesse.conf --socks 1080 --forward 8554:10.78.0.10:554 ``` -The file has the same `key=value` lines as `/etc/vtesse.conf` (see [Build from source](#build-from-source)). For the TAP backend as a systemd service, install it as above. +The file has the same `key=value` lines as `/etc/vtesse.conf` (see [Build from source](#build-from-source)). As a systemd service: install it as above. ### Android @@ -132,33 +131,34 @@ The session name is the device ID (12 characters `0-9A-F`, also used as the MAC) ## Documentation -| Document | Contents | -|--------------------------------------|-------------------------------------------------------------------------------------------------------------------------| -| [Usage](docs/usage.md) | building, `vtesse-cli` options, configuration file, device ID, DHCP and routes, local proxy, error messages, probe tool | -| [VTun protocol](docs/protocol.md) | handshake, challenge response, session flags, framing, keepalive, server behaviour that affects clients, UDP | -| [Architecture](docs/architecture.md) | layers, `tunnel_ops_t`, VTun client, lwIP stack, proxy and flow control, repository layout | -| [Embedding](docs/embedding.md) | using the libraries in another application, loop integration, example | -| [Security](docs/security.md) | VTun authentication weaknesses, clear-text data, local proxy exposure | -| [VTun server](server/README.md) | patched vtund builds, release binaries, patch list | -| [Testing](docs/testing.md) | unit tests, sanitizers, big-endian MIPS, Docker test bench and acceptance tests | +| Document | Contents | +|--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------| +| [Usage](docs/usage.md) | building, `vtesse-cli` options, configuration file, device ID, DHCP and routes, local proxy, inbound forwards, error messages, probe tool | +| [VTun protocol](docs/protocol.md) | handshake, challenge response, session flags, framing, keepalive, server behaviour that affects clients, UDP | +| [Architecture](docs/architecture.md) | layers, `tunnel_ops_t`, VTun client, lwIP stack, proxy and flow control, repository layout | +| [Embedding](docs/embedding.md) | using the libraries in another application, loop integration, example | +| [Security](docs/security.md) | VTun authentication weaknesses, clear-text data, local proxy exposure | +| [VTun server](server/README.md) | patched vtund builds, release binaries, patch list | +| [Testing](docs/testing.md) | unit tests, sanitizers, big-endian MIPS, Docker test bench and acceptance tests | ## Roadmap Stages and tasks are tracked as [GitHub issues](https://github.com/OpenIPC/vtesse/issues): -| Stage | Status | Issues | -|----------------------------------------------------------------------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 0. Test bench (VTun 3.0.2 in Docker, bridge, DHCP with option 121) | ✅ Done | [#1](https://github.com/OpenIPC/vtesse/issues/1) ✅ | -| 1. CLI prototype on Linux: TCP + `type ether` + DHCP over lwIP | ✅ Done | [#2](https://github.com/OpenIPC/vtesse/issues/2) ✅ [#3](https://github.com/OpenIPC/vtesse/issues/3) ✅ [#4](https://github.com/OpenIPC/vtesse/issues/4) ✅ [#5](https://github.com/OpenIPC/vtesse/issues/5) ✅ [#6](https://github.com/OpenIPC/vtesse/issues/6) ✅ [#7](https://github.com/OpenIPC/vtesse/issues/7) ✅ [#8](https://github.com/OpenIPC/vtesse/issues/8) ✅ | -| 2. Local proxy: SOCKS5 and port forwarding | ✅ Done | [#10](https://github.com/OpenIPC/vtesse/issues/10) ✅ [#11](https://github.com/OpenIPC/vtesse/issues/11) ✅ | -| 3. Embedded Linux: ARM/MIPS cross builds, footprint, CI | ✅ Done | [#12](https://github.com/OpenIPC/vtesse/issues/12) ✅ [#20](https://github.com/OpenIPC/vtesse/issues/20) ✅ | -| 3. TAP/TUN backend (kernel interface instead of lwIP, Linux targets) | ✅ Done | [#19](https://github.com/OpenIPC/vtesse/issues/19) ✅ | -| 4. Android app (no VpnService) | ✅ Done | [#13](https://github.com/OpenIPC/vtesse/issues/13) ✅ | -| 5. Optional: UDP transport, TLS wrapper, Blowfish auth | ⏸ Deferred | [#14](https://github.com/OpenIPC/vtesse/issues/14) [#15](https://github.com/OpenIPC/vtesse/issues/15) [#16](https://github.com/OpenIPC/vtesse/issues/16) | -| 6. Reuse of the upper layers for camex | ⏸ Deferred | [#17](https://github.com/OpenIPC/vtesse/issues/17) | -| Cross-cutting: tests, license | ✅ Done | [#9](https://github.com/OpenIPC/vtesse/issues/9) ✅ [#18](https://github.com/OpenIPC/vtesse/issues/18) ✅ | - -✅ — closed issue. ⏸ — deferred. +| Stage | Status | Issues | +|----------------------------------------------------------------------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0. Test bench (VTun 3.0.2 in Docker, bridge, DHCP with option 121) | ✅ Done | [#1](https://github.com/OpenIPC/vtesse/issues/1) ✅ | +| 1. CLI prototype on Linux: TCP + `type ether` + DHCP over lwIP | ✅ Done | [#2](https://github.com/OpenIPC/vtesse/issues/2) ✅ [#3](https://github.com/OpenIPC/vtesse/issues/3) ✅ [#4](https://github.com/OpenIPC/vtesse/issues/4) ✅ [#5](https://github.com/OpenIPC/vtesse/issues/5) ✅ [#6](https://github.com/OpenIPC/vtesse/issues/6) ✅ [#7](https://github.com/OpenIPC/vtesse/issues/7) ✅ [#8](https://github.com/OpenIPC/vtesse/issues/8) ✅ | +| 2. Local proxy: SOCKS5 and port forwarding | ✅ Done | [#10](https://github.com/OpenIPC/vtesse/issues/10) ✅ [#11](https://github.com/OpenIPC/vtesse/issues/11) ✅ | +| 3. Embedded Linux: ARM/MIPS cross builds, footprint, CI | ✅ Done | [#12](https://github.com/OpenIPC/vtesse/issues/12) ✅ [#20](https://github.com/OpenIPC/vtesse/issues/20) ✅ | +| 3. TAP/TUN backend (kernel interface instead of lwIP, Linux targets) | ✖ Removed in 2.0.0 | [#19](https://github.com/OpenIPC/vtesse/issues/19) ✅ | +| 4. Android app (no VpnService) | ✅ Done | [#13](https://github.com/OpenIPC/vtesse/issues/13) ✅ | +| 5. Optional: UDP transport, TLS wrapper, Blowfish auth | ⏸ Deferred | [#14](https://github.com/OpenIPC/vtesse/issues/14) [#15](https://github.com/OpenIPC/vtesse/issues/15) [#16](https://github.com/OpenIPC/vtesse/issues/16) | +| 6. Reuse of the upper layers for camex | ⏸ Deferred | [#17](https://github.com/OpenIPC/vtesse/issues/17) | +| 7. Userspace only: inbound forwards, `ip=learn`, default-route wait | ✅ Done | [#67](https://github.com/OpenIPC/vtesse/issues/67) ✅ [#68](https://github.com/OpenIPC/vtesse/issues/68) ✅ [#72](https://github.com/OpenIPC/vtesse/issues/72) ✅ [#73](https://github.com/OpenIPC/vtesse/issues/73) ✅ | +| Cross-cutting: tests, license | ✅ Done | [#9](https://github.com/OpenIPC/vtesse/issues/9) ✅ [#18](https://github.com/OpenIPC/vtesse/issues/18) ✅ | + +✅ — closed issue. ⏸ — deferred. ✖ — removed. ## License diff --git a/README.ru.md b/README.ru.md index 136b3b9..deb5b7c 100644 --- a/README.ru.md +++ b/README.ru.md @@ -19,7 +19,8 @@ - **Без VPN:** не нужны root, `/dev/net/tun` и Android `VpnService` (нет значка VPN) - **VTun:** серверы 3.0.x, транспорт TCP, сессии `type ether` и `type tun`, ответ на challenge методом XOR, keepalive, автоматическое переподключение с экспоненциальной задержкой -- **Сеть:** адрес и маршруты по DHCP через туннель (опция 121, RFC 3442), IPv4, только исходящие подключения +- **Сеть:** адрес и маршруты по DHCP через туннель (опция 121, RFC 3442), а для `type tun` — вручную или из первого пакета, который пришлёт сервер (`ip=learn`); IPv4 +- **Входящий доступ:** подключения с сервера к адресу туннеля доходят до служб самого устройства (`accept=80`): веб-интерфейс камеры, ssh и RTSP, по-прежнему без интерфейса ядра - **UDP-ретранслятор:** RTP, который камера шлёт на адрес в туннеле, попадает в локальный плеер (`--udp-relay`) - **Локальный прокси:** SOCKS5 `CONNECT` с необязательной аутентификацией по логину и паролю, статический проброс TCP-портов (например, RTSP по TCP для видеоплееров) - **Платформы:** встраиваемый Linux (IP-камеры, роутеры; ARM и MIPS, в том числе big-endian, uClibc/musl), Android, в будущем — встраиваемый модуль связи @@ -37,7 +38,7 @@ | 64-битный ARM: Raspberry Pi с 64-битной ОС, Rockchip, другие платы ARM64 | `arm64` | | ПК и серверы | `x86_64` | -На каждую платформу один бинарник, `vtesse-ARCH`, с обоими режимами: lwIP (SOCKS5 / проброс портов без root) и TAP (интерфейс ядра с `--tap`). Режиму TAP нужны root и драйвер `tun` (`/dev/net/tun`); хук-скрипты `vtesse-hook.sh` и `vtesse-udhcpc.script` встроены в бинарник. До версии 1.12 выкладывался ещё `vtesse-tap-ARCH` (только TAP); его ссылки теперь отдают 404, вместо него — `vtesse-ARCH --tap`. +На каждую платформу один бинарник, `vtesse-ARCH`: стек в пространстве пользователя с локальным прокси и входящими пробросами, root не нужен. Режим TAP (интерфейс ядра с `--tap`) удалён в 2.0.0 ([#72](https://github.com/OpenIPC/vtesse/issues/72)): `--tap` отклоняется, хук-скрипты `vtesse-hook.sh` и `vtesse-udhcpc.script` больше не выкладываются (их ссылки отдают 404). В релизах также есть сервер VTun для ПК и серверов, `vtund-3.0.2-x86_64`: оригинальный VTun 3.0.2 с нашими патчами (отказ во входе пишется в лог с ID устройства и причиной), см. [server/README.md](server/README.md). Для камер и роутеров vtund при необходимости собирается в среде OpenIPC. @@ -45,19 +46,17 @@ Релиз один — [`latest`](https://github.com/OpenIPC/vtesse/releases/latest), с заголовком `VTesse <версия>`; он пересобирается при каждом push в `main`, релизы по версиям не хранятся. В именах файлов нет версии (только у сервера VTun — версия VTun), поэтому эти ссылки не меняются: -| Файл | Что это | Постоянная ссылка | -|------------------------|----------------------------------------------------------------|-----------------------------------------------------------------------------------| -| `vtesse-arm32` | клиент, 32-битный ARM | | -| `vtesse-mipsel` | клиент, MIPS little-endian | | -| `vtesse-mips` | клиент, MIPS big-endian | | -| `vtesse-arm64` | клиент, 64-битный ARM | | -| `vtesse-x86_64` | клиент, ПК и серверы | | -| `vtesse-android.apk` | приложение для Android | | -| `vtund-3.0.2-x86_64` | сервер VTun с нашими патчами, ПК (имя меняется с версией VTun) | | -| `vtesse-hook.sh` | хук-скрипт режима TAP (встроен в бинарник; для `--hook FILE`) | | -| `vtesse-udhcpc.script` | скрипт udhcpc для интерфейса TAP (встроен) | | -| `S99vtesse` | init-скрипт, OpenIPC (BusyBox; его пишет `--install`) | | -| `vtesse.init` | init-скрипт, OpenWrt (procd; его пишет `--install`) | | +| Файл | Что это | Постоянная ссылка | +|----------------------|----------------------------------------------------------------|---------------------------------------------------------------------------------| +| `vtesse-arm32` | клиент, 32-битный ARM | | +| `vtesse-mipsel` | клиент, MIPS little-endian | | +| `vtesse-mips` | клиент, MIPS big-endian | | +| `vtesse-arm64` | клиент, 64-битный ARM | | +| `vtesse-x86_64` | клиент, ПК и серверы | | +| `vtesse-android.apk` | приложение для Android | | +| `vtund-3.0.2-x86_64` | сервер VTun с нашими патчами, ПК (имя меняется с версией VTun) | | +| `S99vtesse` | init-скрипт, OpenIPC (BusyBox; его пишет `--install`) | | +| `vtesse.init` | init-скрипт, OpenWrt (procd; его пишет `--install`) | | ```sh curl -fsSLO https://github.com/OpenIPC/vtesse/releases/latest/download/vtesse-arm32 && @@ -66,7 +65,7 @@ curl -fsSLO https://github.com/OpenIPC/vtesse/releases/latest/download/vtesse-ar ### Установка службой: камера OpenIPC, роутер OpenWrt, Linux на ПК -От root: скачайте бинарник для своего `ARCH` (см. таблицу) и дайте ему установить себя. Он определяет систему, копирует себя в `/usr/bin/vtesse` (на ПК — в `/usr/local/bin`), пишет `/etc/vtesse.conf` (права 600) из ключей и init-скрипт (OpenIPC — `S99vtesse`, OpenWrt — procd `vtesse.init`, systemd — `vtesse.service`) и запускает службу сразу и при каждой загрузке. Хук-скрипты встроены. На OpenWrt установка переживает `sysupgrade`. +От root: скачайте бинарник для своего `ARCH` (см. таблицу) и дайте ему установить себя. Он определяет систему, копирует себя в `/usr/bin/vtesse` (на ПК — в `/usr/local/bin`), пишет `/etc/vtesse.conf` (права 600) из ключей и init-скрипт (OpenIPC — `S99vtesse`, OpenWrt — procd `vtesse.init`, systemd — `vtesse.service`) и запускает службу сразу и при каждой загрузке. На OpenWrt установка переживает `sysupgrade`. Служба работает на стеке в пространстве пользователя; сервер достаёт до веб-интерфейса, ssh и RTSP устройства по адресу туннеля: `--install` пишет в `/etc/vtesse.conf` строки `accept=80`, `accept=22` и `accept=554`, если строк `accept` там нет ([inbound forwards](docs/usage.md#inbound-forwards)), а подключения уходят на собственный LAN-адрес устройства, поэтому веб-интерфейс спрашивает пароль, как из локальной сети. ```sh ARCH=arm32 # arm32 или mipsel на камерах, mipsel / mips / arm64 на роутерах, x86_64 на ПК @@ -79,10 +78,10 @@ curl -fsSL -o /tmp/vtesse https://github.com/OpenIPC/vtesse/releases/latest/down - `--password -` читает пароль со стандартного ввода, а не из командной строки. Без `--port` порт — 5000. - Существующий `/etc/vtesse.conf` сохраняется: `vtesse --install` без ключей только обновляет бинарник и init-скрипт, заданные снова ключи заменяют свои строки. Файл — обычные строки `key=value` ([configuration file](docs/usage.md#configuration-file)). - `vtesse --uninstall` останавливает службу и удаляет всё, включая `/etc/vtesse.conf`. -- `--scripts` дополнительно пишет хук-скрипты в `/usr/libexec/vtesse` для правки; `--no-start` устанавливает без запуска. Все ключи: [Installation](docs/usage.md#installation). -- На OpenWrt режиму TAP нужен драйвер `tun`: `opkg update && opkg install kmod-tun` (на OpenWrt 25+: `apk add kmod-tun`). +- `--no-start` устанавливает без запуска. Все ключи: [Installation](docs/usage.md#installation). +- Обновление установки 1.x удаляет её хук-скрипты; `ARGS="--tap ..."` в `/etc/default/vtesse` установщик называет, и его нужно убрать вручную, иначе служба не запустится. -**Доступ для техподдержки** на камере OpenIPC с установленным vtesse: достаточно `fw_setenv vtesse vtun.example.org` и перезагрузки, как с `fw_setenv vtun` для VTun. Если задан только сервер, порт — 5000, сессия — MAC интерфейса по умолчанию, а пароль получается из него так же, как у VTun-клиента OpenIPC; `vtesse_port`, `vtesse_password` и другие переменные это переопределяют ([U-Boot environment](docs/usage.md#u-boot-environment-helpdesk-access)). `fw_setenv vtesse` выключает доступ. Эти переменные, включая `vtesse_password`, может прочитать любой с root или с дампом флешки, а пароль из MAC не секрет (MAC виден в локальной сети): режим — для доступа техподдержки и защищён не сильнее, чем VTun в OpenIPC ([security](docs/security.md)). +**Доступ для техподдержки** на камере OpenIPC с установленным vtesse: достаточно `fw_setenv vtesse vtun.example.org` и перезагрузки, как с `fw_setenv vtun` для VTun. Если задан только сервер, порт — 5000, сессия — MAC интерфейса по умолчанию, а пароль получается из него так же, как у VTun-клиента OpenIPC; `vtesse_port`, `vtesse_password` и другие переменные это переопределяют ([U-Boot environment](docs/usage.md#u-boot-environment-helpdesk-access)). Сервер тогда достаёт до веб-интерфейса, ssh и RTSP камеры по адресу туннеля (порты 80, 22, 554, если строки `accept=` в `/etc/vtesse.conf` не задают другие). `fw_setenv vtesse` выключает доступ. Эти переменные, включая `vtesse_password`, может прочитать любой с root или с дампом флешки, а пароль из MAC не секрет (MAC виден в локальной сети): режим — для доступа техподдержки и защищён не сильнее, чем VTun в OpenIPC ([security](docs/security.md)). Управление службой: `/etc/init.d/S99vtesse start|stop|restart|status` на OpenIPC (настройки переопределяются в `/etc/default/vtesse`), `/etc/init.d/vtesse start|stop` на OpenWrt, `systemctl status vtesse` на ПК. @@ -90,7 +89,7 @@ curl -fsSL -o /tmp/vtesse https://github.com/OpenIPC/vtesse/releases/latest/down Пока не поддерживается. KeeneticOS (NDMS) — закрытая прошивка: по SSH доступна только командная строка NDMS, Linux-shell нет. Свои программы запускаются только через компонент **OPKG**, который ставит Entware в `/opt` на том хранения (`opkg disk <том> `), поэтому роутеру нужна USB-флешка или внутренний раздел `storage:`. -Проверено на Netcraze Explorer NC-1621 (ребренд Keenetic, NDMS 5.2 beta, MIPS, 128 МБ RAM, `small_flash`): компонент `opkg` есть и ставится (`components install opkg`, `components commit`, прошивка пересобирается, роутер перезагружается), но у модели нет USB и раздела `storage:`, а Entware на RAM-диск `temp:` роутер ставить отказывается (`unsuitable disk type`). Дальше: Keenetic с USB-портом (MT7621 или arm64) и пакет `.ipk` с `vtesse-mipsel`/`vtesse-arm64`, init-скриптом Entware и хуками NDMS для TAP-бэкенда. +Проверено на Netcraze Explorer NC-1621 (ребренд Keenetic, NDMS 5.2 beta, MIPS, 128 МБ RAM, `small_flash`): компонент `opkg` есть и ставится (`components install opkg`, `components commit`, прошивка пересобирается, роутер перезагружается), но у модели нет USB и раздела `storage:`, а Entware на RAM-диск `temp:` роутер ставить отказывается (`unsuitable disk type`). Дальше: Keenetic с USB-портом (MT7621 или arm64) и пакет `.ipk` с `vtesse-mipsel`/`vtesse-arm64` и init-скриптом Entware. ### Linux на ПК, Raspberry Pi @@ -102,7 +101,7 @@ curl -fsSL -o ~/.local/bin/vtesse https://github.com/OpenIPC/vtesse/releases/lat ~/.local/bin/vtesse --config ~/.config/vtesse/vtesse.conf --socks 1080 --forward 8554:10.78.0.10:554 ``` -В файле те же строки `key=value`, что в `/etc/vtesse.conf` (см. [Сборка из исходников](#сборка-из-исходников)). Режим TAP службой systemd — установкой, как выше. +В файле те же строки `key=value`, что в `/etc/vtesse.conf` (см. [Сборка из исходников](#сборка-из-исходников)). Службой systemd — установкой, как выше. ### Android @@ -134,33 +133,34 @@ ffplay -rtsp_transport tcp rtsp://127.0.0.1:8554/stream Документация на английском языке. -| Документ | Содержание | -|--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| -| [Usage](docs/usage.md) | сборка, опции `vtesse-cli`, файл конфигурации, ID устройства, DHCP и маршруты, локальный прокси, сообщения об ошибках, утилита-зонд | -| [VTun protocol](docs/protocol.md) | рукопожатие, ответ на challenge, флаги сессии, кадры, keepalive, особенности сервера, важные для клиента, UDP | -| [Architecture](docs/architecture.md) | слои, `tunnel_ops_t`, клиент VTun, стек lwIP, прокси и управление потоком, структура репозитория | -| [Embedding](docs/embedding.md) | использование библиотек в другом приложении, интеграция с циклом событий, пример | -| [Security](docs/security.md) | слабые места аутентификации VTun, открытый трафик, доступ к локальному прокси | -| [VTun server](server/README.md) | сборки vtund с патчами, бинарники в релизах, список патчей | -| [Testing](docs/testing.md) | юнит-тесты, санитайзеры, big-endian MIPS, тестовый стенд в Docker и приёмочные тесты | +| Документ | Содержание | +|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Usage](docs/usage.md) | сборка, опции `vtesse-cli`, файл конфигурации, ID устройства, DHCP и маршруты, локальный прокси, входящие пробросы, сообщения об ошибках, утилита-зонд | +| [VTun protocol](docs/protocol.md) | рукопожатие, ответ на challenge, флаги сессии, кадры, keepalive, особенности сервера, важные для клиента, UDP | +| [Architecture](docs/architecture.md) | слои, `tunnel_ops_t`, клиент VTun, стек lwIP, прокси и управление потоком, структура репозитория | +| [Embedding](docs/embedding.md) | использование библиотек в другом приложении, интеграция с циклом событий, пример | +| [Security](docs/security.md) | слабые места аутентификации VTun, открытый трафик, доступ к локальному прокси | +| [VTun server](server/README.md) | сборки vtund с патчами, бинарники в релизах, список патчей | +| [Testing](docs/testing.md) | юнит-тесты, санитайзеры, big-endian MIPS, тестовый стенд в Docker и приёмочные тесты | ## План работ Этапы и задачи ведутся в [GitHub Issues](https://github.com/OpenIPC/vtesse/issues): -| Этап | Статус | Задачи | -|---------------------------------------------------------------------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 0. Тестовый стенд (VTun 3.0.2 в Docker, мост, DHCP с опцией 121) | ✅ Готово | [#1](https://github.com/OpenIPC/vtesse/issues/1) ✅ | -| 1. Прототип CLI для Linux: TCP + `type ether` + DHCP поверх lwIP | ✅ Готово | [#2](https://github.com/OpenIPC/vtesse/issues/2) ✅ [#3](https://github.com/OpenIPC/vtesse/issues/3) ✅ [#4](https://github.com/OpenIPC/vtesse/issues/4) ✅ [#5](https://github.com/OpenIPC/vtesse/issues/5) ✅ [#6](https://github.com/OpenIPC/vtesse/issues/6) ✅ [#7](https://github.com/OpenIPC/vtesse/issues/7) ✅ [#8](https://github.com/OpenIPC/vtesse/issues/8) ✅ | -| 2. Локальный прокси: SOCKS5 и проброс портов | ✅ Готово | [#10](https://github.com/OpenIPC/vtesse/issues/10) ✅ [#11](https://github.com/OpenIPC/vtesse/issues/11) ✅ | -| 3. Встраиваемый Linux: кросс-сборка ARM/MIPS, размер и память, CI | ✅ Готово | [#12](https://github.com/OpenIPC/vtesse/issues/12) ✅ [#20](https://github.com/OpenIPC/vtesse/issues/20) ✅ | -| 3. Режим TAP/TUN (интерфейс ядра вместо lwIP, Linux) | ✅ Готово | [#19](https://github.com/OpenIPC/vtesse/issues/19) ✅ | -| 4. Приложение для Android (без VpnService) | ✅ Готово | [#13](https://github.com/OpenIPC/vtesse/issues/13) ✅ | -| 5. Опционально: транспорт UDP, обёртка TLS, аутентификация Blowfish | ⏸ Отложено | [#14](https://github.com/OpenIPC/vtesse/issues/14) [#15](https://github.com/OpenIPC/vtesse/issues/15) [#16](https://github.com/OpenIPC/vtesse/issues/16) | -| 6. Повторное использование верхних слоёв в camex | ⏸ Отложено | [#17](https://github.com/OpenIPC/vtesse/issues/17) | -| Общее: тесты, лицензия | ✅ Готово | [#9](https://github.com/OpenIPC/vtesse/issues/9) ✅ [#18](https://github.com/OpenIPC/vtesse/issues/18) ✅ | - -✅ — задача закрыта. ⏸ — отложено. +| Этап | Статус | Задачи | +|-----------------------------------------------------------------------|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0. Тестовый стенд (VTun 3.0.2 в Docker, мост, DHCP с опцией 121) | ✅ Готово | [#1](https://github.com/OpenIPC/vtesse/issues/1) ✅ | +| 1. Прототип CLI для Linux: TCP + `type ether` + DHCP поверх lwIP | ✅ Готово | [#2](https://github.com/OpenIPC/vtesse/issues/2) ✅ [#3](https://github.com/OpenIPC/vtesse/issues/3) ✅ [#4](https://github.com/OpenIPC/vtesse/issues/4) ✅ [#5](https://github.com/OpenIPC/vtesse/issues/5) ✅ [#6](https://github.com/OpenIPC/vtesse/issues/6) ✅ [#7](https://github.com/OpenIPC/vtesse/issues/7) ✅ [#8](https://github.com/OpenIPC/vtesse/issues/8) ✅ | +| 2. Локальный прокси: SOCKS5 и проброс портов | ✅ Готово | [#10](https://github.com/OpenIPC/vtesse/issues/10) ✅ [#11](https://github.com/OpenIPC/vtesse/issues/11) ✅ | +| 3. Встраиваемый Linux: кросс-сборка ARM/MIPS, размер и память, CI | ✅ Готово | [#12](https://github.com/OpenIPC/vtesse/issues/12) ✅ [#20](https://github.com/OpenIPC/vtesse/issues/20) ✅ | +| 3. Режим TAP/TUN (интерфейс ядра вместо lwIP, Linux) | ✖ Удалён в 2.0.0 | [#19](https://github.com/OpenIPC/vtesse/issues/19) ✅ | +| 4. Приложение для Android (без VpnService) | ✅ Готово | [#13](https://github.com/OpenIPC/vtesse/issues/13) ✅ | +| 5. Опционально: транспорт UDP, обёртка TLS, аутентификация Blowfish | ⏸ Отложено | [#14](https://github.com/OpenIPC/vtesse/issues/14) [#15](https://github.com/OpenIPC/vtesse/issues/15) [#16](https://github.com/OpenIPC/vtesse/issues/16) | +| 6. Повторное использование верхних слоёв в camex | ⏸ Отложено | [#17](https://github.com/OpenIPC/vtesse/issues/17) | +| 7. Только userspace: входящие пробросы, `ip=learn`, ожидание маршрута | ✅ Готово | [#67](https://github.com/OpenIPC/vtesse/issues/67) ✅ [#68](https://github.com/OpenIPC/vtesse/issues/68) ✅ [#72](https://github.com/OpenIPC/vtesse/issues/72) ✅ [#73](https://github.com/OpenIPC/vtesse/issues/73) ✅ | +| Общее: тесты, лицензия | ✅ Готово | [#9](https://github.com/OpenIPC/vtesse/issues/9) ✅ [#18](https://github.com/OpenIPC/vtesse/issues/18) ✅ | + +✅ — задача закрыта. ⏸ — отложено. ✖ — удалено. ## Лицензия diff --git a/README.zh-CN.md b/README.zh-CN.md index 5cc9d02..a3d6ba4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -19,7 +19,8 @@ - **无需 VPN:** 不需要 root、`/dev/net/tun` 或 Android `VpnService`(不显示 VPN 图标) - **VTun:** 3.0.x 服务器,TCP 传输,`type ether` 与 `type tun` 会话,XOR 质询应答,keepalive,按指数退避自动重连 -- **网络:** 通过隧道内的 DHCP 获取地址和路由(选项 121,RFC 3442),IPv4,仅出站连接 +- **网络:** 通过隧道内的 DHCP 获取地址和路由(选项 121,RFC 3442),`type tun` 会话可手动设置,或从服务器发来的第一个数据包中获取(`ip=learn`);IPv4 +- **入站访问:** 服务器连到隧道地址的连接可到达设备自身的服务(`accept=80`):摄像机的网页界面、ssh 和 RTSP,仍无需内核接口 - **UDP 中继:** 摄像机推送到隧道地址的 RTP 可送达本地播放器(`--udp-relay`) - **本地代理:** 支持可选用户名/密码认证的 SOCKS5 `CONNECT`,静态 TCP 端口转发(例如为视频播放器提供基于 TCP 的 RTSP) - **目标平台:** 嵌入式 Linux(IP 摄像机、路由器;ARM 与 MIPS,含大端序,uClibc/musl)、Android,后续还将提供可嵌入的通信模块 @@ -37,7 +38,7 @@ | 64 位 ARM:运行 64 位系统的树莓派、瑞芯微及其他 ARM64 开发板 | `arm64` | | PC 与服务器 | `x86_64` | -每个平台只提供一个二进制文件 `vtesse-ARCH`,包含两种后端:lwIP(无需 root 的 SOCKS5 / 端口转发)和 TAP(使用 `--tap` 的内核接口)。TAP 后端需要 root 和 `tun` 驱动(`/dev/net/tun`);钩子脚本 `vtesse-hook.sh` 和 `vtesse-udhcpc.script` 已内置在二进制文件中。在 1.12 及之前还发布仅含 TAP 的 `vtesse-tap-ARCH`;其链接现在返回 404,请改用 `vtesse-ARCH --tap`。 +每个平台只提供一个二进制文件 `vtesse-ARCH`:用户空间协议栈,带本地代理和入站转发,无需 root。TAP 后端(使用 `--tap` 的内核接口)已在 2.0.0 中移除([#72](https://github.com/OpenIPC/vtesse/issues/72)):`--tap` 会被拒绝,钩子脚本 `vtesse-hook.sh` 和 `vtesse-udhcpc.script` 不再发布(其链接返回 404)。 发布页还提供面向 PC 和服务器的 VTun 服务器 `vtund-3.0.2-x86_64`:原版 VTun 3.0.2 加上我们的补丁(登录被拒时日志会记录设备 ID 和原因),详见 [server/README.md](server/README.md)。摄像头和路由器如需 vtund,请在 OpenIPC 环境中构建。 @@ -45,19 +46,17 @@ 只有一个发布:[`latest`](https://github.com/OpenIPC/vtesse/releases/latest),标题为 `VTesse <版本>`;每次推送到 `main` 都会重新构建它,不保留按版本的发布。文件名不含版本号(只有 VTun 服务器的文件名带 VTun 版本),因此以下链接始终不变: -| 文件 | 说明 | 永久链接 | -|------------------------|--------------------------------------------------------|-----------------------------------------------------------------------------------| -| `vtesse-arm32` | 客户端,32 位 ARM | | -| `vtesse-mipsel` | 客户端,MIPS 小端 | | -| `vtesse-mips` | 客户端,MIPS 大端 | | -| `vtesse-arm64` | 客户端,64 位 ARM | | -| `vtesse-x86_64` | 客户端,PC 和服务器 | | -| `vtesse-android.apk` | Android 应用 | | -| `vtund-3.0.2-x86_64` | 带我们补丁的 VTun 服务器,PC(文件名随 VTun 版本变化) | | -| `vtesse-hook.sh` | TAP 钩子脚本(已内置;用于 `--hook FILE`) | | -| `vtesse-udhcpc.script` | TAP 接口的 udhcpc 脚本(已内置) | | -| `S99vtesse` | 启动脚本,OpenIPC(BusyBox;由 `--install` 写入) | | -| `vtesse.init` | 启动脚本,OpenWrt(procd;由 `--install` 写入) | | +| 文件 | 说明 | 永久链接 | +|----------------------|--------------------------------------------------------|---------------------------------------------------------------------------------| +| `vtesse-arm32` | 客户端,32 位 ARM | | +| `vtesse-mipsel` | 客户端,MIPS 小端 | | +| `vtesse-mips` | 客户端,MIPS 大端 | | +| `vtesse-arm64` | 客户端,64 位 ARM | | +| `vtesse-x86_64` | 客户端,PC 和服务器 | | +| `vtesse-android.apk` | Android 应用 | | +| `vtund-3.0.2-x86_64` | 带我们补丁的 VTun 服务器,PC(文件名随 VTun 版本变化) | | +| `S99vtesse` | 启动脚本,OpenIPC(BusyBox;由 `--install` 写入) | | +| `vtesse.init` | 启动脚本,OpenWrt(procd;由 `--install` 写入) | | ```sh curl -fsSLO https://github.com/OpenIPC/vtesse/releases/latest/download/vtesse-arm32 && @@ -66,7 +65,7 @@ curl -fsSLO https://github.com/OpenIPC/vtesse/releases/latest/download/vtesse-ar ### 作为服务安装:OpenIPC 摄像机、OpenWrt 路由器、Linux PC -以 root 身份:下载对应 `ARCH` 的二进制文件(见上表),让它自行安装。它会识别系统,把自身复制到 `/usr/bin/vtesse`(PC 上为 `/usr/local/bin`),根据参数写入 `/etc/vtesse.conf`(权限 600)和启动脚本(OpenIPC 为 `S99vtesse`,OpenWrt 为 procd `vtesse.init`,systemd 为 `vtesse.service`),并立即启动服务、开机自启。钩子脚本已内置。在 OpenWrt 上,安装在 `sysupgrade` 后仍会保留。 +以 root 身份:下载对应 `ARCH` 的二进制文件(见上表),让它自行安装。它会识别系统,把自身复制到 `/usr/bin/vtesse`(PC 上为 `/usr/local/bin`),根据参数写入 `/etc/vtesse.conf`(权限 600)和启动脚本(OpenIPC 为 `S99vtesse`,OpenWrt 为 procd `vtesse.init`,systemd 为 `vtesse.service`),并立即启动服务、开机自启。在 OpenWrt 上,安装在 `sysupgrade` 后仍会保留。服务运行用户空间协议栈;服务器通过隧道地址访问设备的网页界面、ssh 和 RTSP:若 `/etc/vtesse.conf` 中没有 `accept` 行,`--install` 会写入 `accept=80`、`accept=22` 和 `accept=554`([inbound forwards](docs/usage.md#inbound-forwards)),连接转到设备自身的局域网地址,因此网页界面会像在局域网中一样要求输入密码。 ```sh ARCH=arm32 # 摄像机用 arm32 或 mipsel,路由器用 mipsel / mips / arm64,PC 用 x86_64 @@ -79,10 +78,10 @@ curl -fsSL -o /tmp/vtesse https://github.com/OpenIPC/vtesse/releases/latest/down - `--password -` 从标准输入读取密码,而不是写在命令行上。不指定 `--port` 时端口为 5000。 - 已有的 `/etc/vtesse.conf` 会保留:不带参数的 `vtesse --install` 只更新二进制文件和启动脚本,再次给出的参数替换对应的行。该文件为普通的 `key=value` 行([configuration file](docs/usage.md#configuration-file))。 - `vtesse --uninstall` 停止服务并删除全部文件,包括 `/etc/vtesse.conf`。 -- `--scripts` 另外把钩子脚本写到 `/usr/libexec/vtesse` 以便修改;`--no-start` 只安装不启动。全部选项:[Installation](docs/usage.md#installation)。 -- OpenWrt 上 TAP 后端需要 `tun` 驱动:`opkg update && opkg install kmod-tun`(OpenWrt 25+ 上为 `apk add kmod-tun`)。 +- `--no-start` 只安装不启动。全部选项:[Installation](docs/usage.md#installation)。 +- 升级 1.x 的安装会删除其钩子脚本;`/etc/default/vtesse` 中的 `ARGS="--tap ..."` 会被报告,需要手动删除,否则服务无法启动。 -**技术支持远程访问**:在已安装 vtesse 的 OpenIPC 摄像机上,只需 `fw_setenv vtesse vtun.example.org` 并重启,与 VTun 的 `fw_setenv vtun` 相同。只设置服务器时,端口为 5000,会话为默认接口的 MAC,密码按 OpenIPC 的 VTun 客户端的方式由 MAC 生成;`vtesse_port`、`vtesse_password` 等变量可覆盖这些值([U-Boot environment](docs/usage.md#u-boot-environment-helpdesk-access))。`fw_setenv vtesse` 关闭访问。拥有 root 权限或闪存转储的人都能读取这些变量(包括 `vtesse_password`),由 MAC 生成的密码也不是秘密(MAC 在局域网中可见):此模式仅用于技术支持访问,安全性不高于 OpenIPC 上的 VTun([security](docs/security.md))。 +**技术支持远程访问**:在已安装 vtesse 的 OpenIPC 摄像机上,只需 `fw_setenv vtesse vtun.example.org` 并重启,与 VTun 的 `fw_setenv vtun` 相同。只设置服务器时,端口为 5000,会话为默认接口的 MAC,密码按 OpenIPC 的 VTun 客户端的方式由 MAC 生成;`vtesse_port`、`vtesse_password` 等变量可覆盖这些值([U-Boot environment](docs/usage.md#u-boot-environment-helpdesk-access))。之后服务器可通过隧道地址访问摄像机的网页界面、ssh 和 RTSP(端口 80、22、554,除非 `/etc/vtesse.conf` 中的 `accept=` 行另有设置)。`fw_setenv vtesse` 关闭访问。拥有 root 权限或闪存转储的人都能读取这些变量(包括 `vtesse_password`),由 MAC 生成的密码也不是秘密(MAC 在局域网中可见):此模式仅用于技术支持访问,安全性不高于 OpenIPC 上的 VTun([security](docs/security.md))。 服务管理:OpenIPC 上 `/etc/init.d/S99vtesse start|stop|restart|status`(可在 `/etc/default/vtesse` 中覆盖设置),OpenWrt 上 `/etc/init.d/vtesse start|stop`,PC 上 `systemctl status vtesse`。 @@ -90,7 +89,7 @@ curl -fsSL -o /tmp/vtesse https://github.com/OpenIPC/vtesse/releases/latest/down 暂不支持。KeeneticOS(NDMS)是闭源固件:SSH 只提供 NDMS 命令行,没有 Linux shell。自有软件只能通过 **OPKG** 组件运行,它把 Entware 安装到存储卷上的 `/opt`(`opkg disk <卷> <安装包 URL>`),因此路由器需要 USB 存储或内部 `storage:` 分区。 -已在 Netcraze Explorer NC-1621(Keenetic 换牌型号,NDMS 5.2 beta,MIPS,128 MB 内存,`small_flash`)上测试:`opkg` 组件可用并可安装(`components install opkg`、`components commit`,固件重新生成后路由器重启),但该型号没有 USB,也没有 `storage:` 卷,而将 Entware 安装到内存盘 `temp:` 会被拒绝(`unsuitable disk type`)。下一步:使用带 USB 接口的 Keenetic(MT7621 或 arm64),并制作 `.ipk` 软件包,包含 `vtesse-mipsel`/`vtesse-arm64`、Entware 启动脚本以及 TAP 后端所需的 NDMS 钩子。 +已在 Netcraze Explorer NC-1621(Keenetic 换牌型号,NDMS 5.2 beta,MIPS,128 MB 内存,`small_flash`)上测试:`opkg` 组件可用并可安装(`components install opkg`、`components commit`,固件重新生成后路由器重启),但该型号没有 USB,也没有 `storage:` 卷,而将 Entware 安装到内存盘 `temp:` 会被拒绝(`unsuitable disk type`)。下一步:使用带 USB 接口的 Keenetic(MT7621 或 arm64),并制作 `.ipk` 软件包,包含 `vtesse-mipsel`/`vtesse-arm64` 和 Entware 启动脚本。 ### Linux PC、树莓派 @@ -102,7 +101,7 @@ curl -fsSL -o ~/.local/bin/vtesse https://github.com/OpenIPC/vtesse/releases/lat ~/.local/bin/vtesse --config ~/.config/vtesse/vtesse.conf --socks 1080 --forward 8554:10.78.0.10:554 ``` -该文件与 `/etc/vtesse.conf` 的 `key=value` 行相同(见[从源码构建](#从源码构建))。若要以 systemd 服务运行 TAP 后端,请按上文安装。 +该文件与 `/etc/vtesse.conf` 的 `key=value` 行相同(见[从源码构建](#从源码构建))。若要作为 systemd 服务运行,请按上文安装。 ### Android @@ -134,33 +133,34 @@ ffplay -rtsp_transport tcp rtsp://127.0.0.1:8554/stream 文档为英文。 -| 文档 | 内容 | -|--------------------------------------|---------------------------------------------------------------------------------------| -| [Usage](docs/usage.md) | 构建、`vtesse-cli` 选项、配置文件、设备 ID、DHCP 与路由、本地代理、错误信息、探测工具 | -| [VTun protocol](docs/protocol.md) | 握手、质询应答、会话标志、帧格式、keepalive、影响客户端的服务器行为、UDP | -| [Architecture](docs/architecture.md) | 分层、`tunnel_ops_t`、VTun 客户端、lwIP 协议栈、代理与流量控制、仓库结构 | -| [Embedding](docs/embedding.md) | 在其他应用中使用这些库、事件循环集成、示例 | -| [Security](docs/security.md) | VTun 认证的弱点、明文数据、本地代理的暴露面 | -| [VTun server](server/README.md) | 打补丁的 vtund 构建、发布的二进制文件、补丁列表 | -| [Testing](docs/testing.md) | 单元测试、sanitizer、大端序 MIPS、Docker 测试台与验收测试 | +| 文档 | 内容 | +|--------------------------------------|-------------------------------------------------------------------------------------------------| +| [Usage](docs/usage.md) | 构建、`vtesse-cli` 选项、配置文件、设备 ID、DHCP 与路由、本地代理、入站转发、错误信息、探测工具 | +| [VTun protocol](docs/protocol.md) | 握手、质询应答、会话标志、帧格式、keepalive、影响客户端的服务器行为、UDP | +| [Architecture](docs/architecture.md) | 分层、`tunnel_ops_t`、VTun 客户端、lwIP 协议栈、代理与流量控制、仓库结构 | +| [Embedding](docs/embedding.md) | 在其他应用中使用这些库、事件循环集成、示例 | +| [Security](docs/security.md) | VTun 认证的弱点、明文数据、本地代理的暴露面 | +| [VTun server](server/README.md) | 打补丁的 vtund 构建、发布的二进制文件、补丁列表 | +| [Testing](docs/testing.md) | 单元测试、sanitizer、大端序 MIPS、Docker 测试台与验收测试 | ## 路线图 各阶段与任务记录在 [GitHub Issues](https://github.com/OpenIPC/vtesse/issues) 中: -| 阶段 | 状态 | Issues | -|---------------------------------------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 0. 测试台(Docker 中的 VTun 3.0.2、网桥、带选项 121 的 DHCP) | ✅ 已完成 | [#1](https://github.com/OpenIPC/vtesse/issues/1) ✅ | -| 1. Linux 命令行原型:TCP + `type ether` + 基于 lwIP 的 DHCP | ✅ 已完成 | [#2](https://github.com/OpenIPC/vtesse/issues/2) ✅ [#3](https://github.com/OpenIPC/vtesse/issues/3) ✅ [#4](https://github.com/OpenIPC/vtesse/issues/4) ✅ [#5](https://github.com/OpenIPC/vtesse/issues/5) ✅ [#6](https://github.com/OpenIPC/vtesse/issues/6) ✅ [#7](https://github.com/OpenIPC/vtesse/issues/7) ✅ [#8](https://github.com/OpenIPC/vtesse/issues/8) ✅ | -| 2. 本地代理:SOCKS5 与端口转发 | ✅ 已完成 | [#10](https://github.com/OpenIPC/vtesse/issues/10) ✅ [#11](https://github.com/OpenIPC/vtesse/issues/11) ✅ | -| 3. 嵌入式 Linux:ARM/MIPS 交叉编译、体积与内存、CI | ✅ 已完成 | [#12](https://github.com/OpenIPC/vtesse/issues/12) ✅ [#20](https://github.com/OpenIPC/vtesse/issues/20) ✅ | -| 3. TAP/TUN 后端(用内核接口代替 lwIP,Linux 平台) | ✅ 已完成 | [#19](https://github.com/OpenIPC/vtesse/issues/19) ✅ | -| 4. Android 应用(不使用 VpnService) | ✅ 已完成 | [#13](https://github.com/OpenIPC/vtesse/issues/13) ✅ | -| 5. 可选:UDP 传输、TLS 封装、Blowfish 认证 | ⏸ 已推迟 | [#14](https://github.com/OpenIPC/vtesse/issues/14) [#15](https://github.com/OpenIPC/vtesse/issues/15) [#16](https://github.com/OpenIPC/vtesse/issues/16) | -| 6. 在 camex 中复用上层模块 | ⏸ 已推迟 | [#17](https://github.com/OpenIPC/vtesse/issues/17) | -| 通用:测试、许可证 | ✅ 已完成 | [#9](https://github.com/OpenIPC/vtesse/issues/9) ✅ [#18](https://github.com/OpenIPC/vtesse/issues/18) ✅ | - -✅ — 已关闭的 issue。 ⏸ — 已推迟。 +| 阶段 | 状态 | Issues | +|---------------------------------------------------------------|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0. 测试台(Docker 中的 VTun 3.0.2、网桥、带选项 121 的 DHCP) | ✅ 已完成 | [#1](https://github.com/OpenIPC/vtesse/issues/1) ✅ | +| 1. Linux 命令行原型:TCP + `type ether` + 基于 lwIP 的 DHCP | ✅ 已完成 | [#2](https://github.com/OpenIPC/vtesse/issues/2) ✅ [#3](https://github.com/OpenIPC/vtesse/issues/3) ✅ [#4](https://github.com/OpenIPC/vtesse/issues/4) ✅ [#5](https://github.com/OpenIPC/vtesse/issues/5) ✅ [#6](https://github.com/OpenIPC/vtesse/issues/6) ✅ [#7](https://github.com/OpenIPC/vtesse/issues/7) ✅ [#8](https://github.com/OpenIPC/vtesse/issues/8) ✅ | +| 2. 本地代理:SOCKS5 与端口转发 | ✅ 已完成 | [#10](https://github.com/OpenIPC/vtesse/issues/10) ✅ [#11](https://github.com/OpenIPC/vtesse/issues/11) ✅ | +| 3. 嵌入式 Linux:ARM/MIPS 交叉编译、体积与内存、CI | ✅ 已完成 | [#12](https://github.com/OpenIPC/vtesse/issues/12) ✅ [#20](https://github.com/OpenIPC/vtesse/issues/20) ✅ | +| 3. TAP/TUN 后端(用内核接口代替 lwIP,Linux 平台) | ✖ 已在 2.0.0 中移除 | [#19](https://github.com/OpenIPC/vtesse/issues/19) ✅ | +| 4. Android 应用(不使用 VpnService) | ✅ 已完成 | [#13](https://github.com/OpenIPC/vtesse/issues/13) ✅ | +| 5. 可选:UDP 传输、TLS 封装、Blowfish 认证 | ⏸ 已推迟 | [#14](https://github.com/OpenIPC/vtesse/issues/14) [#15](https://github.com/OpenIPC/vtesse/issues/15) [#16](https://github.com/OpenIPC/vtesse/issues/16) | +| 6. 在 camex 中复用上层模块 | ⏸ 已推迟 | [#17](https://github.com/OpenIPC/vtesse/issues/17) | +| 7. 仅用户空间:入站转发、`ip=learn`、等待默认路由 | ✅ 已完成 | [#67](https://github.com/OpenIPC/vtesse/issues/67) ✅ [#68](https://github.com/OpenIPC/vtesse/issues/68) ✅ [#72](https://github.com/OpenIPC/vtesse/issues/72) ✅ [#73](https://github.com/OpenIPC/vtesse/issues/73) ✅ | +| 通用:测试、许可证 | ✅ 已完成 | [#9](https://github.com/OpenIPC/vtesse/issues/9) ✅ [#18](https://github.com/OpenIPC/vtesse/issues/18) ✅ | + +✅ — 已关闭的 issue。 ⏸ — 已推迟。 ✖ — 已移除。 ## 许可证 diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt index b1d17ab..8722f62 100644 --- a/android/app/src/main/cpp/CMakeLists.txt +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -18,6 +18,7 @@ set(LWIP_SRC set(CORE_SRC ${ROOT}/netcore/tunnel.c ${ROOT}/netcore/loop.c ${ROOT}/netcore/route.c ${ROOT}/netcore/stack.c ${ROOT}/netcore/proxy.c ${ROOT}/netcore/udprelay.c + ${ROOT}/netcore/hostaddr.c ${ROOT}/vtun/vtun_proto.c ${ROOT}/vtun/vtun_client.c) add_library(vtesse SHARED vtesse_jni.c ${CORE_SRC} ${LWIP_SRC}) diff --git a/bench/README.md b/bench/README.md index 140b477..a0fb564 100644 --- a/bench/README.md +++ b/bench/README.md @@ -72,16 +72,22 @@ and 8 MiB TCP echo through forwards with integrity checks, RTSP `OPTIONS`, 8 par downloads, SOCKS5 username/password. The device also serves `/big.bin` (20 MiB, sha256 in `/big.sha256`) and a TCP echo on port 7. -`bench.sh tap-test` is the TAP backend acceptance test (the device ID comes from the MAC -of a dummy interface, `id_from`): it builds `vtesse-cli` with -`LWIP=0` in a Debian bookworm container (`ci/Dockerfile.native`) and runs it with -`--tap` in a second privileged container; addressing via `udhcpc` and -`scripts/vtesse-udhcpc.script`; inbound ping and HTTP from the device to the client. - `bench.sh tun-test` is the `type tun` acceptance test (#8): session `02BE0C000006` with -manual IPv4 (`ip=10.76.0.2/32`, `gw=10.76.0.1`, `route=10.78.0.0/24`); lwIP: HTTP to the -device and inbound ping; `--tap`: a TUN interface addressed by the hook, ping/HTTP to the -device, inbound ping, no default route. +manual IPv4 (`ip=10.76.0.2/32`, `gw=10.76.0.1`, `route=10.78.0.0/24`): HTTP to the device +and inbound ping. + +`bench.sh accept-test` covers inbound forwards (#67) and `ip=learn` (#68): it builds +`vtesse-cli` in a Debian bookworm container (`ci/Dockerfile.native`) and runs it in a +container on the bench network with session `02BE0C000006`, `ip=learn` and +`accept=8080:127.0.0.1:8000` to a web server on its `127.0.0.1`. The server opens +`http://10.76.0.2:8080/` (the first SYN teaches the address), 8 parallel 20 MiB downloads +with sha256, a closed local port (reset at once); after a server restart with the peer +changed to `10.76.0.3` the new address is learnt (the configuration is restored at the +end); `ip=learn` on a `type ether` session stops the client with exit code 2. + +`bench.sh route-wait-test` (#73): the client in a container without a network waits for +a default route and does not connect; SIGTERM during the wait is exit code 0; a default +route ends the wait. The TAP backend and its `tap-test` were removed in 2.0.0 (#72). `bench.sh udp-test` is the UDP relay acceptance test (#28): `vtesse-cli` with `udp_relay=1559`; the device sends 500 numbered datagrams and a fragmented 4000-byte diff --git a/bench/bench.sh b/bench/bench.sh index 6b5f616..2156e79 100755 --- a/bench/bench.sh +++ b/bench/bench.sh @@ -10,8 +10,8 @@ # bench.sh selftest protocol self-test against the running bench # bench.sh cli-test stage 1 acceptance: build/vtesse-cli (lwIP, DHCP) on the bench # bench.sh proxy-test stage 2 acceptance: SOCKS5 and port forwards of build/vtesse-cli -# bench.sh tap-test TAP backend acceptance: vtesse-cli --tap (LWIP=0 build) in a container -# bench.sh tun-test type tun session (#8): manual IPv4, lwIP and --tap (TUN interface) +# bench.sh tun-test type tun session (#8): manual IPv4 +# bench.sh accept-test inbound forwards (#67) and ip=learn (#68) on a type tun session # bench.sh udp-test UDP relay (#28): datagrams from the device to the tunnel address -> 127.0.0.1 # bench.sh fault-test failures (#9): link loss, server restart, no option 121, lease renewal (~6 min) # bench.sh route-wait-test the client waits for a default route before it starts (#73) @@ -116,72 +116,9 @@ cmd_cli_test() { [ "$fails" -eq 0 ] } -cmd_tap_test() { - repo=$(cd "$HERE/.." && pwd) - tapc=vtesse-bench-tapclient - fails=0 - check() { if eval "$2"; then echo "PASS $1"; else echo "FAIL $1"; fails=$((fails + 1)); fi; } - inc() { docker exec "$tapc" sh -c "$1"; } - dev() { docker exec "$SERVER" ip netns exec device sh -c "$1"; } - - # build the TAP-only CLI for the bench image (Debian bookworm) - docker build -q -t vtesse-ci-native -f "$repo/ci/Dockerfile.native" "$repo/ci" >/dev/null - docker run --rm --user "$(id -u):$(id -g)" -v "$repo:/src" -w /src vtesse-ci-native \ - make -s BUILD=build-bench-tap LWIP=0 all >/dev/null - - docker rm -f "$tapc" >/dev/null 2>&1 || true - docker run -d --name "$tapc" --privileged --network "$NET" -v "$repo:/src:ro" "$IMAGE" sh -c " - mkdir -p /var/lib/dhcp - # device ID from an interface MAC (id_from, #6): a dummy NIC with the session's MAC - ip link add idsrc0 type dummy - ip link set idsrc0 address $(echo $SESSION | sed 's/../&:/g; s/:$//') - printf 'host=$SERVER\nport=5000\nid_from=idsrc0\npassword=$PASSWORD\n' > /tmp/vtesse.conf - python3 -m http.server 8080 --bind 0.0.0.0 --directory /etc >/dev/null 2>&1 & - # no --hook: the built-in hook and udhcpc script (#52); tun-test uses the file - exec /src/build-bench-tap/vtesse-cli --tap --ifname vtesse0 --config /tmp/vtesse.conf - " >/dev/null - - i=0 - until inc 'ip -4 addr show vtesse0 2>/dev/null | grep -q "inet 10\.77\.0\."' || [ $i -gt 20 ]; do sleep 1; i=$((i + 1)); done - addr=$(inc "ip -4 -o addr show vtesse0" 2>/dev/null | sed -n 's/.*inet \([0-9.]*\).*/\1/p') - check "device ID taken from the MAC of idsrc0 (id_from)" 'docker logs "$tapc" 2>&1 | grep -q "device ID $SESSION (MAC of idsrc0)"' - check "built-in hook (no --hook)" 'docker logs "$tapc" 2>&1 | grep -q "^hook: built-in"' - check "tap interface vtesse0 with DHCP address" '[ -n "$addr" ]' - check "MAC of vtesse0 is the device ID" 'inc "cat /sys/class/net/vtesse0/address" | grep -qi "^02:be:0c:00:00:01$"' - check "option 121 route via the tunnel, no default route through it" \ - 'inc "ip route" | grep -q "10.78.0.0/24 via 10.77.0.1 dev vtesse0" && ! inc "ip route" | grep -q "default.*vtesse0"' - check "ping the device from the client" 'inc "ping -c 2 -W 2 10.78.0.10" >/dev/null' - check "HTTP to the device from any process (kernel stack)" 'inc "curl -fsS --max-time 5 http://10.78.0.10/" | grep -q "vtesse bench device"' - check "inbound: the device pings the client" 'dev "ping -c 2 -W 2 $addr" >/dev/null' - check "inbound: the device opens TCP to the client (HTTP :8080)" 'dev "curl -fsS --max-time 5 http://$addr:8080/hostname" >/dev/null' - sha=$(docker exec "$SERVER" cat /opt/device/www/big.sha256) - check "20 MiB download through the tap, sha256 matches" \ - '[ "$(inc "curl -fsS --max-time 60 http://10.78.0.10/big.bin | sha256sum | cut -d\" \" -f1")" = "$sha" ]' - - cmd_probe --seconds 1 >/dev/null 2>&1 # take the session over (multi killold) - i=0 - until [ "$(docker logs "$tapc" 2>&1 | grep -c 'carrier on')" -ge 1 ] || [ $i -gt 15 ]; do sleep 1; i=$((i + 1)); done - check "reconnect after take-over keeps the interface" 'docker logs "$tapc" 2>&1 | grep -q "carrier on"' - sleep 2 - check "same address after reconnect" 'inc "ip -4 -o addr show vtesse0" | grep -q "inet $addr/"' - check "traffic works after reconnect (both directions)" \ - 'inc "ping -c 2 -W 2 10.78.0.10" >/dev/null && dev "ping -c 2 -W 2 $addr" >/dev/null' - - docker stop -t 5 "$tapc" >/dev/null - check "clean exit on SIGTERM" 'docker logs "$tapc" 2>&1 | grep -q "frames in"' - echo "--- client log"; docker logs "$tapc" 2>&1 | grep -v "^\(Internet\|Listening\|Sending\|Copyright\|For info\|DHCP\|bound\)" | tail -20 - docker rm -f "$tapc" >/dev/null 2>&1 - echo "$fails failure(s)" - [ "$fails" -eq 0 ] -} - -# type tun (#8): no DHCP over L3, the client address comes from ip=/gw=/route=. -# lwIP: HTTP to the device through the tunnel and inbound ping of the client; -# --tap (LWIP=0 build, as in tap-test): a TUN interface configured by the hook. cmd_tun_test() { repo=$(cd "$HERE/.." && pwd) cli="$repo/build/vtesse-cli" - tunc=vtesse-bench-tunclient [ -x "$cli" ] || { echo "build the CLI first: make"; exit 1; } fails=0 check() { if eval "$2"; then echo "PASS $1"; else echo "FAIL $1"; fails=$((fails + 1)); fi; } @@ -208,28 +145,73 @@ cmd_tun_test() { rm -f "$conf" "$log" sleep 1 - # --- TAP backend: TUN interface, addressing by the hook + echo "$fails failure(s)" + [ "$fails" -eq 0 ] +} + +# Inbound forwards (#67) and ip=learn (#68): vtesse-cli in a container on the bench +# network, type tun session 02BE0C000006 with ip=learn; a web server on its 127.0.0.1 +# is reached from the server through the tunnel address. +cmd_accept_test() { + repo=$(cd "$HERE/.." && pwd) + c=vtesse-bench-acceptclient + fails=0 + check() { if eval "$2"; then echo "PASS $1"; else echo "FAIL $1"; fails=$((fails + 1)); fi; } + srv() { docker exec "$SERVER" sh -c "$1"; } + logs() { docker logs "$c" 2>&1; } + # built for the bench image (Debian bookworm), like tun-test docker build -q -t vtesse-ci-native -f "$repo/ci/Dockerfile.native" "$repo/ci" >/dev/null docker run --rm --user "$(id -u):$(id -g)" -v "$repo:/src" -w /src vtesse-ci-native \ - make -s BUILD=build-bench-tap LWIP=0 all >/dev/null - docker rm -f "$tunc" >/dev/null 2>&1 || true - docker run -d --name "$tunc" --privileged --network "$NET" -v "$repo:/src:ro" "$IMAGE" sh -c " - printf 'host=$SERVER\nport=5000\nsession=$TUN_SESSION\npassword=$TUN_PASSWORD\n$tunconf' > /tmp/vtesse.conf - exec /src/build-bench-tap/vtesse-cli --tap --ifname vtun0 --hook /src/scripts/vtesse-hook.sh --config /tmp/vtesse.conf + make -s BUILD=build-bench-accept all >/dev/null + + docker rm -f "$c" >/dev/null 2>&1 || true + docker run -d --name "$c" --network "$NET" -v "$repo:/src:ro" "$IMAGE" sh -c " + mkdir -p /tmp/www && head -c 20971520 /dev/urandom > /tmp/www/big.bin + sha256sum /tmp/www/big.bin | cut -d' ' -f1 > /tmp/www/big.sha256 + python3 -m http.server 8000 --bind 127.0.0.1 --directory /tmp/www >/dev/null 2>&1 & + printf 'host=$SERVER\nport=5000\nsession=$TUN_SESSION\npassword=$TUN_PASSWORD\nip=learn\n' > /tmp/c.conf + printf 'accept=8080:127.0.0.1:8000\naccept=8081:127.0.0.1:8001\n' >> /tmp/c.conf + exec /src/build-bench-accept/vtesse-cli --no-env --config /tmp/c.conf " >/dev/null - inc() { docker exec "$tunc" sh -c "$1"; } i=0 - until inc 'ip -4 addr show vtun0 2>/dev/null | grep -q "inet 10\.76\.0\.2"' || [ $i -gt 15 ]; do sleep 1; i=$((i + 1)); done - check "tap: vtun0 is a TUN (L3) interface" 'inc "ip -d link show vtun0" | grep -q "tun type tun"' - check "tap: hook set 10.76.0.2 and the route to 10.78.0.0/24" \ - 'inc "ip -4 addr show vtun0" | grep -q "inet 10.76.0.2/32" && inc "ip route" | grep -q "10.78.0.0/24 dev vtun0"' - check "tap: no default route through the tunnel" '! inc "ip route" | grep -q "default.*vtun0"' - check "tap: ping the device from the client" 'inc "ping -c 2 -W 2 10.78.0.10" >/dev/null' - check "tap: HTTP to the device from any process" 'inc "curl -fsS --max-time 5 http://10.78.0.10/" | grep -q "vtesse bench device"' - check "tap: inbound: the device pings the client" 'dev "ping -c 2 -W 2 10.76.0.2" >/dev/null' - docker stop -t 5 "$tunc" >/dev/null - echo "--- tap client log"; docker logs "$tunc" 2>&1 | tail -8 - docker rm -f "$tunc" >/dev/null 2>&1 + until logs | grep -q "waiting for the first packet" || [ $i -gt 20 ]; do sleep 1; i=$((i + 1)); done + sha=$(docker exec "$c" cat /tmp/www/big.sha256) + check "ip=learn: no address before the first packet" '! logs | grep -q "address 10.76"' + check "accept: the server reaches the client's web server (first SYN teaches the address)" \ + 'srv "curl -fsS --max-time 10 -o /dev/null http://10.76.0.2:8080/big.sha256"' + check "ip=learn: address 10.76.0.2/32, peer 10.76.0.1" \ + 'logs | grep -q "address 10.76.0.2 mask 255.255.255.255 (learnt), peer 10.76.0.1"' + check "accept: 8 parallel 20 MiB downloads, sha256 intact" ' + srv "for n in 1 2 3 4 5 6 7 8; do (curl -fsS --max-time 120 http://10.76.0.2:8080/big.bin | sha256sum > /tmp/acc.\$n) & done; wait" + [ "$(srv "cat /tmp/acc.*" | cut -d" " -f1 | sort -u)" = "$sha" ]' + t0=$(date +%s) + check "accept to a closed local port: the connection is reset at once" \ + '! srv "curl -fsS --max-time 10 http://10.76.0.2:8081/" 2>/dev/null && [ $(($(date +%s) - t0)) -lt 5 ]' + check "accept: log lines and counters" 'logs | grep -q "accept 8080 -> 127.0.0.1:8000: connected" && + logs | grep -q "accept 8081 -> 127.0.0.1:8001: Connection refused"' + + # a changed address after a server restart is learnt again + srv "sed -i 's/peer 10.76.0.2 /peer 10.76.0.3 /' /etc/vtund.conf" + docker restart -t 2 "$SERVER" >/dev/null + i=0 + until [ "$(logs | grep -c "\] up: ")" -ge 2 ] || [ $i -gt 60 ]; do sleep 1; i=$((i + 1)); done + sleep 1 + check "after a reconnect with another address: 10.76.0.3 reached and learnt" \ + 'srv "curl -fsS --max-time 10 -o /dev/null http://10.76.0.3:8080/big.sha256" && + logs | grep -q "address 10.76.0.3 mask 255.255.255.255 (learnt)"' + srv "sed -i 's/peer 10.76.0.3 /peer 10.76.0.2 /' /etc/vtund.conf" + docker rm -f "$c" >/dev/null 2>&1 + docker restart -t 2 "$SERVER" >/dev/null + + # ip=learn on a type ether session: refused once the session is up + docker run -d --name "$c" --network "$NET" -v "$repo:/src:ro" "$IMAGE" sh -c " + printf 'host=$SERVER\nport=5000\nsession=$SESSION\npassword=$PASSWORD\nip=learn\n' > /tmp/c.conf + exec /src/build-bench-accept/vtesse-cli --no-env --config /tmp/c.conf + " >/dev/null + check "ip=learn on type ether: refused, exit 2" \ + '[ "$(timeout 60 docker wait "$c")" = 2 ] && logs | grep -q "ip=learn needs a type tun session"' + echo "--- client log"; docker logs "$c" 2>&1 | tail -4 + docker rm -f "$c" >/dev/null 2>&1 echo "$fails failure(s)" [ "$fails" -eq 0 ] } @@ -419,7 +401,7 @@ case "${1:-}" in stock-test) cmd_stock_test ;; probe) shift; cmd_probe "$@" ;; cli-test) cmd_cli_test ;; - tap-test) cmd_tap_test ;; + accept-test) cmd_accept_test ;; tun-test) cmd_tun_test ;; udp-test) cmd_udp_test ;; fault-test) cmd_fault_test ;; diff --git a/ci/build-musl.sh b/ci/build-musl.sh index 3a9a48e..e5269e7 100755 --- a/ci/build-musl.sh +++ b/ci/build-musl.sh @@ -9,8 +9,8 @@ # MediaTek/Atheros routers (MT7620/7621/7628, ath79), whose kernels often # lack FPU emulation, so hard-float code dies with SIGILL (#30) # arm64 | x86_64 | armv5 | armv7 Bootlin musl toolchains -# Produces /vtesse- (lwIP and TAP backends, --tap), stripped; a TAP-only -# build stays a build option (make LWIP=0) and is not published. For x86_64 also /vtund--x86_64: +# Produces /vtesse- (the userspace stack, proxy and inbound forwards), +# stripped. For x86_64 also /vtund--x86_64: # the VTun server with our patches (server/build-vtund.sh), a PC/server binary only; # VTUND=1 builds it for another target too, VTUND=0 skips it. With QEMU=1 the unit tests are run under # qemu-user as well. musl is used because a static glibc binary cannot resolve @@ -103,7 +103,7 @@ cd "$ROOT" build=build-$TARGET-full bin=vtesse-$TARGET rm -rf "$build" # objects of another toolchain or profile are never rebuilt by make -make -s BUILD="$build" CROSS_COMPILE="$CROSS" STATIC=1 LWIP=1 LWIP_PROFILE=$PROFILE CFLAGS=-Os all tests +make -s BUILD="$build" CROSS_COMPILE="$CROSS" STATIC=1 LWIP_PROFILE=$PROFILE CFLAGS=-Os all tests "${CROSS}strip" --strip-unneeded -o "$OUT/$bin" "$build/vtesse-cli" echo "$bin: $(wc -c < "$OUT/$bin") bytes (lwIP profile $PROFILE)" diff --git a/cli/install.c b/cli/install.c index 252b98f..69b2c06 100644 --- a/cli/install.c +++ b/cli/install.c @@ -12,12 +12,13 @@ * 4. writes the init script of the system from the built-in copy: S99vtesse, * the procd vtesse.init with its rc.d link and /lib/upgrade/keep.d/vtesse * (OpenWrt keeps only /etc across sysupgrade), or the systemd unit; - * 5. --scripts also writes the hook scripts to /libexec/vtesse (#50): - * the binary prefers them to its built-in copies, so they can be edited; + * 5. a file-based service gets accept=80, 22 and 554 when it has no accept line + * (#67): the camera's web, ssh and RTSP from the server, as the TAP backend of + * 1.x gave them; the hook scripts of 1.x are removed (#72); * 6. (re)starts the service, unless --no-start. * --uninstall stops the service and removes everything --install may have * written, /etc/vtesse.conf included, for any of the layouts. - * Both can run again: --install updates the binary, the scripts and the given + * Both can run again: --install updates the binary, the init script and the given * keys, --uninstall on a clean system is not an error. * * --root DIR does all of it below DIR and skips the service (tests, staging). @@ -41,14 +42,6 @@ #include "vtesse/vtun_proto.h" #include "vtesse_scripts.h" /* generated from scripts/ (tools/embed-scripts.sh) */ -#ifndef VTESSE_WITH_TAP -#ifdef __linux__ -#define VTESSE_WITH_TAP 1 -#else -#define VTESSE_WITH_TAP 0 -#endif -#endif - #define CONF_PATH "/etc/vtesse.conf" #define CONF_MAX (64 * 1024) @@ -60,24 +53,22 @@ typedef struct layout { const char *init; /* init script or unit */ const char *init_src; /* its built-in copy (scripts/) */ mode_t init_mode; - const char *libexec; /* hook scripts written by --scripts */ } layout_t; static const layout_t layouts[] = { - [SYS_OPENIPC] = { "OpenIPC", "/usr/bin/vtesse", "/etc/init.d/S99vtesse", "S99vtesse", 0755, - "/usr/libexec/vtesse" }, - [SYS_OPENWRT] = { "OpenWrt", "/usr/bin/vtesse", "/etc/init.d/vtesse", "vtesse.init", 0755, - "/usr/libexec/vtesse" }, + [SYS_OPENIPC] = { "OpenIPC", "/usr/bin/vtesse", "/etc/init.d/S99vtesse", "S99vtesse", 0755 }, + [SYS_OPENWRT] = { "OpenWrt", "/usr/bin/vtesse", "/etc/init.d/vtesse", "vtesse.init", 0755 }, [SYS_SYSTEMD] = { "Linux with systemd", "/usr/local/bin/vtesse", "/etc/systemd/system/vtesse.service", - "vtesse.service", 0644, "/usr/local/libexec/vtesse" }, + "vtesse.service", 0644 }, [SYS_BUSYBOX] = { "BusyBox init (/etc/init.d)", "/usr/bin/vtesse", "/etc/init.d/S99vtesse", "S99vtesse", - 0755, "/usr/libexec/vtesse" }, + 0755 }, }; #define OPENWRT_RC "/etc/rc.d/S99vtesse" #define OPENWRT_KEEP "/lib/upgrade/keep.d/vtesse" #define LEGACY_DIR "/usr/share/vtesse" /* the hook scripts up to 1.13 */ +/* the TAP backend's hook scripts of 1.x (removed in 2.0.0, #72): only cleaned up */ static const char *hook_scripts[] = { "vtesse-hook.sh", "vtesse-udhcpc.script" }; /* everything --uninstall removes, whatever the layout */ @@ -121,43 +112,6 @@ const char *vi_self_path(void) return path; } -int vi_run_embedded(int argc, char **argv) -{ - const char *text = vi_script("vtesse-udhcpc.script"); - char *args[16]; - int i, n = 0; - - unsetenv("VTESSE_RUN"); - if (!text) - return 127; - args[n++] = "sh"; - args[n++] = "-c"; - args[n++] = (char *)text; - args[n++] = "vtesse-udhcpc"; - for (i = 1; i < argc && n < 15; i++) - args[n++] = argv[i]; - args[n] = NULL; - execv("/bin/sh", args); - perror("vtesse: /bin/sh"); - return 127; -} - -const char *vi_libexec_hook(void) -{ - static char path[600]; - char dir[512]; - char *slash; - - snprintf(dir, sizeof(dir), "%s", vi_self_path()); - slash = strrchr(dir, '/'); - if (!slash) - return NULL; - *slash = '\0'; - /* /bin/vtesse -> /libexec/vtesse/vtesse-hook.sh */ - snprintf(path, sizeof(path), "%s/../libexec/vtesse/vtesse-hook.sh", dir); - return access(path, R_OK) == 0 ? path : NULL; -} - int vi_wanted(int argc, char **argv) { int i; @@ -519,7 +473,7 @@ static int build_config(const char *old, kv_t *set, int n, char *out, const ve_t { static char iface[64]; int has_host = 0, has_port = 0, bad_port = 0, has_pass = 0, has_session = 0, has_idfrom = 0, has_ip = 0, - has_gw = 0, i; + has_gw = 0, has_accept = 0, i; outbuf_t o = { out, 0 }; const char *p = old; char key[64]; @@ -588,6 +542,8 @@ static int build_config(const char *old, kv_t *set, int n, char *out, const ve_t has_ip = v; else if (!strcmp(key, "gw") || !strcmp(key, "route")) has_gw |= v; + else if (!strcmp(key, "accept")) + has_accept |= v; } p = e + 1; } @@ -616,6 +572,14 @@ static int build_config(const char *old, kv_t *set, int n, char *out, const ve_t fprintf(stderr, "vtesse: gw and route need ip (--ip A.B.C.D/P)\n"); return -1; } + /* inbound access for the server (#67), as the TAP backend of 1.x gave it: the + * camera's web, ssh and RTSP on its LAN address; with the U-Boot environment and + * no accept line the client opens the same itself */ + if (!has_accept && (!env || old)) { + if (out_kv(&o, "accept", "80") < 0 || out_kv(&o, "accept", "22") < 0 || out_kv(&o, "accept", "554") < 0) + return -1; + printf("accept=80, 22, 554: the server reaches the camera's web, ssh and RTSP (#67)\n"); + } if (env) return 0; if (!has_port && out_kv(&o, "port", "5000") < 0) @@ -695,7 +659,7 @@ static void usage(void) fprintf(stderr, "usage: vtesse --install [--host SERVER] [--port N] [--password PASSWORD|-]\n" " [--session ID | --id-from IFACE] [--ip A.B.C.D/P] [--gw A.B.C.D]\n" - " [--scripts] [--no-start] [--system SYSTEM] [--root DIR]\n" + " [--no-start] [--system SYSTEM] [--root DIR]\n" " vtesse --uninstall [--system SYSTEM] [--root DIR]\n" " SYSTEM: openipc, openwrt, systemd or busybox (default: detected)\n" " the keys go to /etc/vtesse.conf; an existing file keeps its other lines\n"); @@ -746,76 +710,53 @@ static int write_script(const char *path, const char *name, mode_t mode) return 0; } -/* Hook scripts of 1.13 and older in /usr/share/vtesse: moved to libexec, where the - * binary finds them, so that a hand-edited copy keeps working. Sets moved[i] for each - * script moved (--scripts must not overwrite it). The 1.13 path in an /etc/default/vtesse - * override (ARGS="... --hook /usr/share/vtesse/vtesse-hook.sh") is changed to match. */ -static void migrate_legacy(const layout_t *l, int moved[]) +/* The hook scripts of 1.x (libexec, and /usr/share/vtesse up to 1.13) belonged to + * the TAP backend, removed in 2.0.0 (#72): an update removes them. Never through a + * symlink: only a real directory of ours is emptied. Returns 0, or -1 when a file + * could not be removed. */ +static int remove_old_scripts(void) { - const char *def_path = rp("/etc/default/vtesse"); - char path[1100]; - size_t i, len; - char *def; - int named; - - /* Only an /etc/default/vtesse that names the old path chose those scripts on - * purpose: they move to libexec and stay in use. Otherwise they were the stock - * scripts of the old init script: they are kept as inactive *.1.13 copies and the - * built-in ones (with their fixes) take over. */ - snprintf(path, sizeof(path), "%s", def_path); - def = read_file(path, &len); - named = def && strstr(def, LEGACY_DIR "/") != NULL; - - for (i = 0; i < sizeof(hook_scripts) / sizeof(hook_scripts[0]); i++) { - char from[1024], to[1024]; - - moved[i] = 0; - snprintf(from, sizeof(from), "%s%s/%s", root, LEGACY_DIR, hook_scripts[i]); - snprintf(to, sizeof(to), "%s%s/%s%s", root, l->libexec, hook_scripts[i], named ? "" : ".1.13"); - if (!exists(from)) + static const char *dirs[] = { "/usr/libexec/vtesse", "/usr/local/libexec/vtesse", LEGACY_DIR }; + size_t i, j; + int rc = 0; + + for (i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) { + struct stat st; + char path[1024]; + + if (lstat(rp(dirs[i]), &st) < 0) continue; - if (exists(to)) { - /* both edited? keep both: the binary uses the libexec copy */ - fprintf(stderr, "vtesse: %s and %s both exist: %s is used, the other is left in place\n", - from, to, to); + if (!S_ISDIR(st.st_mode)) { + fprintf(stderr, "vtesse: %s is not a directory, left in place\n", rp(dirs[i])); continue; } - if (mkdir_p(rp(l->libexec)) == 0 && rename(from, to) == 0) { - moved[i] = named; - if (named) - printf("moved %s to %s, named in /etc/default/vtesse (delete it for the built-in copy)\n", from, to); - else - printf("kept %s as %s, unused: the built-in copy runs (rename it to use it)\n", from, to); - } else { - fprintf(stderr, "vtesse: cannot move %s to %s: %s\n", from, to, strerror(errno)); + for (j = 0; j < sizeof(hook_scripts) / sizeof(hook_scripts[0]); j++) { + snprintf(path, sizeof(path), "%s%s/%s", root, dirs[i], hook_scripts[j]); + if (remove_file(path) < 0) + rc = -1; + snprintf(path, sizeof(path), "%s%s/%s.1.13", root, dirs[i], hook_scripts[j]); + if (remove_file(path) < 0) + rc = -1; } + if (rmdir(rp(dirs[i])) == 0) + printf("removed %s (hook scripts of the TAP backend, gone in 2.0.0)\n", rp(dirs[i])); + else if (errno != ENOENT) + fprintf(stderr, "vtesse: %s is not empty, left in place\n", rp(dirs[i])); } - rmdir(rp(LEGACY_DIR)); + return rc; +} - /* only when the scripts are now in libexec: a failed move keeps the old paths */ - if (named && !exists(rp(LEGACY_DIR "/vtesse-hook.sh"))) { - struct stat st; - mode_t mode = stat(path, &st) == 0 ? st.st_mode & 07777 : 0644; - static char fixed[CONF_MAX + 1]; - const char *p = def, *hit; - size_t n = 0, add = strlen(l->libexec) + 1; - - while ((hit = strstr(p, LEGACY_DIR "/")) && n + (size_t)(hit - p) + add < CONF_MAX) { - memcpy(fixed + n, p, (size_t)(hit - p)); - n += (size_t)(hit - p); - snprintf(fixed + n, CONF_MAX + 1 - n, "%s/", l->libexec); - n += add; - p = hit + strlen(LEGACY_DIR "/"); - } - if (!hit && n + strlen(p) < CONF_MAX) { - memcpy(fixed + n, p, strlen(p)); - n += strlen(p); - if (write_file(path, fixed, n, mode) == 0) - printf("updated %s: %s/ -> %s/\n", path, LEGACY_DIR, l->libexec); - } else { - fprintf(stderr, "vtesse: %s still names %s/: change it by hand\n", path, LEGACY_DIR); - } - } +/* /etc/default/vtesse of 1.x may still pass TAP options (ARGS="--tap ..."): the client + * refuses them, so the service would not start. Said loudly, the file is not changed. */ +static void check_default_args(void) +{ + size_t len; + char *def = read_file(rp("/etc/default/vtesse"), &len); + + if (def && (strstr(def, "--tap") || strstr(def, "--hook") || strstr(def, "--ifname"))) + fprintf(stderr, "vtesse: warning: %s passes --tap/--hook/--ifname: the TAP backend was removed in " + "2.0.0 (#72) and the service will not start; remove those options\n", + rp("/etc/default/vtesse")); free(def); } @@ -879,42 +820,16 @@ static int service(sys_t sys, const layout_t *l, int start) return 0; } -/* udhcpc in PATH (the built-in hook does not fall back to dhclient) */ -static int have_udhcpc(void) -{ - const char *path = getenv("PATH"); - char dir[512], file[600]; - - while (path && *path) { - size_t n = strcspn(path, ":"); - - if (n && n < sizeof(dir)) { - memcpy(dir, path, n); - dir[n] = '\0'; - snprintf(file, sizeof(file), "%s/udhcpc", dir); - if (access(file, X_OK) == 0) - return 1; - } - path += n + (path[n] == ':'); - } - return 0; -} - -static int do_install(sys_t sys, kv_t *set, int n, int scripts, int start) +static int do_install(sys_t sys, kv_t *set, int n, int start) { char env_host[256] = ""; ve_t env; const layout_t *l = &layouts[sys]; static char conf[CONF_MAX + 1]; - int moved[sizeof(hook_scripts) / sizeof(hook_scripts[0])]; size_t len; char *old; int env_write; - if (!VTESSE_WITH_TAP) { - fprintf(stderr, "vtesse: --install needs the TAP backend (this binary is built with TAP=0)\n"); - return 1; - } printf("system: %s%s\n", l->name, root[0] ? " (below --root)" : ""); /* this device's environment: under --root it says nothing about the image */ memset(&env, 0, sizeof(env)); @@ -1022,7 +937,7 @@ static int do_install(sys_t sys, kv_t *set, int n, int scripts, int start) } } if (sys == SYS_OPENWRT) { - const char *keep = "/usr/bin/vtesse\n/usr/libexec/vtesse/\n/etc/init.d/vtesse\n" + const char *keep = "/usr/bin/vtesse\n/etc/init.d/vtesse\n" OPENWRT_RC "\n" CONF_PATH "\n" OPENWRT_KEEP "\n"; unlink(rp(OPENWRT_RC)); @@ -1037,23 +952,9 @@ static int do_install(sys_t sys, kv_t *set, int n, int scripts, int start) printf("wrote %s (kept by sysupgrade)\n", rp(OPENWRT_KEEP)); } - migrate_legacy(l, moved); - if (scripts) { - size_t i; - char path[1024]; - - for (i = 0; i < sizeof(hook_scripts) / sizeof(hook_scripts[0]); i++) { - snprintf(path, sizeof(path), "%s/%s", l->libexec, hook_scripts[i]); - if (moved[i]) - printf("kept %s (moved from %s; delete it for the built-in copy)\n", rp(path), LEGACY_DIR); - else if (write_script(rp(path), hook_scripts[i], 0755) < 0) - return 1; - } - } - - if (sys == SYS_SYSTEMD && !have_udhcpc()) - fprintf(stderr, "vtesse: warning: no udhcpc found: the built-in hook needs BusyBox udhcpc " - "(e.g. the udhcpc package) to get an address on vtesse0\n"); + if (remove_old_scripts() < 0) + return 1; + check_default_args(); if (root[0]) { printf("--root: the service is not started\n"); return 0; @@ -1254,7 +1155,7 @@ int vi_main(int argc, char **argv) static char password[256]; kv_t set[MAX_KEYS]; const char *sys_name = NULL; - int i, n = 0, install = 0, uninstall = 0, scripts = 0, start = 1; + int i, n = 0, install = 0, uninstall = 0, start = 1; sys_t sys; for (i = 1; i < argc; i++) { @@ -1265,8 +1166,10 @@ int vi_main(int argc, char **argv) install = 1; else if (!strcmp(a, "--uninstall")) uninstall = 1; - else if (!strcmp(a, "--scripts")) - scripts = 1; + else if (!strcmp(a, "--scripts")) { + fprintf(stderr, "vtesse: --scripts: the hook scripts went with the TAP backend in 2.0.0 (#72)\n"); + return 2; + } else if (!strcmp(a, "--no-start")) start = 0; else if (!strcmp(a, "--root") && i + 1 < argc) { @@ -1337,7 +1240,7 @@ int vi_main(int argc, char **argv) fprintf(stderr, "vtesse: --session and --id-from exclude each other\n"); return 2; } - if (uninstall && (n || scripts || !start)) { + if (uninstall && (n || !start)) { fprintf(stderr, "vtesse: --uninstall takes only --system and --root\n"); return 2; } @@ -1358,8 +1261,8 @@ int vi_main(int argc, char **argv) return do_uninstall(sys); if (sys == SYS_NONE) { fprintf(stderr, "vtesse: unknown system (no systemd, no BusyBox init): name one with --system,\n" - " or run the binary in the foreground: vtesse --config FILE --tap\n"); + " or run the binary in the foreground: vtesse --config FILE\n"); return 1; } - return do_install(sys, set, n, scripts, start); + return do_install(sys, set, n, start); } diff --git a/cli/install.h b/cli/install.h index 49e02b0..31185e9 100644 --- a/cli/install.h +++ b/cli/install.h @@ -2,7 +2,7 @@ * Self-install (#50, #51, #52): "vtesse --install" copies the running binary, * writes /etc/vtesse.conf from command-line keys, the init script for the * detected system and starts the service; "vtesse --uninstall" undoes it. - * The hook scripts are built into the binary (scripts/ at build time). + * The service scripts are built into the binary (scripts/ at build time). */ #ifndef VTESSE_CLI_INSTALL_H #define VTESSE_CLI_INSTALL_H @@ -19,12 +19,4 @@ const char *vi_script(const char *name); /* path of the running binary (/proc/self/exe), "" if unknown */ const char *vi_self_path(void); -/* /libexec/vtesse/vtesse-hook.sh next to the binary's /bin, if it - * exists ("vtesse --install --scripts"): used instead of the built-in hook */ -const char *vi_libexec_hook(void); - -/* "VTESSE_RUN=udhcpc-script": the built-in vtesse-udhcpc.script is run with argv[1] - * (udhcpc -s , set up by the built-in hook). Returns only on failure. */ -int vi_run_embedded(int argc, char **argv); - #endif diff --git a/cli/vtesse_cli.c b/cli/vtesse_cli.c index bbbc99e..2dc8dee 100644 --- a/cli/vtesse_cli.c +++ b/cli/vtesse_cli.c @@ -1,13 +1,11 @@ /* * vtesse-cli: Linux console client. * - * Two backends: - * - lwIP (default): userspace stack over the tunnel, address and routes via - * DHCP (type ether), local proxy: --socks PORT and --forward LPORT:IP:RPORT; - * --fetch performs one HTTP GET through the stack. - * - TAP (--tap): kernel interface through /dev/net/tun; addressing is done by - * the system (e.g. a DHCP client started from the --hook script). - * Build without lwIP: make LWIP=0 (TAP only). + * A userspace stack (lwIP) over the tunnel: address and routes via DHCP (type + * ether), manual or learnt from the first packet (type tun); local proxy: + * --socks PORT and --forward LPORT:IP:RPORT; inbound forwards from the tunnel + * address: --accept PORT[:IP:PORT]; --fetch performs one HTTP GET through the + * stack. No kernel interface: the TAP backend was removed in 2.0.0 (#72). * * Credentials are read from a key=value file, never from the command line * ("--install" writes that file from its keys, see install.c). @@ -30,37 +28,21 @@ #include "vtesse/vtun_client.h" #include "vtesse/vtun_proto.h" -#ifndef VTESSE_WITH_LWIP -#define VTESSE_WITH_LWIP 1 -#endif - -#if VTESSE_WITH_LWIP #include "lwip/ip4_addr.h" #include "lwip/tcp.h" #include "netcore/proxy.h" #include "netcore/stack.h" -#endif #include "netcore/udprelay.h" /* limits only; the relay itself needs lwIP */ -/* optional modules (make SOCKS=0 UDP_RELAY=0 TAP=0) */ +/* optional modules (make SOCKS=0 UDP_RELAY=0) */ #ifndef NC_WITH_SOCKS -#define NC_WITH_SOCKS VTESSE_WITH_LWIP +#define NC_WITH_SOCKS 1 #endif #ifndef VTESSE_WITH_UDP_RELAY -#define VTESSE_WITH_UDP_RELAY VTESSE_WITH_LWIP -#endif -#ifndef VTESSE_WITH_TAP -#ifdef __linux__ -#define VTESSE_WITH_TAP 1 -#else -#define VTESSE_WITH_TAP 0 -#endif -#endif -#if VTESSE_WITH_TAP -#include "netcore/tapdev.h" +#define VTESSE_WITH_UDP_RELAY 1 #endif -#if VTESSE_WITH_LWIP && !VTESSE_WITH_UDP_RELAY +#if !VTESSE_WITH_UDP_RELAY /* built without the UDP relay (udprelay.c not linked): the options are refused */ int nc_udp_relay_parse(const char *s, uint16_t *port, uint16_t *local_port) { @@ -96,6 +78,8 @@ typedef struct cfg_file { char route[16][32]; /* NET/P, repeatable */ char udp_relay[NC_UDP_RELAY_MAX][16]; /* PORT[:LOCAL_PORT], repeatable */ int udp_relay_count; + char accept[NC_PROXY_MAX_ACCEPTS][32]; /* PORT[:IP:PORT], repeatable (#67) */ + int accept_count; int route_count; char password[128]; char auth[16]; @@ -138,28 +122,22 @@ static void usage(void) { fprintf(stderr, "usage: vtesse-cli [--config FILE] [--seconds N] [-v] [--version]\n" -#if VTESSE_WITH_LWIP - " lwIP backend (default):\n" " [--socks PORT] [--forward LPORT:IP:RPORT]... [--listen ADDR]\n" + " [--accept PORT[:IP:PORT]]... (inbound: tunnel address:PORT -> host)\n" " [--fetch http://IP[:PORT]/PATH] [--udp-relay PORT[:LOCAL_PORT]]...\n" - " [--ip A.B.C.D/P [--gw A.B.C.D]] (manual IPv4 instead of DHCP)\n" -#endif -#if VTESSE_WITH_TAP - " TAP backend: --tap [--ifname NAME] [--hook SCRIPT|none]\n" - " (no --hook: /libexec/vtesse/vtesse-hook.sh, else the built-in one)\n" - " vtesse --install [--host SERVER --password PASSWORD ...] install as a service\n" - " vtesse --uninstall remove it (--install --help)\n" -#endif + " [--ip A.B.C.D/P|learn [--gw A.B.C.D]] (manual IPv4 instead of DHCP)\n" " --id-from IFACE device ID (session name) = MAC of IFACE, e.g. eth0\n" " --show-config print the settings in effect and exit\n" " --no-env ignore the U-Boot environment (fw_printenv vtesse, vtesse_*)\n" " FILE: key=value lines: host, port, session, password, auth (xor),\n" " optional id_from (instead of session: MAC of that interface),\n" " ip=A.B.C.D/P, gw=A.B.C.D, route=NET/P (repeatable): manual IPv4\n" + " (needed for type tun; ip=learn: the address of the first packet),\n" + " accept=PORT[:IP:PORT] (repeatable, up to %d): inbound forwards, by default\n" + " to the same port on the address of the default-route interface,\n" " udp_relay=PORT[:LOCAL_PORT] (repeatable): UDP to the tunnel address -> 127.0.0.1\n" - " (needed for type tun; with --tap passed to the hook),\n" " socks_user, socks_password (SOCKS5 user/password auth)\n" - " default " DEFAULT_CONFIG "\n"); + " default " DEFAULT_CONFIG "\n", NC_PROXY_MAX_ACCEPTS); } static void expand_home(const char *in, char *out, size_t size) @@ -224,7 +202,14 @@ static int load_config(const char *path, cfg_file_t *c, int must_exist) snprintf(c->ip, sizeof(c->ip), "%s", v); else if (!strcmp(k, "gw")) snprintf(c->gw, sizeof(c->gw), "%s", v); - else if (!strcmp(k, "udp_relay")) { + else if (!strcmp(k, "accept")) { + if (c->accept_count >= NC_PROXY_MAX_ACCEPTS) { + fprintf(stderr, "%s: at most %d accept lines\n", full, NC_PROXY_MAX_ACCEPTS); + fclose(f); + return -1; + } + snprintf(c->accept[c->accept_count++], sizeof(c->accept[0]), "%s", v); + } else if (!strcmp(k, "udp_relay")) { if (c->udp_relay_count >= NC_UDP_RELAY_MAX) { fprintf(stderr, "%s: at most %d udp_relay lines\n", full, NC_UDP_RELAY_MAX); fclose(f); @@ -386,7 +371,6 @@ static vt_t *vt; static void print_tunnel_event(int tev, int err); -#if VTESSE_WITH_LWIP /* ---- --fetch: one HTTP GET through lwIP ---- */ @@ -542,13 +526,14 @@ static int parse_forward(const char *s, nc_forward_t *f) /* ---- events ---- */ static int manual_ip; /* ip= / --ip: no DHCP */ -#if VTESSE_WITH_LWIP +static int learn_mode; /* ip=learn: type tun only (#68) */ +static int learnt_once; +static int config_error; /* found only once the session is up: exit code 2 */ static struct { uint16_t port, local; nc_udp_relay_t *r; } relays[NC_UDP_RELAY_MAX]; static int nrelays; -#endif static void print_info(void) { @@ -557,10 +542,14 @@ static void print_info(void) int i; nc_stack_get_info(&in); + learnt_once = 1; nc_ip4_format(in.addr, a, sizeof(a)); nc_ip4_format(in.mask, m, sizeof(m)); nc_ip4_format(in.gw, g, sizeof(g)); - if (manual_ip) + if (learn_mode) + printf("[%8.3f] address %s mask %s (learnt), peer %s%s\n", elapsed(t_start), a, m, g, + in.default_route ? " (default route)" : ""); + else if (manual_ip) printf("[%8.3f] address %s mask %s (manual), gw %s%s\n", elapsed(t_start), a, m, g, in.default_route ? " (default route)" : ""); else @@ -590,7 +579,16 @@ static void on_stack_event(void *user, int ev, int tev, int err) fflush(stdout); } -#endif /* VTESSE_WITH_LWIP */ + +static void print_accept(const nc_accept_t *a, const char *prefix) +{ + char ip[16]; + + if (a->ip) + nc_ip4_format(a->ip, ip, sizeof(ip)); + printf("%s%u -> %s:%u\n", prefix, (unsigned)a->port, a->ip ? ip : "(default-route address)", + (unsigned)a->local_port); +} static void print_tunnel_event(int tev, int err) { @@ -599,6 +597,13 @@ static void print_tunnel_event(int tev, int err) if (tev == TUNNEL_EV_UP) { vt_flags_format(vt_session_flags(vt), flags, sizeof(flags)); printf("[%8.3f] up: %s, flags %s\n", elapsed(t_start), vt_greeting(vt), flags); + if (learn_mode && vt_tunnel(vt).ops->link_type(vt) != TUNNEL_LINK_L3) { + printf("[%8.3f] ip=learn needs a type tun session; this one is type ether (use DHCP)\n", + elapsed(t_start)); + config_error = 1; + loop.stop = 1; + } else if (learn_mode && !learnt_once) + printf("[%8.3f] waiting for the first packet from the server for the address\n", elapsed(t_start)); } else if (err) { printf("[%8.3f] %s: %s\n", elapsed(t_start), tunnel_event_name(tev), tunnel_strerror(err)); } else { @@ -608,151 +613,6 @@ static void print_tunnel_event(int tev, int err) loop.stop = 1; } -#if VTESSE_WITH_TAP -/* ---- TAP backend: hook script ---- */ - -static const char *hook; /* --hook FILE, NULL: the built-in hook */ -static int no_hook; /* --hook none */ -static const char *session_name; -static nc_tap_t *tap; - -static void reap_children(void) -{ - while (waitpid(-1, NULL, WNOHANG) > 0) - ; -} - -/* Hooks still running, by PID: as PID 1 (in a container) we also inherit the DHCP - * client the hook daemonized, so waiting for "any child" would never return. */ -static pid_t hook_pids[16]; - -/* 1: the hook has exited (or was already reaped), 0: still running. EINTR is - * retried, so an interrupted check never counts as an exit. */ -static int hook_done(pid_t pid) -{ - pid_t r; - - while ((r = waitpid(pid, NULL, WNOHANG)) < 0 && errno == EINTR) - ; - return r != 0; /* pid, or -1 with ECHILD */ -} - -/* Waits for the hooks started so far: "stop" must not overtake an earlier "up" that - * has yet to start the DHCP client (it would be left running after we exit). A hook - * still running after HOOK_WAIT_MS is terminated, so shutdown cannot hang on it. */ -#define HOOK_WAIT_MS 5000 - -static void wait_hooks(void) -{ - uint64_t deadline = nc_now_ms() + HOOK_WAIT_MS; - size_t i; - - for (i = 0; i < sizeof(hook_pids) / sizeof(hook_pids[0]); i++) { - while (hook_pids[i] > 0) { - if (hook_done(hook_pids[i])) { - hook_pids[i] = 0; /* finished, or already reaped */ - } else if (nc_now_ms() >= deadline) { - uint64_t grace = nc_now_ms() + 1000; - printf("[%8.3f] hook: pid %d still running, terminated\n", elapsed(t_start), - (int)hook_pids[i]); - /* the whole group: also a udhcpc still in the hook's foreground */ - kill(-hook_pids[i], SIGTERM); - while (!hook_done(hook_pids[i]) && nc_now_ms() < grace) { - struct timespec ts = { 0, 20 * 1000000L }; - nanosleep(&ts, NULL); - } - kill(-hook_pids[i], SIGKILL); - while (waitpid(hook_pids[i], NULL, 0) < 0 && errno == EINTR) - ; - hook_pids[i] = 0; - } else { - struct timespec ts = { 0, 20 * 1000000L }; - nanosleep(&ts, NULL); - } - } - } -} - -/* Runs "$hook " asynchronously with VTESSE_* in the environment. */ -static void run_hook(const char *event) -{ - pid_t pid; - size_t slot; - - reap_children(); - if (no_hook) - return; - /* a free slot first: every hook we start must be one shutdown can wait for */ - for (slot = 0; slot < sizeof(hook_pids) / sizeof(hook_pids[0]); slot++) - if (hook_pids[slot] <= 0 || hook_done(hook_pids[slot])) - break; - if (slot == sizeof(hook_pids) / sizeof(hook_pids[0])) { - printf("[%8.3f] hook: too many hooks still running, \"%s\" not started\n", - elapsed(t_start), event); - return; - } - pid = fork(); - if (pid == 0) { - setpgid(0, 0); /* own process group, so a stuck hook can be ended as a whole */ - setenv("VTESSE_EVENT", event, 1); - setenv("VTESSE_IFNAME", nc_tap_ifname(tap), 1); - setenv("VTESSE_LINK", nc_tap_link(tap) == TUNNEL_LINK_L3 ? "tun" : "ether", 1); - setenv("VTESSE_SESSION", session_name, 1); - if (hook) - execl("/bin/sh", "sh", hook, event, (char *)NULL); - else { - char self[32]; - const char *path = vi_self_path(); - - /* the built-in hook runs the binary itself as its udhcpc script: by its - * path, which a daemonized udhcpc can keep using after we are gone (a - * replaced file is a newer vtesse); /proc//exe only when the file - * was removed, as that path is valid only while this process lives */ - snprintf(self, sizeof(self), "/proc/%d/exe", (int)getppid()); - setenv("VTESSE_SELF", path[0] && access(path, X_OK) == 0 ? path : self, 1); - execl("/bin/sh", "sh", "-c", vi_script("vtesse-hook.sh"), "vtesse-hook", event, (char *)NULL); - } - _exit(127); - } - if (pid < 0) - printf("[%8.3f] hook: fork failed: %s\n", elapsed(t_start), strerror(errno)); - else { - setpgid(pid, pid); /* also here: no race with a kill() before the child ran */ - hook_pids[slot] = pid; - } -} - -static void on_tap_event(void *user, int ev, int tev, int err) -{ - (void)user; - switch (ev) { - case NC_TAP_EV_CREATED: - printf("[%8.3f] interface %s (%s) created\n", elapsed(t_start), nc_tap_ifname(tap), - nc_tap_link(tap) == TUNNEL_LINK_L3 ? "tun" : "tap"); - if (no_hook) - printf("[%8.3f] --hook none: configure the address yourself (e.g. udhcpc -i %s)\n", - elapsed(t_start), nc_tap_ifname(tap)); - run_hook("up"); - break; - case NC_TAP_EV_RECONNECTED: - printf("[%8.3f] interface %s: carrier on\n", elapsed(t_start), nc_tap_ifname(tap)); - run_hook("reconnect"); - break; - case NC_TAP_EV_ERROR: - printf("[%8.3f] cannot create the interface: %s%s\n", elapsed(t_start), strerror(err), - err == EPERM || err == EACCES ? " (needs root or CAP_NET_ADMIN)" : - err == ENOENT || err == ENODEV ? " (no /dev/net/tun: load the tun module)" : ""); - loop.stop = 1; - break; - default: - print_tunnel_event(tev, err); - if (tev == TUNNEL_EV_DOWN && nc_tap_link(tap) != TUNNEL_LINK_NONE) - run_hook("down"); - break; - } - fflush(stdout); -} -#endif /* VTESSE_WITH_TAP */ int main(int argc, char **argv) @@ -768,24 +628,13 @@ int main(int argc, char **argv) uint8_t mac[6]; tunnel_t t; nc_source_t src; - int i, err, rc, use_tap = !VTESSE_WITH_LWIP, lwip_opts = 0, stop_early; -#if VTESSE_WITH_LWIP + int i, err, rc, stop_early, learn_ip = 0; nc_stack_cfg_t sc; nc_proxy_cfg_t pc; nc_proxy_t *px = NULL; -#endif -#if VTESSE_WITH_TAP - nc_tap_cfg_t tc; - memset(&tc, 0, sizeof(tc)); -#endif -#if VTESSE_WITH_LWIP memset(&sc, 0, sizeof(sc)); memset(&pc, 0, sizeof(pc)); -#endif - /* called by udhcpc as its script (the built-in hook sets this up) */ - if (getenv("VTESSE_RUN") && !strcmp(getenv("VTESSE_RUN"), "udhcpc-script")) - return vi_run_embedded(argc, argv); if (vi_wanted(argc, argv)) return vi_main(argc, argv); for (i = 1; i < argc; i++) { @@ -810,38 +659,21 @@ int main(int argc, char **argv) else if (!strcmp(argv[i], "--id-from") && i + 1 < argc) id_from = argv[++i]; else if (!strcmp(argv[i], "--version")) { - printf("vtesse-cli %s (%s%s%s%s%s)\n", VTESSE_VERSION, - VTESSE_WITH_LWIP ? "lwip, forwards" : "", - VTESSE_WITH_LWIP && NC_WITH_SOCKS ? ", socks5" : "", - VTESSE_WITH_LWIP && VTESSE_WITH_UDP_RELAY ? ", udp-relay" : "", - VTESSE_WITH_LWIP && VTESSE_WITH_TAP ? ", " : "", - VTESSE_WITH_TAP ? "tap" : ""); + printf("vtesse-cli %s (lwip, forwards, accept%s%s)\n", VTESSE_VERSION, + NC_WITH_SOCKS ? ", socks5" : "", VTESSE_WITH_UDP_RELAY ? ", udp-relay" : ""); return 0; } -#if VTESSE_WITH_TAP - else if (!strcmp(argv[i], "--tap")) - use_tap = 1; - else if (!strcmp(argv[i], "--ifname") && i + 1 < argc) - tc.ifname = argv[++i]; - else if (!strcmp(argv[i], "--hook") && i + 1 < argc) { - hook = argv[++i]; - no_hook = !strcmp(hook, "none"); /* the last --hook wins */ - if (no_hook) - hook = NULL; - } -#else else if (!strcmp(argv[i], "--tap") || !strcmp(argv[i], "--ifname") || !strcmp(argv[i], "--hook")) { - fprintf(stderr, "%s: built without the TAP/TUN backend (make TAP=0)\n", argv[i]); + fprintf(stderr, "%s: the TAP backend was removed in vtesse 2.0.0 (#72): the client runs the " + "userspace stack only; inbound access: --accept PORT\n", argv[i]); return 2; } -#endif -#if VTESSE_WITH_LWIP else if (!strcmp(argv[i], "--fetch") && i + 1 < argc) { if (parse_url(argv[++i]) < 0) { fprintf(stderr, "--fetch: expected http://IPv4[:port]/path\n"); return 2; } - fetch.wanted = lwip_opts = 1; + fetch.wanted = 1; } else if (!strcmp(argv[i], "--socks") && i + 1 < argc && !NC_WITH_SOCKS) { fprintf(stderr, "--socks: built without SOCKS5 (make SOCKS=0)\n"); return 2; @@ -852,7 +684,6 @@ int main(int argc, char **argv) return 2; } pc.socks_port = (uint16_t)p; - lwip_opts = 1; } else if (!strcmp(argv[i], "--forward") && i + 1 < argc) { if (pc.forward_count >= NC_PROXY_MAX_FORWARDS || parse_forward(argv[++i], &pc.forwards[pc.forward_count]) < 0) { @@ -860,7 +691,6 @@ int main(int argc, char **argv) return 2; } pc.forward_count++; - lwip_opts = 1; } else if (!strcmp(argv[i], "--udp-relay") && !VTESSE_WITH_UDP_RELAY) { fprintf(stderr, "--udp-relay: built without the UDP relay (make UDP_RELAY=0)\n"); return 2; @@ -871,29 +701,25 @@ int main(int argc, char **argv) return 2; } nrelays++; - lwip_opts = 1; + } else if (!strcmp(argv[i], "--accept") && i + 1 < argc) { + if (pc.accept_count >= NC_PROXY_MAX_ACCEPTS || + nc_accept_parse(argv[++i], &pc.accepts[pc.accept_count]) < 0) { + fprintf(stderr, "--accept: expected PORT or PORT:A.B.C.D:PORT (max %d)\n", NC_PROXY_MAX_ACCEPTS); + return 2; + } + pc.accept_count++; } else if (!strcmp(argv[i], "--listen") && i + 1 < argc) { pc.listen_addr = argv[++i]; - lwip_opts = 1; } else if ((!strcmp(argv[i], "--ip") || !strcmp(argv[i], "--debug-ip")) && i + 1 < argc) { ip_arg = argv[++i]; } else if ((!strcmp(argv[i], "--gw") || !strcmp(argv[i], "--debug-gw")) && i + 1 < argc) { gw_arg = argv[++i]; } -#endif else { usage(); return 2; } } - if (use_tap && lwip_opts) { - fprintf(stderr, "--tap cannot be combined with --socks/--forward/--listen/--fetch/--udp-relay\n"); - return 2; - } - if (!VTESSE_WITH_TAP && use_tap) { - fprintf(stderr, "built without any backend\n"); - return 2; - } if (!no_env) env_mode = ve_load(&env); rc = load_config(config, &c, !env_mode); @@ -929,14 +755,10 @@ int main(int argc, char **argv) return 2; } } - if (c.udp_relay_count && !(VTESSE_WITH_LWIP && VTESSE_WITH_UDP_RELAY)) { + if (c.udp_relay_count && !VTESSE_WITH_UDP_RELAY) { fprintf(stderr, "%s: udp_relay: built without the UDP relay\n", config); return 2; } - if (c.udp_relay_count && use_tap) { - fprintf(stderr, "%s: udp_relay needs the lwIP backend (with --tap the kernel receives UDP itself)\n", config); - return 2; - } if (id_from) snprintf(c.id_from, sizeof(c.id_from), "%s", id_from); if (c.id_from[0]) { @@ -977,8 +799,13 @@ int main(int argc, char **argv) fprintf(stderr, "gw/route need ip\n"); return 2; } - if (c.ip[0] && parse_prefix(c.ip, &st_addr, &st_mask) < 0) { - fprintf(stderr, "ip: expected A.B.C.D/P, got '%s'\n", c.ip); + learn_ip = !strcmp(c.ip, "learn"); /* type tun: the first packet tells it (#68) */ + if (learn_ip && c.gw[0]) { + fprintf(stderr, "ip=learn: no gw (the peer is the source of the first packet)\n"); + return 2; + } + if (c.ip[0] && !learn_ip && parse_prefix(c.ip, &st_addr, &st_mask) < 0) { + fprintf(stderr, "ip: expected A.B.C.D/P or learn, got '%s'\n", c.ip); return 2; } if (c.gw[0] && parse_ip4(c.gw, &st_gw) < 0) { @@ -998,11 +825,32 @@ int main(int argc, char **argv) nc_routes_add(&st_routes, net, (uint8_t)p, st_gw); } + /* inbound forwards from the file come after the command line ones (#67) */ + for (i = 0; i < c.accept_count; i++) { + if (pc.accept_count >= NC_PROXY_MAX_ACCEPTS || + nc_accept_parse(c.accept[i], &pc.accepts[pc.accept_count]) < 0) { + fprintf(stderr, "%s: accept: expected PORT or PORT:A.B.C.D:PORT (max %d), got '%s'\n", config, + NC_PROXY_MAX_ACCEPTS, c.accept[i]); + return 2; + } + pc.accept_count++; + } + /* the service started from the U-Boot environment: the camera's web, ssh and RTSP are + * reachable from the server, on the camera's own LAN address (its web asks for the + * password as from the LAN) */ + if (env_mode && !pc.accept_count) { + static const uint16_t def[] = { 80, 22, 554 }; + + for (i = 0; i < (int)(sizeof(def) / sizeof(def[0])); i++) { + pc.accepts[i].port = pc.accepts[i].local_port = def[i]; + pc.accepts[i].ip = 0; + } + pc.accept_count = i; + } if (show_config && !vt_session_name_valid(c.session)) { fprintf(stderr, "session '%s': no spaces, colons or control characters\n", c.session); return 2; } -#if VTESSE_WITH_LWIP /* the same check as at start, without creating the relays */ for (i = 0; show_config && i < c.udp_relay_count; i++) { uint16_t port, local; @@ -1012,7 +860,6 @@ int main(int argc, char **argv) return 2; } } -#endif if (show_config) { printf("host %s\nport %d\nsession %s\npassword %s\n", c.host, c.port, c.session, derived_password ? c.password : "(set)"); @@ -1022,6 +869,8 @@ int main(int argc, char **argv) printf("route %s\n", c.route[i]); for (i = 0; i < c.udp_relay_count; i++) printf("udp_relay %s\n", c.udp_relay[i]); + for (i = 0; i < pc.accept_count; i++) + print_accept(&pc.accepts[i], "accept "); if (c.socks_user[0]) printf("socks_user %s\nsocks_password (set)\n", c.socks_user); return 0; @@ -1059,101 +908,71 @@ int main(int argc, char **argv) src = nc_source_from_tunnel(&t); nc_loop_add(&loop, &src); -#if VTESSE_WITH_TAP - if (use_tap) { - memcpy(tc.mac, mac, 6); - /* manual IPv4 for the hook (it configures the interface) */ - if (c.ip[0]) { - char routes[16 * 32 + 16] = ""; - - for (i = 0; i < c.route_count; i++) { - strcat(routes, i ? " " : ""); - strcat(routes, c.route[i]); - } - setenv("VTESSE_IP", c.ip, 1); - if (c.gw[0]) - setenv("VTESSE_GW", c.gw, 1); - if (routes[0]) - setenv("VTESSE_ROUTES", routes, 1); - } - session_name = c.session; - if (!hook && !no_hook) - hook = vi_libexec_hook(); /* NULL: the built-in hook */ - printf("hook: %s\n", no_hook ? "none" : hook ? hook : "built-in"); - tap = nc_tap_create(&t, &tc, on_tap_event, NULL); - if (!tap) { - fprintf(stderr, "out of memory\n"); + memcpy(sc.mac, mac, 6); + if (learn_ip) { + sc.learn_ip = learn_mode = 1; + sc.routes = st_routes; + } else if (c.ip[0]) { + sc.static_ip = manual_ip = 1; + sc.addr = st_addr; + sc.mask = st_mask; + sc.gw = st_gw; + sc.routes = st_routes; + } + if (nc_stack_init(&t, &sc, on_stack_event, NULL) < 0) { + fprintf(stderr, "stack initialisation failed\n"); + return 2; + } + src = nc_stack_timer_source(); + nc_loop_add(&loop, &src); + /* UDP relays from the file come after the command line ones */ + for (i = 0; i < c.udp_relay_count; i++) { + if (nrelays >= NC_UDP_RELAY_MAX || + nc_udp_relay_parse(c.udp_relay[i], &relays[nrelays].port, &relays[nrelays].local) < 0) { + fprintf(stderr, "%s: udp_relay: expected PORT[:LOCAL_PORT] (max %d)\n", config, NC_UDP_RELAY_MAX); return 2; } - src = nc_tap_source(tap); - nc_loop_add(&loop, &src); + nrelays++; } -#endif -#if VTESSE_WITH_LWIP - if (!use_tap) { - memcpy(sc.mac, mac, 6); - if (c.ip[0]) { - sc.static_ip = manual_ip = 1; - sc.addr = st_addr; - sc.mask = st_mask; - sc.gw = st_gw; - sc.routes = st_routes; - } - if (nc_stack_init(&t, &sc, on_stack_event, NULL) < 0) { - fprintf(stderr, "stack initialisation failed\n"); + for (i = 0; i < nrelays; i++) { + char msg[128]; + + relays[i].r = nc_udp_relay_create(relays[i].port, NULL, relays[i].local, msg, sizeof(msg)); + if (!relays[i].r) { + fprintf(stderr, "%s\n", msg); return 2; } - src = nc_stack_timer_source(); - nc_loop_add(&loop, &src); - /* UDP relays from the file come after the command line ones */ - for (i = 0; i < c.udp_relay_count; i++) { - if (nrelays >= NC_UDP_RELAY_MAX || - nc_udp_relay_parse(c.udp_relay[i], &relays[nrelays].port, &relays[nrelays].local) < 0) { - fprintf(stderr, "%s: udp_relay: expected PORT[:LOCAL_PORT] (max %d)\n", config, NC_UDP_RELAY_MAX); - return 2; - } - nrelays++; + printf("udp relay :%u -> 127.0.0.1:%u\n", (unsigned)relays[i].port, (unsigned)relays[i].local); + } + if (pc.socks_port || pc.forward_count || pc.accept_count) { + pc.socks_user = c.socks_user; + pc.socks_password = c.socks_password; + px = nc_proxy_create(&pc, on_proxy_log, NULL); + memset(c.socks_password, 0, sizeof(c.socks_password)); + if (!px) { + fprintf(stderr, "cannot start the local proxy\n"); + return 2; } - for (i = 0; i < nrelays; i++) { - char msg[128]; - - relays[i].r = nc_udp_relay_create(relays[i].port, NULL, relays[i].local, msg, sizeof(msg)); - if (!relays[i].r) { - fprintf(stderr, "%s\n", msg); - return 2; - } - printf("udp relay :%u -> 127.0.0.1:%u\n", (unsigned)relays[i].port, (unsigned)relays[i].local); - } - if (pc.socks_port || pc.forward_count) { - pc.socks_user = c.socks_user; - pc.socks_password = c.socks_password; - px = nc_proxy_create(&pc, on_proxy_log, NULL); - memset(c.socks_password, 0, sizeof(c.socks_password)); - if (!px) { - fprintf(stderr, "cannot start the local proxy\n"); - return 2; - } - if (pc.socks_port) - printf("socks5 on %s:%u%s\n", pc.listen_addr ? pc.listen_addr : "127.0.0.1", - (unsigned)pc.socks_port, c.socks_user[0] ? " (user/password)" : ""); - for (i = 0; i < pc.forward_count; i++) { - char ip[16]; - nc_ip4_format(pc.forwards[i].ip, ip, sizeof(ip)); - printf("forward %s:%u -> %s:%u\n", pc.listen_addr ? pc.listen_addr : "127.0.0.1", - (unsigned)pc.forwards[i].local_port, ip, (unsigned)pc.forwards[i].port); - } - src = nc_proxy_source(px); - nc_loop_add(&loop, &src); + if (pc.socks_port) + printf("socks5 on %s:%u%s\n", pc.listen_addr ? pc.listen_addr : "127.0.0.1", + (unsigned)pc.socks_port, c.socks_user[0] ? " (user/password)" : ""); + for (i = 0; i < pc.forward_count; i++) { + char ip[16]; + nc_ip4_format(pc.forwards[i].ip, ip, sizeof(ip)); + printf("forward %s:%u -> %s:%u\n", pc.listen_addr ? pc.listen_addr : "127.0.0.1", + (unsigned)pc.forwards[i].local_port, ip, (unsigned)pc.forwards[i].port); } + for (i = 0; i < pc.accept_count; i++) + print_accept(&pc.accepts[i], "accept tunnel:"); + src = nc_proxy_source(px); + nc_loop_add(&loop, &src); } -#endif t_start = nc_now_ms(); vt_start(vt); nc_loop_run(&loop, seconds > 0 ? t_start + (uint64_t)(seconds * 1000) : 0); - rc = vt_is_up(vt) ? 0 : 1; -#if VTESSE_WITH_LWIP + rc = config_error ? 2 : vt_is_up(vt) ? 0 : 1; if (px) { nc_proxy_stats_t ps; nc_proxy_get_stats(px, &ps); @@ -1171,21 +990,6 @@ int main(int argc, char **argv) (unsigned)relays[i].port, us.datagrams, us.bytes, us.dropped); nc_udp_relay_destroy(relays[i].r); } -#endif -#if VTESSE_WITH_TAP - if (tap) { - nc_tap_stats_t ts; - nc_tap_get_stats(tap, &ts); - printf("[%8.3f] interface %s: frames in %lu, out %lu, dropped %lu\n", elapsed(t_start), - nc_tap_ifname(tap), ts.frames_in, ts.frames_out, ts.dropped); - if (nc_tap_link(tap) == TUNNEL_LINK_NONE && vt_is_up(vt)) - rc = 1; /* interface creation failed */ - wait_hooks(); - run_hook("stop"); - wait_hooks(); - nc_tap_destroy(tap); - } -#endif t.ops->close(vt); vt_destroy(vt); return rc; diff --git a/docs/architecture.md b/docs/architecture.md index da46c8b..382ad9d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -7,7 +7,8 @@ - [VTun client](#vtun-client) - [Userspace stack](#userspace-stack) - [Local proxy](#local-proxy) -- [Backends](#backends) +- [UDP relay](#udp-relay) +- [No kernel interface](#no-kernel-interface) - [Android](#android) ## Layers @@ -49,7 +50,7 @@ the tunnel; there is no system network interface. - **Explicit byte order** everywhere (frame headers, DHCP options); tests also run on big-endian MIPS. - **Small footprint:** `-Os`, strip; lwIP memory profiles (see below); optional - modules `LWIP=0`, `SOCKS=0`, `UDP_RELAY=0`, `TAP=0` + modules `SOCKS=0`, `UDP_RELAY=0` ([#29](https://github.com/OpenIPC/vtesse/issues/29), [usage](usage.md#building)). - **Protocol-agnostic upper layers.** Everything above the transport talks to `tunnel_ops_t` only, so the stack, proxy and Android layers can later serve another @@ -57,22 +58,22 @@ the tunnel; there is no system network interface. ## Repository layout -| Path | Contents | -|--------------------------------|---------------------------------------------------------------------------------------------------------| -| `include/netcore/`, `netcore/` | `tunnel_ops_t`, poll loop helper, route table, lwIP stack glue, proxy, TAP backend (protocol-agnostic) | -| `include/vtesse/`, `vtun/` | VTun wire format helpers and the VTun client | -| `cli/` | `vtesse-cli` | -| `port/lwip/` | lwIP configuration (`lwipopts.h`), `arch/cc.h`, hook prototypes | -| `third_party/lwip/` | vendored lwIP 2.2.1, unmodified subset | -| `tests/` | unit tests and client scenarios | -| `bench/` | Docker test bench | -| `android/` | Android app: Gradle project, JNI bridge, foreground service ([android/README.md](../android/README.md)) | -| `server/` | vtund build script and our VTun server patches (bench and release builds) | -| `ci/` | cross builds (`build-musl.sh`), build containers, big-endian MIPS tests under qemu | -| `.github/workflows/` | CI: tests, cross builds, test bench, releases | -| `tools/` | probe script | -| `scripts/` | hook and udhcpc scripts for the TAP backend, service scripts (`S99vtesse`, `vtesse.init`) | -| `docs/` | documentation | +| Path | Contents | +|--------------------------------|----------------------------------------------------------------------------------------------------------------| +| `include/netcore/`, `netcore/` | `tunnel_ops_t`, poll loop helper, route table, lwIP stack glue, proxy and inbound forwards (protocol-agnostic) | +| `include/vtesse/`, `vtun/` | VTun wire format helpers and the VTun client | +| `cli/` | `vtesse-cli` | +| `port/lwip/` | lwIP configuration (`lwipopts.h`), `arch/cc.h`, hook prototypes | +| `third_party/lwip/` | vendored lwIP 2.2.1, unmodified subset | +| `tests/` | unit tests and client scenarios | +| `bench/` | Docker test bench | +| `android/` | Android app: Gradle project, JNI bridge, foreground service ([android/README.md](../android/README.md)) | +| `server/` | vtund build script and our VTun server patches (bench and release builds) | +| `ci/` | cross builds (`build-musl.sh`), build containers, big-endian MIPS tests under qemu | +| `.github/workflows/` | CI: tests, cross builds, test bench, releases | +| `tools/` | probe script | +| `scripts/` | service scripts (`S99vtesse`, `vtesse.init`, `vtesse.service`), built into the binary for `--install` | +| `docs/` | documentation | ## Tunnel interface @@ -136,14 +137,18 @@ See [`include/netcore/tunnel.h`](../include/netcore/tunnel.h). - On every tunnel `UP` the ARP cache is flushed (the bridge MAC may have changed). - After a reconnect the link comes up again and lwIP requests the previous address (DHCP INIT-REBOOT); the lease is never released. +- `type tun` without DHCP: a manual address, or `learn_ip` (#68): the destination of + the first unicast packet from the tunnel becomes the address (`/32`) and its source + the gateway, set in `on_rx` before the packet enters lwIP (a first SYN is accepted); + every `UP` checks the next packet again and replaces a changed address. - `port/lwip/lwipopts.h`: memory profiles (`make LWIP_PROFILE=…`, #12), `MEM_ALIGNMENT` follows the pointer size, system byte-order functions. - | Profile | TCP window | Static pools (arm32 bss) | Peak RSS on a camera | Use | - |---|---|---|---|---| - | large (default) | ~125 KiB, window scaling | ~1.3 MiB | ~1 MiB | PCs, Android, arm64 | - | medium | 64 KiB | ~250 KiB | ~400 KiB | cameras, routers (arm32, mipsel, mips releases) | - | small | 4 × MSS | ~95 KiB | — | very tight devices; ~1 Mbit/s at 50 ms | + | Profile | TCP window | Static pools (arm32 bss) | Peak RSS on a camera | Use | + |-----------------|--------------------------|--------------------------|----------------------|-------------------------------------------------| + | large (default) | ~125 KiB, window scaling | ~1.3 MiB | ~1 MiB | PCs, Android, arm64 | + | medium | 64 KiB | ~250 KiB | ~400 KiB | cameras, routers (arm32, mipsel, mips releases) | + | small | 4 × MSS | ~95 KiB | — | very tight devices; ~1 Mbit/s at 50 ms | One connection carries at most window / RTT: medium gives ~3.4 Mbit/s at 150 ms and ~8.9 Mbit/s was measured at 48 ms on an OpenIPC camera (SigmaStar SSC337, 27 MiB RAM). @@ -162,6 +167,13 @@ TCP connections. - The proxy is a multi-fd loop source (`poll_fill` / `poll_dispatch`). - TCP-over-TCP: inner connections share one VTun TCP stream; on the bench a single download reaches ~110–140 MiB/s and 8 parallel downloads ~55 MiB/s in total. +- Inbound forwards (#67): an lwIP listener per `accept=` port (`MEMP_NUM_TCP_PCB_LISTEN` + 4) on any address of the netif; an accepted PCB gets a connection whose host socket + connects non-blocking (state `C_LCONNECT`) to the target, by default the address of + the default-route interface ([`hostaddr.c`](../netcore/hostaddr.c), looked up per + connection). Data from the tunnel waits in the PCB's receive queue, unacknowledged, + until the host socket is connected; from then on it is the same relay as an outbound + forward. A refused or timed-out host connect aborts the PCB (RST to the tunnel). User-facing behaviour (reply codes, limits): [usage](usage.md#local-proxy). @@ -174,25 +186,14 @@ UDP socket (`127.0.0.1:LOCAL_PORT`). No loop source of its own; a full local buf drops the datagram instead of stalling the tunnel. One direction only (camera → device), for RTP pushed by cameras. User-facing: [usage](usage.md#udp-relay-rtp-from-cameras). -## Backends - -Two consumers of `tunnel_ops_t`: - -| | lwIP (default) | TAP ([`tapdev.h`](../include/netcore/tapdev.h)) | -|---------------------------|-------------------------------------------|------------------------------------------------------| -| Interface in the system | none | `vtesse0` (TAP for `type ether`, TUN for `type tun`) | -| Who can use the tunnel | the application, programs using its proxy | every program | -| Inbound connections | no | yes | -| Addressing | built-in DHCP client (lwIP) | system DHCP client via the hook script | -| Privileges | none | root or `CAP_NET_ADMIN`, `tun` driver | -| Platforms | Linux, Android | Linux | -| Code size (x86_64, `-Os`) | ~107 KiB code + lwIP pools | ~23 KiB (`make LWIP=0`) | - -The TAP backend creates the interface on the first tunnel `UP`, keeps it across -reconnects and switches the carrier off while the tunnel is down -(`TUNSETCARRIER`; on kernels without it the interface just stays up and the hook -flushes neighbours). Frames are read in batches and dropped while the tunnel is down -or its output buffer is full; frames from the tunnel are written non-blocking. +## No kernel interface + +The userspace stack is the only consumer of `tunnel_ops_t` in the CLI: no root, no +`tun` driver, nothing changes in the system's network. The TAP backend (a kernel +interface through `/dev/net/tun`, addressed by hook and udhcpc scripts) was removed in +2.0.0 ([#72](https://github.com/OpenIPC/vtesse/issues/72)). What it gave beyond the +proxy, inbound access to the device's own services, comes from the inbound forwards +(below); a kernel-interface client for stock vtund servers is a separate project. ## Android diff --git a/docs/protocol.md b/docs/protocol.md index 3b72778..53ec728 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -140,8 +140,7 @@ waits ~0.5 s before its first data frame. Keepalive frames are not affected. no fixed MAC, the bridge takes a port's MAC, so the gateway MAC on the segment changes when taps join and leave. A client that cached the gateway MAC before a reconnect sends to a dead MAC until the gateway re-ARPs; VTesse flushes its ARP -cache on every reconnect (lwIP backend) or switches the interface carrier off while -the tunnel is down, which makes the kernel drop its neighbours (TAP backend). +cache on every reconnect. ## UDP transport diff --git a/docs/security.md b/docs/security.md index 61ab609..19a4ef9 100644 --- a/docs/security.md +++ b/docs/security.md @@ -35,9 +35,13 @@ - Credentials are read from a configuration file only (never from the command line, where other users could see them in the process list); keep it with mode 600. The CLI wipes the password from memory after use. -- Only outbound connections are possible through the lwIP backend: nothing behind the - tunnel can connect to services of the device running VTesse (the tunnel address - answers ARP and ping only). +- Without `accept=` only outbound connections are possible: nothing behind the tunnel + can connect to services of the device running VTesse (the tunnel address answers ARP + and ping only). Each `accept=` line ([inbound forwards](usage.md#inbound-forwards)) + opens one port of the device to everyone on the VTun server side, in clear text. The + service started by `vtesse --install` or the U-Boot environment opens 80, 22 and 554 + by default, to the device's LAN address: its own logins (web, ssh, RTSP) still + apply, so keep them set; remove the `accept` lines that are not needed. ## U-Boot environment diff --git a/docs/testing.md b/docs/testing.md index 09fc09d..f386108 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -10,10 +10,11 @@ | Bench self-test | `bench/bench.sh selftest` | server behaviour VTesse relies on (greeting, flags, keepalive, DHCP through the tunnel, `killold`, `ERR` cases, `multi no`) | | Stage 1 acceptance | `bench/bench.sh cli-test` | `vtesse-cli`: DHCP address and option 121 routes, option 3 ignored, HTTP through lwIP, ping of the client from the device, reconnect after a take-over with the same address | | Stage 2 acceptance | `bench/bench.sh proxy-test` | SOCKS5 reply codes, `curl --socks5`, 20 MiB download and 8 MiB echo with integrity checks, RTSP through a forward, 8 parallel downloads, SOCKS5 authentication | -| type tun | `bench/bench.sh tun-test` | `type tun` session with manual IPv4 (`ip`/`gw`/`route`): lwIP — HTTP to the device and inbound ping of the client; `--tap` — TUN interface configured by the hook, ping/HTTP to the device, inbound ping, no default route | +| type tun | `bench/bench.sh tun-test` | `type tun` session with manual IPv4 (`ip`/`gw`/`route`): HTTP to the device and inbound ping of the client | +| Inbound forwards | `bench/bench.sh accept-test` | `accept=` (#67) and `ip=learn` (#68) on a `type tun` session: no address before the first packet, the first SYN from the server teaches it, 8 parallel 20 MiB downloads from the client's local web server with sha256, a closed local port resets at once, a changed address after a server restart is learnt again, `ip=learn` on `type ether` stops with exit code 2 | +| Default-route wait | `bench/bench.sh route-wait-test` | (#73) in a container without a network the client waits and does not connect, SIGTERM during the wait is exit code 0, a default route ends the wait | | UDP relay | `bench/bench.sh udp-test` | the device behind the server sends 500 numbered 1200-byte datagrams and one 4000-byte (fragmented) datagram to the tunnel address; all arrive at `127.0.0.1:1559` in order and intact; relay statistics | | Failures | `bench/bench.sh fault-test` | link loss (server frozen past the keepalive timeout): DOWN, reconnect, same address; server restart: reconnect, same address; no option 121: option 3 becomes the default route; 2-minute leases: renewed at T1, address kept after expiry; dnsmasq restored (~4 min) | -| TAP backend | `bench/bench.sh tap-test` | `vtesse-cli --tap` (`LWIP=0` build) in a container with the built-in hook (no `--hook`): DHCP via udhcpc and the built-in udhcpc script, option 121 route, no default route, HTTP and a 20 MiB download from any process, **inbound** ping and TCP from the device, reconnect after a take-over keeps interface and address, clean exit | | Reference client | `bench/bench.sh stock-test` | the stock `vtund` client + `dhclient` reach the device through the bench | ## CI @@ -21,13 +22,13 @@ [`.github/workflows/build.yml`](../.github/workflows/build.yml) runs on every push and pull request: -| Job | What it does | -|----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `tests` | `make test`, the same under ASan/UBSan, the `LWIP=0` build | -| `build` (arm32, mipsel, arm64, mips, x86_64) | static musl binaries with `ci/build-musl.sh` (`vtesse`; patched `vtund` in the x86_64 job), unit tests and a `vtund -h` smoke test under qemu-user, artifacts | -| `bench` | the Docker test bench (vtund built with our patches): `selftest`, `cli-test`, `proxy-test`, `tap-test`, `tun-test`, `udp-test`, `fault-test` | -| `android` | the APKs in Docker (`android/build.sh`, `ci/Dockerfile.android`); the release APK is signed with the key from the repository secrets | -| `release` | push to `main`: replaces the `latest` release; binaries, `vtund--x86_64`, hook scripts, service scripts | +| Job | What it does | +|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `tests` | `make test`, the same under ASan/UBSan, the minimal build (`SOCKS=0 UDP_RELAY=0`) and its tests | +| `build` (arm32, mipsel, arm64, mips, x86_64) | static musl binaries with `ci/build-musl.sh` (`vtesse`; patched `vtund` in the x86_64 job), unit tests and a `vtund -h` smoke test under qemu-user, artifacts | +| `bench` | the Docker test bench (vtund built with our patches): `selftest`, `cli-test`, `proxy-test`, `accept-test`, `tun-test`, `udp-test`, `fault-test`, `route-wait-test` | +| `android` | the APKs in Docker (`android/build.sh`, `ci/Dockerfile.android`); the release APK is signed with the key from the repository secrets | +| `release` | push to `main`: replaces the `latest` release; binaries, `vtund--x86_64`, service scripts | ### Versions and releases @@ -41,7 +42,7 @@ is `MAJOR*10000 + MINOR*100 + PATCH` (1.13.0 → 11300), larger than the old `MA commit). No versioned releases are kept. - Its title is `VTesse `; the notes are one short line. The change history is [CHANGELOG.md](../CHANGELOG.md), the patch list [server/README.md](../server/README.md). -- File names are stable: `vtesse-` (one binary per platform, lwIP and TAP), and +- File names are stable: `vtesse-` (one binary per platform), and `vtund--x86_64` (e.g. `vtund-3.0.2-x86_64`) for the VTun server, PCs only. - PATCH for fixes and small changes, MINOR for a new feature, MAJOR for an incompatible change or a large update, only when the maintainer decides. A pull request that changes @@ -67,7 +68,7 @@ make bench/bench.sh selftest bench/bench.sh cli-test bench/bench.sh proxy-test -bench/bench.sh tap-test +bench/bench.sh accept-test bench/bench.sh down ``` @@ -116,8 +117,8 @@ and `mips` releases are soft-float (OpenWrt toolchains) and run on routers and I Self-install, 2026-10-18 (1.14.0, #50, #51, #52), from `/tmp` only: `--install --root /tmp/vtesse/root` with a copy of the device's own `/etc/os-release` or `/etc/openwrt_release`, so the system detection saw the real files while nothing outside `/tmp` was written. The -service itself was not started on the devices; the built-in hook and udhcpc script are run -end to end by `bench/bench.sh tap-test`. +service itself was not started on the devices; the built-in hook and udhcpc script were run +end to end by `bench/bench.sh tap-test` (both removed with the TAP backend in 2.0.0, #72). | Device | Checked | |--------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| diff --git a/docs/usage.md b/docs/usage.md index 2e2dd73..0607404 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -2,7 +2,7 @@ - [Building](#building) - [vtesse-cli](#vtesse-cli) -- [TAP backend](#tap-backend) +- [Inbound forwards](#inbound-forwards) - [Installation](#installation) - [U-Boot environment (helpdesk access)](#u-boot-environment-helpdesk-access) - [Configuration file](#configuration-file) @@ -20,9 +20,8 @@ C99 + POSIX, plain `make`. ```sh make # build/libnetcore.a, build/libvtesse.a, build/vtesse-cli make test # unit tests and client scenarios against a scripted server -make LWIP=0 # TAP backend only: no userspace stack and proxy make LWIP_PROFILE=medium # lwIP memory profile: large (default), medium, small -make SOCKS=0 UDP_RELAY=0 TAP=0 # leave out optional modules (default: all in) +make SOCKS=0 UDP_RELAY=0 # leave out optional modules (default: all in) make CROSS_COMPILE=mipsel-linux-musl- STATIC=1 # cross compile, static binary ``` @@ -47,18 +46,16 @@ OpenWrt MT7620 router, #30). Optional modules (#29), `vtesse-cli --version` lists what a binary contains: -| Switch | Leaves out | arm32/x86_64 code saved | -|---------------|---------------------------------------------------------------------|-------------------------| -| `LWIP=0` | lwIP stack, port forwards, SOCKS5, UDP relay (TAP/TUN backend only) | ~75 % | -| `SOCKS=0` | SOCKS5 server (port forwards stay) | ~1.7 KB | -| `UDP_RELAY=0` | UDP relay | ~2 KB | -| `TAP=0` | TAP/TUN backend (kernel interface) | ~5 KB | +| Switch | Leaves out | arm32/x86_64 code saved | +|---------------|------------------------------------|-------------------------| +| `SOCKS=0` | SOCKS5 server (port forwards stay) | ~1.7 KB | +| `UDP_RELAY=0` | UDP relay | ~2 KB | A disabled feature is refused with a clear message (`--socks: built without SOCKS5`). -Release binaries contain everything, one per platform; the `LWIP=0` build is not published -(build it with `make LWIP=0` when the flash is tight). +Release binaries contain everything, one per platform. The TAP backend (`--tap`, a kernel +interface) and the `LWIP=0` and `TAP=0` switches were removed in 2.0.0 (#72). -Each target gives `vtesse-` (lwIP and TAP backends, `--tap`); x86_64 also `vtund-3.0.2-x86_64`, the VTun server with our patches +Each target gives `vtesse-`; x86_64 also `vtund-3.0.2-x86_64`, the VTun server with our patches ([server/README.md](../server/README.md)). Use musl builds on devices: a static glibc binary cannot resolve host names without the NSS libraries of the same glibc. Prebuilt binaries are on the [releases page](https://github.com/OpenIPC/vtesse/releases) (`latest` is rebuilt @@ -69,35 +66,36 @@ from every push to `main`). ``` vtesse-cli [--config FILE] [--seconds N] [-v] [--socks PORT] [--forward LPORT:IP:RPORT]... [--listen ADDR] + [--accept PORT[:IP:PORT]]... [--fetch http://IP[:PORT]/PATH] [--udp-relay PORT[:LOCAL_PORT]]... - [--ip A.B.C.D/P [--gw A.B.C.D]] -vtesse-cli [--config FILE] [--seconds N] --tap [--ifname NAME] [--hook SCRIPT|none] + [--ip A.B.C.D/P|learn [--gw A.B.C.D]] vtesse --install [--host SERVER] [--port N] [--password PASSWORD|-] [--session ID | --id-from IFACE] [--ip A.B.C.D/P] [--gw A.B.C.D] - [--scripts] [--no-start] [--system SYSTEM] [--root DIR] + [--no-start] [--system SYSTEM] [--root DIR] vtesse --uninstall [--system SYSTEM] [--root DIR] ``` -| Option | Meaning | -|----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--config FILE` | configuration file, default `~/.config/vtesse/probe.conf` | -| `--seconds N` | stop after N seconds (default: run until SIGINT/SIGTERM) | -| `--id-from IFACE` | [device ID](#device-id) = MAC of the local interface `IFACE` (overrides `session`/`id_from` of the file) | -| `--socks PORT` | SOCKS5 server on `LISTEN:PORT` | -| `--forward LPORT:IP:RPORT` | TCP forward `LISTEN:LPORT → IP:RPORT` through the tunnel (repeatable, up to 16) | -| `--listen ADDR` | address of the local listeners, default `127.0.0.1` | -| `--udp-relay PORT[:LOCAL_PORT]` | [UDP relay](#udp-relay-rtp-from-cameras): datagrams to the tunnel address on `PORT` go to `127.0.0.1:LOCAL_PORT` (default the same port); repeatable, up to 3 | -| `--fetch URL` | one HTTP GET through the stack once an address is bound (diagnostics) | -| `--ip A.B.C.D/P`, `--gw A.B.C.D` | [manual IPv4](#manual-ipv4-type-tun) instead of DHCP; override `ip`/`gw` of the file (`--debug-ip`/`--debug-gw` are accepted as old names) | -| `--tap` | [TAP backend](#tap-backend): kernel interface instead of lwIP (Linux, root) | -| `--ifname NAME` | interface name for `--tap`, default `vtesse%d` | -| `--hook SCRIPT` | script run on interface events for `--tap`; default: the [built-in hook](#tap-backend), `none`: no hook | -| `--show-config` | print the settings in effect (file, [U-Boot environment](#u-boot-environment-helpdesk-access), defaults) and exit | -| `--no-env` | ignore the U-Boot environment | -| `--install`, `--uninstall` | [install](#installation) the binary as a service, or remove it | +| Option | Meaning | +|----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--config FILE` | configuration file, default `~/.config/vtesse/probe.conf` | +| `--seconds N` | stop after N seconds (default: run until SIGINT/SIGTERM) | +| `--id-from IFACE` | [device ID](#device-id) = MAC of the local interface `IFACE` (overrides `session`/`id_from` of the file) | +| `--socks PORT` | SOCKS5 server on `LISTEN:PORT` | +| `--forward LPORT:IP:RPORT` | TCP forward `LISTEN:LPORT → IP:RPORT` through the tunnel (repeatable, up to 16) | +| `--listen ADDR` | address of the local listeners, default `127.0.0.1` | +| `--accept PORT[:IP:PORT]` | [inbound forward](#inbound-forwards): connections from the tunnel to `PORT` of the tunnel address go to `IP:PORT` on this machine (default: the same port on the address of the default-route interface); repeatable, up to 4 | +| `--udp-relay PORT[:LOCAL_PORT]` | [UDP relay](#udp-relay-rtp-from-cameras): datagrams to the tunnel address on `PORT` go to `127.0.0.1:LOCAL_PORT` (default the same port); repeatable, up to 3 | +| `--fetch URL` | one HTTP GET through the stack once an address is bound (diagnostics) | +| `--ip A.B.C.D/P`, `--gw A.B.C.D` | [manual IPv4](#manual-ipv4-type-tun) instead of DHCP; override `ip`/`gw` of the file (`--debug-ip`/`--debug-gw` are accepted as old names) | +| `--ip learn` | `type tun`: the address is learnt from the first packet the server sends ([manual IPv4](#manual-ipv4-type-tun)) | +| `--show-config` | print the settings in effect (file, [U-Boot environment](#u-boot-environment-helpdesk-access), defaults) and exit | +| `--no-env` | ignore the U-Boot environment | +| `--install`, `--uninstall` | [install](#installation) the binary as a service, or remove it | The CLI brings the tunnel and the userspace stack up, prints link events, the DHCP -address and routes, runs the local proxy and prints proxy statistics on exit. Exit +address and routes, runs the local proxy and the inbound forwards and prints proxy +statistics on exit. There is no kernel interface: the TAP backend (`--tap`, `--ifname`, +`--hook`) was removed in 2.0.0 (#72), and those options are refused with that reason. Exit code 0 means the tunnel was up at the end (and `--fetch`, if given, succeeded). Before it starts, the client waits for a default route (any interface): without one @@ -122,46 +120,36 @@ ffplay -rtsp_transport tcp rtsp://127.0.0.1:8554/stream [ 3.522] fetch 10.78.0.10:80/: ok, 205 bytes, "HTTP/1.0 200 OK" ``` -## TAP backend - -`--tap` passes tunnel frames to a kernel interface (`/dev/net/tun`) instead of the -userspace stack. Every program on the device then uses the tunnel, including -**inbound** connections to the device's own services. Requires root or -`CAP_NET_ADMIN` and the `tun` driver; not available on Android. - -- The interface is created on the first tunnel `UP`: TAP for `type ether`, TUN for - `type tun`; MAC = device ID, MTU 1500, IPv6 disabled on it. -- It is kept across reconnects; while the tunnel is down the carrier is off (which - also clears the kernel's ARP neighbours, see - [protocol](protocol.md#server-behaviour-that-affects-clients)), so addresses and - routes survive a reconnect. -- Addressing is done by the system: a hook runs `SCRIPT ` with - `VTESSE_EVENT`, `VTESSE_IFNAME`, `VTESSE_LINK` (`ether`/`tun`) and - `VTESSE_SESSION` in the environment; events: `up` (interface created), `reconnect`, - `down`, `stop`. - -The hook scripts are built into the binary (#52). Without `--hook` it runs -`/libexec/vtesse/vtesse-hook.sh` next to its `/bin` if that file exists -(`vtesse --install --scripts` writes it, to be edited), otherwise its built-in copy of -[`scripts/vtesse-hook.sh`](../scripts/vtesse-hook.sh); the binary itself then serves as -the `udhcpc` script (called with `VTESSE_RUN=udhcpc-script`, it runs its built-in copy of -[`scripts/vtesse-udhcpc.script`](../scripts/vtesse-udhcpc.script)). `--hook FILE` runs -that file, `--hook none` runs no hook. The hook starts BusyBox `udhcpc` with -`vtesse-udhcpc.script`, which - -- sets the address and installs the option 121 (or 249) routes via the interface; -- **never touches `/etc/resolv.conf`** — stock DHCP scripts replace the device's - resolver with the tunnel DNS, which may be unreachable and breaks resolving the - VTun server itself (found on the bench); -- installs a default route through the tunnel only with `VTESSE_DEFAULT_ROUTE=1` — it - would also capture the connection to the VTun server unless a host route to the - server exists. - -`dhclient` is a fallback of a hook file only; its stock script has the two problems above. The built-in hook needs BusyBox `udhcpc` (on a PC: the `udhcpc` package) and refuses to fall back; `--install` warns on a PC without it. +## Inbound forwards + +Without a kernel interface the device's own services (web interface, ssh, RTSP) are +not on the tunnel by themselves. `accept=PORT[:IP:PORT]` (or `--accept`, #67) makes the +stack listen on `PORT` of the tunnel address and relays every connection from the +tunnel to a host socket: -```sh -vtesse-cli --config /etc/vtesse.conf --tap --ifname vtesse0 ``` +accept=80 # tunnel address:80 -> the device's LAN address:80 +accept=22 +accept=8554:127.0.0.1:554 # explicit target +``` + +- Without `IP:PORT` the target is the same port on the address of the default-route + interface, looked up at each connection: the device's own LAN address. Its services + see a LAN client, so a web interface that trusts `127.0.0.1` still asks for the + password. `127.0.0.1` has to be named explicitly. +- Up to 4 lines; command-line `--accept` come before the file's. Inbound connections + share the proxy's connection limit (24); over it, and when the local port refuses or + does not answer within 15 s, the connection from the tunnel is reset. +- Each connection is logged (`accept 80 -> 192.168.1.10:80: connected`, or the error) + and counted in the proxy statistics. +- The service: `vtesse --install` writes `accept=80`, `accept=22` and `accept=554` to a + configuration without `accept` lines, and a client started from the + [U-Boot environment](#u-boot-environment-helpdesk-access) without `accept` lines opens + the same ports: the server reaches the camera's web interface, ssh and RTSP, as the + TAP backend of 1.x gave it. +- On a `type tun` session the server needs the client's address before it can connect: + set it with `ip=`, or let the client learn it from the first connection + (`ip=learn`, [manual IPv4](#manual-ipv4-type-tun)). ## Installation @@ -175,12 +163,12 @@ vtesse --uninstall 1. It detects the system, or takes `--system`: - | System | Detected by | Binary | Service | Hook copies (`--scripts`) | - |------------------------|----------------------------------------------------------|-------------------------|-----------------------------------------------------------------------------------|-----------------------------| - | `openwrt` | `/etc/openwrt_release` | `/usr/bin/vtesse` | `/etc/init.d/vtesse` (procd), `/etc/rc.d/S99vtesse`, `/lib/upgrade/keep.d/vtesse` | `/usr/libexec/vtesse` | - | `openipc` | `OPENIPC_VERSION` in `/etc/os-release` | `/usr/bin/vtesse` | `/etc/init.d/S99vtesse` | `/usr/libexec/vtesse` | - | `systemd` (a Linux PC) | `/run/systemd/system` | `/usr/local/bin/vtesse` | `/etc/systemd/system/vtesse.service` | `/usr/local/libexec/vtesse` | - | `busybox` | BusyBox init: `/etc/init.d/rcS` runs `S??*` (not OpenRC) | `/usr/bin/vtesse` | `/etc/init.d/S99vtesse` | `/usr/libexec/vtesse` | + | System | Detected by | Binary | Service | + |------------------------|----------------------------------------------------------|-------------------------|-----------------------------------------------------------------------------------| + | `openwrt` | `/etc/openwrt_release` | `/usr/bin/vtesse` | `/etc/init.d/vtesse` (procd), `/etc/rc.d/S99vtesse`, `/lib/upgrade/keep.d/vtesse` | + | `openipc` | `OPENIPC_VERSION` in `/etc/os-release` | `/usr/bin/vtesse` | `/etc/init.d/S99vtesse` | + | `systemd` (a Linux PC) | `/run/systemd/system` | `/usr/local/bin/vtesse` | `/etc/systemd/system/vtesse.service` | + | `busybox` | BusyBox init: `/etc/init.d/rcS` runs `S??*` (not OpenRC) | `/usr/bin/vtesse` | `/etc/init.d/S99vtesse` | 2. It writes `/etc/vtesse.conf` (mode 600) from the keys `--host`, `--port`, `--password`, `--session` or `--id-from`, `--ip`, `--gw` ([configuration file](#configuration-file)). @@ -189,18 +177,19 @@ vtesse --uninstall `id_from=` the interface of the default route (it must have a MAC; with `--root` the identity must be given, as this machine's interfaces say nothing about the image). `--password -` reads the password from standard input instead of the command line (which other users can see in `ps`). - Without a configuration and without keys nothing is installed. + Without a configuration and without keys nothing is installed. A configuration + without `accept` lines gets `accept=80`, `accept=22` and `accept=554` + ([inbound forwards](#inbound-forwards)). 3. It copies itself, writes the service file from its built-in copy and (re)starts the service; `--no-start` only installs. Only the binary, the service file and the - configuration are written: the hook scripts run from the binary. `--scripts` also - writes them to the libexec directory, where the binary finds them. + configuration are written. 4. On OpenWrt `sysupgrade` keeps only the changed files under `/etc`: the keep list in - `/lib/upgrade/keep.d/vtesse` carries the binary and the scripts over. -5. Hook scripts of 1.13 and older in `/usr/share/vtesse`: when `/etc/default/vtesse` - names that path, they were chosen on purpose and move to the libexec directory, where - they stay in use (the path in the file is changed, `--scripts` does not overwrite - them); otherwise they were the stock scripts of the old init script and are kept as - inactive `*.1.13` copies there, and the built-in scripts take over. + `/lib/upgrade/keep.d/vtesse` carries the binary and the init script over. +5. Updating a 1.x installation removes the hook scripts of the TAP backend (in + `/usr/libexec/vtesse`, `/usr/local/libexec/vtesse`, and `/usr/share/vtesse` up to + 1.13). An `/etc/default/vtesse` that still passes `--tap`, `--ifname` or `--hook` is + reported and left alone: the client refuses those options, so the service does not + start until they are removed. `--scripts` is refused. 6. Values are checked against the limits of the service: no spaces around a value, a password of up to 127 characters, a session name of up to 42 characters without spaces or colons. A kept `/etc/vtesse.conf` is set to mode 600 as well. @@ -214,8 +203,8 @@ an error. `--root DIR` does the same below `DIR` without touching the service `/etc/init.d` but another init (OpenRC, sysvinit) is not guessed: name it with `--system`. The service files are also in [`scripts/`](../scripts): `S99vtesse` -(`start|stop|restart|status`, overrides in `/etc/default/vtesse`, loads the `tun` -module), `vtesse.init` (procd, respawn) and `vtesse.service` (systemd, restart). +(`start|stop|restart|status`, overrides in `/etc/default/vtesse`), `vtesse.init` (procd, +respawn) and `vtesse.service` (systemd, restart). ## U-Boot environment (helpdesk access) @@ -250,8 +239,11 @@ Optional variables override these and the file, one key at a time: | `vtesse_password` | `password` | | `vtesse_ip`, `vtesse_gw` | `ip`, `gw` | -Everything else (`route`, `udp_relay`, SOCKS5 users, `auth`) is set only in -`/etc/vtesse.conf`. The order for each key: command line, U-Boot environment, +Everything else (`route`, `udp_relay`, `accept`, SOCKS5 users, `auth`) is set only in +`/etc/vtesse.conf`. Without `accept` lines there, a client started with `vtesse` set +opens ports 80, 22 and 554 to the server ([inbound forwards](#inbound-forwards)): the +camera's web interface, ssh and RTSP on its own LAN address. `vtesse_ip=learn` takes +the address from the first packet of a `type tun` session. The order for each key: command line, U-Boot environment, `/etc/vtesse.conf`, the VTun defaults (only with `vtesse` set). The variables are read with `fw_printenv`; without it (a PC, most OpenWrt routers) this step is skipped. `--no-env` ignores them; `--show-config` prints the result. @@ -276,17 +268,18 @@ on OpenIPC ([security](security.md)). `key=value` lines, `#` starts a comment. Credentials are read only from this file, never from the command line; keep it outside the repository with mode 600. -| Key | Meaning | -|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| -| `host` | VTun server name or IPv4 address | -| `port` | VTun server TCP port | -| `session` | session name = [device ID](#device-id) | -| `id_from` | instead of `session`: take the device ID from the MAC of this interface, e.g. `eth0` | -| `udp_relay` | `PORT[:LOCAL_PORT]`, repeatable: [UDP relay](#udp-relay-rtp-from-cameras) (lwIP backend) | -| `ip`, `gw`, `route` | [manual IPv4](#manual-ipv4-type-tun): `ip=A.B.C.D/P`, optional `gw=A.B.C.D`, `route=NET/P` (repeatable, up to 16); required for `type tun` | -| `password` | session password | -| `auth` | challenge response, `xor` (default; servers built without OpenSSL) | -| `socks_user`, `socks_password` | optional SOCKS5 username/password (RFC 1929) | +| Key | Meaning | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `host` | VTun server name or IPv4 address | +| `port` | VTun server TCP port | +| `session` | session name = [device ID](#device-id) | +| `id_from` | instead of `session`: take the device ID from the MAC of this interface, e.g. `eth0` | +| `udp_relay` | `PORT[:LOCAL_PORT]`, repeatable: [UDP relay](#udp-relay-rtp-from-cameras) | +| `accept` | `PORT[:IP:PORT]`, repeatable, up to 4: [inbound forwards](#inbound-forwards) | +| `ip`, `gw`, `route` | [manual IPv4](#manual-ipv4-type-tun): `ip=A.B.C.D/P` or `ip=learn`, optional `gw=A.B.C.D`, `route=NET/P` (repeatable, up to 16); required for `type tun` | +| `password` | session password | +| `auth` | challenge response, `xor` (default; servers built without OpenSSL) | +| `socks_user`, `socks_password` | optional SOCKS5 username/password (RFC 1929) | ## Device ID @@ -303,8 +296,7 @@ client's Ethernet MAC in `type ether` sessions. `/sys/class/net/eth0/address` and uses it as the session name, e.g. `a4:5e:60:12:34:5c` → `A45E6012345C`. The CLI prints `device ID … (MAC of eth0)` and refuses a multicast or all-zero MAC. `session` and `id_from` are mutually - exclusive in the file. With `--tap` the tunnel interface gets the same MAC as - `eth0`; they are different L2 segments, so this is harmless. + exclusive in the file. - Android: the hardware MAC is not available to apps — a random locally administered unicast ID is generated once and stored (second character `2`, `6`, `A` or `E`), or the ID is entered manually. A session with that name must exist on the server. @@ -340,12 +332,28 @@ route=10.78.0.0/24 # networks behind the server, repeatable - `route` lines are the reachable networks, like option 121; without them and with `gw` there is a default route through the tunnel. -- lwIP backend: applied when the tunnel comes up; the CLI prints - `address … (manual)`. The same keys work on a `type ether` session instead of DHCP. -- TAP backend (`--tap`): a TUN interface is created; `vtesse-hook.sh` gets - `VTESSE_IP`, `VTESSE_GW`, `VTESSE_ROUTES` and sets the address and routes - (`ip addr`/`ip route`) instead of starting DHCP. A default route via `gw` is added - only with `VTESSE_DEFAULT_ROUTE=1`. +- Applied when the tunnel comes up; the CLI prints `address … (manual)`. The same + keys work on a `type ether` session instead of DHCP. + +`ip=learn` (or `--ip learn`, #68): VTun does not hand out addresses, so with a manual +address every device needs its own configuration. A stock server keeps the address in +the session's `up {}` and routes only that address into the session, so the +destination of the first unicast IPv4 packet from the server is ours, and its source is +the server's end: + +- the address (`/32`) and the peer are set before that packet enters the stack, so a + first SYN (an [inbound forward](#inbound-forwards)) is accepted; +- broadcast, multicast and `0.0.0.0` destinations are ignored; +- after a reconnect the next packet is checked again: a changed address replaces the + old one, the same address changes nothing; +- `route` lines apply as with a manual address; without them the default route points + into the link (the peer). `gw` is refused (the peer is learnt); +- until the address is known, outbound use (SOCKS5, forwards) fails as without DHCP; + the CLI prints `waiting for the first packet from the server for the address`, then + `address … (learnt), peer …`; +- `type tun` only: on a `type ether` session the client stops (exit code 2) and DHCP is + the way. To make the address known right after connect, the server can send a packet + from `up {}` (e.g. `program "ping -c1
"`). - Acceptance: `bench/bench.sh tun-test` (both backends). ## Local proxy @@ -417,7 +425,6 @@ via `--udp-relay 1559:5600`). as IP fragments and are reassembled. - The tunnel address is stable: the DHCP lease is kept and requested again after a reconnect, so the camera's setting stays valid. -- lwIP backend only: with `--tap` the kernel receives UDP on the interface itself. - Statistics at exit: `udp relay :1559: datagrams N, B bytes, dropped D`. - Android: per-server option on port S559 (1559, 2559, 3559), see [android/README.md](../android/README.md). diff --git a/include/netcore/hostaddr.h b/include/netcore/hostaddr.h new file mode 100644 index 0000000..2251164 --- /dev/null +++ b/include/netcore/hostaddr.h @@ -0,0 +1,14 @@ +/* + * Addresses of the host itself (not of the tunnel). + */ +#ifndef NETCORE_HOSTADDR_H +#define NETCORE_HOSTADDR_H + +#include + +/* The IPv4 address (host byte order) of the interface of the first default route + * in /proc/net/route; 0: no default route or no IPv4 address on it. Inbound + * forwards (#67) go there by default: the camera's own LAN address. */ +uint32_t nc_default_route_addr(void); + +#endif diff --git a/include/netcore/proxy.h b/include/netcore/proxy.h index cbdcc7d..511fd59 100644 --- a/include/netcore/proxy.h +++ b/include/netcore/proxy.h @@ -1,8 +1,10 @@ /* * Local proxy: SOCKS5 (CONNECT) and static TCP port forwards from host - * sockets to destinations behind the tunnel, through the lwIP stack. + * sockets to destinations behind the tunnel, through the lwIP stack, and + * inbound forwards (#67): connections from the tunnel to a port of the + * tunnel address are relayed to a host socket. * - * Outbound TCP only, IPv4 only. Flow control is end to end: data is read from + * TCP only, IPv4 only. Flow control is end to end: data is read from * a local socket only when lwIP has send buffer space, and data received from * lwIP is acknowledged (window reopened) only after it was written locally. */ @@ -14,6 +16,7 @@ #include "netcore/loop.h" #define NC_PROXY_MAX_FORWARDS 16 +#define NC_PROXY_MAX_ACCEPTS 4 /* lwIP listeners: MEMP_NUM_TCP_PCB_LISTEN */ typedef struct nc_forward { uint16_t local_port; @@ -21,6 +24,15 @@ typedef struct nc_forward { uint16_t port; } nc_forward_t; +/* Inbound forward: the tunnel address, port -> ip:local_port on the host. + * ip 0: the address of the host's default-route interface at the time of each + * connection (the camera's LAN address, so its services see a LAN client). */ +typedef struct nc_accept { + uint16_t port; + uint32_t ip; /* host byte order, 0 = default-route interface */ + uint16_t local_port; +} nc_accept_t; + typedef struct nc_proxy_cfg { const char *listen_addr; /* default "127.0.0.1" */ uint16_t socks_port; /* 0 = no SOCKS5 server */ @@ -28,7 +40,9 @@ typedef struct nc_proxy_cfg { const char *socks_password; nc_forward_t forwards[NC_PROXY_MAX_FORWARDS]; int forward_count; - int max_conns; /* default 24 (bounded by lwIP TCP PCBs) */ + nc_accept_t accepts[NC_PROXY_MAX_ACCEPTS]; + int accept_count; + int max_conns; /* default 24 (bounded by lwIP TCP PCBs); shared by all */ int connect_timeout_ms; /* default 15000 */ } nc_proxy_cfg_t; @@ -41,7 +55,11 @@ typedef struct nc_proxy nc_proxy_t; typedef void (*nc_proxy_log_cb)(void *user, const char *msg); -/* Opens the listening sockets. NULL on error (errno-style message via log). */ +/* Parses "PORT[:IP:PORT]" of accept= / --accept (#67). 0 on success. */ +int nc_accept_parse(const char *s, nc_accept_t *out); + +/* Opens the listening sockets and the lwIP listeners (after nc_stack_init). + * NULL on error (errno-style message via log). */ nc_proxy_t *nc_proxy_create(const nc_proxy_cfg_t *cfg, nc_proxy_log_cb log, void *user); void nc_proxy_destroy(nc_proxy_t *px); nc_source_t nc_proxy_source(nc_proxy_t *px); diff --git a/include/netcore/stack.h b/include/netcore/stack.h index a189e49..9afb432 100644 --- a/include/netcore/stack.h +++ b/include/netcore/stack.h @@ -25,8 +25,10 @@ typedef void (*nc_stack_event_cb)(void *user, int event, int tunnel_event, int e typedef struct nc_stack_cfg { uint8_t mac[6]; /* own MAC for L2 links (from the device ID) */ int static_ip; /* 1: use addr/mask/gw/routes below instead of DHCP */ + int learn_ip; /* type tun (#68): the address is the destination of the first + * unicast packet, gw its source; routes below, none: default */ uint32_t addr, mask, gw; /* host byte order */ - nc_routes_t routes; /* static routes (static_ip only); none: default route via gw */ + nc_routes_t routes; /* static routes (static_ip, learn_ip); none: default route via gw */ int opt3_default_route; /* debug: keep option 3 as default route even with 121 */ } nc_stack_cfg_t; diff --git a/include/netcore/tapdev.h b/include/netcore/tapdev.h deleted file mode 100644 index a5194c7..0000000 --- a/include/netcore/tapdev.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * TAP/TUN backend (Linux): passes tunnel frames to a kernel interface - * created through /dev/net/tun instead of the userspace stack. - * - * The kernel owns addressing and routing; every program on the device uses - * the tunnel, inbound connections included. The interface is created on the - * first tunnel UP (TAP for L2 sessions, TUN for L3) and kept across reconnects; - * while the tunnel is down the carrier is off, which also makes the kernel - * forget its ARP neighbours (the remote bridge MAC may change). - * - * Requires root or CAP_NET_ADMIN and the tun driver. Not for Android. - */ -#ifndef NETCORE_TAPDEV_H -#define NETCORE_TAPDEV_H - -#include - -#include "netcore/loop.h" -#include "netcore/tunnel.h" - -enum nc_tap_event { - NC_TAP_EV_TUNNEL = 1, /* tunnel event; see tunnel_event / err */ - NC_TAP_EV_CREATED, /* interface created and up (first tunnel UP) */ - NC_TAP_EV_RECONNECTED, /* tunnel up again, same interface */ - NC_TAP_EV_ERROR /* interface could not be created; err = errno */ -}; - -typedef void (*nc_tap_event_cb)(void *user, int event, int tunnel_event, int err); - -typedef struct nc_tap_cfg { - uint8_t mac[6]; /* TAP MAC (from the device ID) */ - const char *ifname; /* requested name, default "vtesse%d" */ - int mtu; /* default 1500 */ -} nc_tap_cfg_t; - -typedef struct nc_tap_stats { - unsigned long frames_in, frames_out, dropped; -} nc_tap_stats_t; - -typedef struct nc_tap nc_tap_t; - -/* Attaches to the tunnel (takes over its rx and event callbacks). */ -nc_tap_t *nc_tap_create(const tunnel_t *t, const nc_tap_cfg_t *cfg, nc_tap_event_cb cb, void *user); -void nc_tap_destroy(nc_tap_t *tap); -nc_source_t nc_tap_source(nc_tap_t *tap); -const char *nc_tap_ifname(const nc_tap_t *tap); /* "" before creation */ -int nc_tap_link(const nc_tap_t *tap); /* enum tunnel_link of the interface */ -void nc_tap_get_stats(const nc_tap_t *tap, nc_tap_stats_t *st); - -#endif diff --git a/netcore/hostaddr.c b/netcore/hostaddr.c new file mode 100644 index 0000000..99a9ec0 --- /dev/null +++ b/netcore/hostaddr.c @@ -0,0 +1,44 @@ +/* struct ifreq and SIOCGIFADDR; kept apart from lwIP, whose TCP_MSS clashes with + * under _DEFAULT_SOURCE */ +#define _DEFAULT_SOURCE 1 + +#include "netcore/hostaddr.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +uint32_t nc_default_route_addr(void) +{ + FILE *f = fopen("/proc/net/route", "r"); + char line[256], ifname[IFNAMSIZ]; + unsigned long dst, gw, mask; + unsigned flags; + int ref, use, metric, s, found = 0; + struct ifreq ifr; + uint32_t a = 0; + + if (!f) + return 0; + while (!found && fgets(line, sizeof(line), f)) + found = sscanf(line, "%15s %lx %lx %x %d %d %d %lx", ifname, &dst, &gw, &flags, &ref, &use, + &metric, &mask) == 8 && dst == 0 && mask == 0; + fclose(f); + if (!found || (s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) + return 0; + memset(&ifr, 0, sizeof(ifr)); + memcpy(ifr.ifr_name, ifname, strlen(ifname) + 1); + if (ioctl(s, SIOCGIFADDR, &ifr) == 0 && ifr.ifr_addr.sa_family == AF_INET) { + struct sockaddr_in sin; + + memcpy(&sin, &ifr.ifr_addr, sizeof(sin)); + a = ntohl(sin.sin_addr.s_addr); + } + close(s); + return a; +} diff --git a/netcore/proxy.c b/netcore/proxy.c index 15b265e..5fd2f16 100644 --- a/netcore/proxy.c +++ b/netcore/proxy.c @@ -13,6 +13,7 @@ #include #include "lwip/tcp.h" +#include "netcore/hostaddr.h" #include "netcore/route.h" #include "netcore/stack.h" @@ -45,6 +46,7 @@ enum conn_state { C_AUTH, C_REQUEST, C_CONNECTING, + C_LCONNECT, /* inbound (#67): connecting the host socket */ C_RELAY }; @@ -54,6 +56,8 @@ typedef struct conn { struct tcp_pcb *pcb; int state; int socks; /* SOCKS5 client: a reply is owed */ + int inbound; /* accepted from the tunnel (#67); dst:dport is the host side */ + uint16_t aport; /* inbound: the port on the tunnel address */ int replied; uint8_t hs[HS_MAX]; @@ -83,6 +87,8 @@ struct nc_proxy { int auth; int lfd[LISTEN_MAX]; /* [0] SOCKS, [1 + i] forward i; -1 if unused */ + struct tcp_pcb *lpcb[NC_PROXY_MAX_ACCEPTS]; /* accept= listeners on lwIP */ + struct lctx { nc_proxy_t *px; int i; } lctx[NC_PROXY_MAX_ACCEPTS]; conn_t **conns; int nconns; @@ -145,7 +151,10 @@ static void fail_conn(conn_t *c, uint8_t rep, const char *why) char d[24]; dst_str(c, d, sizeof(d)); - logf_(c->px, "%s %s: %s", c->socks ? "socks" : "forward", d, why); + if (c->inbound) + logf_(c->px, "accept %u -> %s: %s", (unsigned)c->aport, d, why); + else + logf_(c->px, "%s %s: %s", c->socks ? "socks" : "forward", d, why); socks_reply(c, rep); c->px->st.failed++; c->dead = 1; @@ -260,7 +269,8 @@ static err_t on_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) } else { c->rx = p; } - pump_down(c); + if (c->state == C_RELAY) + pump_down(c); /* inbound: kept until the host socket is connected */ return ERR_OK; } @@ -325,6 +335,117 @@ static void start_connect(conn_t *c) } } +/* ---- inbound forwards (#67) ---- */ + +static void local_connected(conn_t *c) +{ + char d[24]; + + c->state = C_RELAY; + c->deadline = 0; + c->px->st.connected++; + dst_str(c, d, sizeof(d)); + logf_(c->px, "accept %u -> %s: connected", (unsigned)c->aport, d); + pump_down(c); /* data the remote side sent meanwhile */ + flush_tx(c); +} + +static void start_local_connect(conn_t *c) +{ + struct sockaddr_in a; + int fd, one = 1; + + if (!c->dst && !(c->dst = nc_default_route_addr())) { + fail_conn(c, REP_FAILURE, "no default-route interface address"); + return; + } + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + fail_conn(c, REP_FAILURE, strerror(errno)); + return; + } + c->fd = fd; + set_nonblock(fd); + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); + memset(&a, 0, sizeof(a)); + a.sin_family = AF_INET; + a.sin_port = htons(c->dport); + a.sin_addr.s_addr = htonl(c->dst); + c->state = C_LCONNECT; + c->deadline = nc_now_ms() + (uint64_t)c->px->cfg.connect_timeout_ms; + if (connect(fd, (struct sockaddr *)&a, sizeof(a)) == 0) + local_connected(c); + else if (errno != EINPROGRESS) + fail_conn(c, REP_FAILURE, strerror(errno)); +} + +static void finish_local_connect(conn_t *c) +{ + int e = 0; + socklen_t len = sizeof(e); + + if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &e, &len) < 0) + e = errno; + if (e) + fail_conn(c, REP_FAILURE, strerror(e)); + else + local_connected(c); +} + +static err_t on_accept(void *arg, struct tcp_pcb *pcb, err_t err) +{ + struct lctx *l = arg; + nc_proxy_t *px = l->px; + const nc_accept_t *a = &px->cfg.accepts[l->i]; + conn_t *c; + + if (err != ERR_OK || !pcb) + return ERR_VAL; + if (px->nconns >= px->cfg.max_conns || !(c = calloc(1, sizeof(*c)))) { + px->st.rejected++; + logf_(px, "accept %u: connection limit reached (%d), reset", (unsigned)a->port, px->cfg.max_conns); + tcp_abort(pcb); + return ERR_ABRT; + } + c->px = px; + c->fd = -1; + c->pcb = pcb; + c->inbound = 1; + c->aport = a->port; + c->dst = a->ip; + c->dport = a->local_port; + px->conns[px->nconns++] = c; + px->st.accepted++; + px->st.active++; + tcp_arg(pcb, c); + tcp_recv(pcb, on_recv); + tcp_err(pcb, on_err); + tcp_nagle_disable(pcb); + start_local_connect(c); + return ERR_OK; +} + +int nc_accept_parse(const char *s, nc_accept_t *out) +{ + unsigned p, lp, a, b, cc, d; + char tail; + + memset(out, 0, sizeof(*out)); + if (sscanf(s, "%u%c", &p, &tail) == 1) { + lp = p; + } else if (sscanf(s, "%u:%u.%u.%u.%u:%u%c", &p, &a, &b, &cc, &d, &lp, &tail) == 6 && + a <= 255 && b <= 255 && cc <= 255 && d <= 255) { + out->ip = a << 24 | b << 16 | cc << 8 | d; + } else { + return -1; + } + if (!p || p > 65535 || !lp || lp > 65535 || strspn(s, "0123456789:.") != strlen(s)) + return -1; + out->port = (uint16_t)p; + out->local_port = (uint16_t)lp; + return 0; +} + #if NC_WITH_SOCKS /* ---- SOCKS5 handshake ---- */ @@ -581,8 +702,12 @@ static int px_fill(void *ctx, struct pollfd *pfd, int max) conn_t *c = px->conns[i]; short ev = 0; + if (c->fd < 0) + continue; /* inbound, the host socket is not open (yet) */ if (c->state < C_CONNECTING) ev = POLLIN; + else if (c->state == C_LCONNECT) + ev = POLLOUT; else if (c->state == C_RELAY) { if (can_read_local(c)) ev |= POLLIN; @@ -615,6 +740,10 @@ static void px_dispatch(void *ctx, const struct pollfd *pfd, int n) } if (c->dead) continue; + if (c->state == C_LCONNECT) { + finish_local_connect(c); /* POLLOUT, POLLERR or POLLHUP: the result is in SO_ERROR */ + continue; + } if (re & POLLERR) { c->dead = c->abort = 1; continue; @@ -641,7 +770,7 @@ static int px_next_timeout(void *ctx) for (i = 0; i < px->nconns; i++) { const conn_t *c = px->conns[i]; - if (c->state <= C_CONNECTING && !c->dead && c->deadline && (!next || c->deadline < next)) + if (c->state != C_RELAY && !c->dead && c->deadline && (!next || c->deadline < next)) next = c->deadline; } if (!next) @@ -661,6 +790,9 @@ static void px_timer(void *ctx) struct tcp_pcb *pcb = c->pcb; c->deadline = 0; tcp_abort(pcb); /* calls on_err(ERR_ABRT) -> "connect timeout" */ + } else if (c->state == C_LCONNECT && !c->dead && c->deadline && now >= c->deadline) { + c->deadline = 0; + fail_conn(c, REP_FAILURE, "connect timeout"); } else if (c->state < C_CONNECTING && !c->dead && c->deadline && now >= c->deadline) { logf_(px, "socks: handshake timeout"); c->deadline = 0; @@ -754,6 +886,29 @@ nc_proxy_t *nc_proxy_create(const nc_proxy_cfg_t *cfg, nc_proxy_log_cb log, void for (i = 0; i < px->cfg.forward_count; i++) if ((px->lfd[1 + i] = open_listener(px, px->cfg.forwards[i].local_port)) < 0) goto fail; + if (px->cfg.accept_count < 0 || px->cfg.accept_count > NC_PROXY_MAX_ACCEPTS) + goto fail; + for (i = 0; i < px->cfg.accept_count; i++) { + struct tcp_pcb *pcb = tcp_new(), *l; + + /* any address: the netif has only the tunnel address, whatever it is now */ + if (!pcb || tcp_bind(pcb, IP_ANY_TYPE, px->cfg.accepts[i].port) != ERR_OK) { + logf_(px, "accept %u: cannot listen in the stack", (unsigned)px->cfg.accepts[i].port); + if (pcb) + tcp_abort(pcb); + goto fail; + } + if (!(l = tcp_listen(pcb))) { + tcp_abort(pcb); + logf_(px, "accept %u: cannot listen in the stack", (unsigned)px->cfg.accepts[i].port); + goto fail; + } + px->lctx[i].px = px; + px->lctx[i].i = i; + tcp_arg(l, &px->lctx[i]); + tcp_accept(l, on_accept); + px->lpcb[i] = l; + } return px; fail: nc_proxy_destroy(px); @@ -773,6 +928,12 @@ void nc_proxy_destroy(nc_proxy_t *px) for (i = 0; i < LISTEN_MAX; i++) if (px->lfd[i] >= 0) close(px->lfd[i]); + for (i = 0; i < NC_PROXY_MAX_ACCEPTS; i++) + if (px->lpcb[i]) { + tcp_arg(px->lpcb[i], NULL); + tcp_accept(px->lpcb[i], NULL); + tcp_close(px->lpcb[i]); + } memset(px->pass, 0, sizeof(px->pass)); free(px->conns); free(px); diff --git a/netcore/stack.c b/netcore/stack.c index 975d5a5..4cddd6a 100644 --- a/netcore/stack.c +++ b/netcore/stack.c @@ -27,6 +27,7 @@ static struct { int link; /* enum tunnel_link the netif is configured for */ int dhcp_started; int bound; + int learnt; /* learn_ip: the address of this session is known */ nc_routes_t routes; /* in effect */ int use_default; /* default route via netif gw */ @@ -277,6 +278,39 @@ static int l2_accept(const uint8_t *f, size_t len) return type == 0x0800 || type == 0x0806; } +/* 0.0.0.0/8, multicast 224/4 and everything above (class E, broadcast) are not unicast */ +static int unicast(uint32_t a) +{ + return (a >> 24) != 0 && (a >> 28) < 14; +} + +/* learn_ip (#68): the server routes only our address into a type tun session, so the + * destination of the first unicast packet is it, the source the peer. Set before the + * packet enters lwIP, so a first SYN is accepted. A packet of a new session with the + * same address changes nothing; another address replaces the old one. */ +static void learn(const uint8_t *pkt) +{ + uint32_t src = (uint32_t)pkt[12] << 24 | (uint32_t)pkt[13] << 16 | (uint32_t)pkt[14] << 8 | pkt[15]; + uint32_t dst = (uint32_t)pkt[16] << 24 | (uint32_t)pkt[17] << 16 | (uint32_t)pkt[18] << 8 | pkt[19]; + ip4_addr_t a, m, g; + int was_bound = st.bound; + + if ((pkt[0] & 0x0f) < 5 || !unicast(dst) || !unicast(src) || dst == src) + return; + st.learnt = 1; + if (was_bound && lwip_ntohl(ip4_addr_get_u32(netif_ip4_addr(&st.netif))) == dst && + lwip_ntohl(ip4_addr_get_u32(netif_ip4_gw(&st.netif))) == src) + return; + ip4_addr_set_u32(&a, lwip_htonl(dst)); + ip4_addr_set_u32(&m, lwip_htonl(0xffffffffu)); + ip4_addr_set_u32(&g, lwip_htonl(src)); + /* like the manual address: explicit routes, or a default route via the peer */ + apply_routes(&st.cfg.routes, st.cfg.routes.count == 0); + netif_set_addr(&st.netif, &a, &m, &g); + if (was_bound) + emit(NC_STACK_EV_ADDR_BOUND, 0, 0); /* changed: status_cb reports only the first */ +} + static void on_rx(void *user, const uint8_t *pkt, size_t len) { struct pbuf *p; @@ -284,6 +318,8 @@ static void on_rx(void *user, const uint8_t *pkt, size_t len) (void)user; if (st.link == TUNNEL_LINK_L2 ? !l2_accept(pkt, len) : (len < 20 || (pkt[0] >> 4) != 4)) return; + if (st.link == TUNNEL_LINK_L3 && st.cfg.learn_ip && !st.learnt) + learn(pkt); p = pbuf_alloc(PBUF_RAW, (u16_t)len, PBUF_POOL); if (!p) return; @@ -308,6 +344,7 @@ static void on_tunnel_event(void *user, int ev, int err) etharp_cleanup_netif(&st.netif); netif_set_up(&st.netif); netif_set_link_up(&st.netif); /* restarts DHCP with the previous address */ + st.learnt = 0; /* learn_ip: the next packet tells the address again */ if (st.cfg.static_ip) { ip4_addr_t a, m, g; diff --git a/netcore/tapdev.c b/netcore/tapdev.c deleted file mode 100644 index 8fb4c47..0000000 --- a/netcore/tapdev.c +++ /dev/null @@ -1,257 +0,0 @@ -#include "netcore/tapdev.h" - -#include -#include -#include -#include -#include -#include -#include /* before linux/if.h: old kernel headers need struct sockaddr */ -#include - -#include -#include - -#ifndef ARPHRD_ETHER -#define ARPHRD_ETHER 1 /* linux/if_arp.h drags in netlink headers */ -#endif - -#ifndef TUNSETCARRIER -#define TUNSETCARRIER _IOW('T', 226, int) -#endif - -#define FRAME_BUF (2048 + 64) -#define READ_BUDGET 64 /* frames per readiness event */ - -struct nc_tap { - tunnel_t tun; - nc_tap_cfg_t cfg; - char req_name[IFNAMSIZ]; - nc_tap_event_cb cb; - void *user; - - int fd; - char ifname[IFNAMSIZ]; - int link; - int carrier_ok; /* TUNSETCARRIER supported */ - nc_tap_stats_t st; - uint8_t buf[FRAME_BUF]; -}; - -static void emit(nc_tap_t *tap, int ev, int tev, int err) -{ - if (tap->cb) - tap->cb(tap->user, ev, tev, err); -} - -static int ifreq_ioctl(unsigned long req, struct ifreq *ifr) -{ - int s = socket(AF_INET, SOCK_DGRAM, 0), rc; - - if (s < 0) - return -1; - rc = ioctl(s, req, ifr); - if (rc < 0) { - int e = errno; - close(s); - errno = e; - return -1; - } - close(s); - return 0; -} - -static void disable_ipv6(const char *ifname) -{ - char path[96]; - int fd; - - /* IPv4 only; best effort (keeps router solicitations off the bridge) */ - snprintf(path, sizeof(path), "/proc/sys/net/ipv6/conf/%s/disable_ipv6", ifname); - fd = open(path, O_WRONLY | O_CLOEXEC); - if (fd >= 0) { - (void)!write(fd, "1", 1); - close(fd); - } -} - -static void set_carrier(nc_tap_t *tap, int on) -{ - if (tap->fd < 0 || !tap->carrier_ok) - return; - if (ioctl(tap->fd, TUNSETCARRIER, &on) < 0) - tap->carrier_ok = 0; /* old kernel: interface stays up */ -} - -static int create_iface(nc_tap_t *tap, int link) -{ - struct ifreq ifr; - int fd, e; - - fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC); - if (fd < 0) - return -errno; - memset(&ifr, 0, sizeof(ifr)); - ifr.ifr_flags = (short)((link == TUNNEL_LINK_L2 ? IFF_TAP : IFF_TUN) | IFF_NO_PI); - snprintf(ifr.ifr_name, IFNAMSIZ, "%s", tap->req_name); - if (ioctl(fd, TUNSETIFF, &ifr) < 0) - goto fail; - snprintf(tap->ifname, sizeof(tap->ifname), "%s", ifr.ifr_name); - - if (link == TUNNEL_LINK_L2) { - memset(&ifr.ifr_ifru, 0, sizeof(ifr.ifr_ifru)); - ifr.ifr_hwaddr.sa_family = ARPHRD_ETHER; - memcpy(ifr.ifr_hwaddr.sa_data, tap->cfg.mac, 6); - if (ifreq_ioctl(SIOCSIFHWADDR, &ifr) < 0) - goto fail; - } - memset(&ifr.ifr_ifru, 0, sizeof(ifr.ifr_ifru)); - ifr.ifr_mtu = tap->cfg.mtu; - if (ifreq_ioctl(SIOCSIFMTU, &ifr) < 0) - goto fail; - disable_ipv6(tap->ifname); - memset(&ifr.ifr_ifru, 0, sizeof(ifr.ifr_ifru)); - if (ifreq_ioctl(SIOCGIFFLAGS, &ifr) < 0) - goto fail; - ifr.ifr_flags |= IFF_UP; - if (ifreq_ioctl(SIOCSIFFLAGS, &ifr) < 0) - goto fail; - - tap->fd = fd; - tap->link = link; - tap->carrier_ok = 1; - set_carrier(tap, 1); - return 0; -fail: - e = errno; - close(fd); - tap->ifname[0] = '\0'; - return -e; -} - -/* ---- tunnel callbacks ---- */ - -static void on_rx(void *user, const uint8_t *pkt, size_t len) -{ - nc_tap_t *tap = user; - - if (tap->fd < 0) - return; - if (write(tap->fd, pkt, len) == (ssize_t)len) - tap->st.frames_in++; - else - tap->st.dropped++; /* EAGAIN: kernel queue full, like a lost frame */ -} - -static void on_tunnel_event(void *user, int ev, int err) -{ - nc_tap_t *tap = user; - - if (ev == TUNNEL_EV_UP) { - int link = tap->tun.ops->link_type(tap->tun.ctx); - - if (tap->fd < 0) { - int rc = create_iface(tap, link); - emit(tap, NC_TAP_EV_TUNNEL, ev, err); - if (rc < 0) - emit(tap, NC_TAP_EV_ERROR, 0, -rc); - else - emit(tap, NC_TAP_EV_CREATED, 0, 0); - return; - } - if (link != tap->link) { - emit(tap, NC_TAP_EV_TUNNEL, ev, err); - emit(tap, NC_TAP_EV_ERROR, 0, EPROTOTYPE); /* session type changed */ - return; - } - set_carrier(tap, 1); - emit(tap, NC_TAP_EV_TUNNEL, ev, err); - emit(tap, NC_TAP_EV_RECONNECTED, 0, 0); - return; - } - if (ev == TUNNEL_EV_DOWN || ev == TUNNEL_EV_FAILED) - set_carrier(tap, 0); - emit(tap, NC_TAP_EV_TUNNEL, ev, err); -} - -/* ---- loop source ---- */ - -static int tap_get_fd(void *ctx) -{ - return ((nc_tap_t *)ctx)->fd; -} - -static void tap_readable(void *ctx) -{ - nc_tap_t *tap = ctx; - int i; - - for (i = 0; i < READ_BUDGET && tap->fd >= 0; i++) { - ssize_t n = read(tap->fd, tap->buf, sizeof(tap->buf)); - - if (n <= 0) - return; /* EAGAIN or error: wait for the next event */ - if (tap->tun.ops->send(tap->tun.ctx, tap->buf, (size_t)n) == 0) - tap->st.frames_out++; - else - tap->st.dropped++; /* tunnel down or output buffer full */ - } -} - -nc_source_t nc_tap_source(nc_tap_t *tap) -{ - nc_source_t s; - - memset(&s, 0, sizeof(s)); - s.get_fd = tap_get_fd; - s.on_readable = tap_readable; - s.ctx = tap; - return s; -} - -/* ---- public ---- */ - -nc_tap_t *nc_tap_create(const tunnel_t *t, const nc_tap_cfg_t *cfg, nc_tap_event_cb cb, void *user) -{ - nc_tap_t *tap = calloc(1, sizeof(*tap)); - - if (!tap) - return NULL; - tap->tun = *t; - tap->cfg = *cfg; - tap->cb = cb; - tap->user = user; - tap->fd = -1; - snprintf(tap->req_name, sizeof(tap->req_name), "%s", - cfg->ifname && *cfg->ifname ? cfg->ifname : "vtesse%d"); - tap->cfg.ifname = NULL; - if (tap->cfg.mtu <= 0) - tap->cfg.mtu = 1500; - t->ops->set_rx_cb(t->ctx, on_rx, tap); - t->ops->set_event_cb(t->ctx, on_tunnel_event, tap); - return tap; -} - -void nc_tap_destroy(nc_tap_t *tap) -{ - if (!tap) - return; - if (tap->fd >= 0) - close(tap->fd); /* a non-persistent interface disappears */ - free(tap); -} - -const char *nc_tap_ifname(const nc_tap_t *tap) -{ - return tap->ifname; -} - -int nc_tap_link(const nc_tap_t *tap) -{ - return tap->fd >= 0 ? tap->link : TUNNEL_LINK_NONE; -} - -void nc_tap_get_stats(const nc_tap_t *tap, nc_tap_stats_t *st) -{ - *st = tap->st; -} diff --git a/port/lwip/lwipopts.h b/port/lwip/lwipopts.h index e3dac8d..f8b177f 100644 --- a/port/lwip/lwipopts.h +++ b/port/lwip/lwipopts.h @@ -68,7 +68,7 @@ #define MEM_ALIGNMENT 4 #endif #define MEMP_NUM_UDP_PCB 4 -#define MEMP_NUM_TCP_PCB_LISTEN 1 +#define MEMP_NUM_TCP_PCB_LISTEN 4 /* accept= listeners (#67) */ #define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS)) /* ---- protocols ---- */ diff --git a/scripts/S99vtesse b/scripts/S99vtesse index 5576548..93727fb 100755 --- a/scripts/S99vtesse +++ b/scripts/S99vtesse @@ -3,15 +3,15 @@ # # /etc/init.d/S99vtesse start|stop|restart|status # -# Installed by "vtesse --install". Runs the TAP backend with the built-in hook -# scripts, or with the copies in /usr/libexec/vtesse when they exist -# ("vtesse --install --scripts"). Settings can be overridden in /etc/default/vtesse. +# Installed by "vtesse --install". Runs the userspace stack; the server reaches the +# camera's services through accept= (#67). Settings can be overridden in +# /etc/default/vtesse. # The U-Boot variables vtesse (the server) and vtesse_* override the config. DAEMON=/usr/bin/vtesse CONFIG=/etc/vtesse.conf PIDFILE=/var/run/vtesse.pid -ARGS="--tap --ifname vtesse0" +ARGS= [ -f /etc/default/vtesse ] && . /etc/default/vtesse # stop/status match the process by name, not by the executable: "vtesse --install" # replaces the binary under a running daemon @@ -28,7 +28,6 @@ start() { echo "vtesse: neither $CONFIG nor vtesse in the U-Boot environment, not starting" return 1 fi - [ -e /dev/net/tun ] || modprobe tun 2>/dev/null printf "Starting vtesse: " start-stop-daemon -S -q -b -m -p "$PIDFILE" -x "$DAEMON" -- --config "$CONFIG" $ARGS \ && echo "OK" || echo "FAIL" diff --git a/scripts/vtesse-hook.sh b/scripts/vtesse-hook.sh deleted file mode 100755 index 864b991..0000000 --- a/scripts/vtesse-hook.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/sh -# The TAP backend's hook. vtesse carries a copy and runs it when --hook is not -# given (the built-in hook); this file is for --hook FILE, e.g. a copy edited by -# hand ("vtesse --install --scripts" writes one to /libexec/vtesse): -# vtesse-cli --tap --hook scripts/vtesse-hook.sh -# -# Called as "$0 " with VTESSE_IFNAME, VTESSE_LINK (ether|tun), -# VTESSE_SESSION and VTESSE_EVENT in the environment: -# up interface created (first tunnel UP): start the DHCP client -# reconnect tunnel up again: forget neighbours (the remote bridge MAC may -# have changed), ask the DHCP client to renew -# down tunnel lost (the interface stays, the lease is kept) -# stop vtesse-cli exits: stop the DHCP client -# -# DHCP: BusyBox udhcpc with vtesse-udhcpc.script (address and option 121/249 -# routes only; DNS and the default route are left alone, see the script). -# dhclient is a fallback for a hook file only (not for the built-in hook): its -# stock script may rewrite /etc/resolv.conf with the tunnel DNS and install a -# default route through the tunnel. -# Manual IPv4 (ip=, gw=, route= in the vtesse-cli config; required for type tun, -# which has no DHCP over L3) arrives as VTESSE_IP (A.B.C.D/P), VTESSE_GW and -# VTESSE_ROUTES ("NET/P NET/P ..."): the address and routes are set here instead of -# DHCP. Without routes and with VTESSE_GW a default route through the tunnel is -# installed only with VTESSE_DEFAULT_ROUTE=1 (as in vtesse-udhcpc.script). - -IF=$VTESSE_IFNAME -PIDFILE=/var/run/vtesse-dhcp.$IF.pid -UDHCPC_SCRIPT=$(dirname "$0")/vtesse-udhcpc.script -# The built-in hook gets the path of the binary in VTESSE_SELF: the binary runs -# its built-in copy of vtesse-udhcpc.script when called with VTESSE_RUN set. -if [ -n "$VTESSE_SELF" ]; then - UDHCPC_SCRIPT=$VTESSE_SELF - export VTESSE_RUN=udhcpc-script -fi - -dhcp_start() { - if command -v udhcpc >/dev/null 2>&1; then - udhcpc -i "$IF" -b -p "$PIDFILE" -s "$UDHCPC_SCRIPT" -O staticroutes -O msstaticroutes - elif [ -n "$VTESSE_SELF" ]; then - # the built-in hook does not fall back to dhclient: its stock script may - # replace /etc/resolv.conf and add a default route through the tunnel - echo "vtesse-hook: no udhcpc found (BusyBox udhcpc, e.g. the udhcpc package);" \ - "install it, or run with --hook FILE / --hook none" >&2 - exit 1 - elif command -v dhclient >/dev/null 2>&1; then - dhclient -nw -pf "$PIDFILE" -lf "/var/lib/dhcp/vtesse.$IF.leases" "$IF" - else - echo "vtesse-hook: no DHCP client (udhcpc or dhclient) found" >&2 - exit 1 - fi -} - -static_up() { - ip addr replace "$VTESSE_IP" dev "$IF" - for r in $VTESSE_ROUTES; do - if [ "$VTESSE_LINK" = tun ] || [ -z "$VTESSE_GW" ]; then - ip route replace "$r" dev "$IF" - else - ip route replace "$r" via "$VTESSE_GW" dev "$IF" - fi - done - if [ -z "$VTESSE_ROUTES" ] && [ -n "$VTESSE_GW" ] && [ "${VTESSE_DEFAULT_ROUTE:-0}" = 1 ]; then - ip route replace default via "$VTESSE_GW" dev "$IF" - fi -} - -# The DHCP client of the PID file, only if that process is still a DHCP client: a -# stale file whose PID was reused must not make us signal another program. -dhcp_pid() { - [ -f "$PIDFILE" ] || return 1 - pid=$(cat "$PIDFILE") - case $(cat "/proc/$pid/comm" 2>/dev/null) in - udhcpc|dhclient) echo "$pid" ;; - *) return 1 ;; - esac -} - -case "$1" in - up) - # a DHCP client left by a previous instance that did not stop cleanly - pid=$(dhcp_pid) && kill "$pid" 2>/dev/null - rm -f "$PIDFILE" - if [ -n "$VTESSE_IP" ]; then - static_up - elif [ "$VTESSE_LINK" = ether ]; then - dhcp_start - else - echo "vtesse-hook: type tun needs ip= (and route=) in the config" >&2 - fi - ;; - reconnect) - ip neigh flush dev "$IF" 2>/dev/null - pid=$(dhcp_pid) && [ -n "$pid" ] && command -v udhcpc >/dev/null 2>&1 && kill -USR1 "$pid" 2>/dev/null - ;; - down) - ;; - stop) - pid=$(dhcp_pid) && [ -n "$pid" ] && kill "$pid" 2>/dev/null - rm -f "$PIDFILE" - ;; -esac -exit 0 diff --git a/scripts/vtesse-udhcpc.script b/scripts/vtesse-udhcpc.script deleted file mode 100755 index 07cf4b8..0000000 --- a/scripts/vtesse-udhcpc.script +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/sh -# udhcpc script for VTesse tap interfaces (BusyBox udhcpc -s ...). -# -# Configures the address and the routes of the tunnel interface only: -# - never touches /etc/resolv.conf (the tunnel DNS must not replace the -# device's resolver: it may be unreachable and would break resolving the -# VTun server itself); -# - classless routes (option 121 "staticroutes", or 249 "msstaticroutes") -# are installed via the interface; with them the router option is ignored -# (RFC 3442); -# - a default route through the tunnel is installed only when -# VTESSE_DEFAULT_ROUTE=1: it would also capture the connection to the VTun -# server unless a host route to the server exists. - -STATE=/var/run/vtesse-routes.$interface - -routes_del() { - [ -f "$STATE" ] || return 0 - while read -r dst gw; do - if [ "$gw" = 0.0.0.0 ]; then - ip route del "$dst" dev "$interface" 2>/dev/null - else - ip route del "$dst" via "$gw" dev "$interface" 2>/dev/null - fi - done < "$STATE" - rm -f "$STATE" -} - -routes_add() { - : > "$STATE" - list=${staticroutes:-$msstaticroutes} - if [ -n "$list" ]; then - set -- $list - while [ $# -ge 2 ]; do - if [ "$2" = 0.0.0.0 ]; then # on-link destination (RFC 3442) - ip route add "$1" dev "$interface" && echo "$1 $2" >> "$STATE" - else - ip route add "$1" via "$2" dev "$interface" && echo "$1 $2" >> "$STATE" - fi - shift 2 - done - elif [ -n "$router" ] && [ "$VTESSE_DEFAULT_ROUTE" = 1 ]; then - set -- $router - ip route add default via "$1" dev "$interface" metric 500 && echo "default $1" >> "$STATE" - fi -} - -case "$1" in - deconfig) - routes_del - ip -4 addr flush dev "$interface" - ip link set "$interface" up - ;; - bound|renew) - if ! ip -4 addr show dev "$interface" | grep -q "inet $ip/"; then - ip -4 addr flush dev "$interface" - ip addr add "$ip/${mask:-24}" ${broadcast:+broadcast "$broadcast"} dev "$interface" - fi - routes_del - routes_add - ;; -esac -exit 0 diff --git a/scripts/vtesse.init b/scripts/vtesse.init index ccc3570..5bbb0d3 100755 --- a/scripts/vtesse.init +++ b/scripts/vtesse.init @@ -3,9 +3,8 @@ # # /etc/init.d/vtesse enable && /etc/init.d/vtesse start # -# Installed by "vtesse --install". Runs the TAP backend with the built-in hook -# scripts, or with the copies in /usr/libexec/vtesse when they exist -# ("vtesse --install --scripts"), and restarts it if it exits. +# Installed by "vtesse --install". Runs the userspace stack and restarts it if it +# exits; the server reaches the router's services through accept= (#67). START=99 USE_PROCD=1 @@ -14,9 +13,8 @@ start_service() { # helpdesk access (#64): "fw_setenv vtesse " is enough, without a config [ -f /etc/vtesse.conf ] || [ -n "$(fw_printenv -n vtesse 2>/dev/null)" ] || { echo "vtesse: neither /etc/vtesse.conf nor vtesse in the U-Boot environment"; return 1; } - [ -e /dev/net/tun ] || modprobe tun 2>/dev/null procd_open_instance - procd_set_param command /usr/bin/vtesse --config /etc/vtesse.conf --tap --ifname vtesse0 + procd_set_param command /usr/bin/vtesse --config /etc/vtesse.conf procd_set_param respawn procd_set_param stdout 1 procd_set_param stderr 1 diff --git a/scripts/vtesse.service b/scripts/vtesse.service index e7e23aa..d001bb0 100644 --- a/scripts/vtesse.service +++ b/scripts/vtesse.service @@ -2,9 +2,8 @@ # # systemctl enable --now vtesse # -# Installed by "vtesse --install". Runs the TAP backend with the built-in hook -# scripts, or with the copies in /usr/local/libexec/vtesse when they exist -# ("vtesse --install --scripts"). +# Installed by "vtesse --install". Runs the userspace stack; the server reaches +# the services of this machine through the accept= lines of /etc/vtesse.conf. [Unit] Description=VTesse VTun client @@ -12,8 +11,7 @@ Wants=network-online.target After=network-online.target [Service] -ExecStartPre=-/sbin/modprobe tun -ExecStart=/usr/local/bin/vtesse --config /etc/vtesse.conf --tap --ifname vtesse0 +ExecStart=/usr/local/bin/vtesse --config /etc/vtesse.conf Restart=always RestartSec=5 diff --git a/tests/test_install.sh b/tests/test_install.sh index 70904e2..6492e49 100755 --- a/tests/test_install.sh +++ b/tests/test_install.sh @@ -1,7 +1,8 @@ #!/bin/sh # shellcheck disable=SC2034 # SCRIPTS, rc: used inside the eval strings of check # tests/test_install.sh CLI — "vtesse --install" / "--uninstall" below --root -# (#50, #51, #52), and the built-in udhcpc script. Run by "make test". +# (#50, #51, #52), the inbound defaults of the service (#67) and the clean-up of +# the TAP backend's files of 1.x (#72). Run by "make test". set -u CLI=$1 HERE=$(cd "$(dirname "$0")" && pwd) @@ -12,11 +13,6 @@ trap 'rm -rf "$T"' EXIT check() { if eval "$2"; then echo "PASS $1"; else echo "FAIL $1"; fails=$((fails + 1)); fi; } -if ! "$CLI" --version | grep -q tap; then - echo "SKIP built without the TAP backend" - exit 0 -fi - conf_has() { grep -qx "$2" "$1/etc/vtesse.conf"; } mode() { stat -c %a "$1"; } @@ -41,7 +37,9 @@ check "OpenIPC: S99vtesse from the built-in copy" 'cmp -s "$SCRIPTS/S99vtesse" " check "OpenIPC: config with the keys, port 5000 by default, mode 600" \ 'conf_has "$R" host=vtun.example.org && conf_has "$R" password=secret && conf_has "$R" session=02AB34CD56EF && conf_has "$R" port=5000 && [ "$(mode "$R/etc/vtesse.conf")" = 600 ]' -check "OpenIPC: no hook files by default" '[ ! -e "$R/usr/libexec/vtesse" ]' +check "OpenIPC: no hook files" '[ ! -e "$R/usr/libexec/vtesse" ]' +check "OpenIPC: a new config opens the camera's web, ssh and RTSP to the server (#67)" \ + 'conf_has "$R" accept=80 && conf_has "$R" accept=22 && conf_has "$R" accept=554' printf '# my note\nudp_relay=1559\n' >> "$R/etc/vtesse.conf" "$CLI" --install --root "$R" --port 6000 --id-from eth1 >"$T/log" 2>&1 @@ -61,10 +59,15 @@ echo "$long" >> "$R/etc/vtesse.conf" "$CLI" --install --root "$R" --port 6001 >/dev/null 2>&1 check "a 2000-character line survives an update" 'conf_has "$R" "$long" && conf_has "$R" port=6001' -"$CLI" --install --root "$R" --scripts >/dev/null 2>&1 -check "--scripts: hook scripts in /usr/libexec/vtesse" \ - 'cmp -s "$SCRIPTS/vtesse-hook.sh" "$R/usr/libexec/vtesse/vtesse-hook.sh" && - cmp -s "$SCRIPTS/vtesse-udhcpc.script" "$R/usr/libexec/vtesse/vtesse-udhcpc.script"' +"$CLI" --install --root "$R" --scripts >"$T/log" 2>&1; rc=$? +check "--scripts: refused, the hook scripts went with the TAP backend" \ + '[ $rc -eq 2 ] && grep -q "TAP backend" "$T/log" && [ ! -e "$R/usr/libexec/vtesse" ]' +check "accept lines are written once" '[ "$(grep -c "^accept=" "$R/etc/vtesse.conf")" = 3 ]' +sed -i '/^accept=/d' "$R/etc/vtesse.conf" +echo accept=8080 >> "$R/etc/vtesse.conf" +"$CLI" --install --root "$R" >/dev/null 2>&1 +check "a config with its own accept line keeps it, no defaults added" \ + 'conf_has "$R" accept=8080 && [ "$(grep -c "^accept=" "$R/etc/vtesse.conf")" = 1 ]' "$CLI" --uninstall --root "$R" >"$T/log" 2>&1 check "uninstall: everything removed, the config included" \ @@ -73,7 +76,7 @@ check "uninstall: everything removed, the config included" \ "$CLI" --uninstall --root "$R" >"$T/log" 2>&1; rc=$? check "uninstall on a clean system is not an error" '[ $rc -eq 0 ]' -# ---- OpenWrt: rc.d link and the sysupgrade keep list; old hook scripts moved +# ---- OpenWrt: rc.d link and the sysupgrade keep list; hook scripts of 1.x removed R=$T/openwrt mkdir -p "$R/etc" "$R/usr/share/vtesse" echo "DISTRIB_ID='OpenWrt'" > "$R/etc/openwrt_release" @@ -88,22 +91,21 @@ check "OpenWrt: procd init script and its rc.d link" \ check "OpenWrt: keep list for sysupgrade" \ 'grep -qx /usr/bin/vtesse "$R/lib/upgrade/keep.d/vtesse" && grep -qx /etc/vtesse.conf "$R/lib/upgrade/keep.d/vtesse"' check "OpenWrt: id_from as given" 'grep -qx "id_from=eth0" "$R/etc/vtesse.conf"' -check "1.13 hook scripts not named in /etc/default/vtesse: kept as inactive *.1.13 copies" \ - 'grep -q "edited by hand" "$R/usr/libexec/vtesse/vtesse-hook.sh.1.13" && - [ ! -e "$R/usr/libexec/vtesse/vtesse-hook.sh" ] && [ ! -e "$R/usr/share/vtesse" ]' +check "hook scripts of 1.13 (/usr/share/vtesse) removed with the TAP backend" '[ ! -e "$R/usr/share/vtesse" ]' +check "OpenWrt: the keep list has no libexec any more" '! grep -q libexec "$R/lib/upgrade/keep.d/vtesse"' check "OpenWrt: the keep list carries the rc.d link" 'grep -qx /etc/rc.d/S99vtesse "$R/lib/upgrade/keep.d/vtesse"' "$CLI" --uninstall --root "$R" >/dev/null 2>&1 -# a hand-edited 1.13 hook survives --install --scripts; /etc/default/vtesse follows the move -mkdir -p "$R/usr/share/vtesse" "$R/etc/default" -echo "# edited by hand" > "$R/usr/share/vtesse/vtesse-hook.sh" -echo 'ARGS="--tap --ifname vtesse0 --hook /usr/share/vtesse/vtesse-hook.sh"' > "$R/etc/default/vtesse" -"$CLI" --install --root "$R" --host h --password p --id-from eth0 --scripts >"$T/log" 2>&1 -check "--scripts keeps a moved hand-edited hook" \ - 'grep -q "edited by hand" "$R/usr/libexec/vtesse/vtesse-hook.sh" && - cmp -s "$SCRIPTS/vtesse-udhcpc.script" "$R/usr/libexec/vtesse/vtesse-udhcpc.script"' -check "/etc/default/vtesse: --hook path moved to /usr/libexec/vtesse" \ - 'grep -q -- "--hook /usr/libexec/vtesse/vtesse-hook.sh" "$R/etc/default/vtesse"' +# an update of a 1.x installation: its libexec hook scripts go, TAP options in +# /etc/default/vtesse are reported (the client would refuse them), the file stays +mkdir -p "$R/usr/libexec/vtesse" "$R/etc/default" +echo "# hook" > "$R/usr/libexec/vtesse/vtesse-hook.sh" +echo "# udhcpc" > "$R/usr/libexec/vtesse/vtesse-udhcpc.script" +echo 'ARGS="--tap --ifname vtesse0"' > "$R/etc/default/vtesse" +"$CLI" --install --root "$R" --host h --password p --id-from eth0 >"$T/log" 2>&1 +check "update from 1.x: libexec hook scripts removed" '[ ! -e "$R/usr/libexec/vtesse" ]' +check "update from 1.x: --tap in /etc/default/vtesse reported, the file kept" \ + 'grep -q "TAP backend was removed" "$T/log" && grep -q -- --tap "$R/etc/default/vtesse"' "$CLI" --uninstall --root "$R" >/dev/null 2>&1 check "OpenWrt: uninstall removes the link, the keep list and the scripts" \ '[ ! -e "$R/etc/rc.d/S99vtesse" ] && [ ! -e "$R/lib/upgrade/keep.d/vtesse" ] && [ ! -e "$R/usr/libexec/vtesse" ]' @@ -176,12 +178,9 @@ printf 'host=server\npassword=p\nsession=02AB34CD56EF\nport=abc\n' > "$R/etc/vte "$CLI" --install --root "$R" --system systemd >/dev/null 2>&1; rc=$? check "port=abc in the file: refused" '[ $rc -ne 0 ]' -# ---- the built-in udhcpc script: the binary runs it with VTESSE_RUN=udhcpc-script -mkdir -p "$T/bin" -printf '#!/bin/sh\necho "ip $*" >> "%s/ip.log"\n' "$T" > "$T/bin/ip" -chmod 755 "$T/bin/ip" -PATH=$T/bin:$PATH VTESSE_RUN=udhcpc-script interface=vt0 "$CLI" deconfig -check "built-in udhcpc script runs (deconfig flushes the address)" 'grep -q "ip -4 addr flush dev vt0" "$T/ip.log"' +# ---- TAP options of 1.x: refused with the reason (#72) +"$CLI" --tap --config "$T/none.conf" >"$T/log" 2>&1; rc=$? +check "--tap: refused, the TAP backend was removed" '[ $rc -eq 2 ] && grep -q "TAP backend was removed" "$T/log"' # ---- U-Boot environment (#64): fake fw_printenv / fw_setenv in PATH mkdir -p "$T/env" @@ -203,6 +202,10 @@ if [ -n "$iface" ] && [ -r "/sys/class/net/$iface/address" ]; then check "env: only the server -> port 5000, session = MAC of $iface, password as VTun" \ 'grep -qx "host vtun.helpdesk" "$T/log" && grep -qx "port 5000" "$T/log" && grep -qx "session $id" "$T/log" && grep -qx "password $pw" "$T/log"' + check "env: the service opens 80, 22 and 554 to the default-route address (#67)" \ + 'grep -qx "accept 80 -> (default-route address):80" "$T/log" && + grep -qx "accept 22 -> (default-route address):22" "$T/log" && + grep -qx "accept 554 -> (default-route address):554" "$T/log"' printf 'vtesse=vtun.helpdesk\nvtesse_session=02AB34CD56EF\n' > "$T/env/vars" PATH=$EP "$CLI" --config "$T/none.conf" --show-config >"$T/log" 2>&1 check "env: an explicit session still gets the MAC-derived password" \ @@ -219,9 +222,12 @@ PATH=$EP "$CLI" --config "$T/f.conf" --show-config >"$T/log" 2>&1 check "env overrides the file key by key" \ 'grep -qx "host env.example" "$T/log" && grep -qx "port 7000" "$T/log" && grep -qx "session 02AB34CD56EF" "$T/log" && grep -qx "password (set)" "$T/log"' -printf 'route=10.78.0.0/24\nip=10.76.0.2/32\nudp_relay=1559\n' >> "$T/f.conf" +printf 'route=10.78.0.0/24\nip=10.76.0.2/32\nudp_relay=1559\naccept=8080:127.0.0.1:80\n' >> "$T/f.conf" PATH=$EP "$CLI" --config "$T/f.conf" --show-config >"$T/log" 2>&1 -check "--show-config lists routes and UDP relays" 'grep -qx "route 10.78.0.0/24" "$T/log" && grep -qx "udp_relay 1559" "$T/log"' +check "--show-config lists routes, UDP relays and accepts" \ + 'grep -qx "route 10.78.0.0/24" "$T/log" && grep -qx "udp_relay 1559" "$T/log" && + grep -qx "accept 8080 -> 127.0.0.1:80" "$T/log" && ! grep -q "accept 80 " "$T/log"' +sed -i '/^accept=/d' "$T/f.conf" printf 'port=abc\n' >> "$T/f.conf" PATH=$EP "$CLI" --config "$T/f.conf" --show-config >"$T/log" 2>&1; rc=$? printf 'vtesse=env.example\nvtesse_port=7000\n' > "$T/env/vars" diff --git a/tools/embed-scripts.sh b/tools/embed-scripts.sh index e5a818c..228fe73 100755 --- a/tools/embed-scripts.sh +++ b/tools/embed-scripts.sh @@ -2,7 +2,7 @@ # embed-scripts.sh FILE... > vtesse_scripts.h # # Turns the scripts from scripts/ into NUL-terminated byte arrays, so that the -# binary carries them (built-in hook, "vtesse --install"). Byte arrays instead of +# binary carries them ("vtesse --install"). Byte arrays instead of # string literals: no escaping, and no limit on the length of a literal. set -e From 6c38b8fa7ca76b635e2a571b9076c2089fc66a84 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Thu, 22 Oct 2026 21:19:12 +0000 Subject: [PATCH 3/5] Review fixes, round 1 (Qodo): learnt routes, reconnect, accept=none, checks before the wait --- bench/bench.sh | 21 +++++++ cli/vtesse_cli.c | 135 ++++++++++++++++++++++++------------------ docs/security.md | 3 +- docs/usage.md | 6 +- netcore/hostaddr.c | 26 ++++---- netcore/stack.c | 7 ++- tests/test_install.sh | 26 +++++++- 7 files changed, 150 insertions(+), 74 deletions(-) diff --git a/bench/bench.sh b/bench/bench.sh index 2156e79..9088f1a 100755 --- a/bench/bench.sh +++ b/bench/bench.sh @@ -175,6 +175,7 @@ cmd_accept_test() { " >/dev/null i=0 until logs | grep -q "waiting for the first packet" || [ $i -gt 20 ]; do sleep 1; i=$((i + 1)); done + # shellcheck disable=SC2034 # sha, t0: used inside the eval strings of check sha=$(docker exec "$c" cat /tmp/www/big.sha256) check "ip=learn: no address before the first packet" '! logs | grep -q "address 10.76"' check "accept: the server reaches the client's web server (first SYN teaches the address)" \ @@ -184,11 +185,31 @@ cmd_accept_test() { check "accept: 8 parallel 20 MiB downloads, sha256 intact" ' srv "for n in 1 2 3 4 5 6 7 8; do (curl -fsS --max-time 120 http://10.76.0.2:8080/big.bin | sha256sum > /tmp/acc.\$n) & done; wait" [ "$(srv "cat /tmp/acc.*" | cut -d" " -f1 | sort -u)" = "$sha" ]' + # shellcheck disable=SC2034 t0=$(date +%s) check "accept to a closed local port: the connection is reset at once" \ '! srv "curl -fsS --max-time 10 http://10.76.0.2:8081/" 2>/dev/null && [ $(($(date +%s) - t0)) -lt 5 ]' check "accept: log lines and counters" 'logs | grep -q "accept 8080 -> 127.0.0.1:8000: connected" && logs | grep -q "accept 8081 -> 127.0.0.1:8001: Connection refused"' + # the proxy's connection limit (24, shared with outbound forwards): the 25th is reset + # shellcheck disable=SC2034 # over: used inside the eval string of check + over=$(srv "python3 -c ' +import socket, time +held = [socket.create_connection((\"10.76.0.2\", 8080), timeout=5) for _ in range(24)] +time.sleep(1) +x = socket.create_connection((\"10.76.0.2\", 8080), timeout=5) +x.settimeout(5) +try: + print(\"reset\" if x.recv(1) == b\"\" else \"data\") +except ConnectionResetError: + print(\"reset\") +except socket.timeout: + print(\"timeout\") +for s in held: + s.close() +'" 2>&1) + check "accept: over the connection limit (24 held) the next one is reset" \ + '[ "$over" = reset ] && logs | grep -q "accept 8080: connection limit reached (24), reset"' # a changed address after a server restart is learnt again srv "sed -i 's/peer 10.76.0.2 /peer 10.76.0.3 /' /etc/vtund.conf" diff --git a/cli/vtesse_cli.c b/cli/vtesse_cli.c index 2dc8dee..331ae74 100644 --- a/cli/vtesse_cli.c +++ b/cli/vtesse_cli.c @@ -233,6 +233,13 @@ static int load_config(const char *path, cfg_file_t *c, int must_exist) snprintf(c->socks_user, sizeof(c->socks_user), "%s", v); else if (!strcmp(k, "socks_password")) snprintf(c->socks_password, sizeof(c->socks_password), "%s", v); + else if (!strcmp(k, "tap") || !strcmp(k, "hook") || !strcmp(k, "ifname")) { + /* a 1.x setting must not be ignored silently: the client would run another mode */ + fprintf(stderr, "%s: %s: the TAP backend was removed in vtesse 2.0.0 (#72); remove the line " + "(inbound access: accept=PORT)\n", full, k); + fclose(f); + return -1; + } } fclose(f); return 0; @@ -628,7 +635,7 @@ int main(int argc, char **argv) uint8_t mac[6]; tunnel_t t; nc_source_t src; - int i, err, rc, stop_early, learn_ip = 0; + int i, err, rc, stop_early, learn_ip = 0, accept_none = 0; nc_stack_cfg_t sc; nc_proxy_cfg_t pc; nc_proxy_t *px = NULL; @@ -701,6 +708,9 @@ int main(int argc, char **argv) return 2; } nrelays++; + } else if (!strcmp(argv[i], "--accept") && i + 1 < argc && !strcmp(argv[i + 1], "none")) { + accept_none = 1; + i++; } else if (!strcmp(argv[i], "--accept") && i + 1 < argc) { if (pc.accept_count >= NC_PROXY_MAX_ACCEPTS || nc_accept_parse(argv[++i], &pc.accepts[pc.accept_count]) < 0) { @@ -735,60 +745,8 @@ int main(int argc, char **argv) } if (!no_env && env_any(&env)) apply_env(&c, &env); - /* without a default route there is nothing to connect to (#73): wait for one in every - * start mode, with no time limit. At boot the uplink (wlan0 with a background udhcpc) - * may get it after the service starts; the MAC is then the uplink's, not eth0's. Any - * default route ends the wait: through PPPoE or tun (no MAC) eth0 is taken as before. */ - if (!show_config) { - signal(SIGINT, on_signal); - signal(SIGTERM, on_signal); - if (wait_default_route() < 0) - return 0; /* stopped while waiting */ - } - if (env_mode) { - /* only the server given: the VTun defaults of OpenIPC's tunnel */ - if (!c.port) - c.port = 5000; - if (!c.session[0] && !c.id_from[0] && !id_from && - ve_default_iface(c.id_from, sizeof(c.id_from)) < 0) { - fprintf(stderr, "no interface with a MAC for the device ID: set vtesse_session or vtesse_id_from\n"); - return 2; - } - } - if (c.udp_relay_count && !VTESSE_WITH_UDP_RELAY) { - fprintf(stderr, "%s: udp_relay: built without the UDP relay\n", config); - return 2; - } - if (id_from) - snprintf(c.id_from, sizeof(c.id_from), "%s", id_from); - if (c.id_from[0]) { - if (c.session[0] && !id_from) { - fprintf(stderr, "%s: session and id_from are mutually exclusive\n", config); - return 2; - } - if (id_from_interface(c.id_from, c.session, sizeof(c.session)) < 0) - return 2; - printf("device ID %s (MAC of %s)\n", c.session, c.id_from); - } else if (!c.session[0]) { - fprintf(stderr, "%s: session or id_from is required\n", config); - return 2; - } - if (env_mode && !c.password[0]) { - char mac_str[18], pw_if[32]; - - /* the default-route interface, as OpenIPC's tunnel does, whatever the session */ - if (ve_default_iface(pw_if, sizeof(pw_if)) < 0) - pw_if[0] = '\0'; - if (!pw_if[0] || ve_iface_mac(pw_if, mac_str) < 0) { - fprintf(stderr, "no interface with a MAC for the password: set vtesse_password\n"); - return 2; - } - ve_vtun_password(mac_str, c.password); - derived_password = 1; - printf("password: SHA-1 of the MAC of %s, as VTun on OpenIPC\n", pw_if); - } - if (check_config(&c, env_mode ? "U-Boot environment and config" : config) < 0) - return 2; + /* the settings that do not need the device's MAC are checked before the route wait: + * a mistake must not wait for the network to be reported */ /* manual IPv4: command line overrides the file */ if (ip_arg) snprintf(c.ip, sizeof(c.ip), "%s", ip_arg); @@ -825,8 +783,13 @@ int main(int argc, char **argv) nc_routes_add(&st_routes, net, (uint8_t)p, st_gw); } - /* inbound forwards from the file come after the command line ones (#67) */ + /* inbound forwards from the file come after the command line ones (#67); "none" (file + * or command line) means no inbound forwards at all, also no service defaults */ for (i = 0; i < c.accept_count; i++) { + if (!strcmp(c.accept[i], "none")) { + accept_none = 1; + continue; + } if (pc.accept_count >= NC_PROXY_MAX_ACCEPTS || nc_accept_parse(c.accept[i], &pc.accepts[pc.accept_count]) < 0) { fprintf(stderr, "%s: accept: expected PORT or PORT:A.B.C.D:PORT (max %d), got '%s'\n", config, @@ -838,7 +801,11 @@ int main(int argc, char **argv) /* the service started from the U-Boot environment: the camera's web, ssh and RTSP are * reachable from the server, on the camera's own LAN address (its web asks for the * password as from the LAN) */ - if (env_mode && !pc.accept_count) { + if (accept_none && pc.accept_count) { + fprintf(stderr, "accept=none cannot be combined with other accept lines or --accept\n"); + return 2; + } + if (env_mode && !pc.accept_count && !accept_none) { static const uint16_t def[] = { 80, 22, 554 }; for (i = 0; i < (int)(sizeof(def) / sizeof(def[0])); i++) { @@ -847,6 +814,60 @@ int main(int argc, char **argv) } pc.accept_count = i; } + /* without a default route there is nothing to connect to (#73): wait for one in every + * start mode, with no time limit. At boot the uplink (wlan0 with a background udhcpc) + * may get it after the service starts; the MAC is then the uplink's, not eth0's. Any + * default route ends the wait: through PPPoE or tun (no MAC) eth0 is taken as before. */ + if (!show_config) { + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); + if (wait_default_route() < 0) + return 0; /* stopped while waiting */ + } + if (env_mode) { + /* only the server given: the VTun defaults of OpenIPC's tunnel */ + if (!c.port) + c.port = 5000; + if (!c.session[0] && !c.id_from[0] && !id_from && + ve_default_iface(c.id_from, sizeof(c.id_from)) < 0) { + fprintf(stderr, "no interface with a MAC for the device ID: set vtesse_session or vtesse_id_from\n"); + return 2; + } + } + if (c.udp_relay_count && !VTESSE_WITH_UDP_RELAY) { + fprintf(stderr, "%s: udp_relay: built without the UDP relay\n", config); + return 2; + } + if (id_from) + snprintf(c.id_from, sizeof(c.id_from), "%s", id_from); + if (c.id_from[0]) { + if (c.session[0] && !id_from) { + fprintf(stderr, "%s: session and id_from are mutually exclusive\n", config); + return 2; + } + if (id_from_interface(c.id_from, c.session, sizeof(c.session)) < 0) + return 2; + printf("device ID %s (MAC of %s)\n", c.session, c.id_from); + } else if (!c.session[0]) { + fprintf(stderr, "%s: session or id_from is required\n", config); + return 2; + } + if (env_mode && !c.password[0]) { + char mac_str[18], pw_if[32]; + + /* the default-route interface, as OpenIPC's tunnel does, whatever the session */ + if (ve_default_iface(pw_if, sizeof(pw_if)) < 0) + pw_if[0] = '\0'; + if (!pw_if[0] || ve_iface_mac(pw_if, mac_str) < 0) { + fprintf(stderr, "no interface with a MAC for the password: set vtesse_password\n"); + return 2; + } + ve_vtun_password(mac_str, c.password); + derived_password = 1; + printf("password: SHA-1 of the MAC of %s, as VTun on OpenIPC\n", pw_if); + } + if (check_config(&c, env_mode ? "U-Boot environment and config" : config) < 0) + return 2; if (show_config && !vt_session_name_valid(c.session)) { fprintf(stderr, "session '%s': no spaces, colons or control characters\n", c.session); return 2; diff --git a/docs/security.md b/docs/security.md index 19a4ef9..eabce7d 100644 --- a/docs/security.md +++ b/docs/security.md @@ -41,7 +41,8 @@ opens one port of the device to everyone on the VTun server side, in clear text. The service started by `vtesse --install` or the U-Boot environment opens 80, 22 and 554 by default, to the device's LAN address: its own logins (web, ssh, RTSP) still - apply, so keep them set; remove the `accept` lines that are not needed. + apply, so keep them set. Remove the `accept` lines that are not needed; `accept=none` + opens no port at all (and `vtesse --install` then adds none). ## U-Boot environment diff --git a/docs/usage.md b/docs/usage.md index 0607404..1f23da8 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -146,7 +146,9 @@ accept=8554:127.0.0.1:554 # explicit target configuration without `accept` lines, and a client started from the [U-Boot environment](#u-boot-environment-helpdesk-access) without `accept` lines opens the same ports: the server reaches the camera's web interface, ssh and RTSP, as the - TAP backend of 1.x gave it. + TAP backend of 1.x gave it. `accept=none` (or `--accept none`) turns that off: no + inbound forwards, and `--install` adds nothing; it cannot be combined with other + `accept` lines. - On a `type tun` session the server needs the client's address before it can connect: set it with `ip=`, or let the client learn it from the first connection (`ip=learn`, [manual IPv4](#manual-ipv4-type-tun)). @@ -275,7 +277,7 @@ never from the command line; keep it outside the repository with mode 600. | `session` | session name = [device ID](#device-id) | | `id_from` | instead of `session`: take the device ID from the MAC of this interface, e.g. `eth0` | | `udp_relay` | `PORT[:LOCAL_PORT]`, repeatable: [UDP relay](#udp-relay-rtp-from-cameras) | -| `accept` | `PORT[:IP:PORT]`, repeatable, up to 4: [inbound forwards](#inbound-forwards) | +| `accept` | `PORT[:IP:PORT]`, repeatable, up to 4: [inbound forwards](#inbound-forwards); `none`: none at all, not even the service defaults | | `ip`, `gw`, `route` | [manual IPv4](#manual-ipv4-type-tun): `ip=A.B.C.D/P` or `ip=learn`, optional `gw=A.B.C.D`, `route=NET/P` (repeatable, up to 16); required for `type tun` | | `password` | session password | | `auth` | challenge response, `xor` (default; servers built without OpenSSL) | diff --git a/netcore/hostaddr.c b/netcore/hostaddr.c index 99a9ec0..f6cd24d 100644 --- a/netcore/hostaddr.c +++ b/netcore/hostaddr.c @@ -25,20 +25,26 @@ uint32_t nc_default_route_addr(void) if (!f) return 0; - while (!found && fgets(line, sizeof(line), f)) + if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + fclose(f); + return 0; + } + /* every default route in order: one through an interface without IPv4 is skipped */ + while (!a && fgets(line, sizeof(line), f)) { found = sscanf(line, "%15s %lx %lx %x %d %d %d %lx", ifname, &dst, &gw, &flags, &ref, &use, &metric, &mask) == 8 && dst == 0 && mask == 0; - fclose(f); - if (!found || (s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) - return 0; - memset(&ifr, 0, sizeof(ifr)); - memcpy(ifr.ifr_name, ifname, strlen(ifname) + 1); - if (ioctl(s, SIOCGIFADDR, &ifr) == 0 && ifr.ifr_addr.sa_family == AF_INET) { - struct sockaddr_in sin; + if (!found) + continue; + memset(&ifr, 0, sizeof(ifr)); + memcpy(ifr.ifr_name, ifname, strlen(ifname) + 1); + if (ioctl(s, SIOCGIFADDR, &ifr) == 0 && ifr.ifr_addr.sa_family == AF_INET) { + struct sockaddr_in sin; - memcpy(&sin, &ifr.ifr_addr, sizeof(sin)); - a = ntohl(sin.sin_addr.s_addr); + memcpy(&sin, &ifr.ifr_addr, sizeof(sin)); + a = ntohl(sin.sin_addr.s_addr); + } } + fclose(f); close(s); return a; } diff --git a/netcore/stack.c b/netcore/stack.c index 4cddd6a..8a829f2 100644 --- a/netcore/stack.c +++ b/netcore/stack.c @@ -145,6 +145,10 @@ int nc_stack_route_ok(uint32_t dst) if (!st.bound) return 0; + /* learn_ip: after a reconnect the old address may be gone; nothing goes out until a + * packet of the new session confirms (or replaces) it (#68) */ + if (st.cfg.learn_ip && !st.learnt) + return 0; addr = lwip_ntohl(ip4_addr_get_u32(netif_ip4_addr(&st.netif))); mask = lwip_ntohl(ip4_addr_get_u32(netif_ip4_netmask(&st.netif))); if ((dst & mask) == (addr & mask)) @@ -253,7 +257,8 @@ static void status_cb(struct netif *n) int has_addr = netif_is_up(n) && !ip4_addr_isany_val(*netif_ip4_addr(n)); if (has_addr && !st.bound) { - if (!st.cfg.static_ip) + /* a manual or learnt address has its routes already (#68): only DHCP sets them here */ + if (!st.cfg.static_ip && !st.cfg.learn_ip) apply_dhcp_routes(netif_dhcp_data(n)); st.bound = 1; emit(NC_STACK_EV_ADDR_BOUND, 0, 0); diff --git a/tests/test_install.sh b/tests/test_install.sh index 6492e49..93062a9 100755 --- a/tests/test_install.sh +++ b/tests/test_install.sh @@ -14,6 +14,8 @@ trap 'rm -rf "$T"' EXIT check() { if eval "$2"; then echo "PASS $1"; else echo "FAIL $1"; fails=$((fails + 1)); fi; } conf_has() { grep -qx "$2" "$1/etc/vtesse.conf"; } +# a build without the UDP relay (make UDP_RELAY=0) refuses udp_relay lines +if "$CLI" --version | grep -q udp-relay; then RELAY=udp_relay=1559; else RELAY="# no UDP relay in this build"; fi mode() { stat -c %a "$1"; } # ---- OpenIPC: detected from /etc/os-release @@ -68,6 +70,10 @@ echo accept=8080 >> "$R/etc/vtesse.conf" "$CLI" --install --root "$R" >/dev/null 2>&1 check "a config with its own accept line keeps it, no defaults added" \ 'conf_has "$R" accept=8080 && [ "$(grep -c "^accept=" "$R/etc/vtesse.conf")" = 1 ]' +sed -i 's/^accept=8080$/accept=none/' "$R/etc/vtesse.conf" +"$CLI" --install --root "$R" >/dev/null 2>&1 +check "accept=none: the opt-out is kept, no defaults added" \ + 'conf_has "$R" accept=none && [ "$(grep -c "^accept=" "$R/etc/vtesse.conf")" = 1 ]' "$CLI" --uninstall --root "$R" >"$T/log" 2>&1 check "uninstall: everything removed, the config included" \ @@ -181,6 +187,9 @@ check "port=abc in the file: refused" '[ $rc -ne 0 ]' # ---- TAP options of 1.x: refused with the reason (#72) "$CLI" --tap --config "$T/none.conf" >"$T/log" 2>&1; rc=$? check "--tap: refused, the TAP backend was removed" '[ $rc -eq 2 ] && grep -q "TAP backend was removed" "$T/log"' +printf 'host=h\nport=5000\nsession=02AB34CD56EF\npassword=p\ntap=1\n' > "$T/tap.conf" +"$CLI" --no-env --config "$T/tap.conf" --show-config >"$T/log" 2>&1; rc=$? +check "tap= in the configuration: refused, not ignored" '[ $rc -ne 0 ] && grep -q "tap: the TAP backend was removed" "$T/log"' # ---- U-Boot environment (#64): fake fw_printenv / fw_setenv in PATH mkdir -p "$T/env" @@ -206,6 +215,13 @@ if [ -n "$iface" ] && [ -r "/sys/class/net/$iface/address" ]; then 'grep -qx "accept 80 -> (default-route address):80" "$T/log" && grep -qx "accept 22 -> (default-route address):22" "$T/log" && grep -qx "accept 554 -> (default-route address):554" "$T/log"' + printf 'accept=none\n' > "$T/none-accept.conf" + echo "vtesse=vtun.helpdesk" > "$T/env/vars" + PATH=$EP "$CLI" --config "$T/none-accept.conf" --show-config >"$T/log" 2>&1 + check "env: accept=none turns the default ports off" '! grep -q "^accept " "$T/log" && grep -qx "host vtun.helpdesk" "$T/log"' + printf 'accept=none\naccept=80\n' > "$T/none-accept.conf" + PATH=$EP "$CLI" --config "$T/none-accept.conf" --show-config >"$T/log" 2>&1; rc=$? + check "accept=none with another accept line: refused" '[ $rc -eq 2 ] && grep -q "accept=none cannot be combined" "$T/log"' printf 'vtesse=vtun.helpdesk\nvtesse_session=02AB34CD56EF\n' > "$T/env/vars" PATH=$EP "$CLI" --config "$T/none.conf" --show-config >"$T/log" 2>&1 check "env: an explicit session still gets the MAC-derived password" \ @@ -222,10 +238,10 @@ PATH=$EP "$CLI" --config "$T/f.conf" --show-config >"$T/log" 2>&1 check "env overrides the file key by key" \ 'grep -qx "host env.example" "$T/log" && grep -qx "port 7000" "$T/log" && grep -qx "session 02AB34CD56EF" "$T/log" && grep -qx "password (set)" "$T/log"' -printf 'route=10.78.0.0/24\nip=10.76.0.2/32\nudp_relay=1559\naccept=8080:127.0.0.1:80\n' >> "$T/f.conf" +printf 'route=10.78.0.0/24\nip=10.76.0.2/32\n%s\naccept=8080:127.0.0.1:80\n' "$RELAY" >> "$T/f.conf" PATH=$EP "$CLI" --config "$T/f.conf" --show-config >"$T/log" 2>&1 check "--show-config lists routes, UDP relays and accepts" \ - 'grep -qx "route 10.78.0.0/24" "$T/log" && grep -qx "udp_relay 1559" "$T/log" && + 'grep -qx "route 10.78.0.0/24" "$T/log" && { [ "$RELAY" != udp_relay=1559 ] || grep -qx "udp_relay 1559" "$T/log"; } && grep -qx "accept 8080 -> 127.0.0.1:80" "$T/log" && ! grep -q "accept 80 " "$T/log"' sed -i '/^accept=/d' "$T/f.conf" printf 'port=abc\n' >> "$T/f.conf" @@ -255,7 +271,11 @@ check "vtesse_ip with a malformed prefix: refused, not taken for /0" '[ $rc -eq printf 'host=file.example\nport=6000\nsession=02AB34CD56EF\npassword=filepw\nudp_relay=not-a-port\n' > "$T/r.conf" echo vtesse=env.example > "$T/env/vars" PATH=$EP "$CLI" --config "$T/r.conf" --show-config >"$T/log" 2>&1; rc=$? -check "--show-config refuses a udp_relay the client would refuse" '[ $rc -eq 2 ] && grep -q "udp_relay: expected" "$T/log"' +if [ "$RELAY" = udp_relay=1559 ]; then + check "--show-config refuses a udp_relay the client would refuse" '[ $rc -eq 2 ] && grep -q "udp_relay: expected" "$T/log"' +else + check "--show-config refuses udp_relay in a build without it" '[ $rc -eq 2 ] && grep -q "built without the UDP relay" "$T/log"' +fi printf 'vtesse=env.example\nvtesse_port=7000\n' > "$T/env/vars" echo 1 > "$T/env/rc" PATH=$EP "$CLI" --config "$T/f.conf" --show-config >"$T/log" 2>&1 From 8b70e539c3e299059270e51a3b55deb144f9ec7f Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Thu, 22 Oct 2026 21:30:25 +0000 Subject: [PATCH 4/5] Review fixes, round 2 (Qodo): learn only from a valid IPv4 header, more checks before the wait --- cli/vtesse_cli.c | 34 +++++++++++++++++++++++++--------- netcore/stack.c | 24 ++++++++++++++++++++---- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/cli/vtesse_cli.c b/cli/vtesse_cli.c index 331ae74..7990deb 100644 --- a/cli/vtesse_cli.c +++ b/cli/vtesse_cli.c @@ -814,6 +814,31 @@ int main(int argc, char **argv) } pc.accept_count = i; } + /* the same check as at start, without creating the relays; before the route wait */ + if (c.udp_relay_count && !VTESSE_WITH_UDP_RELAY) { + fprintf(stderr, "%s: udp_relay: built without the UDP relay\n", config); + return 2; + } + for (i = 0; i < c.udp_relay_count; i++) { + uint16_t port, local; + + if (nrelays + i >= NC_UDP_RELAY_MAX || nc_udp_relay_parse(c.udp_relay[i], &port, &local) < 0) { + fprintf(stderr, "%s: udp_relay: expected PORT[:LOCAL_PORT] (max %d)\n", config, NC_UDP_RELAY_MAX); + return 2; + } + } + /* what needs no MAC is checked before the route wait as well (#73): with only the + * U-Boot environment the password and the session may still come from the MAC */ + if (!env_mode && c.session[0] && !c.id_from[0] && !id_from && !vt_session_name_valid(c.session)) { + fprintf(stderr, "session '%s': no spaces, colons or control characters\n", c.session); + return 2; + } + if (!env_mode && check_config(&c, config) < 0) + return 2; + if (env_mode && strcmp(c.auth, "xor") != 0) { + fprintf(stderr, "U-Boot environment and config: unsupported auth '%s' (only xor)\n", c.auth); + return 2; + } /* without a default route there is nothing to connect to (#73): wait for one in every * start mode, with no time limit. At boot the uplink (wlan0 with a background udhcpc) * may get it after the service starts; the MAC is then the uplink's, not eth0's. Any @@ -872,15 +897,6 @@ int main(int argc, char **argv) fprintf(stderr, "session '%s': no spaces, colons or control characters\n", c.session); return 2; } - /* the same check as at start, without creating the relays */ - for (i = 0; show_config && i < c.udp_relay_count; i++) { - uint16_t port, local; - - if (nrelays + i >= NC_UDP_RELAY_MAX || nc_udp_relay_parse(c.udp_relay[i], &port, &local) < 0) { - fprintf(stderr, "%s: udp_relay: expected PORT[:LOCAL_PORT] (max %d)\n", config, NC_UDP_RELAY_MAX); - return 2; - } - } if (show_config) { printf("host %s\nport %d\nsession %s\npassword %s\n", c.host, c.port, c.session, derived_password ? c.password : "(set)"); diff --git a/netcore/stack.c b/netcore/stack.c index 8a829f2..1e2785a 100644 --- a/netcore/stack.c +++ b/netcore/stack.c @@ -293,15 +293,31 @@ static int unicast(uint32_t a) * destination of the first unicast packet is it, the source the peer. Set before the * packet enters lwIP, so a first SYN is accepted. A packet of a new session with the * same address changes nothing; another address replaces the old one. */ -static void learn(const uint8_t *pkt) +/* a whole, well-formed IPv4 header: lengths and checksum, as lwIP checks them, so a + * packet lwIP would drop cannot teach the address */ +static int ip4_header_ok(const uint8_t *pkt, size_t len) +{ + size_t hl = (size_t)(pkt[0] & 0x0f) * 4, tot = (size_t)pkt[2] << 8 | pkt[3], i; + uint32_t sum = 0; + + if (hl < 20 || hl > len || tot < hl || tot > len) + return 0; + for (i = 0; i < hl; i += 2) + sum += (uint32_t)pkt[i] << 8 | pkt[i + 1]; + while (sum >> 16) + sum = (sum & 0xffff) + (sum >> 16); + return sum == 0xffff; +} + +static void learn(const uint8_t *pkt, size_t len) { uint32_t src = (uint32_t)pkt[12] << 24 | (uint32_t)pkt[13] << 16 | (uint32_t)pkt[14] << 8 | pkt[15]; uint32_t dst = (uint32_t)pkt[16] << 24 | (uint32_t)pkt[17] << 16 | (uint32_t)pkt[18] << 8 | pkt[19]; ip4_addr_t a, m, g; int was_bound = st.bound; - if ((pkt[0] & 0x0f) < 5 || !unicast(dst) || !unicast(src) || dst == src) - return; + if (!ip4_header_ok(pkt, len) || !unicast(dst) || !unicast(src) || dst == src) + return; /* the next packet may teach it */ st.learnt = 1; if (was_bound && lwip_ntohl(ip4_addr_get_u32(netif_ip4_addr(&st.netif))) == dst && lwip_ntohl(ip4_addr_get_u32(netif_ip4_gw(&st.netif))) == src) @@ -324,7 +340,7 @@ static void on_rx(void *user, const uint8_t *pkt, size_t len) if (st.link == TUNNEL_LINK_L2 ? !l2_accept(pkt, len) : (len < 20 || (pkt[0] >> 4) != 4)) return; if (st.link == TUNNEL_LINK_L3 && st.cfg.learn_ip && !st.learnt) - learn(pkt); + learn(pkt, len); p = pbuf_alloc(PBUF_RAW, (u16_t)len, PBUF_POOL); if (!p) return; From 06fdd8e419383439b272d1dfc2ccf70afbca2fcf Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Thu, 22 Oct 2026 21:30:25 +0000 Subject: [PATCH 5/5] 2.0.0: userspace only, inbound forwards, ip=learn, default-route wait --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ include/vtesse/version.h | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22730c8..3a6911b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,38 @@ from every push to `main`; this file is the change history. Versions after 1.12 `MAJOR.MINOR.PATCH` (1.12 is followed by 1.12.1 or 1.13.0); the headings below keep the numbers as released. +## 2.0.0 — 2026-10-22 + +The userspace stack only: inbound access without a kernel interface, the TAP backend +removed (#72). Incompatible: read the last points before updating a device. + +- Inbound forwards (#67): `accept=PORT[:IP:PORT]` / `--accept`, up to 4: the stack + listens on `PORT` of the tunnel address and relays each connection from the server to + a host socket, by default the same port on the address of the default-route interface + (the device's own LAN address, so its web interface asks for the password as on the + LAN). A closed or silent local port resets the connection; each connection is logged + and counted. +- `ip=learn` / `--ip learn` for `type tun` (#68): the address is the destination of the + first unicast packet from the server, its source the peer; set before the packet + enters the stack, so a first SYN is accepted; a changed address after a reconnect + replaces the old one. `type ether` with `ip=learn` stops the client (exit code 2). +- The service opens the device's web interface, ssh and RTSP to the server: `--install` + writes `accept=80`, `accept=22` and `accept=554` to a configuration without `accept` + lines, and a client started from the U-Boot environment (`fw_setenv vtesse`) without + `accept` lines opens the same ports. `accept=none` (or `--accept none`) turns them off. +- The client always waits for a default route before it starts (#73), in every start + mode, with no time limit: one line when the wait starts and one per minute; + SIGINT/SIGTERM end it (exit code 0). It used to wait up to 60 s, only in the U-Boot + environment mode. +- Removed (#72): the TAP backend (`--tap`, `--ifname`, `--hook`, refused with the reason, as + are `tap=`, `hook=` and `ifname=` lines in the configuration), + the `LWIP=0` and `TAP=0` build switches, the hook scripts `vtesse-hook.sh` and + `vtesse-udhcpc.script` (no longer in the release) and `--install --scripts`. +- Updating a 1.x installation: `vtesse --install` removes the old hook scripts and adds + the `accept` lines; an `/etc/default/vtesse` with `ARGS="--tap ..."` is reported and + must be cleaned by hand, otherwise the service does not start. The service scripts no + longer load `tun`. + ## 1.15.0 — 2026-10-18 Settings in the U-Boot environment for helpdesk access (#64). diff --git a/include/vtesse/version.h b/include/vtesse/version.h index 294a35c..7570abf 100644 --- a/include/vtesse/version.h +++ b/include/vtesse/version.h @@ -3,6 +3,6 @@ /* MAJOR.MINOR.PATCH (up to 1.12 it was MAJOR.MM). Shown by --version and in the * title of the "latest" release (CI); record each bump in CHANGELOG.md. */ -#define VTESSE_VERSION "1.15.0" +#define VTESSE_VERSION "2.0.0" #endif