Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b7da268
Initial opteadm work
FelixMcFelix Nov 13, 2023
9db946d
Separate Inbound and Outbound NAT
FelixMcFelix Nov 14, 2023
5818f46
Fix missed port definition in XDE tests
FelixMcFelix Nov 14, 2023
3395297
Rename `external_ip` -> `ephemeral_ip`
FelixMcFelix Nov 14, 2023
50d10a2
Initial separation of api::VpcCfg from internal Cfg
FelixMcFelix Nov 15, 2023
06f4879
Add Ioctl to pass in ExternalIpCfg
FelixMcFelix Nov 15, 2023
50a9535
Actually replace rules on NAT update
FelixMcFelix Nov 16, 2023
cd4f515
Initial multi-external tests
FelixMcFelix Nov 16, 2023
b21c06c
Integration test for update logic
FelixMcFelix Nov 17, 2023
bdccda4
Implement missing test around epoch asssertions
FelixMcFelix Nov 17, 2023
4a53739
Fixup multiple external IP tests.
FelixMcFelix Nov 17, 2023
2644aba
Implement first pass at soft flow-table invalidation
FelixMcFelix Nov 20, 2023
8596f7c
Fixup docs
FelixMcFelix Nov 20, 2023
0dbfdc8
Merge branch 'master' into floating-ip
FelixMcFelix Nov 20, 2023
93b520e
Missed a `fmt` check
FelixMcFelix Nov 20, 2023
1f7174f
Rename `Resource`->`Dynamic`, fix flowtable limits
FelixMcFelix Nov 20, 2023
d164250
Implement outbound flow reassignment on NAT change
FelixMcFelix Nov 20, 2023
8e1b01b
Explanatory comment on shared locking around revalidation.
FelixMcFelix Nov 20, 2023
946ded3
Ensure engine-only deps are optional
FelixMcFelix Nov 27, 2023
d508dcd
Merge branch 'master' into floating-ip
FelixMcFelix Nov 27, 2023
bfe90c3
Expose Floating IP info in `opteadm list-ports`
FelixMcFelix Nov 28, 2023
6977256
Merge branch 'master' into floating-ip
FelixMcFelix Nov 28, 2023
fff7085
Address review feedback
FelixMcFelix Nov 29, 2023
9121723
Add missed `set_external_ips` to opte-ioctl
FelixMcFelix Nov 29, 2023
e0b4149
Accidentally missed a `cargo fmt`.
FelixMcFelix Nov 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ anyhow = "1.0"
bitflags = "2"
cfg-if = "1"
clap = { version = "4", features = ["derive", "string"] }
crc32fast = { version = "1", default-features = false }
ctor = "0.2"
dyn-clone = "1.0"
heapless = "0.7"
ipnetwork = { version = "0.20", default-features = false }
itertools = "0.12"
itertools = { version = "0.12", default-features = false }
libc = "0.2"
libnet = { git = "https://github.com/oxidecomputer/netadm-sys" }
pcap-parser = "0.14"
Expand Down
190 changes: 152 additions & 38 deletions bin/opteadm/src/bin/opteadm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@

// Copyright 2023 Oxide Computer Company

use std::io;
use std::str::FromStr;

use clap::Args;
use clap::Parser;

use opte::api::Direction;
use opte::api::DomainName;
use opte::api::IpAddr;
use opte::api::IpCidr;
use opte::api::Ipv4Addr;
use opte::api::Ipv6Addr;
use opte::api::MacAddr;
use opte::api::Vni;
Expand All @@ -29,6 +26,7 @@ use oxide_vpc::api::AddRouterEntryReq;
use oxide_vpc::api::Address;
use oxide_vpc::api::BoundaryServices;
use oxide_vpc::api::DhcpCfg;
use oxide_vpc::api::ExternalIpCfg;
use oxide_vpc::api::Filters as FirewallFilters;
use oxide_vpc::api::FirewallAction;
use oxide_vpc::api::FirewallRule;
Expand All @@ -43,9 +41,12 @@ use oxide_vpc::api::RemFwRuleReq;
use oxide_vpc::api::RouterTarget;
use oxide_vpc::api::SNat4Cfg;
use oxide_vpc::api::SNat6Cfg;
use oxide_vpc::api::SetExternalIpsReq;
use oxide_vpc::api::SetVirt2PhysReq;
use oxide_vpc::api::VpcCfg;
use oxide_vpc::engine::print::print_v2p;
use std::io;
use std::str::FromStr;

