Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions .github/buildomat/jobs/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,15 @@ pfexec add_drv xde
banner "test"
pfexec chmod +x /input/xde/work/test/loopback
pfexec /input/xde/work/test/loopback --nocapture

# Multicast tests must run with --test-threads=1 because they share
# hardcoded device names (xde_test_sim0/1, xde_test_vnic0/1) that conflict
# when tests run in parallel
pfexec chmod +x /input/xde/work/test/multicast_rx
pfexec /input/xde/work/test/multicast_rx --nocapture --test-threads=1

pfexec chmod +x /input/xde/work/test/multicast_multi_sub
pfexec /input/xde/work/test/multicast_multi_sub --nocapture --test-threads=1

pfexec chmod +x /input/xde/work/test/multicast_validation
pfexec /input/xde/work/test/multicast_validation --nocapture --test-threads=1
21 changes: 21 additions & 0 deletions .github/buildomat/jobs/xde.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
#: "=/work/release/xde_link.so",
#: "=/work/release/xde_link.so.sha256",
#: "=/work/test/loopback",
#: "=/work/test/multicast_rx",
#: "=/work/test/multicast_multi_sub",
#: "=/work/test/multicast_validation",
#: "=/work/xde.conf",
#: ]
#:
Expand Down Expand Up @@ -116,5 +119,23 @@ loopback_test=$(
cargo build -q --test loopback --message-format=json |\
jq -r "select(.profile.test == true) | .filenames[]"
)
cargo build --test multicast_rx
multicast_rx_test=$(
cargo build -q --test multicast_rx --message-format=json |\
jq -r "select(.profile.test == true) | .filenames[]"
)
cargo build --test multicast_multi_sub
multicast_multi_sub_test=$(
cargo build -q --test multicast_multi_sub --message-format=json |\
jq -r "select(.profile.test == true) | .filenames[]"
)
cargo build --test multicast_validation
multicast_validation_test=$(
cargo build -q --test multicast_validation --message-format=json |\
jq -r "select(.profile.test == true) | .filenames[]"
)
mkdir -p /work/test
cp $loopback_test /work/test/loopback
cp $multicast_rx_test /work/test/multicast_rx
cp $multicast_multi_sub_test /work/test/multicast_multi_sub
cp $multicast_validation_test /work/test/multicast_validation
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.html
target
download
.DS_STORE
scripts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a part of your local setup you're trying to keep out-of-tree?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had an xde reset thing I was using. I'll remove this.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove. I had an xde-reset script in here for testing.

.DS_STORE
1 change: 1 addition & 0 deletions Cargo.lock

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

