TLS guardrails (insecure http refusal, custom CAs) and deployment guide
This commit is contained in:
@@ -3,7 +3,8 @@
|
|||||||
## Repo shape
|
## Repo shape
|
||||||
- `rfc.txt` (FRX — Federated Retrieval Exchange, Draft 0.5) is the normative spec; `src/` is the Phase 1 `frxd` implementation (single crate, two binaries).
|
- `rfc.txt` (FRX — Federated Retrieval Exchange, Draft 0.5) is the normative spec; `src/` is the Phase 1 `frxd` implementation (single crate, two binaries).
|
||||||
- `frxd` is the member node (init/add/index/serve/relay/query/status); `frx` is the thin client (search/query/status). Relay and node roles are separate subcommands.
|
- `frxd` is the member node (init/add/index/serve/relay/query/status); `frx` is the thin client (search/query/status). Relay and node roles are separate subcommands.
|
||||||
- Commands: `cargo build`, `cargo test` (90 tests: unit in `src/`; e2e `tests/phase1.rs`; conformance `tests/conformance.rs`; aggregates + member directory `tests/aggregates.rs`; registry `tests/registry.rs`; federation/isolation/admission `tests/federation.rs`; SSE `tests/streaming.rs`; encrypted unicast `tests/encryption.rs`; concurrency/restart `tests/concurrency.rs`; real subprocess CLI `tests/cli.rs`; 1000-doc `tests/scale.rs`; purge-log absence `tests/purges.rs`; shared fixtures `tests/common/mod.rs`). No CI/lint config.
|
- `DEPLOY.md` documents the TLS/deployment story: members need no TLS (outbound HTTPS), relays terminate TLS with Caddy or a tunnel, `ca_cert` adds private CAs, `allow_insecure` opts into plain http on private networks, and non-loopback `http://` is refused by default.
|
||||||
|
- Commands: `cargo build`, `cargo test` (97 tests: unit in `src/`; e2e `tests/phase1.rs`; conformance `tests/conformance.rs`; aggregates + member directory `tests/aggregates.rs`; registry `tests/registry.rs`; federation/isolation/admission `tests/federation.rs`; SSE `tests/streaming.rs`; encrypted unicast `tests/encryption.rs`; concurrency/restart `tests/concurrency.rs`; real subprocess CLI `tests/cli.rs`; 1000-doc `tests/scale.rs`; purge-log absence `tests/purges.rs`; shared fixtures `tests/common/mod.rs`). No CI/lint config.
|
||||||
- E2E pattern: relay + nodes in-process on ephemeral ports with tempdir corpora; use `tests/common/mod.rs` helpers (`spawn_relay*`, `query_envelope`, `poll_messages`, `register`) for new coverage. Raw relay polls return envelopes (payload under `body`), not response bodies.
|
- E2E pattern: relay + nodes in-process on ephemeral ports with tempdir corpora; use `tests/common/mod.rs` helpers (`spawn_relay*`, `query_envelope`, `poll_messages`, `register`) for new coverage. Raw relay polls return envelopes (payload under `body`), not response bodies.
|
||||||
|
|
||||||
## Editing the spec
|
## Editing the spec
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ tokio = { version = "1.53.1", features = ["full"] }
|
|||||||
toml = "1.1.6"
|
toml = "1.1.6"
|
||||||
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bytes = "1.12.1"
|
bytes = "1.12.1"
|
||||||
tempfile = "3.27.0"
|
tempfile = "3.27.0"
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Deploying FRX
|
||||||
|
|
||||||
|
Three roles: **member node** (`frxd serve`), **relay** (`frxd relay`), **registry** (`frxd registry`, run by the MA).
|
||||||
|
|
||||||
|
TLS is only a concern for servers. Members connect outbound and need no domain, port, or certificate.
|
||||||
|
|
||||||
|
## Member node (zero TLS work)
|
||||||
|
|
||||||
|
```
|
||||||
|
frxd init --name alice --id alice.frx.example \
|
||||||
|
--registry https://ma.example.com/registry.json --ma-key <ma-hex> \
|
||||||
|
--relay https://relay.example.com --data-dir ./alice-data
|
||||||
|
frxd add ~/documents --name docs --shared --exposure full
|
||||||
|
frxd serve
|
||||||
|
```
|
||||||
|
|
||||||
|
The node connects outbound over HTTPS, verifies the registry with the pinned MA key, and holds an SSE stream per relay. Nothing inbound, no DNS, no certificates. Members at departmental level can start here.
|
||||||
|
|
||||||
|
## Relay with TLS (one line)
|
||||||
|
|
||||||
|
Run `frxd` on loopback and terminate TLS with Caddy:
|
||||||
|
|
||||||
|
```
|
||||||
|
caddy reverse-proxy --from relay.example.com --to 127.0.0.1:7700
|
||||||
|
```
|
||||||
|
|
||||||
|
Caddyfile equivalent:
|
||||||
|
|
||||||
|
```
|
||||||
|
relay.example.com {
|
||||||
|
reverse_proxy 127.0.0.1:7700
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Relay command (peers and registry gated by the MA):
|
||||||
|
|
||||||
|
```
|
||||||
|
frxd relay --listen 127.0.0.1:7700 \
|
||||||
|
--url https://relay.example.com \
|
||||||
|
--peer https://relay2.example.com \
|
||||||
|
--registry https://ma.example.com/registry.json --ma-key <ma-hex>
|
||||||
|
```
|
||||||
|
|
||||||
|
No domain or open ports? Tunnel it:
|
||||||
|
|
||||||
|
```
|
||||||
|
frxd relay --listen 127.0.0.1:7700 --allow-insecure
|
||||||
|
cloudflared tunnel --url http://127.0.0.1:7700
|
||||||
|
```
|
||||||
|
|
||||||
|
`systemd` unit example:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=FRX relay
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/frxd relay --listen 127.0.0.1:7700 \
|
||||||
|
--url https://relay.example.com \
|
||||||
|
--registry https://ma.example.com/registry.json --ma-key <ma-hex>
|
||||||
|
Restart=on-failure
|
||||||
|
DynamicUser=yes
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
The same unit shape works for `frxd serve` (add `--config /etc/frxd/frxd.toml`).
|
||||||
|
|
||||||
|
## Registry (MA)
|
||||||
|
|
||||||
|
```
|
||||||
|
frxd registry --dir /var/lib/frxd/registry init
|
||||||
|
frxd registry --dir /var/lib/frxd/registry add alice.frx.example <key> --enc-key <enc>
|
||||||
|
frxd registry --dir /var/lib/frxd/registry serve --listen 127.0.0.1:7800
|
||||||
|
```
|
||||||
|
|
||||||
|
Put the same Caddy in front, or distribute `registry.json` out of band (it is signed, so the channel does not matter). The snapshot is versioned; nodes reject rollback and fail static during outages.
|
||||||
|
|
||||||
|
## Private networks and custom CAs
|
||||||
|
|
||||||
|
- `ca_cert = "/etc/ssl/private-ca.pem"` in `[node]`, or `--ca-cert` on the relay: adds a private/corporate root CA for relay and registry connections.
|
||||||
|
- `allow_insecure = true` / `--allow-insecure`: explicit opt-in for plain `http://` on a VPN/LAN. Without it, non-loopback `http://` endpoints are refused at startup.
|
||||||
|
- `http://127.0.0.1` is always allowed for development.
|
||||||
|
|
||||||
|
## Static binary
|
||||||
|
|
||||||
|
```
|
||||||
|
rustup target add x86_64-unknown-linux-musl
|
||||||
|
cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
```
|
||||||
|
|
||||||
|
`[profile.release]` enables LTO and stripping. All dependencies are pure Rust, so the musl build has no system-library requirements.
|
||||||
|
|
||||||
|
## What TLS does and does not cover
|
||||||
|
|
||||||
|
Envelopes are signed and registry snapshots are MA-signed, so TLS is not what protects message authenticity or registry integrity. TLS protects traffic from network observers, authenticates the relay endpoint, and hides mailbox metadata. Unicast response bodies are already encrypted end-to-end to the recipient's X25519 key.
|
||||||
@@ -27,6 +27,10 @@ pub struct NodeSection {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ma_key: Option<String>,
|
pub ma_key: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub ca_cert: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub allow_insecure: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub dev_bootstrap: bool,
|
pub dev_bootstrap: bool,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub responder: bool,
|
pub responder: bool,
|
||||||
@@ -132,6 +136,8 @@ impl Config {
|
|||||||
relays,
|
relays,
|
||||||
registry: None,
|
registry: None,
|
||||||
ma_key: None,
|
ma_key: None,
|
||||||
|
ca_cert: None,
|
||||||
|
allow_insecure: false,
|
||||||
dev_bootstrap: false,
|
dev_bootstrap: false,
|
||||||
responder: true,
|
responder: true,
|
||||||
},
|
},
|
||||||
@@ -162,6 +168,14 @@ impl Config {
|
|||||||
PathBuf::from(&self.index.data_dir)
|
PathBuf::from(&self.index.data_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insecure_endpoints(&self) -> Vec<String> {
|
||||||
|
let mut urls = self.node.relays.clone();
|
||||||
|
if let Some(registry) = &self.node.registry {
|
||||||
|
urls.push(registry.clone());
|
||||||
|
}
|
||||||
|
crate::net::insecure_http_urls(urls)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn index_dir(&self) -> PathBuf {
|
pub fn index_dir(&self) -> PathBuf {
|
||||||
self.data_dir().join("index")
|
self.data_dir().join("index")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pub mod crypto;
|
|||||||
pub mod extract;
|
pub mod extract;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
pub mod net;
|
||||||
pub mod node;
|
pub mod node;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod relay;
|
pub mod relay;
|
||||||
|
|||||||
+16
@@ -39,6 +39,10 @@ enum Command {
|
|||||||
registry: Option<String>,
|
registry: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
ma_key: Option<String>,
|
ma_key: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
ca_cert: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
allow_insecure: bool,
|
||||||
},
|
},
|
||||||
Add {
|
Add {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@@ -78,6 +82,10 @@ enum Command {
|
|||||||
registry: Option<String>,
|
registry: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
ma_key: Option<String>,
|
ma_key: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
ca_cert: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
allow_insecure: bool,
|
||||||
#[arg(long, default_value_t = 3)]
|
#[arg(long, default_value_t = 3)]
|
||||||
max_hops: usize,
|
max_hops: usize,
|
||||||
},
|
},
|
||||||
@@ -194,6 +202,8 @@ async fn main() -> Result<()> {
|
|||||||
id,
|
id,
|
||||||
registry,
|
registry,
|
||||||
ma_key,
|
ma_key,
|
||||||
|
ca_cert,
|
||||||
|
allow_insecure,
|
||||||
} => {
|
} => {
|
||||||
if cli.config.exists() && !force {
|
if cli.config.exists() && !force {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -208,6 +218,8 @@ async fn main() -> Result<()> {
|
|||||||
config.node.id = id;
|
config.node.id = id;
|
||||||
config.node.registry = registry;
|
config.node.registry = registry;
|
||||||
config.node.ma_key = ma_key;
|
config.node.ma_key = ma_key;
|
||||||
|
config.node.ca_cert = ca_cert;
|
||||||
|
config.node.allow_insecure = allow_insecure;
|
||||||
if config.node.registry.is_none() {
|
if config.node.registry.is_none() {
|
||||||
config.node.dev_bootstrap = true;
|
config.node.dev_bootstrap = true;
|
||||||
println!(
|
println!(
|
||||||
@@ -262,6 +274,8 @@ async fn main() -> Result<()> {
|
|||||||
url,
|
url,
|
||||||
registry,
|
registry,
|
||||||
ma_key,
|
ma_key,
|
||||||
|
ca_cert,
|
||||||
|
allow_insecure,
|
||||||
max_hops,
|
max_hops,
|
||||||
} => {
|
} => {
|
||||||
let options = relay::RelayOptions {
|
let options = relay::RelayOptions {
|
||||||
@@ -270,6 +284,8 @@ async fn main() -> Result<()> {
|
|||||||
url,
|
url,
|
||||||
registry,
|
registry,
|
||||||
ma_key,
|
ma_key,
|
||||||
|
ca_cert,
|
||||||
|
allow_insecure,
|
||||||
max_hops,
|
max_hops,
|
||||||
};
|
};
|
||||||
let (listener, addr) = relay::bind(&listen).await?;
|
let (listener, addr) = relay::bind(&listen).await?;
|
||||||
|
|||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
pub fn build_client(ca_cert: Option<&Path>, timeout: Duration) -> Result<Client> {
|
||||||
|
let mut builder = Client::builder().timeout(timeout);
|
||||||
|
if let Some(path) = ca_cert {
|
||||||
|
let pem = std::fs::read(path)
|
||||||
|
.with_context(|| format!("reading CA certificate {}", path.display()))?;
|
||||||
|
let certificates =
|
||||||
|
reqwest::Certificate::from_pem_bundle(&pem).context("parsing CA certificate bundle")?;
|
||||||
|
for certificate in certificates {
|
||||||
|
builder = builder.add_root_certificate(certificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.build().context("building http client")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_loopback_url(url: &str) -> bool {
|
||||||
|
let rest = url.split_once("://").map(|(_, rest)| rest).unwrap_or(url);
|
||||||
|
let host_port = rest.split(['/', '?', '#']).next().unwrap_or("");
|
||||||
|
let host = if let Some(stripped) = host_port.strip_prefix('[') {
|
||||||
|
stripped.split(']').next().unwrap_or("")
|
||||||
|
} else {
|
||||||
|
host_port.split(':').next().unwrap_or("")
|
||||||
|
};
|
||||||
|
matches!(host, "127.0.0.1" | "localhost" | "::1")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insecure_http_urls<I, S>(urls: I) -> Vec<String>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = S>,
|
||||||
|
S: AsRef<str>,
|
||||||
|
{
|
||||||
|
urls.into_iter()
|
||||||
|
.map(|url| url.as_ref().to_string())
|
||||||
|
.filter(|url| url.starts_with("http://") && !is_loopback_url(url))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loopback_detection() {
|
||||||
|
assert!(is_loopback_url("http://127.0.0.1:7700"));
|
||||||
|
assert!(is_loopback_url("http://localhost:7700/v1"));
|
||||||
|
assert!(is_loopback_url("https://[::1]:7700"));
|
||||||
|
assert!(!is_loopback_url("http://10.0.0.1:7700"));
|
||||||
|
assert!(!is_loopback_url("https://relay.example.com"));
|
||||||
|
assert!(!is_loopback_url("http://relay.example.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insecure_filter_keeps_only_plain_non_loopback() {
|
||||||
|
let urls = vec![
|
||||||
|
"http://127.0.0.1:7700",
|
||||||
|
"https://relay.example.com",
|
||||||
|
"http://relay.example.com",
|
||||||
|
"http://10.0.0.1:7700",
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
insecure_http_urls(urls),
|
||||||
|
vec!["http://relay.example.com", "http://10.0.0.1:7700"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_ca_file_is_an_error() {
|
||||||
|
let result = build_client(
|
||||||
|
Some(Path::new("/nonexistent/ca.pem")),
|
||||||
|
Duration::from_secs(1),
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-4
@@ -119,10 +119,19 @@ impl Node {
|
|||||||
let members_mtime = fs::metadata(&members_path)
|
let members_mtime = fs::metadata(&members_path)
|
||||||
.and_then(|metadata| metadata.modified())
|
.and_then(|metadata| metadata.modified())
|
||||||
.ok();
|
.ok();
|
||||||
let client = reqwest::Client::builder()
|
if !config.node.allow_insecure {
|
||||||
.timeout(Duration::from_secs(15))
|
let insecure = config.insecure_endpoints();
|
||||||
.build()
|
if !insecure.is_empty() {
|
||||||
.context("building http client")?;
|
return Err(anyhow!(
|
||||||
|
"refusing plain http endpoints (use https, configure ca_cert, or set allow_insecure for private networks): {}",
|
||||||
|
insecure.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let client = crate::net::build_client(
|
||||||
|
config.node.ca_cert.as_deref().map(std::path::Path::new),
|
||||||
|
Duration::from_secs(15),
|
||||||
|
)?;
|
||||||
let registry = match (
|
let registry = match (
|
||||||
config.node.registry.as_deref(),
|
config.node.registry.as_deref(),
|
||||||
config.node.ma_key.as_deref(),
|
config.node.ma_key.as_deref(),
|
||||||
|
|||||||
+55
-3
@@ -31,6 +31,8 @@ pub struct RelayOptions {
|
|||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
pub registry: Option<String>,
|
pub registry: Option<String>,
|
||||||
pub ma_key: Option<String>,
|
pub ma_key: Option<String>,
|
||||||
|
pub ca_cert: Option<String>,
|
||||||
|
pub allow_insecure: bool,
|
||||||
pub max_hops: usize,
|
pub max_hops: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +44,8 @@ impl Default for RelayOptions {
|
|||||||
url: None,
|
url: None,
|
||||||
registry: None,
|
registry: None,
|
||||||
ma_key: None,
|
ma_key: None,
|
||||||
|
ca_cert: None,
|
||||||
|
allow_insecure: false,
|
||||||
max_hops: 3,
|
max_hops: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,6 +88,23 @@ impl Relay {
|
|||||||
if !options.peers.is_empty() && options.url.is_none() {
|
if !options.peers.is_empty() && options.url.is_none() {
|
||||||
return Err(anyhow!("--url is required when --peer is set"));
|
return Err(anyhow!("--url is required when --peer is set"));
|
||||||
}
|
}
|
||||||
|
if !options.allow_insecure {
|
||||||
|
let mut urls = options.peers.clone();
|
||||||
|
if let Some(registry) = &options.registry {
|
||||||
|
urls.push(registry.clone());
|
||||||
|
}
|
||||||
|
let insecure = crate::net::insecure_http_urls(urls);
|
||||||
|
if !insecure.is_empty() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"refusing plain http endpoints (use https, configure --ca-cert, or set --allow-insecure for private networks): {}",
|
||||||
|
insecure.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let client = crate::net::build_client(
|
||||||
|
options.ca_cert.as_deref().map(std::path::Path::new),
|
||||||
|
Duration::from_secs(5),
|
||||||
|
)?;
|
||||||
let relay = Arc::new(Self {
|
let relay = Arc::new(Self {
|
||||||
inner: Mutex::new(Inner {
|
inner: Mutex::new(Inner {
|
||||||
members: HashMap::new(),
|
members: HashMap::new(),
|
||||||
@@ -93,9 +114,7 @@ impl Relay {
|
|||||||
}),
|
}),
|
||||||
options,
|
options,
|
||||||
registry,
|
registry,
|
||||||
client: reqwest::Client::builder()
|
client,
|
||||||
.timeout(Duration::from_secs(5))
|
|
||||||
.build()?,
|
|
||||||
notify: Notify::new(),
|
notify: Notify::new(),
|
||||||
});
|
});
|
||||||
if let Some(watcher) = &relay.registry {
|
if let Some(watcher) = &relay.registry {
|
||||||
@@ -520,3 +539,36 @@ fn backpressure(status: StatusCode, missed: u64) -> Response {
|
|||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn refuses_plain_http_peers_off_loopback() {
|
||||||
|
let options = RelayOptions {
|
||||||
|
peers: vec!["http://10.0.0.1:7700".to_string()],
|
||||||
|
url: Some("http://10.0.0.1:7700".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(Relay::new(options).is_err());
|
||||||
|
|
||||||
|
let options = RelayOptions {
|
||||||
|
peers: vec!["http://10.0.0.1:7700".to_string()],
|
||||||
|
url: Some("http://10.0.0.1:7700".to_string()),
|
||||||
|
allow_insecure: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(Relay::new(options).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loopback_peers_need_no_opt_in() {
|
||||||
|
let options = RelayOptions {
|
||||||
|
peers: vec!["http://127.0.0.1:7701".to_string()],
|
||||||
|
url: Some("http://127.0.0.1:7700".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(Relay::new(options).is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -324,6 +324,40 @@ fn cli_key_rotation() {
|
|||||||
assert!(data.join("key.hex.bak").exists());
|
assert!(data.join("key.hex.bak").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_relay_refuses_plain_http_off_loopback() {
|
||||||
|
let output = frxd()
|
||||||
|
.args([
|
||||||
|
"relay",
|
||||||
|
"--listen",
|
||||||
|
"127.0.0.1:0",
|
||||||
|
"--url",
|
||||||
|
"http://10.0.0.1:1",
|
||||||
|
"--peer",
|
||||||
|
"http://10.0.0.1:2",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(!output.status.success());
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(stderr.contains("allow-insecure"), "{stderr}");
|
||||||
|
|
||||||
|
let port = common::free_port();
|
||||||
|
let _service = spawn_service(
|
||||||
|
&[
|
||||||
|
"relay".to_string(),
|
||||||
|
"--listen".to_string(),
|
||||||
|
format!("127.0.0.1:{port}"),
|
||||||
|
"--url".to_string(),
|
||||||
|
"http://10.0.0.1:1".to_string(),
|
||||||
|
"--peer".to_string(),
|
||||||
|
"http://10.0.0.1:2".to_string(),
|
||||||
|
"--allow-insecure".to_string(),
|
||||||
|
],
|
||||||
|
"relay listening",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_full_network_pipeline() {
|
fn cli_full_network_pipeline() {
|
||||||
let root = tempfile::tempdir().unwrap();
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ pub fn config_for(dir: &Path, name: &str, relay_url: &str) -> Config {
|
|||||||
relays: vec![relay_url.to_string()],
|
relays: vec![relay_url.to_string()],
|
||||||
registry: None,
|
registry: None,
|
||||||
ma_key: None,
|
ma_key: None,
|
||||||
|
ca_cert: None,
|
||||||
|
allow_insecure: false,
|
||||||
dev_bootstrap: true,
|
dev_bootstrap: true,
|
||||||
responder: true,
|
responder: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -448,6 +448,33 @@ async fn rotated_keys_are_accepted_through_previous_listing() {
|
|||||||
assert!(revoked.is_empty(), "revoked key was still accepted");
|
assert!(revoked.is_empty(), "revoked key was still accepted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
|
async fn plain_http_transport_is_refused_off_loopback() {
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let mut config = config_for(&root.path().join("alice"), "alice", "http://10.0.0.1:1");
|
||||||
|
let refused = Node::start(config.clone()).await;
|
||||||
|
assert!(
|
||||||
|
refused.is_err(),
|
||||||
|
"plain http to a non-loopback relay must be refused"
|
||||||
|
);
|
||||||
|
|
||||||
|
config.node.allow_insecure = true;
|
||||||
|
let node = Node::start(config).await.unwrap();
|
||||||
|
let outcome = frxd::node::control_query(
|
||||||
|
&format!("http://{}", node.addr),
|
||||||
|
"rust",
|
||||||
|
Some(5),
|
||||||
|
Some(100),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
outcome.pointer("/local/total").and_then(Value::as_u64),
|
||||||
|
Some(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
async fn stale_envelopes_are_rejected() {
|
async fn stale_envelopes_are_rejected() {
|
||||||
let relay_url = spawn_relay().await;
|
let relay_url = spawn_relay().await;
|
||||||
|
|||||||
Reference in New Issue
Block a user