stp: treat a link aggregation group as one entity - #397
Conversation
Spanning tree so far saw only physical ports. Two ports aggregated towards the same neighbour look to the protocol like two parallel paths, so it blocks one of them and cuts the group in half. Give each of the four groups an STP entity of its own, numbered above the physical ports. A member port answers to its group: BPDUs arriving on it are accounted to the entity, the entity's state is written to every member together, and its BPDUs carry the state of a live member. Ports outside any group keep behaving exactly as before. Membership comes from the aggregation tables once a second; when it changed, the entity's state is reapplied to the new set of members. The CLI takes lag <1-4> wherever it takes port <n>, the web UI shows a group as one row, and the saved config keeps the lag form of the commands.
|
Good call to close #325. This looks much better. Still needs a bit of reviewing (saw a couple of hard-coded things like the CPU-port or the number of LAGs, already), I will do that and testing later today. Also need to think about how to test the LAG part... Maybe with 2 RTL switches? One can also set up LAGs in Linux, but that is really cumbersome, Want to make sure all lag members can be blocked. |
|
You don't need a second switch, or Linux, for the blocking part — one switch and one cable does it, and the check you want falls out of a register read. Take two ports with nothing plugged into them and make a static LAG of them. No LACP needed, Then read the MSTP state register, two bits per port from the LSB, Both members are blocked in silicon with only one of them cabled, which is the thing you wanted to be sure of: the decision is taken once for the entity and written to every member, not to the port the BPDU happened to arrive on. I also saved and rebooted with the loop still in place, so it exists from the first second rather than being introduced into an already converged tree. It latches immediately and held over 40 s, register On the hard-coded values, you are right on both. The port count is the bare I would rather not move the code while you are reading it. Say when you are through and I will fold both into named constants, or tell me to keep it inside this file and leave |
|
I tried creating a LAG via the web-interface, on a 4+2 device. I want to put both STP-ports into LAG 1 (I have one of those dirt-cheap Intel 82599ES dual SFP+ PCIe-cards on the Linux-PC side.). However, this does not work, there is no reaction on the web-page and on the console I see: It looks like the web-page sends an incorrectly configured lag command. Indeed, Wireshark says, the command is |
|
|
||
| ## Link aggregation | ||
|
|
||
| A LAG is a port to the protocol, the way the vendor firmware presents it as |
There was a problem hiding this comment.
Suggestion, just write: A LAG (Link Aggregation Group) is handled like an additional port by the STP protocol with its own timers and states. The switch-devices support up to 4 LAGs which are shown alongside physical ports in the STP status.
I.e.: No reference to OEM firmware, which may change the name or have different names on different versions. Also, mixing trunk and LAG is kind of confusing.
| stp lag 1 edge off | ||
| ``` | ||
|
|
||
| Membership comes from the aggregation registers the `lag` command writes, re-read |
There was a problem hiding this comment.
Suggestion: LAGs are configure via the lag command. Once a port is configured as a member of a LAG, it can no longer be configured individually for STP, e.g. via the stp port <n> command. The switch hardware itself does not handle STP state for a LAG as a whole, instead STP-status changes for the members of a LAG must be handled by the firmware by updating all member ports, which is possible using a single register write.
|
|
||
| function members(mask) { | ||
| const a = []; | ||
| for (let i = 0; i < 10; i++) |
|
In the above image, Trk(4, 9) is confusing. These are the logical ports. Needs to be translated to LAG1(5, 6). |
|
Dissolving the lag works nicely, although the lag command documentation is incorrect, |
| p.n = p.lag ? "lag " + p.lag : "port " + p.p; | ||
| p.k = p.lag ? "L" + p.lag : p.p; | ||
| tr.insertCell().textContent = p.lag | ||
| ? "Trk" + p.lag + " (" + members(p.mbr) + ")" |
There was a problem hiding this comment.
LAG or Trunk? Usually a trunk is the central backbone of a network carrying multiple VLANs, and LAG is very specific about combining multiple Links/Ports. I would prefer to stay with LAG, which is also used by Linux and Windows. Also, the menu entry is called LAG, and not "Trunking".
| const sr = stat.insertRow(); | ||
| sr.insertCell().textContent = p.p; | ||
| sr.insertCell().textContent = p.lag | ||
| ? "Trk" + p.lag + " (" + members(p.mbr) + ")" |
| function members(mask) { | ||
| const a = []; | ||
| for (let i = 0; i < 10; i++) | ||
| if (mask & (1 << i)) a.push(i + 1); |
There was a problem hiding this comment.
Here, you need to push machine.log_to_phys_port[i]);
| if (stp_lag_mask[stp_scratch] != stp_scratch16) | ||
| stp_map_dirty = 1; | ||
| stp_lag_mask[stp_scratch] = stp_scratch16; | ||
| for (stp_ss_i = 0; stp_ss_i < 10; stp_ss_i++) |
| stp_st_of = 0; | ||
| while (stp_st_of < 10 && !((stp_lag_mask[stp_i - STP_LAG_BASE] >> stp_st_of) & 1)) | ||
| stp_st_of++; | ||
| if (stp_st_of >= 10) |
| stp_scratch = 3 - (port >> 2); | ||
| sfr_data[stp_scratch] &= ~(uint8_t)(0b11 << ((port << 1) & 0x7)); | ||
| sfr_data[stp_scratch] |= (uint8_t)(state << ((port << 1) & 0x7)); | ||
| for (stp_ss_i = 0; stp_ss_i < 10; stp_ss_i++) { |
| STP_O->rtl_tag.pmask = HTONS(((uint16_t)1) << port); | ||
| if (port >= STP_LAG_BASE) { | ||
| stp_scratch = 0; | ||
| while (stp_scratch < 10 && !stp_ent_has(port, stp_scratch)) |
| stp_scratch = 0; | ||
| while (stp_scratch < 10 && !stp_ent_has(port, stp_scratch)) | ||
| stp_scratch++; | ||
| if (stp_scratch >= 10) { |
| if (!(stp_pflags[stp_i] & STP_PF_ENABLED) || (stp_pflags[stp_i] & STP_PF_ADMEDGE)) { | ||
| port_timers[stp_i] = 0; | ||
| port_hello[stp_i] = (uint16_t)stp_hello_s * STP_HZ; | ||
| if (stp_i < 10 && (stp_i < machine.min_port || stp_i > machine.max_port)) |
| stp_rstp = 1; | ||
| stp_txhold = 6; | ||
| for (stp_i = 0; stp_i < 10; stp_i++) { | ||
| for (stp_i = 0; stp_i < 10; stp_i++) |
I want to test this through with practical examples. It is often surprising what kind of errors and "undocumented hardware features" one finds when actually doing real-life tests. I also do lots of other driver development and skipping this step can become very embarrassing when users start using your code. Just reading the register, we could overlook that other registers are also necessary to be correctly set. Or one would need to first disable a port before taking it out of a LAG or other funny things one would not expect. BTW, the most thorough tester I have ever met is @vDorst ;-) |
The members list on the STP page was built straight from the bit index, which happens to be right on a board where logical and physical numbering differ by one and wrong everywhere else. On a 4+2 device a group of ports 5 and 6 read as "Trk (4, 9)", which is the mask spelled out in logical ports. logToPhysPort is already there and already used by the LAG page, the mirror page and the L2 table, so the fix is to use it here too. The label says LAG rather than Trk while we are in there, since that is what everything else calls it now. Reported by logicog, who found it in about a minute on hardware that does not have the mapping I had been testing against.
|
You are right about the testing, and this is a good demonstration of why. My register read was true and it was true about one board. Fixed in The other three you hit are not from this branch, so I put them in #399 rather than widen this one:
I have not clicked through the GUI half of #399 myself for the same reason as above: on my board the numbering bug is invisible. It builds and |


STP on this tree has no idea link aggregation exists. Every member of a LAG is its own port to it, so it sees several parallel paths to the same neighbour, does exactly what it is meant to do, and blocks all but one of them. A four-link aggregate ends up carrying one link's worth. The LAG side is fine and the STP side is fine; it is the join between them that is missing.
This puts entities above the physical ports, 10 to 13 for the four groups, so an entity is either a port or a LAG and everything downstream stops having to care which. Membership comes from
port_lag_members_get()from #351, re-read once a second, so a group that gains or loses ports is picked up without anything else noticing.stp_ent_apply()is the only place that knows the formula for an entity's state, and it writes that state to every member. Members drop out of the port list instp statusand on the STP page, because they are not entities any more, the group is.With no LAG configured the tree reduces to what is there today on every path. That was the case I most wanted not to break.
Tested on a SWTGW218AS: a static LAG of two ports, then a cable from one member back to an ordinary port on the same switch, so there is a loop through the aggregate. The LAG goes to blocking and the ordinary port stays forwarding, which is the right way round, the aggregate being entity 12 and losing the tie against port 3. The part I wanted to see was the ASIC rather than the status page:
regget 5310came back0x000ff577, and both members are blocked in silicon, not only the one with the cable in it. Traffic on the looped pair stayed at BPDU rate throughout.Saved and rebooted with the loop still in place. It latches from the first second and held over 40 s, register
0x000ff5f7.This is meant to replace #325, which carries the same idea but as 61 commits from before the trunk/lag rename, 45 of which have since landed here by other routes. Happy to close that one in favour of this, or the other way round if you prefer.