Skip to content
Open
12 changes: 8 additions & 4 deletions .github/buildomat/jobs/packet-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
#### - JUST_TEST=1 Just runs the tests, skipping system prep.
#### - TESTNAME='$name' Will just run the specified test.
#### - STARTUP_TIMEOUT=n Seconds to wait for tofino-model/dpd to start.
#### Defaults to 15.
#### Defaults to 45.
#### - NOBUILD=1 Don't build sidecar.p4 (in case you've already
#### built it)
####
#### <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

Expand All @@ -33,7 +35,7 @@ source .github/buildomat/linux.sh

wd=`pwd`
export WS=$wd
STARTUP_TIMEOUT=${STARTUP_TIMEOUT:=15}
STARTUP_TIMEOUT=${STARTUP_TIMEOUT:=45}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I've never seen this be an issue before. Are you seeing this in CI or somewhere else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see it regularly when running the tests locally on a development VM.


function cleanup {
set +o errexit
Expand Down Expand Up @@ -76,8 +78,10 @@ fi
export SDE=/opt/oxide/tofino_sde

banner "Build"
cargo build --features=tofino_asic --bin dpd --bin swadm
cargo xtask codegen --stages 19
if [[ $NOBUILD -ne 1 ]]; then
cargo build --features=tofino_asic --bin dpd --bin swadm
cargo xtask codegen --stages 19
fi

banner "Test"
sudo -E ./tools/veth_setup.sh
Expand Down
64 changes: 64 additions & 0 deletions asic/src/softnpu/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,35 @@ impl TableOps<Handle> for Table {
}
("forward", params)
}
(ROUTER4_LOOKUP_RT, "forward_v6") => {
let mut params = Vec::new();
for arg in action_data.args.iter() {
match &arg.value {
ValueTypes::U64(v) => {
// 16 bit port
match arg.name.as_str() {
"port" => {
params.extend_from_slice(
&(*v as u16).to_le_bytes(),
);
}
x => {
error!(
hdl.log,
"unexpected parameter: {dpd_table}::forward {x}"
)
}
}
}
ValueTypes::Ptr(v) => {
let mut buf = v.clone();
buf.reverse();
params.extend_from_slice(buf.as_slice());
}
}
}
("forward", params)
}
(ROUTER4_LOOKUP_RT, "forward_vlan") => {
let mut params = Vec::new();
for arg in action_data.args.iter() {
Expand Down Expand Up @@ -241,6 +270,41 @@ impl TableOps<Handle> for Table {
}
("forward_vlan", params)
}
(ROUTER4_LOOKUP_RT, "forward_vlan_v6") => {
let mut params = Vec::new();
for arg in action_data.args.iter() {
match &arg.value {
ValueTypes::U64(v) => {
// 16 bit port
// 12 bit vlan
match arg.name.as_str() {
"vlan_id" => {
params.extend_from_slice(
&(*v as u16).to_le_bytes(),
);
}
"port" => {
params.extend_from_slice(
&(*v as u16).to_le_bytes(),
);
}
x => {
error!(
hdl.log,
"unexpected parameter: {dpd_table}::forward_vlan {x}"
)
}
}
}
ValueTypes::Ptr(v) => {
let mut buf = v.clone();
buf.reverse();
params.extend_from_slice(buf.as_slice());
}
}
}
("forward_vlan", params)
}
(ROUTER6_LOOKUP_IDX, "index") => {
let mut params = Vec::new();
for arg in action_data.args.iter() {
Expand Down
120 changes: 115 additions & 5 deletions dpd-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// 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 2025 Oxide Computer Company
// Copyright 2026 Oxide Computer Company

//! DPD endpoint definitions.

Expand All @@ -25,7 +25,7 @@ use dpd_types::{
link::{LinkFsmCounters, LinkId, LinkUpCounter},
mcast, oxstats,
port_map::BackplaneLink,
route::{Ipv4Route, Ipv6Route},
route::{Ipv4Route, Ipv6Route, Route},
switch_identifiers::SwitchIdentifiers,
switch_port::{Led, ManagementMode},
transceivers::Transceiver,
Expand All @@ -44,6 +44,8 @@ use transceiver_controller::{
Datapath, Monitors, PowerState, message::LedState,
};

mod v1;

api_versions!([
// WHEN CHANGING THE API (part 1 of 2):
//
Expand All @@ -56,6 +58,7 @@ api_versions!([
// | example for the next person.
// v
// (next_int, IDENT),
(3, V4_OVER_V6_ROUTES),
(2, DUAL_STACK_NAT_WORKFLOW),
(1, INITIAL),
]);
Expand Down Expand Up @@ -283,6 +286,48 @@ pub trait DpdApi {
#[endpoint {
method = GET,
path = "/route/ipv4",
versions = ..VERSION_V4_OVER_V6_ROUTES
}]
async fn route_ipv4_list_v1(
rqctx: RequestContext<Self::Context>,
query: Query<PaginationParams<EmptyScanParams, Ipv4RouteToken>>,
) -> Result<HttpResponseOk<ResultsPage<v1::Ipv4Routes>>, HttpError> {
let result = Self::route_ipv4_list(rqctx, query).await?.0;

let mut v2_result = Vec::default();
for x in result.items {
let mut v2_routes = v1::Ipv4Routes {
cidr: x.cidr,
targets: Vec::default(),
};
for t in x.targets {
if let Route::V4(r) = &t {
v2_routes.targets.push(Ipv4Route {
tag: r.tag.clone(),
port_id: r.port_id,
link_id: r.link_id,
tgt_ip: r.tgt_ip,
vlan_id: r.vlan_id,
});
}
}
v2_result.push(v2_routes);
}

Ok(HttpResponseOk(ResultsPage {
next_page: result.next_page,
items: v2_result,
}))
}

/**
* Fetch the configured IPv4 routes, mapping IPv4 CIDR blocks to the switch port
* used for sending out that traffic, and optionally a gateway.
*/
#[endpoint {
method = GET,
path = "/route/ipv4",
versions = VERSION_V4_OVER_V6_ROUTES..
}]
async fn route_ipv4_list(
rqctx: RequestContext<Self::Context>,
Expand All @@ -295,11 +340,33 @@ pub trait DpdApi {
#[endpoint {
method = GET,
path = "/route/ipv4/{cidr}",
versions = VERSION_V4_OVER_V6_ROUTES..
}]
async fn route_ipv4_get(
rqctx: RequestContext<Self::Context>,
path: Path<RoutePathV4>,
) -> Result<HttpResponseOk<Vec<Ipv4Route>>, HttpError>;
) -> Result<HttpResponseOk<Vec<Route>>, HttpError>;

#[endpoint {
method = GET,
path = "/route/ipv4/{cidr}",
versions = ..VERSION_V4_OVER_V6_ROUTES
}]
async fn route_ipv4_get_v2(
rqctx: RequestContext<Self::Context>,
path: Path<RoutePathV4>,
) -> Result<HttpResponseOk<Vec<Ipv4Route>>, HttpError> {
let result = Self::route_ipv4_get(rqctx, path).await?.0;
Ok(HttpResponseOk(
result
.into_iter()
.flat_map(|r| match r {
Route::V4(r) => Some(r),
_ => None,
})
.collect(),
))
}

