1use crate::{
2 interface_config::{InterfaceConfig, InterfaceInfo, ServerInfo},
3 AddCidrOpts, AddDeleteAssociationOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree,
4 DeleteCidrOpts, EnableDisablePeerOpts, Endpoint, Error, Hostname, IpNetExt, ListenPortOpts,
5 Peer, PeerContents, RenameCidrOpts, RenamePeerOpts, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
6};
7use anyhow::anyhow;
8use colored::*;
9use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
10use innernet_publicip::Preference;
11use ipnet::IpNet;
12use once_cell::sync::Lazy;
13use std::{
14 fmt::{Debug, Display},
15 fs::{File, OpenOptions},
16 io,
17 net::{IpAddr, Ipv4Addr, SocketAddr},
18 str::FromStr,
19 time::SystemTime,
20};
21use wireguard_control::{InterfaceName, KeyPair};
22
23pub static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);
24
25pub fn ensure_interactive(prompt: &str) -> Result<(), io::Error> {
26 if atty::is(atty::Stream::Stdin) {
27 Ok(())
28 } else {
29 Err(io::Error::new(
30 io::ErrorKind::BrokenPipe,
31 format!("Prompt \"{prompt}\" failed because TTY isn't connected."),
32 ))
33 }
34}
35
36pub fn confirm(prompt: &str) -> Result<bool, dialoguer::Error> {
37 ensure_interactive(prompt)?;
38 Confirm::with_theme(&*THEME)
39 .wait_for_newline(true)
40 .with_prompt(prompt)
41 .default(false)
42 .interact()
43}
44
45pub fn select<'a, T: ToString>(
46 prompt: &str,
47 items: &'a [T],
48) -> Result<(usize, &'a T), dialoguer::Error> {
49 ensure_interactive(prompt)?;
50 let choice = Select::with_theme(&*THEME)
51 .with_prompt(prompt)
52 .items(items)
53 .interact()?;
54 Ok((choice, &items[choice]))
55}
56
57pub enum Prefill<T> {
58 Default(T),
59 Editable(String),
60 None,
61}
62
63pub fn input<T>(prompt: &str, prefill: Prefill<T>) -> Result<T, dialoguer::Error>
64where
65 T: Clone + FromStr + Display,
66 T::Err: Display + Debug,
67{
68 ensure_interactive(prompt)?;
69 let input = Input::with_theme(&*THEME);
70 match prefill {
71 Prefill::Default(value) => input.default(value),
72 Prefill::Editable(value) => input.with_initial_text(value),
73 _ => input,
74 }
75 .with_prompt(prompt)
76 .interact_text()
78}
79
80pub fn add_cidr(cidrs: &[Cidr], request: &AddCidrOpts) -> Result<Option<CidrContents>, Error> {
82 let parent_cidr = if let Some(ref parent_name) = request.parent {
83 cidrs
84 .iter()
85 .find(|cidr| &cidr.name == parent_name)
86 .ok_or_else(|| anyhow!("No parent CIDR with that name exists."))?
87 } else {
88 choose_cidr(cidrs, "Parent CIDR")?
89 };
90
91 let name = if let Some(ref name) = request.name {
92 name.clone()
93 } else {
94 input("Name", Prefill::None)?
95 };
96
97 let cidr = if let Some(cidr) = request.cidr {
98 cidr
99 } else {
100 input("CIDR", Prefill::None)?
101 };
102
103 let cidr_request = CidrContents {
104 name: name.to_string(),
105 cidr,
106 parent: Some(parent_cidr.id),
107 };
108
109 Ok(
110 if request.yes || confirm(&format!("Create CIDR \"{}\"?", cidr_request.name))? {
111 Some(cidr_request)
112 } else {
113 None
114 },
115 )
116}
117
118pub fn rename_cidr(
120 cidrs: &[Cidr],
121 args: &RenameCidrOpts,
122) -> Result<Option<(CidrContents, String)>, Error> {
123 let old_cidr = if let Some(ref name) = args.name {
124 cidrs
125 .iter()
126 .find(|c| &c.name == name)
127 .ok_or_else(|| anyhow!("CIDR '{}' does not exist", name))?
128 .clone()
129 } else {
130 let (cidr_index, _) = select(
131 "CIDR to rename",
132 &cidrs.iter().map(|ep| ep.name.clone()).collect::<Vec<_>>(),
133 )?;
134 cidrs[cidr_index].clone()
135 };
136 let old_name = old_cidr.name.clone();
137 let new_name = if let Some(ref name) = args.new_name {
138 name.clone()
139 } else {
140 input("New Name", Prefill::None)?
141 };
142
143 let mut new_cidr = old_cidr;
144 new_cidr.contents.name.clone_from(&new_name);
145
146 Ok(
147 if args.yes
148 || confirm(&format!(
149 "Rename CIDR {} to {}?",
150 old_name.yellow(),
151 new_name.yellow()
152 ))?
153 {
154 Some((new_cidr.contents, old_name))
155 } else {
156 None
157 },
158 )
159}
160
161pub fn delete_cidr(cidrs: &[Cidr], peers: &[Peer], request: &DeleteCidrOpts) -> Result<i64, Error> {
163 let eligible_cidrs: Vec<_> = cidrs
164 .iter()
165 .filter(|cidr| {
166 !peers.iter().any(|peer| peer.contents.cidr_id == cidr.id) &&
167 !cidrs.iter().any(
168 |cidr2| matches!(cidr2.contents.parent, Some(parent_id) if parent_id == cidr.id)
169 )
170 })
171 .collect();
172 let cidr = if let Some(ref name) = request.name {
173 cidrs
174 .iter()
175 .find(|cidr| &cidr.name == name)
176 .ok_or_else(|| anyhow!("CIDR {} doesn't exist or isn't eligible for deletion", name))?
177 } else {
178 select("Delete CIDR", &eligible_cidrs)?.1
179 };
180
181 if request.yes || confirm(&format!("Delete CIDR \"{}\"?", cidr.name))? {
182 Ok(cidr.id)
183 } else {
184 Err(anyhow!("Canceled"))
185 }
186}
187
188pub fn choose_cidr<'a>(cidrs: &'a [Cidr], text: &'static str) -> Result<&'a Cidr, Error> {
189 let eligible_cidrs: Vec<_> = cidrs
190 .iter()
191 .filter(|cidr| cidr.name != "innernet-server")
192 .collect();
193 Ok(select(text, &eligible_cidrs)?.1)
194}
195
196pub fn choose_association<'a>(
197 associations: &'a [Association],
198 cidrs: &'a [Cidr],
199 args: &AddDeleteAssociationOpts,
200) -> Result<&'a Association, Error> {
201 match (&args.cidr1, &args.cidr2) {
202 (Some(cidr1_name), Some(cidr2_name)) => {
203 let cidr1 = find_cidr(cidrs, cidr1_name)?;
204 let cidr2 = find_cidr(cidrs, cidr2_name)?;
205 associations
206 .iter()
207 .find(|association| {
208 (association.cidr_id_1 == cidr1.id && association.cidr_id_2 == cidr2.id)
209 || (association.cidr_id_1 == cidr2.id && association.cidr_id_2 == cidr1.id)
210 })
211 .ok_or_else(|| anyhow!("CIDR association does not exist"))
212 },
213 _ => {
214 let names: Vec<_> = associations
215 .iter()
216 .map(|association| {
217 format!(
218 "{}: {} <=> {}",
219 association.id,
220 &cidrs
221 .iter()
222 .find(|c| c.id == association.cidr_id_1)
223 .unwrap()
224 .name,
225 &cidrs
226 .iter()
227 .find(|c| c.id == association.cidr_id_2)
228 .unwrap()
229 .name
230 )
231 })
232 .collect();
233 let (index, _) = select("Association", &names)?;
234
235 Ok(&associations[index])
236 },
237 }
238}
239
240fn find_cidr<'a>(cidrs: &'a [Cidr], name: &str) -> Result<&'a Cidr, Error> {
241 cidrs
242 .iter()
243 .find(|c| c.name == name)
244 .ok_or_else(|| anyhow!("can't find cidr '{}'", name))
245}
246
247fn find_or_prompt_cidr<'a>(
248 cidrs: &'a [Cidr],
249 sub_opt: &Option<String>,
250 prompt: &'static str,
251) -> Result<&'a Cidr, Error> {
252 if let Some(name) = sub_opt {
253 find_cidr(cidrs, name)
254 } else {
255 choose_cidr(cidrs, prompt)
256 }
257}
258
259pub fn add_association<'a>(
260 cidrs: &'a [Cidr],
261 args: &AddDeleteAssociationOpts,
262) -> Result<Option<(&'a Cidr, &'a Cidr)>, Error> {
263 let cidr1 = find_or_prompt_cidr(cidrs, &args.cidr1, "First CIDR")?;
264 let cidr2 = find_or_prompt_cidr(cidrs, &args.cidr2, "Second CIDR")?;
265
266 Ok(
267 if args.yes
268 || confirm(&format!(
269 "Add association: {} <=> {}?",
270 cidr1.name.yellow().bold(),
271 cidr2.name.yellow().bold()
272 ))?
273 {
274 Some((cidr1, cidr2))
275 } else {
276 None
277 },
278 )
279}
280
281pub fn delete_association<'a>(
282 associations: &'a [Association],
283 cidrs: &'a [Cidr],
284 args: &AddDeleteAssociationOpts,
285) -> Result<Option<&'a Association>, Error> {
286 let association = choose_association(associations, cidrs, args)?;
287
288 Ok(
289 if args.yes || confirm(&format!("Delete association #{}?", association.id))? {
290 Some(association)
291 } else {
292 None
293 },
294 )
295}
296
297pub fn add_peer(
299 peers: &[Peer],
300 cidr_tree: &CidrTree,
301 args: &AddPeerOpts,
302) -> Result<Option<(PeerContents, KeyPair, String, File)>, Error> {
303 let leaves = cidr_tree.leaves();
304
305 let cidr = if let Some(ref parent_name) = args.cidr {
306 leaves
307 .iter()
308 .find(|cidr| &cidr.name == parent_name)
309 .ok_or_else(|| anyhow!("No eligible CIDR with that name exists."))?
310 } else {
311 choose_cidr(&leaves[..], "Eligible CIDRs for peer")?
312 };
313
314 let mut available_ip = None;
315 let candidate_ips = cidr.hosts().filter(|ip| cidr.is_assignable(ip));
316 for ip in candidate_ips {
317 if !peers.iter().any(|peer| peer.ip == ip) {
318 available_ip = Some(ip);
319 break;
320 }
321 }
322
323 let available_ip = available_ip.expect("No IPs in this CIDR are avavilable");
324
325 let ip = if let Some(ip) = args.ip {
326 ip
327 } else if args.auto_ip {
328 available_ip
329 } else {
330 input("IP", Prefill::Default(available_ip))?
331 };
332
333 let name = if let Some(ref name) = args.name {
334 name.clone()
335 } else {
336 input("Name", Prefill::None)?
337 };
338
339 let is_admin = if let Some(is_admin) = args.admin {
340 is_admin
341 } else {
342 confirm(&format!("Make {name} an admin?"))?
343 };
344
345 let invite_expires = if let Some(ref invite_expires) = args.invite_expires {
346 invite_expires.clone()
347 } else {
348 input(
349 "Invite expires after",
350 Prefill::Default("14d".parse().map_err(|s: &str| anyhow!(s))?),
351 )?
352 };
353
354 let invite_save_path = if let Some(ref location) = args.save_config {
355 location.clone()
356 } else {
357 input(
358 "Save peer invitation file to",
359 Prefill::Default(format!("{name}.toml")),
360 )?
361 };
362
363 let default_keypair = KeyPair::generate();
364 let peer_request = PeerContents {
365 name,
366 ip,
367 cidr_id: cidr.id,
368 public_key: default_keypair.public.to_base64(),
369 endpoint: None,
370 is_admin,
371 is_disabled: false,
372 is_redeemed: false,
373 persistent_keepalive_interval: Some(PERSISTENT_KEEPALIVE_INTERVAL_SECS),
374 invite_expires: Some(SystemTime::now() + invite_expires.into()),
375 candidates: vec![],
376 };
377
378 Ok(
379 if args.yes || confirm(&format!("Create peer {}?", peer_request.name.yellow()))? {
380 let invite_file = OpenOptions::new()
381 .read(true)
382 .write(true)
383 .create_new(true)
384 .open(&invite_save_path)?;
385 Some((peer_request, default_keypair, invite_save_path, invite_file))
386 } else {
387 None
388 },
389 )
390}
391
392pub fn rename_peer(
394 peers: &[Peer],
395 args: &RenamePeerOpts,
396) -> Result<Option<(PeerContents, Hostname)>, Error> {
397 let eligible_peers = peers
398 .iter()
399 .filter(|p| &*p.name != "innernet-server")
400 .collect::<Vec<_>>();
401 let old_peer = if let Some(ref name) = args.name {
402 eligible_peers
403 .into_iter()
404 .find(|p| &p.name == name)
405 .ok_or_else(|| anyhow!("Peer '{}' does not exist", name))?
406 .clone()
407 } else {
408 let (peer_index, _) = select(
409 "Peer to rename",
410 &eligible_peers
411 .iter()
412 .map(|ep| ep.name.clone())
413 .collect::<Vec<_>>(),
414 )?;
415 eligible_peers[peer_index].clone()
416 };
417 let old_name = old_peer.name.clone();
418 let new_name = if let Some(ref name) = args.new_name {
419 name.clone()
420 } else {
421 input("New Name", Prefill::None)?
422 };
423
424 let mut new_peer = old_peer;
425 new_peer.contents.name = new_name.clone();
426
427 Ok(
428 if args.yes
429 || confirm(&format!(
430 "Rename peer {} to {}?",
431 old_name.yellow(),
432 new_name.yellow()
433 ))?
434 {
435 Some((new_peer.contents, old_name))
436 } else {
437 None
438 },
439 )
440}
441
442pub fn enable_or_disable_peer(
445 peers: &[Peer],
446 args: &EnableDisablePeerOpts,
447 enable: bool,
448) -> Result<Option<Peer>, Error> {
449 let enabled_peers: Vec<_> = peers
450 .iter()
451 .filter(|peer| enable && peer.is_disabled || !enable && !peer.is_disabled)
452 .collect();
453
454 let peer = if let Some(ref name) = args.name {
455 enabled_peers
456 .into_iter()
457 .find(|p| &p.name == name)
458 .ok_or_else(|| anyhow!("Peer '{}' does not exist", name))?
459 } else {
460 let peer_selection: Vec<_> = enabled_peers
461 .iter()
462 .map(|peer| format!("{} ({})", &peer.name, &peer.ip))
463 .collect();
464 let (index, _) = select(
465 &format!("Peer to {}able", if enable { "en" } else { "dis" }),
466 &peer_selection,
467 )?;
468 enabled_peers[index]
469 };
470
471 Ok(
472 if args.yes
473 || confirm(&format!(
474 "{}able peer {}?",
475 if enable { "En" } else { "Dis" },
476 peer.name.yellow()
477 ))?
478 {
479 Some(peer.clone())
480 } else {
481 None
482 },
483 )
484}
485
486pub fn write_peer_invitation(
488 target_file: (&mut File, &str),
489 network_name: &InterfaceName,
490 peer: &Peer,
491 server_peer: &Peer,
492 root_cidr: &Cidr,
493 keypair: KeyPair,
494 server_api_addr: &SocketAddr,
495) -> Result<(), Error> {
496 let peer_invitation = InterfaceConfig {
497 interface: InterfaceInfo {
498 network_name: network_name.to_string(),
499 private_key: keypair.private.to_base64(),
500 address: IpNet::new(peer.ip, root_cidr.prefix_len())?,
501 listen_port: None,
502 },
503 server: ServerInfo {
504 external_endpoint: server_peer
505 .endpoint
506 .clone()
507 .expect("The innernet server should have a WireGuard endpoint"),
508 internal_endpoint: *server_api_addr,
509 public_key: server_peer.public_key.clone(),
510 },
511 };
512
513 peer_invitation.write_to(target_file.0, true, None)?;
514
515 println!(
516 "\nPeer \"{}\" added\n\
517 Peer invitation file written to {}\n\
518 Please send it to them securely (eg. via magic-wormhole) \
519 to bootstrap them onto the network.",
520 peer.name.bold(),
521 target_file.1.bold()
522 );
523
524 Ok(())
525}
526
527pub fn set_listen_port(
528 interface: &InterfaceInfo,
529 args: ListenPortOpts,
530) -> Result<Option<Option<u16>>, Error> {
531 let listen_port = if let Some(listen_port) = args.listen_port {
532 Some(listen_port)
533 } else if !args.unset {
534 Some(input(
535 "Listen port",
536 Prefill::Default(interface.listen_port.unwrap_or(51820)),
537 )?)
538 } else {
539 None
540 };
541
542 let confirmation = Confirm::with_theme(&*THEME)
543 .wait_for_newline(true)
544 .with_prompt(
545 &(if let Some(port) = &listen_port {
546 format!("Set listen port to {port}?")
547 } else {
548 "Unset and randomize listen port?".to_string()
549 }),
550 )
551 .default(false);
552
553 if listen_port == interface.listen_port {
554 println!("No change necessary - interface already has this setting.");
555 Ok(None)
556 } else if args.yes || confirmation.interact()? {
557 Ok(Some(listen_port))
558 } else {
559 Ok(None)
560 }
561}
562
563pub fn unspecified_ip_and_auto_detection_flow() -> Result<Option<IpAddr>, Error> {
564 if confirm_unspecified_ip_usage()? {
565 Ok(Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED)))
566 } else {
567 ip_auto_detection_flow()
568 }
569}
570
571pub fn ip_auto_detection_flow() -> Result<Option<IpAddr>, Error> {
572 let ip_addr = if confirm_ip_auto_detection()? {
573 innernet_publicip::get_any(Preference::Ipv4)
574 } else {
575 None
576 };
577
578 Ok(ip_addr)
579}
580
581fn confirm_ip_auto_detection() -> Result<bool, Error> {
582 let answer = Confirm::with_theme(&*THEME)
583 .wait_for_newline(true)
584 .with_prompt("Auto-detect external endpoint IP address (via DNS query to 9.9.9.9)?")
585 .interact()?;
586
587 Ok(answer)
588}
589
590fn confirm_unspecified_ip_usage() -> Result<bool, Error> {
591 log::info!(
592 "Note: use unspecified IP address (all zeros) if you do not have a fixed global IP but the \
593 port is forwarded."
594 );
595 let answer = Confirm::with_theme(&*THEME)
596 .wait_for_newline(true)
597 .with_prompt("Use an unspecified IP address and override just the port?")
598 .interact()?;
599
600 Ok(answer)
601}
602
603pub fn input_external_endpoint(
604 external_ip: Option<IpAddr>,
605 listen_port: u16,
606) -> Result<Endpoint, Error> {
607 let endpoint = input(
608 "External endpoint",
609 match external_ip {
610 Some(ip) => Prefill::Editable(SocketAddr::new(ip, listen_port).to_string()),
611 None => Prefill::None,
612 },
613 )?;
614
615 Ok(endpoint)
616}