Compare commits

..
Author SHA1 Message Date
renovate Bot 0cc09c7b91 chore(deps): maintain cargo lockfile 2026-08-29 14:37:25 +00:00
Nils Andreas Svee 45351e0ba1 chore(release): irtt-server 0.5.2, irtt-rs 0.6.2
Publish irtt-server image / publish (push) Successful in 1m17s
irtt-server's hex-pattern chunking moves to the stable as_chunks API
with no behavior change. irtt-rs picks up IRTT_SERVER_* environment
variable configuration, SIGTERM handling via ctrlc's termination
feature, and the Dockerfile/Gitea Actions pipeline that publishes its
server image to Gitea and GHCR.
2026-08-26 17:03:47 +07:00
Nils Andreas Svee e2b70ac50a ci: add Dockerfile and Gitea Actions workflow for irtt-server
Introduce a multi-platform build pipeline to automate the publication of a minimal, statically linked irtt-server image to Gitea and GHCR.
2026-08-25 00:50:04 +07:00
Nils Andreas Svee 95037f7fa0 fix(clippy): use as_chunks instead of chunks_exact 2026-08-25 00:16:21 +07:00
Nils Andreas Svee 614579b8b6 build(app): enable termination feature for ctrlc dependency
GitHub Actions Security / actionlint (push) Failing after 36s
Rust CI / cargo fmt (push) Successful in 40s
Rust CI / cargo clippy (push) Failing after 45s
Rust CI / cargo doc (push) Successful in 45s
Netem Interop / netem interop (push) Failing after 45s
Supply chain / cargo-deny advisories (push) Failing after 9s
GitHub Actions Security / zizmor (push) Failing after 3m52s
Proto Fuzz Smoke / cargo fuzz smoke (push) Successful in 1m57s
Supply chain / cargo-deny bans licenses sources (push) Failing after 8s
Supply chain / actionlint (push) Failing after 38s
Clippy SARIF / Clippy SARIF (push) Failing after 5m13s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo check (aarch64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo test (aarch64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo check (aarch64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo test (aarch64-unknown-linux-musl) (push) Canceled after 0s
Enable the termination feature to allow the application to handle SIGTERM signals gracefully, ensuring proper cleanup during shutdown.
2026-08-24 16:13:23 +07:00
Lochnair 46d7fdfe30 Merge pull request 'feat(app): add environment variable support for server configuration' (#129) from feature/clap-env into main
GitHub Actions Security / actionlint (push) Failing after 46s
Rust CI / cargo fmt (push) Successful in 42s
Rust CI / cargo clippy (push) Failing after 45s
Rust CI / cargo doc (push) Successful in 46s
Netem Interop / netem interop (push) Failing after 49s
GitHub Actions Security / zizmor (push) Failing after 3m37s
Supply chain / cargo-deny advisories (push) Failing after 8s
Proto Fuzz Smoke / cargo fuzz smoke (push) Successful in 2m5s
Supply chain / cargo-deny bans licenses sources (push) Failing after 8s
Supply chain / actionlint (push) Failing after 46s
Clippy SARIF / Clippy SARIF (push) Failing after 5m22s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo check (aarch64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo test (aarch64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo check (aarch64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo test (aarch64-unknown-linux-musl) (push) Canceled after 0s
Reviewed-on: Lochnair/irtt-rs#129
2026-08-24 08:56:47 +00:00
Nils Andreas Svee 8602f73728 feat(app): add environment variable support for server configuration
GitHub Actions Security / actionlint (pull_request) Failing after 46s
Rust CI / cargo fmt (pull_request) Successful in 36s
Rust CI / cargo clippy (pull_request) Failing after 41s
Rust CI / cargo doc (pull_request) Successful in 46s
Netem Interop / netem interop (pull_request) Failing after 40s
Supply chain / cargo-deny advisories (pull_request) Failing after 8s
Supply chain / cargo-deny bans licenses sources (pull_request) Failing after 8s
GitHub Actions Security / zizmor (pull_request) Failing after 3m46s
Proto Fuzz Smoke / cargo fuzz smoke (pull_request) Successful in 2m0s
Supply chain / actionlint (pull_request) Failing after 45s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (pull_request) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (pull_request) Canceled after 0s
Rust CI / cargo check (aarch64-unknown-linux-gnu) (pull_request) Canceled after 0s
Rust CI / cargo test (aarch64-unknown-linux-gnu) (pull_request) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-musl) (pull_request) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-musl) (pull_request) Canceled after 0s
Rust CI / cargo check (aarch64-unknown-linux-musl) (pull_request) Canceled after 0s
Rust CI / cargo test (aarch64-unknown-linux-musl) (pull_request) Canceled after 0s
Release / plan (pull_request) Canceled after 0s
Release / build-local-artifacts (${{ join(matrix.targets, ', ') }}) (pull_request) Canceled after 0s
Release / build-global-artifacts (pull_request) Canceled after 0s
Release / host (pull_request) Canceled after 0s
Release / announce (pull_request) Canceled after 0s
Enable configuration via IRTT_SERVER_* environment variables to simplify deployments in containerized or orchestrated environments. This adds the clap "env" feature and includes thread-safe tests to verify variable precedence.
2026-08-24 15:47:09 +07:00
13 changed files with 373 additions and 55 deletions

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
name: Publish irtt-server image
on:
push:
tags:
# This repo tags each crate independently (irtt-proto/vX, irtt-client/vX,
# ...); only the app package's tag corresponds to this image.
- "irtt-rs/v*"
workflow_dispatch:
env:
IMAGE_NAME: irtt-server
IMAGE_OWNER: lochnair
GITEA_REGISTRY: git.lochnair.net
GHCR_REGISTRY: ghcr.io
# Pinned so a floating "latest" release doesn't silently change the build.
CRANE_VERSION: v0.20.2
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Determine image tags
id: meta
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
version="${GITHUB_REF_NAME#irtt-rs/v}"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "tags=${version},latest" >> "$GITHUB_OUTPUT"
else
# Manual dispatch off a branch: tag by commit only, never touch
# "latest", so an ad-hoc build can't clobber the last release.
version="sha-$(git rev-parse --short HEAD)"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "tags=${version}" >> "$GITHUB_OUTPUT"
fi
# Cross-compilation is handled inside the Dockerfile via tonistiigi/xx
# (native builds, no emulation), but buildx still needs a driver that
# can assemble a multi-platform manifest list.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.GITEA_REGISTRY }}
# The automatic per-job token comes back unauthorized for package
# writes on this instance, so use a PAT (scope: write:package) instead.
# (Secret names can't start with GITEA_/GITHUB_, hence REGISTRY_*.)
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ secrets.GHCR_USERNAME }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Build tag list
id: tags
run: |
IFS=',' read -ra tag_list <<< "${{ steps.meta.outputs.tags }}"
gitea_tags=""
for t in "${tag_list[@]}"; do
gitea_tags="${gitea_tags}${GITEA_REGISTRY}/${IMAGE_OWNER}/${IMAGE_NAME}:${t},"
done
echo "gitea=${gitea_tags%,}" >> "$GITHUB_OUTPUT"
# Gitea is the primary publish target: this is the only build. Every
# other registry gets an exact digest copy of this same image (see
# below), never a separate build, so the hash matches everywhere.
- name: Build and push to Gitea
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.tags.outputs.gitea }}
- name: Install crane
run: |
mkdir -p "${HOME}/.local/bin"
curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" \
| tar -xz -C "${HOME}/.local/bin" crane
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
- name: Duplicate image to GHCR (same digest, no rebuild)
run: |
source_ref="${GITEA_REGISTRY}/${IMAGE_OWNER}/${IMAGE_NAME}@${{ steps.build.outputs.digest }}"
IFS=',' read -ra tag_list <<< "${{ steps.meta.outputs.tags }}"
for t in "${tag_list[@]}"; do
crane copy "${source_ref}" "${GHCR_REGISTRY}/${IMAGE_OWNER}/${IMAGE_NAME}:${t}"
done
+17
View File
@@ -50,6 +50,12 @@ release. The format is loosely based on
## irtt-server ## irtt-server
### 0.5.2
#### Changed
- `parse_hex_pattern`'s chunking now uses the stable `slice::as_chunks` API instead of `chunks_exact`; no behavior change.
### 0.5.1 ### 0.5.1
#### Added #### Added
@@ -96,6 +102,17 @@ release. The format is loosely based on
## irtt-rs ## irtt-rs
### 0.6.2
#### Added
- Every `irtt-server` applet flag can now also be set via an `IRTT_SERVER_*` environment variable (`IRTT_SERVER_BIND`, `IRTT_SERVER_HMAC`, `IRTT_SERVER_MAX_SESSIONS`, `IRTT_SERVER_MAX_PACKET_LENGTH`, `IRTT_SERVER_MIN_INTERVAL`, `IRTT_SERVER_BURST`, `IRTT_SERVER_IDLE_TIMEOUT`, `IRTT_SERVER_MAX_DURATION`, `IRTT_SERVER_TIMESTAMP_ALLOWANCE`, `IRTT_SERVER_NO_DSCP`), for deployments that configure by environment (containers, orchestrators) instead of command-line flags. An explicit flag on the command line still overrides the corresponding variable.
- A `Dockerfile` and Gitea Actions workflow publishing a minimal, statically linked `irtt-server` container image (cross-compiled for `amd64`/`arm64` via `tonistiigi/xx`, no QEMU) to this project's Gitea and GHCR container registries.
#### Changed
- The bundled `ctrlc` dependency now enables its `termination` feature, so the applet also handles `SIGTERM` (not just `SIGINT`) for graceful shutdown.
### 0.6.1 ### 0.6.1
#### Added #### Added
Generated
+28 -28
View File
@@ -242,7 +242,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.3", "syn 3.0.4",
] ]
[[package]] [[package]]
@@ -385,9 +385,9 @@ dependencies = [
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.24.0" version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"darling_macro", "darling_macro",
@@ -395,26 +395,26 @@ dependencies = [
[[package]] [[package]]
name = "darling_core" name = "darling_core"
version = "0.24.0" version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
dependencies = [ dependencies = [
"ident_case", "ident_case",
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 3.0.3", "syn 3.0.4",
] ]
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.24.0" version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 3.0.3", "syn 3.0.4",
] ]
[[package]] [[package]]
@@ -496,9 +496,9 @@ dependencies = [
[[package]] [[package]]
name = "either" name = "either"
version = "1.17.0" version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
@@ -710,7 +710,7 @@ dependencies = [
"indoc", "indoc",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.3", "syn 3.0.4",
] ]
[[package]] [[package]]
@@ -749,7 +749,7 @@ dependencies = [
[[package]] [[package]]
name = "irtt-rs" name = "irtt-rs"
version = "0.6.1" version = "0.6.2"
dependencies = [ dependencies = [
"clap", "clap",
"crossterm", "crossterm",
@@ -764,7 +764,7 @@ dependencies = [
[[package]] [[package]]
name = "irtt-server" name = "irtt-server"
version = "0.5.1" version = "0.5.2"
dependencies = [ dependencies = [
"getrandom 0.4.3", "getrandom 0.4.3",
"irtt-proto", "irtt-proto",
@@ -881,15 +881,15 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.33" version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]] [[package]]
name = "lru" name = "lru"
version = "0.18.2" version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c"
dependencies = [ dependencies = [
"hashbrown 0.17.1", "hashbrown 0.17.1",
] ]
@@ -1185,7 +1185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [ dependencies = [
"phf_shared", "phf_shared",
"rand 0.8.7", "rand 0.8.8",
] ]
[[package]] [[package]]
@@ -1294,9 +1294,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.7" version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
dependencies = [ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
] ]
@@ -1569,7 +1569,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.3", "syn 3.0.4",
] ]
[[package]] [[package]]
@@ -1705,9 +1705,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "3.0.3" version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1840,7 +1840,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.3", "syn 3.0.4",
] ]
[[package]] [[package]]
@@ -1886,7 +1886,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.3", "syn 3.0.4",
] ]
[[package]] [[package]]
@@ -1944,9 +1944,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.24.1" version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
dependencies = [ dependencies = [
"atomic", "atomic",
"getrandom 0.4.3", "getrandom 0.4.3",
+1 -1
View File
@@ -19,7 +19,7 @@ homepage = "https://github.com/Lochnair/irtt-rs"
[workspace.dependencies] [workspace.dependencies]
irtt-proto = { version = "0.5.0", path = "crates/irtt-proto" } irtt-proto = { version = "0.5.0", path = "crates/irtt-proto" }
irtt-client = { version = "0.5.0", path = "crates/irtt-client" } irtt-client = { version = "0.5.0", path = "crates/irtt-client" }
irtt-server = { version = "0.5.1", path = "crates/irtt-server" } irtt-server = { version = "0.5.2", path = "crates/irtt-server" }
irtt-stats = { version = "0.5.0", path = "crates/irtt-stats" } irtt-stats = { version = "0.5.0", path = "crates/irtt-stats" }
[profile.release] [profile.release]
+42
View File
@@ -0,0 +1,42 @@
# syntax=docker/dockerfile:1
# Builds the irtt-server applet only, statically linked against musl, and
# ships it in a scratch image with no shell or libc.
#
# Cross-compiles for TARGETPLATFORM using tonistiigi/xx: the builder stage
# always runs natively on BUILDPLATFORM (no QEMU emulation) and xx-cargo
# picks the right musl target triple, sysroot, and linker for whatever
# platform buildx asked for.
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
FROM --platform=$BUILDPLATFORM rust:1.88-alpine AS builder
COPY --from=xx / /
RUN apk add --no-cache clang lld file
ARG TARGETPLATFORM
RUN xx-apk add --no-cache musl-dev
WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY crates ./crates
RUN xx-cargo build \
--release \
--locked \
-p irtt-rs \
--bin irtt-server \
--no-default-features \
--features server \
--target-dir /build/target \
&& xx-verify /build/target/$(xx-cargo --print-target-triple)/release/irtt-server \
&& cp /build/target/$(xx-cargo --print-target-triple)/release/irtt-server /irtt-server
FROM scratch
COPY --from=builder /irtt-server /irtt-server
ENTRYPOINT ["/irtt-server"]
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "irtt-rs" name = "irtt-rs"
version = "0.6.1" version = "0.6.2"
description = "IRTT-compatible client, TUI, and server applications" description = "IRTT-compatible client, TUI, and server applications"
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -47,9 +47,9 @@ server = ["dep:irtt-server", "dep:tokio"]
tui =["dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats", "dep:ratatui", "dep:crossterm"] tui =["dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats", "dep:ratatui", "dep:crossterm"]
[dependencies] [dependencies]
clap = { version = "4.6.6", features = ["derive"] } clap = { version = "4.6.6", features = ["derive", "env"] }
crossterm = { version = "0.29.0", optional = true } crossterm = { version = "0.29.0", optional = true }
ctrlc = "3.5.2" ctrlc = { version = "3.5.2", features = ["termination"] }
irtt-client = { workspace = true, optional = true } irtt-client = { workspace = true, optional = true }
irtt-proto = { workspace = true, optional = true } irtt-proto = { workspace = true, optional = true }
irtt-server = { workspace = true, optional = true } irtt-server = { workspace = true, optional = true }
+124 -5
View File
@@ -23,7 +23,8 @@ pub struct ServerArgs {
#[arg( #[arg(
long, long,
value_name = "ADDR", value_name = "ADDR",
long_help = "Local UDP address to bind, as ADDR:PORT, for example 127.0.0.1:2112 or [::1]:2112. Host names are not resolved.\n\nRepeat the option to serve several addresses from one process, in the order given. Every listener applies the same policy options, but each keeps its own sessions and tokens, so a session belongs to the address it was opened on. Binding is all or nothing: if any address cannot be bound, none are served.\n\nWith no --bind at all, the server binds the wildcard IRTT port on both address families, [::]:2112 then 0.0.0.0:2112, on platforms with wildcard reply-source support; elsewhere it fails and asks for an explicit address instead of guessing one. Any explicit --bind replaces that default pair rather than adding to it.\n\nA wildcard bind such as 0.0.0.0:2112 answers each request from the address it was sent to, using per-packet destination metadata. That is supported on Linux, macOS and FreeBSD; on other systems a wildcard bind is refused and an explicit interface address is required.\n\nA port of 0 selects an unused port per listener, so two such binds get two different ports." env = "IRTT_SERVER_BIND",
long_help = "Local UDP address to bind, as ADDR:PORT, for example 127.0.0.1:2112 or [::1]:2112. Host names are not resolved.\n\nRepeat the option to serve several addresses from one process, in the order given. Every listener applies the same policy options, but each keeps its own sessions and tokens, so a session belongs to the address it was opened on. Binding is all or nothing: if any address cannot be bound, none are served.\n\nWith no --bind at all, the server binds the wildcard IRTT port on both address families, [::]:2112 then 0.0.0.0:2112, on platforms with wildcard reply-source support; elsewhere it fails and asks for an explicit address instead of guessing one. Any explicit --bind replaces that default pair rather than adding to it.\n\nA wildcard bind such as 0.0.0.0:2112 answers each request from the address it was sent to, using per-packet destination metadata. That is supported on Linux, macOS and FreeBSD; on other systems a wildcard bind is refused and an explicit interface address is required.\n\nA port of 0 selects an unused port per listener, so two such binds get two different ports.\n\nIRTT_SERVER_BIND sets the same thing for deployments that configure by environment (systemd, Docker, ...), and takes exactly one address, the same as one --bind. Leave it unset on ordinary hosts: the wildcard default above is already what you want there. A --bind on the command line overrides it entirely."
)] )]
pub bind: Vec<SocketAddr>, pub bind: Vec<SocketAddr>,
@@ -32,7 +33,8 @@ pub struct ServerArgs {
long, long,
value_name = "KEY", value_name = "KEY",
value_parser = parse_hmac_key, value_parser = parse_hmac_key,
long_help = "HMAC key, taken as the UTF-8 bytes of this argument.\n\nAuthentication is global: with a key configured, every request must carry a valid MAC and every reply is authenticated. Without one, authenticated requests are dropped. The key is visible in the process arguments." env = "IRTT_SERVER_HMAC",
long_help = "HMAC key, taken as the UTF-8 bytes of this argument.\n\nAuthentication is global: with a key configured, every request must carry a valid MAC and every reply is authenticated. Without one, authenticated requests are dropped. The key is visible in the process arguments, or in the environment as IRTT_SERVER_HMAC."
)] )]
pub hmac: Option<String>, pub hmac: Option<String>,
@@ -40,6 +42,7 @@ pub struct ServerArgs {
#[arg( #[arg(
long, long,
value_name = "COUNT", value_name = "COUNT",
env = "IRTT_SERVER_MAX_SESSIONS",
long_help = "Maximum number of simultaneously live sessions per listener. Once a listener's table is full, a session-creating open to it is dropped silently; nothing is evicted. Zero refuses every session-creating open.\n\nThis is a per-listener bound, not a process-wide one: with two --bind options, each listener admits up to this many sessions." long_help = "Maximum number of simultaneously live sessions per listener. Once a listener's table is full, a session-creating open to it is dropped silently; nothing is evicted. Zero refuses every session-creating open.\n\nThis is a per-listener bound, not a process-wide one: with two --bind options, each listener admits up to this many sessions."
)] )]
pub max_sessions: Option<usize>, pub max_sessions: Option<usize>,
@@ -48,6 +51,7 @@ pub struct ServerArgs {
#[arg( #[arg(
long, long,
value_name = "BYTES", value_name = "BYTES",
env = "IRTT_SERVER_MAX_PACKET_LENGTH",
long_help = "Maximum echo datagram size a session may negotiate, in bytes. A longer requested length is reduced during negotiation, an open whose mandatory field block would not fit is refused, and inbound echo requests are admitted against the same limit. This is a resource bound, not an MTU." long_help = "Maximum echo datagram size a session may negotiate, in bytes. A longer requested length is reduced during negotiation, an open whose mandatory field block would not fit is refused, and inbound echo requests are admitted against the same limit. This is a resource bound, not an MTU."
)] )]
pub max_packet_length: Option<usize>, pub max_packet_length: Option<usize>,
@@ -57,6 +61,7 @@ pub struct ServerArgs {
long, long,
value_name = "DURATION", value_name = "DURATION",
value_parser = parse_duration_allow_zero, value_parser = parse_duration_allow_zero,
env = "IRTT_SERVER_MIN_INTERVAL",
long_help = "Floor on the send interval a session may negotiate. The negotiated interval is still capped at a quarter of the idle timeout afterwards, so a value above that cap is not what actually gets negotiated.\n\nA session's reply allowance refills at the shorter of this floor and the interval it actually negotiated. Ordinarily that is this floor itself; it is the shorter, negotiated value only where the idle-timeout cap above pulled the negotiated interval below it. Use 0 for no time-based throttling." long_help = "Floor on the send interval a session may negotiate. The negotiated interval is still capped at a quarter of the idle timeout afterwards, so a value above that cap is not what actually gets negotiated.\n\nA session's reply allowance refills at the shorter of this floor and the interval it actually negotiated. Ordinarily that is this floor itself; it is the shorter, negotiated value only where the idle-timeout cap above pulled the negotiated interval below it. Use 0 for no time-based throttling."
)] )]
pub min_interval: Option<Duration>, pub min_interval: Option<Duration>,
@@ -65,6 +70,7 @@ pub struct ServerArgs {
#[arg( #[arg(
long, long,
value_name = "COUNT", value_name = "COUNT",
env = "IRTT_SERVER_BURST",
long_help = "Echo replies one session may have answered before its allowance has to refill. Use 0 for no allowance at all, which rate-limits every echo request whatever the interval is." long_help = "Echo replies one session may have answered before its allowance has to refill. Use 0 for no allowance at all, which rate-limits every echo request whatever the interval is."
)] )]
pub burst: Option<u32>, pub burst: Option<u32>,
@@ -74,6 +80,7 @@ pub struct ServerArgs {
long, long,
value_name = "DURATION", value_name = "DURATION",
value_parser = parse_duration_allow_zero, value_parser = parse_duration_allow_zero,
env = "IRTT_SERVER_IDLE_TIMEOUT",
long_help = "Release a session after this long without a served or rate-limited echo request. The deadline runs from the open, and release is silent. Use 0 to expire a session at the next evaluation; it is not a way to disable expiry." long_help = "Release a session after this long without a served or rate-limited echo request. The deadline runs from the open, and release is silent. Use 0 to expire a session at the next evaluation; it is not a way to disable expiry."
)] )]
pub idle_timeout: Option<Duration>, pub idle_timeout: Option<Duration>,
@@ -83,6 +90,7 @@ pub struct ServerArgs {
long, long,
value_name = "DURATION", value_name = "DURATION",
value_parser = parse_positive_duration, value_parser = parse_positive_duration,
env = "IRTT_SERVER_MAX_DURATION",
long_help = "Maximum test duration a session may negotiate. A longer request is reduced to it, and a continuous request is answered with it. Omit this flag for no maximum; a maximum of zero cannot be expressed, because a negotiated duration of zero means continuous." long_help = "Maximum test duration a session may negotiate. A longer request is reduced to it, and a continuous request is answered with it. Omit this flag for no maximum; a maximum of zero cannot be expressed, because a negotiated duration of zero means continuous."
)] )]
pub max_duration: Option<Duration>, pub max_duration: Option<Duration>,
@@ -92,6 +100,7 @@ pub struct ServerArgs {
long, long,
value_name = "MODE", value_name = "MODE",
value_enum, value_enum,
env = "IRTT_SERVER_TIMESTAMP_ALLOWANCE",
long_help = "How many timestamps this server will provide.\n\ndual: honor a request for send, receive, both or midpoint timestamps.\nsingle: provide at most one timestamp instant; a request for both is negotiated to midpoint.\nnone: provide no timestamps at all.\n\nThe requested clock source is never changed, only which instants are reported, so a single instant is still reported once per requested clock. Omit this flag to honor every requested placement." long_help = "How many timestamps this server will provide.\n\ndual: honor a request for send, receive, both or midpoint timestamps.\nsingle: provide at most one timestamp instant; a request for both is negotiated to midpoint.\nnone: provide no timestamps at all.\n\nThe requested clock source is never changed, only which instants are reported, so a single instant is still reported once per requested clock. Omit this flag to honor every requested placement."
)] )]
pub timestamp_allowance: Option<TimestampAllowanceArg>, pub timestamp_allowance: Option<TimestampAllowanceArg>,
@@ -99,6 +108,7 @@ pub struct ServerArgs {
/// Refuse to provide requested DSCP marking. /// Refuse to provide requested DSCP marking.
#[arg( #[arg(
long, long,
env = "IRTT_SERVER_NO_DSCP",
long_help = "Refuse to provide requested traffic-class marking. Any requested DSCP is negotiated to zero, so the client is told its echo replies will be unmarked, and they are sent unmarked. The session is not refused." long_help = "Refuse to provide requested traffic-class marking. Any requested DSCP is negotiated to zero, so the client is told its echo replies will be unmarked, and they are sent unmarked. The session is not refused."
)] )]
pub no_dscp: bool, pub no_dscp: bool,
@@ -241,11 +251,14 @@ mod tests {
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
use super::*; use super::*;
use crate::cmd::server::env_lock::with_env_lock;
fn parse(args: &[&str]) -> Result<ServerArgs, clap::Error> { fn parse(args: &[&str]) -> Result<ServerArgs, clap::Error> {
let mut argv = vec!["irtt-server"]; with_env_lock(|| {
argv.extend_from_slice(args); let mut argv = vec!["irtt-server"];
ServerArgs::try_parse_from(argv) argv.extend_from_slice(args);
ServerArgs::try_parse_from(argv)
})
} }
fn bound(args: &[&str]) -> ServerArgs { fn bound(args: &[&str]) -> ServerArgs {
@@ -479,4 +492,110 @@ mod tests {
assert!(parse(&["--bind", "127.0.0.1:2112", "--max-duration", "0s"]).is_err()); assert!(parse(&["--bind", "127.0.0.1:2112", "--max-duration", "0s"]).is_err());
assert_eq!(bound(&[]).server_config().max_test_duration(), None); assert_eq!(bound(&[]).server_config().max_test_duration(), None);
} }
/// Runs `body` with the given environment variables set, restoring
/// whatever was there before (present or absent) once `body` returns.
/// Held under `with_env_lock` for the whole span, including `body`, so a
/// `parse` call elsewhere never observes a partially-applied environment.
fn with_env<T>(vars: &[(&str, &str)], body: impl FnOnce() -> T) -> T {
with_env_lock(|| {
let previous: Vec<(&str, Option<String>)> = vars
.iter()
.map(|(k, _)| (*k, std::env::var(k).ok()))
.collect();
for (k, v) in vars {
std::env::set_var(k, v);
}
let result = body();
for (k, v) in previous {
match v {
Some(v) => std::env::set_var(k, v),
None => std::env::remove_var(k),
}
}
result
})
}
#[test]
fn environment_variables_supply_policy_when_no_flag_is_given() {
with_env(
&[
("IRTT_SERVER_MAX_SESSIONS", "512"),
("IRTT_SERVER_MAX_PACKET_LENGTH", "1472"),
("IRTT_SERVER_MIN_INTERVAL", "20ms"),
("IRTT_SERVER_BURST", "3"),
("IRTT_SERVER_IDLE_TIMEOUT", "30s"),
("IRTT_SERVER_MAX_DURATION", "2m"),
("IRTT_SERVER_TIMESTAMP_ALLOWANCE", "single"),
("IRTT_SERVER_HMAC", "secret"),
("IRTT_SERVER_NO_DSCP", "true"),
],
|| {
let config = parse(&["--bind", "127.0.0.1:2112"])
.unwrap()
.server_config();
assert_eq!(config.max_sessions(), 512);
assert_eq!(config.max_packet_length(), 1472);
assert_eq!(config.min_send_interval(), Duration::from_millis(20));
assert_eq!(config.burst_allowance(), 3);
assert_eq!(config.idle_timeout(), Duration::from_secs(30));
assert_eq!(config.max_test_duration(), Some(Duration::from_secs(120)));
assert_eq!(config.timestamp_allowance(), TimestampAllowance::Single);
assert_eq!(config.hmac_key(), Some(b"secret".as_slice()));
assert!(!config.dscp_allowed());
},
);
}
#[test]
fn an_explicit_flag_overrides_the_environment() {
with_env(&[("IRTT_SERVER_MAX_SESSIONS", "512")], || {
let config = parse(&["--bind", "127.0.0.1:2112", "--max-sessions", "4"])
.unwrap()
.server_config();
assert_eq!(
config.max_sessions(),
4,
"the command-line value wins over the environment"
);
});
}
#[test]
fn the_environment_wins_over_the_built_in_default() {
with_env(&[("IRTT_SERVER_BURST", "9")], || {
let config = bound(&[]).server_config();
assert_eq!(
config.burst_allowance(),
9,
"an unset flag still falls back to the environment before the library default"
);
});
}
#[test]
fn irtt_server_bind_supplies_a_single_address_from_the_environment() {
with_env(&[("IRTT_SERVER_BIND", "127.0.0.1:2113")], || {
let args = parse(&[]).unwrap();
assert_eq!(args.bind, [SocketAddr::from((Ipv4Addr::LOCALHOST, 2113))]);
});
}
#[test]
fn an_explicit_bind_flag_overrides_irtt_server_bind() {
with_env(&[("IRTT_SERVER_BIND", "127.0.0.1:2113")], || {
let args = parse(&["--bind", "[::1]:2112"]).unwrap();
assert_eq!(args.bind, [SocketAddr::from((Ipv6Addr::LOCALHOST, 2112))]);
});
}
#[test]
fn leaving_irtt_server_bind_unset_keeps_the_wildcard_default() {
let args = parse(&[]).unwrap();
assert!(
args.bind.is_empty(),
"with no --bind and no IRTT_SERVER_BIND, resolve_binds still falls back to the wildcard pair"
);
}
} }
@@ -0,0 +1,30 @@
//! Serializes every test in this module tree that touches process
//! environment variables.
//!
//! `ServerArgs` parsing reads `IRTT_SERVER_*` via `clap`'s `env` support, and
//! `std::env::set_var`/`remove_var` mutate genuinely process-wide state.
//! Every test that parses `ServerArgs` — whether or not it sets an env var
//! itself — must go through [`with_env_lock`], or a parse in one test thread
//! can observe a value another thread's test set only for the duration of
//! its own assertions. Reentrant per-thread, since a test that sets env vars
//! typically calls `parse` again from inside its own locked section.
use std::{cell::Cell, sync::Mutex};
static ENV_LOCK: Mutex<()> = Mutex::new(());
thread_local! {
static ENV_LOCK_HELD: Cell<bool> = const { Cell::new(false) };
}
pub(crate) fn with_env_lock<T>(f: impl FnOnce() -> T) -> T {
if ENV_LOCK_HELD.with(Cell::get) {
return f();
}
let _guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
ENV_LOCK_HELD.with(|held| held.set(true));
let result = f();
ENV_LOCK_HELD.with(|held| held.set(false));
result
}
+2
View File
@@ -1,4 +1,6 @@
pub mod args; pub mod args;
#[cfg(test)]
pub(crate) mod env_lock;
pub mod run; pub mod run;
pub use args::ServerArgs; pub use args::ServerArgs;
+19 -12
View File
@@ -111,12 +111,15 @@ mod tests {
use clap::Parser; use clap::Parser;
use super::*; use super::*;
use crate::cmd::server::env_lock::with_env_lock;
/// The ordinary invocation, and the one that keeps the single-listener case /// The ordinary invocation, and the one that keeps the single-listener case
/// on the same orchestration path as every other. /// on the same orchestration path as every other.
#[test] #[test]
fn an_already_requested_shutdown_binds_one_listener_and_returns() { fn an_already_requested_shutdown_binds_one_listener_and_returns() {
let args = ServerArgs::try_parse_from(["irtt-server", "--bind", "127.0.0.1:0"]).unwrap(); let args = with_env_lock(|| {
ServerArgs::try_parse_from(["irtt-server", "--bind", "127.0.0.1:0"]).unwrap()
});
let shutdown_requested = AtomicBool::new(true); let shutdown_requested = AtomicBool::new(true);
run_server(args, &shutdown_requested).unwrap(); run_server(args, &shutdown_requested).unwrap();
@@ -124,23 +127,27 @@ mod tests {
#[test] #[test]
fn an_already_requested_shutdown_binds_every_listener_and_returns() { fn an_already_requested_shutdown_binds_every_listener_and_returns() {
let args = ServerArgs::try_parse_from([ let args = with_env_lock(|| {
"irtt-server", ServerArgs::try_parse_from([
"--bind", "irtt-server",
"127.0.0.1:0", "--bind",
"--bind", "127.0.0.1:0",
"127.0.0.1:0", "--bind",
]) "127.0.0.1:0",
.unwrap(); ])
.unwrap()
});
let shutdown_requested = AtomicBool::new(true); let shutdown_requested = AtomicBool::new(true);
run_server(args, &shutdown_requested).unwrap(); run_server(args, &shutdown_requested).unwrap();
} }
fn default_binds() -> Vec<SocketAddr> { fn default_binds() -> Vec<SocketAddr> {
ServerArgs::try_parse_from(["irtt-server"]) with_env_lock(|| {
.unwrap() ServerArgs::try_parse_from(["irtt-server"])
.resolve_binds() .unwrap()
.resolve_binds()
})
} }
#[test] #[test]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "irtt-server" name = "irtt-server"
version = "0.5.1" version = "0.5.2"
description = "IRTT-compatible Rust server library" description = "IRTT-compatible Rust server library"
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
+3 -1
View File
@@ -115,7 +115,9 @@ fn parse_hex_pattern(body: &str) -> Option<Vec<u8>> {
return None; return None;
} }
digits digits
.chunks_exact(2) .as_chunks::<2>()
.0
.iter()
.map(|pair| Some(hex_digit(pair[0])? << 4 | hex_digit(pair[1])?)) .map(|pair| Some(hex_digit(pair[0])? << 4 | hex_digit(pair[1])?))
.collect() .collect()
} }
+4 -4
View File
@@ -19,9 +19,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.4.3" version = "1.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -204,9 +204,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "3.0.3" version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",