Files
RTLPlayground/doc/link_aggregation.md
T
DrDoof c3b4cc893b lacp: implement IEEE 802.3ad link aggregation
Dynamic link aggregation on top of the static LAGG, for RTL8372/8373:
receive, selection and mux state machines (coupled mux, one aggregator
per group), with "lacp show" to look inside. Each of the four trunk
groups runs its own aggregator on the candidate ports assigned with
"lag <n> lacp <ports>"; the engine starts with the first such group and
stops with the last.

LACPDUs reach the CPU through the reserved-multicast trap, so delivery
needs no L2 entry and nothing written per VLAN or per PVID. The trap
acts on the address before the VLAN lookup and before the ingress
filters, so a LACPDU also arrives on a port that admits tagged frames
only and on a port STP holds in blocking, neither of which a forwarded
one reaches. A trapped frame carries no VLAN tag,
so lacp_in() takes the offset of the PDU body from TRAP_RX_BODY(). The
action is set with trap_rma_set(), which leaves the flags sharing that
register alone, and "lacp off" puts the slow-protocols drop default
back. doc/lacp.md and doc/link_aggregation.md record what was measured,
including why the trap looked like a dead end at first.

The web UI gets a Static/LACP mode per group on the LAG page with live
status, and the startup-config validator knows the per-group syntax. A
host-side sandbox for the state machines runs in the test harness
together with the LAG hash check. It drives both forms of an incoming
LACPDU, so the body offset of a trapped one is covered as well.

The L2 table page iterated with the "next unicast" read method, so
static multicast entries such as an IGMP group were invisible. It now
iterates with "next address", skips L3 entries (their layout carries IP
addresses where a MAC is expected), and reports the lowest member of a
multicast entry's port mask, so such an entry shows up as a static
entry on that port.

A port taken out of LACP forgets its partner, so "lacp show" after
"lacp off" no longer reports one. Applying the same candidate set to a
group again, as the LAG page does on every Apply, leaves the trunk
alone instead of tearing it down for a second or two. lacp_in() checks
the frame length and ethertype before reading a LACPDU, so a runt or a
foreign frame on the slow-protocols address cannot leave stale buffer
bytes as a partner. The sandbox covers these three in T20-T22. On the
LAG page, LACP mode with no ports checked sends "lag N lacp off", and
the startup-config validator wants at least one port after "lacp".
2026-10-12 15:09:57 +02:00

279 lines
14 KiB
Markdown

