diff --git a/.github/buildomat/illumos.sh b/.github/buildomat/illumos.sh index ed7f7039..f8fd4f79 100644 --- a/.github/buildomat/illumos.sh +++ b/.github/buildomat/illumos.sh @@ -1,6 +1,10 @@ # Download the SDE from CI, verify its integrity, and install it banner "sde setup" +# Ensure the commands below don't abort this script if they succeed because +# there was nothing to do. +export PKG_SUCCESS_ON_NOP=1 + export PKG=tofino_sde.p5p curl -OL $SDE_DIR/$PKG SDE_CALC=`digest -a sha256 $PKG` @@ -15,7 +19,7 @@ export LD_LIBRARY_PATH="$SDE/lib:$LD_LIBRARY_PATH" # Install a couple of non-standard packages needed to build dendrite banner "packages" -pfexec pkg install clang-15 pcap +pfexec pkg install clang-15 pcap gcc14 pfexec pkg set-mediator -V 15 clang llvm cargo --version diff --git a/.github/buildomat/jobs/image.sh b/.github/buildomat/jobs/image.sh index 02d128c6..dfb7e6d5 100755 --- a/.github/buildomat/jobs/image.sh +++ b/.github/buildomat/jobs/image.sh @@ -2,7 +2,7 @@ #: #: name = "image" #: variety = "basic" -#: target = "helios-2.0" +#: target = "helios-3.0" #: rust_toolchain = true #: output_rules = [ #: "/out/*", @@ -100,8 +100,8 @@ pfexec mkdir -p /out pfexec chown "$UID" /out banner "P4 Codegen" -# Add gcc-12 so the p4 compiler can find cpp -PATH=/opt/gcc-12/bin:$PATH cargo xtask codegen --stages $TOFINO_STAGES +# Add gcc-14 so the p4 compiler can find cpp +PATH=/opt/gcc-14/bin:$PATH cargo xtask codegen --stages $TOFINO_STAGES # Preserve all the diagnostics spit out by the compiler mkdir -p /out/p4c-diags diff --git a/.github/buildomat/jobs/test.sh b/.github/buildomat/jobs/test.sh index 6b6726c4..0cbc05f6 100755 --- a/.github/buildomat/jobs/test.sh +++ b/.github/buildomat/jobs/test.sh @@ -2,7 +2,7 @@ #: #: name = "test" #: variety = "basic" -#: target = "helios-2.0" +#: target = "helios-3.0" #: rust_toolchain = true #: access_repos = [ #: "oxidecomputer/p4", diff --git a/Cargo.lock b/Cargo.lock index 75952de7..d47d1424 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -425,7 +425,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.12.1", @@ -462,11 +462,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -927,7 +927,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1097,7 +1097,7 @@ version = "0.0.0" source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" dependencies = [ "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis)", - "bitflags 2.9.4", + "bitflags 2.11.1", "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis)", "thiserror 1.0.69", ] @@ -1177,7 +1177,7 @@ source = "git+https://github.com/oxidecomputer/crucible?rev=7103cd3a3d7b0112d294 dependencies = [ "crucible-workspace-hack", "libc", - "num-derive 0.4.2", + "num-derive", "num-traits", "thiserror 2.0.18", ] @@ -1308,9 +1308,9 @@ dependencies = [ [[package]] name = "daft" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce0bf145758082da552af574185d4726000cd4b6e017d7966c99e7b2a5a086ce" +checksum = "b6a26f1f0a7934549bf8d8448d9da072c31f14e1e407b6cbacfdc07b3777988e" dependencies = [ "daft-derive", "newtype-uuid", @@ -1321,9 +1321,9 @@ dependencies = [ [[package]] name = "daft-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ad40aef90652e771af668d28abcc3ef35fd0d39438706a76a61588cf8e8e84a" +checksum = "27c6a4a4003df965e441d13b2a7044efa44334b567c984701f8a2773f815c5e2" dependencies = [ "proc-macro2", "quote", @@ -1587,6 +1587,7 @@ dependencies = [ "aal_macros", "anyhow", "asic", + "bytes", "cfg-if", "chrono", "clap", @@ -1620,6 +1621,7 @@ dependencies = [ "reqwest 0.12.23", "reqwest 0.13.2", "schemars 0.8.22", + "scuffle", "semver 1.0.27", "serde", "serde_json", @@ -2304,7 +2306,7 @@ name = "gateway-messages" version = "0.1.0" source = "git+https://github.com/oxidecomputer/management-gateway-service?rev=ea2f39ccdea124b5affcad0ca17bc5dacf65823a#ea2f39ccdea124b5affcad0ca17bc5dacf65823a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "hubpack", "serde", "serde-big-array", @@ -2437,7 +2439,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "libc", "libgit2-sys", "log", @@ -2912,7 +2914,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.0", "system-configuration", "tokio", "tower-layer", @@ -3033,9 +3035,9 @@ dependencies = [ [[package]] name = "iddqd" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b215e67ed1d1a4b1702acd787c487d16e4c977c5dcbcc4587bdb5ea26b6ce06" +checksum = "616230c7d641ef971a3a5bfcc654c6b7524ab9c9fb665693c6a397dba9a14aca" dependencies = [ "allocator-api2", "daft", @@ -3091,7 +3093,7 @@ name = "illumos-sys-hdrs" version = "0.1.0" source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7#4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", ] [[package]] @@ -3174,7 +3176,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a17a93829808685f3b6882763901d7489efc1155ad4ae568499d1b303067ca6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "ingot-macros", "ingot-types", "macaddr", @@ -3570,19 +3572,19 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "libc", "redox_syscall", ] [[package]] name = "libscf-sys" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f02d0eda38e8cc453c5ec5d49945545d8d9eb0e59cb2ce4152ba6518f373e7" +checksum = "d0d7bd6cfd9b5d32738cebd83a1b68060d96b1ca10d88bf9a5cb10dfac0f1cdf" dependencies = [ "libc", - "num-derive 0.3.3", + "num-derive", "num-traits", ] @@ -3637,9 +3639,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -4054,7 +4056,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -4112,17 +4114,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "num-derive" version = "0.4.2" @@ -4420,7 +4411,7 @@ version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "cfg-if", "foreign-types 0.3.2", "libc", @@ -4463,7 +4454,7 @@ name = "opte" version = "0.1.0" source = "git+https://github.com/oxidecomputer/opte?rev=4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7#4bd8a40c0f5c05de7bb29b5f592f2dc99b4fd1d7" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "dyn-clone", "illumos-sys-hdrs", "ingot", @@ -5435,7 +5426,7 @@ dependencies = [ "anyhow", "async-trait", "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis)", - "bitflags 2.9.4", + "bitflags 2.11.1", "bitstruct", "byteorder", "cpuid_utils", @@ -5506,7 +5497,7 @@ checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.4", + "bitflags 2.11.1", "lazy_static", "num-traits", "rand 0.9.2", @@ -5601,7 +5592,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.32", - "socket2 0.5.10", + "socket2 0.6.0", "thiserror 2.0.18", "tokio", "tracing", @@ -5638,7 +5629,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.0", "tracing", "windows-sys 0.60.2", ] @@ -5768,7 +5759,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", ] [[package]] @@ -5924,7 +5915,7 @@ version = "0.0.0" source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" dependencies = [ "ascii", - "bitflags 2.9.4", + "bitflags 2.11.1", "futures", "rgb_frame", "strum 0.26.3", @@ -5986,7 +5977,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -5995,14 +5986,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.0", ] @@ -6237,6 +6228,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "scuffle" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/scuffle#7181674535cc5ee0d461b2d53be19f181b9257e5" +dependencies = [ + "bitflags 2.11.1", + "chrono", + "libc", + "libscf-sys", + "num-traits", + "oxnet", + "thiserror 2.0.18", +] + [[package]] name = "search_path" version = "0.1.4" @@ -6258,7 +6263,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "core-foundation", "core-foundation-sys", "libc", @@ -7089,7 +7094,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "core-foundation", "system-configuration-sys", ] @@ -7192,14 +7197,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.1.2", + "rustix 1.1.4", "windows-sys 0.61.0", ] @@ -7218,7 +7223,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 1.1.2", + "rustix 1.1.4", "windows-sys 0.60.2", ] @@ -7275,6 +7280,7 @@ dependencies = [ "dpd-client 0.1.0", "futures", "http", + "iddqd", "internet-checksum", "ispf", "kstat-rs", @@ -7782,7 +7788,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "bytes", "futures-util", "http", @@ -7894,7 +7900,7 @@ name = "transceiver-messages" version = "0.1.1" source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#59b8432ec26c7a3725d5494937ca8bd6886c06a5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.1", "clap", "hubpack", "schemars 0.8.22", @@ -8686,7 +8692,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.0", ] [[package]] @@ -9076,7 +9082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.2", + "rustix 1.1.4", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 51a04a23..362dee8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ packet = { path = "packet" } pcap = { path = "pcap" } # oxide dependencies from github +scuffle = { git = "https://github.com/oxidecomputer/scuffle", version = "0.1.0", features = ["smf-by-instance"] } internal-dns-resolver = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } internal-dns-types = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } ispf = { git = "https://github.com/oxidecomputer/ispf" } @@ -78,6 +79,7 @@ expectorate = "1" futures = "0.3" http = "1.4.0" humantime = "2.3" +iddqd = "0.3.18" kstat-rs = "0.2.4" lazy_static = "1.5" libc = "0.2" diff --git a/asic/build.rs b/asic/build.rs index 48d2b3e8..f7d1e6a4 100644 --- a/asic/build.rs +++ b/asic/build.rs @@ -124,7 +124,7 @@ fn main() -> Result<()> { #[cfg(target_os = "illumos")] unsafe { env::set_var("AR", "/usr/bin/gar"); - env::set_var("LIBCLANG_PATH", "/opt/ooce/llvm/lib"); + env::set_var("LIBCLANG_PATH", "/opt/ooce/llvm-15/lib"); } if env::var("CARGO_FEATURE_TOFINO_ASIC").is_ok() { diff --git a/common/src/illumos.rs b/common/src/illumos.rs index 5eca9c2b..d3277981 100644 --- a/common/src/illumos.rs +++ b/common/src/illumos.rs @@ -14,6 +14,11 @@ pub mod smf; type Result = std::result::Result; +/// The suffix for the addrobj name for IPv6 link-local addresses on each tfport. +/// +/// E.g., all IPv6 addresses are named like `tfportrear0_0/ll`. +pub const IPV6_LINK_LOCAL_NAME: &str = "ll"; + #[derive(Debug, PartialEq, thiserror::Error)] pub enum IllumosError { /// This error indicates that the requested command wasn't able to run @@ -147,11 +152,8 @@ pub async fn address_add( let addr_obj = format!("{iface}/{tag}"); let addr: oxnet::IpNet = addr.into(); - if addr.is_ipv6() { - let tag = "ll"; - if !address_exists(iface, tag).await? { - linklocal_add(iface, tag).await?; - } + if addr.is_ipv6() && !address_exists(iface, IPV6_LINK_LOCAL_NAME).await? { + linklocal_add(iface, IPV6_LINK_LOCAL_NAME).await?; } let addr = addr.to_string(); diff --git a/common/src/network.rs b/common/src/network.rs index 6727804b..1a448820 100644 --- a/common/src/network.rs +++ b/common/src/network.rs @@ -129,6 +129,11 @@ impl MacAddr { self.a[5], ] } + + /// Return the bytes of the MAC as a slice. + pub const fn as_slice(&self) -> &[u8] { + self.a.as_slice() + } } #[derive(Error, Debug, Clone)] diff --git a/dpd/Cargo.toml b/dpd/Cargo.toml index 7a15905f..b733dbdb 100644 --- a/dpd/Cargo.toml +++ b/dpd/Cargo.toml @@ -31,6 +31,7 @@ dpd-types.workspace = true dpd-types-versions.workspace = true anyhow.workspace = true +bytes.workspace = true cfg-if.workspace = true chrono.workspace = true clap.workspace = true @@ -71,6 +72,7 @@ oximeter.workspace = true oximeter-producer.workspace = true smf.workspace = true transceiver-controller = { workspace = true, features = [ "api-traits" ] } +scuffle = { workspace = true, features = ["smf-by-instance"] } [dev-dependencies] expectorate.workspace = true diff --git a/dpd/build.rs b/dpd/build.rs index c5ddcc17..cb76a508 100644 --- a/dpd/build.rs +++ b/dpd/build.rs @@ -122,7 +122,7 @@ fn main() -> anyhow::Result<()> { #[cfg(target_os = "illumos")] unsafe { std::env::set_var("AR", "/usr/bin/gar"); - std::env::set_var("LIBCLANG_PATH", "/opt/ooce/llvm/lib"); + std::env::set_var("LIBCLANG_PATH", "/opt/ooce/llvm-15/lib"); } #[cfg(feature = "tofino_asic")] diff --git a/dpd/misc/ndpd.conf b/dpd/misc/ndpd.conf new file mode 100644 index 00000000..bf302750 --- /dev/null +++ b/dpd/misc/ndpd.conf @@ -0,0 +1 @@ +ifdefault StatefulAddrConf false diff --git a/dpd/src/api_server.rs b/dpd/src/api_server.rs index 3025d3cc..acc21638 100644 --- a/dpd/src/api_server.rs +++ b/dpd/src/api_server.rs @@ -2933,7 +2933,7 @@ fn path_to_qsfp(path: Path) -> Result { } } -fn build_info() -> BuildInfo { +pub(crate) fn build_info() -> BuildInfo { BuildInfo { version: env!("CARGO_PKG_VERSION").to_string(), git_sha: env!("VERGEN_GIT_SHA").to_string(), diff --git a/dpd/src/dhcpv6.rs b/dpd/src/dhcpv6.rs new file mode 100644 index 00000000..74e478f6 --- /dev/null +++ b/dpd/src/dhcpv6.rs @@ -0,0 +1,102 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +//! Code for managing DHCPv6 addresses on the technician ports. +//! +//! Some customers expect to lease IPv6 addresses to the technician ports using +//! DHCPv6. That protocol requires a stable client identifier, which is commonly +//! based on the MAC address of an interface on the host. However, we don't have +//! a stable MAC address on startup. The switch zone is started with a random +//! locally-administered MAC address for its bootstrap VNIC, which isn't an +//! acceptable basis for the client ID. We _do_ have a stable, unique MAC +//! address once we've fetched them from the switch VPD during bootstrapping. +//! But that bootstrapping requires a temporary, random MAC address, over which +//! we fetch the real MAC address. +//! +//! This presents a bit of a problem. `tfportd` is normally responsible for +//! creating and assigning IP addresses to the technician ports (along with all +//! the other interfaces). But it can't reliably be responsible for initiating +//! the DHCPv6 negotiation. `tfportd` does not and cannot know when we've +//! actually collected the stable MAC address from the switch VPD. It sees the +//! initial random MAC address, and creates the VLANs for Dendrite to fetch the +//! switch VPD. It then sees the new, real MAC address, and recreates all those +//! VLANs based on that. So `tfportd` doesn't really know the difference between +//! the initial random and real MAC addresses. +//! +//! Instead, we're intentionally violating the separation between `dpd` and +//! `tfportd` in this module. Dendrite knows when it's gotten the real MAC +//! addresses, and so it knows when DHCPv6 can proceed using a stable client ID. +//! Here we write that stable ID once we have it, wait for `tfportd` to create +//! the corresponding link-local IPv6 address on the technician ports, and then +//! start the DHCP agent running on those interfaces too. DHCP is not run on any +//! other interfaces at all. + +#[cfg_attr(target_os = "illumos", path = "dhcpv6/illumos.rs")] +#[cfg_attr(not(target_os = "illumos"), path = "dhcpv6/dummy.rs")] +mod dhcpv6_impl; + +use common::network::MacAddr; +use slog::Logger; + +/// Ensure that DHCPv6 is running on the technician ports. +/// +/// This is a small reconciler that continually ensures: +/// +/// - The client-identifier is written to disk +/// - The DHCP agent is running on the technician ports. +pub(crate) async fn ensure_dhcpv6_agent(log: Logger, base_mac: MacAddr) { + dhcpv6_impl::allow_dhcpv6_on_techports(log, base_mac).await +} + +#[cfg(any(target_os = "illumos", test))] +pub fn create_duid_bytes(base_mac: &MacAddr) -> Vec { + use bytes::BufMut as _; + + // To ensure we have a _stable_ DUID, which doesn't change during zone + // reboots, we use only the link-layer address. + // + // See https://www.rfc-editor.org/rfc/rfc8415#section-11.4 for details. + const DUID_TYPE: u16 = 0x03; + + // We're running on Ethernet links. + // + // See + // https://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml#arp-parameters-2. + const HARDWARE_TYPE: u16 = 0x01; + + // illumos creates its DUIDs using the `make_stable_duid()` function + // defined here: + // https://github.com/oxidecomputer/illumos-gate/blob/71b1f26fe641fba9ad5b9bca63cb9d00024578e5/usr/src/lib/libdhcpagent/common/dhcp_stable.c#L130. + // + // Importantly, it does no interpretation of the contents of the file + // when _using_ the DUID in a DHCPv6 exchange, so we're writing out the + // literal contents of the DUID-LL object, defined here: + // https://www.rfc-editor.org/rfc/rfc8415#section-11.4. + let sl = base_mac.as_slice(); + let mut bytes = Vec::with_capacity( + std::mem::size_of::() + std::mem::size_of::() + sl.len(), + ); + bytes.put_u16(DUID_TYPE); + bytes.put_u16(HARDWARE_TYPE); + bytes.put(sl); + + bytes +} + +#[cfg(test)] +mod tests { + use super::create_duid_bytes; + + #[tokio::test] + async fn test_create_duid_bytes() { + let mac = [0xa8, 0x40, 0x25, 0xfe, 0xfe, 0xfe]; + let bytes = create_duid_bytes(&mac.into()); + assert_eq!(bytes.len(), 2 + 2 + 6); + assert_eq!(u16::from_be_bytes(bytes[..2].try_into().unwrap()), 3); + assert_eq!(u16::from_be_bytes(bytes[2..4].try_into().unwrap()), 1); + assert_eq!(&bytes[4..], &mac); + } +} diff --git a/dpd/src/dhcpv6/dummy.rs b/dpd/src/dhcpv6/dummy.rs new file mode 100644 index 00000000..be51d9eb --- /dev/null +++ b/dpd/src/dhcpv6/dummy.rs @@ -0,0 +1,16 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +use common::network::MacAddr; +use slog::Logger; + +pub async fn allow_dhcpv6_on_techports(log: Logger, _base_mac: MacAddr) { + slog::debug!( + log, + "Not manipulating DHCPv6 at all. This software is not built for \ + both illumos and the Tofino ASIC feature"; + ); +} diff --git a/dpd/src/dhcpv6/illumos.rs b/dpd/src/dhcpv6/illumos.rs new file mode 100644 index 00000000..757a0fa1 --- /dev/null +++ b/dpd/src/dhcpv6/illumos.rs @@ -0,0 +1,210 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +use anyhow::Context as _; +use common::network::MacAddr; +use scuffle::Scf; +use slog::Logger; +use slog::debug; +use slog::error; +use slog::info; +use std::fmt::Write; +use std::time::Duration; + +/// Path of the final resulting DUID file. +const DUID_PATH: &str = "/etc/dhcp/duid"; + +/// Path of the temp DUID file, so we can atomically swap it. +const TEMP_DUID_PATH: &str = "/etc/dhcp/duid.temp"; + +/// Interval on which we retry the various operations we need to succeed. +const RETRY_INTERVAL: Duration = Duration::from_secs(5); + +/// List of techports we run DHCPv6 on. +const TECHPORTS: [&str; 2] = ["techport0", "techport1"]; + +/// Path of `in.ndpd`'s configuration file. +const NDPD_CONF_FILE: &str = "/etc/inet/ndpd.conf"; + +/// FMRI for the service running `in.ndpd +const NDPD_FMRI: &str = "svc:/network/routing/ndp:default"; + +/// Ensure that DHCPv6 is allowed on the techports. +pub async fn allow_dhcpv6_on_techports(log: Logger, base_mac: MacAddr) { + // First, always ensure the DUID file is written correctly. We can do this + // with no coordination and it has to be done first. + ensure_duid_file_exists(&log, &base_mac).await; + + // Now, rewrite the the NDP configuration file to allow DHCPv6 on the two + // techport interfaces. + rewrite_ndpd_conf(&log).await; + + // Restart `in.ndpd`. + // + // This can happen before or after `tfportd` creates the link-local + // addresses using the new MAC. If the restart happens first, `in.ndpd` will + // start DHCPv6 if possible when the addresses are recreated. In the other + // order, `in.ndpd` will read all the addresses on startup and being NDP and + // DHCPv6 if possible. + restart_ndpd(&log).await; +} + +/// Restart `in.ndpd`. +/// +/// This blocks until the restart has occurred. +async fn restart_ndpd(log: &Logger) { + loop { + let Err(e) = restart_ndpd_once().await else { + info!(log, "restarted `in.ndpd`"); + return; + }; + error!( + log, + "failed to start `in.ndpd`, will retry"; + "error" => %e, + ); + tokio::time::sleep(RETRY_INTERVAL).await; + } +} + +async fn restart_ndpd_once() -> anyhow::Result<()> { + let scf = Scf::connect_global_zone() + .context("connecting to SCF in current zone")?; + let mut instance = scf + .instance_from_fmri(NDPD_FMRI) + .context("creating SMF instance from FMRI")?; + instance.smf_restart().context("restarting NDPD SMF service") +} + +/// Write out new lines to the NDP configuration file allowing DHCPv6. +/// +/// This blocks until the rewrite has occurred. +async fn rewrite_ndpd_conf(log: &Logger) { + loop { + let Err(e) = rewrite_ndpd_conf_once().await else { + info!( + log, + "updated in.ndpd configuration file"; + "path" => NDPD_CONF_FILE, + ); + return; + }; + error!( + log, + "failed to update in.ndpd configuration file, will retry"; + "path" => NDPD_CONF_FILE, + "error" => %e, + ); + tokio::time::sleep(RETRY_INTERVAL).await; + } +} + +async fn rewrite_ndpd_conf_once() -> anyhow::Result<()> { + // NOTE: We completely replace the file. + // + // It might be safer to write only the parts we need, but it's very hard to + // ensure that we do that correctly, e.g., if there are partial writes. The + // cost is that this will be super confusing if we ever have more content in + // the `ndpd.conf` we ship with the switch zone. Those contents will be + // overwritten here. + let mut content = String::from("ifdefault StatefulAddrConf false\n"); + for techport in TECHPORTS { + writeln!(&mut content, "if {techport} StatefulAddrConf true") + .with_context(|| { + format!("writing if line for techport {techport}") + })?; + } + tokio::fs::write(NDPD_CONF_FILE, content) + .await + .context("writing in.ndpd conf file") +} + +/// Ensure our DHCPv6 Unique Identifier (DUID) is written persistently to disk. +/// +/// Some deployments run DHCPv6 over our technician ports. In that protocol, +/// clients identify themselves with a DUID, which is supposed to be a stable, +/// unique identifier so that servers can assign consistent configuration data +/// to the client. By default illumos's `dhcpagent` uses the Link-Layer Address +/// Plus Time option for this ID. However both the time and MAC address can +/// change, violating the stability requirement. The latter changes because the +/// switch zone is assigned a VNIC as its "first" datalink from the sled-agent +/// in the global zone. That VNIC has a random locally-administered prefix, +/// 02:08:20:... +/// +/// Once the `dhcpagent` has a DUID, it persists it to a file and reads that +/// whenever starting a new exchange. In the Oxide product, we're ensuring this +/// file contains our expected, stable ID, based on the MAC address stored in +/// the switch's SP FRUID EEPROM. +/// +/// This blocks until the file has been written. +async fn ensure_duid_file_exists(log: &Logger, base_mac: &MacAddr) { + // Always overwrite the file. There's no harm to doing so, and it avoids + // potential races when we restart. + loop { + let Err(e) = write_duid_file_once(log, base_mac).await else { + info!( + log, + "wrote DUID based on MAC to disk"; + "path" => DUID_PATH, + "MAC" => %base_mac, + ); + return; + }; + error!( + log, + "failed to write DUID to disk, will retry"; + "path" => DUID_PATH, + "MAC" => %base_mac, + "error" => %e, + ); + tokio::time::sleep(RETRY_INTERVAL).await; + } +} + +/// Atomically write the DUID file once. +async fn write_duid_file_once( + log: &Logger, + base_mac: &MacAddr, +) -> anyhow::Result<()> { + // Write the DUID into a temporary file. Note that this needs to be on the + // same filesystem as the real one, or the rename will fail. + // + // This creates the file if needed, and replaces the entire contents if + // it already exists. + let bytes = super::create_duid_bytes(base_mac); + match tokio::fs::write(&TEMP_DUID_PATH, &bytes).await { + Ok(_) => debug!(log, "wrote DUID to tempfile"), + Err(e) => { + anyhow::bail!( + "failed to write DUID to tempfile '{}': {}", + TEMP_DUID_PATH, + e, + ); + } + } + + // Atomically swap it into place. + match tokio::fs::rename(&TEMP_DUID_PATH, DUID_PATH).await { + Ok(_) => { + info!( + log, + "wrote stable DHCPv6 DUID"; + "path" => DUID_PATH, + "MAC" => base_mac.as_slice(), + ); + Ok(()) + } + Err(e) => { + anyhow::bail!( + "failed to rename DUID temp file '{}' to \ + real path '{}': {}", + TEMP_DUID_PATH, + DUID_PATH, + e, + ); + } + } +} diff --git a/dpd/src/macaddrs.rs b/dpd/src/macaddrs.rs index 7acff4e6..f526098b 100644 --- a/dpd/src/macaddrs.rs +++ b/dpd/src/macaddrs.rs @@ -371,27 +371,31 @@ impl Switch { } } - // We may start `dpd` without a base MAC address for assigning addresses to - // links. In that situation, we need to fetch the base MAC from the Sidecar - // SP via the management network. This presents us with a bootstrapping - // problem: we need a MAC to bring up that link, via which we'd fetch the - // MACs. To break the circularity, we will use a random MAC to temporarily - // bring up the CPU link; fetch the real MAC addresses; tear down the CPU - // link; and the continue as normal. We'll cache that as an SMF property to - // avoid the complexity each time we restart. - // - // In general, our process is: - // - // - Create a link on the CPU port, using a random MAC address. - // - Create a transceiver controller. This _will block_ until tfportd makes - // us the `sidecar0` VLAN interface that we're expecting to use. - // - Fetch the MAC address range from the SP using the controller. - // - Update the MAC address on the CPU link with the final address derived - // from the base_mac + /// Set the base MAC address for Dendrite. + /// + /// We may start `dpd` without a base MAC address for assigning addresses to + /// links. In that situation, we need to fetch the base MAC from the Sidecar + /// SP via the management network. This presents us with a bootstrapping + /// problem: we need a MAC to bring up that link, via which we'd fetch the + /// MACs. To break the circularity, we will use a random MAC to temporarily + /// bring up the CPU link; fetch the real MAC addresses; tear down the CPU + /// link; and the continue as normal. We'll cache that as an SMF property to + /// avoid the complexity each time we restart. + /// + /// In general, our process is: + /// + /// - Create a link on the CPU port, using a random MAC address. + /// - Create a transceiver controller. This _will block_ until tfportd makes + /// us the `sidecar0` VLAN interface that we're expecting to use. + /// - Fetch the MAC address range from the SP using the controller. + /// - Update the MAC address on the CPU link with the final address derived + /// from the base_mac + /// + /// This returns the base MAC address in any case. pub async fn set_base_mac_address( &self, autoconfig_links: &Option, - ) -> anyhow::Result { + ) -> anyhow::Result { slog::info!( self.log, "no base MAC address found, fetching from Sidecar FRUID" @@ -472,19 +476,22 @@ impl Switch { .expect("Expected a MAC for the internal CPU link"); self.set_link_mac_address(port_id, link_id, cpu_mac)?; - Ok(true) + Ok(base_mac) } } #[cfg(not(feature = "tofino_asic"))] impl Switch { /// Assign a permanent but random base MAC address on non-Tofino systems. + /// + /// Return the MAC address. pub async fn set_base_mac_address( &self, _autoconfig_links: &Option, - ) -> anyhow::Result { + ) -> anyhow::Result { // For non-ASIC builds, we just use a random MAC as our base address. - let base_mac = BaseMac::Permanent(MacAddr::random_oxide()); + let mac = MacAddr::random_oxide(); + let base_mac = BaseMac::Permanent(mac); debug!( self.log, "assigning random base MAC address"; @@ -492,7 +499,7 @@ impl Switch { ); let mut mgr = self.mac_mgmt.lock().unwrap(); mgr.set_base_mac(base_mac)?; - Ok(false) + Ok(mac) } } diff --git a/dpd/src/main.rs b/dpd/src/main.rs index a2b37ddc..a55df82d 100644 --- a/dpd/src/main.rs +++ b/dpd/src/main.rs @@ -59,6 +59,7 @@ mod arp; mod attached_subnet; mod config; mod counters; +mod dhcpv6; mod fault; mod freemap; mod link; @@ -411,9 +412,11 @@ impl Switch { entries: t .get_entries::(&self.asic_hdl, from_hardware) .map_err(|e| { - error!(self.log, "failed to get table contents"; - "table" => t.type_.to_string(), - "error" => %e); + error!( + self.log, "failed to get table contents"; + "table" => t.type_.to_string(), + "error" => %e + ); e }) .map(|vec| { @@ -436,9 +439,11 @@ impl Switch { t.get_counters::(&self.asic_hdl, force_sync) .map_err(|e| { - error!(self.log, "failed to get counter data"; - "table" => t.type_.to_string(), - "error" => %e); + error!( + self.log, "failed to get counter data"; + "table" => t.type_.to_string(), + "error" => %e + ); e }) .map(|vec| { @@ -701,9 +706,9 @@ async fn sidecar_main(mut switch: Switch) -> anyhow::Result<()> { // If there has been no base mac address configured via SMF or the command // line, then we need to fetch it from the SP (for real sidecars) or just // make one up (everywhere else). - let skip_cpu_link = match config_base_mac { - Some(base_mac) => { - let base_mac = BaseMac::Permanent(base_mac); + let (skip_cpu_link, base_mac) = match config_base_mac { + Some(config_mac) => { + let base_mac = BaseMac::Permanent(config_mac); debug!( switch.log, "permanent base MAC address already set, it will be kept"; @@ -711,11 +716,20 @@ async fn sidecar_main(mut switch: Switch) -> anyhow::Result<()> { ); let mut mgr = switch.mac_mgmt.lock().unwrap(); assert_eq!(mgr.set_base_mac(base_mac)?, None); - false + (false, config_mac) + } + None => { + let base_mac = + switch.set_base_mac_address(&autoconfig_links).await?; + (true, base_mac) } - None => switch.set_base_mac_address(&autoconfig_links).await?, }; + // Start the task managing DHCPv6 addresses on the technician ports. + let dhcpv6_log = switch.log.new(slog::o!("unit" => "dhcpv6-task")); + let dhcpv6_task = + tokio::task::spawn(dhcpv6::ensure_dhcpv6_agent(dhcpv6_log, base_mac)); + if let Some(auto_conf) = &autoconfig_links { // If we've created the link on the CPU port above, to fetch the MAC // addresses from the Sidecar, then we skip that particular link in this @@ -780,6 +794,7 @@ async fn sidecar_main(mut switch: Switch) -> anyhow::Result<()> { api_server_manager .await .expect("while shutting down the api_server_manager"); + dhcpv6_task.await?; info!(switch.log, "shutting down switch driver"); switch.asic_hdl.fini(); @@ -788,7 +803,6 @@ async fn sidecar_main(mut switch: Switch) -> anyhow::Result<()> { Ok(()) } - fn main() -> anyhow::Result<()> { let cli = Cli::parse(); @@ -802,7 +816,11 @@ async fn run_dpd(opt: Opt) -> anyhow::Result<()> { let log = common::logging::init("dpd", &config.log_file, config.log_format)?; - info!(log, "dpd config: {config:#?}"); + info!( + log, "starting dpd"; + "config" => ?config, + "build_info" => ?api_server::build_info(), + ); let p4_name = std::env::var("P4_NAME").unwrap_or_else(|_| String::from("sidecar")); diff --git a/pcap/build.rs b/pcap/build.rs index 94c8a543..239b94fb 100644 --- a/pcap/build.rs +++ b/pcap/build.rs @@ -41,7 +41,7 @@ fn main() { #[cfg(target_os = "illumos")] unsafe { std::env::set_var("AR", "/usr/bin/gar"); - std::env::set_var("LIBCLANG_PATH", "/opt/ooce/llvm/lib"); + std::env::set_var("LIBCLANG_PATH", "/opt/ooce/llvm-15/lib"); } gen_bindings().unwrap(); diff --git a/tfportd/Cargo.toml b/tfportd/Cargo.toml index 06476957..5ef214ed 100644 --- a/tfportd/Cargo.toml +++ b/tfportd/Cargo.toml @@ -15,6 +15,7 @@ chrono.workspace = true csv.workspace = true futures.workspace = true http.workspace = true +iddqd.workspace = true internet-checksum.workspace = true ispf.workspace = true libc.workspace = true diff --git a/tfportd/src/linklocal.rs b/tfportd/src/linklocal.rs index 4b61b242..2ebc419c 100644 --- a/tfportd/src/linklocal.rs +++ b/tfportd/src/linklocal.rs @@ -10,14 +10,11 @@ use std::net::Ipv6Addr; use anyhow::Result; use anyhow::anyhow; use slog::debug; +use slog::error; use crate::Global; use common::illumos; - -/// The suffix for the addrobj name for IPv6 link-local addresses on each tfport. -/// -/// E.g., all IPv6 addresses are named like `tfportrear0_0/ll`. -const IPV6_LINK_LOCAL_NAME: &str = "ll"; +use common::illumos::IPV6_LINK_LOCAL_NAME; // Parse a single line of ipadm output to extract the addrobj name and link-local // address. This function returns an error if the ipadm command fails or the @@ -77,10 +74,13 @@ pub async fn get_all() -> Result> { } // Create a link-local address for an interface +// +// TODO-cleanup: This function is actually infallible. We should either +// propagate the error or actually reflect its infallibility in the return type. pub async fn create(g: &Global, iface: &str) -> anyhow::Result<()> { debug!(g.log, "creating link-local address for {iface}"); if let Err(e) = illumos::linklocal_add(iface, IPV6_LINK_LOCAL_NAME).await { - slog::error!(g.log, "failed to create link-local address: {e:?}"); + error!(g.log, "failed to create link-local address: {e:?}"); } Ok(()) } diff --git a/tfportd/src/ports.rs b/tfportd/src/ports.rs index c300fa1a..8005d598 100644 --- a/tfportd/src/ports.rs +++ b/tfportd/src/ports.rs @@ -134,17 +134,21 @@ async fn dpd_port_update(g: &Global, links: &mut LinkMap) -> Result<()> { // Any change here should cause a mismatch when looking at the existing // tfports. if link.mac != entry_mac { - warn!(g.log, "tfport changed mac addresses"; - "tfport" => &expected_tfport, - "mac" => entry_mac.to_string(), - "stale_mac" => link.mac.to_string()); + warn!( + g.log, "tfport changed mac addresses"; + "tfport" => &expected_tfport, + "mac" => entry_mac.to_string(), + "stale_mac" => link.mac.to_string() + ); link.mac = entry_mac; } if link.asic_id != entry.asic_id { - warn!(g.log, "tfport changed asic IDs"; - "tfport" => &expected_tfport, - "asic_id" => entry.asic_id, - "stale_asic_id" => link.asic_id); + warn!( + g.log, "tfport changed asic IDs"; + "tfport" => &expected_tfport, + "asic_id" => entry.asic_id, + "stale_asic_id" => link.asic_id + ); link.asic_id = entry.asic_id; } } @@ -184,18 +188,19 @@ async fn illumos_port_update( link.tfport_link_local = data.link_local; continue; } else { - info!(g.log, "tfport found with stale data"; - "tfport" => tfport, - "mac" => link.mac.to_string(), - "stale_mac" => data.mac.to_string(), - "asic_id" => link.asic_id, - "stale_asic_id" => data.port); + info!( + g.log, "tfport found with stale data"; + "tfport" => tfport, + "mac" => link.mac.to_string(), + "stale_mac" => data.mac.to_string(), + "asic_id" => link.asic_id, + "stale_asic_id" => data.port + ); } } None => { if link.tfport.is_some() { - info!(g.log, "tfport disappeared"; - "tfport" => tfport); + info!(g.log, "tfport disappeared"; "tfport" => tfport); } } } @@ -282,9 +287,11 @@ pub async fn port_loop(g: Arc) { // Clean up any orphaned tfports for tfport in orphans { if let Err(e) = tfport::tfport_delete(&g, &tfport).await { - error!(g.log, - "failed to clean up stale tfport: {e:?}"; - "tfport" => tfport) + error!( + g.log, + "failed to clean up stale tfport: {e:?}"; + "tfport" => tfport + ); } } @@ -300,15 +307,19 @@ pub async fn port_loop(g: Arc) { } if let Err(e) = tfport::tfport_ensure(&g, tfport, link).await { - error!(g.log, - "tfport_ensure() failed: {e:?}"; - "tfport" => tfport) + error!( + g.log, + "tfport_ensure() failed: {e:?}"; + "tfport" => tfport + ); } if let Err(e) = ensure_address_match(&g, link).await { - error!(g.log, - "ensure_address_match() failed: {e:?}"; - "tfport" => tfport) + error!( + g.log, + "ensure_address_match() failed: {e:?}"; + "tfport" => tfport + ); } } *g.tfport_to_asic.lock().unwrap() = tfport_to_asic; diff --git a/tfportd/src/vlans.rs b/tfportd/src/vlans.rs index 6aac5f8d..7dbea3d3 100644 --- a/tfportd/src/vlans.rs +++ b/tfportd/src/vlans.rs @@ -11,19 +11,27 @@ use std::net::Ipv6Addr; use anyhow::Context; use anyhow::anyhow; use anyhow::bail; +use iddqd::IdOrdItem; +use iddqd::IdOrdMap; +use iddqd::id_upcast; use serde::Deserialize; use slog::error; use slog::info; +use slog::warn; use crate::Global; use crate::linklocal; use crate::oxstats::link; use common::illumos; +/// An entry in the `port_map.csv` file provided at program startup. #[derive(Debug, Deserialize)] struct PortMapEntry { + /// The VSC7448 port number. port: u16, + /// A human-friendly string naming the logical partner on the link. _link_partner: String, + /// The name of the VLAN object to be created mapping to the link partner. vlan_name: String, } @@ -34,9 +42,11 @@ pub struct Vlan { pub name: String, } -/// The information illumos maintains about a single vlan +/// The information illumos maintains about a single VLAN #[derive(Debug)] pub struct VlanInfo { + /// The name of the VLAN device. + pub name: String, /// VLAN ID pub vid: u16, /// index of the interface created by `ipadm` @@ -45,18 +55,28 @@ pub struct VlanInfo { pub link_local: Option, } +impl IdOrdItem for VlanInfo { + type Key<'a> = &'a str; + + fn key(&self) -> Self::Key<'_> { + &self.name + } + + id_upcast!(); +} + /// Get the list of vlans created on top of a tfport -async fn vlans_get(tfport: &str) -> anyhow::Result> { +async fn vlans_get(tfport: &str) -> anyhow::Result> { let link_locals = linklocal::get_all().await?; let lines = illumos::dladm(&["show-vlan", "-p", "-o", "link,vid,over"]).await?; // Iterate over the dladm output, extracting the vlan name and vid from each // line. For each vlan created on top of this tfport, add an entry to the - // BTreeMap with the network configuration for each one. - let mut rval = BTreeMap::new(); + // map with the network configuration for each one. + let mut rval = IdOrdMap::new(); for vlan in lines { - let fields: Vec = vlan.split(':').map(str::to_string).collect(); + let fields: Vec<_> = vlan.split(':').collect(); if fields.len() != 3 { bail!("show-vlan returned invalid result: {vlan}"); } @@ -68,7 +88,11 @@ async fn vlans_get(tfport: &str) -> anyhow::Result> { let vid = fields[1].parse::().context("invalid vlan_id")?; let ifindex = crate::netsupport::get_ifindex(&link); let link_local = link_locals.get(&link).copied(); - rval.insert(link, VlanInfo { vid, ifindex, link_local }); + let vlan = VlanInfo { name: link, vid, ifindex, link_local }; + + // NOTE: We previously used a BTreeMap here, and ignored any duplicates. + // Keep the same behavior, ignoring the error. + let _ = rval.insert_overwrite(vlan); } Ok(rval) } @@ -92,7 +116,8 @@ pub async fn vlans_cleanup(g: &Global, tfport: &str) -> anyhow::Result<()> { .await .map_err(|e| anyhow!("failed to get vlan list for {tfport}: {e:?}"))?; - for (name, vlan) in &vlans { + for vlan in &vlans { + let name = &vlan.name; let vid = vlan.vid; match vlan_delete(g, name).await { Ok(_) => info!(g.log, "deleted vlan {vid}:{name} on {tfport}"), @@ -117,14 +142,17 @@ pub async fn ensure_vlans(g: &Global, link: &str) -> anyhow::Result<()> { // links that should be created and/or deleted. We start by pessimistically // assuming that each vlan needs to be deleted, removing them from the // to_delete list if we find them on the "expected" list below. - let mut to_delete = - existing_vlans.keys().cloned().collect::>(); + let mut to_delete = existing_vlans + .iter() + .map(|vlan| vlan.name.to_string()) + .collect::>(); for expected_vlan in &g.vlans { - if let Some(current_vlan) = existing_vlans.get(&expected_vlan.name) + if let Some(current_vlan) = + existing_vlans.get(expected_vlan.name.as_str()) && current_vlan.vid == expected_vlan.vid { // This vlan has the right name and ID, so we leave it alone - let _ = to_delete.remove(&expected_vlan.name); + let _ = to_delete.remove(expected_vlan.name.as_str()); continue; } to_create.insert(expected_vlan.name.to_string(), expected_vlan.vid); @@ -135,7 +163,7 @@ pub async fn ensure_vlans(g: &Global, link: &str) -> anyhow::Result<()> { if let Err(e) = vlan_delete(g, name).await { error!(g.log, "failed to delete vlan {name}: {e:?}"); } - let _ = existing_vlans.remove(name); + let _ = existing_vlans.remove(name.as_str()); } // Create any missing vlans @@ -143,16 +171,28 @@ pub async fn ensure_vlans(g: &Global, link: &str) -> anyhow::Result<()> { match illumos::vlan_create(link, vid, &name).await { Ok(()) => { info!(g.log, "created vlan {vid}:{name} on {link}"); - existing_vlans.insert( - name.to_string(), - VlanInfo { vid, ifindex: None, link_local: None }, - ); + let vlan = VlanInfo { + name: name.clone(), + vid, + ifindex: None, + link_local: None, + }; + // NOTE: We previously used a BTreeMap here, and ignored any duplicates. + // Keep the same behavior, logging an error. + if let Some(old) = existing_vlans.insert_overwrite(vlan) { + warn!( + &g.log, + "overwriting duplicate VLAN for tfport"; + "tfport" => link, + "vlan" => old.name, + "vid" => old.vid, + ); + } // Once the vlan is created, we can track it as a potential // network link. - if let Err(e) = g - .link_tracker - .track_link(name.to_string(), link::ModelType::Vlan) + if let Err(e) = + g.link_tracker.track_link(&name, link::ModelType::Vlan) { error!(g.log, "failed to track vlan {name}: {e:?}"); } @@ -165,39 +205,36 @@ pub async fn ensure_vlans(g: &Global, link: &str) -> anyhow::Result<()> { // Iterate over all of the vlans (old and new) and ensure that they have a // link-local address. - for (name, info) in existing_vlans.iter_mut() { + for mut info in existing_vlans.iter_mut() { // If the interface exists but the address doesn't, we need to remove the // interface before creating the link-local address due to stlouis#531. if info.link_local.is_none() && info.ifindex.is_some() - && illumos::iface_remove(name).await.is_ok() + && illumos::iface_remove(&info.name).await.is_ok() { info.ifindex = None; } if info.ifindex.is_none() { - match illumos::iface_ensure(name).await { + match illumos::iface_ensure(&info.name).await { Ok(()) => { - slog::debug!(g.log, "created interface for vlan: {name}") + slog::debug!( + g.log, + "created interface for vlan: {}", + &info.name + ) } Err(e) => { slog::error!( g.log, - "failed to create interface for vlan: {name}: {e}" + "failed to create interface for vlan: {}: {e}", + &info.name, ); continue; } } } if info.link_local.is_none() { - match linklocal::create(g, name).await { - Ok(()) => { - slog::debug!(g.log, "created link-local for vlan: {name}") - } - Err(e) => slog::error!( - g.log, - "failed to create link-local for vlan: {name}: {e}" - ), - } + let _ = linklocal::create(g, &info.name).await; } } diff --git a/tools/omicron-asic-manifest.toml b/tools/omicron-asic-manifest.toml index 01b7231d..8bc68a2b 100644 --- a/tools/omicron-asic-manifest.toml +++ b/tools/omicron-asic-manifest.toml @@ -23,6 +23,7 @@ source.paths = [ {from = "target/proto/opt/oxide/tofino_sde/lib/libtarget_utils.so" , to = "/opt/oxide/tofino_sde/lib/libtarget_utils.so"}, {from = "target/proto/opt/oxide/tofino_sde/lib/libclish.so" , to = "/opt/oxide/tofino_sde/lib/libclish.so"}, {from = "target/proto/opt/oxide/dendrite/sidecar/share/platforms/board-maps/oxide/" , to = "/opt/oxide/dendrite/sidecar/share/platforms/board-maps/oxide/"}, - {from = "target/proto/opt/oxide/dendrite/sidecar/share/cli/xml" , to = "/opt/oxide/dendrite/sidecar/share/cli/xml"} + {from = "target/proto/opt/oxide/dendrite/sidecar/share/cli/xml" , to = "/opt/oxide/dendrite/sidecar/share/cli/xml"}, + {from = "dpd/misc/ndpd.conf" , to = "/etc/inet/ndpd.conf"} ] output.type = "zone" diff --git a/uplinkd/src/main.rs b/uplinkd/src/main.rs index 7ac1d757..08d3c83a 100644 --- a/uplinkd/src/main.rs +++ b/uplinkd/src/main.rs @@ -40,6 +40,7 @@ use anyhow::Result; use anyhow::anyhow; use clap::Parser; use common::illumos::AddressFamily; +use common::illumos::IPV6_LINK_LOCAL_NAME; use libc::c_int; use oxnet::IpNet; use oxnet::Ipv4Net; @@ -413,9 +414,11 @@ async fn create_link(iface: &str, tag: &str, addr: &IpNet) -> Result { // Add a link-local address using ipadm async fn create_linklocal(iface: &str) -> Result { - illumos::linklocal_add(iface, "ll") + illumos::linklocal_add(iface, IPV6_LINK_LOCAL_NAME) .await - .map(|_| format!("created link-local as {iface}/ll")) + .map(|_| { + format!("created link-local as {iface}/{IPV6_LINK_LOCAL_NAME}") + }) .map_err(|e| e.into()) } diff --git a/xtask/src/codegen.rs b/xtask/src/codegen.rs index a89078a7..87fedc8a 100644 --- a/xtask/src/codegen.rs +++ b/xtask/src/codegen.rs @@ -175,7 +175,10 @@ pub fn build( args.push(app_path); println!("op: {args:?}"); - let out = Command::new(&p4c_path).args(&args).output()?; + let out = Command::new(&p4c_path) + .args(&args) + .output() + .with_context(|| format!("opening '{p4c_path}'"))?; let stdout = String::from_utf8_lossy(&out.stdout); let stderr = String::from_utf8_lossy(&out.stderr); if !out.status.success() {