Merge branch 'net-hsr-prp-redbox-prp-san-support'

Xin Xie says:

====================
net: hsr: PRP RedBox (PRP-SAN) support

This series adds PRP RedBox support to the hsr driver: a PRP node that
proxies one or more SANs sitting behind an interlink port (IEC 62439-3,
PRP-SAN). HSR-SAN has been supported since commit 5055cccfc2 ("net: hsr:
Provide RedBox support (HSR-SAN)"); this extends the equivalent capability
to PRP, reusing the existing protocol-neutral proxy machinery
(proxy_node_db, hsr_proxy_announce(), hsr_prune_proxy_nodes()).

A SAN behind the interlink does bidirectional unicast with peers on the PRP
network, its source MAC is preserved on the wire, the PRP RCT is correct,
and the RedBox announces each proxied SAN with the RedBox-MAC TLV (Type 30)
in its supervision frames.

The series is bisect-safe: the datapath, duplicate discard and supervision
support are added first; the rtnetlink rejection of "type hsr ... interlink
<dev> proto 1" is removed only in patch 3, once the feature is complete.

Design notes:

 - prp_drop_frame() does not walk the node tables. The destination
   classification (PRP-network node vs proxied SAN) is resolved once per
   frame in fill_frame_info() and cached in struct hsr_frame_info, so the
   per egress-port drop decision is O(1) in the softIRQ path. The
   classification is gated on PRP RedBox devices (prot_version == PRP_V1 &&
   hsr->redbox), so HSR RedBox traffic is not affected.

 - The LAN A/B duplicate test is factored into prp_is_lan_dup() so the new
   PRP interlink rules in prp_drop_frame() do not change hsr_drop_frame()
   behaviour, including the NETIF_F_HW_HSR_FWD path. This is software PRP
   RedBox only; it adds no new hardware-offload contract.

 - The supervision emitter uses pre-reserved tailroom (hsr_init_skb() +
   skb_put()) on the existing GFP_ATOMIC path; no skb_linearize() or
   pskb_expand_head(). The RedBox-MAC TLV is followed by an explicit EOT
   (Type 0, Length 0); padding via skb_put_padto(ETH_ZLEN) and the 6-byte
   PRP RCT remain at the absolute tail of the egress frame.

 - The hsr_get_node() hsr_ethhdr length guard is relaxed only for PRP
   supervision frames (prot_version == PRP_V1 && ETH_P_PRP && is_sup), which
   are untagged with mac_len == ETH_HLEN. HSR (ETH_P_HSR) supervision is
   front-tagged and keeps the original length requirement, so HSR
   malformed-frame filtering is unchanged.
====================

Link: https://patch.msgid.link/20260717201457.54-1-xiexinet@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Jakub Kicinski 2026-07-27 17:46:47 -07:00
commit d5990d2a86
8 changed files with 204 additions and 25 deletions

View File

@ -372,10 +372,21 @@ static void send_prp_supervision_frame(struct hsr_port *master,
{
struct hsr_priv *hsr = master->hsr;
struct hsr_sup_payload *hsr_sp;
struct hsr_sup_tlv *hsr_stlv;
struct hsr_sup_tag *hsr_stag;
struct sk_buff *skb;
bool redbox_proxy;
int extra = 0;
skb = hsr_init_skb(master, 0);
redbox_proxy = hsr->redbox && master->type == HSR_PT_INTERLINK;
/* A proxy-announce carries a RedBox-MAC TLV and an EOT marker. */
if (redbox_proxy)
extra = sizeof(struct hsr_sup_tlv) +
sizeof(struct hsr_sup_payload) +
sizeof(struct hsr_sup_tlv);
skb = hsr_init_skb(master, extra);
if (!skb) {
netdev_warn_once(master->dev, "PRP: Could not send supervision frame\n");
return;
@ -393,9 +404,25 @@ static void send_prp_supervision_frame(struct hsr_port *master,
hsr_stag->tlv.HSR_TLV_type = PRP_TLV_LIFE_CHECK_DD;
hsr_stag->tlv.HSR_TLV_length = sizeof(struct hsr_sup_payload);
/* Payload: MacAddressA */
/* Payload: MacAddressA, the announced node. */
hsr_sp = skb_put(skb, sizeof(struct hsr_sup_payload));
ether_addr_copy(hsr_sp->macaddress_A, master->dev->dev_addr);
ether_addr_copy(hsr_sp->macaddress_A, addr);
/* Proxy-announce: append the RedBox-MAC TLV (Type 30) and an explicit
* EOT to terminate the TLV chain before zero padding.
*/
if (redbox_proxy) {
hsr_stlv = skb_put(skb, sizeof(struct hsr_sup_tlv));
hsr_stlv->HSR_TLV_type = PRP_TLV_REDBOX_MAC;
hsr_stlv->HSR_TLV_length = sizeof(struct hsr_sup_payload);
hsr_sp = skb_put(skb, sizeof(struct hsr_sup_payload));
ether_addr_copy(hsr_sp->macaddress_A, hsr->macaddress_redbox);
hsr_stlv = skb_put(skb, sizeof(struct hsr_sup_tlv));
hsr_stlv->HSR_TLV_type = HSR_TLV_EOT;
hsr_stlv->HSR_TLV_length = 0;
}
if (skb_put_padto(skb, ETH_ZLEN)) {
spin_unlock_bh(&hsr->seqnr_lock);
@ -768,6 +795,15 @@ int hsr_dev_finalize(struct net_device *hsr_dev, struct net_device *slave[2],
/* Make sure the 1st call to netif_carrier_on() gets through */
netif_carrier_off(hsr_dev);
/* Publish the RedBox state before any port is attached: the rx
* handlers are live from hsr_add_port() on, and hsr_add_node()
* sizes each node's per-port sequence state from hsr->redbox.
*/
if (interlink) {
hsr->redbox = true;
ether_addr_copy(hsr->macaddress_redbox, interlink->dev_addr);
}
res = hsr_add_port(hsr, hsr_dev, HSR_PT_MASTER, extack);
if (res)
goto err_add_master;
@ -805,8 +841,6 @@ int hsr_dev_finalize(struct net_device *hsr_dev, struct net_device *slave[2],
if (res)
goto err_unregister;
hsr->redbox = true;
ether_addr_copy(hsr->macaddress_redbox, interlink->dev_addr);
mod_timer(&hsr->prune_proxy_timer,
jiffies + msecs_to_jiffies(PRUNE_PROXY_PERIOD));
}

View File

@ -440,12 +440,34 @@ static int hsr_xmit(struct sk_buff *skb, struct hsr_port *port,
return dev_queue_xmit(skb);
}
static bool prp_is_lan_dup(enum hsr_port_type rx, struct hsr_port *port)
{
return (rx == HSR_PT_SLAVE_A && port->type == HSR_PT_SLAVE_B) ||
(rx == HSR_PT_SLAVE_B && port->type == HSR_PT_SLAVE_A);
}
bool prp_drop_frame(struct hsr_frame_info *frame, struct hsr_port *port)
{
return ((frame->port_rcv->type == HSR_PT_SLAVE_A &&
port->type == HSR_PT_SLAVE_B) ||
(frame->port_rcv->type == HSR_PT_SLAVE_B &&
port->type == HSR_PT_SLAVE_A));
enum hsr_port_type rx = frame->port_rcv->type;
/* Supervision frames are not delivered to a SAN on the interlink. */
if (frame->is_supervision && port->type == HSR_PT_INTERLINK)
return true;
if (prp_is_lan_dup(rx, port))
return true;
/* LAN to interlink: keep PRP-network unicast off the SAN segment. */
if ((rx == HSR_PT_SLAVE_A || rx == HSR_PT_SLAVE_B) &&
port->type == HSR_PT_INTERLINK)
return frame->dst_in_node_db;
/* Interlink to LAN: keep SAN-to-SAN unicast local. */
if ((port->type == HSR_PT_SLAVE_A || port->type == HSR_PT_SLAVE_B) &&
rx == HSR_PT_INTERLINK)
return frame->dst_in_proxy_node_db;
return false;
}
bool hsr_drop_frame(struct hsr_frame_info *frame, struct hsr_port *port)
@ -453,7 +475,7 @@ bool hsr_drop_frame(struct hsr_frame_info *frame, struct hsr_port *port)
struct sk_buff *skb;
if (port->dev->features & NETIF_F_HW_HSR_FWD)
return prp_drop_frame(frame, port);
return prp_is_lan_dup(frame->port_rcv->type, port);
/* RedBox specific frames dropping policies
*
@ -466,7 +488,7 @@ bool hsr_drop_frame(struct hsr_frame_info *frame, struct hsr_port *port)
* are addressed to interlink port (and are in the ProxyNodeTable).
*/
skb = frame->skb_hsr;
if (skb && prp_drop_frame(frame, port) &&
if (skb && prp_is_lan_dup(frame->port_rcv->type, port) &&
is_unicast_ether_addr(eth_hdr(skb)->h_dest) &&
hsr_is_node_in_db(&port->hsr->proxy_node_db,
eth_hdr(skb)->h_dest)) {
@ -706,6 +728,18 @@ static int fill_frame_info(struct hsr_frame_info *frame,
frame->is_vlan = false;
proto = ethhdr->h_proto;
/* PRP RedBox only: classify the unicast destination once so the
* per-egress-port decision in prp_drop_frame() stays O(1). HSR RedBox
* does its own classification and must not pay these node-table walks.
*/
if (hsr->prot_version == PRP_V1 && hsr->redbox &&
is_unicast_ether_addr(ethhdr->h_dest)) {
frame->dst_in_node_db =
hsr_is_node_in_db(&hsr->node_db, ethhdr->h_dest);
frame->dst_in_proxy_node_db =
hsr_is_node_in_db(&hsr->proxy_node_db, ethhdr->h_dest);
}
if (proto == htons(ETH_P_8021Q))
frame->is_vlan = true;

View File

@ -199,7 +199,7 @@ static struct hsr_node *hsr_add_node(struct hsr_priv *hsr,
spin_lock_init(&new_node->seq_out_lock);
if (hsr->prot_version == PRP_V1)
new_node->seq_port_cnt = 1;
new_node->seq_port_cnt = hsr->redbox ? 2 : 1;
else
new_node->seq_port_cnt = HSR_PT_PORTS - 1;
@ -293,8 +293,17 @@ struct hsr_node *hsr_get_node(struct hsr_port *port, struct list_head *node_db,
*/
if (ethhdr->h_proto == htons(ETH_P_PRP) ||
ethhdr->h_proto == htons(ETH_P_HSR)) {
/* Check if skb contains hsr_ethhdr */
if (skb->mac_len < sizeof(struct hsr_ethhdr))
bool prp_sup;
/* A PRP supervision frame is an untagged ETH_P_PRP frame
* (mac_len == ETH_HLEN); its RCT is appended only on egress.
* HSR (ETH_P_HSR) supervision is front-tagged and still must
* contain a struct hsr_ethhdr.
*/
prp_sup = hsr->prot_version == PRP_V1 &&
ethhdr->h_proto == htons(ETH_P_PRP) && is_sup;
if (!prp_sup && skb->mac_len < sizeof(struct hsr_ethhdr))
return NULL;
} else {
rct = skb_get_PRP_rct(skb);
@ -381,6 +390,7 @@ void hsr_handle_sup_frame(struct hsr_frame_info *frame)
struct ethhdr *ethhdr;
unsigned int total_pull_size = 0;
unsigned int pull_size = 0;
unsigned int seq_port_cnt;
unsigned long idx;
int i;
@ -474,6 +484,7 @@ void hsr_handle_sup_frame(struct hsr_frame_info *frame)
}
}
seq_port_cnt = min(node_real->seq_port_cnt, node_curr->seq_port_cnt);
xa_for_each(&node_curr->seq_blocks, idx, src_blk) {
if (hsr_seq_block_is_old(src_blk))
continue;
@ -482,7 +493,7 @@ void hsr_handle_sup_frame(struct hsr_frame_info *frame)
if (!merge_blk)
continue;
merge_blk->time = min(merge_blk->time, src_blk->time);
for (i = 0; i < node_real->seq_port_cnt; i++) {
for (i = 0; i < seq_port_cnt; i++) {
bitmap_or(merge_blk->seq_nrs[i], merge_blk->seq_nrs[i],
src_blk->seq_nrs[i], HSR_SEQ_BLOCK_SIZE);
}
@ -649,9 +660,13 @@ int prp_register_frame_out(struct hsr_port *port, struct hsr_frame_info *frame)
if (frame->port_rcv->type == HSR_PT_MASTER)
return 0;
/* for PRP we should only forward frames from the slave ports
* to the master port
/* RedBox: forward LAN frames out the interlink to a SAN, deduping the
* two LAN copies on a dedicated slot.
*/
if (port->type == HSR_PT_INTERLINK)
return hsr_check_duplicate(frame, 1);
/* For PRP only slave-to-master frames are forwarded. */
if (port->type != HSR_PT_MASTER)
return 1;

View File

@ -27,6 +27,8 @@ struct hsr_frame_info {
bool is_local_dest;
bool is_local_exclusive;
bool is_from_san;
bool dst_in_node_db;
bool dst_in_proxy_node_db;
};
void hsr_del_self_node(struct hsr_priv *hsr);

View File

@ -211,7 +211,7 @@ struct hsr_priv {
*/
bool fwd_offloaded; /* Forwarding offloaded to HW */
bool redbox; /* Device supports HSR RedBox */
unsigned char macaddress_redbox[ETH_ALEN];
unsigned char macaddress_redbox[ETH_ALEN] __aligned(sizeof(u16));
unsigned char sup_multicast_addr[ETH_ALEN] __aligned(sizeof(u16));
/* Align to u16 boundary to avoid unaligned access
* in ether_addr_equal

View File

@ -121,14 +121,8 @@ static int hsr_newlink(struct net_device *dev,
}
}
if (proto == HSR_PROTOCOL_PRP) {
if (proto == HSR_PROTOCOL_PRP)
proto_version = PRP_V1;
if (interlink) {
NL_SET_ERR_MSG_MOD(extack,
"Interlink only works with HSR");
return -EINVAL;
}
}
return hsr_dev_finalize(dev, link, interlink, multicast_spec,
proto_version, extack);

View File

@ -4,6 +4,7 @@ top_srcdir = ../../../../..
TEST_PROGS := \
hsr_ping.sh \
hsr_prp_redbox.sh \
hsr_redbox.sh \
link_faults.sh \
prp_ping.sh \

View File

@ -0,0 +1,99 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test a PRP RedBox (PRP-SAN): a SAN that sits behind the interlink port must
# reach, and be reached by, a peer DANP on the PRP network with its own MAC
# preserved on the wire, and the RedBox must announce the SAN with a RedBox-MAC
# TLV (terminated by an EOT marker) in its PRP supervision frames.
#
# RB PRP RedBox: prp0 over rb_a/rb_b (LAN A/B) + interlink rb_il
# PEER peer DANP : prp0 over pe_a/pe_b, 100.64.0.2
# SAN SAN : san_il, own MAC, 100.64.0.51 (behind the interlink)
ipv6=false
source ./hsr_common.sh
check_prerequisites
if ! command -v tcpdump >/dev/null 2>&1; then
echo "SKIP: This test requires tcpdump"
exit $ksft_skip
fi
if ! ip link help hsr 2>&1 | grep -q interlink; then
echo "SKIP: iproute2 too old (no hsr interlink support)"
exit $ksft_skip
fi
setup_ns RB PEER SAN
trap 'cleanup_ns "$RB" "$PEER" "$SAN"' EXIT
ip link add rb_a netns "$RB" type veth peer name pe_a netns "$PEER"
ip link add rb_b netns "$RB" type veth peer name pe_b netns "$PEER"
ip link add rb_il netns "$RB" type veth peer name san_il netns "$SAN"
ip -n "$RB" link set rb_a up
ip -n "$RB" link set rb_b up
ip -n "$RB" link set rb_il up
ip -n "$PEER" link set pe_a up
ip -n "$PEER" link set pe_b up
ip -n "$SAN" link set san_il up
ip -n "$SAN" addr add 100.64.0.51/24 dev san_il
# Feature gate: PRP interlink (RedBox) creation. A kernel without PRP RedBox
# support rejects this with -EINVAL, so SKIP rather than FAIL.
if ! ip -n "$RB" link add name prp0 type hsr slave1 rb_a slave2 rb_b \
interlink rb_il proto 1 2>/dev/null; then
echo "SKIP: kernel without PRP RedBox (interlink) support"
exit $ksft_skip
fi
ip -n "$RB" link set prp0 up
ip -n "$PEER" link add name prp0 type hsr slave1 pe_a slave2 pe_b proto 1
ip -n "$PEER" link set prp0 up
ip -n "$PEER" addr add 100.64.0.2/24 dev prp0
sleep 1
san_mac=$(ip -n "$SAN" -br link show san_il | awk '{print $3}')
rb_mac=$(ip -n "$RB" -br link show rb_il | awk '{print $3}')
# Bidirectional unicast across the interlink.
do_ping "$PEER" 100.64.0.51
do_ping "$SAN" 100.64.0.2
stop_if_error "PRP RedBox bidirectional unicast failed"
# The SAN source MAC must be preserved on the PRP network, not laundered to the
# RedBox MAC: the peer resolves the SAN IP to the SAN's own MAC.
neigh=$(ip -n "$PEER" neigh show 100.64.0.51 | awk '{print $5}')
if [ "$neigh" != "$san_mac" ]; then
echo "SAN MAC preservation [ FAIL ]: peer resolved 100.64.0.51 to" \
"'$neigh', expected $san_mac" 1>&2
ret=1
fi
stop_if_error "SAN MAC not preserved on the PRP network"
# The proxy-announce supervision frame must carry, in order, the life-check TLV
# (type 0x14, len 6) + MacAddressA == SAN MAC + the RedBox-MAC TLV (type 0x1e,
# len 6) + MacAddressRedBox == RedBox MAC + the EOT marker (0x0000).
ip netns exec "$SAN" ping -i 0.2 -q 100.64.0.2 >/dev/null 2>&1 &
ping_pid=$!
cap=$(ip netns exec "$PEER" timeout 5 tcpdump -i pe_a -nn -x \
"ether proto 0x88fb and ether src $rb_mac" 2>/dev/null || true)
kill "$ping_pid" 2>/dev/null || true
wait "$ping_pid" 2>/dev/null || true
san_hex=$(echo "$san_mac" | tr -d ':')
rb_hex=$(echo "$rb_mac" | tr -d ':')
# Reassemble contiguous frame hex: drop the "0x0010:" offset labels and spaces.
frame_hex=$(echo "$cap" | awk '/^[[:space:]]*0x[0-9a-f]+:/ {
sub(/^[[:space:]]*0x[0-9a-f]+:[[:space:]]*/, "");
gsub(/ /, ""); printf "%s", $0 }')
if ! echo "$frame_hex" | grep -q "1406${san_hex}1e06${rb_hex}0000"; then
echo "supervision RedBox-MAC TLV [ FAIL ]: missing SAN MAC, Type-30" \
"payload, or EOT" 1>&2
ret=1
fi
stop_if_error "PRP RedBox supervision RedBox-MAC TLV/EOT check failed"
echo "INFO: PRP RedBox (PRP-SAN) conformance checks passed"
exit $ret