Retire the mixnet exit; document Tor as the transport
The Goblin wallet moved off the Nym mixnet to Tor: it now reaches relays over a Tor circuit to their clearnet endpoint, so the co-located mixnet exit this package bundled is retired. - delete the vendored `mixexit/` crate and the hardened `deploy/systemd/floonet-mixexit.service` bare-metal unit - docker-compose: drop the `mixexit` service (COMPOSE_PROFILES=exit) and its `mixexit-data` volume; fix the top-of-file service list - .env.example: replace the "Mixnet exit" block (COMPOSE_PROFILES=exit, FLOONET_EXIT_UPSTREAM) with the Tor onion toggle - README: replace the "Mixnet exit" section with "Tor onion", fix the feature table, deploy paths, and the COMPOSE_PROFILES reference row Add Tor as the first-class replacement deploy option, the same recipe already proven in production: an optional `tor` compose service (COMPOSE_PROFILES=tor) plus deploy/tor/torrc, a stock system tor daemon whose hidden service forwards straight to strfry's websocket listener (no TLS on that hop, the onion is already encrypted end to end). strfry core stays stock; this is packaging and docs, no relay patch. The default stack is unchanged (the onion is opt-in, as the exit was). name-authority builds green; cargo test and clippy pass. docker-compose validated (structure + YAML).
This commit is contained in:
+8
-11
@@ -71,18 +71,15 @@ GOBLINPAY_WEBHOOK_SECRET=
|
||||
# Seconds the write policy plugin caches paid-status verdicts.
|
||||
FLOONET_PAID_CACHE_SECS=60
|
||||
|
||||
# --- Mixnet exit (optional) ---
|
||||
# --- Tor onion (optional) ---
|
||||
|
||||
# Uncomment to ALSO run the bundled scoped mixnet exit, so wallets can reach
|
||||
# this relay over the mixnet. The exit forwards ONLY to this stack's own TLS
|
||||
# front (never arbitrary targets) and sees only ciphertext. On first start it
|
||||
# prints (and stores) its stable mixnet address; publish that address in the
|
||||
# relay pool listing so wallets can use it.
|
||||
#COMPOSE_PROFILES=exit
|
||||
|
||||
# Where the exit pipes accepted streams. The default is this stack's own
|
||||
# proxy; only change it if your TLS terminates elsewhere.
|
||||
FLOONET_EXIT_UPSTREAM=caddy:443
|
||||
# Uncomment to ALSO run a Tor onion in front of the relay (the `tor` compose
|
||||
# service), so wallets can reach it over Tor without a Tor exit hop. The onion
|
||||
# forwards straight to the relay's websocket listener; see deploy/tor/torrc.
|
||||
# tor prints the .onion address to its logs on first start and stores its key
|
||||
# on the tor-data volume (back it up: losing it rotates the address). Publish
|
||||
# the .onion so wallets can find it.
|
||||
#COMPOSE_PROFILES=tor
|
||||
|
||||
# --- Name authority policy tunables ---
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ through strfry's own extension points:
|
||||
| --- | --- |
|
||||
| `plugin/floonet_writepolicy.py` | The write policy plugin: default-deny kind whitelist, optional NIP-42 gate, optional paid-write gate |
|
||||
| `name-authority/` | The bundled name authority (Rust/axum/SQLite): NIP-05 resolution, NIP-98 self-service registration, optional GoblinPay paywall — co-located on the relay's own domain by default |
|
||||
| `mixexit/` | An optional, scoped mixnet exit so wallets can reach this relay over the mixnet |
|
||||
| `deploy/tor/` | An optional Tor onion service so wallets can reach this relay over Tor without a Tor exit hop |
|
||||
| `deploy/` | strfry conf + Dockerfile + apply-spec.sh, Caddy TLS proxy, landing page, hardened systemd units |
|
||||
|
||||
## Deploy
|
||||
@@ -23,7 +23,7 @@ Pick your comfort level. All three paths produce the same relay.
|
||||
### 1. Docker Compose (recommended)
|
||||
|
||||
One command brings up the whole unit: relay + name authority + auto-TLS
|
||||
proxy (and, if enabled, the mixnet exit).
|
||||
proxy (and, if enabled, a Tor onion).
|
||||
|
||||
```sh
|
||||
cp .env.example .env # set FLOONET_DOMAIN, FLOONET_BASE_URL, FLOONET_RELAYS
|
||||
@@ -44,17 +44,18 @@ cd name-authority && cargo build --release
|
||||
```
|
||||
|
||||
Then install the hardened units from `deploy/systemd/` (each unit's header
|
||||
has the exact install commands): `floonet-strfry.service`,
|
||||
`floonet-authority.service` and, optionally, `floonet-mixexit.service`.
|
||||
Put Caddy or nginx in front (see `deploy/Caddyfile`); the proxy MUST set
|
||||
`X-Real-IP`, the authority's rate limiting keys off it.
|
||||
has the exact install commands): `floonet-strfry.service` and
|
||||
`floonet-authority.service`. Put Caddy or nginx in front (see
|
||||
`deploy/Caddyfile`); the proxy MUST set `X-Real-IP`, the authority's rate
|
||||
limiting keys off it. To also front the relay with a Tor onion, run a system
|
||||
tor with the snippet in `deploy/tor/torrc` (see "Tor onion" below).
|
||||
|
||||
### 3. From source (developers)
|
||||
|
||||
`deploy/strfry/Dockerfile` and `apply-spec.sh` document the strfry build
|
||||
exactly; the authority and the exit are plain `cargo build` crates; the
|
||||
plugin is a single Python file with no dependencies. `plugin/test_policy.py`
|
||||
and `cargo test` in `name-authority/` run the test suites.
|
||||
exactly; the authority is a plain `cargo build` crate; the plugin is a single
|
||||
Python file with no dependencies. `plugin/test_policy.py` and `cargo test` in
|
||||
`name-authority/` run the test suites.
|
||||
|
||||
## The kind whitelist (the keystone)
|
||||
|
||||
@@ -184,24 +185,29 @@ event ids (replay rejection).
|
||||
and the rest of `/api/*` stay on the authority's own domain. The snippet sets
|
||||
`X-Real-IP` (load-bearing — the authority's per-IP rate limiter keys off it).
|
||||
|
||||
## Mixnet exit (optional)
|
||||
## Tor onion (optional)
|
||||
|
||||
Uncomment `COMPOSE_PROFILES=exit` in `.env` and the package also runs
|
||||
`floonet-mixexit`: a small, unbonded mixnet client that accepts incoming
|
||||
mixnet streams and pipes every one of them to this stack's own TLS front.
|
||||
Wallets that prefer not to touch DNS or reveal their relay choice can then
|
||||
reach this relay entirely over the mixnet, with end-to-end TLS; the exit
|
||||
sees only ciphertext.
|
||||
Goblin wallets connect to relays over Tor: the client opens a Tor circuit and
|
||||
reaches the relay's ordinary clearnet endpoint (`FLOONET_DOMAIN`) through a
|
||||
Tor exit, so the relay never sees the user's real IP. That works against any
|
||||
Floonet relay with no extra setup here, and it is the whole transport story:
|
||||
Tor hides the user's network location; the kind whitelist and gift-wrapped
|
||||
(kind 1059) payloads hide everything else from the relay itself. The relay
|
||||
needs no privacy component of its own.
|
||||
|
||||
It is deliberately **scoped**: per-stream targets are never honored, the one
|
||||
upstream is fixed by config, so it is structurally not an open proxy and
|
||||
carries no open-proxy liability. No bonding, no tokens, no directory
|
||||
listing.
|
||||
An operator who wants to remove the Tor-exit hop entirely can front the relay
|
||||
with a **Tor onion service**. Uncomment `COMPOSE_PROFILES=tor` in `.env` and
|
||||
the package also runs the `tor` service: a stock system tor daemon whose
|
||||
hidden service forwards straight to strfry's websocket listener (no TLS on
|
||||
that hop, since the onion transport is already encrypted and authenticated end
|
||||
to end). Wallets then reach the relay over an `.onion` with no exit hop at all.
|
||||
|
||||
On first start it prints its **stable mixnet address** (also written to the
|
||||
data volume's `nym_address.txt`). Publish that address in your relay pool
|
||||
listing (the `exit` field) so wallets can find it, and back the data
|
||||
directory up: losing it rotates the address.
|
||||
tor prints the `.onion` address to its logs on first start and stores its key
|
||||
on the `tor-data` volume; back that volume up, since losing it rotates the
|
||||
address. Publish the `.onion` so wallets can find it. Without Docker, run a
|
||||
system tor with the snippet in `deploy/tor/torrc` (a `HiddenServiceDir` plus a
|
||||
`HiddenServicePort` pointed at the relay's local websocket port) alongside the
|
||||
`floonet-strfry.service` unit.
|
||||
|
||||
## Extending the policy (plugins, paid resources)
|
||||
|
||||
@@ -258,7 +264,7 @@ essentials:
|
||||
| `FLOONET_WRITE_PRICE_GRIN` | `0` | price of write access, in GRIN |
|
||||
| `GOBLINPAY_URL` / `GOBLINPAY_TOKEN` | unset | your GoblinPay server |
|
||||
| `GOBLINPAY_WEBHOOK_SECRET` | unset | enables the webhook receiver |
|
||||
| `COMPOSE_PROFILES` | unset | `exit` also runs the mixnet exit |
|
||||
| `COMPOSE_PROFILES` | unset | `tor` also runs a Tor onion in front of the relay |
|
||||
|
||||
## Note for Goblin wallet users
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# Hardened systemd unit for the bundled mixnet exit on bare metal.
|
||||
#
|
||||
# Install:
|
||||
# cd mixexit && cargo build --release
|
||||
# sudo install -m0755 target/release/floonet-mixexit /usr/local/bin/
|
||||
# sudo install -m0644 ../deploy/systemd/floonet-mixexit.service /etc/systemd/system/
|
||||
# sudo systemctl daemon-reload && sudo systemctl enable --now floonet-mixexit
|
||||
#
|
||||
# The exit pipes every accepted mixnet stream to ONE fixed upstream (your own
|
||||
# relay's TLS front) and honors no per-stream targets, so it is structurally
|
||||
# not an open proxy. Its mixnet identity persists in the state directory:
|
||||
# back it up, losing it rotates the exit's address and strands wallet pins.
|
||||
# After first start, publish the address from
|
||||
# /var/lib/floonet-mixexit/nym_address.txt in your relay pool listing.
|
||||
|
||||
[Unit]
|
||||
Description=floonet-mixexit (scoped mixnet exit for the co-located relay)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
DynamicUser=yes
|
||||
|
||||
# Where the exit pipes accepted streams: your relay's public TLS host:port.
|
||||
Environment=FLOONET_EXIT_UPSTREAM=127.0.0.1:443
|
||||
|
||||
# Persistent mixnet identity at /var/lib/floonet-mixexit.
|
||||
StateDirectory=floonet-mixexit
|
||||
StateDirectoryMode=0750
|
||||
Environment=FLOONET_MIXEXIT_DIR=/var/lib/floonet-mixexit
|
||||
|
||||
ExecStart=/usr/local/bin/floonet-mixexit
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# --- hardening ---
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectClock=yes
|
||||
ProtectHostname=yes
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
LockPersonality=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources
|
||||
ReadWritePaths=/var/lib/floonet-mixexit
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,7 @@
|
||||
# Minimal Tor daemon that fronts the Floonet relay with an onion service.
|
||||
# Stock tor from Alpine, no patches; all config lives in the mounted torrc.
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache tor
|
||||
# tor starts as root only long enough to create/own its data dir, then drops
|
||||
# to the unprivileged `tor` user named in torrc (`User tor`).
|
||||
ENTRYPOINT ["tor", "-f", "/etc/tor/torrc"]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Tor onion service in front of the Floonet relay (docker-compose `tor`
|
||||
# profile). The hidden service forwards straight to strfry's websocket
|
||||
# listener over the compose network; no TLS is needed on that hop because
|
||||
# the onion transport is already encrypted and authenticated end to end.
|
||||
#
|
||||
# The .onion address is derived from the key tor writes under
|
||||
# HiddenServiceDir. It is printed to the tor logs on first start and lives on
|
||||
# the tor-data volume; back that volume up, since losing it rotates the
|
||||
# address. Publish the .onion so wallets can reach this relay over Tor.
|
||||
|
||||
User tor
|
||||
DataDirectory /var/lib/tor
|
||||
|
||||
HiddenServiceDir /var/lib/tor/floonet-relay
|
||||
# onion virtual port 80 (ws default) -> the relay container's ws listener
|
||||
HiddenServicePort 80 relay:7777
|
||||
+19
-20
@@ -10,8 +10,9 @@
|
||||
# - authority : the bundled name authority (name@domain -> pubkey, with
|
||||
# optional paid names / paid write access via GoblinPay)
|
||||
# - caddy : auto-TLS reverse proxy terminating HTTPS for both
|
||||
# - mixexit : OPTIONAL scoped mixnet exit (COMPOSE_PROFILES=exit), so
|
||||
# wallets can reach this relay over the mixnet
|
||||
# - tor : OPTIONAL Tor onion in front of the relay
|
||||
# (COMPOSE_PROFILES=tor), so wallets can reach it over Tor
|
||||
# without a Tor exit hop. See deploy/tor/torrc.
|
||||
#
|
||||
# Set FLOONET_DOMAIN / FLOONET_BASE_URL / FLOONET_RELAYS in `.env` (copy
|
||||
# .env.example) BEFORE bringing it up: Caddy obtains a certificate for
|
||||
@@ -86,30 +87,28 @@ services:
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
|
||||
# The optional co-located mixnet exit. Off unless the `exit` profile is
|
||||
# active (set COMPOSE_PROFILES=exit in .env, the package's exit toggle).
|
||||
# It pipes every accepted mixnet stream to this stack's own TLS front, so
|
||||
# wallets reach the relay over the mixnet with end-to-end TLS; the exit
|
||||
# sees only ciphertext and can reach nothing but this relay. Its stable
|
||||
# mixnet address is printed at startup and written to the volume's
|
||||
# nym_address.txt; publish that address (relay pool `exit` field) so
|
||||
# wallets can find it.
|
||||
mixexit:
|
||||
build: ./mixexit
|
||||
image: floonet-mixexit:latest
|
||||
# Optional Tor onion service in front of the relay. Off unless the `tor`
|
||||
# profile is active (set COMPOSE_PROFILES=tor in .env). It runs a plain
|
||||
# system tor daemon whose hidden service forwards straight to the relay's
|
||||
# websocket listener (no TLS on that hop: the onion transport is already
|
||||
# encrypted and authenticated end to end). Wallets can then reach the relay
|
||||
# over Tor without a Tor exit hop. The .onion address is derived from a key
|
||||
# in the tor-data volume and printed to the tor logs on first start; back
|
||||
# the volume up, since losing it rotates the address. See deploy/tor/torrc.
|
||||
tor:
|
||||
build: ./deploy/tor
|
||||
image: floonet-tor:latest
|
||||
restart: unless-stopped
|
||||
profiles: ["exit"]
|
||||
profiles: ["tor"]
|
||||
depends_on:
|
||||
- caddy
|
||||
environment:
|
||||
FLOONET_MIXEXIT_DIR: /data
|
||||
FLOONET_EXIT_UPSTREAM: ${FLOONET_EXIT_UPSTREAM:-caddy:443}
|
||||
- relay
|
||||
volumes:
|
||||
- mixexit-data:/data
|
||||
- ./deploy/tor/torrc:/etc/tor/torrc:ro
|
||||
- tor-data:/var/lib/tor
|
||||
|
||||
volumes:
|
||||
relay-data:
|
||||
authority-data:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
mixexit-data:
|
||||
tor-data:
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "floonet-mixexit"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
description = "Scoped mixnet exit bundled with a Floonet relay: pipes accepted mixnet streams to ONE fixed upstream (never arbitrary targets)."
|
||||
|
||||
## Pinned upstream nym rev. This is the same nym-sdk revision the Goblin
|
||||
## wallet builds against (its `goblin` branch is this rev plus one
|
||||
## Android-only TLS-roots commit), so both ends speak the same MixnetStream
|
||||
## protocol. Developing against a local nym checkout instead:
|
||||
## cargo build --config 'patch."https://github.com/nymtech/nym".nym-sdk.path="../../nym/sdk/rust/nym-sdk"'
|
||||
[dependencies]
|
||||
nym-sdk = { git = "https://github.com/nymtech/nym", rev = "b6eb391e85be7eb8fca62def6d1ac32fd1108c30" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "signal"] }
|
||||
## Only to surface nym-sdk's tracing logs (RUST_LOG-style filtering).
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
@@ -1,34 +0,0 @@
|
||||
# Builds the bundled mixnet exit (floonet-mixexit): a scoped, unbonded mixnet
|
||||
# client that accepts incoming mixnet streams and pipes every one of them to
|
||||
# ONE fixed upstream, the TLS front of this very stack. It is structurally not
|
||||
# an open proxy: per-stream targets are never honored, so running it carries
|
||||
# no open-proxy liability and needs no exit policy.
|
||||
#
|
||||
# Enabled by the `exit` compose profile (COMPOSE_PROFILES=exit in .env).
|
||||
# Note: the first build compiles the pinned nym-sdk from source; expect it to
|
||||
# take a while.
|
||||
FROM rust:1-bookworm AS builder
|
||||
WORKDIR /build
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends pkg-config libssl-dev protobuf-compiler \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY Cargo.toml ./
|
||||
COPY src ./src
|
||||
RUN cargo build --release
|
||||
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# Non-root; the persistent mixnet identity lives under /data. Back that
|
||||
# directory up: losing it rotates the exit's mixnet address and strands
|
||||
# wallet pins until their next relay-pool refresh.
|
||||
RUN useradd --system --uid 10001 --home-dir /data --shell /usr/sbin/nologin mixexit \
|
||||
&& mkdir -p /data \
|
||||
&& chown -R mixexit:mixexit /data
|
||||
COPY --from=builder /build/target/release/floonet-mixexit /usr/local/bin/floonet-mixexit
|
||||
USER mixexit
|
||||
WORKDIR /data
|
||||
VOLUME ["/data"]
|
||||
ENV FLOONET_MIXEXIT_DIR=/data
|
||||
ENTRYPOINT ["/usr/local/bin/floonet-mixexit"]
|
||||
@@ -1,2 +0,0 @@
|
||||
hard_tabs = true
|
||||
edition = "2024"
|
||||
@@ -1,184 +0,0 @@
|
||||
// Copyright 2026 The Goblin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! floonet-mixexit — the SCOPED Nym exit bundled with a Floonet relay.
|
||||
//!
|
||||
//! An ordinary UNBONDED mixnet client (no nym-node, no pledge, no directory
|
||||
//! listing) that accepts incoming [`MixnetStream`]s and pipes each one to ONE
|
||||
//! fixed upstream — the operator's own relay. No per-stream target or host
|
||||
//! header is honored, so this is structurally NOT an open proxy: the only
|
||||
//! thing it can ever reach is the configured relay, which is why operators
|
||||
//! carry zero open-proxy liability and need no exit policy.
|
||||
//!
|
||||
//! The mixnet identity persists in `FLOONET_MIXEXIT_DIR`, so `nym_address()`
|
||||
//! is STABLE across restarts — that address is what wallets pin (relay-pool
|
||||
//! `exit` field / NIP-11 `nym_exit`). Wallets run hostname-validated TLS
|
||||
//! (SNI = the relay host) end-to-end THROUGH the pipe, so this exit sees only
|
||||
//! ciphertext. Design: ~/.claude/plans/floonet-nym-exit.md.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use nym_sdk::mixnet::{MixnetClientBuilder, MixnetStream, StoragePaths};
|
||||
use tokio::io::copy_bidirectional;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
const USAGE: &str = "\
|
||||
floonet-mixexit — scoped Nym exit for a Floonet relay
|
||||
|
||||
Accepts incoming mixnet streams and pipes each one to ONE fixed upstream
|
||||
(the co-located relay). Per-stream targets are never honored, so this is
|
||||
structurally not an open proxy. The mixnet identity persists in the data
|
||||
dir, keeping the nym address stable across restarts.
|
||||
|
||||
USAGE:
|
||||
floonet-mixexit [--help | --selftest]
|
||||
|
||||
MODES:
|
||||
(none) serve: accept mixnet streams, pipe each to the upstream
|
||||
--selftest connect to the mixnet, print the (stable) nym address and
|
||||
exit — never touches the upstream
|
||||
--help this text
|
||||
|
||||
ENVIRONMENT:
|
||||
FLOONET_MIXEXIT_DIR data dir for the persistent mixnet identity;
|
||||
the nym address is also written to
|
||||
<dir>/nym_address.txt [default: ./mixexit-data]
|
||||
FLOONET_EXIT_UPSTREAM fixed host:port every stream is piped to
|
||||
[default: relay.goblin.st:443]
|
||||
RUST_LOG nym-sdk log filter [default: warn]
|
||||
";
|
||||
|
||||
/// Data dir for the persistent mixnet identity (`FLOONET_MIXEXIT_DIR`).
|
||||
fn data_dir() -> PathBuf {
|
||||
std::env::var_os("FLOONET_MIXEXIT_DIR")
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|| PathBuf::from("./mixexit-data"))
|
||||
}
|
||||
|
||||
/// The ONE upstream every stream is piped to (`FLOONET_EXIT_UPSTREAM`).
|
||||
fn upstream() -> String {
|
||||
std::env::var("FLOONET_EXIT_UPSTREAM").unwrap_or_else(|_| "relay.goblin.st:443".to_string())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mode = std::env::args().nth(1);
|
||||
match mode.as_deref() {
|
||||
Some("--help" | "-h") => {
|
||||
print!("{USAGE}");
|
||||
return Ok(());
|
||||
}
|
||||
None | Some("--selftest") => {}
|
||||
Some(other) => {
|
||||
eprintln!("unknown argument: {other}\n\n{USAGE}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "warn".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
// Persistent identity: same data dir → same keystore (generated on first
|
||||
// run) → the SAME nym address across restarts. That address is what
|
||||
// wallets pin, so back this directory up — losing it rotates the address
|
||||
// and strands wallet pins until the next pool/NIP-11 refresh.
|
||||
let dir = data_dir();
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let storage_paths = StoragePaths::new_from_dir(&dir)?;
|
||||
let mut client = MixnetClientBuilder::new_with_default_storage(storage_paths)
|
||||
.await?
|
||||
.build()?
|
||||
.connect_to_mixnet()
|
||||
.await?;
|
||||
|
||||
let address = *client.nym_address();
|
||||
let address_file = dir.join("nym_address.txt");
|
||||
std::fs::write(&address_file, format!("{address}\n"))?;
|
||||
println!("=============================================================");
|
||||
println!(" floonet-mixexit is on the mixnet. Nym address (STABLE — pin");
|
||||
println!(" this in the relay pool `exit` field / NIP-11 `nym_exit`):");
|
||||
println!(" {address}");
|
||||
println!(" also written to {}", address_file.display());
|
||||
println!("=============================================================");
|
||||
|
||||
if mode.as_deref() == Some("--selftest") {
|
||||
println!("selftest OK");
|
||||
client.disconnect().await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let upstream = upstream();
|
||||
println!("piping every accepted stream to fixed upstream {upstream}");
|
||||
|
||||
let mut listener = client.listener()?;
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown_signal() => {
|
||||
println!("shutdown signal received; stopping");
|
||||
break;
|
||||
}
|
||||
accepted = listener.accept() => match accepted {
|
||||
Some(stream) => {
|
||||
let upstream = upstream.clone();
|
||||
tokio::spawn(pipe(stream, upstream));
|
||||
}
|
||||
None => {
|
||||
eprintln!("mixnet stream router stopped; exiting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.disconnect().await;
|
||||
println!("floonet-mixexit stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One accepted stream: TCP to the FIXED upstream (never a caller-chosen
|
||||
/// target), then bytes both ways until either side closes. Errors are logged
|
||||
/// and drop only this stream — the accept loop keeps serving.
|
||||
async fn pipe(mut mix: MixnetStream, upstream: String) {
|
||||
let mut tcp = match TcpStream::connect(&upstream).await {
|
||||
Ok(tcp) => tcp,
|
||||
Err(e) => {
|
||||
eprintln!("stream dropped: upstream {upstream} connect failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match copy_bidirectional(&mut mix, &mut tcp).await {
|
||||
Ok((up, down)) => println!("stream closed ({up} B in → relay, {down} B relay → out)"),
|
||||
Err(e) => eprintln!("stream ended with error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves on SIGINT (Ctrl-C) or SIGTERM (systemd/docker stop).
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = tokio::signal::ctrl_c();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut term = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("SIGTERM handler");
|
||||
tokio::select! {
|
||||
_ = ctrl_c => {}
|
||||
_ = term.recv() => {}
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = ctrl_c.await;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user