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