diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b66936..bf55a1f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,18 +1,36 @@ name: Build -on: [push] +on: + push: + pull_request: + workflow_dispatch: jobs: build: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: [ubuntu-latest, macOS-latest] + include: + - os: ubuntu-latest + install: | + sudo apt-get update + sudo apt-get install -y meson ninja-build pkg-config libsodium-dev + - os: macos-latest + install: | + brew update + brew install meson ninja libsodium pkg-config steps: - - uses: actions/checkout@v2 - - name: Build glorytun - run: | - git submodule update --init --recursive - ./sodium.sh - make + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Install dependencies + run: ${{ matrix.install }} + + - name: Meson build + run: | + meson setup build --buildtype=release -Dstatic=false + meson compile -C build diff --git a/.github/workflows/upload.yml b/.github/workflows/upload.yml index 7216f3b..09bb730 100644 --- a/.github/workflows/upload.yml +++ b/.github/workflows/upload.yml @@ -1,29 +1,134 @@ -name: Upload +# Tag releases: source tarball (autotools distcheck) + static Meson binaries + publish. +name: Release on: push: - tags: - - "v*" + tags: ['v*'] + +permissions: + contents: write jobs: - build: + source-tarball: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libtool pkg-config libsodium-dev + + - name: distcheck + run: | + ./autogen.sh + ./configure --disable-dependency-tracking + make distcheck + + - name: Upload source tarball artifact + uses: actions/upload-artifact@v4 + with: + name: source-tarball + path: glorytun-*.tar.gz + + linux-binaries: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - id: linux-x86_64-glibc-static + experimental: false + - id: linux-x86_64-musl-static + experimental: false + - id: linux-x86-musl-static + experimental: false + - id: linux-aarch64-musl-static + experimental: false + - id: linux-armv7-musl-static + experimental: false + - id: linux-mips-musl-static + experimental: true + - id: linux-mipsel-musl-static + experimental: true + - id: linux-mips64-musl-static + experimental: true + - id: linux-mips64el-musl-static + experimental: true steps: - - name: Install libsodium-dev - run: | - sudo apt-get install libsodium-dev - - - uses: actions/checkout@v2 - - name: Run distcheck - run: | - git submodule update --init --recursive - ./autogen.sh - ./configure --disable-dependency-tracking - make distcheck - - - name: Upload dist - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release upload v$(cat VERSION) $(echo glorytun-*.tar.gz) + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y build-essential meson ninja-build pkg-config curl file git + + - name: Install Zig (musl cross targets) + if: ${{ matrix.id != 'linux-x86_64-glibc-static' }} + uses: ziglang/setup-zig@v2 + with: + version: 0.13.0 + + - name: Build static binary + id: build + continue-on-error: ${{ matrix.experimental }} + run: | + chmod +x scripts/ci/build-static-binary.sh + scripts/ci/build-static-binary.sh "${{ matrix.id }}" + + - name: Upload binary artifact + if: steps.build.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: binary-${{ matrix.id }} + path: glorytun-*-${{ matrix.id }} + if-no-files-found: error + + macos-binary: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Build (darwin-arm64, libsodium static; links dynamically to system libs) + run: | + chmod +x scripts/ci/build-static-binary.sh + scripts/ci/build-static-binary.sh darwin-arm64 + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: binary-darwin-arm64 + path: glorytun-*-darwin-arm64 + if-no-files-found: error + + publish: + runs-on: ubuntu-latest + needs: [source-tarball, linux-binaries, macos-binary] + steps: + - name: Download release assets + uses: actions/download-artifact@v4 + with: + pattern: '*' + merge-multiple: true + path: release-assets + + - name: Create GitHub release and upload files + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: ${{ github.ref_name }} + generate_release_notes: true + files: | + release-assets/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index a9094c3..3213dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,12 @@ build-aux .deps .dirstamp .sodium +.ci-cache +.zig-cache glorytun build* VERSION +*~ +cross-linux-* +*.tar.gz +.nfs* diff --git a/.gitmodules b/.gitmodules index c9814e3..720b878 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "mud"] path = mud - url = https://github.com/angt/mud.git + url = https://github.com/haqa/mud.git ignore = dirty [submodule "argz"] path = argz diff --git a/COMMAND-LINE.md b/COMMAND-LINE.md new file mode 100644 index 0000000..d2409c4 --- /dev/null +++ b/COMMAND-LINE.md @@ -0,0 +1,291 @@ +# Glorytun Command-Line Reference + +Glorytun uses the [argz](https://github.com/angt/argz) library for declarative, +table-driven command-line parsing. Arguments are keyword-based (not prefixed with +`--`). The special keyword `help` can be appended after any command or nested +context to display its usage line. + +``` +glorytun [options...] +``` + +## Commands + +Running `glorytun` with no arguments (or an unknown command) prints this list. + +| Command | Description | +|-----------|--------------------------------| +| `list` | List tunnel control socket paths | +| `show` | Show tunnel info or errors | +| `bench` | Run a crypto benchmark | +| `bind` | Start a new tunnel | +| `set` | Change tunnel timing options | +| `keygen` | Generate a new secret key | +| `path` | Show or configure paths | +| `version` | Show version and libsodium | + +--- + +## `glorytun bind` + +Start a new tunnel. Creates a tunnel device, establishes the encrypted UDP +transport, and enters the main packet-forwarding loop. + +``` +glorytun bind keyfile FILE [dev NAME] + [from addr IPADDR port PORT] + [to addr IPADDR port PORT] + [chacha] [persist] +``` + +| Option | Type | Description | +|--------|------|-------------| +| `keyfile FILE` | string | **Required.** Path to the secret key file (hex). | +| `dev NAME` | string | Tunnel interface name to create or use. | +| `from addr … port …` | nested | Local UDP bind address and port (IPv4 or IPv6). | +| `to addr … port …` | nested | Remote peer address and port (client / outbound). | +| `chacha` | flag | Force ChaCha20-Poly1305 instead of AEGIS-256 when available. | +| `persist` | flag | Keep the tunnel device after the process exits. | + +Both `from` and `to` use a nested form: the keyword `from` or `to`, then +`addr` and an IP address, then `port` and a port number. Omit `from` to use the +defaults (IPv4 `0.0.0.0`, UDP port `5000`). Omit `to` for server mode (listen +only). + +**Defaults** + +- Local bind defaults to `0.0.0.0:5000` if `from` is omitted. +- If AEGIS-256 is not available on the platform, ChaCha20-Poly1305 is used + automatically unless `chacha` forces the fallback. +- On `SIGHUP`, persist mode is enabled on the device before exit (graceful + reload). + +**Examples** + +Server — listen on all interfaces, port 5000: + +```sh +glorytun bind keyfile /etc/glorytun/key dev tun0 persist +``` + +Server — listen on a specific address: + +```sh +glorytun bind keyfile /etc/glorytun/key dev tun0 persist \ + from addr 203.0.113.1 port 5000 +``` + +Client — connect to a remote peer: + +```sh +glorytun bind keyfile /etc/glorytun/key dev tun0 persist \ + to addr 203.0.113.1 port 5000 +``` + +--- + +## `glorytun show` + +Show information about a running tunnel. + +``` +glorytun show [dev NAME] [errors] +``` + +| Option | Type | Description | +|------------|------|-------------| +| `dev NAME` | string | Tunnel device. If omitted and only one tunnel exists, it may be auto-selected (see `glorytun list`). | +| `errors` | flag | Show per-error counters instead of status. | + +**Without `errors`**, prints: tunnel name, local and remote address.port, PID, +MTU, and cipher (`aegis256` or `chacha20poly1305`). + +**With `errors`**, prints counters for `decrypt`, `clocksync`, and `keyx`, each +with last source address and port when present. + +--- + +## `glorytun set` + +Change timing-related properties of a running tunnel. + +``` +glorytun set [dev NAME] [kxtimeout TIME] [timetolerance TIME] [keepalive TIME] +``` + +| Option | Type | Description | +|--------|------|-------------| +| `dev NAME` | string | Tunnel device. | +| `kxtimeout TIME` | time | Key exchange (rotation) timeout. | +| `timetolerance TIME` | time | Clock sync tolerance between peers. | +| `keepalive TIME` | time | Keepalive interval. | + +All time values accept [time suffixes](#time-suffixes). Values are applied +together in one control message; omitted options keep their previous values on +the daemon side as implemented. + +--- + +## `glorytun path` + +Manage paths on a running tunnel (multipath). You must select a tunnel with +`dev`, optionally narrow the path with `addr` (local IP) and `to` (remote), then +either **show** status or **set** configuration. + +**View path status (default)** + +``` +glorytun path [dev NAME] [addr IP] [to addr IP port PORT] + [show [mtu|rtt|stat]] +``` + +With no `set` keyword, the daemon returns path status. Optional `show` picks a +view: `mtu` (MTU probe columns), `rtt` (RTT / RTT variance), `stat` (TX/RX rates +and loss). Without `show`, a default wide status table is printed. + +**Change path settings** + +``` +glorytun path [dev NAME] [addr IP] [to addr IP port PORT] set + [up|down] + [rate [fixed|auto] [tx RATE] [rx RATE]] + [beat TIME] [pref N] [losslimit PERCENT] +``` + +| Option | Type | Description | +|--------|------|-------------| +| `dev NAME` | string | Tunnel device (**required** for path operations). | +| `addr IP` | IPv4/IPv6 | Select path by local source address. | +| `to addr … port …` | nested | Select path by remote address (optional filter). | +| `set` | context | Introduces path configuration keywords below. | +| `up` / `down` | choice | Enable or disable the path (mutually exclusive). | +| `rate` | nested | `fixed` or `auto` (mutually exclusive), plus `tx` / `rx` caps ([size suffixes](#size-suffixes)). | +| `beat TIME` | time | Internal heartbeat / probe interval. | +| `pref N` | integer | Path preference (0–127); higher values bias scheduling. | +| `losslimit PERCENT` | percent | Loss threshold to treat the path as too lossy (0–100; optional `%`). | + +The `path set` form is required to change state or rates. There is **no** +separate `backup` keyword on the CLI; the daemon may still use internal backup +semantics. To favour some paths over others, use different `pref` and `tx` / +`rx` values. + +**Examples** + +```sh +glorytun path dev tun0 +glorytun path dev tun0 addr 192.168.1.100 +glorytun path dev tun0 show rtt +glorytun path dev tun0 addr 192.168.1.100 set up beat 5s losslimit 30% \ + rate fixed tx 12500k rx 12500k +glorytun path dev tun0 addr 192.168.1.100 set down +``` + +With **`rate auto`**, `tx` and `rx` are ceilings: the implementation probes up to +those limits. + +--- + +## `glorytun bench` + +Run a cryptographic throughput benchmark (min / mean / max Mbps over packet +sizes). + +``` +glorytun bench [fallback] +``` + +| Option | Type | Description | +|--------|------|-------------| +| `fallback` | flag | Benchmark ChaCha20-Poly1305 instead of AEGIS-256. | + +If `fallback` is omitted and AEGIS-256 is available, it is benchmarked. If +AEGIS-256 is not available, ChaCha20-Poly1305 is used. + +--- + +## `glorytun keygen` + +Print a new random 256-bit secret key as 64 hex characters. No options. + +``` +glorytun keygen +``` + +--- + +## `glorytun list` + +Print paths to control sockets for running tunnels (one line per socket), for +use with `show` / `set` / `path`. + +``` +glorytun list +``` + +--- + +## `glorytun version` + +Print the program version and linked libsodium version. + +``` +glorytun version +``` + +--- + +## Value Types + +### Time suffixes + +Options that take time support: + +| Suffix | Meaning | +|--------|---------| +| `ms` | milliseconds | +| *(none)*, `s` | seconds (value scaled to milliseconds internally) | +| `m` | minutes | +| `h` | hours | +| `d` | days | +| `w` | weeks | + +### Size suffixes + +Rate and size options accept SI/binary prefixes and `bit` / `byte` units; see +the argz helpers in the build. Examples: `100Mbit`, `12500k`, `1GiB`. + +### Percent suffix + +`losslimit` accepts an optional `%` suffix (e.g. `30%`) or a raw 0–100 value. + +### Pipe-separated choices + +Where this document lists `a|b` (e.g. `fixed|auto`, `up|down`), only one of the +group may be set; duplicates produce an error. + +--- + +## Help + +```sh +glorytun bind help +glorytun path help +glorytun set help +glorytun show help +glorytun bench help +glorytun list help +glorytun version help +glorytun keygen help +``` + +--- + +## Option parsing rules + +- **Named options** (`dev`, `keyfile`, `addr`, …) are keywords followed by + values where required. +- **Flags** (`persist`, `chacha`, `fallback`, `errors`) need no value. +- **Nested groups** (`from` / `to` with `addr` and `port`; `path` … `set` with + `rate`, `up`, …) consume following tokens until the group is complete. +- **Order** of top-level keywords is generally flexible; use `help` on each + command to see the exact usage string the binary expects. diff --git a/MULTIPATH-EXAMPLE.md b/MULTIPATH-EXAMPLE.md new file mode 100644 index 0000000..209d194 --- /dev/null +++ b/MULTIPATH-EXAMPLE.md @@ -0,0 +1,340 @@ +# Multipath Tunnel Example + +This example sets up a Glorytun multipath tunnel between two machines: + +- **Machine A** — a site device with four network interfaces (`eth1`–`eth4`), + each receiving a DHCP address. +- **Machine B** — a cloud concentrator with a single public IP address. + +All four WAN links on Machine A are bonded into one tunnel, providing +aggregated bandwidth and failover. + +## Network Topology + +``` +Machine A (site) Machine B (cloud concentrator) +┌─────────────────────┐ ┌─────────────────────┐ +│ │ │ │ +│ eth1 192.168.1.x ─┼───── path 1 ─────┼─▶ │ +│ eth2 10.0.0.x ─┼───── path 2 ─────┼─▶ eth0 203.0.113.1│ +│ eth3 172.16.0.x ─┼───── path 3 ─────┼─▶ │ +│ eth4 10.1.0.x ─┼───── path 4 ─────┼─▶ │ +│ │ │ │ +│ tun0 ──────┼───── tunnel ──────┼────── tun0 │ +└─────────────────────┘ └─────────────────────┘ +``` + +The example addresses below are placeholders. Replace them with the actual +DHCP-assigned addresses on Machine A and the real public IP of Machine B. + +## Prerequisites + +Glorytun creates TUN interfaces via `/dev/net/tun` on Linux. This requires: + +1. **The `tun` kernel module must be loaded.** Check and load it on both + machines: + +```sh +lsmod | grep tun +modprobe tun +``` + +To make it persistent across reboots: + +```sh +echo tun >> /etc/modules-load.d/tun.conf +``` + +2. **`/dev/net/tun` must exist.** On most distributions this is created + automatically when the module loads. If it is missing: + +```sh +mkdir -p /dev/net +mknod /dev/net/tun c 10 200 +chmod 0666 /dev/net/tun +``` + +3. **Root privileges (or `CAP_NET_ADMIN`).** Creating TUN devices and + configuring interfaces requires elevated permissions. Run glorytun as root + or grant the capability: + +```sh +setcap cap_net_admin+ep /usr/local/bin/glorytun +``` + +4. **A key directory.** Create it on both machines: + +```sh +mkdir -p /etc/glorytun +``` + +## Step 1 — Generate a Shared Key (once, on either machine) + +```sh +glorytun keygen > /etc/glorytun/key +chmod 600 /etc/glorytun/key +``` + +Copy `/etc/glorytun/key` to the other machine via a secure channel (e.g. `scp`). +Both sides must use the same key. + +## Step 2 — Start the Tunnel on Machine B (concentrator) + +Machine B listens on its public IP, port 5000. Omit `to` so it runs in server +mode — it accepts incoming connections from any peer. + +`glorytun bind` creates the TUN interface and runs in the foreground. Use `&` +to background it, or run it in a dedicated terminal / systemd service. + +```sh +glorytun bind keyfile /etc/glorytun/key dev tun0 persist \ + from addr 203.0.113.1 port 5000 & +``` + +The `persist` flag keeps the TUN interface alive if the process exits. Without +it, the interface disappears when glorytun stops. + +Wait a moment for the interface to appear, then assign an IP and bring it up +(glorytun creates the interface but does not configure addressing or link state): + +```sh +ip addr add 10.99.0.1/30 dev tun0 +ip link set tun0 up +``` + +## Step 3 — Start the Tunnel on Machine A (site) + +Machine A uses the default local bind (`0.0.0.0:5000`) and connects to +Machine B using `to addr` / `port`: + +```sh +glorytun bind keyfile /etc/glorytun/key dev tun0 persist \ + to addr 203.0.113.1 port 5000 & +``` + +Wait for the interface, then configure it: + +```sh +ip addr add 10.99.0.2/30 dev tun0 +ip link set tun0 up +``` + +At this point the tunnel is up with a single default path. The next steps +configure source-based routing and register the additional interfaces as +separate paths. + +**Troubleshooting:** If `glorytun bind` prints `couldn't create tun device`, +check that the `tun` module is loaded and `/dev/net/tun` exists (see +[Prerequisites](#prerequisites)). + +## Step 4 — Source-Based Policy Routing on Machine A + +Linux routes all outbound packets based on the **destination** in the main +routing table. Without additional configuration, all four paths' packets exit +through whichever interface has the default route — even though Glorytun sets +the correct source IP via `IP_PKTINFO`. This means only one path works and the +rest are DEGRADED. + +The fix is **source-based policy routing**: a separate routing table per +interface, with a rule that selects the table based on the packet's source +address. + +Retrieve the current DHCP addresses and gateways: + +```sh +ETH1_IP=$(ip -4 -o addr show eth1 | awk '{print $4}' | cut -d/ -f1) +ETH2_IP=$(ip -4 -o addr show eth2 | awk '{print $4}' | cut -d/ -f1) +ETH3_IP=$(ip -4 -o addr show eth3 | awk '{print $4}' | cut -d/ -f1) +ETH4_IP=$(ip -4 -o addr show eth4 | awk '{print $4}' | cut -d/ -f1) + +ETH1_GW=$(ip -4 route show dev eth1 | awk '/default/ {print $3}') +ETH2_GW=$(ip -4 route show dev eth2 | awk '/default/ {print $3}') +ETH3_GW=$(ip -4 route show dev eth3 | awk '/default/ {print $3}') +ETH4_GW=$(ip -4 route show dev eth4 | awk '/default/ {print $3}') +``` + +Create a routing table and policy rule for each interface: + +```sh +ip route add default via $ETH1_GW dev eth1 table 101 +ip rule add from $ETH1_IP table 101 + +ip route add default via $ETH2_GW dev eth2 table 102 +ip rule add from $ETH2_IP table 102 + +ip route add default via $ETH3_GW dev eth3 table 103 +ip rule add from $ETH3_IP table 103 + +ip route add default via $ETH4_GW dev eth4 table 104 +ip rule add from $ETH4_IP table 104 +``` + +Disable strict reverse path filtering (which can drop packets arriving on +"unexpected" interfaces in a multi-homed setup): + +```sh +sysctl -w net.ipv4.conf.all.rp_filter=2 +sysctl -w net.ipv4.conf.default.rp_filter=2 +``` + +Value `2` is loose mode — it checks the source is reachable via *any* interface +rather than requiring the *same* interface the packet arrived on. + +Verify that each source address routes through the correct interface: + +```sh +ip route get 203.0.113.1 from $ETH1_IP # should show dev eth1 +ip route get 203.0.113.1 from $ETH2_IP # should show dev eth2 +ip route get 203.0.113.1 from $ETH3_IP # should show dev eth3 +ip route get 203.0.113.1 from $ETH4_IP # should show dev eth4 +``` + +To make these rules persistent across reboots, add them to your network +configuration (e.g. `/etc/network/interfaces` post-up scripts, NetworkManager +dispatcher, or netplan routing policies). + +## Step 5 — Add Paths for Each Interface on Machine A + +Each `path` command registers a local address (from a physical interface) as a +separate path. Use `addr` with the local IPv4 address and `path … set` for +configuration. + +The `$ETH1_IP` .. `$ETH4_IP` variables were already set in the previous step. + +Register each path, bring it up, and **set a rate**. A rate is required for data +to flow — without it, only control messages are exchanged and the path stays at +0 bytes/sec throughput. Use `rate fixed tx … rx …` with the link speed, or +`rate auto tx … rx …` for dynamic adjustment with an upper bound. + +**Heterogeneous links:** Glorytun assigns each packet to a path using weights +proportional to each path’s configured `tx` rate (see `mud_select_path` in the +codebase). If every path uses the same `tx`/`rx` (e.g. the default `12500k` from +the OpenRC/systemd templates), traffic is split **evenly** across paths — not +according to real link speed. A slow link then sees a disproportionate share of +traffic, loss rises above `losslimit`, and that path stops being used (`ok` goes +false). Throughput can collapse to something closer to **one surviving path** +than the sum of all links. For 100 / 10 / 40 / 40 Mbit paths, set **per-path** +`tx`/`rx` in proportion to each capacity (or use `auto` with a **per-path** +ceiling matched to that link). + +Here each link is assumed to be 100 Mbit (12,500 KB/s): + +```sh +glorytun path dev tun0 addr $ETH1_IP set up beat 5s losslimit 30% \ + rate fixed tx 12500k rx 12500k +glorytun path dev tun0 addr $ETH2_IP set up beat 5s losslimit 30% \ + rate fixed tx 12500k rx 12500k +glorytun path dev tun0 addr $ETH3_IP set up beat 5s losslimit 30% \ + rate fixed tx 12500k rx 12500k +glorytun path dev tun0 addr $ETH4_IP set up beat 5s losslimit 30% \ + rate fixed tx 12500k rx 12500k +``` + +The `rx` rate is communicated to the server via control messages, so the server +automatically learns the maximum rate it can send back on each path. You only +need to configure rates on the client side. + +## Step 6 — Verify + +Check the tunnel status: + +```sh +glorytun show dev tun0 +``` + +List all paths and their state: + +```sh +glorytun path dev tun0 +``` + +Check a specific path: + +```sh +glorytun path dev tun0 addr $ETH1_IP +``` + +## Optional — Rate Limiting + +If the WAN links have known bandwidth caps, apply fixed rate limits to prevent +congestion. For example, if eth1 and eth2 are 100 Mbit links and eth3 and eth4 +are 50 Mbit links: + +```sh +glorytun path dev tun0 addr $ETH1_IP set up rate fixed tx 12500k rx 12500k +glorytun path dev tun0 addr $ETH2_IP set up rate fixed tx 12500k rx 12500k +glorytun path dev tun0 addr $ETH3_IP set up rate fixed tx 6250k rx 6250k +glorytun path dev tun0 addr $ETH4_IP set up rate fixed tx 6250k rx 6250k +``` + +Rate values use [size suffixes](COMMAND-LINE.md#value-types). For bits, use e.g. +`tx 100Mbit rx 100Mbit`. + +Alternatively, use `rate auto` to let Glorytun detect available bandwidth up to +a ceiling: + +```sh +glorytun path dev tun0 addr $ETH1_IP set up rate auto +``` + +## Optional — Preferring Some Paths (failover-style) + +The CLI exposes `path … set up` and `path … set down`, plus `pref` (0–127) to +bias which path is preferred for scheduling. There is no separate `backup` +keyword on the command line; the daemon may still classify paths internally. + +To make two paths primary and two secondary, use **higher** `pref` on the +primaries and **lower** `pref` on the others (exact behaviour depends on the +running version — see `glorytun path help` and [COMMAND-LINE.md](COMMAND-LINE.md)): + +```sh +glorytun path dev tun0 addr $ETH1_IP set up pref 64 beat 5s losslimit 30% \ + rate fixed tx 12500k rx 12500k +glorytun path dev tun0 addr $ETH2_IP set up pref 64 beat 5s losslimit 30% \ + rate fixed tx 12500k rx 12500k +glorytun path dev tun0 addr $ETH3_IP set up pref 8 beat 5s losslimit 30% \ + rate fixed tx 12500k rx 12500k +glorytun path dev tun0 addr $ETH4_IP set up pref 8 beat 5s losslimit 30% \ + rate fixed tx 12500k rx 12500k +``` + +## Optional — Tuning Tunnel Timing + +Adjust key exchange timeout, time tolerance, and keepalive on a running tunnel: + +```sh +glorytun set dev tun0 kxtimeout 1h timetolerance 10s keepalive 25s +``` + +DSCP / traffic-class marking for tunnel packets is not exposed on the `glorytun +set` CLI in current versions; configure at the IP layer (e.g. `iptables`/`nft` +marks) if you need it. + +## Handling DHCP Renewals + +When a DHCP lease changes the IP on an interface, the old path becomes stale. +To update, bring the old path down and register the new address: + +```sh +OLD_IP="192.168.1.100" +NEW_IP="192.168.1.200" + +glorytun path dev tun0 addr $OLD_IP set down +glorytun path dev tun0 addr $NEW_IP set up beat 5s losslimit 30% \ + rate fixed tx 12500k rx 12500k +``` + +This can be automated with a DHCP hook script. + +## Summary of Commands + +| Step | Machine | Command | +|------|---------|---------| +| Key generation | Either | `glorytun keygen > /etc/glorytun/key` | +| Start tunnel | B (server) | `glorytun bind keyfile … dev tun0 persist from addr 203.0.113.1 port 5000` | +| Start tunnel | A (client) | `glorytun bind keyfile … dev tun0 persist to addr 203.0.113.1 port 5000` | +| Add path | A | `glorytun path dev tun0 addr set up beat 5s losslimit 30% rate fixed tx 12500k rx 12500k` | +| Check tunnel | Either | `glorytun show dev tun0` | +| Check paths | Either | `glorytun path dev tun0` | + +For the full syntax reference, see [COMMAND-LINE.md](COMMAND-LINE.md). diff --git a/Makefile b/Makefile deleted file mode 100644 index a30afbd..0000000 --- a/Makefile +++ /dev/null @@ -1,50 +0,0 @@ -NAME = glorytun -VERSION != ./version.sh -DIST = $(NAME)-$(VERSION) - -CC = cc -CFLAGS = -std=c99 -O2 -Wall -fstack-protector-strong -CPPFLAGS = -I.sodium/$(X)build/src/libsodium/include -LDFLAGS = -L.sodium/$(X)build/src/libsodium/.libs -LDLIBS = -lsodium -prefix = /usr/local -PREFIX = $(prefix) - -$(NAME): - $(X)$(CC) $(EXTRA) \ - $(CFLAGS) \ - $(CPPFLAGS) \ - $(LDFLAGS) \ - -DPACKAGE_NAME=\"$(NAME)\" \ - -DPACKAGE_VERSION=\"$(VERSION)\" \ - argz/argz.c \ - mud/aegis256/aegis256.c \ - mud/mud.c \ - src/argz.c \ - src/bench.c \ - src/bind.c \ - src/common.c \ - src/ctl.c \ - src/iface.c \ - src/keygen.c \ - src/list.c \ - src/main.c \ - src/path.c \ - src/set.c \ - src/show.c \ - src/tun.c \ - src/version.c \ - -o $(NAME) \ - $(LDLIBS) - -install: $(NAME) - mkdir -p $(DESTDIR)$(PREFIX)/bin - mv -f $(NAME) $(DESTDIR)$(PREFIX)/bin - -uninstall: - rm -f $(DESTDIR)$(PREFIX)/bin/$(NAME) - -clean: - rm -f $(NAME) - -.PHONY: $(NAME) install uninstall clean diff --git a/Makefile.am b/Makefile.am index 19abaa7..74b56e9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -39,8 +39,24 @@ EXTRA_DIST = \ VERSION \ autogen.sh \ meson.build \ + meson_options.txt \ mud/LICENSE \ mud/README.md \ sodium.sh \ - systemd \ + scripts/ci/build-static-binary.sh \ + scripts/client/openrc/README.md \ + scripts/client/openrc/glorytun-client \ + scripts/client/openrc/glorytun-client.conf \ + scripts/client/systemd/README.md \ + scripts/client/systemd/glorytun-client \ + scripts/client/systemd/glorytun-client-start \ + scripts/client/systemd/glorytun-client-teardown \ + scripts/client/systemd/glorytun-client.service \ + scripts/server/openrc/README.md \ + scripts/server/openrc/glorytun-server \ + scripts/server/openrc/glorytun-server.conf \ + scripts/server/systemd/README.md \ + scripts/server/systemd/glorytun-server \ + scripts/server/systemd/glorytun-server-start \ + scripts/server/systemd/glorytun-server@.service \ version.sh diff --git a/PERFORMANCE-BASE-SCHEDULING.md b/PERFORMANCE-BASE-SCHEDULING.md new file mode 100644 index 0000000..914c6ca --- /dev/null +++ b/PERFORMANCE-BASE-SCHEDULING.md @@ -0,0 +1,242 @@ +# Performance-based multipath scheduling + +This document describes how Glorytun schedules traffic across paths today, outlines **Plan B** in detail (separate scheduling weights from rate caps and window pacing), and summarizes **Options A–C** so future work can start from a known baseline. + +--- + +## 0. Repository layout and submodule scope (read this first) + +Glorytun vendors two Git submodules (see `.gitmodules`): + +| Submodule | Upstream | Role | +|-----------|----------|------| +| `mud/` | [angt/mud](https://github.com/angt/mud) | Encrypted UDP transport, path state, `mud_select_path`, window pacing, feedback | +| `argz/` | [angt/argz](https://github.com/angt/argz) | Declarative CLI parsing (`argz()` tables) | + +**Policy for this tree:** scheduling and path logic **live in `mud`**. Options A–C and Plan B all require **core changes to `mud`** (`mud/mud.h`, `mud/mud.c`). Those changes belong **upstream** (or in a dedicated fork of `angt/mud`), with this repo **bumping the submodule pointer** — not maintaining a long‑lived, glorytun‑only fork of `mud/` inside this workspace without upstream alignment. + +**`argz/`** should only change if the **parser library** itself needs new primitives. New `glorytun path` keywords are normally added in **`src/path.c`** using existing `argz` APIs; that is **not** a submodule edit. + +**Glorytun-only touch points after mud exposes new APIs:** `src/bind.c` (control socket handling), `src/path.c` (CLI / status tables), `src/ctl.h` (if `ctl_msg` layout must grow — keep ABI in mind), plus docs and scripts. + +--- + +## 1. Current behaviour (baseline) + +*Verified against the scheduling loop in `mud/mud.c`: `mud_select_path`, `mud_update`, `mud_update_window`, `mud_recv_msg`, `mud_send`.* + +### 1.1 Path selection + +Outbound packets are assigned to a path in `mud_select_path()` (`mud/mud.c`). A 16-bit value read from the tail of the encrypted payload (`mud_send`) acts as `cursor`; the path is chosen by walking a **weighted** list: + +- `k = (cursor * mud->rate) >> 16` +- Only paths with `path->ok == 1` are considered. +- Each such path contributes a bucket of size `path->tx.rate`; `k` walks those buckets until it falls inside one path’s range. + +`mud->rate` is set at the **end** of `mud_update()` to the **sum** of `path->tx.rate` over paths that passed `mud_path_is_ok()` for that tick (see §1.2). + +### 1.2 Overloaded meaning of `path->tx.rate` + +The same field is used for several concerns: + +| Concern | Role of `path->tx.rate` | +|--------|-------------------------| +| Scheduler | Bucket size in `mud_select_path` (with `mud->rate` as total weight scale) | +| Send window | `mud_update()` sums `path->tx.rate` over OK paths; that sum refills `mud->window` | +| Auto mode | `mud_update_window()` updates `path->tx.rate` from feedback when `!fixed_rate`, then clamps to `tx_max_rate` | +| Peer interaction | In `mud_recv_msg`, when `tx_time == 0`, `path->tx.rate` can be set from the peer’s advertised `max_rate` | +| Observability | Shown as TX rate in `glorytun path` status (`src/path.c`) | + +Because of this coupling, “reweight by measured throughput” without a separate field tends to fight **pacing**, **auto-rate logic**, and **peer signalling**. + +### 1.3 Caps and signalling + +- `path->conf.tx_max_rate` / `path->conf.rx_max_rate` hold configured ceilings; the wire format advertises receive-side limits to the peer (`mud_send_msg` / `mud_recv_msg`). +- `fixed_rate` gates whether `mud_update_window` mutates `path->tx.rate`. + +### 1.4 CLI `pref` (Glorytun front-end) + +[`COMMAND-LINE.md`](COMMAND-LINE.md) documents `pref N` on `glorytun path … set` as a path preference that **biases scheduling**. Wiring for `pref` is present in `src/path.c` (`CTL_PATH_CONF`). Whether and how `pref` maps into the mud core depends on the **mud** revision linked by the submodule; Plan B’s `sched_weight` may subsume or combine with `pref` — decide explicitly when implementing. + +--- + +## 2. Plan B — Detailed design (recommended direction) + +**Goal:** Drive **relative** traffic share from **measured performance** (e.g. decaying average goodput, optionally loss/RTT), while keeping **absolute** limits and peer semantics on explicit cap fields and a well-defined aggregate pacing rule. + +**Where it is implemented:** Primarily **upstream `mud`**; Glorytun updates **submodule + `src/`** as in §0. + +### 2.1 Principles + +1. **Scheduling weight** is for *proportions only* (which path gets the next packet among OK paths). +2. **Aggregate send budget** (window refill) must not silently shrink because weights were lowered by congestion; it should follow an explicit **tunnel or per-path cap policy**. +3. **Peer-visible limits** remain `tx_max_rate` / `rx_max_rate` (or successors); local weights are not confused with advertised maxima unless deliberately mapped. +4. **Exploration:** paths with little recent traffic keep a **minimum weight** so they are not starved and can be measured again. +5. **Bidirectional awareness:** the peer runs its own scheduler for return traffic; the same algorithm (or compatible parameters) should be used on both ends if both are multipath-capable. + +### 2.2 Data model changes + +**In upstream `struct mud_path` (`mud/mud.h`):** + +| Field | Purpose | +|-------|---------| +| `sched_weight` | `uint64_t` — used only by `mud_select_path` (and status if desired). Relative weights; implementation may normalize internally. | +| `sched_weight_ema` (optional) | Internal smoothed estimate if you separate “raw EMA” from “weight after floor/cap” | +| Keep `conf.tx_max_rate` / `conf.rx_max_rate` | Unchanged semantics: ceilings and advertisement | +| `path->tx.rate` | Either: (a) retain for auto-mode estimate + display only, or (b) gradually align naming in docs to “estimated goodput” vs `sched_weight` | + +**In upstream `struct mud` (if needed):** + +- Optional `uint64_t sched_total_cap` or reuse `sum(tx_max_rate)` for window math — see §2.4. + +**In Glorytun (`src/ctl.h`, `src/path.c`, [`COMMAND-LINE.md`](COMMAND-LINE.md)):** + +- Extend `ctl_msg` / path status only if new fields must cross the UNIX socket (coordinate with `struct mud_path` size and versioning). +- Current user-facing syntax is documented under **`glorytun path`** in [`COMMAND-LINE.md`](COMMAND-LINE.md): `dev`, `addr`, `to`, `show`, `set` with `up`/`down`, `rate fixed|auto`, `tx`/`rx`, `beat`, `pref`, `losslimit`. +- Optional future work: expose `sched_weight` in `show stat` (or new `show`), or add keywords — preferably **without** editing the `argz` submodule unless a new parser feature is required. + +### 2.3 Algorithm: updating `sched_weight` + +**Signal sources (prefer data plane):** + +- Byte deltas on the path: counters already consumed in `mud_update_window()` when feedback is valid (`path->msg.set` and MTU state allow). +- Auxiliary signals: `path->tx.loss`, `path->rtt` — can scale weight down for high loss or RTT variance if desired. + +**Suggested core loop (conceptual):** + +1. On each valid feedback interval (same gates as today for `mud_update_window`, or a slightly relaxed cadence): + - Compute instantaneous goodput estimate `g` (bytes/sec) for that path over the interval. + - Update EMA: `ema = alpha * g + (1 - alpha) * ema` with fixed-point or integer-friendly shifts (e.g. `alpha = 1/8`). +2. Map EMA to a **provisional weight** `w = f(ema, loss, rtt)` — start with `w = max(ema, w_min)` where `w_min` is a small floor for exploration. +3. Optional: **cap** per-path weight at `tx_max_rate` or a fraction of it so weights stay comparable to “capacity-ish” numbers. +4. Normalize for numerics if needed: selection only cares about ratios; avoid overflow in `k * mud->rate >> 16` (document scaling or use wider intermediates). + +**Cold start / idle paths:** + +- If no feedback since `T` ms, decay `sched_weight` toward `w_min` rather than to zero, or refresh from configured `tx_max_rate` * small fraction so the path is occasionally tried. + +**Interaction with `fixed_rate` and `pref`:** + +- **Fixed mode:** either `sched_weight` tracks configured static share (e.g. proportional to `tx_max_rate`) and is not EMA-updated, or EMA is disabled when the operator requests a fixed split. +- **`pref`:** define whether it adds a bias on top of `sched_weight`, or is folded into initial `sched_weight` — avoid double-counting. + +### 2.4 Send window (`mud->window`) and aggregate rate + +Today: `mud_update()` accumulates `rate += path->tx.rate` for OK paths, assigns `mud->rate = rate`, and refills `mud->window` with `rate * elapsed` when `mud->window < 1500`. + +**Plan B change:** + +- Compute `rate_for_window` independently, for example: + - `rate_for_window = sum(path->conf.tx_max_rate)` over OK paths (excluding backup-only as today), **or** + - `min(sum(tx_max_rate), measured_aggregate_ema)` if tunnel-level measurement is added later, **or** + - a single `mud->tunnel_cap` from config. + +**Invariant:** the window should allow the tunnel to use the **intended aggregate throughput** when the network allows it; weights only redistribute among paths. + +**Edge case:** very large “unlimited” caps — keep a sane default for window growth or align with existing auto behaviour. + +### 2.5 `mud_select_path` changes (upstream) + +- Use `path->sched_weight` (or `mud_path_sched_weight(path)`) instead of `path->tx.rate` for bucket sizes. +- Recompute the total used in `k = (cursor * total) >> 16` as the **sum of scheduling weights** for OK paths — keep this distinct from `rate_for_window` unless intentionally unified and documented. + +### 2.6 Peer control messages (`mud_recv_msg`, `mud_send_msg`) + +- When handling control packets (`tx_time == 0`), avoid assigning `path->tx.rate = max_rate` in a way that destroys `sched_weight`. Prefer updating `conf.tx_max_rate` / peer sync fields only, and keep scheduling state in `sched_weight` (or explicitly resync from caps when appropriate). + +Audit every assignment to `path->tx.rate` after this split. + +### 2.7 `mud_set_state` / `mud_set_path` and `glorytun path` + +- Initial registration: initialize `sched_weight` from operator `tx` (compatibility) or from `tx_max_rate` until the first EMA sample. +- When the operator changes `rate fixed|auto tx … rx …`, update caps; optionally reset or blend `sched_weight` for predictable behaviour. + +Exact entry symbol names (`mud_set_state` vs `mud_set_path`) depend on the **mud** revision; align with the linked submodule API. + +### 2.8 Status and documentation + +- Path status printing: `src/path.c` (`gt_path_status` / related helpers) — add a column or line for `sched_weight` if exposed. +- Update [`COMMAND-LINE.md`](COMMAND-LINE.md), [`MULTIPATH-EXAMPLE.md`](MULTIPATH-EXAMPLE.md), and client `scripts/**/README.md` when new knobs land. + +### 2.9 Testing strategy + +1. **Unit-style / deterministic:** fixed weights and caps; assert selection distribution approximates weight ratios over many samples (upstream mud tests or harness). +2. **Simulation:** four paths with different simulated goodput; verify weights converge and minimum path still receives traffic. +3. **Regression:** single-path and equal-weight multipath throughput within tolerance. +4. **Bidirectional:** iperf with asymmetric caps; return path does not collapse because peer weights are zero. + +### 2.10 Risks and mitigations + +| Risk | Mitigation | +|------|------------| +| Oscillating weights | Stronger EMA, caps on change per beat, optional loss penalty | +| Window too large vs reality | Tie `rate_for_window` to sum of caps or explicit tunnel cap | +| Starvation | `w_min` exploration floor | +| ABI / ctl struct size | Version ctl messages or extend carefully; pad reserved fields if needed | +| Confusion between weight and Mbit/s | Clear naming and CLI labels | +| Submodule drift | Implement in upstream mud; bump submodule; run integration tests in Glorytun | + +### 2.11 Implementation order (suggested) + +1. **Upstream mud:** add `sched_weight` + initialization; make `mud_select_path` use it while the window still uses `sum(tx.rate)` — behaviour matches today if `sched_weight := tx.rate`. +2. **Upstream mud:** switch window to `rate_for_window` from `sum(tx_max_rate)` (or chosen rule); benchmark. +3. **Upstream mud:** add EMA updater alongside `mud_update_window`; gate behind a mode or extend `auto`. +4. **Upstream mud:** detangle `mud_recv_msg` overwrites from scheduling state. +5. **Glorytun:** bump `mud` submodule; adjust `src/path.c` / `src/ctl.h` / `src/bind.c` if status or `ctl_msg` changes; update docs. + +A **step-by-step todo list with files per task** is maintained in [PLAN-B-TODO.md](PLAN-B-TODO.md). + +--- + +## 3. Summary of options (for future pivots) + +### Option A — Evolve current “auto” mode in place + +**Idea:** Keep a single `path->tx.rate` but improve `mud_update_window()` to use a **decaying average** of measured goodput (and optionally loss/RTT), still clamped by `tx_max_rate`. + +**Pros:** Smaller diff inside mud; fewer new fields; builds on existing feedback. + +**Cons:** Same field still mixes scheduler weight, window sum, auto estimate, and peer overwrites — **must** carefully fix `mud_recv_msg` and window math so they don’t erase or mis-scale the EMA. + +**Start from:** `mud/mud.c` — `mud_update_window`, `mud_update`, `mud_recv_msg`; clarify window refill vs sum of weights. **Land changes upstream in `mud`**, then bump the submodule here. + +--- + +### Option B — Separate scheduling weight from caps and pacing (this document’s main plan) + +**Idea:** Introduce `sched_weight` (or equivalent) for `mud_select_path` only; keep `tx_max_rate` / `rx_max_rate` for caps and signalling; drive `mud->window` from an explicit aggregate rule (e.g. sum of caps). + +**Pros:** Clean separation; performance-based mixing without breaking pacing or peer semantics; clearest long-term model. + +**Cons:** More structural change in **mud** (struct, all `tx.rate` touch points); Glorytun must update `src/` for any new status/ctl fields. + +**Start from:** §2 of this file; **upstream** `mud/mud.h`, `mud/mud.c`; then Glorytun `src/` + docs; follow §2.11. + +--- + +### Option C — Heartbeat-only rate adjustments + +**Idea:** On each control **beat**, recompute a decaying average and push updates equivalent to path configuration (or mutate rates) from heartbeat timing alone. + +**Pros:** Simple mental model; slow, coarse control. + +**Cons:** Heartbeats are **sparse** vs data; noisy unless aggregated over many intervals; easy to conflict with peer control messages and auto mode; **inferior signal** to data-plane feedback already available in `mud_update_window`. + +**Start from:** `mud_send_msg` / `mud_recv_msg` beat cadence and path timing in **upstream mud**. Prefer **not** as primary design unless operational constraints require rare updates only. + +--- + +## 4. File index + +| Area | Location | Notes | +|------|----------|--------| +| Core scheduling, window, feedback | **`mud/mud.h`**, **`mud/mud.c`** | Submodule; **upstream changes** | +| `argz` parser core | **`argz/`** | Submodule; avoid edits unless parser primitives are required | +| CLI, status tables | **`src/path.c`** | Glorytun; uses `argz` **API** from submodule | +| Control messages | **`src/ctl.h`**, **`src/bind.c`** | Glorytun; must match `mud_path` / `ctl_msg` layout | +| User docs | **`COMMAND-LINE.md`**, **`MULTIPATH-EXAMPLE.md`** | Glorytun | +| Init scripts | **`scripts/client/*`** | Optional defaults for caps/rates | + +--- + +*Last updated: revalidated against post-merge tree; core scheduling references in §1 are `mud_select_path` / `mud_update` / `mud_update_window` / `mud_recv_msg` in `mud/mud.c`. Line numbers drift with submodule commits — re-read those functions when implementing.* diff --git a/PLAN-B-TODO.md b/PLAN-B-TODO.md new file mode 100644 index 0000000..707cb6e --- /dev/null +++ b/PLAN-B-TODO.md @@ -0,0 +1,113 @@ +# Plan B — implementation checklist + +Step-by-step tasks derived from [PERFORMANCE-BASE-SCHEDULING.md](PERFORMANCE-BASE-SCHEDULING.md) §2 (Plan B) and §2.11. **Upstream work** targets the [`mud`](https://github.com/angt/mud) submodule; **Glorytun** changes are under `src/` and docs after the submodule exposes new fields/APIs. + +--- + +## Phase 0 — ABI and control surface (optional but recommended before coding) + +| Step | Task | Files (typical) | +|------|------|-----------------| +| 0.1 | Decide whether `sched_weight` appears in `ctl_msg` / `glorytun path show` and whether `struct mud_path` grows (padding vs versioned ctl). | `mud/mud.h`, `src/ctl.h`, design note in [PERFORMANCE-BASE-SCHEDULING.md](PERFORMANCE-BASE-SCHEDULING.md) or this file | + +--- + +## Phase 1 — `sched_weight` + selection only (upstream `mud`) + +**Goal:** `mud_select_path` uses `sched_weight`; keep **window refill** on `sum(path->tx.rate)` unchanged. If `sched_weight` is initialized equal to `tx.rate` everywhere, behaviour matches pre-change. + +| Step | Task | Files | +|------|------|--------| +| 1.1 | Add `sched_weight` (and optional `sched_weight_ema` if you split raw vs smoothed) to `struct mud_path`. | `mud/mud.h` | +| 1.2 | Initialize `sched_weight` wherever a path is zeroed or created (`memset`, new path, reset). Set `sched_weight = path->tx.rate` (or `tx_max_rate`) at creation so weights match current semantics. | `mud/mud.c` (e.g. path allocation, `mud_reset_path`, `mud_get_path` / path init) | +| 1.3 | On path config apply (`mud_set_state` / `mud_set_path` or equivalent in your mud revision), set `sched_weight` from operator `tx` / caps for backward compatibility. | `mud/mud.c` | +| 1.4 | Replace bucket size in `mud_select_path`: use `sched_weight` instead of `path->tx.rate`. Recompute the total passed into `k = (cursor * total) >> 16` as **sum of `sched_weight`** over OK paths (consider renaming `mud->rate` to `mud->sched_weight_sum` in mud for clarity — same file). | `mud/mud.c` | +| 1.5 | Grep for every read/write of scheduling totals; ensure `mud_update` still uses **`tx.rate` only for window** until Phase 2. | `mud/mud.c` | + +**Validation:** unit or scripted sampling of `mud_select_path` distribution; throughput regression vs main branch. + +--- + +## Phase 2 — Decouple window pacing from schedule weights (upstream `mud`) + +**Goal:** `mud->window` refills from `rate_for_window` (e.g. `sum(tx_max_rate)` over OK paths), not `sum(tx.rate)`. + +| Step | Task | Files | +|------|------|--------| +| 2.1 | Introduce `rate_for_window` (local or `mud` field): implement chosen policy (`sum(conf.tx_max_rate)`, backup exclusion rules unchanged from today). | `mud/mud.c` (`mud_update`) | +| 2.2 | Replace `rate += path->tx.rate` used **only** for window refill with `rate_for_window` aggregation. Keep a separate sum for **`sched_weight`** if `mud->rate` is split into two concepts. | `mud/mud.c` | +| 2.3 | Handle edge cases: zero caps, “unlimited” large caps, no OK paths (`!rate` branch). | `mud/mud.c` | +| 2.4 | Benchmark single- and multi-path throughput; tune caps if window is too aggressive or too conservative. | `mud/mud.c`; optional `mud/test.c` | + +--- + +## Phase 3 — EMA / performance-based `sched_weight` (upstream `mud`) + +**Goal:** Update `sched_weight` from measured goodput (EWMA), with floors/ceilings; gate behind extended `auto` or a new flag if needed. + +| Step | Task | Files | +|------|------|--------| +| 3.1 | Add EMA state (on path or separate field) and helpers: update from `mud_update_window` feedback intervals or equivalent. | `mud/mud.h`, `mud/mud.c` | +| 3.2 | Implement `w_min` exploration floor and optional caps vs `tx_max_rate`. | `mud/mud.c` | +| 3.3 | Define interaction with `fixed_rate`: when fixed, do not EMA-update `sched_weight` (or copy from static policy). | `mud/mud.c` (`mud_update_window`, config apply) | +| 3.4 | Define interaction with **`pref`** (if present in your mud revision): bias vs EMA — document and implement in one place. | `mud/mud.c`, [COMMAND-LINE.md](COMMAND-LINE.md) when behaviour is final | + +--- + +## Phase 4 — Peer control messages (upstream `mud`) + +**Goal:** Stop clobbering scheduling state when applying peer `max_rate` / control fields. + +| Step | Task | Files | +|------|------|--------| +| 4.1 | Audit `mud_recv_msg` (`tx_time == 0`): update `conf.tx_max_rate` / peer sync without blindly setting `path->tx.rate = max_rate` if that destroys `sched_weight` or EMA. | `mud/mud.c` | +| 4.2 | Audit `mud_send_msg` for consistency of advertised limits vs local `sched_weight`. | `mud/mud.c` | +| 4.3 | Grep `path->tx.rate` in `mud/mud.c`; classify each site as **estimate**, **signalling**, or **obsolete** after split. | `mud/mud.c` | + +--- + +## Phase 5 — Tests (upstream `mud`) + +| Step | Task | Files | +|------|------|--------| +| 5.1 | Distribution / property tests for weighted selection (ratios vs weights). | `mud/test.c` (or new test harness) | +| 5.2 | Multi-path simulation or integration tests for convergence + minimum exploration. | `mud/test.c` | + +--- + +## Phase 6 — Glorytun tree (submodule + `src/` + docs) + +**Goal:** Consume new mud; expose status/CLI if new fields exist. + +| Step | Task | Files | +|------|------|--------| +| 6.1 | Bump **`mud` submodule** to the commit that contains Phases 1–5. | `.gitmodules` (only if URL changes), `mud/` (gitlink), release notes / changelog if you maintain one | +| 6.2 | If `struct mud_path` or ctl layout changed: update **`ctl_msg`** and any `memcpy` of `path`. | `src/ctl.h`, `src/bind.c` | +| 6.3 | Path status / `glorytun path show`: add columns or rows for `sched_weight` (and document units). | `src/path.c` | +| 6.4 | New CLI keywords **only if** needed: extend `argz` table in `path` command (prefer **no** `argz/` submodule edit — use existing `argz()` features). | `src/path.c` | +| 6.5 | User documentation: path behaviour, multipath examples, tuning. | [COMMAND-LINE.md](COMMAND-LINE.md), [MULTIPATH-EXAMPLE.md](MULTIPATH-EXAMPLE.md) | +| 6.6 | Init script hints (optional): document caps vs auto weights for operators. | `scripts/client/openrc/README.md`, `scripts/client/systemd/README.md` | + +--- + +## Phase 7 — `argz` submodule (only if unavoidable) + +| Step | Task | Files | +|------|------|--------| +| 7.1 | New parser primitives (rare): land in [angt/argz](https://github.com/angt/argz), bump `argz/` submodule, then use from `src/path.c`. | `argz/` (upstream), `argz/` (submodule pointer), `src/path.c` | + +--- + +## Quick reference — files touched by layer + +| Layer | Files | +|-------|--------| +| Core | `mud/mud.h`, `mud/mud.c` | +| Optional tests | `mud/test.c` | +| Glorytun control / CLI | `src/ctl.h`, `src/bind.c`, `src/path.c` | +| Docs | `PERFORMANCE-BASE-SCHEDULING.md`, `COMMAND-LINE.md`, `MULTIPATH-EXAMPLE.md`, `scripts/client/**/README.md` | +| Submodules | `mud/` (required), `argz/` (only if Phase 7) | + +--- + +*This checklist is a guide; exact symbol names (`mud_set_state` vs `mud_set_path`) and struct layouts depend on the `mud` revision you ship.* diff --git a/argz b/argz index eb8b122..9aeb9e0 160000 --- a/argz +++ b/argz @@ -1 +1 @@ -Subproject commit eb8b122247bd48b11984ae4f8755ae6a4059e6c2 +Subproject commit 9aeb9e035c80236dfc86c2b3bd124044419d9531 diff --git a/configure.ac b/configure.ac index 2e25e6c..7f44eac 100644 --- a/configure.ac +++ b/configure.ac @@ -11,7 +11,7 @@ AM_INIT_AUTOMAKE([1.12 -Wall -Werror foreign tar-ustar subdir-objects]) AM_DEP_TRACK AM_SILENT_RULES([yes]) AM_PROG_CC_C_O -AC_PROG_CC_C99 +AC_PROG_CC AC_USE_SYSTEM_EXTENSIONS AC_SEARCH_LIBS([socket], [socket]) AC_CHECK_LIB([rt], [clock_gettime]) diff --git a/meson.build b/meson.build index 31154bc..184f890 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('glorytun', 'c', - version: run_command('./version.sh').stdout(), + version: run_command('./version.sh', check: false).stdout().strip(), license: 'BSD-3-Clause', default_options : [ 'buildtype=debugoptimized', @@ -7,18 +7,17 @@ project('glorytun', 'c', ] ) -cc = meson.get_compiler('c') - -prefix = get_option('prefix') -bindir = join_paths(prefix, get_option('bindir')) - -conf_data = configuration_data() -conf_data.set('prefix', prefix) -conf_data.set('bindir', bindir) - add_global_arguments('-DPACKAGE_VERSION="'+meson.project_version()+'"', language : 'c') add_global_arguments('-DPACKAGE_NAME="'+meson.project_name()+'"', language : 'c') +static_link = get_option('static') +libsodium_dep = dependency('libsodium', version : '>=1.0.4', static: static_link) + +exe_link_args = [] +if static_link and host_machine.system() == 'linux' + exe_link_args += ['-static'] +endif + executable('glorytun', install: true, sources: [ 'argz/argz.c', @@ -39,27 +38,9 @@ executable('glorytun', install: true, 'src/tun.c', 'src/version.c', ], - dependencies: [ - dependency('libsodium', version : '>=1.0.4'), - ] + dependencies: [libsodium_dep], + link_args: exe_link_args, ) -systemd = dependency('systemd', required: false) - -if systemd.found() - systemdutildir = systemd.get_pkgconfig_variable('systemdutildir') - configure_file( - input: 'systemd/glorytun@.service.in', - output: 'glorytun@.service', - configuration: conf_data, - install_dir: join_paths(systemdutildir, 'system') - ) - install_data('systemd/glorytun.network', - install_dir: join_paths(systemdutildir, 'network')) - install_data('systemd/glorytun-client.network', - install_dir: join_paths(systemdutildir, 'network')) - install_data('systemd/glorytun-run', - install_dir: bindir) - install_data('systemd/glorytun-setup', - install_dir: bindir) -endif +# Systemd units and network snippets live under scripts/{client,server}/systemd/ +# and are installed manually or by packaging; see those READMEs. diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..b2c8e1f --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,2 @@ +option('static', type: 'boolean', value: false, + description: 'Link libsodium statically; on Linux also pass -static for a fully static executable (musl or glibc).') diff --git a/mud b/mud index e991dc9..400f626 160000 --- a/mud +++ b/mud @@ -1 +1 @@ -Subproject commit e991dc9b4510367c0d03540dabdc84188ab9d009 +Subproject commit 400f6263be8395ece9c3b89bd1c673a0e8a89d09 diff --git a/scripts/client/openrc/README.md b/scripts/client/openrc/README.md new file mode 100644 index 0000000..e57be21 --- /dev/null +++ b/scripts/client/openrc/README.md @@ -0,0 +1,133 @@ +# Glorytun Client — OpenRC + +Service scripts for running a Glorytun multipath client tunnel under OpenRC. + +## Files + +| File | Install to | Description | +|------|-----------|-------------| +| `glorytun-client` | `/etc/init.d/glorytun-client` | Init script | +| `glorytun-client.conf` | `/etc/conf.d/glorytun-client` | Configuration file | + +## Installation + +```sh +cp glorytun-client /etc/init.d/glorytun-client +chmod 755 /etc/init.d/glorytun-client + +cp glorytun-client.conf /etc/conf.d/glorytun-client +``` + +## Configuration + +Edit `/etc/conf.d/glorytun-client`. The main settings are: + +### Tunnel + +| Variable | Default | Description | +|----------|---------|-------------| +| `TUN_DEV` | `tun0` | TUN device name | +| `SERVER_ADDR` | — | Remote server IP address | +| `SERVER_PORT` | — | Remote server UDP port | +| `KEYFILE` | — | Path to the shared key file | +| `TUN_LOCAL_IP` | — | Local tunnel IP in CIDR notation (e.g. `10.99.0.2/30`). Leave empty to configure addressing elsewhere. | + +### Interfaces + +`INTERFACES` is a space-separated list of `INTERFACE:GATEWAY` pairs. Each +interface becomes a separate path through the tunnel. The gateway is the +next-hop for that interface — needed because the default route may point +elsewhere and source-based policy routing must know where to send each +interface's traffic. + +```sh +INTERFACES="eth1:192.168.1.254 eth2:10.0.0.1 eth3:172.16.0.1 eth4:10.1.0.1" +``` + +### Path settings + +| Variable | Default | Description | +|----------|---------|-------------| +| `RATE_MODE` | `fixed` | Rate mode: `fixed` or `auto` | +| `RATE_TX` | `12500k` (`fixed`) / large cap (`auto`) | Transmit limit per path (bytes/sec with suffix). With `auto`, this is a **ceiling** for probing, not a fixed bitrate. If unset under `auto`, a large default cap is used (see `RATE_AUTO_*_MAX`). | +| `RATE_RX` | (same as `RATE_TX`) | Receive limit advertised to the peer; same semantics as `RATE_TX`. | +| `RATE_AUTO_TX_MAX` | `12500000k` | When `RATE_MODE=auto` and `RATE_TX` is unset/empty, use this ceiling. | +| `RATE_AUTO_RX_MAX` | `12500000k` | When `RATE_MODE=auto` and `RATE_RX` is unset/empty, use this ceiling. | +| `BEAT` | `5s` | Keepalive beat interval | +| `LOSSLIMIT` | `30%` | Loss percentage threshold for path degradation | +| `HEARTBEAT_MISS` | *(unset)* | Optional. Passed as `miss N` on `glorytun path … set` (max beats without reply before degraded). Unset or empty uses the MUD default; `0` requests the default explicitly. Range 0–255 when set. | +| `PATH_STATE` | `up` | Passed to `glorytun path … set` (`up` or `down`) | + +The init script always passes explicit `tx`/`rx` to `glorytun path`. Commenting out `RATE_TX`/`RATE_RX` in the config does **not** omit them — `${VAR:-default}` still applies. Under `fixed`, the default stays ~100 Mbit; under `auto`, the default ceiling is much higher so variable links (e.g. LTE) are not capped at ~100 Mbit unless you set a lower `RATE_TX`/`RATE_RX`. + +### Routing + +| Variable | Default | Description | +|----------|---------|-------------| +| `RT_TABLE_START` | `101` | First routing table number (incremented per interface) | +| `RP_FILTER` | `2` | Reverse path filter mode (2 = loose, recommended) | + +### Glorytun binary + +| Variable | Default | Description | +|----------|---------|-------------| +| `GLORYTUN_BIN` | `/usr/local/bin/glorytun` | Path to glorytun | +| `GLORYTUN_OPTS` | `persist` | Extra options for `glorytun bind` | + +## Key generation + +```sh +mkdir -p /etc/glorytun +glorytun keygen > /etc/glorytun/key +chmod 600 /etc/glorytun/key +``` + +Copy the same key to the server via a secure channel. + +## Usage + +```sh +# Start the tunnel, policy routes, and all paths +rc-service glorytun-client start + +# Stop everything and clean up routes +rc-service glorytun-client stop + +# Show tunnel and path status +rc-service glorytun-client status + +# Enable at boot +rc-update add glorytun-client default + +# Disable at boot +rc-update del glorytun-client default +``` + +## What the init script does + +On **start**, in order: + +1. Starts `glorytun bind to addr port ...` in the background. +2. Waits for the TUN device to appear (up to 5 seconds). +3. Assigns `TUN_LOCAL_IP` to the TUN device and brings it up. +4. Sets `rp_filter` to loose mode on all interfaces. +5. For each interface in `INTERFACES`: + - Resolves the interface's current IPv4 address. + - Creates a source-based policy route through the specified gateway. + - Registers the path with glorytun (state, rate, beat, loss limit). + +On **stop**, in reverse: + +1. Brings each path down via `glorytun path dev … addr … set down`. +2. Removes the source-based policy routes and flushes routing tables. +3. Stops the glorytun process. + +## Notes + +- Interface IP addresses are resolved at startup. If an interface has no IPv4 + address (e.g. DHCP hasn't completed), that path is skipped with a warning. +- The `status` command shows both tunnel status (`glorytun show`) and path + status (`glorytun path`). +- For DHCP environments where addresses change, consider a DHCP hook script + that brings the old path down and registers the new address (see + `MULTIPATH-EXAMPLE.md`). diff --git a/scripts/client/openrc/glorytun-client b/scripts/client/openrc/glorytun-client new file mode 100644 index 0000000..185da33 --- /dev/null +++ b/scripts/client/openrc/glorytun-client @@ -0,0 +1,196 @@ +#!/sbin/openrc-run +# glorytun-client - OpenRC init script for Glorytun multipath client +# Install to: /etc/init.d/glorytun-client +# Config: /etc/conf.d/glorytun-client + +description="Glorytun encrypted UDP tunnel client" + +GLORYTUN_BIN="${GLORYTUN_BIN:-/usr/local/bin/glorytun}" +GLORYTUN_OPTS="${GLORYTUN_OPTS:-persist}" +PIDDIR="/run/glorytun" + +depend() { + need net + after firewall +} + +get_iface_ip() { + ip -4 -o addr show dev "$1" 2>/dev/null \ + | awk '{print $4}' | cut -d/ -f1 | head -n1 +} + +wait_for_dev() { + local dev="$1" tries=0 + while [ $tries -lt 50 ]; do + ip link show "$dev" >/dev/null 2>&1 && return 0 + tries=$((tries + 1)) + sleep 0.1 + done + return 1 +} + +setup_policy_route() { + local iface="$1" gateway="$2" table="$3" src_ip="$4" + + ip route replace default via "$gateway" dev "$iface" table "$table" 2>/dev/null + ip rule add from "$src_ip" table "$table" 2>/dev/null +} + +teardown_policy_route() { + local table="$1" src_ip="$2" + + ip rule del from "$src_ip" table "$table" 2>/dev/null + ip route flush table "$table" 2>/dev/null +} + +register_path() { + local src_ip="$1" mode eff_tx eff_rx + + # ${VAR:-word} applies when VAR is unset *or empty*, so commenting + # RATE_TX/RATE_RX in the config still hits the defaults here — you are + # never "disabling" the rate arguments from the CLI. + # + # In auto mode, tx/rx are ceilings (see mud_update_window); the old + # single default (~100 Mbit) made mobile links look "stuck". When RATE_TX + # / RATE_RX are unset, use a large cap (override with RATE_AUTO_*_MAX). + mode="${RATE_MODE:-fixed}" + if [ "$mode" = "auto" ]; then + eff_tx="${RATE_TX:-${RATE_AUTO_TX_MAX:-12500000k}}" + eff_rx="${RATE_RX:-${RATE_AUTO_RX_MAX:-12500000k}}" + else + eff_tx="${RATE_TX:-12500k}" + eff_rx="${RATE_RX:-12500k}" + fi + + if [ -n "${HEARTBEAT_MISS+x}" ] && [ -n "$HEARTBEAT_MISS" ]; then + miss_arg="miss $HEARTBEAT_MISS" + else + miss_arg= + fi + + "$GLORYTUN_BIN" path dev "$TUN_DEV" addr "$src_ip" set "${PATH_STATE:-up}" \ + beat "${BEAT:-5s}" \ + losslimit "${LOSSLIMIT:-30%}" \ + ${miss_arg:+$miss_arg }\ + rate "$mode" tx "$eff_tx" rx "$eff_rx" +} + +deregister_path() { + local src_ip="$1" + "$GLORYTUN_BIN" path dev "$TUN_DEV" addr "$src_ip" set down 2>/dev/null +} + +start() { + if [ -z "$INTERFACES" ]; then + eerror "No interfaces configured in /etc/conf.d/glorytun-client" + return 1 + fi + + if [ -z "$SERVER_ADDR" ] || [ -z "$SERVER_PORT" ]; then + eerror "SERVER_ADDR and SERVER_PORT must be set in /etc/conf.d/glorytun-client" + return 1 + fi + + if [ ! -f "$KEYFILE" ]; then + eerror "Key file $KEYFILE not found" + return 1 + fi + + checkpath --directory --mode 0755 "$PIDDIR" + + # Start the tunnel process + ebegin "Starting tunnel ${TUN_DEV} to ${SERVER_ADDR}:${SERVER_PORT}" + start-stop-daemon --start --background \ + --make-pidfile --pidfile "$PIDDIR/${TUN_DEV}.pid" \ + --exec "$GLORYTUN_BIN" \ + bind to addr "$SERVER_ADDR" port "$SERVER_PORT" dev "$TUN_DEV" \ + keyfile "$KEYFILE" $GLORYTUN_OPTS + eend $? || return 1 + + # Wait for the TUN device to appear + ebegin "Waiting for ${TUN_DEV}" + wait_for_dev "$TUN_DEV" + eend $? || { eerror "${TUN_DEV} did not appear"; return 1; } + + # Configure tunnel addressing + if [ -n "$TUN_LOCAL_IP" ]; then + ebegin "Configuring ${TUN_DEV} with ${TUN_LOCAL_IP}" + ip addr add "$TUN_LOCAL_IP" dev "$TUN_DEV" 2>/dev/null + ip link set "$TUN_DEV" up + eend $? + fi + + # Set loose reverse path filtering + if [ -n "$RP_FILTER" ]; then + sysctl -qw net.ipv4.conf.all.rp_filter="$RP_FILTER" + sysctl -qw net.ipv4.conf.default.rp_filter="$RP_FILTER" + fi + + # Configure each interface: policy routing + path registration + local table="${RT_TABLE_START:-101}" + local ret=0 + + for spec in $INTERFACES; do + local iface="${spec%%:*}" + local gateway="${spec#*:}" + local src_ip + + src_ip=$(get_iface_ip "$iface") + if [ -z "$src_ip" ]; then + ewarn "No IPv4 address on $iface — skipping" + table=$((table + 1)) + continue + fi + + ebegin "Policy route: ${src_ip} via ${gateway} dev ${iface} (table ${table})" + setup_policy_route "$iface" "$gateway" "$table" "$src_ip" + eend $? + + ebegin "Registering path ${src_ip} on ${TUN_DEV}" + register_path "$src_ip" + eend $? || ret=1 + + table=$((table + 1)) + done + + return $ret +} + +stop() { + local table="${RT_TABLE_START:-101}" + + # Tear down paths and policy routes + for spec in $INTERFACES; do + local iface="${spec%%:*}" + local src_ip + + src_ip=$(get_iface_ip "$iface") + if [ -n "$src_ip" ]; then + ebegin "Removing path ${src_ip}" + deregister_path "$src_ip" + eend $? + + ebegin "Removing policy route for ${src_ip} (table ${table})" + teardown_policy_route "$table" "$src_ip" + eend $? + fi + + table=$((table + 1)) + done + + # Stop the tunnel process + ebegin "Stopping tunnel ${TUN_DEV}" + start-stop-daemon --stop --pidfile "$PIDDIR/${TUN_DEV}.pid" + eend $? +} + +status() { + if [ -f "$PIDDIR/${TUN_DEV}.pid" ] && kill -0 "$(cat "$PIDDIR/${TUN_DEV}.pid")" 2>/dev/null; then + einfo "Tunnel ${TUN_DEV} is running (pid $(cat "$PIDDIR/${TUN_DEV}.pid"))" + "$GLORYTUN_BIN" show dev "$TUN_DEV" 2>/dev/null + "$GLORYTUN_BIN" path dev "$TUN_DEV" 2>/dev/null + else + ewarn "Tunnel ${TUN_DEV} is not running" + return 1 + fi +} diff --git a/scripts/client/openrc/glorytun-client.conf b/scripts/client/openrc/glorytun-client.conf new file mode 100644 index 0000000..5d5fe6b --- /dev/null +++ b/scripts/client/openrc/glorytun-client.conf @@ -0,0 +1,89 @@ +# Configuration for glorytun-client +# Install to: /etc/conf.d/glorytun-client +# +# Defines a single multipath tunnel from this machine (client) to a remote +# server, using one or more local network interfaces as separate paths. + +# ── Tunnel ──────────────────────────────────────────────────────────────── + +# TUN device name +TUN_DEV="tun0" + +# Remote server address and port +SERVER_ADDR="203.0.113.1" +SERVER_PORT="5000" + +# Key file for this tunnel +KEYFILE="/etc/glorytun/key" + +# Tunnel IP addressing — applied to TUN_DEV after it is created. +# Use CIDR notation. Leave empty to skip (if managed elsewhere). +TUN_LOCAL_IP="10.99.0.2/30" + +# ── Interfaces / paths ─────────────────────────────────────────────────── +# +# Space-separated list of INTERFACE:GATEWAY pairs. +# Each interface becomes a separate path through the tunnel. +# The gateway is used for source-based policy routing so that packets +# with that interface's source IP leave via the correct physical link. +# +# Example: +# INTERFACES="eth1:192.168.1.254 eth2:192.168.12.252 eth3:192.168.11.5" + +INTERFACES="eth1:192.168.1.254 eth2:10.0.0.1 eth3:172.16.0.1 eth4:10.1.0.1" + +# ── Path settings ──────────────────────────────────────────────────────── + +# Rate mode: "fixed" or "auto" +RATE_MODE="fixed" + +# Transmit / receive limits per path (bytes/sec with optional suffix). +# +# The init script always passes `rate … tx … rx …` to glorytun. If you comment +# these lines out, the shell still substitutes built-in defaults — you are +# not omitting the limits. +# +# fixed: values are the actual rate. Unset defaults to ~100 Mbit each way. +# auto: values are *ceilings*; the daemon probes below that. If you still +# see ~100 Mbit after switching to auto, you were likely still using +# 12500k as the ceiling — raise RATE_TX/RATE_RX or leave them unset +# (auto uses a much larger default cap, or set RATE_AUTO_*_MAX). +RATE_TX="12500k" +RATE_RX="12500k" + +# Optional: when RATE_MODE=auto and RATE_TX / RATE_RX are unset, use these +# ceilings instead of the script default (≈100 Gbit class). +# RATE_AUTO_TX_MAX="12500000k" +# RATE_AUTO_RX_MAX="12500000k" + +# Keepalive beat interval (time suffix: s, ms, etc.) +BEAT="500ms" + +# Packet loss percentage at which a path is considered degraded +LOSSLIMIT="30%" + +# Max heartbeats without a reply before the path is treated as degraded +# (`glorytun path … set miss N`). Omit or leave empty to use the MUD default; +# set to 0 to request the default explicitly. Range 0–255 when set. +# HEARTBEAT_MISS="5" + +# Initial path state for `glorytun path … set`: "up" or "down" +PATH_STATE="up" + +# ── Routing ────────────────────────────────────────────────────────────── + +# Starting routing table number for source-based policy routes. +# Each interface gets its own table: RT_TABLE_START, RT_TABLE_START+1, ... +RT_TABLE_START=101 + +# Reverse path filter mode applied to all interfaces. +# 0 = disabled, 1 = strict, 2 = loose (recommended for multipath) +RP_FILTER=2 + +# ── Glorytun binary ────────────────────────────────────────────────────── + +# Path to the glorytun binary +GLORYTUN_BIN="/usr/local/bin/glorytun" + +# Extra options passed to glorytun bind (e.g. "chacha persist") +GLORYTUN_OPTS="persist" diff --git a/scripts/client/systemd/README.md b/scripts/client/systemd/README.md new file mode 100644 index 0000000..a6e730d --- /dev/null +++ b/scripts/client/systemd/README.md @@ -0,0 +1,156 @@ +# Glorytun Client — systemd + +Service files for running a Glorytun multipath client tunnel under systemd. + +## Files + +| File | Install to | Description | +|------|-----------|-------------| +| `glorytun-client.service` | `/etc/systemd/system/glorytun-client.service` | Service unit | +| `glorytun-client-start` | `/usr/local/lib/glorytun/glorytun-client-start` | Startup script (tunnel + routing + paths) | +| `glorytun-client-teardown` | `/usr/local/lib/glorytun/glorytun-client-teardown` | Teardown script (routes + paths cleanup) | +| `glorytun-client` | `/etc/default/glorytun-client` | Configuration file | + +## Installation + +```sh +cp glorytun-client.service /etc/systemd/system/ +systemctl daemon-reload + +mkdir -p /usr/local/lib/glorytun +cp glorytun-client-start /usr/local/lib/glorytun/ +cp glorytun-client-teardown /usr/local/lib/glorytun/ +chmod 755 /usr/local/lib/glorytun/glorytun-client-start +chmod 755 /usr/local/lib/glorytun/glorytun-client-teardown + +cp glorytun-client /etc/default/glorytun-client +``` + +## Configuration + +Edit `/etc/default/glorytun-client`. The main settings are: + +### Tunnel + +| Variable | Default | Description | +|----------|---------|-------------| +| `TUN_DEV` | `tun0` | TUN device name | +| `SERVER_ADDR` | — | Remote server IP address | +| `SERVER_PORT` | — | Remote server UDP port | +| `KEYFILE` | — | Path to the shared key file | +| `TUN_LOCAL_IP` | — | Local tunnel IP in CIDR notation (e.g. `10.99.0.2/30`). Leave empty to configure addressing elsewhere. | + +### Interfaces + +`INTERFACES` is a space-separated list of `INTERFACE:GATEWAY` pairs. Each +interface becomes a separate path through the tunnel. The gateway is the +next-hop for that interface — needed because the default route may point +elsewhere and source-based policy routing must know where to send each +interface's traffic. + +```sh +INTERFACES="eth1:192.168.1.254 eth2:10.0.0.1 eth3:172.16.0.1 eth4:10.1.0.1" +``` + +### Path settings + +| Variable | Default | Description | +|----------|---------|-------------| +| `RATE_MODE` | `fixed` | Rate mode: `fixed` or `auto` | +| `RATE_TX` | `12500k` (`fixed`) / large cap (`auto`) | Transmit limit per path (bytes/sec with suffix). With `auto`, this is a **ceiling** for probing. Unset under `auto` uses a large script default or `RATE_AUTO_TX_MAX`. | +| `RATE_RX` | (same pattern) | Receive limit; same semantics as `RATE_TX`. | +| `RATE_AUTO_TX_MAX` | `12500000k` | Ceiling when `RATE_MODE=auto` and `RATE_TX` is unset/empty. | +| `RATE_AUTO_RX_MAX` | `12500000k` | Ceiling when `RATE_MODE=auto` and `RATE_RX` is unset/empty. | +| `BEAT` | `5s` | Keepalive beat interval | +| `LOSSLIMIT` | `30%` | Loss percentage threshold for path degradation | +| `HEARTBEAT_MISS` | *(unset)* | Optional. Passed as `miss N` on `glorytun path … set` (max beats without reply before degraded). Unset or empty uses the MUD default; `0` requests the default explicitly. Range 0–255 when set. | +| `PATH_STATE` | `up` | Passed to `glorytun path … set` (`up` or `down`) | + +Commenting out `RATE_TX`/`RATE_RX` does not remove limits: the start script uses `${VAR:-default}`. Use `auto` with unset rates (or raise `RATE_TX`/`RATE_RX`) so mobile links are not stuck at ~100 Mbit. + +### Routing + +| Variable | Default | Description | +|----------|---------|-------------| +| `RT_TABLE_START` | `101` | First routing table number (incremented per interface) | +| `RP_FILTER` | `2` | Reverse path filter mode (2 = loose, recommended) | + +### Glorytun binary + +| Variable | Default | Description | +|----------|---------|-------------| +| `GLORYTUN_BIN` | `/usr/local/bin/glorytun` | Path to glorytun | +| `GLORYTUN_OPTS` | `persist` | Extra options for `glorytun bind` | + +## Key generation + +```sh +mkdir -p /etc/glorytun +glorytun keygen > /etc/glorytun/key +chmod 600 /etc/glorytun/key +``` + +Copy the same key to the server via a secure channel. + +## Usage + +```sh +# Enable and start the tunnel +systemctl enable --now glorytun-client + +# Stop the tunnel (teardown runs automatically) +systemctl stop glorytun-client + +# Check status +systemctl status glorytun-client + +# View logs +journalctl -u glorytun-client + +# Follow logs in real time +journalctl -fu glorytun-client + +# Disable at boot +systemctl disable glorytun-client +``` + +## What the scripts do + +### glorytun-client-start (ExecStart) + +1. Sources `/etc/default/glorytun-client`. +2. Starts `glorytun bind to addr port ...` in the background. +3. Waits for the TUN device to appear (up to 5 seconds). +4. Assigns `TUN_LOCAL_IP` to the TUN device and brings it up. +5. Sets `rp_filter` to loose mode on all interfaces. +6. For each interface in `INTERFACES`: + - Resolves the interface's current IPv4 address. + - Creates a source-based policy route via the specified gateway. + - Registers the path with glorytun (state, rate, beat, loss limit, optional `miss` from `HEARTBEAT_MISS`). +7. Waits on the glorytun process (so systemd tracks the main PID). + +### glorytun-client-teardown (ExecStopPost) + +Runs after the service stops (including on failure/restart): + +1. Brings each path down via `glorytun path dev … addr … set down`. +2. Removes source-based policy rules and flushes routing tables. + +This ensures policy routes don't linger if the service crashes. + +## Notes + +- Interface IP addresses are resolved at startup. If an interface has no IPv4 + address (e.g. DHCP hasn't completed), that path is skipped with a warning + visible in `journalctl`. +- The service automatically restarts on failure after a 5-second delay. The + teardown script runs before each restart to clean up stale routes. +- The unit includes systemd hardening (read-only filesystem, restricted + capabilities). If you encounter permission issues, check the + `ProtectSystem`, `ReadWritePaths`, and `CapabilityBoundingSet` settings in + the unit file. +- For DHCP environments where addresses change, consider a DHCP hook script + that brings the old path down and registers the new address (see + `MULTIPATH-EXAMPLE.md`). +- Unlike the server scripts (which use a template unit per tunnel), the client + is a single service managing one tunnel with multiple paths. diff --git a/scripts/client/systemd/glorytun-client b/scripts/client/systemd/glorytun-client new file mode 100644 index 0000000..5c10f9e --- /dev/null +++ b/scripts/client/systemd/glorytun-client @@ -0,0 +1,77 @@ +# Configuration for glorytun-client +# Install to: /etc/default/glorytun-client +# +# Defines a single multipath tunnel from this machine (client) to a remote +# server, using one or more local network interfaces as separate paths. + +# ── Tunnel ──────────────────────────────────────────────────────────────── + +# TUN device name +TUN_DEV="tun0" + +# Remote server address and port +SERVER_ADDR="203.0.113.1" +SERVER_PORT="5000" + +# Key file for this tunnel +KEYFILE="/etc/glorytun/key" + +# Tunnel IP addressing — applied to TUN_DEV after it is created. +# Use CIDR notation. Leave empty to skip (if managed elsewhere). +TUN_LOCAL_IP="10.99.0.2/30" + +# ── Interfaces / paths ─────────────────────────────────────────────────── +# +# Space-separated list of INTERFACE:GATEWAY pairs. +# Each interface becomes a separate path through the tunnel. +# The gateway is used for source-based policy routing so that packets +# with that interface's source IP leave via the correct physical link. +# +# Example: +# INTERFACES="eth1:192.168.1.254 eth2:192.168.12.252 eth3:192.168.11.5" + +INTERFACES="eth1:192.168.1.254 eth2:10.0.0.1 eth3:172.16.0.1 eth4:10.1.0.1" + +# ── Path settings ──────────────────────────────────────────────────────── + +# Rate mode: "fixed" or "auto" +RATE_MODE="fixed" + +# Transmit / receive limits per path (bytes/sec with optional suffix). +# Commenting these out does not remove limits — the start script substitutes +# defaults. In auto mode they are ceilings, not fixed throughput; unset uses +# a large default cap (see RATE_AUTO_*_MAX in glorytun-client-start). +RATE_TX="12500k" +RATE_RX="12500k" + +# RATE_AUTO_TX_MAX="12500000k" +# RATE_AUTO_RX_MAX="12500000k" + +# Keepalive beat interval (time suffix: s, ms, etc.) +BEAT="500ms" + +# Packet loss percentage at which a path is considered degraded +LOSSLIMIT="30%" + +# Max heartbeats without a reply before the path is treated as degraded +# (`glorytun path … set miss N`). Omit or leave empty to use the MUD default; +# set to 0 to request the default explicitly. Range 0–255 when set. +# HEARTBEAT_MISS="5" + +# Initial path state for `glorytun path … set`: "up" or "down" +PATH_STATE="up" + +# ── Routing ────────────────────────────────────────────────────────────── + +# Starting routing table number for source-based policy routes. +RT_TABLE_START=101 + +# Reverse path filter mode (0=off, 1=strict, 2=loose — 2 recommended) +RP_FILTER=2 + +# ── Glorytun binary ────────────────────────────────────────────────────── + +GLORYTUN_BIN="/usr/local/bin/glorytun" + +# Extra options passed to glorytun bind (e.g. "chacha persist") +GLORYTUN_OPTS="persist" diff --git a/scripts/client/systemd/glorytun-client-start b/scripts/client/systemd/glorytun-client-start new file mode 100644 index 0000000..df41add --- /dev/null +++ b/scripts/client/systemd/glorytun-client-start @@ -0,0 +1,140 @@ +#!/bin/sh +# Helper script for glorytun-client systemd service. +# Starts the tunnel, configures addressing, sets up source-based policy +# routing for each interface, and registers paths. +# +# Install to: /usr/local/lib/glorytun/glorytun-client-start +# Called by: glorytun-client.service + +set -e + +CONFIG="/etc/default/glorytun-client" + +if [ ! -f "$CONFIG" ]; then + echo "Config file $CONFIG not found" >&2 + exit 1 +fi + +. "$CONFIG" + +GLORYTUN_BIN="${GLORYTUN_BIN:-/usr/local/bin/glorytun}" +GLORYTUN_OPTS="${GLORYTUN_OPTS:-persist}" +TUN_DEV="${TUN_DEV:-tun0}" + +# Make sure the control socket is where we can find it. +ln -s /run/glorytun.0 /run/user/0/glorytun > /dev/null 2>&1 || true + +# ── Validation ──────────────────────────────────────────────────────────── + +if [ -z "$INTERFACES" ]; then + echo "INTERFACES not set in $CONFIG" >&2 + exit 1 +fi + +if [ -z "$SERVER_ADDR" ] || [ -z "$SERVER_PORT" ]; then + echo "SERVER_ADDR and SERVER_PORT must be set in $CONFIG" >&2 + exit 1 +fi + +if [ ! -f "$KEYFILE" ]; then + echo "Key file $KEYFILE not found" >&2 + exit 1 +fi + +# ── Helpers ─────────────────────────────────────────────────────────────── + +get_iface_ip() { + ip -4 -o addr show dev "$1" 2>/dev/null \ + | awk '{print $4}' | cut -d/ -f1 | head -n1 +} + +wait_for_dev() { + local dev="$1" tries=0 + while [ $tries -lt 50 ]; do + ip link show "$dev" >/dev/null 2>&1 && return 0 + tries=$((tries + 1)) + sleep 0.1 + done + return 1 +} + +# ── Start tunnel in background ──────────────────────────────────────────── + +"$GLORYTUN_BIN" bind to addr "$SERVER_ADDR" port "$SERVER_PORT" \ + dev "$TUN_DEV" keyfile "$KEYFILE" $GLORYTUN_OPTS & +GT_PID=$! + +echo "Tunnel PID: $GT_PID" + +# Wait for the TUN device +if ! wait_for_dev "$TUN_DEV"; then + echo "${TUN_DEV} did not appear — killing glorytun" >&2 + kill "$GT_PID" 2>/dev/null + exit 1 +fi + +# ── Configure tunnel addressing ────────────────────────────────────────── + +if [ -n "$TUN_LOCAL_IP" ]; then + ip addr add "$TUN_LOCAL_IP" dev "$TUN_DEV" 2>/dev/null || true + ip link set "$TUN_DEV" up +fi + +# ── Reverse path filtering ─────────────────────────────────────────────── + +RP_FILTER="${RP_FILTER:-2}" +sysctl -qw net.ipv4.conf.all.rp_filter="$RP_FILTER" +sysctl -qw net.ipv4.conf.default.rp_filter="$RP_FILTER" + +# ── Per-interface setup ─────────────────────────────────────────────────── + +table="${RT_TABLE_START:-101}" + +rate_mode="${RATE_MODE:-fixed}" +if [ "$rate_mode" = "auto" ]; then + rate_tx="${RATE_TX:-${RATE_AUTO_TX_MAX:-12500000k}}" + rate_rx="${RATE_RX:-${RATE_AUTO_RX_MAX:-12500000k}}" +else + rate_tx="${RATE_TX:-12500k}" + rate_rx="${RATE_RX:-12500k}" +fi + +for spec in $INTERFACES; do + iface="${spec%%:*}" + gateway="${spec#*:}" + + src_ip=$(get_iface_ip "$iface") + if [ -z "$src_ip" ]; then + echo "Warning: no IPv4 address on $iface — skipping" >&2 + table=$((table + 1)) + continue + fi + + # Source-based policy route + ip route replace default via "$gateway" dev "$iface" table "$table" 2>/dev/null + ip rule add from "$src_ip" table "$table" 2>/dev/null || true + echo "Policy route: ${src_ip} via ${gateway} dev ${iface} table ${table}" + + # Register path — rate_tx/rate_rx computed once before the loop; see + # scripts/client/openrc/glorytun-client register_path() for semantics. + if [ -n "${HEARTBEAT_MISS+x}" ] && [ -n "$HEARTBEAT_MISS" ]; then + miss_arg="miss $HEARTBEAT_MISS" + else + miss_arg= + fi + + "$GLORYTUN_BIN" path dev "$TUN_DEV" addr "$src_ip" set "${PATH_STATE:-up}" \ + beat "${BEAT:-5s}" \ + losslimit "${LOSSLIMIT:-30%}" \ + ${miss_arg:+$miss_arg }\ + rate "$rate_mode" tx "$rate_tx" rx "$rate_rx" + echo "Path registered: ${src_ip} on ${TUN_DEV}" + + table=$((table + 1)) +done + +# ── Wait for tunnel process ────────────────────────────────────────────── +# The service type is "forking" via the wrapper, or we can just wait here +# and let systemd manage the main PID. + +wait "$GT_PID" diff --git a/scripts/client/systemd/glorytun-client-teardown b/scripts/client/systemd/glorytun-client-teardown new file mode 100644 index 0000000..f30ff3a --- /dev/null +++ b/scripts/client/systemd/glorytun-client-teardown @@ -0,0 +1,37 @@ +#!/bin/sh +# Teardown script for glorytun-client systemd service. +# Removes source-based policy routes and brings paths down. +# +# Install to: /usr/local/lib/glorytun/glorytun-client-teardown +# Called by: glorytun-client.service ExecStopPost + +CONFIG="/etc/default/glorytun-client" + +if [ ! -f "$CONFIG" ]; then + exit 0 +fi + +. "$CONFIG" + +GLORYTUN_BIN="${GLORYTUN_BIN:-/usr/local/bin/glorytun}" +TUN_DEV="${TUN_DEV:-tun0}" + +get_iface_ip() { + ip -4 -o addr show dev "$1" 2>/dev/null \ + | awk '{print $4}' | cut -d/ -f1 | head -n1 +} + +table="${RT_TABLE_START:-101}" + +for spec in $INTERFACES; do + iface="${spec%%:*}" + src_ip=$(get_iface_ip "$iface") + + if [ -n "$src_ip" ]; then + "$GLORYTUN_BIN" path dev "$TUN_DEV" addr "$src_ip" set down 2>/dev/null || true + ip rule del from "$src_ip" table "$table" 2>/dev/null || true + ip route flush table "$table" 2>/dev/null || true + fi + + table=$((table + 1)) +done diff --git a/scripts/client/systemd/glorytun-client.service b/scripts/client/systemd/glorytun-client.service new file mode 100644 index 0000000..88f02b3 --- /dev/null +++ b/scripts/client/systemd/glorytun-client.service @@ -0,0 +1,30 @@ +# Glorytun client - systemd service unit +# Install to: /etc/systemd/system/glorytun-client.service +# +# Usage: +# systemctl enable --now glorytun-client + +[Unit] +Description=Glorytun multipath tunnel client +After=network-online.target +Wants=network-online.target +Documentation=man:glorytun + +[Service] +Type=simple +ExecStart=/usr/local/lib/glorytun/glorytun-client-start +ExecStopPost=/usr/local/lib/glorytun/glorytun-client-teardown +Restart=on-failure +RestartSec=5 + +ProtectSystem=strict +ProtectHome=yes +ReadOnlyPaths=/ +ReadWritePaths=/dev/net/tun /run +PrivateTmp=yes +NoNewPrivileges=no +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target diff --git a/scripts/server/openrc/README.md b/scripts/server/openrc/README.md new file mode 100644 index 0000000..9a30b4c --- /dev/null +++ b/scripts/server/openrc/README.md @@ -0,0 +1,103 @@ +# Glorytun Server — OpenRC + +Service scripts for running Glorytun server tunnels under OpenRC. + +## Files + +| File | Install to | Description | +|------|-----------|-------------| +| `glorytun-server` | `/etc/init.d/glorytun-server` | Init script | +| `glorytun-server.conf` | `/etc/conf.d/glorytun-server` | Configuration file | + +## Installation + +```sh +cp glorytun-server /etc/init.d/glorytun-server +chmod 755 /etc/init.d/glorytun-server + +cp glorytun-server.conf /etc/conf.d/glorytun-server +``` + +## Configuration + +Edit `/etc/conf.d/glorytun-server`. The key setting is `TUNNELS`, which lists +all tunnels to manage. Each entry has the format: + +``` +TUN_NAME:PORT:INTERFACE:KEYFILE[:TUN_LOCAL_IP] +``` + +| Field | Description | +|---------------|----------------------------------------------------------------------| +| `TUN_NAME` | Name of the TUN device to create (e.g. `tun0`) | +| `PORT` | UDP port to listen on | +| `INTERFACE` | Network interface to bind to (IP is resolved at startup) | +| `KEYFILE` | Path to the secret key file for this tunnel | +| `TUN_LOCAL_IP`| Optional. CIDR address for the tunnel (e.g. `10.99.0.1/30`). The script waits for the TUN device, assigns this IP, and brings the interface up. Omit to configure elsewhere. | + +Example with two tunnels on different interfaces, each with its own key and IP: + +```sh +TUNNELS="tun0:5000:eth0:/etc/glorytun/tun0.key:10.99.0.1/30 tun1:5000:eth1:/etc/glorytun/tun1.key:10.99.1.1/30" +``` + +Example with two tunnels on the same interface using different ports and a +shared key: + +```sh +TUNNELS="tun0:5000:eth0:/etc/glorytun/key:10.99.0.1/30 tun1:5001:eth0:/etc/glorytun/key:10.99.1.1/30" +``` + +Generate a key file with: + +```sh +mkdir -p /etc/glorytun +glorytun keygen > /etc/glorytun/key +chmod 600 /etc/glorytun/key +``` + +Optional settings in the config file: + +| Variable | Default | Description | +|----------------|--------------------------------|------------------------------------| +| `GLORYTUN_BIN` | `/usr/local/bin/glorytun` | Path to the glorytun binary | +| `GLORYTUN_OPTS`| `persist` | Extra options passed to `glorytun bind` | + +## Usage + +```sh +# Start all configured tunnels +rc-service glorytun-server start + +# Stop all tunnels +rc-service glorytun-server stop + +# Check status of all tunnels +rc-service glorytun-server status + +# Enable at boot +rc-update add glorytun-server default + +# Disable at boot +rc-update del glorytun-server default +``` + +## Notes + +- The init script manages all tunnels defined in `TUNNELS` as a single service. + Starting or stopping the service affects all tunnels. +- Each tunnel runs as a separate background process with a PID file in + `/run/glorytun/`. +- The IP address is resolved from the named interface at startup. If the + interface has no IPv4 address, that tunnel fails to start with an error. +- The `persist` option (default in `GLORYTUN_OPTS`) keeps the TUN device alive + if glorytun exits, allowing a restart without disrupting the interface + configuration. +- The init script waits for the TUN device, optionally assigns `TUN_LOCAL_IP` + if specified in the tunnel spec, and brings the interface up. If you omit + `TUN_LOCAL_IP`, configure the TUN interface via `/etc/network/interfaces` + or a post-up script. + +**Troubleshooting:** If `glorytun show` reports "no device", the control socket +is per-user. The service runs as root, so run `sudo glorytun show` (or +`sudo glorytun show dev tun0`) to query the tunnel status. diff --git a/scripts/server/openrc/glorytun-server b/scripts/server/openrc/glorytun-server new file mode 100755 index 0000000..322a04e --- /dev/null +++ b/scripts/server/openrc/glorytun-server @@ -0,0 +1,139 @@ +#!/sbin/openrc-run +# glorytun-server - OpenRC init script for Glorytun server tunnels +# Install to: /etc/init.d/glorytun-server +# Config: /etc/conf.d/glorytun-server + +description="Glorytun encrypted UDP tunnel server" + +GLORYTUN_BIN="${GLORYTUN_BIN:-/usr/local/bin/glorytun}" +GLORYTUN_OPTS="${GLORYTUN_OPTS:-persist}" +PIDDIR="/run/glorytun" + +depend() { + need net + after firewall +} + +get_iface_ip() { + local iface="$1" + ip -4 -o addr show dev "$iface" 2>/dev/null \ + | awk '{print $4}' | cut -d/ -f1 | head -n1 +} + +wait_for_dev() { + local dev="$1" tries=0 + while [ $tries -lt 50 ]; do + ip link show "$dev" >/dev/null 2>&1 && return 0 + tries=$((tries + 1)) + sleep 0.1 + done + return 1 +} + +parse_tunnel() { + local spec="$1" + TUN_NAME="${spec%%:*}" + local rest="${spec#*:}" + TUN_PORT="${rest%%:*}" + rest="${rest#*:}" + TUN_IFACE="${rest%%:*}" + rest="${rest#*:}" + case "$rest" in + *:*) TUN_KEYFILE="${rest%:*}" ; TUN_LOCAL_IP="${rest##*:}" ;; + *) TUN_KEYFILE="$rest" ; TUN_LOCAL_IP="" ;; + esac +} + +start_tunnel() { + local spec="$1" + parse_tunnel "$spec" + + if [ ! -f "$TUN_KEYFILE" ]; then + eerror "Key file $TUN_KEYFILE not found for tunnel $TUN_NAME" + return 1 + fi + + local bind_ip + bind_ip=$(get_iface_ip "$TUN_IFACE") + + if [ -z "$bind_ip" ]; then + eerror "No IPv4 address on interface $TUN_IFACE for tunnel $TUN_NAME" + return 1 + fi + + ebegin "Starting tunnel $TUN_NAME on $bind_ip:$TUN_PORT ($TUN_IFACE)" + start-stop-daemon --start --background \ + --make-pidfile --pidfile "$PIDDIR/$TUN_NAME.pid" \ + --exec "$GLORYTUN_BIN" -- \ + bind from addr "$bind_ip" port "$TUN_PORT" dev "$TUN_NAME" \ + keyfile "$TUN_KEYFILE" $GLORYTUN_OPTS + eend $? || return 1 + + # Wait for the TUN device to appear + ebegin "Waiting for ${TUN_NAME}" + wait_for_dev "$TUN_NAME" + eend $? || { eerror "${TUN_NAME} did not appear"; return 1; } + + # Configure tunnel addressing (glorytun creates the device but does not set IP or link state) + if [ -n "$TUN_LOCAL_IP" ]; then + ebegin "Configuring ${TUN_NAME} with ${TUN_LOCAL_IP}" + ip addr add "$TUN_LOCAL_IP" dev "$TUN_NAME" 2>/dev/null || true + eend $? + fi + # Bring the interface up — required for the tunnel to accept traffic from clients + ebegin "Bringing ${TUN_NAME} up" + ip link set "$TUN_NAME" up + eend $? +} + +stop_tunnel() { + local spec="$1" + parse_tunnel "$spec" + + ebegin "Stopping tunnel $TUN_NAME" + start-stop-daemon --stop --pidfile "$PIDDIR/$TUN_NAME.pid" + eend $? +} + +status_tunnel() { + local spec="$1" + parse_tunnel "$spec" + + if [ -f "$PIDDIR/$TUN_NAME.pid" ] && kill -0 "$(cat "$PIDDIR/$TUN_NAME.pid")" 2>/dev/null; then + einfo "Tunnel $TUN_NAME is running (pid $(cat "$PIDDIR/$TUN_NAME.pid"))" + else + ewarn "Tunnel $TUN_NAME is not running" + return 1 + fi +} + +start() { + if [ -z "$TUNNELS" ]; then + eerror "No tunnels configured in /etc/conf.d/glorytun-server" + return 1 + fi + + checkpath --directory --mode 0755 "$PIDDIR" + + local ret=0 + for spec in $TUNNELS; do + start_tunnel "$spec" || ret=1 + done + return $ret +} + +stop() { + local ret=0 + for spec in $TUNNELS; do + stop_tunnel "$spec" || ret=1 + done + return $ret +} + +status() { + local ret=0 + for spec in $TUNNELS; do + status_tunnel "$spec" || ret=1 + done + return $ret +} diff --git a/scripts/server/openrc/glorytun-server.conf b/scripts/server/openrc/glorytun-server.conf new file mode 100644 index 0000000..e9dab27 --- /dev/null +++ b/scripts/server/openrc/glorytun-server.conf @@ -0,0 +1,26 @@ +# Configuration for glorytun-server +# Install to: /etc/conf.d/glorytun-server +# +# Each tunnel is defined as TUN_NAME:PORT:INTERFACE:KEYFILE[:TUN_LOCAL_IP] +# The server binds to the IP address of INTERFACE on the given PORT, +# creates the TUN_NAME device, and uses KEYFILE for authentication. +# Optional TUN_LOCAL_IP (CIDR) is assigned to the tunnel and the interface +# is brought up. Without it, you must configure the TUN interface elsewhere. +# +# Examples: +# Single tunnel with IP: +# TUNNELS="tun0:5000:eth0:/etc/glorytun/key:10.99.0.1/30" +# +# Single tunnel (configure TUN elsewhere): +# TUNNELS="tun0:5000:eth0:/etc/glorytun/key" +# +# Multiple tunnels with separate keys: +# TUNNELS="tun0:5000:eth0:/etc/glorytun/tun0.key:10.99.0.1/30 tun1:5001:eth0:/etc/glorytun/tun1.key:10.99.1.1/30" + +TUNNELS="tun0:5000:eth0:/etc/glorytun/key:10.99.0.1/30" + +# Path to the glorytun binary +GLORYTUN_BIN="/usr/local/bin/glorytun" + +# Extra options passed to glorytun bind (e.g. "chacha" to force fallback cipher) +GLORYTUN_OPTS="persist" diff --git a/scripts/server/systemd/README.md b/scripts/server/systemd/README.md new file mode 100644 index 0000000..d757b43 --- /dev/null +++ b/scripts/server/systemd/README.md @@ -0,0 +1,128 @@ +# Glorytun Server — systemd + +Service files for running Glorytun server tunnels under systemd. + +## Files + +| File | Install to | Description | +|------|-----------|-------------| +| `glorytun-server@.service` | `/etc/systemd/system/glorytun-server@.service` | Template service unit | +| `glorytun-server-start` | `/usr/local/lib/glorytun/glorytun-server-start` | Helper script called by the service | +| `glorytun-server` | `/etc/default/glorytun-server` | Configuration file | + +## Installation + +```sh +cp glorytun-server@.service /etc/systemd/system/ +systemctl daemon-reload + +mkdir -p /usr/local/lib/glorytun +cp glorytun-server-start /usr/local/lib/glorytun/ +chmod 755 /usr/local/lib/glorytun/glorytun-server-start + +cp glorytun-server /etc/default/glorytun-server +``` + +## Configuration + +Edit `/etc/default/glorytun-server`. The key setting is `TUNNELS`, which lists +all tunnels to manage. Each entry has the format: + +``` +TUN_NAME:PORT:INTERFACE:KEYFILE[:TUN_LOCAL_IP] +``` + +| Field | Description | +|---------------|----------------------------------------------------------------------| +| `TUN_NAME` | Name of the TUN device to create (e.g. `tun0`) | +| `PORT` | UDP port to listen on | +| `INTERFACE` | Network interface to bind to (IP is resolved at startup) | +| `KEYFILE` | Path to the secret key file for this tunnel | +| `TUN_LOCAL_IP`| Optional. CIDR address for the tunnel (e.g. `10.99.0.1/30`). The script waits for the TUN device, assigns this IP, and brings the interface up. Omit to configure elsewhere. | + +Example with two tunnels on different interfaces, each with its own key and IP: + +```sh +TUNNELS="tun0:5000:eth0:/etc/glorytun/tun0.key:10.99.0.1/30 tun1:5000:eth1:/etc/glorytun/tun1.key:10.99.1.1/30" +``` + +Example with two tunnels on the same interface using different ports and a +shared key: + +```sh +TUNNELS="tun0:5000:eth0:/etc/glorytun/key:10.99.0.1/30 tun1:5001:eth0:/etc/glorytun/key:10.99.1.1/30" +``` + +Generate a key file with: + +```sh +mkdir -p /etc/glorytun +glorytun keygen > /etc/glorytun/key +chmod 600 /etc/glorytun/key +``` + +Optional settings in the config file: + +| Variable | Default | Description | +|----------------|--------------------------------|------------------------------------| +| `GLORYTUN_BIN` | `/usr/local/bin/glorytun` | Path to the glorytun binary | +| `GLORYTUN_OPTS`| `persist` | Extra options passed to `glorytun bind` | + +## Usage + +This is a template unit. The instance name (after the `@`) is the tunnel name, +which must match an entry in the `TUNNELS` variable. + +```sh +# Enable and start a tunnel +systemctl enable --now glorytun-server@tun0 + +# Enable and start multiple tunnels +systemctl enable --now glorytun-server@tun0 glorytun-server@tun1 + +# Stop a tunnel +systemctl stop glorytun-server@tun0 + +# Check status +systemctl status glorytun-server@tun0 + +# View logs +journalctl -u glorytun-server@tun0 + +# Follow logs in real time +journalctl -fu glorytun-server@tun0 + +# Disable a tunnel (won't start at boot) +systemctl disable glorytun-server@tun0 +``` + +## Notes + +- Each tunnel runs as an independent systemd service. You can start, stop, + restart, and inspect logs for each tunnel individually. +- The service automatically restarts on failure after a 5-second delay. +- The helper script reads `/etc/default/glorytun-server`, finds the matching + tunnel entry, resolves the IP from the named interface, and runs + `glorytun bind from addr … port … dev … keyfile …`. +- The IP address is resolved from the named interface at startup. If the + interface has no IPv4 address, the service fails with an error visible in + `journalctl`. +- The `persist` option (default in `GLORYTUN_OPTS`) keeps the TUN device alive + if glorytun exits, allowing a restart without disrupting the interface + configuration. +- The unit includes systemd hardening (read-only filesystem, restricted + capabilities). If you encounter permission issues, check the + `ProtectSystem`, `ReadWritePaths`, and `CapabilityBoundingSet` settings in + the unit file. +- The helper script waits for the TUN device, optionally assigns `TUN_LOCAL_IP` + if specified in the tunnel spec, and brings the interface up. If you omit + `TUN_LOCAL_IP`, configure the TUN interface via systemd-networkd or elsewhere. + +**Troubleshooting:** If `glorytun show` reports "no device", glorytun uses +different control socket paths depending on whether `/run/user/0` exists. The +service hides `/run/user` so it uses `/run/glorytun.0`. If your root session has +`/run/user/0` (e.g. from logind), run glorytun in the same sandbox: + +```sh +systemd-run --scope -p InaccessiblePaths=/run/user -G glorytun show +``` diff --git a/scripts/server/systemd/glorytun-server b/scripts/server/systemd/glorytun-server new file mode 100644 index 0000000..000636d --- /dev/null +++ b/scripts/server/systemd/glorytun-server @@ -0,0 +1,26 @@ +# Configuration for glorytun-server +# Install to: /etc/default/glorytun-server +# +# Each tunnel is defined as TUN_NAME:PORT:INTERFACE:KEYFILE[:TUN_LOCAL_IP] +# The server binds to the IP address of INTERFACE on the given PORT, +# creates the TUN_NAME device, and uses KEYFILE for authentication. +# Optional TUN_LOCAL_IP (CIDR) is assigned to the tunnel and the interface +# is brought up. Without it, you must configure the TUN interface elsewhere. +# +# Examples: +# Single tunnel with IP: +# TUNNELS="tun0:5000:eth0:/etc/glorytun/key:10.99.0.1/30" +# +# Single tunnel (configure TUN elsewhere): +# TUNNELS="tun0:5000:eth0:/etc/glorytun/key" +# +# Multiple tunnels with separate keys: +# TUNNELS="tun0:5000:eth0:/etc/glorytun/tun0.key:10.99.0.1/30 tun1:5001:eth0:/etc/glorytun/tun1.key:10.99.1.1/30" + +TUNNELS="tun0:5000:eth0:/etc/glorytun/key:10.99.0.1/30" + +# Path to the glorytun binary +GLORYTUN_BIN="/usr/local/bin/glorytun" + +# Extra options passed to glorytun bind (e.g. "chacha" to force fallback cipher) +GLORYTUN_OPTS="persist" diff --git a/scripts/server/systemd/glorytun-server-start b/scripts/server/systemd/glorytun-server-start new file mode 100755 index 0000000..27d0103 --- /dev/null +++ b/scripts/server/systemd/glorytun-server-start @@ -0,0 +1,100 @@ +#!/bin/sh +# Helper script for glorytun-server systemd template unit. +# Parses the shared config file, resolves the interface IP for the +# requested tunnel, starts glorytun, waits for the TUN device, configures +# addressing if TUN_LOCAL_IP is set, and runs until glorytun exits. +# +# Install to: /usr/local/lib/glorytun/glorytun-server-start +# Called by: glorytun-server@.service with the tunnel name as $1 + +set -e + +TUNNEL_NAME="$1" + +if [ -z "$TUNNEL_NAME" ]; then + echo "usage: glorytun-server-start " >&2 + exit 1 +fi + +CONFIG="/etc/default/glorytun-server" + +if [ ! -f "$CONFIG" ]; then + echo "Config file $CONFIG not found" >&2 + exit 1 +fi + +. "$CONFIG" + +GLORYTUN_BIN="${GLORYTUN_BIN:-/usr/local/bin/glorytun}" +GLORYTUN_OPTS="${GLORYTUN_OPTS:-persist}" + +FOUND="" +tun_local_ip="" +for spec in $TUNNELS; do + tun="${spec%%:*}" + rest="${spec#*:}" + port="${rest%%:*}" + rest="${rest#*:}" + iface="${rest%%:*}" + rest="${rest#*:}" + case "$rest" in + *:*) keyfile="${rest%:*}" ; tun_local_ip="${rest##*:}" ;; + *) keyfile="$rest" ; tun_local_ip="" ;; + esac + + if [ "$tun" = "$TUNNEL_NAME" ]; then + FOUND=1 + break + fi + tun_local_ip="" +done + +if [ -z "$FOUND" ]; then + echo "Tunnel $TUNNEL_NAME not found in TUNNELS in $CONFIG" >&2 + exit 1 +fi + +if [ ! -f "$keyfile" ]; then + echo "Key file $keyfile not found for tunnel $TUNNEL_NAME" >&2 + exit 1 +fi + +bind_ip=$(ip -4 -o addr show dev "$iface" 2>/dev/null \ + | awk '{print $4}' | cut -d/ -f1 | head -n1) + +if [ -z "$bind_ip" ]; then + echo "No IPv4 address on interface $iface for tunnel $TUNNEL_NAME" >&2 + exit 1 +fi + +wait_for_dev() { + local dev="$1" tries=0 + while [ $tries -lt 50 ]; do + ip link show "$dev" >/dev/null 2>&1 && return 0 + tries=$((tries + 1)) + sleep 0.1 + done + return 1 +} + +# Start glorytun in background +"$GLORYTUN_BIN" bind from addr "$bind_ip" port "$port" dev "$TUNNEL_NAME" \ + keyfile "$keyfile" $GLORYTUN_OPTS & +GT_PID=$! + +# Wait for the TUN device to appear +if ! wait_for_dev "$TUNNEL_NAME"; then + echo "${TUNNEL_NAME} did not appear — killing glorytun" >&2 + kill "$GT_PID" 2>/dev/null + exit 1 +fi + +# Configure tunnel addressing (glorytun creates the device but does not set IP or link state) +if [ -n "$tun_local_ip" ]; then + ip addr add "$tun_local_ip" dev "$TUNNEL_NAME" 2>/dev/null || true +fi +# Bring the interface up — required for the tunnel to accept traffic from clients +ip link set "$TUNNEL_NAME" up + +# Wait for glorytun to exit (service stays running until then) +wait "$GT_PID" diff --git a/scripts/server/systemd/glorytun-server@.service b/scripts/server/systemd/glorytun-server@.service new file mode 100644 index 0000000..61d23db --- /dev/null +++ b/scripts/server/systemd/glorytun-server@.service @@ -0,0 +1,38 @@ +# Glorytun server - systemd template unit +# Install to: /etc/systemd/system/glorytun-server@.service +# +# Usage: +# systemctl enable --now glorytun-server@tun0 +# systemctl enable --now glorytun-server@tun1 +# +# The instance name (%i) must match a tunnel name defined in +# /etc/default/glorytun-server + +[Unit] +Description=Glorytun tunnel server (%i) +After=network-online.target +Wants=network-online.target +Documentation=man:glorytun + +[Service] +Type=simple +ExecStart=/usr/local/lib/glorytun/glorytun-server-start %i +Restart=on-failure +RestartSec=5 + +# Force control socket to /run/glorytun.0 (glorytun prefers /run/user/0 when it exists; +# hiding /run/user makes both service and interactive root use the same path) +InaccessiblePaths=/run/user + +# Hardening +ProtectSystem=strict +ProtectHome=yes +ReadOnlyPaths=/ +ReadWritePaths=/dev/net/tun /run +PrivateTmp=yes +NoNewPrivileges=no +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target diff --git a/src/path.c b/src/path.c index 37fbcc6..6efa315 100644 --- a/src/path.c +++ b/src/path.c @@ -56,10 +56,12 @@ gt_path_conf(struct ctl_msg *res) return; printf("path dev %s addr %s to addr %s port %"PRIu16" " - "set %s pref %u beat %s losslimit %u%% rate %s tx %s rx %s\n", + "set %s pref %u beat %s miss %u losslimit %u%% rate %s tx %s rx %s\n", res->tun_name, local, remote, gt_get_port(&res->path.conf.remote), state, res->path.conf.pref, beat, + res->path.conf.heartbeat_miss_max + ? res->path.conf.heartbeat_miss_max : MUD_HEARTBEAT_MISS_DEFAULT, res->path.conf.loss_limit * 100U / 255U, res->path.conf.fixed_rate ? "fixed" : "auto", tx, rx); @@ -212,6 +214,7 @@ gt_path(int argc, char **argv, void *data) struct argz_ull beat = {.suffix = argz_time_suffix}; struct argz_ull pref = {.max = 0xFF >> 1}; struct argz_ull loss = {.max = 100, .suffix = gt_argz_percent_suffix}; + struct argz_ull miss = {.max = 255}; struct argz setz[] = { {"up", "Enable path", .grp = 2}, @@ -220,6 +223,8 @@ gt_path(int argc, char **argv, void *data) {"beat", "Internal beat rate", argz_ull, &beat}, {"pref", "Path preference", argz_ull, &pref}, {"losslimit", "Disable lossy path", argz_ull, &loss}, + {"miss", "Max beats without reply before degraded (0=default 5)", + argz_ull, &miss}, {0}}; struct argz showz[] = { @@ -266,6 +271,8 @@ gt_path(int argc, char **argv, void *data) .pref = argz_is_set(setz, "pref") ? (pref.value << 1) | 1 : 0, .loss_limit = loss.value * 255 / 100, + .heartbeat_miss_max = argz_is_set(setz, "miss") + ? (unsigned char)miss.value : 0, }, }, res = {0}; diff --git a/systemd/glorytun-client.network b/systemd/glorytun-client.network deleted file mode 100644 index e80b7ab..0000000 --- a/systemd/glorytun-client.network +++ /dev/null @@ -1,10 +0,0 @@ -[Match] -Name=gtc-* - -[Network] -Description=Glorytun client device -DHCP=ipv4 - -[DHCP] -CriticalConnection=yes -RouteTable=200 diff --git a/systemd/glorytun-run b/systemd/glorytun-run deleted file mode 100755 index cf5a8d9..0000000 --- a/systemd/glorytun-run +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -exec glorytun bind "$@" \ - $BIND $BIND_PORT \ - ${DEV:+dev "$DEV"} \ - ${HOST:+to "$HOST" "$PORT"} diff --git a/systemd/glorytun-setup b/systemd/glorytun-setup deleted file mode 100755 index 7e69027..0000000 --- a/systemd/glorytun-setup +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/sh - -set -e - -_ask() { - printf "%s: " "$1" - read -r "$2" -} - -_ask "Config filename (tun0)" NAME -NAME=${NAME:-tun0} -DIR="/etc/glorytun/$NAME" - -if [ -d "$DIR" ]; then - echo "This config already exit!" - exit 1 -fi - -_ask "Server ip (enter for server conf)" HOST -if [ -z "$HOST" ]; then - _ask "Bind to port (5000)" BIND_PORT - BIND_PORT=${BIND_PORT:-5000} -else - _ask "Server port (5000)" PORT - PORT=${PORT:-5000} -fi - -BIND=0.0.0.0 -case "$HOST" in - *:*) BIND=:: -esac - -_ask "Server key (enter to generate a new one)" KEY -if [ -z "$KEY" ]; then - KEY=$(glorytun keygen) - echo "Your new key: $KEY" -fi - -# install files -mkdir -p "$DIR" - -cat > "$DIR/env" < "$DIR/key" ) - -[ "$HOST" ] && cat > "$DIR/post.sh" <<'EOF' -#!/bin/sh - -PREF=32765 -TABLE=200 - -# keep the current route to HOST -SRC=$(ip route get "$HOST" | awk '/src/{getline;print $0}' RS=' ') -ip rule add from "$SRC" table main pref "$((PREF-1))" || true - -# limit to 100Mbit by default -glorytun path up "$SRC" dev "$DEV" rate rx 12500000 tx 12500000 - -# forward everything else to the tunnel -ip rule add from all table "$TABLE" pref "$PREF" || true -EOF -[ -f "$DIR/post.sh" ] && chmod u+x "$DIR/post.sh" - -# start services -_ask "Start glorytun now ? (enter to skip)" START -case "$START" in y*|Y*) - systemctl start glorytun@"$NAME" ;; -esac diff --git a/systemd/glorytun.network b/systemd/glorytun.network deleted file mode 100644 index 6ea6d2b..0000000 --- a/systemd/glorytun.network +++ /dev/null @@ -1,14 +0,0 @@ -[Match] -Name=gt-* - -[Network] -Description=Glorytun server device -Address=0.0.0.0/24 -DHCPServer=yes -IPMasquerade=yes - -[DHCPServer] -PoolOffset=2 -PoolSize=1 -EmitDNS=yes -DNS=9.9.9.9 diff --git a/systemd/glorytun@.service.in b/systemd/glorytun@.service.in deleted file mode 100644 index 00cbc64..0000000 --- a/systemd/glorytun@.service.in +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=Glorytun on %I -After=network.target - -[Service] -Type=simple -Restart=always -EnvironmentFile=/etc/glorytun/%i/env -ExecStart=@bindir@/glorytun-run keyfile /etc/glorytun/%i/key $OPTIONS -ExecStartPost=-/etc/glorytun/%i/post.sh -CapabilityBoundingSet=CAP_NET_ADMIN - -[Install] -WantedBy=multi-user.target