# Link Aggregation (aka Trunking)
The RTL827x allows to combine multiple ports to a single logical link
(Link Aggregation / Trunking) according to IEEE 802.3ad. LAGs allow to
combine the individual physical links into a single link with the combined
throughput and automatic redundancy when one of the link fails.
Up to 4 Link Aggregation Groups (LAGs) can be defined on the switch devices.
## LAG control
Four registers `RTL837X_TRK_MBR_CTRL_BASE(lag) (0x4f38-0x4f44)` define the LAG membership
via a port mask of the logical port numbers.
A hash algorithm applied to L2, L3 and L4 properties of a packet are used to decide which
of the links (ports) is being used to transfer the packet. The possible properties used in the
hash are:
```
#define LAG_HASH_SOURCE_PORT_NUMBER 0x01
#define LAG_HASH_L2_SMAC 0x02
#define LAG_HASH_L2_DMAC 0x04
#define LAG_HASH_L3_SIP 0x08
#define LAG_HASH_L3_DIP 0x10
#define LAG_HASH_L4_SPORT 0x20
#define LAG_HASH_L4_DPORT 0x40
#define LAG_HASH_DEFAULT (LAG_HASH_L2_SMAC | LAG_HASH_L2_DMAC | LAG_HASH_L3_SIP | LAG_HASH_L3_DIP | LAG_HASH_L4_SPORT | LAG_HASH_L4_DPORT)
```
The hash algorithm used to select links (exit ports) is defined for each LAG individually in
`RTL837X_TRK_HASH_CTRL_BASE (0x4f48-0x4f54)`.
## Trunking API
The code currently provides the following functions:
```
/*
* Configure LAGs
* Sets the members via port bitmask of a given Link Aggregation Group
* The groups have numbers 0-3
* The bitmask represents up to 10 ports
* If currently no LAG has algorithm used, a default is applied
*/
void port_lag_members_set(__xdata uint8_t lag, __xdata uint16_t members) __banked;
/*
* Configures the hash algorithm used for a LAG
* lag is the Group to configure and hash is a bitmask
*/
void port_lag_hash_set(__xdata uint8_t lag, __xdata uint8_t hash_bits) __banked;
```
## LAG configuration on the Serial Console
For testing the following commands are provided on the serial console:
```
> lag <LAG-ID> <p1> [p2]...
Create or set a LAG. LAG-ID is 1 to 4. Ports are physical ports.
> lag <LAG-ID> d
Delete the LAG.
> lag show
Shows information on all 4 lags
> laghash 0 [hash1] [hash2]...
Uses the given packet properties when hashing the packet to select the link
Names for the hashes are spa, smac, dmac, sip, dip, sport, dport
```
When a lag is creates, by default the hash is based on smac, dmac, sip, dip, sport, dport. When you
use your own hash settings, make sure that the hash always uses both the source and destination
property of the packet, as otherwise pakets will not be routed symmetrically.
## LAG configuration via the Web Interface
In the web-interface select Link Aggregation in the left navigation panel. The page will look like this:
![Alt text](images/LAG_config.png?raw=true "Link Aggregation Web-Page")
Each of th 4 LAGs is configured separately. After the web-page has loaded, the current configuration
can be edited by clicking on the port-images to include that port or exclude it from a LAG.
When pressing on the Create/Update button, the LAG will be automatically created if not yet done, or
updated. If a lage is updated to not having any members, then it is effectively deleted.
All LAGs are created with the default hash-function (see above). This currently cannot be changed
from the Web.
## A Test using a single Linux Desktop
The following is a simple test using 2 RTL 2.5 GBit switches with at least 1 SFP+-port each. You
will also need 4 10GBit SFP+ modules (DAC or Fiber) and 2 SFP+ ports on your desktop.
The following shows the network configuration
```
----------------- -----------------
Linux Comuter | | 2.5 GBit | | same Linux Computer
---------- 10G | P1 |------------| P1 | 10G ----------
192.168.9.1 | SFP+ |==========| Switch 1 | 2.5 GBit | Switch 2 |==========| SFP+ | 192.168.9.2
enp1s0f0 ---------- | P2 |------------| P2 | ---------- enp1s0f1
| | | |
------------------ -----------------
```
On _both_ switches create a LAG with ports 1 and 2 inside and the default hash algorithm which takes
source and destination ports into account, e.g. just use the default:
```
> lag 1 1 2
```
The following shows the configuration on the desktop using a dual 10GBit card with 2 SFP+ modules:
```
[234690.755634] ixgbe: Intel(R) 10 Gigabit PCI Express Network Driver
[234690.755637] ixgbe: Copyright (c) 1999-2016 Intel Corporation.
[234690.921614] ixgbe 0000:01:00.0: Multiqueue Enabled: Rx Queue count = 12, Tx Queue count = 12 XDP Queue count = 0
[234690.921914] ixgbe 0000:01:00.0: 32.000 Gb/s available PCIe bandwidth (5.0 GT/s PCIe x8 link)
[234690.921999] ixgbe 0000:01:00.0: MAC: 2, PHY: 19, SFP+: 5, PBA No: FFFFFF-0FF
[234690.922002] ixgbe 0000:01:00.0: 28:41:c6:xx:xx:aa
[234690.924946] ixgbe 0000:01:00.0: Intel(R) 10 Gigabit Network Connection
[234690.990024] ixgbe 0000:01:00.0 enp1s0f0: renamed from eth0
[234691.056447] ixgbe 0000:01:00.0: registered PHC device on enp1s0f0
[234691.089417] ixgbe 0000:01:00.1: Multiqueue Enabled: Rx Queue count = 12, Tx Queue count = 12 XDP Queue count = 0
[234691.089706] ixgbe 0000:01:00.1: 32.000 Gb/s available PCIe bandwidth (5.0 GT/s PCIe x8 link)
[234691.089788] ixgbe 0000:01:00.1: MAC: 2, PHY: 19, SFP+: 18, PBA No: FFFFFF-0FF
[234691.089790] ixgbe 0000:01:00.1: 28:41:c6:xx:xx:ab
[234691.160997] ixgbe 0000:01:00.1: Intel(R) 10 Gigabit Network Connection
[234691.166102] ixgbe 0000:01:00.1 enp1s0f1: renamed from eth0
[234691.231579] ixgbe 0000:01:00.1: registered PHC device on enp1s0f1
[234691.236965] ixgbe 0000:01:00.0 enp1s0f0: detected SFP+: 5
[234691.485031] ixgbe 0000:01:00.0 enp1s0f0: NIC Link is Up 10 Gbps, Flow Control: RX/TX
[234691.557003] ixgbe 0000:01:00.1 enp1s0f1: detected SFP+: 18
[234691.753061] ixgbe 0000:01:00.1 enp1s0f1: NIC Link is Up 10 Gbps, Flow Control: RX/TX
```
Now set up 2 network namespaces and put each interface inside one:
```
sudo ip netns add netns_eth0
sudo ip netns add netns_eth1
sudo ip link set enp1s0f0 netns netns_eth0
sudo ip link set enp1s0f1 netns netns_eth1
```
Configure network interface addresses 192.168.9.2 and 192.168.9.1 in each namespace:
```
sudo ip netns exec netns_eth0 ifconfig enp1s0f0 192.168.9.1 netmask 255.255.255.0
sudo ip netns exec netns_eth0 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
24: enp1s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 28:41:c6:xx:xx:aa brd ff:ff:ff:ff:ff:ff
altname enx2841c6xxxxaa
inet 192.168.9.1/24 scope global enp1s0f0
valid_lft forever preferred_lft forever
inet6 fe80::2a41:c6ff:fexx:xxaa/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever
sudo ip netns exec netns_eth1 ifconfig enp1s0f1 192.168.9.2 netmask 255.255.255.0
sudo ip netns exec netns_eth1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
25: enp1s0f1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 28:41:c6:xx:xx:ab brd ff:ff:ff:ff:ff:ff
altname enx2841c6xxxxab
inet 192.168.9.2/24 scope global enp1s0f1
valid_lft forever preferred_lft forever
inet6 fe80::2a41:c6ff:fexx:xxab/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever
```
Test is using ping. On both switches one of the 2.5Gbit links and all 10GBit links should show activity:
```
$ sudo ip netns exec netns_eth1 ping 192.168.9.1
PING 192.168.9.1 (192.168.9.1) 56(84) bytes of data.
64 bytes from 192.168.9.1: icmp_seq=1 ttl=64 time=0.082 ms
64 bytes from 192.168.9.1: icmp_seq=2 ttl=64 time=0.130 ms
^C
--- 192.168.9.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1030ms
rtt min/avg/max/mdev = 0.082/0.106/0.130/0.024 ms
```
You can also verify that the redundancy works by unplugging the active link, the ping should continue
undisturbed with the other link now tranporting the pakets.
In 2 shells, start 2 instances of iperf, listening on 2 different ports. You will need to make sure that
the hash algorithm assigns different switch ports for the different port numbers. You can check this by
running the iperf3 client against each server instance and verify that different links show activity:
```
sudo ip netns exec netns_eth0 iperf3 -s
sudo ip netns exec netns_eth0 iperf3 -s -p 5333
```
Now you can run the clients in parallel:
```
$ sudo ip netns exec netns_eth1 iperf3 -c 192.168.9.1 & sudo ip netns exec netns_eth1 iperf3 -p 5333 -c 192.168.9.1
[1] 295484
Connecting to host 192.168.9.1, port 5201
[ 5] local 192.168.9.2 port 60996 connected to 192.168.9.1 port 5201
Connecting to host 192.168.9.1, port 5333
[ 5] local 192.168.9.2 port 39660 connected to 192.168.9.1 port 5333
[ ID] Interval Transfer Bitrate Retr Cwnd
[ 5] 0.00-1.00 sec 283 MBytes 2.37 Gbits/sec 485 272 KBytes
[ ID] Interval Transfer Bitrate Retr Cwnd
[ 5] 0.00-1.00 sec 283 MBytes 2.37 Gbits/sec 479 379 KBytes
[ 5] 1.00-2.00 sec 280 MBytes 2.35 Gbits/sec 444 260 KBytes
[ 5] 1.00-2.00 sec 280 MBytes 2.35 Gbits/sec 578 267 KBytes
[ 5] 2.00-3.00 sec 281 MBytes 2.36 Gbits/sec 385 263 KBytes
[ 5] 2.00-3.00 sec 280 MBytes 2.35 Gbits/sec 373 375 KBytes
[ 5] 3.00-4.00 sec 280 MBytes 2.35 Gbits/sec 430 385 KBytes
[ 5] 3.00-4.00 sec 280 MBytes 2.35 Gbits/sec 452 273 KBytes
[ 5] 4.00-5.00 sec 281 MBytes 2.36 Gbits/sec 319 256 KBytes
[ 5] 4.00-5.00 sec 281 MBytes 2.36 Gbits/sec 425 269 KBytes
[ 5] 5.00-6.00 sec 280 MBytes 2.35 Gbits/sec 364 264 KBytes
[ 5] 5.00-6.00 sec 281 MBytes 2.36 Gbits/sec 561 264 KBytes
[ 5] 6.00-7.00 sec 281 MBytes 2.35 Gbits/sec 446 255 KBytes
[ 5] 6.00-7.00 sec 280 MBytes 2.35 Gbits/sec 582 263 KBytes
[ 5] 7.00-8.00 sec 281 MBytes 2.35 Gbits/sec 494 263 KBytes
[ 5] 7.00-8.00 sec 280 MBytes 2.35 Gbits/sec 539 181 KBytes
[ 5] 8.00-9.00 sec 281 MBytes 2.36 Gbits/sec 617 389 KBytes
[ 5] 8.00-9.00 sec 280 MBytes 2.35 Gbits/sec 490 232 KBytes
[ 5] 9.00-10.00 sec 281 MBytes 2.35 Gbits/sec 363 215 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 2.74 GBytes 2.36 Gbits/sec 4347 sender
[ 5] 0.00-10.00 sec 2.74 GBytes 2.35 Gbits/sec receiver
iperf Done.
[ 5] 9.00-10.00 sec 282 MBytes 2.36 Gbits/sec 536 380 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 2.74 GBytes 2.36 Gbits/sec 5015 sender
[ 5] 0.00-10.00 sec 2.74 GBytes 2.35 Gbits/sec receiver
iperf Done.
[1]+ Done sudo ip netns exec netns_eth1 iperf3 -c 192.168.9.1
```
As you can see, the total throughput was 4.71 GBit/sec which is close to the
maximum possible with a single 5GBit link.
## LACP: how the PDUs reach the CPU
Slow-protocol frames go to the reserved group `01:80:C2:00:00:02`, and the
ASIC's default action for it is *drop*. The engine sets the action to *trap*
while it runs. A trap hands the frame to the port named in `EXT_CPU_CTRL`,
which names no port out of reset - that is why the trap first looked like no
help at all: the receive counters stayed frozen while the partner kept sending,
and the frames were counted as ingress discards. `trap_init()` points the
register at the CPU port once at startup.
The *forward* action would reach the CPU as well, since the CPU port is in the
forwarding domain, but it leaves the frame in the L2 lookup and so floods it
across the ingress VLAN. That leaks LACPDUs to unrelated ports, and the damage
is real: a bond's own ports see each other's PDUs and Linux reports an illegal
loopback, a second bond on the same switch is poisoned, and the flood escapes
through uplinks. Containing it would take a static L2 multicast entry with a
CPU-only mask for every PVID in use, since untagged LACPDUs classify into the
ingress port's PVID and the forward action is not limited to the LACP ports.
The trap keys on the address instead, before the VLAN lookup and before the
ingress filters, which settles three things at once:
* Nothing is written per VLAN or per PVID, so delivery no longer depends on
which PVIDs the ports happen to carry.
* A port that admits tagged frames only still receives: an untagged forwarded
LACPDU is dropped by the acceptable-frame-type filter, a trapped one is not.
* A port STP holds in blocking still receives, so a group converges without
waiting out the forward delay.
A trapped frame arrives without the VLAN tag the hardware inserts into a
forwarded one, so the PDU body sits four bytes earlier; `TRAP_RX_BODY()` gives
the offset for both forms.
`lacp off` puts the action back to *drop*, which acts before the L2 lookup.
## LACP: what the implementation leaves out
The four state machines of Clause 43 are present in simplified form. Mux control
is coupled rather than independent, one partner system is elected per LAG, and
there are no churn detection machines.
One interoperability rule is worth stating because getting it wrong is silent.
The Partner block of an outgoing PDU has to echo the peer's own actor identity
verbatim, meaning system and priority, key, port and port priority, and the
aggregation flag. A Linux partner's `__record_pdu()` accepts our SYNC bit only
when that block mirrors what it sent. Hardcoding the priorities to zero
mismatches Linux's default of 0xffff, which clears partner SYNC, moves the state
byte from 0x3f to 0x37, and the bond never reaches collecting and distributing.