Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
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
60 changes: 60 additions & 0 deletions crates/opte-api/src/ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,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 @@ -1002,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 @@ -1159,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 @@ -1481,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
Loading