1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
use std::net::SocketAddr;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
use crate::mesh::{DiscoveryChoice, RelayChoice};
pub const DEFAULT_MTU: u32 = 1160;
pub const DEFAULT_TUN_NAME: &str = "swmesh0";
pub const DEFAULT_SECRET_FILE: &str = "/var/lib/socialweb/meshd/secret";
pub const DEFAULT_METRICS_LISTEN: &str = "127.0.0.1:9469";
pub const EXIT_OK: i32 = 0;
pub const EXIT_RUNTIME: i32 = 1;
pub const EXIT_USAGE: i32 = 2;
pub const EXIT_IDENTITY_OR_PEERS: i32 = 3;
pub const EXIT_KUBE: i32 = 4;
pub const EXIT_NETWORK_DEVICE: i32 = 5;
pub const EXIT_ENDPOINT: i32 = 6;
pub const EXIT_STATUS_LISTEN: i32 = 7;
pub const EXIT_CODES_HELP: &str = "\
Exit codes:
0 stopped by SIGTERM or SIGINT
1 a running task ended (a source task or the outbound loop stopped)
2 usage error
3 the secret file or the peers file is unreadable or malformed
4 the kubeconfig is malformed or refused (insecure-skip-tls-verify, no CA), or patching
this node's own Node was forbidden (--node-name is not the kubeconfig's node)
5 the tun device could not be created, brought up or read (needs CAP_NET_ADMIN and
/dev/net/tun; a name already in use is refused). A route or address that fails to
install is not fatal: it is retried on the next Node change, counted and logged
6 the iroh endpoint could not bind
7 the --metrics-listen address could not bind
Not fatal, retried: a missing kubeconfig, an unreachable apiserver, a list forbidden by
the node authorizer, an own Node not yet registered or without a podCIDR.";
pub const RUN_HELP: &str = "\
Node annotations written (kube mode) and read on every Node:
socialweb.computer/mesh-endpoint iroh endpoint id, 64 hex chars
socialweb.computer/mesh-did did:key of the same ed25519 key
socialweb.computer/mesh-addrs optional, read only: comma-separated direct ip:port hints
Status (loopback): GET /status (JSON), /metrics (Prometheus text), /healthz (200 once ready).
Ready means: tun up, own Node seen with a podCIDR, own annotations visible (kube mode), routes
synced. READY=1 is sent to $NOTIFY_SOCKET then (systemd Type=notify).";
#[derive(Debug, Parser)]
#[command(
name = "socialweb-meshd",
version,
about = "Pod network for socialweb.computer market nodes: each peer Node's podCIDR is routed through a tun device to that Node's iroh endpoint, one IP packet per QUIC datagram",
after_help = EXIT_CODES_HELP
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
#[command(about = "Run the mesh daemon", after_help = format!("{RUN_HELP}\n\n{EXIT_CODES_HELP}"))]
Run(Box<RunArgs>),
#[command(
about = "Print the endpoint id and did:key of the secret file as JSON, creating the secret (0600) if absent"
)]
Identity(IdentityArgs),
}
#[derive(Debug, Args)]
pub struct IdentityArgs {
#[arg(long, env = "SOCIALWEB_MESHD_SECRET_FILE", default_value = DEFAULT_SECRET_FILE, help = "ed25519 secret as 64 hex chars (the IROH_SECRET encoding)")]
pub secret_file: PathBuf,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum DiscoveryArg {
N0,
None,
}
impl From<DiscoveryArg> for DiscoveryChoice {
fn from(value: DiscoveryArg) -> DiscoveryChoice {
match value {
DiscoveryArg::N0 => DiscoveryChoice::N0,
DiscoveryArg::None => DiscoveryChoice::None,
}
}
}
pub fn parse_relay(text: &str) -> Result<RelayChoice, String> {
match text {
"default" => Ok(RelayChoice::Default),
"disabled" => Ok(RelayChoice::Disabled),
urls => {
let parsed: Result<Vec<iroh::RelayUrl>, String> = urls
.split(',')
.map(str::trim)
.filter(|part| !part.is_empty())
.map(|part| {
part.parse::<iroh::RelayUrl>()
.map_err(|err| format!("relay url {part:?}: {err}"))
})
.collect();
let parsed = parsed?;
if parsed.is_empty() {
return Err("relay must be default, disabled or one or more URLs".to_string());
}
Ok(RelayChoice::Custom(parsed))
}
}
}
#[derive(Debug, Args)]
#[command(group(clap::ArgGroup::new("source").required(true).args(["kubeconfig", "peers_file"])))]
pub struct RunArgs {
#[arg(
long,
env = "SOCIALWEB_MESHD_NODE_NAME",
help = "This node's Kubernetes Node name (the kubelet's --hostname-override / node name)"
)]
pub node_name: String,
#[arg(
long,
env = "SOCIALWEB_MESHD_KUBECONFIG",
help = "Kubelet kubeconfig: reads all Nodes, patches its own Node's annotations"
)]
pub kubeconfig: Option<PathBuf>,
#[arg(
long,
env = "SOCIALWEB_MESHD_PEERS_FILE",
help = "Test mode instead of --kubeconfig: a NodeList JSON file (kubectl get nodes -o json shape), re-read when it changes; nothing is written"
)]
pub peers_file: Option<PathBuf>,
#[arg(long, env = "SOCIALWEB_MESHD_SECRET_FILE", default_value = DEFAULT_SECRET_FILE, help = "ed25519 secret as 64 hex chars (the IROH_SECRET encoding); generated 0600 if absent")]
pub secret_file: PathBuf,
#[arg(long, env = "SOCIALWEB_MESHD_TUN_NAME", default_value = DEFAULT_TUN_NAME, help = "tun device to create")]
pub tun_name: String,
#[arg(long, env = "SOCIALWEB_MESHD_MTU", default_value_t = DEFAULT_MTU, help = "tun MTU; the default fits one IP packet into one QUIC datagram on a 1200-byte path")]
pub mtu: u32,
#[arg(long, env = "SOCIALWEB_MESHD_METRICS_LISTEN", default_value = DEFAULT_METRICS_LISTEN, help = "Status and metrics HTTP listen address; keep it on loopback")]
pub metrics_listen: SocketAddr,
#[arg(long, env = "SOCIALWEB_MESHD_RELAY", default_value = "default", value_parser = parse_relay, help = "iroh relays: default (n0 production relays), disabled, or comma-separated relay URLs")]
pub relay: RelayChoice,
#[arg(
long,
env = "SOCIALWEB_MESHD_DISCOVERY",
value_enum,
default_value = "n0",
help = "iroh address lookup: n0 (publish and resolve through n0 DNS/pkarr) or none"
)]
pub discovery: DiscoveryArg,
#[arg(
long = "bind",
env = "SOCIALWEB_MESHD_BIND",
value_delimiter = ',',
help = "UDP socket address(es) for iroh; default: iroh's own (all interfaces, ephemeral port)"
)]
pub bind: Vec<SocketAddr>,
#[arg(
long,
env = "RUST_LOG",
default_value = "info,iroh=warn,noq=warn,noq_proto=warn,iroh_relay=warn",
help = "tracing filter for the JSON log lines on stderr"
)]
pub log_filter: String,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn the_cli_definition_is_consistent() {
Cli::command().debug_assert();
}
#[test]
fn run_needs_exactly_one_of_kubeconfig_and_peers_file() {
let base = ["socialweb-meshd", "run", "--node-name", "a"];
assert!(Cli::try_parse_from(base).is_err());
let mut both = base.to_vec();
both.extend(["--kubeconfig", "k", "--peers-file", "p"]);
assert!(Cli::try_parse_from(both).is_err());
let mut kube = base.to_vec();
kube.extend([
"--kubeconfig",
"/var/lib/rancher/k3s/agent/kubelet.kubeconfig",
]);
let parsed = Cli::try_parse_from(kube).unwrap();
let Command::Run(args) = parsed.command else {
panic!("not run")
};
assert_eq!(args.tun_name, DEFAULT_TUN_NAME);
assert_eq!(args.mtu, DEFAULT_MTU);
assert_eq!(args.relay, RelayChoice::Default);
assert_eq!(args.discovery, DiscoveryArg::N0);
assert!(args.bind.is_empty());
}
#[test]
fn relay_accepts_default_disabled_or_urls() {
assert_eq!(parse_relay("default").unwrap(), RelayChoice::Default);
assert_eq!(parse_relay("disabled").unwrap(), RelayChoice::Disabled);
match parse_relay("https://relay.example.com,https://r2.example.com").unwrap() {
RelayChoice::Custom(urls) => assert_eq!(urls.len(), 2),
other => panic!("{other:?}"),
}
assert!(parse_relay("not a url").is_err());
}
}