/// Administer the Oxide Packet Transformation Engine (OPTE)
#[derive(Debug, Parser)]
Expand Down Expand Up @@ -184,14 +185,11 @@ enum Command {
src_underlay_addr: Ipv6Addr,

#[command(flatten)]
snat: Option<SnatConfig>,
external_net: ExternalNetConfig,

#[command(flatten)]
dhcp: DhcpConfig,

#[arg(long)]
external_ip: Option<IpAddr>,

#[arg(long)]
passthrough: bool,
},
Expand All @@ -215,6 +213,16 @@ enum Command {
/// The location to which traffic matching the destination is sent.
target: RouterTarget,
},

/// Configure external network addresses used by a port for VPC-external traffic.
SetExternalIps {
/// The OPTE port to which the route is added
#[arg(short)]
port: String,

#[command(flatten)]
external_net: ExternalNetConfig,
},
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -242,18 +250,100 @@ impl From<Filters> for FirewallFilters {
}
}

#[derive(Args, Debug)]
// TODO: expand this to allow for v4 and v6 simultaneously?
/// Per-port configuration for rack-external networking.
#[derive(Args, Clone, Debug)]
struct ExternalNetConfig {
#[command(flatten)]
snat: Option<SnatConfig>,

/// An external IP address used for 1-to-1 NAT.
///
/// If `floating_ip`s are defined, then a port will receive and reply
/// (but not originate traffic) on this address.
#[arg(long)]
ephemeral_ip: Option<IpAddr>,

/// A comma-separated list of floating IP addresses which a port will prefer
/// for sending and receiving traffic.
#[arg(long)]
floating_ip: Vec<IpAddr>,
}

impl TryFrom<ExternalNetConfig> for ExternalIpCfg<Ipv4Addr> {
type Error = anyhow::Error;

fn try_from(value: ExternalNetConfig) -> Result<Self, Self::Error> {
let snat = value.snat.map(SNat4Cfg::try_from).transpose()?;

let ephemeral_ip = match value.ephemeral_ip {
Some(IpAddr::Ip4(ip)) => Some(ip),
Some(IpAddr::Ip6(_)) => {
anyhow::bail!("expected IPv4 external IP");
}
None => None,
};

let floating_ips = value
.floating_ip
.iter()
.copied()
.map(|ip| match ip {
IpAddr::Ip4(ip) => Ok(ip),
_ => anyhow::bail!("expected IPv4 floating IP"),
})
.collect::<Result<Vec<opte::api::Ipv4Addr>, _>>()?;

Ok(Self { snat, ephemeral_ip, floating_ips })
}
}

impl TryFrom<ExternalNetConfig> for ExternalIpCfg<Ipv6Addr> {
type Error = anyhow::Error;

fn try_from(value: ExternalNetConfig) -> Result<Self, Self::Error> {
let snat = value.snat.map(SNat6Cfg::try_from).transpose()?;

let ephemeral_ip = match value.ephemeral_ip {
Some(IpAddr::Ip4(_)) => {
anyhow::bail!("expected IPv6 external IP");
}
Some(IpAddr::Ip6(ip)) => Some(ip),
None => None,
};

let floating_ips = value
.floating_ip
.iter()
.copied()
.map(|ip| match ip {
IpAddr::Ip6(ip) => Ok(ip),
_ => anyhow::bail!("expected IPv6 floating IP"),
})
.collect::<Result<Vec<opte::api::Ipv6Addr>, _>>()?;

Ok(Self { snat, ephemeral_ip, floating_ips })
}
}

#[derive(Args, Clone, Copy, Debug)]
#[group(requires_all = ["snat_ip", "snat_start", "snat_end"], multiple = true)]
struct SnatConfig {
/// The external IP address used for source NAT for the guest.
///
/// Requires `snat_ip`, `snat_start`, and `snat_end` to be defined.
#[arg(long, required = false)]
snat_ip: IpAddr,

/// The starting L4 port used for source NAT for the guest.
///
/// See `snat_ip` for mandatory shared arguments.
#[arg(long, required = false)]
snat_start: u16,

/// The ending L4 port used for source NAT for the guest.
///
/// See `snat_ip` for mandatory shared arguments.
#[arg(long, required = false)]
snat_end: u16,
}
Expand Down Expand Up @@ -331,33 +421,51 @@ fn opte_pkg_version() -> String {
format!("{MAJOR_VERSION}.{API_VERSION}.{COMMIT_COUNT}")
}

