parent
ae2c554b23
commit
ec754e60c4
|
@ -9,9 +9,9 @@ use shared::{
|
||||||
prompts,
|
prompts,
|
||||||
wg::{DeviceExt, PeerInfoExt},
|
wg::{DeviceExt, PeerInfoExt},
|
||||||
AddAssociationOpts, AddCidrOpts, AddPeerOpts, Association, AssociationContents, Cidr, CidrTree,
|
AddAssociationOpts, AddCidrOpts, AddPeerOpts, Association, AssociationContents, Cidr, CidrTree,
|
||||||
DeleteCidrOpts, Endpoint, EndpointContents, InstallOpts, Interface, IoErrorContext, NatOpts,
|
DeleteCidrOpts, Endpoint, EndpointContents, InstallOpts, Interface, IoErrorContext,
|
||||||
NetworkOpts, Peer, RedeemContents, RenamePeerOpts, State, WrappedIoError,
|
ListenPortOpts, NatOpts, NetworkOpts, OverrideEndpointOpts, Peer, RedeemContents,
|
||||||
REDEEM_TRANSITION_WAIT,
|
RenamePeerOpts, State, WrappedIoError, REDEEM_TRANSITION_WAIT,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
fmt, io,
|
fmt, io,
|
||||||
|
@ -50,7 +50,7 @@ struct Opts {
|
||||||
#[structopt(subcommand)]
|
#[structopt(subcommand)]
|
||||||
command: Option<Command>,
|
command: Option<Command>,
|
||||||
|
|
||||||
/// Verbose output, use -vv for even higher verbositude.
|
/// Verbose output, use -vv for even higher verbositude
|
||||||
#[structopt(short, long, parse(from_occurrences))]
|
#[structopt(short, long, parse(from_occurrences))]
|
||||||
verbose: u64,
|
verbose: u64,
|
||||||
|
|
||||||
|
@ -66,11 +66,11 @@ struct Opts {
|
||||||
|
|
||||||
#[derive(Clone, Debug, StructOpt)]
|
#[derive(Clone, Debug, StructOpt)]
|
||||||
struct HostsOpt {
|
struct HostsOpt {
|
||||||
/// The path to write hosts to.
|
/// The path to write hosts to
|
||||||
#[structopt(long = "hosts-path", default_value = "/etc/hosts")]
|
#[structopt(long = "hosts-path", default_value = "/etc/hosts")]
|
||||||
hosts_path: PathBuf,
|
hosts_path: PathBuf,
|
||||||
|
|
||||||
/// Don't write to any hosts files.
|
/// Don't write to any hosts files
|
||||||
#[structopt(long = "no-write-hosts", conflicts_with = "hosts-path")]
|
#[structopt(long = "no-write-hosts", conflicts_with = "hosts-path")]
|
||||||
no_write_hosts: bool,
|
no_write_hosts: bool,
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ impl From<HostsOpt> for Option<PathBuf> {
|
||||||
|
|
||||||
#[derive(Clone, Debug, StructOpt)]
|
#[derive(Clone, Debug, StructOpt)]
|
||||||
enum Command {
|
enum Command {
|
||||||
/// Install a new innernet config.
|
/// Install a new innernet config
|
||||||
#[structopt(alias = "redeem")]
|
#[structopt(alias = "redeem")]
|
||||||
Install {
|
Install {
|
||||||
/// Path to the invitation file
|
/// Path to the invitation file
|
||||||
|
@ -99,7 +99,7 @@ enum Command {
|
||||||
nat: NatOpts,
|
nat: NatOpts,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Enumerate all innernet connections.
|
/// Enumerate all innernet connections
|
||||||
#[structopt(alias = "list")]
|
#[structopt(alias = "list")]
|
||||||
Show {
|
Show {
|
||||||
/// One-line peer list
|
/// One-line peer list
|
||||||
|
@ -113,15 +113,15 @@ enum Command {
|
||||||
interface: Option<Interface>,
|
interface: Option<Interface>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Bring up your local interface, and update it with latest peer list.
|
/// Bring up your local interface, and update it with latest peer list
|
||||||
Up {
|
Up {
|
||||||
/// Enable daemon mode i.e. keep the process running, while fetching
|
/// Enable daemon mode i.e. keep the process running, while fetching
|
||||||
/// the latest peer list periodically.
|
/// the latest peer list periodically
|
||||||
#[structopt(short, long)]
|
#[structopt(short, long)]
|
||||||
daemon: bool,
|
daemon: bool,
|
||||||
|
|
||||||
/// Keep fetching the latest peer list at the specified interval in
|
/// Keep fetching the latest peer list at the specified interval in
|
||||||
/// seconds. Valid only in daemon mode.
|
/// seconds. Valid only in daemon mode
|
||||||
#[structopt(long, default_value = "60")]
|
#[structopt(long, default_value = "60")]
|
||||||
interval: u64,
|
interval: u64,
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ enum Command {
|
||||||
interface: Interface,
|
interface: Interface,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Fetch and update your local interface with the latest peer list.
|
/// Fetch and update your local interface with the latest peer list
|
||||||
Fetch {
|
Fetch {
|
||||||
interface: Interface,
|
interface: Interface,
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ enum Command {
|
||||||
/// Bring down the interface (equivalent to 'wg-quick down <interface>')
|
/// Bring down the interface (equivalent to 'wg-quick down <interface>')
|
||||||
Down { interface: Interface },
|
Down { interface: Interface },
|
||||||
|
|
||||||
/// Add a new peer.
|
/// Add a new peer
|
||||||
///
|
///
|
||||||
/// By default, you'll be prompted interactively to create a peer, but you can
|
/// By default, you'll be prompted interactively to create a peer, but you can
|
||||||
/// also specify all the options in the command, eg:
|
/// also specify all the options in the command, eg:
|
||||||
|
@ -164,7 +164,7 @@ enum Command {
|
||||||
sub_opts: AddPeerOpts,
|
sub_opts: AddPeerOpts,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Rename a peer.
|
/// Rename a peer
|
||||||
///
|
///
|
||||||
/// By default, you'll be prompted interactively to select a peer, but you can
|
/// By default, you'll be prompted interactively to select a peer, but you can
|
||||||
/// also specify all the options in the command, eg:
|
/// also specify all the options in the command, eg:
|
||||||
|
@ -177,7 +177,7 @@ enum Command {
|
||||||
sub_opts: RenamePeerOpts,
|
sub_opts: RenamePeerOpts,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Add a new CIDR.
|
/// Add a new CIDR
|
||||||
AddCidr {
|
AddCidr {
|
||||||
interface: Interface,
|
interface: Interface,
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@ enum Command {
|
||||||
sub_opts: AddCidrOpts,
|
sub_opts: AddCidrOpts,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Delete a CIDR.
|
/// Delete a CIDR
|
||||||
DeleteCidr {
|
DeleteCidr {
|
||||||
interface: Interface,
|
interface: Interface,
|
||||||
|
|
||||||
|
@ -193,7 +193,7 @@ enum Command {
|
||||||
sub_opts: DeleteCidrOpts,
|
sub_opts: DeleteCidrOpts,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// List CIDRs.
|
/// List CIDRs
|
||||||
ListCidrs {
|
ListCidrs {
|
||||||
interface: Interface,
|
interface: Interface,
|
||||||
|
|
||||||
|
@ -202,13 +202,13 @@ enum Command {
|
||||||
tree: bool,
|
tree: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Disable an enabled peer.
|
/// Disable an enabled peer
|
||||||
DisablePeer { interface: Interface },
|
DisablePeer { interface: Interface },
|
||||||
|
|
||||||
/// Enable a disabled peer.
|
/// Enable a disabled peer
|
||||||
EnablePeer { interface: Interface },
|
EnablePeer { interface: Interface },
|
||||||
|
|
||||||
/// Add an association between CIDRs.
|
/// Add an association between CIDRs
|
||||||
AddAssociation {
|
AddAssociation {
|
||||||
interface: Interface,
|
interface: Interface,
|
||||||
|
|
||||||
|
@ -216,28 +216,26 @@ enum Command {
|
||||||
sub_opts: AddAssociationOpts,
|
sub_opts: AddAssociationOpts,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Delete an association between CIDRs.
|
/// Delete an association between CIDRs
|
||||||
DeleteAssociation { interface: Interface },
|
DeleteAssociation { interface: Interface },
|
||||||
|
|
||||||
/// List existing assocations between CIDRs.
|
/// List existing assocations between CIDRs
|
||||||
ListAssociations { interface: Interface },
|
ListAssociations { interface: Interface },
|
||||||
|
|
||||||
/// Set the local listen port.
|
/// Set the local listen port.
|
||||||
SetListenPort {
|
SetListenPort {
|
||||||
interface: Interface,
|
interface: Interface,
|
||||||
|
|
||||||
/// Unset the local listen port to use a randomized port.
|
#[structopt(flatten)]
|
||||||
#[structopt(short, long)]
|
sub_opts: ListenPortOpts,
|
||||||
unset: bool,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Override your external endpoint that the server sends to other peers.
|
/// Override your external endpoint that the server sends to other peers
|
||||||
OverrideEndpoint {
|
OverrideEndpoint {
|
||||||
interface: Interface,
|
interface: Interface,
|
||||||
|
|
||||||
/// Unset an existing override to use the automatic endpoint discovery.
|
#[structopt(flatten)]
|
||||||
#[structopt(short, long)]
|
sub_opts: OverrideEndpointOpts,
|
||||||
unset: bool,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Generate shell completion scripts
|
/// Generate shell completion scripts
|
||||||
|
@ -535,7 +533,7 @@ fn fetch(
|
||||||
.with_str(interface.to_string())?;
|
.with_str(interface.to_string())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("fetching state from server.");
|
log::info!("fetching state from server...");
|
||||||
let mut store = DataStore::open_or_create(&opts.data_dir, interface)?;
|
let mut store = DataStore::open_or_create(&opts.data_dir, interface)?;
|
||||||
let api = Api::new(&config.server);
|
let api = Api::new(&config.server);
|
||||||
let State { peers, cidrs } = api.http("GET", "/user/state")?;
|
let State { peers, cidrs } = api.http("GET", "/user/state")?;
|
||||||
|
@ -563,7 +561,7 @@ fn fetch(
|
||||||
println!();
|
println!();
|
||||||
log::info!("updated interface {}\n", interface.as_str_lossy().yellow());
|
log::info!("updated interface {}\n", interface.as_str_lossy().yellow());
|
||||||
} else {
|
} else {
|
||||||
log::info!("{}", "peers are already up to date.".green());
|
log::info!("{}", "peers are already up to date".green());
|
||||||
}
|
}
|
||||||
let interface_updated_time = Instant::now();
|
let interface_updated_time = Instant::now();
|
||||||
|
|
||||||
|
@ -890,11 +888,11 @@ fn list_associations(interface: &InterfaceName, opts: &Opts) -> Result<(), Error
|
||||||
fn set_listen_port(
|
fn set_listen_port(
|
||||||
interface: &InterfaceName,
|
interface: &InterfaceName,
|
||||||
opts: &Opts,
|
opts: &Opts,
|
||||||
unset: bool,
|
sub_opts: ListenPortOpts,
|
||||||
) -> Result<Option<u16>, Error> {
|
) -> Result<Option<u16>, Error> {
|
||||||
let mut config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
|
let mut config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
|
||||||
|
|
||||||
let listen_port = prompts::set_listen_port(&config.interface, unset)?;
|
let listen_port = prompts::set_listen_port(&config.interface, sub_opts)?;
|
||||||
if let Some(listen_port) = listen_port {
|
if let Some(listen_port) = listen_port {
|
||||||
wg::set_listen_port(interface, listen_port, opts.network.backend)?;
|
wg::set_listen_port(interface, listen_port, opts.network.backend)?;
|
||||||
log::info!("the interface is updated");
|
log::info!("the interface is updated");
|
||||||
|
@ -909,33 +907,33 @@ fn set_listen_port(
|
||||||
Ok(listen_port.flatten())
|
Ok(listen_port.flatten())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn override_endpoint(interface: &InterfaceName, opts: &Opts, unset: bool) -> Result<(), Error> {
|
fn override_endpoint(
|
||||||
|
interface: &InterfaceName,
|
||||||
|
opts: &Opts,
|
||||||
|
sub_opts: OverrideEndpointOpts,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
|
let config = InterfaceConfig::from_interface(&opts.config_dir, interface)?;
|
||||||
let endpoint_contents = if unset {
|
let port = match config.interface.listen_port {
|
||||||
prompts::unset_override_endpoint()?.then(|| EndpointContents::Unset)
|
Some(port) => port,
|
||||||
|
None => bail!("you need to set a listen port with set-listen-port before overriding the endpoint (otherwise port randomization on the interface would make it useless).")
|
||||||
|
};
|
||||||
|
|
||||||
|
let endpoint_contents = if sub_opts.unset {
|
||||||
|
prompts::unset_override_endpoint(&sub_opts)?.then(|| EndpointContents::Unset)
|
||||||
} else {
|
} else {
|
||||||
let listen_port = if let Some(listen_port) = config.interface.listen_port {
|
let endpoint = prompts::override_endpoint(&sub_opts, port)?;
|
||||||
Some(listen_port)
|
|
||||||
} else {
|
|
||||||
println!(
|
|
||||||
"{}: you need to set a listen port for your interface first.",
|
|
||||||
"note".bold().yellow()
|
|
||||||
);
|
|
||||||
set_listen_port(interface, opts, unset)?
|
|
||||||
};
|
|
||||||
let endpoint = if let Some(port) = listen_port {
|
|
||||||
prompts::override_endpoint(port)?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
endpoint.map(EndpointContents::Set)
|
endpoint.map(EndpointContents::Set)
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(contents) = endpoint_contents {
|
if let Some(contents) = endpoint_contents {
|
||||||
log::info!("Updating endpoint.");
|
log::info!("requesting endpoint update...");
|
||||||
Api::new(&config.server).http_form("PUT", "/user/endpoint", contents)?;
|
Api::new(&config.server).http_form("PUT", "/user/endpoint", contents)?;
|
||||||
|
log::info!(
|
||||||
|
"endpoint override {}",
|
||||||
|
if sub_opts.unset { "unset" } else { "set" }
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
log::info!("exiting without overriding endpoint.");
|
log::info!("exiting without overriding endpoint");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1193,11 +1191,17 @@ fn run(opts: &Opts) -> Result<(), Error> {
|
||||||
} => add_association(&interface, opts, sub_opts)?,
|
} => add_association(&interface, opts, sub_opts)?,
|
||||||
Command::DeleteAssociation { interface } => delete_association(&interface, opts)?,
|
Command::DeleteAssociation { interface } => delete_association(&interface, opts)?,
|
||||||
Command::ListAssociations { interface } => list_associations(&interface, opts)?,
|
Command::ListAssociations { interface } => list_associations(&interface, opts)?,
|
||||||
Command::SetListenPort { interface, unset } => {
|
Command::SetListenPort {
|
||||||
set_listen_port(&interface, opts, unset)?;
|
interface,
|
||||||
|
sub_opts,
|
||||||
|
} => {
|
||||||
|
set_listen_port(&interface, opts, sub_opts)?;
|
||||||
},
|
},
|
||||||
Command::OverrideEndpoint { interface, unset } => {
|
Command::OverrideEndpoint {
|
||||||
override_endpoint(&interface, opts, unset)?;
|
interface,
|
||||||
|
sub_opts,
|
||||||
|
} => {
|
||||||
|
override_endpoint(&interface, opts, sub_opts)?;
|
||||||
},
|
},
|
||||||
Command::Completions { shell } => {
|
Command::Completions { shell } => {
|
||||||
Opts::clap().gen_completions_to("innernet", shell, &mut std::io::stdout());
|
Opts::clap().gen_completions_to("innernet", shell, &mut std::io::stdout());
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
interface_config::{InterfaceConfig, InterfaceInfo, ServerInfo},
|
interface_config::{InterfaceConfig, InterfaceInfo, ServerInfo},
|
||||||
AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, DeleteCidrOpts, Endpoint,
|
AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, DeleteCidrOpts, Endpoint,
|
||||||
Error, Hostname, Peer, PeerContents, RenamePeerOpts, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
|
Error, Hostname, ListenPortOpts, OverrideEndpointOpts, Peer, PeerContents, RenamePeerOpts,
|
||||||
|
PERSISTENT_KEEPALIVE_INTERVAL_SECS,
|
||||||
};
|
};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use colored::*;
|
use colored::*;
|
||||||
|
@ -426,16 +427,18 @@ pub fn write_peer_invitation(
|
||||||
|
|
||||||
pub fn set_listen_port(
|
pub fn set_listen_port(
|
||||||
interface: &InterfaceInfo,
|
interface: &InterfaceInfo,
|
||||||
unset: bool,
|
args: ListenPortOpts,
|
||||||
) -> Result<Option<Option<u16>>, Error> {
|
) -> Result<Option<Option<u16>>, Error> {
|
||||||
let listen_port = (!unset)
|
let listen_port = if let Some(listen_port) = args.listen_port {
|
||||||
.then(|| {
|
Some(listen_port)
|
||||||
input(
|
} else if !args.unset {
|
||||||
"Listen port",
|
Some(input(
|
||||||
Prefill::Default(interface.listen_port.unwrap_or(51820)),
|
"Listen port",
|
||||||
)
|
Prefill::Default(interface.listen_port.unwrap_or(51820)),
|
||||||
})
|
)?)
|
||||||
.transpose()?;
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mut confirmation = Confirm::with_theme(&*THEME);
|
let mut confirmation = Confirm::with_theme(&*THEME);
|
||||||
confirmation
|
confirmation
|
||||||
|
@ -452,7 +455,7 @@ pub fn set_listen_port(
|
||||||
if listen_port == interface.listen_port {
|
if listen_port == interface.listen_port {
|
||||||
println!("No change necessary - interface already has this setting.");
|
println!("No change necessary - interface already has this setting.");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else if confirmation.interact()? {
|
} else if args.yes || confirmation.interact()? {
|
||||||
Ok(Some(listen_port))
|
Ok(Some(listen_port))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
@ -464,7 +467,7 @@ pub fn ask_endpoint(listen_port: u16) -> Result<Endpoint, Error> {
|
||||||
|
|
||||||
let external_ip = if Confirm::with_theme(&*THEME)
|
let external_ip = if Confirm::with_theme(&*THEME)
|
||||||
.wait_for_newline(true)
|
.wait_for_newline(true)
|
||||||
.with_prompt("Auto-fill public IP address (using a DNS query to 1.1.1.1)?")
|
.with_prompt("Auto-fill public IP address (via a DNS query to 1.1.1.1)?")
|
||||||
.interact()?
|
.interact()?
|
||||||
{
|
{
|
||||||
publicip::get_any(Preference::Ipv4)
|
publicip::get_any(Preference::Ipv4)
|
||||||
|
@ -481,17 +484,21 @@ pub fn ask_endpoint(listen_port: u16) -> Result<Endpoint, Error> {
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn override_endpoint(listen_port: u16) -> Result<Option<Endpoint>, Error> {
|
pub fn override_endpoint(
|
||||||
let endpoint = ask_endpoint(listen_port)?;
|
args: &OverrideEndpointOpts,
|
||||||
if confirm(&format!("Set external endpoint to {}?", endpoint))? {
|
listen_port: u16,
|
||||||
|
) -> Result<Option<Endpoint>, Error> {
|
||||||
|
let endpoint = match &args.endpoint {
|
||||||
|
Some(endpoint) => endpoint.clone(),
|
||||||
|
None => ask_endpoint(listen_port)?,
|
||||||
|
};
|
||||||
|
if args.yes || confirm(&format!("Set external endpoint to {}?", endpoint))? {
|
||||||
Ok(Some(endpoint))
|
Ok(Some(endpoint))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unset_override_endpoint() -> Result<bool, Error> {
|
pub fn unset_override_endpoint(args: &OverrideEndpointOpts) -> Result<bool, Error> {
|
||||||
Ok(confirm(
|
Ok(args.yes || confirm("Unset external endpoint to enable automatic endpoint discovery?")?)
|
||||||
"Unset external endpoint to enable automatic endpoint discovery?",
|
|
||||||
)?)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -387,6 +387,36 @@ pub struct AddAssociationOpts {
|
||||||
pub cidr2: Option<String>,
|
pub cidr2: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, StructOpt)]
|
||||||
|
pub struct ListenPortOpts {
|
||||||
|
/// The listen port you'd like to set for the interface
|
||||||
|
#[structopt(short, long)]
|
||||||
|
pub listen_port: Option<u16>,
|
||||||
|
|
||||||
|
/// Unset the local listen port to use a randomized port
|
||||||
|
#[structopt(short, long, conflicts_with = "listen-port")]
|
||||||
|
pub unset: bool,
|
||||||
|
|
||||||
|
/// Bypass confirmation
|
||||||
|
#[structopt(long)]
|
||||||
|
pub yes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, StructOpt)]
|
||||||
|
pub struct OverrideEndpointOpts {
|
||||||
|
/// The listen port you'd like to set for the interface
|
||||||
|
#[structopt(short, long)]
|
||||||
|
pub endpoint: Option<Endpoint>,
|
||||||
|
|
||||||
|
/// Unset an existing override to use the automatic endpoint discovery
|
||||||
|
#[structopt(short, long, conflicts_with = "endpoint")]
|
||||||
|
pub unset: bool,
|
||||||
|
|
||||||
|
/// Bypass confirmation
|
||||||
|
#[structopt(long)]
|
||||||
|
pub yes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, StructOpt)]
|
#[derive(Debug, Clone, StructOpt)]
|
||||||
pub struct NatOpts {
|
pub struct NatOpts {
|
||||||
#[structopt(long)]
|
#[structopt(long)]
|
||||||
|
|
Loading…
Reference in New Issue