The zone and the server, without any of the operating system yet. `dns::zone` decides what the answer is and knows nothing about packets or sockets, so the parts worth getting right are testable on their own: which names exist, that a neighbouring name like `evillab` is not inside `lab`, and the difference between a name that is absent and one that exists with nothing of the type asked for. Getting that last one wrong would teach a resolver to stop asking for the A record it could have had. Names come from signed state, which is the point: a member that is switched off still resolves, because its claim outlived the session. Only IPv4 is served. The IPv6 overlay address derives from a WireGuard key that travels in live announcements and is not in signed state, so it cannot be answered for an absent member, and answering for some members and not others depending on who happens to be online is worse than not answering. `dns::server` puts that on the wire with simple-dns, which is already in the tree through iroh — a packet codec rather than a server framework, which is the right size for answering A records from memory. respond() goes from bytes to bytes so everything done to a packet is tested without a socket. It is authoritative for one zone and refuses everything else: no recursion, no forwarding, no cache, so pointing a resolver here can never make it a path to the outside. A message that is not a question gets no reply at all, rather than making this a reflector for anyone who can spoof a source address, and ANY is answered as an address question rather than by dumping the zone. Answers too large for the client's UDP limit are truncated so a resolver retries over TCP instead of waiting; TCP reads are length-checked before allocating, timed out, and bounded in number. The zone is shared rather than copied in, so a member joining is one write instead of a rebind that would drop questions in flight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
3.3 KiB
TOML
85 lines
3.3 KiB
TOML
[package]
|
|
name = "tsunagi"
|
|
version = "0.1.0"
|
|
edition = "2024"
|
|
rust-version = "1.91"
|
|
license = "MIT OR Apache-2.0"
|
|
description = "Proof-of-concept library for small private mesh networks: persistent agent identity, deterministic network spaces, iroh-based control plane."
|
|
repository = "https://github.com/tsunagi-net/tsunagi"
|
|
readme = "README.md"
|
|
keywords = ["mesh", "p2p", "iroh", "networking"]
|
|
categories = ["network-programming"]
|
|
|
|
[features]
|
|
default = ["cli"]
|
|
# The `tsunagi` command line binary. Library users can opt out.
|
|
cli = ["dep:clap", "dep:anstream", "dep:anstyle", "dep:tracing-subscriber", "tokio/signal", "tun-device"]
|
|
# A real TUN device, so the WireGuard plugin can carry actual IP traffic.
|
|
# Needs CAP_NET_ADMIN at run time; without it the plugin still runs and its
|
|
# in-memory device can be used for tests.
|
|
tun-device = ["dep:tun", "dep:rtnetlink", "dep:caps", "dep:futures-util"]
|
|
|
|
[[bin]]
|
|
name = "tsunagi"
|
|
path = "src/bin/tsunagi.rs"
|
|
required-features = ["cli"]
|
|
|
|
[dependencies]
|
|
clap = { version = "4.5", features = ["derive", "env"], optional = true }
|
|
# Already in the tree through clap. `anstream` strips the escapes when stdout
|
|
# is not a terminal and turns on virtual terminal processing on Windows, so
|
|
# colour is never written where it would show up as rubbish.
|
|
anstream = { version = "1.0", optional = true }
|
|
anstyle = { version = "1.0", optional = true }
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true }
|
|
iroh = { version = "1.2", default-features = false, features = ["tls-ring"] }
|
|
tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros"] }
|
|
rusqlite = { version = "0.40", features = ["bundled"] }
|
|
hkdf = "0.13"
|
|
hmac = "0.13"
|
|
sha2 = "0.11"
|
|
subtle = "2.6"
|
|
zeroize = { version = "1.9", features = ["derive"] }
|
|
rand = "0.10"
|
|
serde = { version = "1.0", features = ["derive"] }
|
|
postcard = { version = "1.1", default-features = false, features = ["use-std"] }
|
|
# Already in the tree through iroh. A packet codec, not a DNS server: the
|
|
# zone logic is ours and a full server framework would be a large dependency
|
|
# for answering A records from memory.
|
|
simple-dns = "0.12"
|
|
data-encoding = "2.11"
|
|
hex = "0.4"
|
|
thiserror = "2.0"
|
|
tracing = "0.1"
|
|
fs4 = { version = "1.1", features = ["sync"] }
|
|
directories = "6.0"
|
|
# The real system hostname, without a libc call of our own: this crate is
|
|
# forbidden `unsafe` and will not make one.
|
|
gethostname = "1.1"
|
|
netwatch = "0.19.3"
|
|
bytes = "1.12.1"
|
|
boringtun = { version = "0.7.1", default-features = false }
|
|
tun = { version = "0.8", features = ["async"], optional = true }
|
|
|
|
# Linux-only interface provisioning. `rtnetlink` configures the interface in
|
|
# process, so no `ip` invocation is ever needed; `caps` keeps CAP_NET_ADMIN
|
|
# out of the effective set except during the moments it is used.
|
|
[target.'cfg(target_os = "linux")'.dependencies]
|
|
rtnetlink = { version = "0.23", optional = true }
|
|
caps = { version = "0.5", optional = true }
|
|
futures-util = { version = "0.3", default-features = false, optional = true }
|
|
|
|
[dev-dependencies]
|
|
tokio = { version = "1.53", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "process"] }
|
|
tempfile = "3.24"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
|
|
[lints.rust]
|
|
missing_docs = "warn"
|
|
unsafe_code = "forbid"
|
|
|
|
[lints.clippy]
|
|
unwrap_used = "warn"
|
|
expect_used = "warn"
|
|
panic = "warn"
|