innernet_shared/
prompts.rs

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
79/// Bring up a prompt to create a new CIDR. Returns the peer request.
80pub 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
117/// Bring up a prompt to rename an existing CIDR. Returns the CIDR request.
118pub 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
160/// Bring up a prompt to delete a CIDR. Returns the peer request.
161pub 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
296/// Bring up a prompt to create a new peer. Returns the peer request.
297pub 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
391/// Bring up a prompt to rename an existing peer. Returns the peer request.
392pub 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
441/// Presents a selection and confirmation of eligible peers for either disabling or enabling,
442/// and returns back the ID of the selected peer.
443pub 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
485/// Confirm and write a innernet invitation file after a peer has been created.
486pub 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}