mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
Add Nym mixnet transport and single-container demo
Add an outbound-only Nym mixnet transport that tunnels FMP peer links through a local nym-socks5-client SOCKS5 proxy into the Nym mixnet. It structurally mirrors the Tor SOCKS5 transport (connection pool, connect-on-send background promotion, FMP-v0 framing reused from TCP) with the onion, inbound-listener, and control-port machinery removed. Wires the transport through the full TransportHandle dispatch, NymConfig (standard transport-instance pattern), and node instantiation, and surfaces its counters in fipstop. Includes a mock SOCKS5 harness and unit coverage for the address-parsing paths. Also adds an isolated single-container example (examples/sidecar-nostr-mixnet-relay/) demonstrating FIPS peering across the mixnet end to end. No new crate dependencies: tokio_socks, socket2, and futures are already pulled in by the Tor transport.
This commit is contained in:
committed by
Johnathan Corgan
parent
fb8bb4fb97
commit
4e43cb81e9
@@ -16,6 +16,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Nym mixnet transport (`transports.nym`) for outbound peer links
|
||||
tunneled through a local `nym-socks5-client` SOCKS5 proxy into the
|
||||
Nym mixnet, as a privacy transport alongside Tor. Outbound-only and
|
||||
not platform-gated, it reuses the existing FMP framing and adds no new
|
||||
crate dependencies. A single-container example
|
||||
(`examples/sidecar-nostr-mixnet-relay/`) demonstrates FIPS peering
|
||||
across the mixnet end to end.
|
||||
- Typed `RejectReason` classification for receive-path silent-rejection
|
||||
sites across the node. Each rejection-and-return path now passes a
|
||||
typed reason to `NodeStats::record_reject`, which routes it to a
|
||||
|
||||
@@ -169,6 +169,12 @@ and [testing/README.md](testing/README.md).
|
||||
reachable exclusively over the FIPS mesh. The relay container
|
||||
shares the FIPS sidecar's network namespace and is isolated from
|
||||
the host network.
|
||||
- **[examples/sidecar-nostr-mixnet-relay/](examples/sidecar-nostr-mixnet-relay/)** —
|
||||
Single-container demo of FIPS peering through a **mixnet**
|
||||
(implemented with [Nym](https://nym.com/)): the FIPS daemon, the mixnet
|
||||
proxy, and a strfry Nostr relay all in one isolated container, with
|
||||
the direct route to the peer firewalled off so traffic provably
|
||||
crosses the mixnet.
|
||||
- **[examples/k8s-sidecar/](examples/k8s-sidecar/)** — Run FIPS as
|
||||
a Kubernetes Pod sidecar. The sidecar creates `fips0` in the
|
||||
Pod's shared network namespace so every other container in the
|
||||
|
||||
37
examples/sidecar-nostr-mixnet-relay/.env
Normal file
37
examples/sidecar-nostr-mixnet-relay/.env
Normal file
@@ -0,0 +1,37 @@
|
||||
# FIPS-over-Nym-mixnet demo configuration.
|
||||
# Override these values or create a .env.local file.
|
||||
|
||||
# Node identity — generate with: fipsctl keygen
|
||||
# Must be set before running: export FIPS_NSEC=<your-nsec>
|
||||
FIPS_NSEC=
|
||||
|
||||
# Peer configuration (leave FIPS_PEER_NPUB empty for standalone operation).
|
||||
# The peer MUST expose a TCP endpoint in nym mode — the Nym SOCKS5 proxy
|
||||
# tunnels TCP streams. Find more public peers at https://join.fips.network/
|
||||
# Default peer test-us03 exposes tcp:54.183.70.180:443 and udp:…:2121.
|
||||
# For udp mode (see FIPS_PEER_TRANSPORT below) change FIPS_PEER_ADDR to
|
||||
# 54.183.70.180:2121.
|
||||
# The alias doubles as the peer's .fips hostname (<alias>.fips), so it
|
||||
# must be a plain hostname label — no dots.
|
||||
FIPS_PEER_NPUB=npub136yqae6na688fs75g95ppps3lxe07fvxefj77938zf47uhm6074sxw8ctm
|
||||
FIPS_PEER_ADDR=54.183.70.180:443
|
||||
FIPS_PEER_ALIAS=test-us03
|
||||
|
||||
# Transport — THE switch that selects mixnet vs. direct: nym | tcp | udp
|
||||
# nym : peer traffic goes through the Nym mixnet via the in-container
|
||||
# nym-socks5-client (started automatically, before FIPS). DEFAULT.
|
||||
# tcp : direct TCP to FIPS_PEER_ADDR; the nym client is NOT started.
|
||||
# udp : direct UDP — also set FIPS_PEER_ADDR to the peer's :2121 endpoint.
|
||||
# To go back to a direct link, just set this to tcp (or udp) and re-run
|
||||
# `docker compose up`. Nothing else needs to change for tcp.
|
||||
FIPS_PEER_TRANSPORT=nym
|
||||
|
||||
# ----- Nym mixnet (only used when FIPS_PEER_TRANSPORT=nym) -----
|
||||
# Network-requester service provider. Leave empty to auto-discover the
|
||||
# best-scored provider from https://harbourmaster.nymtech.net/ at startup.
|
||||
NYM_SERVICE_PROVIDER=
|
||||
NYM_CLIENT_ID=fips-nym-client
|
||||
FIPS_NYM_SOCKS5_ADDR=127.0.0.1:1080
|
||||
|
||||
# Logging
|
||||
RUST_LOG=info
|
||||
1
examples/sidecar-nostr-mixnet-relay/.gitignore
vendored
Normal file
1
examples/sidecar-nostr-mixnet-relay/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.env.local
|
||||
151
examples/sidecar-nostr-mixnet-relay/Dockerfile
Normal file
151
examples/sidecar-nostr-mixnet-relay/Dockerfile
Normal file
@@ -0,0 +1,151 @@
|
||||
# Single-container FIPS-over-Nym-mixnet demo.
|
||||
#
|
||||
# Everything runs in ONE container: the FIPS daemon, the nym-socks5-client
|
||||
# mixnet proxy, the strfry Nostr relay, nginx, and dnsmasq. The entrypoint
|
||||
# starts them in strict order so the SOCKS5 proxy is up before FIPS dials
|
||||
# its peer through the mixnet.
|
||||
#
|
||||
# Platform: the image builds NATIVE for the host. The FIPS daemon must NOT
|
||||
# run under emulation — Rosetta mis-translates the amd64 ChaCha20-Poly1305
|
||||
# assembly (ring/BoringSSL), silently failing AEAD on larger frames (bloom
|
||||
# filter announces are the first casualty). fips is therefore always
|
||||
# compiled for the native arch.
|
||||
#
|
||||
# nym-socks5-client is the official prebuilt amd64 binary (Nym ships no
|
||||
# other arch). On amd64 hosts it runs natively; on arm64 (Apple Silicon)
|
||||
# it runs via Docker Desktop's binfmt/Rosetta handler, with the x86-64
|
||||
# loader + glibc copied in from an amd64 stage. The nym client tolerates
|
||||
# emulation; fips does not.
|
||||
|
||||
# ── Build stage: compile FIPS from source ──
|
||||
FROM rust:1.94-slim-trixie AS builder
|
||||
|
||||
# bluer (BLE) and rustables (nftables) are unconditional dependencies on
|
||||
# glibc Linux: bluer needs the dbus headers, rustables runs bindgen
|
||||
# (libclang) against the libnftnl headers.
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
pkg-config libdbus-1-dev libnftnl-dev libclang-dev clang && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build
|
||||
COPY Cargo.toml Cargo.lock rust-toolchain.toml build.rs ./
|
||||
COPY src ./src
|
||||
|
||||
RUN cargo build --release && \
|
||||
cp target/release/fips target/release/fipsctl target/release/fipstop /usr/local/bin/
|
||||
|
||||
# ── strfry stage: collect the binary and its musl runtime ──
|
||||
# The official strfry image is alpine (musl) based; the runtime stage below
|
||||
# is debian (glibc), so the musl dynamic loader and the exact set of shared
|
||||
# libraries strfry links against must come along.
|
||||
FROM ghcr.io/hoytech/strfry:latest AS strfry
|
||||
RUN mkdir -p /strfry-libs && \
|
||||
ldd /app/strfry | awk '$3 ~ /^\// {print $3}' | xargs -I{} cp {} /strfry-libs/ && \
|
||||
cp /lib/ld-musl-*.so.1 /strfry-libs/
|
||||
|
||||
# ── nym stage: official prebuilt amd64 binary + its glibc runtime ──
|
||||
# Pinned amd64-only stage regardless of host arch. Collects the x86-64
|
||||
# dynamic loader and the binary's library closure so the binary can run
|
||||
# inside the native (possibly arm64) runtime image via binfmt/Rosetta.
|
||||
FROM --platform=linux/amd64 debian:trixie-slim AS nym
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
# Bumping the version = edit the tag in this URL.
|
||||
RUN curl -sSL --retry 3 \
|
||||
"https://github.com/nymtech/nym/releases/download/nym-binaries-v2026.11-xynomizithra/nym-socks5-client" \
|
||||
-o /nym-socks5-client && \
|
||||
chmod +x /nym-socks5-client
|
||||
RUN mkdir -p /nym-rt/lib64 /nym-rt/libs && \
|
||||
cp /lib64/ld-linux-x86-64.so.2 /nym-rt/lib64/ && \
|
||||
ldd /nym-socks5-client | awk '$3 ~ /^\// {print $3}' | xargs -I{} cp {} /nym-rt/libs/
|
||||
|
||||
# ── Runtime stage ──
|
||||
FROM debian:trixie-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
iproute2 iputils-ping dnsutils dnsmasq iptables \
|
||||
openssh-client openssh-server python3 \
|
||||
tcpdump netcat-openbsd curl jq iperf3 nginx && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install nak (Nostr Army Knife) — detect arch and download the correct binary.
|
||||
# Asset naming: nak-<version>-linux-<arch>
|
||||
RUN ARCH=$(dpkg --print-architecture) && \
|
||||
case "$ARCH" in \
|
||||
amd64) NAK_ARCH="linux-amd64" ;; \
|
||||
arm64) NAK_ARCH="linux-arm64" ;; \
|
||||
armhf) NAK_ARCH="linux-arm" ;; \
|
||||
*) echo "Unsupported arch: $ARCH" && exit 1 ;; \
|
||||
esac && \
|
||||
NAK_VERSION=$(curl -sSL --retry 3 \
|
||||
"https://api.github.com/repos/fiatjaf/nak/releases/latest" \
|
||||
| grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*/\1/') && \
|
||||
echo "Installing nak ${NAK_VERSION} for ${NAK_ARCH}" && \
|
||||
curl -sSL --retry 3 \
|
||||
"https://github.com/fiatjaf/nak/releases/download/${NAK_VERSION}/nak-${NAK_VERSION}-${NAK_ARCH}" \
|
||||
-o /usr/local/bin/nak && \
|
||||
chmod +x /usr/local/bin/nak && \
|
||||
nak --version
|
||||
|
||||
# nym-socks5-client: prebuilt amd64 binary plus its x86-64 loader/glibc.
|
||||
# On amd64 these COPYs overwrite identical files; on arm64 they add the
|
||||
# x86-64 runtime alongside the native one (paths don't collide). The
|
||||
# version check doubles as a binfmt/Rosetta smoke test on arm64 hosts.
|
||||
COPY --from=nym /nym-socks5-client /usr/local/bin/nym-socks5-client
|
||||
COPY --from=nym /nym-rt/lib64/ /lib64/
|
||||
COPY --from=nym /nym-rt/libs/ /lib/x86_64-linux-gnu/
|
||||
RUN echo "Installed:" && /usr/local/bin/nym-socks5-client --version
|
||||
|
||||
# Setup SSH server with no authentication (test only!)
|
||||
RUN mkdir -p /var/run/sshd && \
|
||||
ssh-keygen -A && \
|
||||
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
|
||||
sed -i 's/#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config && \
|
||||
sed -i 's/UsePAM yes/UsePAM no/' /etc/ssh/sshd_config && \
|
||||
passwd -d root
|
||||
|
||||
# dnsmasq: forward .fips to FIPS daemon, everything else to Docker DNS
|
||||
RUN printf '%s\n' \
|
||||
'port=53' \
|
||||
'listen-address=127.0.0.1' \
|
||||
'bind-interfaces' \
|
||||
'server=/fips/127.0.0.1#5354' \
|
||||
'server=127.0.0.11' \
|
||||
'no-resolv' \
|
||||
>> /etc/dnsmasq.conf
|
||||
|
||||
# strfry: binary, musl loader, and its libraries in a private directory.
|
||||
# /etc/ld-musl-<arch>.path tells the musl loader where to search, keeping
|
||||
# the musl libraries invisible to the system glibc loader.
|
||||
COPY --from=strfry /app/strfry /usr/local/bin/strfry
|
||||
COPY --from=strfry /strfry-libs/ /opt/strfry-libs/
|
||||
RUN mv /opt/strfry-libs/ld-musl-*.so.1 /lib/ && \
|
||||
echo "/opt/strfry-libs" > /etc/ld-musl-$(uname -m).path && \
|
||||
mkdir -p /usr/src/app/strfry-db && \
|
||||
strfry --version
|
||||
|
||||
# nginx: reverse proxy port 80 (IPv4 + IPv6) → strfry 127.0.0.1:7777
|
||||
RUN printf 'server {\n\
|
||||
listen 80;\n\
|
||||
listen [::]:80;\n\
|
||||
location / {\n\
|
||||
proxy_pass http://127.0.0.1:7777;\n\
|
||||
proxy_http_version 1.1;\n\
|
||||
proxy_read_timeout 1d;\n\
|
||||
proxy_send_timeout 1d;\n\
|
||||
proxy_set_header Upgrade $http_upgrade;\n\
|
||||
proxy_set_header Connection "Upgrade";\n\
|
||||
proxy_set_header Host $host;\n\
|
||||
}\n\
|
||||
}\n' > /etc/nginx/conf.d/nostr-relay.conf && \
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
COPY --from=builder /usr/local/bin/fips /usr/local/bin/fipsctl /usr/local/bin/fipstop /usr/local/bin/
|
||||
|
||||
COPY examples/sidecar-nostr-mixnet-relay/entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,7 @@
|
||||
target/
|
||||
target-*/
|
||||
.git/
|
||||
.github/
|
||||
testing/
|
||||
examples/
|
||||
!examples/sidecar-nostr-mixnet-relay/
|
||||
173
examples/sidecar-nostr-mixnet-relay/README.md
Normal file
173
examples/sidecar-nostr-mixnet-relay/README.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# FIPS over a Mixnet — Single-Container Demo (Nym)
|
||||
|
||||
An isolated environment demonstrating how FIPS peer traffic can travel
|
||||
through a **mixnet** — a network that hides traffic patterns by routing
|
||||
each packet through several relays with cover traffic and timing
|
||||
obfuscation. The mixnet here is [Nym](https://nym.com/), but the FIPS side
|
||||
is transport-agnostic: it just sees a SOCKS5 proxy, so any mixnet exposing
|
||||
one would slot in the same way.
|
||||
|
||||
**Everything runs in one Docker container**: the FIPS daemon, the mixnet
|
||||
proxy (`nym-socks5-client`), a [strfry](https://github.com/hoytech/strfry)
|
||||
Nostr relay behind nginx, and dnsmasq.
|
||||
|
||||
```
|
||||
┌────────────────────────── one container ───────────────────────────┐
|
||||
│ │
|
||||
│ nginx :80 ──► strfry :7777 (Nostr relay, fips0-only) │
|
||||
│ │
|
||||
│ fips daemon ── transports.nym ──► nym-socks5-client :1080 │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ Sphinx packets │
|
||||
│ fips0 (TUN, fd00::/8) Nym gateway ► 3 mix hops ► │
|
||||
│ network requester ► peer (TCP) │
|
||||
│ │
|
||||
│ iptables: direct route to the peer is DROPped — the FIPS link │
|
||||
│ can only exist through the mixnet. │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
How the pieces interlock:
|
||||
|
||||
- The FIPS **nym transport** dials peers through a local SOCKS5 proxy; the
|
||||
proxy routes each TCP stream through the mixnet (gateway → 3 mix hops →
|
||||
network requester), which performs the final TCP connection to the peer.
|
||||
The peer address must therefore be a **TCP endpoint** — find public peers
|
||||
at <https://join.fips.network/>.
|
||||
- The `nym-socks5-client` is started by the entrypoint **only when the
|
||||
generated FIPS config contains a `transports.nym` block**
|
||||
(`FIPS_PEER_TRANSPORT=nym`), and always **before** the FIPS daemon, so
|
||||
the proxy is listening by the time FIPS dials.
|
||||
- In nym mode, iptables **drops the direct route to the peer**: if the peer
|
||||
handshake completes, the traffic provably went through the mixnet.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# 1. Generate a node identity (any machine with fipsctl, or reuse one):
|
||||
fipsctl keygen
|
||||
|
||||
# 2. Put the nsec into the environment:
|
||||
export FIPS_NSEC=<your-nsec>
|
||||
|
||||
# 3. Build and run (native image; FIPS compiles for your host's arch):
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Watch the logs: the entrypoint auto-discovers a Nym service provider,
|
||||
bootstraps the SOCKS5 client (`Nym SOCKS5 proxy ready …`), and only then
|
||||
starts FIPS. After the mixnet handshake completes (can take 30–120 s):
|
||||
|
||||
```bash
|
||||
docker compose exec fips fipsctl show transports # nym transport: up
|
||||
docker compose exec fips fipsctl show peers # test-us03: active
|
||||
```
|
||||
|
||||
## Switching transport: mixnet ↔ direct (TCP/UDP)
|
||||
|
||||
The single knob is `FIPS_PEER_TRANSPORT` in `.env` (or an inline override).
|
||||
It selects how FIPS reaches the peer **and** whether the mixnet proxy runs
|
||||
at all — the two are always in sync.
|
||||
|
||||
```bash
|
||||
# Default — through the Nym mixnet (anonymized, ~1-2 s RTT):
|
||||
FIPS_PEER_TRANSPORT=nym docker compose up -d # or just `docker compose up -d`
|
||||
|
||||
# Direct TCP (no mixnet, ~50-300 ms RTT). The nym client is NOT started:
|
||||
FIPS_PEER_TRANSPORT=tcp docker compose up -d
|
||||
|
||||
# Direct UDP — also point FIPS_PEER_ADDR at the peer's UDP endpoint:
|
||||
FIPS_PEER_TRANSPORT=udp FIPS_PEER_ADDR=54.183.70.180:2121 docker compose up -d
|
||||
```
|
||||
|
||||
What changes under the hood for each value:
|
||||
|
||||
| `FIPS_PEER_TRANSPORT` | nym client | FIPS config block | peer endpoint used | direct route to peer |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `nym` (default) | started, before FIPS | `transports.nym` | `FIPS_PEER_ADDR` (TCP) via SOCKS5 | **firewalled off** |
|
||||
| `tcp` | not started | `transports.tcp` | `FIPS_PEER_ADDR` (TCP) direct | allowed |
|
||||
| `udp` | not started | `transports.udp` | `FIPS_PEER_ADDR` (UDP `:2121`) direct | allowed |
|
||||
|
||||
To **switch back to a direct link**, set the value to `tcp` (no other change)
|
||||
or `udp` (and swap `FIPS_PEER_ADDR` to the `:2121` endpoint), then re-run
|
||||
`docker compose up -d`. To **return to the mixnet**, set it back to `nym`.
|
||||
Persist your choice by editing `.env` instead of prefixing the command.
|
||||
The same node can be compared both ways — direct shows ~50-300 ms RTT,
|
||||
the mixnet ~1-2 s, which is the visible signature that traffic is routing
|
||||
through the Sphinx mix hops.
|
||||
|
||||
## Verifying the traffic really crosses the mixnet
|
||||
|
||||
```bash
|
||||
# The direct route to the peer is dropped — the only way packets reach
|
||||
# the peer is via the nym-socks5-client:
|
||||
docker compose exec fips iptables -L OUTPUT -v -n # DROP rule for peer IP
|
||||
|
||||
# Mixnet activity (Sphinx packet flow) in the nym client output:
|
||||
docker compose logs fips | grep -i nym
|
||||
|
||||
# End-to-end data plane across the mesh. FIPS addresses every node by its
|
||||
# key as <npub>.fips (each npub maps into fd00::/8); short names like
|
||||
# `test-us03` are only local aliases for the peer you configured. Pick a
|
||||
# node you are NOT directly linked to — grab a current npub from
|
||||
# https://join.fips.network/ — so the ICMPv6 echo routes over the mixnet
|
||||
# to your peer and then hop-by-hop across the mesh to the target:
|
||||
docker compose exec fips ping6 -c3 <peer-npub>.fips
|
||||
|
||||
# A reply while the direct route is DROPped proves the traffic crossed the
|
||||
# mixnet; the seconds-range RTT is the Sphinx path's signature, and a few
|
||||
# extra hundred ms over reaching your own peer is the added mesh hops (a
|
||||
# direct, non-mixnet connection would be ~30 ms).
|
||||
```
|
||||
|
||||
The Nostr relay answers only over the FIPS mesh (fd00::/8) and on the
|
||||
container's loopback — inbound eth0 traffic, including the host's port-80
|
||||
mapping, is dropped by the isolation rules. Check it from inside:
|
||||
|
||||
```bash
|
||||
docker compose exec fips curl -s -H "Accept: application/nostr+json" http://127.0.0.1/
|
||||
```
|
||||
|
||||
## Configuration (.env)
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `FIPS_NSEC` | *(required)* | Node identity, `fipsctl keygen` |
|
||||
| `FIPS_PEER_NPUB` | test-us03's npub | Peer to dial; empty = standalone |
|
||||
| `FIPS_PEER_ADDR` | `54.183.70.180:443` | **TCP** endpoint in nym/tcp mode (use `:2121` for udp) |
|
||||
| `FIPS_PEER_TRANSPORT` | `nym` | `nym` \| `tcp` \| `udp` — see "Switching transport" above |
|
||||
| `NYM_SERVICE_PROVIDER` | *(auto)* | Network requester; empty = pick the best-scored from [harbourmaster](https://harbourmaster.nymtech.net/) |
|
||||
| `NYM_CLIENT_ID` | `fips-nym-client` | Nym client identity (kept in the `nym-data` volume) |
|
||||
|
||||
With `FIPS_PEER_TRANSPORT=tcp` or `udp` the nym client is **not started at
|
||||
all** and FIPS connects directly — useful as a baseline comparison.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **`could not auto-discover a Nym service provider`** — the harbourmaster
|
||||
API was unreachable or returned no providers; pick one manually from
|
||||
<https://harbourmaster.nymtech.net/> and set `NYM_SERVICE_PROVIDER`.
|
||||
- **Slow or failing mixnet bootstrap** — service providers and gateways
|
||||
vary in quality. Delete the client state and retry with another provider:
|
||||
`docker compose down -v && NYM_SERVICE_PROVIDER=<other> docker compose up`.
|
||||
(The provider is baked into the client state at init; changing it
|
||||
requires wiping the `nym-data` volume.)
|
||||
- **Peer never becomes active** — confirm the peer's TCP endpoint is
|
||||
reachable from the open internet (the network requester dials it from
|
||||
the Nym exit side, not from your machine).
|
||||
- **Never run this image under emulation** — the image builds native for
|
||||
a reason: under Rosetta/qemu, the FIPS daemon's ChaCha20-Poly1305
|
||||
assembly (ring/BoringSSL) silently fails AEAD on larger frames; bloom
|
||||
filter announces are dropped and multi-hop routing never converges,
|
||||
while small control traffic keeps working — a maddeningly subtle
|
||||
failure mode. Only the embedded amd64 `nym-socks5-client` (Nym ships no
|
||||
other arch) runs emulated on Apple Silicon, which it tolerates.
|
||||
|
||||
## Notes
|
||||
|
||||
- The container's lifecycle follows the FIPS daemon; strfry, nginx and the
|
||||
nym client run as background processes inside the same container and are
|
||||
restarted with it (`restart: unless-stopped`).
|
||||
- SSH (port 22, no auth) and tools like `tcpdump`, `nak`, `iperf3` are
|
||||
inside the image for poking around — this is a demo image, do not expose
|
||||
it beyond your machine.
|
||||
55
examples/sidecar-nostr-mixnet-relay/docker-compose.yml
Normal file
55
examples/sidecar-nostr-mixnet-relay/docker-compose.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
networks:
|
||||
fips-net:
|
||||
name: ${FIPS_NETWORK:-fips-mixnet-net}
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: ${FIPS_SUBNET:-172.20.2.0/24}
|
||||
|
||||
services:
|
||||
# Single container running ALL services: fips daemon, nym-socks5-client,
|
||||
# strfry Nostr relay, nginx, dnsmasq. See entrypoint.sh for start order.
|
||||
fips:
|
||||
# Builds NATIVE for the host — fips must not run under emulation
|
||||
# (Rosetta breaks its AEAD on larger frames). Only the embedded
|
||||
# amd64 nym-socks5-client is emulated on Apple Silicon.
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: examples/sidecar-nostr-mixnet-relay/Dockerfile
|
||||
hostname: fips-mixnet
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
sysctls:
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "2121:2121/udp" # FIPS UDP transport
|
||||
- "8443:8443/tcp" # FIPS TCP transport
|
||||
- "80:80/tcp" # Nostr relay WebSocket (via nginx)
|
||||
environment:
|
||||
- RUST_LOG=${RUST_LOG:-info}
|
||||
- FIPS_NSEC=${FIPS_NSEC}
|
||||
- FIPS_PEER_NPUB=${FIPS_PEER_NPUB:-}
|
||||
- FIPS_PEER_ADDR=${FIPS_PEER_ADDR:-}
|
||||
- FIPS_PEER_ALIAS=${FIPS_PEER_ALIAS:-peer}
|
||||
- FIPS_PEER_TRANSPORT=${FIPS_PEER_TRANSPORT:-nym}
|
||||
- FIPS_UDP_BIND=${FIPS_UDP_BIND:-0.0.0.0:2121}
|
||||
- FIPS_TUN_MTU=${FIPS_TUN_MTU:-1280}
|
||||
- FIPS_UDP_MTU=${FIPS_UDP_MTU:-1472}
|
||||
- FIPS_NYM_SOCKS5_ADDR=${FIPS_NYM_SOCKS5_ADDR:-127.0.0.1:1080}
|
||||
- NYM_CLIENT_ID=${NYM_CLIENT_ID:-fips-nym-client}
|
||||
- NYM_SERVICE_PROVIDER=${NYM_SERVICE_PROVIDER:-}
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./relay/strfry.conf:/usr/src/app/strfry.conf:ro
|
||||
- relay-data:/usr/src/app/strfry-db
|
||||
- nym-data:/root/.nym
|
||||
networks:
|
||||
fips-net:
|
||||
ipv4_address: ${FIPS_IPV4:-172.20.2.20}
|
||||
|
||||
volumes:
|
||||
relay-data:
|
||||
nym-data:
|
||||
221
examples/sidecar-nostr-mixnet-relay/entrypoint.sh
Executable file
221
examples/sidecar-nostr-mixnet-relay/entrypoint.sh
Executable file
@@ -0,0 +1,221 @@
|
||||
#!/bin/bash
|
||||
# Single-container entrypoint: generate the FIPS config, apply iptables
|
||||
# isolation, start the Nostr relay (strfry + nginx), start the Nym SOCKS5
|
||||
# client (only when the FIPS config enables the nym transport), and launch
|
||||
# FIPS last — so the mixnet proxy is provably up before FIPS dials its peer.
|
||||
set -e
|
||||
|
||||
# --- Generate FIPS config from environment variables ---
|
||||
|
||||
FIPS_NSEC="${FIPS_NSEC:?FIPS_NSEC is required}"
|
||||
FIPS_UDP_BIND="${FIPS_UDP_BIND:-0.0.0.0:2121}"
|
||||
FIPS_TCP_BIND="${FIPS_TCP_BIND:-0.0.0.0:8443}"
|
||||
FIPS_TUN_MTU="${FIPS_TUN_MTU:-1280}"
|
||||
FIPS_UDP_MTU="${FIPS_UDP_MTU:-1472}"
|
||||
FIPS_PEER_TRANSPORT="${FIPS_PEER_TRANSPORT:-nym}"
|
||||
FIPS_NYM_SOCKS5_ADDR="${FIPS_NYM_SOCKS5_ADDR:-127.0.0.1:1080}"
|
||||
NYM_CLIENT_ID="${NYM_CLIENT_ID:-fips-nym-client}"
|
||||
NYM_STARTUP_TIMEOUT="${NYM_STARTUP_TIMEOUT:-180}"
|
||||
|
||||
mkdir -p /etc/fips
|
||||
|
||||
# Build peers section
|
||||
PEERS_SECTION=""
|
||||
if [ -n "$FIPS_PEER_NPUB" ] && [ -n "$FIPS_PEER_ADDR" ]; then
|
||||
FIPS_PEER_ALIAS="${FIPS_PEER_ALIAS:-peer}"
|
||||
PEERS_SECTION=" - npub: \"${FIPS_PEER_NPUB}\"
|
||||
alias: \"${FIPS_PEER_ALIAS}\"
|
||||
addresses:
|
||||
- transport: ${FIPS_PEER_TRANSPORT}
|
||||
addr: \"${FIPS_PEER_ADDR}\"
|
||||
connect_policy: auto_connect"
|
||||
fi
|
||||
|
||||
# The nym transport block is emitted only in nym mode; the SOCKS5 client
|
||||
# below starts only when this block is present in the config.
|
||||
NYM_SECTION=""
|
||||
if [ "$FIPS_PEER_TRANSPORT" = "nym" ]; then
|
||||
NYM_SECTION=" nym:
|
||||
socks5_addr: \"${FIPS_NYM_SOCKS5_ADDR}\"
|
||||
startup_timeout_secs: 120"
|
||||
fi
|
||||
|
||||
cat > /etc/fips/fips.yaml <<EOF
|
||||
node:
|
||||
identity:
|
||||
nsec: "${FIPS_NSEC}"
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
name: fips0
|
||||
mtu: ${FIPS_TUN_MTU}
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
bind_addr: "127.0.0.1"
|
||||
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "${FIPS_UDP_BIND}"
|
||||
# 1472 = Docker bridge IPv4 max (1500 MTU - 8 UDP - 20 IPv4 header).
|
||||
# Override with FIPS_UDP_MTU=1280 for IPv6-min-safe deploys.
|
||||
mtu: ${FIPS_UDP_MTU}
|
||||
tcp:
|
||||
bind_addr: "${FIPS_TCP_BIND}"
|
||||
${NYM_SECTION}
|
||||
|
||||
peers:
|
||||
${PEERS_SECTION:- []}
|
||||
EOF
|
||||
|
||||
echo "Generated /etc/fips/fips.yaml"
|
||||
|
||||
# --- Start local DNS first ---
|
||||
# resolv.conf points at 127.0.0.1; dnsmasq must be up before anything
|
||||
# below (peer-IP resolution, harbourmaster query, nym gateway lookup).
|
||||
dnsmasq
|
||||
|
||||
# --- Apply iptables rules for strict network isolation ---
|
||||
#
|
||||
# Goal: only FIPS transport traffic may use eth0. All other eth0 traffic is
|
||||
# dropped. fips0 and loopback are unrestricted, so the relay (sharing this
|
||||
# namespace) is reachable only over the FIPS mesh.
|
||||
#
|
||||
# In nym mode the direct route to the peer is explicitly DROPped before the
|
||||
# general TCP accept for the mixnet gateways: if the peer comes up, the
|
||||
# connection can only have travelled through the mixnet.
|
||||
|
||||
# IPv4: allow only FIPS transport on eth0
|
||||
iptables -A OUTPUT -o lo -j ACCEPT
|
||||
iptables -A INPUT -i lo -j ACCEPT
|
||||
iptables -A OUTPUT -o eth0 -p udp --dport 2121 -j ACCEPT
|
||||
iptables -A OUTPUT -o eth0 -p udp --sport 2121 -j ACCEPT
|
||||
iptables -A INPUT -i eth0 -p udp --dport 2121 -j ACCEPT
|
||||
iptables -A INPUT -i eth0 -p udp --sport 2121 -j ACCEPT
|
||||
iptables -A OUTPUT -o eth0 -p tcp --dport 443 -j ACCEPT
|
||||
iptables -A INPUT -i eth0 -p tcp --sport 443 -j ACCEPT
|
||||
|
||||
PEER_HOST="${FIPS_PEER_ADDR%:*}"
|
||||
PEER_PORT="${FIPS_PEER_ADDR##*:}"
|
||||
case "$FIPS_PEER_TRANSPORT" in
|
||||
nym)
|
||||
# Block the direct path to the peer (mixnet-only proof). IPv4 only:
|
||||
# eth0 IPv6 is dropped wholesale by the ip6tables rules below.
|
||||
if [ -n "$FIPS_PEER_ADDR" ]; then
|
||||
PEER_IP=$(getent ahostsv4 "$PEER_HOST" | awk '{print $1; exit}' || true)
|
||||
if [ -n "$PEER_IP" ]; then
|
||||
iptables -A OUTPUT -o eth0 -p tcp -d "$PEER_IP" --dport "$PEER_PORT" -j DROP
|
||||
echo "Direct path to peer ${PEER_HOST} (${PEER_IP}:${PEER_PORT}) blocked — mixnet only"
|
||||
fi
|
||||
fi
|
||||
# … then allow outbound TCP for the nym client's gateway connections.
|
||||
iptables -A OUTPUT -o eth0 -p tcp -j ACCEPT
|
||||
iptables -A INPUT -i eth0 -p tcp -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
;;
|
||||
tcp)
|
||||
# Allow dialing the peer's TCP endpoint directly, and inbound FIPS TCP.
|
||||
if [ -n "$FIPS_PEER_ADDR" ]; then
|
||||
iptables -A OUTPUT -o eth0 -p tcp --dport "$PEER_PORT" -j ACCEPT
|
||||
iptables -A INPUT -i eth0 -p tcp --sport "$PEER_PORT" -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
fi
|
||||
iptables -A INPUT -i eth0 -p tcp --dport "${FIPS_TCP_BIND##*:}" -j ACCEPT
|
||||
iptables -A OUTPUT -o eth0 -p tcp --sport "${FIPS_TCP_BIND##*:}" -j ACCEPT
|
||||
;;
|
||||
esac
|
||||
|
||||
iptables -A OUTPUT -o eth0 -j DROP
|
||||
iptables -A INPUT -i eth0 -j DROP
|
||||
|
||||
# IPv6: allow fips0 and loopback, block eth0
|
||||
ip6tables -A OUTPUT -o lo -j ACCEPT
|
||||
ip6tables -A INPUT -i lo -j ACCEPT
|
||||
ip6tables -A OUTPUT -o fips0 -j ACCEPT
|
||||
ip6tables -A INPUT -i fips0 -j ACCEPT
|
||||
ip6tables -A OUTPUT -o eth0 -j DROP
|
||||
ip6tables -A INPUT -i eth0 -j DROP
|
||||
|
||||
echo "iptables isolation rules applied"
|
||||
|
||||
# --- Start the Nostr relay app (strfry + nginx) ---
|
||||
|
||||
(cd /usr/src/app && exec strfry relay) &
|
||||
nginx
|
||||
echo "Nostr relay started (strfry on 127.0.0.1:7777, nginx on :80)"
|
||||
|
||||
# --- Start the Nym SOCKS5 client (only if the config enables nym) ---
|
||||
|
||||
if [ "$FIPS_PEER_TRANSPORT" = "nym" ]; then
|
||||
NYM_HOST="${FIPS_NYM_SOCKS5_ADDR%:*}"
|
||||
NYM_PORT="${FIPS_NYM_SOCKS5_ADDR##*:}"
|
||||
|
||||
# Auto-discover a network-requester service provider when none is set.
|
||||
if [ ! -d "${HOME}/.nym/socks5-clients/${NYM_CLIENT_ID}" ]; then
|
||||
# The provider is only consulted at init time, so auto-discovery
|
||||
# runs only when a fresh client must be initialized. Failures
|
||||
# (harbourmaster down or serving HTML) must not crash the
|
||||
# container with a bare jq error — hence the `|| true` guards
|
||||
# and the explicit empty-check below.
|
||||
if [ -z "$NYM_SERVICE_PROVIDER" ]; then
|
||||
echo "NYM_SERVICE_PROVIDER not set — querying harbourmaster.nymtech.net …"
|
||||
NYM_SERVICE_PROVIDER=$(curl -fsSL --retry 3 \
|
||||
"https://harbourmaster.nymtech.net/v2/services?order_by=routing_score&order_direction=desc&size=100" \
|
||||
2>/dev/null \
|
||||
| jq -r '[.items[] | select(.routing_score == 1.0)]
|
||||
| sort_by(.last_updated_utc) | last
|
||||
| .service_provider_client_id // empty' 2>/dev/null \
|
||||
|| true)
|
||||
if [ -z "$NYM_SERVICE_PROVIDER" ]; then
|
||||
# Last resort: a provider known to work at the time of
|
||||
# writing (2026-06). Providers are volatile community
|
||||
# infra — if the mixnet connects but no traffic flows
|
||||
# ('no node with identity … is known' warnings), this
|
||||
# fallback has gone stale: pick a current one from
|
||||
# https://harbourmaster.nymtech.net/ and set it in .env.
|
||||
NYM_SERVICE_PROVIDER="${NYM_FALLBACK_PROVIDER:-7sfw3sEtSPwhWLmEasVmPXKxqioCo4GaXRkm9bW6yWGZ.CkhMoH85wfNcV2fwoBjc6QDbcaFZHzKqFFvXWfYMw19y@4ScsM6AVowhKTMWaH98NLntKDwbu2ZMEycUk4mZiZppG}"
|
||||
echo "WARNING: harbourmaster auto-discovery failed — using the" >&2
|
||||
echo "baked-in fallback provider (may be stale; see .env):" >&2
|
||||
echo " ${NYM_SERVICE_PROVIDER}" >&2
|
||||
else
|
||||
echo "Auto-selected service provider: ${NYM_SERVICE_PROVIDER}"
|
||||
fi
|
||||
fi
|
||||
echo "Initializing Nym SOCKS5 client '${NYM_CLIENT_ID}' …"
|
||||
nym-socks5-client init \
|
||||
--id "${NYM_CLIENT_ID}" \
|
||||
--provider "${NYM_SERVICE_PROVIDER}" \
|
||||
--port "${NYM_PORT}" \
|
||||
--host "${NYM_HOST}"
|
||||
else
|
||||
# The provider is baked into the client state at init time — a value
|
||||
# set or discovered now does NOT apply to an existing client. Surface
|
||||
# the one actually in effect so a stale/dead provider isn't chased
|
||||
# silently (symptom: 'no node with identity … is known' warnings).
|
||||
STORED_PROVIDER=$(grep -m1 -oE '[1-9A-HJ-NP-Za-km-z]{20,}\.[1-9A-HJ-NP-Za-km-z]{20,}@[1-9A-HJ-NP-Za-km-z]{20,}' \
|
||||
"${HOME}/.nym/socks5-clients/${NYM_CLIENT_ID}/config/config.toml" 2>/dev/null || true)
|
||||
echo "Reusing existing Nym client state (provider: ${STORED_PROVIDER:-unknown})."
|
||||
echo "To switch provider, remove the nym-data volume: docker compose down -v"
|
||||
fi
|
||||
|
||||
echo "Starting Nym SOCKS5 client (mixnet bootstrap may take a minute) …"
|
||||
nym-socks5-client run \
|
||||
--id "${NYM_CLIENT_ID}" \
|
||||
--port "${NYM_PORT}" \
|
||||
--host "${NYM_HOST}" &
|
||||
|
||||
# FIPS must not start dialing before the proxy accepts connections.
|
||||
elapsed=0
|
||||
until nc -z "$NYM_HOST" "$NYM_PORT" 2>/dev/null; do
|
||||
if [ "$elapsed" -ge "$NYM_STARTUP_TIMEOUT" ]; then
|
||||
echo "ERROR: Nym SOCKS5 proxy not ready after ${NYM_STARTUP_TIMEOUT}s" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
echo "Nym SOCKS5 proxy ready at ${FIPS_NYM_SOCKS5_ADDR} (after ~${elapsed}s)"
|
||||
fi
|
||||
|
||||
# --- Launch FIPS (container lifecycle follows the daemon) ---
|
||||
|
||||
echo "Starting FIPS daemon..."
|
||||
exec fips --config /etc/fips/fips.yaml
|
||||
71
examples/sidecar-nostr-mixnet-relay/relay/strfry.conf
Normal file
71
examples/sidecar-nostr-mixnet-relay/relay/strfry.conf
Normal file
@@ -0,0 +1,71 @@
|
||||
##
|
||||
## strfry configuration for FIPS mesh deployment.
|
||||
## Full reference: https://github.com/hoytech/strfry
|
||||
##
|
||||
|
||||
db = "/usr/src/app/strfry-db/"
|
||||
|
||||
dbParams {
|
||||
# Maximum size of the database (bytes). 10 GiB is a safe default.
|
||||
mapsize = 10737418240
|
||||
}
|
||||
|
||||
relay {
|
||||
# Bind on all interfaces so the FIPS TUN (IPv4 + IPv6) can reach it.
|
||||
bind = "0.0.0.0"
|
||||
port = 7777
|
||||
|
||||
nofiles = 0
|
||||
|
||||
info {
|
||||
name = "FIPS Nostr Relay"
|
||||
description = "A Nostr relay accessible over the FIPS mesh network."
|
||||
pubkey = ""
|
||||
contact = ""
|
||||
}
|
||||
|
||||
# Maximum size of an inbound WebSocket message (bytes).
|
||||
maxWebsocketPayloadSize = 131072
|
||||
|
||||
# Send a ping every N seconds to keep connections alive.
|
||||
autoPingSeconds = 55
|
||||
|
||||
# Enable per-message compression.
|
||||
enableTcpNoDelay = false
|
||||
|
||||
rejectFutureEventsSeconds = 900
|
||||
rejectEphemeralEventsOlderThanSeconds = 60
|
||||
rejectEventsNewerThanSeconds = 900
|
||||
|
||||
maxFilterLimit = 500
|
||||
maxSubsPerConnection = 20
|
||||
|
||||
writePolicy {
|
||||
# Plugin executable for write-policy decisions (leave empty to allow all).
|
||||
plugin = ""
|
||||
}
|
||||
|
||||
compression {
|
||||
enabled = true
|
||||
slidingWindow = true
|
||||
}
|
||||
|
||||
logging {
|
||||
dumpInAll = false
|
||||
dumpInEvents = false
|
||||
dumpInReqs = false
|
||||
dbScanPerf = false
|
||||
}
|
||||
|
||||
numThreads {
|
||||
ingester = 3
|
||||
reqWorker = 3
|
||||
reqMonitor = 3
|
||||
negentropy = 2
|
||||
}
|
||||
|
||||
negentropy {
|
||||
enabled = true
|
||||
maxSyncEvents = 1000000
|
||||
}
|
||||
}
|
||||
1
examples/sidecar-nostr-mixnet-relay/resolv.conf
Normal file
1
examples/sidecar-nostr-mixnet-relay/resolv.conf
Normal file
@@ -0,0 +1 @@
|
||||
nameserver 127.0.0.1
|
||||
@@ -481,6 +481,26 @@ fn draw_transport_detail(frame: &mut Frame, app: &App, area: Rect, t: &serde_jso
|
||||
&helpers::nested_u64(t, "stats", "connect_refused"),
|
||||
));
|
||||
}
|
||||
"nym" => {
|
||||
lines.push(helpers::kv_line(
|
||||
"MTU Exceeded",
|
||||
&helpers::nested_u64(t, "stats", "mtu_exceeded"),
|
||||
));
|
||||
lines.push(helpers::kv_line(
|
||||
"SOCKS5 Errors",
|
||||
&helpers::nested_u64(t, "stats", "socks5_errors"),
|
||||
));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(helpers::section_header("Connections"));
|
||||
lines.push(helpers::kv_line(
|
||||
"Established",
|
||||
&helpers::nested_u64(t, "stats", "connections_established"),
|
||||
));
|
||||
lines.push(helpers::kv_line(
|
||||
"Timeouts",
|
||||
&helpers::nested_u64(t, "stats", "connect_timeouts"),
|
||||
));
|
||||
}
|
||||
"ethernet" => {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(helpers::section_header("Beacons"));
|
||||
|
||||
@@ -39,8 +39,8 @@ pub use node::{
|
||||
};
|
||||
pub use peer::{ConnectPolicy, PeerAddress, PeerConfig};
|
||||
pub use transport::{
|
||||
BleConfig, DirectoryServiceConfig, EthernetConfig, TcpConfig, TorConfig, TransportInstances,
|
||||
TransportsConfig, UdpConfig,
|
||||
BleConfig, DirectoryServiceConfig, EthernetConfig, NymConfig, TcpConfig, TorConfig,
|
||||
TransportInstances, TransportsConfig, UdpConfig,
|
||||
};
|
||||
|
||||
/// Default config filename.
|
||||
|
||||
@@ -801,6 +801,77 @@ impl BleConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Nym Transport Configuration
|
||||
// ============================================================================
|
||||
|
||||
/// Default Nym SOCKS5 proxy address (nym-socks5-client).
|
||||
const DEFAULT_NYM_SOCKS5_ADDR: &str = "127.0.0.1:1080";
|
||||
|
||||
/// Default Nym connect timeout in milliseconds (300s — Nym mixnet
|
||||
/// SOCKS5 connections require multiple round-trips through 3 mix nodes
|
||||
/// with timing obfuscation, which can take several minutes).
|
||||
const DEFAULT_NYM_CONNECT_TIMEOUT_MS: u64 = 300_000;
|
||||
|
||||
/// Default Nym MTU (same as TCP).
|
||||
const DEFAULT_NYM_MTU: u16 = 1400;
|
||||
|
||||
/// Default Nym startup timeout in seconds (time to wait for
|
||||
/// nym-socks5-client to become ready before giving up).
|
||||
const DEFAULT_NYM_STARTUP_TIMEOUT_SECS: u64 = 120;
|
||||
|
||||
/// Nym transport instance configuration.
|
||||
///
|
||||
/// Outbound-only connections through a nym-socks5-client SOCKS5 proxy.
|
||||
/// The nym-socks5-client must be running separately (e.g., as a sidecar
|
||||
/// process or container).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NymConfig {
|
||||
/// SOCKS5 proxy address (host:port). Defaults to "127.0.0.1:1080".
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub socks5_addr: Option<String>,
|
||||
|
||||
/// Outbound connect timeout in milliseconds. Defaults to 300000 (300s).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub connect_timeout_ms: Option<u64>,
|
||||
|
||||
/// Default MTU for Nym connections. Defaults to 1400.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mtu: Option<u16>,
|
||||
|
||||
/// Seconds to wait for nym-socks5-client to become ready at startup.
|
||||
/// Defaults to 120.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub startup_timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
impl NymConfig {
|
||||
/// Get the SOCKS5 proxy address. Default: "127.0.0.1:1080".
|
||||
pub fn socks5_addr(&self) -> &str {
|
||||
self.socks5_addr
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_NYM_SOCKS5_ADDR)
|
||||
}
|
||||
|
||||
/// Get the connect timeout in milliseconds. Default: 300000.
|
||||
pub fn connect_timeout_ms(&self) -> u64 {
|
||||
self.connect_timeout_ms
|
||||
.unwrap_or(DEFAULT_NYM_CONNECT_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
/// Get the default MTU. Default: 1400.
|
||||
pub fn mtu(&self) -> u16 {
|
||||
self.mtu.unwrap_or(DEFAULT_NYM_MTU)
|
||||
}
|
||||
|
||||
/// Get the startup timeout in seconds. Default: 120.
|
||||
pub fn startup_timeout_secs(&self) -> u64 {
|
||||
self.startup_timeout_secs
|
||||
.unwrap_or(DEFAULT_NYM_STARTUP_TIMEOUT_SECS)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TransportsConfig
|
||||
// ============================================================================
|
||||
@@ -827,6 +898,10 @@ pub struct TransportsConfig {
|
||||
#[serde(default, skip_serializing_if = "is_transport_empty")]
|
||||
pub tor: TransportInstances<TorConfig>,
|
||||
|
||||
/// Nym transport instances.
|
||||
#[serde(default, skip_serializing_if = "is_transport_empty")]
|
||||
pub nym: TransportInstances<NymConfig>,
|
||||
|
||||
/// BLE transport instances.
|
||||
#[serde(default, skip_serializing_if = "is_transport_empty")]
|
||||
pub ble: TransportInstances<BleConfig>,
|
||||
@@ -844,6 +919,7 @@ impl TransportsConfig {
|
||||
&& self.ethernet.is_empty()
|
||||
&& self.tcp.is_empty()
|
||||
&& self.tor.is_empty()
|
||||
&& self.nym.is_empty()
|
||||
&& self.ble.is_empty()
|
||||
}
|
||||
|
||||
@@ -863,6 +939,9 @@ impl TransportsConfig {
|
||||
if !other.tor.is_empty() {
|
||||
self.tor = other.tor;
|
||||
}
|
||||
if !other.nym.is_empty() {
|
||||
self.nym = other.nym;
|
||||
}
|
||||
if !other.ble.is_empty() {
|
||||
self.ble = other.ble;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ pub use identity::{
|
||||
};
|
||||
|
||||
// Re-export config types
|
||||
pub use config::{Config, ConfigError, IdentityConfig, TorConfig, UdpConfig};
|
||||
pub use config::{Config, ConfigError, IdentityConfig, NymConfig, TorConfig, UdpConfig};
|
||||
pub use upper::config::{DnsConfig, TunConfig};
|
||||
|
||||
// Re-export discovery types
|
||||
|
||||
@@ -50,6 +50,7 @@ use crate::node::session::SessionEntry;
|
||||
use crate::peer::{ActivePeer, PeerConnection};
|
||||
#[cfg(unix)]
|
||||
use crate::transport::ethernet::EthernetTransport;
|
||||
use crate::transport::nym::NymTransport;
|
||||
use crate::transport::tcp::TcpTransport;
|
||||
use crate::transport::tor::TorTransport;
|
||||
use crate::transport::udp::UdpTransport;
|
||||
@@ -982,6 +983,21 @@ impl Node {
|
||||
transports.push(TransportHandle::Tor(tor));
|
||||
}
|
||||
|
||||
// Create Nym transport instances
|
||||
let nym_instances: Vec<_> = self
|
||||
.config()
|
||||
.transports
|
||||
.nym
|
||||
.iter()
|
||||
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
|
||||
.collect();
|
||||
|
||||
for (name, nym_config) in nym_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let nym = NymTransport::new(transport_id, name, nym_config, packet_tx.clone());
|
||||
transports.push(TransportHandle::Nym(nym));
|
||||
}
|
||||
|
||||
// Create BLE transport instances
|
||||
#[cfg(bluer_available)]
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod loopback;
|
||||
pub mod nym;
|
||||
pub mod tcp;
|
||||
pub mod tor;
|
||||
pub mod udp;
|
||||
@@ -22,6 +23,7 @@ use ble::DefaultBleTransport;
|
||||
use ethernet::EthernetTransport;
|
||||
#[cfg(test)]
|
||||
use loopback::LoopbackTransport;
|
||||
use nym::NymTransport;
|
||||
use secp256k1::XOnlyPublicKey;
|
||||
use std::fmt;
|
||||
use std::net::SocketAddr;
|
||||
@@ -259,6 +261,13 @@ impl TransportType {
|
||||
reliable: true, // in-process channel delivery is lossless
|
||||
};
|
||||
|
||||
/// Nym mixnet transport (via SOCKS5).
|
||||
pub const NYM: TransportType = TransportType {
|
||||
name: "nym",
|
||||
connection_oriented: true,
|
||||
reliable: true,
|
||||
};
|
||||
|
||||
/// Check if the transport is connectionless.
|
||||
pub fn is_connectionless(&self) -> bool {
|
||||
!self.connection_oriented
|
||||
@@ -891,6 +900,8 @@ pub enum TransportHandle {
|
||||
Tcp(TcpTransport),
|
||||
/// Tor transport (via SOCKS5).
|
||||
Tor(TorTransport),
|
||||
/// Nym mixnet transport (via SOCKS5).
|
||||
Nym(NymTransport),
|
||||
/// BLE L2CAP transport.
|
||||
#[cfg(target_os = "linux")]
|
||||
Ble(DefaultBleTransport),
|
||||
@@ -908,6 +919,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.start_async().await,
|
||||
TransportHandle::Tcp(t) => t.start_async().await,
|
||||
TransportHandle::Tor(t) => t.start_async().await,
|
||||
TransportHandle::Nym(t) => t.start_async().await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.start_async().await,
|
||||
#[cfg(test)]
|
||||
@@ -923,6 +935,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.stop_async().await,
|
||||
TransportHandle::Tcp(t) => t.stop_async().await,
|
||||
TransportHandle::Tor(t) => t.stop_async().await,
|
||||
TransportHandle::Nym(t) => t.stop_async().await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.stop_async().await,
|
||||
#[cfg(test)]
|
||||
@@ -938,6 +951,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.send_async(addr, data).await,
|
||||
TransportHandle::Tcp(t) => t.send_async(addr, data).await,
|
||||
TransportHandle::Tor(t) => t.send_async(addr, data).await,
|
||||
TransportHandle::Nym(t) => t.send_async(addr, data).await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.send_async(addr, data).await,
|
||||
#[cfg(test)]
|
||||
@@ -953,6 +967,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.transport_id(),
|
||||
TransportHandle::Tcp(t) => t.transport_id(),
|
||||
TransportHandle::Tor(t) => t.transport_id(),
|
||||
TransportHandle::Nym(t) => t.transport_id(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.transport_id(),
|
||||
#[cfg(test)]
|
||||
@@ -968,6 +983,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.name(),
|
||||
TransportHandle::Tcp(t) => t.name(),
|
||||
TransportHandle::Tor(t) => t.name(),
|
||||
TransportHandle::Nym(t) => t.name(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.name(),
|
||||
#[cfg(test)]
|
||||
@@ -983,6 +999,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.transport_type(),
|
||||
TransportHandle::Tcp(t) => t.transport_type(),
|
||||
TransportHandle::Tor(t) => t.transport_type(),
|
||||
TransportHandle::Nym(t) => t.transport_type(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.transport_type(),
|
||||
#[cfg(test)]
|
||||
@@ -998,6 +1015,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.state(),
|
||||
TransportHandle::Tcp(t) => t.state(),
|
||||
TransportHandle::Tor(t) => t.state(),
|
||||
TransportHandle::Nym(t) => t.state(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.state(),
|
||||
#[cfg(test)]
|
||||
@@ -1013,6 +1031,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.mtu(),
|
||||
TransportHandle::Tcp(t) => t.mtu(),
|
||||
TransportHandle::Tor(t) => t.mtu(),
|
||||
TransportHandle::Nym(t) => t.mtu(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.mtu(),
|
||||
#[cfg(test)]
|
||||
@@ -1031,6 +1050,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.link_mtu(addr),
|
||||
TransportHandle::Tcp(t) => t.link_mtu(addr),
|
||||
TransportHandle::Tor(t) => t.link_mtu(addr),
|
||||
TransportHandle::Nym(t) => t.link_mtu(addr),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.link_mtu(addr),
|
||||
#[cfg(test)]
|
||||
@@ -1046,6 +1066,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(_) => None,
|
||||
TransportHandle::Tcp(t) => t.local_addr(),
|
||||
TransportHandle::Tor(_) => None,
|
||||
TransportHandle::Nym(_) => None,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(_) => None,
|
||||
#[cfg(test)]
|
||||
@@ -1061,6 +1082,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => Some(t.interface_name()),
|
||||
TransportHandle::Tcp(_) => None,
|
||||
TransportHandle::Tor(_) => None,
|
||||
TransportHandle::Nym(_) => None,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(_) => None,
|
||||
#[cfg(test)]
|
||||
@@ -1100,6 +1122,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.discover(),
|
||||
TransportHandle::Tcp(t) => t.discover(),
|
||||
TransportHandle::Tor(t) => t.discover(),
|
||||
TransportHandle::Nym(t) => t.discover(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.discover(),
|
||||
#[cfg(test)]
|
||||
@@ -1115,6 +1138,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.auto_connect(),
|
||||
TransportHandle::Tcp(t) => t.auto_connect(),
|
||||
TransportHandle::Tor(t) => t.auto_connect(),
|
||||
TransportHandle::Nym(t) => t.auto_connect(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.auto_connect(),
|
||||
#[cfg(test)]
|
||||
@@ -1130,6 +1154,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.accept_connections(),
|
||||
TransportHandle::Tcp(t) => t.accept_connections(),
|
||||
TransportHandle::Tor(t) => t.accept_connections(),
|
||||
TransportHandle::Nym(t) => t.accept_connections(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.accept_connections(),
|
||||
#[cfg(test)]
|
||||
@@ -1139,7 +1164,7 @@ impl TransportHandle {
|
||||
|
||||
/// Initiate a non-blocking connection to a remote address.
|
||||
///
|
||||
/// For connection-oriented transports (TCP, Tor), spawns a background
|
||||
/// For connection-oriented transports (TCP, Tor, Nym), spawns a background
|
||||
/// task to establish the connection. For connectionless transports
|
||||
/// (UDP, Ethernet), this is a no-op that returns Ok immediately.
|
||||
///
|
||||
@@ -1151,6 +1176,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(_) => Ok(()), // connectionless
|
||||
TransportHandle::Tcp(t) => t.connect_async(addr).await,
|
||||
TransportHandle::Tor(t) => t.connect_async(addr).await,
|
||||
TransportHandle::Nym(t) => t.connect_async(addr).await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.connect_async(addr).await,
|
||||
#[cfg(test)]
|
||||
@@ -1170,6 +1196,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(_) => ConnectionState::Connected,
|
||||
TransportHandle::Tcp(t) => t.connection_state_sync(addr),
|
||||
TransportHandle::Tor(t) => t.connection_state_sync(addr),
|
||||
TransportHandle::Nym(t) => t.connection_state_sync(addr),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.connection_state_sync(addr),
|
||||
#[cfg(test)]
|
||||
@@ -1179,7 +1206,7 @@ impl TransportHandle {
|
||||
|
||||
/// Close a specific connection on this transport.
|
||||
///
|
||||
/// No-op for connectionless transports. For TCP/Tor, removes the
|
||||
/// No-op for connectionless transports. For TCP/Tor/Nym, removes the
|
||||
/// connection from the pool and drops the stream.
|
||||
pub async fn close_connection(&self, addr: &TransportAddr) {
|
||||
match self {
|
||||
@@ -1188,6 +1215,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(t) => t.close_connection(addr),
|
||||
TransportHandle::Tcp(t) => t.close_connection_async(addr).await,
|
||||
TransportHandle::Tor(t) => t.close_connection_async(addr).await,
|
||||
TransportHandle::Nym(t) => t.close_connection_async(addr).await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => t.close_connection_async(addr).await,
|
||||
#[cfg(test)]
|
||||
@@ -1212,6 +1240,7 @@ impl TransportHandle {
|
||||
TransportHandle::Ethernet(_) => TransportCongestion::default(),
|
||||
TransportHandle::Tcp(_) => TransportCongestion::default(),
|
||||
TransportHandle::Tor(_) => TransportCongestion::default(),
|
||||
TransportHandle::Nym(_) => TransportCongestion::default(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(_) => TransportCongestion::default(),
|
||||
#[cfg(test)]
|
||||
@@ -1249,6 +1278,9 @@ impl TransportHandle {
|
||||
TransportHandle::Tor(t) => {
|
||||
serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
|
||||
}
|
||||
TransportHandle::Nym(t) => {
|
||||
serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ble(t) => {
|
||||
serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
|
||||
|
||||
203
src/transport/nym/mock_socks5.rs
Normal file
203
src/transport/nym/mock_socks5.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
//! Mock SOCKS5 server for testing the Nym transport's connect path.
|
||||
//!
|
||||
//! A copy of the Tor transport's mock, implementing just enough of the
|
||||
//! SOCKS5 protocol (RFC 1928) to support the no-auth (and, defensively,
|
||||
//! username/password) CONNECT flow, then proxying bytes bidirectionally to a
|
||||
//! fixed target.
|
||||
//!
|
||||
//! Difference from the Tor mock: this one accepts connections in a loop and
|
||||
//! handles each on its own task. `NymTransport::start_async` first probes the
|
||||
//! proxy port for readiness (opening and immediately dropping a connection);
|
||||
//! looping lets the mock shrug that probe off — its handler returns on the
|
||||
//! short first read — and still serve the real data connection that follows.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// SOCKS5 protocol constants.
|
||||
const SOCKS_VERSION: u8 = 0x05;
|
||||
const AUTH_NONE: u8 = 0x00;
|
||||
const AUTH_PASSWORD: u8 = 0x02;
|
||||
const CMD_CONNECT: u8 = 0x01;
|
||||
const ATYP_IPV4: u8 = 0x01;
|
||||
const ATYP_DOMAIN: u8 = 0x03;
|
||||
const REP_SUCCESS: u8 = 0x00;
|
||||
|
||||
/// Username/password auth sub-negotiation version (RFC 1929).
|
||||
const AUTH_SUBNEG_VERSION: u8 = 0x01;
|
||||
const AUTH_SUBNEG_SUCCESS: u8 = 0x00;
|
||||
|
||||
/// A minimal mock SOCKS5 proxy server for testing.
|
||||
///
|
||||
/// Accepts connections in a loop, performs the SOCKS5 handshake (supporting
|
||||
/// both no-auth and username/password auth), then connects to a fixed target
|
||||
/// address and proxies bytes bidirectionally.
|
||||
pub struct MockSocks5Server {
|
||||
/// Address the mock proxy is listening on.
|
||||
addr: SocketAddr,
|
||||
/// The real target address to connect to (ignores SOCKS5 requested target).
|
||||
target_addr: SocketAddr,
|
||||
/// Listener handle.
|
||||
listener: Option<TcpListener>,
|
||||
}
|
||||
|
||||
impl MockSocks5Server {
|
||||
/// Create a new mock SOCKS5 server that forwards to the given target.
|
||||
///
|
||||
/// Binds to `127.0.0.1:0` (OS-assigned port).
|
||||
pub async fn new(target_addr: SocketAddr) -> std::io::Result<Self> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let addr = listener.local_addr()?;
|
||||
Ok(Self {
|
||||
addr,
|
||||
target_addr,
|
||||
listener: Some(listener),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the proxy's listen address (for `NymConfig.socks5_addr`).
|
||||
pub fn addr(&self) -> SocketAddr {
|
||||
self.addr
|
||||
}
|
||||
|
||||
/// Run the proxy, accepting connections in a loop and proxying each.
|
||||
///
|
||||
/// Returns a JoinHandle for the accept loop.
|
||||
pub fn spawn(mut self) -> JoinHandle<()> {
|
||||
let listener = self.listener.take().expect("listener already consumed");
|
||||
let target_addr = self.target_addr;
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let (client, _) = match listener.accept().await {
|
||||
Ok(c) => c,
|
||||
Err(_) => break,
|
||||
};
|
||||
// Handle each connection independently so the readiness probe
|
||||
// (which opens and drops a connection) cannot block the real
|
||||
// data connection behind it.
|
||||
tokio::spawn(handle_conn(client, target_addr));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a single accepted connection: SOCKS5 handshake then byte proxy.
|
||||
async fn handle_conn(mut client: tokio::net::TcpStream, target_addr: SocketAddr) {
|
||||
// === Method negotiation ===
|
||||
// Client sends: [version, nmethods, methods...]
|
||||
let mut ver_nmethods = [0u8; 2];
|
||||
if client.read_exact(&mut ver_nmethods).await.is_err() {
|
||||
// Readiness probe (or any early close) — nothing to serve.
|
||||
return;
|
||||
}
|
||||
assert_eq!(ver_nmethods[0], SOCKS_VERSION, "expected SOCKS5");
|
||||
let nmethods = ver_nmethods[1] as usize;
|
||||
|
||||
let mut methods = vec![0u8; nmethods];
|
||||
client.read_exact(&mut methods).await.expect("read methods");
|
||||
|
||||
// Prefer username/password auth if offered, fall back to no-auth.
|
||||
let selected = if methods.contains(&AUTH_PASSWORD) {
|
||||
AUTH_PASSWORD
|
||||
} else if methods.contains(&AUTH_NONE) {
|
||||
AUTH_NONE
|
||||
} else {
|
||||
panic!("no supported auth method offered");
|
||||
};
|
||||
|
||||
// Reply: [version, selected_method]
|
||||
client
|
||||
.write_all(&[SOCKS_VERSION, selected])
|
||||
.await
|
||||
.expect("write method reply");
|
||||
|
||||
// === Username/password sub-negotiation (RFC 1929) ===
|
||||
if selected == AUTH_PASSWORD {
|
||||
// Client sends: [ver(1), ulen(1), uname(ulen), plen(1), passwd(plen)]
|
||||
let mut subneg_header = [0u8; 2];
|
||||
client
|
||||
.read_exact(&mut subneg_header)
|
||||
.await
|
||||
.expect("read subneg header");
|
||||
assert_eq!(
|
||||
subneg_header[0], AUTH_SUBNEG_VERSION,
|
||||
"expected auth subneg v1"
|
||||
);
|
||||
|
||||
let ulen = subneg_header[1] as usize;
|
||||
let mut uname = vec![0u8; ulen];
|
||||
client.read_exact(&mut uname).await.expect("read username");
|
||||
|
||||
let mut plen_buf = [0u8; 1];
|
||||
client.read_exact(&mut plen_buf).await.expect("read plen");
|
||||
let plen = plen_buf[0] as usize;
|
||||
let mut passwd = vec![0u8; plen];
|
||||
client.read_exact(&mut passwd).await.expect("read password");
|
||||
|
||||
client
|
||||
.write_all(&[AUTH_SUBNEG_VERSION, AUTH_SUBNEG_SUCCESS])
|
||||
.await
|
||||
.expect("write subneg reply");
|
||||
}
|
||||
|
||||
// === Connect request ===
|
||||
// Client sends: [version, cmd, rsv, atyp, addr..., port]
|
||||
let mut header = [0u8; 4];
|
||||
client
|
||||
.read_exact(&mut header)
|
||||
.await
|
||||
.expect("read connect header");
|
||||
assert_eq!(header[0], SOCKS_VERSION);
|
||||
assert_eq!(header[1], CMD_CONNECT);
|
||||
|
||||
// Read and skip the address (we connect to target_addr regardless).
|
||||
match header[3] {
|
||||
ATYP_IPV4 => {
|
||||
let mut addr_port = [0u8; 6]; // 4 IP + 2 port
|
||||
client
|
||||
.read_exact(&mut addr_port)
|
||||
.await
|
||||
.expect("read IPv4 addr");
|
||||
}
|
||||
ATYP_DOMAIN => {
|
||||
let mut len_buf = [0u8; 1];
|
||||
client
|
||||
.read_exact(&mut len_buf)
|
||||
.await
|
||||
.expect("read domain len");
|
||||
let domain_len = len_buf[0] as usize;
|
||||
let mut domain_port = vec![0u8; domain_len + 2]; // domain + 2 port
|
||||
client
|
||||
.read_exact(&mut domain_port)
|
||||
.await
|
||||
.expect("read domain addr");
|
||||
}
|
||||
other => panic!("unsupported ATYP: {}", other),
|
||||
}
|
||||
|
||||
// Connect to the real target.
|
||||
let mut target = tokio::net::TcpStream::connect(target_addr)
|
||||
.await
|
||||
.expect("connect to target");
|
||||
|
||||
// Reply: success, bind addr = 0.0.0.0:0
|
||||
let reply = [
|
||||
SOCKS_VERSION,
|
||||
REP_SUCCESS,
|
||||
0x00, // RSV
|
||||
ATYP_IPV4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0, // bind addr
|
||||
0,
|
||||
0, // bind port
|
||||
];
|
||||
client.write_all(&reply).await.expect("write connect reply");
|
||||
|
||||
// Proxy bytes bidirectionally.
|
||||
let _ = tokio::io::copy_bidirectional(&mut client, &mut target).await;
|
||||
}
|
||||
1141
src/transport/nym/mod.rs
Normal file
1141
src/transport/nym/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
119
src/transport/nym/stats.rs
Normal file
119
src/transport/nym/stats.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! Nym transport statistics.
|
||||
|
||||
use portable_atomic::{AtomicU64, Ordering};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Statistics for a Nym transport instance.
|
||||
///
|
||||
/// Uses atomic counters for lock-free updates from per-connection
|
||||
/// receive loops and the send path concurrently.
|
||||
pub struct NymStats {
|
||||
pub packets_sent: AtomicU64,
|
||||
pub bytes_sent: AtomicU64,
|
||||
pub packets_recv: AtomicU64,
|
||||
pub bytes_recv: AtomicU64,
|
||||
pub send_errors: AtomicU64,
|
||||
pub recv_errors: AtomicU64,
|
||||
pub mtu_exceeded: AtomicU64,
|
||||
pub connections_established: AtomicU64,
|
||||
pub connect_timeouts: AtomicU64,
|
||||
pub socks5_errors: AtomicU64,
|
||||
}
|
||||
|
||||
impl NymStats {
|
||||
/// Create a new stats instance with all counters at zero.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
packets_sent: AtomicU64::new(0),
|
||||
bytes_sent: AtomicU64::new(0),
|
||||
packets_recv: AtomicU64::new(0),
|
||||
bytes_recv: AtomicU64::new(0),
|
||||
send_errors: AtomicU64::new(0),
|
||||
recv_errors: AtomicU64::new(0),
|
||||
mtu_exceeded: AtomicU64::new(0),
|
||||
connections_established: AtomicU64::new(0),
|
||||
connect_timeouts: AtomicU64::new(0),
|
||||
socks5_errors: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a successful send.
|
||||
pub fn record_send(&self, bytes: usize) {
|
||||
self.packets_sent.fetch_add(1, Ordering::Relaxed);
|
||||
self.bytes_sent.fetch_add(bytes as u64, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a successful receive.
|
||||
pub fn record_recv(&self, bytes: usize) {
|
||||
self.packets_recv.fetch_add(1, Ordering::Relaxed);
|
||||
self.bytes_recv.fetch_add(bytes as u64, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a send error.
|
||||
pub fn record_send_error(&self) {
|
||||
self.send_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a receive error.
|
||||
pub fn record_recv_error(&self) {
|
||||
self.recv_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record an MTU exceeded rejection.
|
||||
pub fn record_mtu_exceeded(&self) {
|
||||
self.mtu_exceeded.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a successful outbound connection.
|
||||
pub fn record_connection_established(&self) {
|
||||
self.connections_established.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a connect timeout.
|
||||
pub fn record_connect_timeout(&self) {
|
||||
self.connect_timeouts.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a SOCKS5 protocol error.
|
||||
pub fn record_socks5_error(&self) {
|
||||
self.socks5_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Take a snapshot of all counters.
|
||||
pub fn snapshot(&self) -> NymStatsSnapshot {
|
||||
NymStatsSnapshot {
|
||||
packets_sent: self.packets_sent.load(Ordering::Relaxed),
|
||||
bytes_sent: self.bytes_sent.load(Ordering::Relaxed),
|
||||
packets_recv: self.packets_recv.load(Ordering::Relaxed),
|
||||
bytes_recv: self.bytes_recv.load(Ordering::Relaxed),
|
||||
send_errors: self.send_errors.load(Ordering::Relaxed),
|
||||
recv_errors: self.recv_errors.load(Ordering::Relaxed),
|
||||
mtu_exceeded: self.mtu_exceeded.load(Ordering::Relaxed),
|
||||
connections_established: self.connections_established.load(Ordering::Relaxed),
|
||||
connect_timeouts: self.connect_timeouts.load(Ordering::Relaxed),
|
||||
socks5_errors: self.socks5_errors.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NymStats {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Point-in-time snapshot of Nym stats (non-atomic, copyable).
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct NymStatsSnapshot {
|
||||
pub packets_sent: u64,
|
||||
pub bytes_sent: u64,
|
||||
pub packets_recv: u64,
|
||||
pub bytes_recv: u64,
|
||||
pub send_errors: u64,
|
||||
pub recv_errors: u64,
|
||||
pub mtu_exceeded: u64,
|
||||
pub connections_established: u64,
|
||||
pub connect_timeouts: u64,
|
||||
pub socks5_errors: u64,
|
||||
}
|
||||
Reference in New Issue
Block a user