Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ binda --gossip 0.0.0.0:9530 --resolver 0.0.0.0:9531 --api 0.0.0.0:9532 \
- `--peer` — a known peer's gossip address; repeatable. Peers also learn
about each other dynamically from inbound gossip, so only one bootstrap
peer per new node is typically needed.
- `--insecure-skip-rdns-verification` — accept every claimed `rdns`
hostname without checking it (see
[`fcrdns::AllowAllVerifier`](crates/binda-core/src/fcrdns.rs)). This is
for running a node locally, for demos and manual testing, where no
claimed hostname could ever genuinely forward-confirm. **Never pass
this on a node anyone else can reach** — it turns the "5 domains per
live socket" cap back into "5 domains per free keypair."

A node's clock is disciplined against public NTP servers
([`ntp::NtpTimeSource`](crates/binda-core/src/ntp.rs)) rather than trusting
Expand All @@ -121,10 +128,13 @@ See [`examples/client_demo.rs`](crates/binda/examples/client_demo.rs) for
a full walkthrough of a client: probe liveness, register a Unicode
domain, publish an A record, then resolve it both via BINDA's own
resolver protocol and via BINDA's native, DNS-shaped protocol — proving
the label crosses the wire as raw UTF-8, not Punycode.
the label crosses the wire as raw UTF-8, not Punycode. The node below is
started with `--insecure-skip-rdns-verification` so the demo's made-up
`rdns` claim is accepted and `register` actually succeeds; drop that flag
to see the real FCrDNS check correctly refuse it instead.

```bash
cargo run -p binda --bin binda -- --gossip 127.0.0.1:9530 --resolver 127.0.0.1:9531 --api 127.0.0.1:9532 --dns 127.0.0.1:9533 &
cargo run -p binda --bin binda -- --gossip 127.0.0.1:9530 --resolver 127.0.0.1:9531 --api 127.0.0.1:9532 --dns 127.0.0.1:9533 --insecure-skip-rdns-verification &
cargo run -p binda --example client_demo
```

Expand Down
28 changes: 28 additions & 0 deletions crates/binda-core/src/fcrdns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ impl RdnsVerifier for FcrdnsVerifier {
}
}

/// A verifier that accepts every claimed hostname without checking it
/// against anything.
///
/// **Never use this for a real deployment.** It exists purely so a node
/// can be run locally (on a laptop with no reverse DNS delegation, behind
/// NAT, on 127.0.0.1) for demos, examples, and manual testing, where no
/// claimed `rdns` could ever forward-confirm for real. With this
/// verifier installed, the "5 domains per live socket" cap once again
/// means nothing more than "5 domains per free keypair" — exactly the
/// gap [`FcrdnsVerifier`] exists to close. A node only uses this if it
/// was explicitly started with a flag that says so out loud (see the
/// `binda` binary's `--insecure-skip-rdns-verification`).
#[derive(Debug, Default, Clone, Copy)]
pub struct AllowAllVerifier;

impl RdnsVerifier for AllowAllVerifier {
fn verify(&self, _source_ip: IpAddr, _claimed_rdns: &str) -> bool {
true
}
}

/// Full FCrDNS check: `claimed_rdns` must appear in `source_ip`'s PTR
/// records, and a forward lookup of `claimed_rdns` must resolve back to
/// `source_ip`.
Expand Down Expand Up @@ -358,6 +379,13 @@ mod tests {
assert!(!verifier.verify(ip, "someone-else.example.net"));
}

#[test]
fn allow_all_verifier_accepts_any_claim() {
let ip: IpAddr = "203.0.113.7".parse().unwrap();
assert!(AllowAllVerifier.verify(ip, "literally-anything.invalid"));
assert!(AllowAllVerifier.verify(ip, ""));
}