54 changes: 54 additions & 0 deletions bin/opteadm/src/bin/opteadm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ use oxide_vpc::api::AddFwRuleReq;
use oxide_vpc::api::AddRouterEntryReq;
use oxide_vpc::api::Address;
use oxide_vpc::api::BOUNDARY_SERVICES_VNI;
use oxide_vpc::api::ClearMcastForwardingReq;
use oxide_vpc::api::ClearVirt2BoundaryReq;
use oxide_vpc::api::ClearVirt2PhysReq;
use oxide_vpc::api::DEFAULT_MULTICAST_VNI;
use oxide_vpc::api::DelRouterEntryReq;
use oxide_vpc::api::DelRouterEntryResp;
use oxide_vpc::api::DhcpCfg;
Expand All @@ -39,22 +41,26 @@ use oxide_vpc::api::FirewallRule;
use oxide_vpc::api::IpCfg;
use oxide_vpc::api::Ipv4Cfg;
use oxide_vpc::api::Ipv6Cfg;
use oxide_vpc::api::NextHopV6;
use oxide_vpc::api::PhysNet;
use oxide_vpc::api::PortInfo;
use oxide_vpc::api::Ports;
use oxide_vpc::api::ProtoFilter;
use oxide_vpc::api::RemFwRuleReq;
use oxide_vpc::api::RemoveCidrResp;
use oxide_vpc::api::Replication;
use oxide_vpc::api::RouterClass;
use oxide_vpc::api::RouterTarget;
use oxide_vpc::api::SNat4Cfg;
use oxide_vpc::api::SNat6Cfg;
use oxide_vpc::api::SetExternalIpsReq;
use oxide_vpc::api::SetFwRulesReq;
use oxide_vpc::api::SetMcastForwardingReq;
use oxide_vpc::api::SetVirt2BoundaryReq;
use oxide_vpc::api::SetVirt2PhysReq;
use oxide_vpc::api::TunnelEndpoint;
use oxide_vpc::api::VpcCfg;
use oxide_vpc::print::print_mcast_fwd;
use oxide_vpc::print::print_v2b;
use oxide_vpc::print::print_v2p;
use std::io;
Expand Down Expand Up @@ -225,6 +231,31 @@ enum Command {
/// Clear a virtual-to-boundary mapping
ClearV2B { prefix: IpCidr, tunnel_endpoint: Vec<Ipv6Addr> },

/// Set a multicast forwarding entry
SetMcastFwd {
/// The multicast group address (IPv4 or IPv6)
group: IpAddr,
/// Next hop IPv6 address
next_hop_addr: Ipv6Addr,
/// Next hop VNI (defaults to fleet-level DEFAULT_MULTICAST_VNI)
#[arg(default_value_t = Vni::new(DEFAULT_MULTICAST_VNI).unwrap())]
next_hop_vni: Vni,
/// Delivery mode (replication):
/// - external: local guests in same VNI
/// - underlay: infrastructure via underlay multicast
/// - all: both local and underlay
replication: Replication,
},

/// Clear a multicast forwarding entry
ClearMcastFwd {
/// The multicast group address (IPv4 or IPv6)
group: IpAddr,
},

/// Dump the multicast forwarding table
DumpMcastFwd,

/// Add a new router entry, either IPv4 or IPv6.
AddRouterEntry {
#[command(flatten)]
Expand Down Expand Up @@ -764,6 +795,29 @@ fn main() -> anyhow::Result<()> {
hdl.clear_v2b(&req)?;
}

Command::SetMcastFwd {
group,
next_hop_addr,
next_hop_vni,
replication,
} => {
let next_hop = NextHopV6::new(next_hop_addr, next_hop_vni);
let req = SetMcastForwardingReq {
group,
next_hops: vec![(next_hop, replication)],
};
hdl.set_mcast_fwd(&req)?;
}

Command::ClearMcastFwd { group } => {
let req = ClearMcastForwardingReq { group };
hdl.clear_mcast_fwd(&req)?;
}

Command::DumpMcastFwd => {
print_mcast_fwd(&hdl.dump_mcast_fwd()?)?;
}

Command::AddRouterEntry {
route: RouterRule { port, dest, target, class },
} => {
Expand Down
2 changes: 2 additions & 0 deletions crates/illumos-sys-hdrs/src/kernel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,8 @@ unsafe extern "C" {
pub fn freemsg(mp: *mut mblk_t);
pub fn freemsgchain(mp: *mut mblk_t);

pub fn msgpullup(mp: *mut mblk_t, n_bytes: isize) -> *mut mblk_t;

pub fn gethrtime() -> hrtime_t;

pub fn getmajor(dev: dev_t) -> major_t;
Expand Down
66 changes: 41 additions & 25 deletions crates/opte-api/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,38 @@ pub const XDE_IOC_OPTE_CMD: i32 = XDE_IOC as i32 | 0x01;
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub enum OpteCmd {
ListPorts = 1, // list all ports
AddFwRule = 20, // add firewall rule
RemFwRule = 21, // remove firewall rule
SetFwRules = 22, // set/replace all firewall rules at once
DumpTcpFlows = 30, // dump TCP flows
DumpLayer = 31, // dump the specified Layer
DumpUft = 32, // dump the Unified Flow Table
ListLayers = 33, // list the layers on a given port
ClearUft = 40, // clear the UFT
ClearLft = 41, // clear the given Layer's Flow Table
SetVirt2Phys = 50, // set a v2p mapping
DumpVirt2Phys = 51, // dump the v2p mappings
SetVirt2Boundary = 52, // set a v2b mapping
ClearVirt2Boundary = 53, // clear a v2b mapping
DumpVirt2Boundary = 54, // dump the v2b mappings
ClearVirt2Phys = 55, // clear a v2p mapping
AddRouterEntry = 60, // add a router entry for IP dest
DelRouterEntry = 61, // remove a router entry for IP dest
CreateXde = 70, // create a new xde device
DeleteXde = 71, // delete an xde device
SetXdeUnderlay = 72, // set xde underlay devices
ClearXdeUnderlay = 73, // clear xde underlay devices
SetExternalIps = 80, // set xde external IPs for a port
AllowCidr = 90, // allow ip block through gateway tx/rx
RemoveCidr = 91, // deny ip block through gateway tx/rx
ListPorts = 1, // list all ports
AddFwRule = 20, // add firewall rule
RemFwRule = 21, // remove firewall rule
SetFwRules = 22, // set/replace all firewall rules at once
DumpTcpFlows = 30, // dump TCP flows
DumpLayer = 31, // dump the specified Layer
DumpUft = 32, // dump the Unified Flow Table
ListLayers = 33, // list the layers on a given port
ClearUft = 40, // clear the UFT
ClearLft = 41, // clear the given Layer's Flow Table
SetVirt2Phys = 50, // set a v2p mapping
DumpVirt2Phys = 51, // dump the v2p mappings
SetVirt2Boundary = 52, // set a v2b mapping
ClearVirt2Boundary = 53, // clear a v2b mapping
DumpVirt2Boundary = 54, // dump the v2b mappings
ClearVirt2Phys = 55, // clear a v2p mapping
AddRouterEntry = 60, // add a router entry for IP dest
DelRouterEntry = 61, // remove a router entry for IP dest
CreateXde = 70, // create a new xde device
DeleteXde = 71, // delete an xde device
SetXdeUnderlay = 72, // set xde underlay devices
ClearXdeUnderlay = 73, // clear xde underlay devices
SetExternalIps = 80, // set xde external IPs for a port
AllowCidr = 90, // allow ip block through gateway tx/rx
RemoveCidr = 91, // deny ip block through gateway tx/rx
SetMcastForwarding = 100, // set multicast forwarding entries
ClearMcastForwarding = 101, // clear multicast forwarding entries
DumpMcastForwarding = 102, // dump multicast forwarding table
McastSubscribe = 103, // subscribe a port to a multicast group
McastUnsubscribe = 104, // unsubscribe a port from a multicast group
SetMcast2Phys = 105, // set M2P mapping (group -> underlay mcast)
ClearMcast2Phys = 106, // clear M2P mapping
}

impl TryFrom<c_int> for OpteCmd {
Expand Down Expand Up @@ -82,6 +89,13 @@ impl TryFrom<c_int> for OpteCmd {
80 => Ok(Self::SetExternalIps),
90 => Ok(Self::AllowCidr),
91 => Ok(Self::RemoveCidr),
100 => Ok(Self::SetMcastForwarding),
101 => Ok(Self::ClearMcastForwarding),
102 => Ok(Self::DumpMcastForwarding),
103 => Ok(Self::McastSubscribe),
104 => Ok(Self::McastUnsubscribe),
105 => Ok(Self::SetMcast2Phys),
106 => Ok(Self::ClearMcast2Phys),
_ => Err(()),
}
}
Expand Down Expand Up @@ -177,6 +191,7 @@ pub enum OpteError {
dest: IpCidr,
target: String,
},
InvalidUnderlayMulticast(String),
LayerNotFound(String),
MacExists {
port: String,
Expand Down Expand Up @@ -230,6 +245,7 @@ impl OpteError {
Self::DeserCmdReq(_) => ENOMSG,
Self::FlowExists(_) => EEXIST,
Self::InvalidRouterEntry { .. } => EINVAL,
Self::InvalidUnderlayMulticast(_) => EINVAL,
Self::LayerNotFound(_) => ENOENT,
Self::MacExists { .. } => EEXIST,
Self::MaxCapacity(_) => ENFILE,
Expand Down
73 changes: 73 additions & 0 deletions crates/opte-api/src/ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,15 @@ pub enum IpAddr {
Ip6(Ipv6Addr),
}

impl IpAddr {
pub const fn is_multicast(&self) -> bool {
match self {
IpAddr::Ip4(v4) => v4.is_multicast(),
IpAddr::Ip6(v6) => v6.is_multicast(),
}
}
}

impl From<Ipv4Addr> for IpAddr {
fn from(ipv4: Ipv4Addr) -> Self {
IpAddr::Ip4(ipv4)
Expand Down Expand Up @@ -431,6 +440,10 @@ impl Ipv4Addr {
// u32.
u32::from_be_bytes(self.bytes()).to_be()
}

pub const fn is_multicast(&self) -> bool {
matches!(self.inner[0], 224..240)
}
}

impl From<core::net::Ipv4Addr> for Ipv4Addr {
Expand Down Expand Up @@ -640,6 +653,24 @@ impl Ipv6Addr {
self.inner[0] == 0xFF
}

/// Return `true` if this is a multicast IPv6 address with administrative scope
/// (admin-local, site-local, or organization-local) as defined in RFC 4291 and RFC 7346.
///
/// The three administrative scopes are:
/// - `0x4`: admin-local scope
/// - `0x5`: site-local scope
/// - `0x8`: organization-local scope
pub const fn is_admin_scoped_multicast(&self) -> bool {
if !self.is_multicast() {
return false;
}

// Extract the scope field from the lower 4 bits of the second byte
// (first byte is 0xFF for all multicast, second byte contains flags and scope)
let scope = self.inner[1] & 0x0F;
matches!(scope, 0x4 | 0x5 | 0x8)
}

/// Return the bytes of the address.
pub fn bytes(&self) -> [u8; 16] {
self.inner
Expand Down Expand Up @@ -989,6 +1020,12 @@ impl Display for Ipv4Cidr {
}

impl Ipv4Cidr {
/// IPv4 multicast address range, `224.0.0.0/4`.
pub const MCAST: Self = Self {
ip: Ipv4Addr::from_const([224, 0, 0, 0]),
prefix_len: Ipv4PrefixLen(4),
};

pub fn ip(&self) -> Ipv4Addr {
self.parts().0
}
Expand Down Expand Up @@ -1146,6 +1183,24 @@ impl Ipv6Cidr {
prefix_len: Ipv6PrefixLen(64),
};

/// IPv6 admin-local multicast scope prefix, `ff04::/16`.
pub const MCAST_ADMIN_LOCAL: Self = Self {
ip: Ipv6Addr::from_const([0xff04, 0, 0, 0, 0, 0, 0, 0]),
prefix_len: Ipv6PrefixLen(16),
};

/// IPv6 site-local multicast scope prefix, `ff05::/16`.
pub const MCAST_SITE_LOCAL: Self = Self {
ip: Ipv6Addr::from_const([0xff05, 0, 0, 0, 0, 0, 0, 0]),
prefix_len: Ipv6PrefixLen(16),
};

/// IPv6 organization-local multicast scope prefix, `ff08::/16`.
pub const MCAST_ORG_LOCAL: Self = Self {
ip: Ipv6Addr::from_const([0xff08, 0, 0, 0, 0, 0, 0, 0]),
prefix_len: Ipv6PrefixLen(16),
};

pub fn new(ip: Ipv6Addr, prefix_len: Ipv6PrefixLen) -> Self {
let ip = ip.safe_mask(prefix_len);
Ipv6Cidr { ip, prefix_len }
Expand Down Expand Up @@ -1468,6 +1523,24 @@ mod test {
assert_eq!(addr.solicited_node_multicast(), expected);
}

#[test]
fn test_ipv6_admin_scoped_multicast() {
// Test the three valid administrative scopes
assert!(to_ipv6("ff04::1").is_admin_scoped_multicast()); // admin-local (0x4)
assert!(to_ipv6("ff05::1").is_admin_scoped_multicast()); // site-local (0x5)
assert!(to_ipv6("ff08::1").is_admin_scoped_multicast()); // organization-local (0x8)

// Test non-admin scoped multicast addresses
assert!(!to_ipv6("ff01::1").is_admin_scoped_multicast()); // interface-local
assert!(!to_ipv6("ff02::1").is_admin_scoped_multicast()); // link-local
assert!(!to_ipv6("ff0e::1").is_admin_scoped_multicast()); // global

// Test non-multicast addresses
assert!(!to_ipv6("fd00::1").is_admin_scoped_multicast()); // ULA
assert!(!to_ipv6("fe80::1").is_admin_scoped_multicast()); // link-local unicast
assert!(!to_ipv6("2001:db8::1").is_admin_scoped_multicast()); // global unicast
}

#[test]
fn dhcp_fqdn() {
let no_host = DhcpCfg { hostname: None, ..Default::default() };
Expand Down
2 changes: 1 addition & 1 deletion crates/opte-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub use ulp::*;
///
/// We rely on CI and the check-api-version.sh script to verify that
/// this number is incremented anytime the oxide-api code changes.
pub const API_VERSION: u64 = 37;
pub const API_VERSION: u64 = 38;

/// Major version of the OPTE package.
pub const MAJOR_VERSION: u64 = 0;
Expand Down
Loading