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