// XXX: These are growing unwieldy to the point we might want an actual table
// pretty-printer for opte outputs.
fn print_port_header() {
println!(
"{:<32} {:<24} {:<16} {:<16} {:<40} {:<40} {:<8}",
"{:<32} {:<24} {:<16} {:<16} {:<16} {:<40} {:<40} {:<40} {:<8}",
"LINK",
"MAC ADDRESS",
"IPv4 ADDRESS",
"EXTERNAL IPv4",
"EPHEMERAL IPv4",
"FLOATING IPv4",
"IPv6 ADDRESS",
"EXTERNAL IPv6",
"FLOATING IPv6",
"STATE"
);
}

fn print_port(pi: PortInfo) {
let none = String::from("None");
println!(
"{:<32} {:<24} {:<16} {:<16} {:<40} {:<40} {:<8}",
"{:<32} {:<24} {:<16} {:<16} {:<16} {:<40} {:<40} {:<40} {:<8}",
pi.name,
pi.mac_addr.to_string(),
pi.ip4_addr.map(|x| x.to_string()).unwrap_or_else(|| none.clone()),
pi.external_ip4_addr
pi.ephemeral_ip4_addr
.map(|x| x.to_string())
.unwrap_or_else(|| none.clone()),
pi.floating_ip4_addrs
.map(|vec| vec
.into_iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join(","))
.unwrap_or_else(|| none.clone()),
pi.ip6_addr.map(|x| x.to_string()).unwrap_or_else(|| none.clone()),
pi.external_ip6_addr
pi.ephemeral_ip6_addr
.map(|x| x.to_string())
.unwrap_or_else(|| none.clone()),
pi.floating_ip6_addrs
.map(|vec| vec
.into_iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join(","))
.unwrap_or_else(|| none.clone()),
pi.state,
);
}
Expand Down Expand Up @@ -438,9 +546,8 @@ fn main() -> anyhow::Result<()> {
bsvc_mac,
vpc_vni,
src_underlay_addr,
snat,
dhcp,
external_ip,
external_net,
passthrough,
} => {
let ip_cfg = match private_ip {
Expand All @@ -453,22 +560,13 @@ fn main() -> anyhow::Result<()> {
anyhow::bail!("expected IPv4 gateway IP");
};

let snat = snat.map(SNat4Cfg::try_from).transpose()?;

let external_ip = match external_ip {
Some(IpAddr::Ip4(ip)) => Some(ip),
Some(IpAddr::Ip6(_)) => {
anyhow::bail!("expected IPv4 external IP");
}
None => None,
};
let external_ips = external_net.try_into()?;

IpCfg::Ipv4(Ipv4Cfg {
vpc_subnet,
private_ip,
gateway_ip,
snat,
external_ips: external_ip,
external_ips,
})
}
IpAddr::Ip6(private_ip) => {
Expand All @@ -480,22 +578,13 @@ fn main() -> anyhow::Result<()> {
anyhow::bail!("expected IPv6 gateway IP");
};

let snat = snat.map(SNat6Cfg::try_from).transpose()?;

let external_ip = match external_ip {
Some(IpAddr::Ip4(_)) => {
anyhow::bail!("expected IPv6 external IP");
}
Some(IpAddr::Ip6(ip)) => Some(ip),
None => None,
};
let external_ips = external_net.try_into()?;

IpCfg::Ipv6(Ipv6Cfg {
vpc_subnet,
private_ip,
gateway_ip,
snat,
external_ips: external_ip,
external_ips,
})
}
};
Expand Down Expand Up @@ -539,6 +628,31 @@ fn main() -> anyhow::Result<()> {
let req = AddRouterEntryReq { port_name: port, dest, target };
hdl.add_router_entry(&req)?;
}

Command::SetExternalIps { port, external_net } => {
if let Ok(cfg) =
ExternalIpCfg::<Ipv4Addr>::try_from(external_net.clone())
{
let req = SetExternalIpsReq {
port_name: port,
external_ips_v4: Some(cfg),
external_ips_v6: None,
};
hdl.set_external_ips(&req)?;
} else if let Ok(cfg) =
ExternalIpCfg::<Ipv6Addr>::try_from(external_net)
{
let req = SetExternalIpsReq {
port_name: port,
external_ips_v6: Some(cfg),
external_ips_v4: None,
};
hdl.set_external_ips(&req)?;
} else {
// TODO: show *actual* parse failure.
anyhow::bail!("expected IPv4 *or* IPv6 config.");
}
}
}

Ok(())
Expand Down
Loading