/**
* Route an IPv4 subnet to a link and a nexthop gateway.
Expand All @@ -316,6 +383,22 @@ pub trait DpdApi {
update: TypedBody<Ipv4RouteUpdate>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/**
* Route an IPv4 subnet to a link and an IPv6 nexthop gateway.
*
* This call can be used to create a new single-path route or to add new targets
* to a multipath route.
*/
#[endpoint {
method = POST,
path = "/route/ipv4-over-ipv6",
versions = VERSION_V4_OVER_V6_ROUTES..,
}]
async fn route_ipv4_over_ipv6_add(
rqctx: RequestContext<Self::Context>,
update: TypedBody<Ipv4OverIpv6RouteUpdate>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/**
* Route an IPv4 subnet to a link and a nexthop gateway.
*
Expand All @@ -331,6 +414,22 @@ pub trait DpdApi {
update: TypedBody<Ipv4RouteUpdate>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/**
* Route an IPv4 subnet to a link and an IPv6 nexthop gateway.
*
* This call can be used to create a new single-path route or to replace any
* existing routes with a new single-path route.
*/
#[endpoint {
method = PUT,
path = "/route/ipv4-over-ipv6",
versions = VERSION_V4_OVER_V6_ROUTES..,
}]
async fn route_ipv4_over_ipv6_set(
rqctx: RequestContext<Self::Context>,
update: TypedBody<Ipv4OverIpv6RouteUpdate>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/**
* Remove all targets for the given subnet
*/
Expand Down Expand Up @@ -1929,6 +2028,18 @@ pub struct Ipv4RouteUpdate {
pub replace: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Ipv4OverIpv6RouteUpdate {
/// Traffic destined for any address within the CIDR block is routed using
/// this information.
pub cidr: Ipv4Net,
/// A single Route associated with this CIDR
pub target: Ipv6Route,
/// Should this route replace any existing route? If a route exists and
/// this parameter is false, then the call will fail.
pub replace: bool,
}

/// Represents a new or replacement mapping of a subnet to a single IPv6
/// RouteTarget nexthop target.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
Expand All @@ -1943,14 +2054,13 @@ pub struct Ipv6RouteUpdate {
pub replace: bool,
}

/// Represents all mappings of an IPv4 subnet to a its nexthop target(s).
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Ipv4Routes {
/// Traffic destined for any address within the CIDR block is routed using
/// this information.
pub cidr: Ipv4Net,
/// All RouteTargets associated with this CIDR
pub targets: Vec<Ipv4Route>,
pub targets: Vec<Route>,
}

/// Represents all mappings of an IPv6 subnet to a its nexthop target(s).
Expand Down
20 changes: 20 additions & 0 deletions dpd-api/src/v1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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 dpd_types::route::Ipv4Route;
use oxnet::Ipv4Net;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Represents all mappings of an IPv4 subnet to a its nexthop target(s).
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Ipv4Routes {
/// Traffic destined for any address within the CIDR block is routed using
/// this information.
pub cidr: Ipv4Net,
/// All RouteTargets associated with this CIDR
pub targets: Vec<Ipv4Route>,
}
2 changes: 1 addition & 1 deletion dpd-client/README-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ All environment variables are prefixed by `DENDRITE_TEST_` for clarity.
representation of packets on failure. The second bit controls whether to
display the hex of each packet body as well.
- `DENDRITE_TEST_TIMEOUT`: The amount of time to wait for any single test's
network traffic to complete, specified in milliseconds. The default is 500,
network traffic to complete, specified in milliseconds. The default is 1500,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I never (almost never?) hit this in CI. When running on the colo, 500ms works for everything except a single multicast test. I'd rather not change the default if it's just failing on a single machine somewhere, since it seems like it could tripe the time it takes to run the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've hit what I believe is this several times in CI recently, i would even characterize it as hitting often. It also happens for me regularly on a development VM.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I suspect any timing issues you hit in CI were #172.

which works for a reasonably powerful system under light load.

## Parallelization
Expand Down
Loading