fn encode_name(name: &str) -> Vec<u8> {
let mut buf = Vec::new();
for label in name.split('.') {
Expand Down
22 changes: 14 additions & 8 deletions crates/binda/examples/client_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
//! resolver protocol and via BINDA's native DNS-shaped wire protocol
//! (which carries the label as raw UTF-8 — no Punycode involved).
//!
//! Note: the `rdns` claimed below (`client-demo.example.net`) almost
//! certainly won't forward-confirm against wherever you actually run
//! this from (see `binda_core::fcrdns`), so `register` is expected to
//! come back as an `Error` unless you edit `rdns` to a hostname that
//! genuinely resolves back to your machine's real source IP.
//!
//! Run a node first, then this example against it:
//! The `rdns` claimed below (`client-demo.example.net`) is made up and
//! won't forward-confirm against wherever you actually run this from
//! (see `binda_core::fcrdns`) — that check needs a real, reverse-DNS-
//! delegated host, which a laptop or CI runner doesn't have. To see the
//! full flow succeed locally, start the node with
//! `--insecure-skip-rdns-verification` (see
//! [`binda_core::fcrdns::AllowAllVerifier`] for exactly what that turns
//! off, and why it must never be used on a node anyone else can reach):
//!
//! ```bash
//! cargo run -p binda --bin binda -- --gossip 127.0.0.1:9530 --resolver 127.0.0.1:9531 --api 127.0.0.1:9532 --dns 127.0.0.1:9533 &
//! cargo run -p binda --bin binda -- --gossip 127.0.0.1:9530 --resolver 127.0.0.1:9531 --api 127.0.0.1:9532 --dns 127.0.0.1:9533 --insecure-skip-rdns-verification &
//! cargo run -p binda --example client_demo
//! ```
//!
//! Run it *without* that flag instead if you want to see the FCrDNS
//! check do its job and correctly refuse the made-up `rdns` — `register`
//! will come back as an `Error` in that case, which is expected, not a
//! bug.

use std::net::UdpSocket;
use std::time::{SystemTime, UNIX_EPOCH};
Expand Down
36 changes: 35 additions & 1 deletion crates/binda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@
//! protocol, not RFC 1035; a production deployment might still bind it to
//! port 53 as a convenient, familiar-looking address, which needs
//! elevated privilege on most systems).
//!
//! `--insecure-skip-rdns-verification` disables the FCrDNS check a
//! `Register` request's claimed `rdns` normally has to pass (see
//! [`binda_core::fcrdns`]). It exists so a node can be run locally — on
//! a laptop, behind NAT, with no real reverse-DNS delegation — for
//! demos and manual testing, where no claimed hostname could ever
//! genuinely forward-confirm. **Never pass this flag on a node anyone
//! else can reach**: it turns the "5 domains per live socket" cap back
//! into "5 domains per free keypair."

// `main` itself is excluded from coverage accounting (see the attribute
// on it below): it's pure top-level wiring — parse args, construct a
Expand All @@ -40,6 +49,7 @@ struct Args {
api_addr: SocketAddr,
dns_addr: Option<SocketAddr>,
peers: Vec<SocketAddr>,
insecure_skip_rdns_verification: bool,
}

fn parse_args() -> Args {
Expand All @@ -55,6 +65,7 @@ fn parse_args_from(args: impl Iterator<Item = String>) -> Args {
let mut api_addr: SocketAddr = "0.0.0.0:9532".parse().unwrap();
let mut dns_addr: Option<SocketAddr> = None;
let mut peers = Vec::new();
let mut insecure_skip_rdns_verification = false;

let mut args = args;
while let Some(flag) = args.next() {
Expand Down Expand Up @@ -96,6 +107,9 @@ fn parse_args_from(args: impl Iterator<Item = String>) -> Args {
.expect("invalid --peer address");
peers.push(addr);
}
"--insecure-skip-rdns-verification" => {
insecure_skip_rdns_verification = true;
}
other => {
eprintln!("binda: ignoring unrecognized argument {other}");
}
Expand All @@ -108,6 +122,7 @@ fn parse_args_from(args: impl Iterator<Item = String>) -> Args {
api_addr,
dns_addr,
peers,
insecure_skip_rdns_verification,
}
}

Expand Down Expand Up @@ -171,7 +186,16 @@ async fn main() -> std::io::Result<()> {
let args = parse_args();
println!("binda (bind10) — starting node");

let node = Node::new(args.peers.clone());
let node = if args.insecure_skip_rdns_verification {
eprintln!(
"binda: WARNING — running with --insecure-skip-rdns-verification: \
every claimed rdns hostname is accepted unchecked. Never run a \
node reachable by anyone else this way."
);
Node::new_allowing_any_rdns(args.peers.clone())
} else {
Node::new(args.peers.clone())
};
node.spawn_rate_limiter_maintenance();
println!(
"binda: known peers at startup: {:?}",
Expand Down Expand Up @@ -203,6 +227,13 @@ mod tests {
assert_eq!(parsed.api_addr, "0.0.0.0:9532".parse().unwrap());
assert_eq!(parsed.dns_addr, None);
assert!(parsed.peers.is_empty());
assert!(!parsed.insecure_skip_rdns_verification);
}

#[test]
fn insecure_skip_rdns_verification_flag_is_recognized() {
let parsed = args(&["--insecure-skip-rdns-verification"]);
assert!(parsed.insecure_skip_rdns_verification);
}

#[test]
Expand Down Expand Up @@ -273,6 +304,7 @@ mod tests {
api_addr: "127.0.0.1:29592".parse().unwrap(),
dns_addr: Some("127.0.0.1:29593".parse().unwrap()),
peers: Vec::new(),
insecure_skip_rdns_verification: false,
};
let tasks = spawn_listener_tasks(&node, &parsed_args);
assert!(tasks.dns.is_some());
Expand All @@ -291,6 +323,7 @@ mod tests {
api_addr: "127.0.0.1:29596".parse().unwrap(),
dns_addr: None,
peers: Vec::new(),
insecure_skip_rdns_verification: false,
};
let tasks = spawn_listener_tasks(&node, &parsed_args);
assert!(tasks.dns.is_none());
Expand Down Expand Up @@ -321,6 +354,7 @@ mod tests {
api_addr,
dns_addr: Some(dns_addr),
peers: Vec::new(),
insecure_skip_rdns_verification: false,
};
let tasks = spawn_listener_tasks(&node, &parsed_args);

Expand Down
Loading
Loading