Files
DrDoof bdca888ecd stp: use the front panel number as the port ID
The port ID in the BPDUs was the chip port plus one, so on most boards
it did not match the port label, neither on the Spanning Tree page nor
on the neighbour. Send the front panel number instead, and 101 and up
for the LAGs, the same numbers the page already uses for the Port
column. A returned BPDU of our own is mapped back to the port by that
number.

Scenario 66 gives one port a front panel number that differs from its
chip port.
2026-10-28 16:05:12 +01:00

2382 lines
64 KiB
C

#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stddef.h>
#include "rtl837x_common.h"
#include "machine.h"
#include "uip.h"
#include "rtl837x_regs.h"
#include "rtl837x_stp.h"
#define NPORTS 9 /* indices 0..8, front-panel 1..9 */
uint8_t uip_buf[UIP_CONF_BUFFER_ALLOC_SIZE];
uint16_t uip_len;
struct uip_eth_addr uip_ethaddr = { .addr = { 0x1c, 0x2a, 0xa3, 0x1a, 0x72, 0x4e } };
const struct machine machine = {
.log_to_phys_port = { 1, 2, 3, 4, 5, 6, 10, 8, 9 }
};
struct machine_runtime machine_detected = { .isRTL8373 = 1 };
uint8_t sfr_data[4];
bool stp_enabled;
uint16_t management_vlan;
uint8_t cmd_buffer[CMD_BUF_SIZE];
uint8_t cmd_words_len;
uint8_t cmd_words_b[15];
char save_cmd;
uint8_t err_status;
uint8_t atoi_results_u8;
static int verbose;
void print_string(const char *s)
{
if (verbose)
fputs(s, stdout);
}
void print_byte(uint8_t b)
{
if (verbose)
printf("%02x", b);
}
void print_short(uint16_t v)
{
if (verbose)
printf("%04x", v);
}
void print_long(uint32_t v)
{
if (verbose)
printf("%08x", v);
}
void write_char(char c)
{
if (verbose)
putchar(c);
}
void itoa(uint8_t v)
{
if (verbose)
printf("%u", v);
}
void print_reg(uint16_t reg)
{
(void)reg;
}
uint8_t cmd_compare(uint8_t start, const uint8_t *cmd)
{
const char *w;
size_t n;
if (start >= cmd_words_len)
return 0;
w = (const char *)&cmd_buffer[cmd_words_b[start]];
n = strlen((const char *)cmd);
return strncmp(w, (const char *)cmd, n) == 0 && (w[n] == ' ' || w[n] == 0);
}
uint8_t atoi_byte(uint8_t idx)
{
unsigned v = 0, n = 0;
while (cmd_buffer[idx + n] >= '0' && cmd_buffer[idx + n] <= '9')
v = v * 10 + (cmd_buffer[idx + n++] - '0');
atoi_results_u8 = (uint8_t)v;
return (uint8_t)n;
}
uint8_t cmd_parse_port_separator(uint8_t idx)
{
if (!atoi_byte(idx) || atoi_results_u8 < 1 || atoi_results_u8 > NPORTS)
return 0;
atoi_results_u8--;
return 1;
}
static void sim_cmd(const char *line)
{
memset(cmd_buffer, 0, sizeof(cmd_buffer));
strncpy((char *)cmd_buffer, line, sizeof(cmd_buffer) - 1);
cmd_words_len = 0;
for (uint8_t i = 0; cmd_buffer[i]; i++)
if (cmd_buffer[i] != ' ' && (i == 0 || cmd_buffer[i - 1] == ' '))
cmd_words_b[cmd_words_len++] = i;
err_status = ERR_OK;
stp_parse();
}
void execute_config(void)
{
}
int8_t vlan_get(uint16_t vlan)
{
(void)vlan;
return -1;
}
uint16_t port_pvid_get(uint8_t port)
{
(void)port;
return 1;
}
static int l2mc_calls;
static uint16_t l2mc_pmask;
void port_l2mc_set(uint8_t mac_last, uint16_t vid, uint16_t pmask)
{
(void)mac_last;
(void)vid;
l2mc_calls++;
l2mc_pmask = pmask;
}
void trap_rma_set(uint16_t reg, uint8_t act)
{
(void)reg;
(void)act;
}
uint8_t port_ingress_filter_get(uint8_t port)
{
(void)port;
return 0;
}
static int flush_count[NPORTS];
void port_l2_forget_port(uint8_t port)
{
if (port < NPORTS)
flush_count[port]++;
}
static uint8_t mstp_reg[4];
static uint16_t sim_lag[4];
uint16_t port_lag_members_get(uint8_t lag)
{
return sim_lag[lag];
}
static uint16_t sim_links;
static uint8_t sim_speed[NPORTS]; /* nibble as the ASIC reports it: 2 = 1G, 4 = 10G */
static int tx_frames[NPORTS];
struct sim_pkt_out {
uint8_t stp_addr[6];
uint8_t src_addr[6];
struct rtl_tag rtl_tag;
uint16_t msg_len;
uint8_t dsap;
uint8_t ssap;
uint8_t ctrl;
uint16_t proto;
uint8_t version;
uint8_t bpdu_type;
uint8_t flags;
struct bridge root;
uint32_t root_path_cost;
struct bridge bridge;
uint8_t port_prio;
uint8_t port_id;
uint16_t age;
uint16_t age_max;
uint16_t hello;
uint16_t fwd_delay;
uint8_t version1_length;
};
/* SDCC uses byte alignment; both firmware and fixtures must match wire
* offsets rather than sharing the host compiler's padding. */
_Static_assert(sizeof(struct sim_pkt_out) == 61, "RST BPDU with CPU tag must be 61 bytes");
_Static_assert(offsetof(struct sim_pkt_out, root) == 30, "root identifier wire offset");
static struct sim_pkt_out last_tx[NPORTS];
static int tc_frames[NPORTS];
static int tcn_frames[NPORTS];
void reg_read_m(uint16_t addr)
{
memset(sfr_data, 0, 4);
if (addr == RTL837X_MSTP_STATES)
memcpy(sfr_data, mstp_reg, 4);
else if (addr == RTL837X_REG_LINKS_STS) {
sfr_data[1] = (uint8_t)sim_links;
sfr_data[2] = (uint8_t)(sim_links >> 8);
} else if (addr == RTL837X_REG_LINKS || addr == RTL837X_REG_LINKS_89) {
uint8_t base = (addr == RTL837X_REG_LINKS) ? 0 : 8;
for (uint8_t k = 0; k < 8 && base + k < NPORTS; k++)
sfr_data[3 - (k >> 1)] |= (k & 1) ? (uint8_t)(sim_speed[base + k] << 4) : sim_speed[base + k];
}
}
void reg_write_m(uint16_t addr)
{
if (addr == RTL837X_MSTP_STATES) {
memcpy(mstp_reg, sfr_data, 4);
if (verbose > 1)
printf(" [mstp <- %02x %02x %02x %02x]\n", mstp_reg[0], mstp_reg[1], mstp_reg[2],
mstp_reg[3]);
}
}
void tcpip_output(void)
{
struct sim_pkt_out *f = (struct sim_pkt_out *)&uip_buf[RTL_FRAME_DESC_SIZE];
uint16_t pmask = HTONS(f->rtl_tag.pmask);
for (uint8_t p = 0; p < NPORTS; p++)
if (pmask & (1 << p)) {
tx_frames[p]++;
if (f->bpdu_type == 0x80)
tcn_frames[p]++;
else if (f->flags & 0x01)
tc_frames[p]++;
memcpy(&last_tx[p], f, sizeof(*f));
}
uip_len = 0;
}
static uint8_t port_state(uint8_t port)
{
return (mstp_reg[3 - (port >> 2)] >> ((port << 1) & 0x7)) & 0x3;
}
static const char *state_name(uint8_t s)
{
static const char *n[] = { "off", "discarding", "learning", "forwarding" };
return n[s & 3];
}
static void links_set(uint16_t mask)
{
sim_links = mask;
}
static void tick(unsigned n)
{
while (n--)
stp_timers();
}
static void secs(unsigned s)
{
tick(s * STP_HZ);
}
struct sim_bpdu {
uint8_t port; /* ingress port index */
uint8_t root_prio; /* high byte of the root priority */
uint8_t root_mac[6];
uint32_t root_cost; /* root path cost as advertised */
uint8_t br_prio; /* sender's own bridge priority high byte */
uint8_t br_mac[6];
uint8_t port_id; /* sender's port number, 1-based */
uint8_t port_prio; /* 0 = 0x80 */
uint8_t flags; /* 0 = designated, learning + forwarding */
uint8_t age; /* message age, seconds */
uint8_t maxage; /* 0 = 20 s, and hello/fwd default with it */
uint8_t hello;
uint8_t fwd;
uint8_t legacy; /* 1 = 802.1D Config BPDU, 2 = TCN */
uint8_t msg_len; /* 0 = the right length for the type */
};
struct sim_pkt_in {
uint8_t stp_addr[6];
uint8_t src_addr[6];
struct rtl_tag rtl_tag;
struct vlan_tag vlan_tag;
uint16_t msg_len;
uint8_t dsap;
uint8_t ssap;
uint8_t ctrl;
uint16_t proto;
uint8_t version;
uint8_t bpdu_type;
uint8_t flags;
struct bridge root;
uint32_t root_path_cost;
struct bridge bridge;
uint8_t port_prio;
uint8_t port_id;
uint16_t age;
uint16_t age_max;
uint16_t hello;
uint16_t fwd_delay;
uint8_t version1_length;
};
_Static_assert(sizeof(struct sim_pkt_in) == 65, "VLAN-tagged RX BPDU must be 65 bytes");
static uint32_t swap32(uint32_t v)
{
return (v << 24) | ((v & 0xff00) << 8) | ((v >> 8) & 0xff00) | (v >> 24);
}
static void bpdu_in(const struct sim_bpdu *b)
{
struct sim_pkt_in *f = (struct sim_pkt_in *)uip_buf;
memset(uip_buf, 0, sizeof(uip_buf));
f->vlan_tag.svlan = HTONS(0x8100);
memcpy(f->stp_addr, "\x01\x80\xc2\x00\x00\x00", 6);
memcpy(f->src_addr, b->br_mac, 6);
f->rtl_tag.pmask = HTONS(b->port);
f->msg_len = HTONS(b->msg_len ? b->msg_len : (b->legacy == 2 ? 7 : (b->legacy ? 38 : 39)));
f->dsap = 0x42;
f->ssap = 0x42;
f->ctrl = 0x03;
f->proto = 0;
f->version = b->legacy ? 0x00 : 0x02;
f->bpdu_type = b->legacy == 2 ? 0x80 : (b->legacy ? 0x00 : 0x02);
f->flags = b->flags ? b->flags : (b->legacy ? 0x00 : 0x3c);
f->root.prio = b->root_prio;
f->root.ext = 0;
memcpy(f->root.mac, b->root_mac, 6);
f->root_path_cost = swap32(b->root_cost);
f->bridge.prio = b->br_prio;
f->bridge.ext = 0;
memcpy(f->bridge.mac, b->br_mac, 6);
f->port_prio = b->port_prio ? b->port_prio : 0x80;
f->port_id = b->port_id;
f->age = b->age;
f->age_max = b->maxage ? b->maxage : 20;
f->hello = b->maxage ? b->hello : 2;
f->fwd_delay = b->maxage ? b->fwd : 15;
uip_len = sizeof(struct sim_pkt_in);
stp_in();
}
static int failures;
static void check(int cond, const char *what)
{
if (!cond) {
printf(" FAIL: %s\n", what);
failures++;
} else if (verbose)
printf(" ok: %s\n", what);
}
static void check_state(uint8_t port, uint8_t want, const char *what)
{
uint8_t got = port_state(port);
if (got != want) {
printf(" FAIL: %s: port %u is %s, expected %s\n", what, port + 1, state_name(got), state_name(want));
failures++;
} else if (verbose)
printf(" ok: %s (port %u %s)\n", what, port + 1, state_name(got));
}
static void reset_all(void)
{
memset(mstp_reg, 0, sizeof(mstp_reg));
memset(sim_lag, 0, sizeof(sim_lag));
memset(flush_count, 0, sizeof(flush_count));
memset(tx_frames, 0, sizeof(tx_frames));
memset(last_tx, 0, sizeof(last_tx));
memset(tc_frames, 0, sizeof(tc_frames));
memset(tcn_frames, 0, sizeof(tcn_frames));
for (uint8_t p = 0; p < NPORTS; p++)
sim_speed[p] = 2;
stp_enabled = 1;
stp_defaults();
links_set(0);
stp_setup();
tick(2 * STP_HZ);
}
static const uint8_t ROOT_MAC[6] = { 0x1c, 0x2a, 0xa3, 0x1e, 0xc2, 0x03 };
static const uint8_t PEER_MAC[6] = { 0x00, 0x82, 0x44, 0x2a, 0x74, 0x82 };
static void scen_edge_ports(void)
{
printf("1. a port nobody talks STP on becomes an edge port\n");
reset_all();
links_set(1 << 3);
secs(1);
check_state(3, 1, "port with fresh carrier starts discarding");
secs(4);
check_state(3, 3, "silent port goes forwarding as an edge port");
}
static void scen_root_port(void)
{
struct sim_bpdu root_bpdu = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 };
printf("2. the bridge follows the root heard on port 9\n");
reset_all();
links_set(1 << 8);
secs(1);
memcpy(root_bpdu.root_mac, ROOT_MAC, 6);
memcpy(root_bpdu.br_mac, ROOT_MAC, 6);
bpdu_in(&root_bpdu);
check(stp_root_port == 8, "port 9 becomes the root port");
secs(20);
bpdu_in(&root_bpdu);
check_state(8, 3, "root port forwards after the listen period");
}
static void ring(void)
{
struct sim_bpdu from_root = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 };
struct sim_bpdu from_peer = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 };
reset_all();
links_set((1 << 0) | (1 << 8));
secs(1);
memcpy(from_root.root_mac, ROOT_MAC, 6);
memcpy(from_root.br_mac, ROOT_MAC, 6);
memcpy(from_peer.root_mac, ROOT_MAC, 6);
memcpy(from_peer.br_mac, PEER_MAC, 6);
for (int i = 0; i < 12; i++) {
bpdu_in(&from_root);
bpdu_in(&from_peer);
secs(2);
}
check(stp_root_port == 8, "the direct uplink stays the root port");
check_state(8, 3, "root port forwards");
check_state(0, 1, "port 1 is discarding (alternate)");
}
static void scen_ring(void)
{
printf("3. a neighbour with a cheaper path to the root must block us\n");
ring();
}
static void scen_ring_clears(void)
{
struct sim_bpdu from_root = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 };
printf("4. when the better neighbour goes quiet the port comes back\n");
ring();
memcpy(from_root.root_mac, ROOT_MAC, 6);
memcpy(from_root.br_mac, ROOT_MAC, 6);
bpdu_in(&from_root);
secs(2);
bpdu_in(&from_root);
secs(2);
check_state(0, 1, "port 1 still discards within three of the peer's hellos");
for (int i = 0; i < 18; i++) {
bpdu_in(&from_root);
secs(2);
}
check_state(0, 3, "port 1 forwards again once the peer information aged out");
}
static void scen_cheaper_path(void)
{
struct sim_bpdu from_root_p9 = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 };
struct sim_bpdu from_root_p1;
printf("5. the cheaper of two paths to the root becomes the root port\n");
reset_all();
stp_pcost[0] = 2000;
links_set((1 << 0) | (1 << 8));
secs(1);
memcpy(from_root_p9.root_mac, ROOT_MAC, 6);
memcpy(from_root_p9.br_mac, ROOT_MAC, 6);
from_root_p1 = from_root_p9;
from_root_p1.port = 0;
from_root_p1.port_id = 7;
for (int i = 0; i < 12; i++) {
bpdu_in(&from_root_p9);
bpdu_in(&from_root_p1);
secs(2);
}
check(stp_root_port == 0, "port 1 wins on cost and becomes the root port");
check(root_bridge_cost == 2000, "the root path cost follows that port");
check_state(0, 3, "the root port forwards");
check_state(8, 1, "the dearer path to the same root is discarding");
stp_pcost[0] = 0;
}
static void scen_we_are_better(void)
{
struct sim_bpdu from_root = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 };
struct sim_bpdu from_peer = {
.port = 0, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 1, .flags = 0x0c
};
printf("6. a neighbour with a worse path does not block us\n");
reset_all();
links_set((1 << 0) | (1 << 8));
secs(1);
memcpy(from_root.root_mac, ROOT_MAC, 6);
memcpy(from_root.br_mac, ROOT_MAC, 6);
memcpy(from_peer.root_mac, ROOT_MAC, 6);
memcpy(from_peer.br_mac, PEER_MAC, 6);
for (int i = 0; i < 18; i++) {
bpdu_in(&from_root);
bpdu_in(&from_peer);
secs(2);
}
check(stp_root_port == 8, "the uplink stays the root port");
check_state(0, 3, "port 1 keeps forwarding");
}
static void scen_alt_survives_link_bounce(void)
{
struct sim_bpdu from_root = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 };
struct sim_bpdu from_peer = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 };
printf("7. a blocked port stays blocked when the carrier returns\n");
ring();
memcpy(from_root.root_mac, ROOT_MAC, 6);
memcpy(from_root.br_mac, ROOT_MAC, 6);
memcpy(from_peer.root_mac, ROOT_MAC, 6);
memcpy(from_peer.br_mac, PEER_MAC, 6);
links_set(1 << 8);
secs(2);
links_set((1 << 0) | (1 << 8));
bpdu_in(&from_peer);
for (int i = 0; i < 15; i++) {
bpdu_in(&from_root);
bpdu_in(&from_peer);
secs(2);
}
check_state(0, 1, "port 1 never reaches forwarding while the peer is better");
check(stp_root_port == 8, "the uplink is still the root port");
}
static void scen_speed_cost(void)
{
struct sim_bpdu from_root = { .port = 8, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 };
struct sim_bpdu slow;
printf("8. the faster of two links to one root wins on cost\n");
reset_all();
sim_speed[8] = 4;
sim_speed[0] = 2;
links_set((1 << 0) | (1 << 8));
secs(1);
memcpy(from_root.root_mac, ROOT_MAC, 6);
memcpy(from_root.br_mac, ROOT_MAC, 6);
slow = from_root;
slow.port = 0;
slow.port_id = 7;
for (int i = 0; i < 12; i++) {
bpdu_in(&from_root);
bpdu_in(&slow);
secs(2);
}
check(stp_root_port == 8, "the 10G port is the root port");
check(root_bridge_cost == 2000, "its cost is the 10G value, not a flat default");
}
static struct sim_bpdu root_on(uint8_t port)
{
struct sim_bpdu b = { .port = port, .root_prio = 0x40, .root_cost = 0, .br_prio = 0x40, .port_id = 2 };
memcpy(b.root_mac, ROOT_MAC, 6);
memcpy(b.br_mac, ROOT_MAC, 6);
return b;
}
static const uint8_t THIRD_MAC[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55 };
static void scen_root_times(void)
{
struct sim_bpdu r;
printf("9. the times of the root travel on and time the listen period\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set(1 << 8);
secs(1);
r = root_on(8);
r.age = 3;
r.maxage = 12;
r.hello = 1;
r.fwd = 8;
bpdu_in(&r);
check(stp_root_port == 8, "port 9 is the root port");
links_set((1 << 1) | (1 << 8));
for (int i = 0; i < 2; i++) {
bpdu_in(&r);
secs(1);
}
check_state(1, 1, "a new designated port starts discarding");
for (int i = 0; i < 5; i++) {
bpdu_in(&r);
secs(1);
}
check_state(1, 1, "it still discards short of the root's forward delay");
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(1);
}
check_state(1, 2, "it learns after the root's 8 s, not our own 15 s");
for (int i = 0; i < 8; i++) {
bpdu_in(&r);
secs(1);
}
check_state(1, 3, "and forwards after a second 8 s");
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(1);
}
check(tx_frames[1] > 0, "the designated port sends BPDUs");
check((uint8_t)last_tx[1].age == 4, "the message age is one more than received");
check((uint8_t)last_tx[1].age_max == 12, "max age is the root's");
check((uint8_t)last_tx[1].fwd_delay == 8, "forward delay is the root's");
check((uint8_t)last_tx[1].hello == 2, "hello time stays our own");
}
static void scen_too_old(void)
{
struct sim_bpdu r;
printf("10. information as old as max age is not used\n");
reset_all();
links_set(1 << 8);
secs(1);
r = root_on(8);
r.age = 20;
r.maxage = 20;
r.hello = 2;
r.fwd = 15;
bpdu_in(&r);
check(stp_root_port == 0xff, "the switch stays its own root");
r.age = 19;
bpdu_in(&r);
check(stp_root_port == 8, "one second younger it is accepted");
}
static void scen_info_expiry(void)
{
struct sim_bpdu r;
printf("11. received information lasts three of the sender's hellos\n");
reset_all();
links_set(1 << 8);
secs(1);
r = root_on(8);
r.maxage = 20;
r.hello = 1;
r.fwd = 15;
bpdu_in(&r);
check(stp_root_port == 8, "port 9 is the root port");
tick(2 * STP_HZ + STP_HZ / 2);
check(stp_root_port == 8, "still the root port 2.5 s later");
tick(STP_HZ);
check(stp_root_port == 0xff, "gone after 3 s with a 1 s hello");
}
static void scen_dport_tiebreak(void)
{
struct sim_bpdu a;
struct sim_bpdu b;
printf("12. equal cost to one bridge: the lower designated port ID wins\n");
reset_all();
links_set((1 << 1) | (1 << 2));
secs(1);
a = root_on(1);
a.port_id = 5;
b = root_on(2);
b.port_id = 2;
for (int i = 0; i < 12; i++) {
bpdu_in(&a);
bpdu_in(&b);
secs(2);
}
check(stp_root_port == 2, "port 3, which hears port ID 8002, is the root port");
check_state(1, 1, "port 2, which hears 8005, is discarding");
}
static void scen_rxport_tiebreak(void)
{
struct sim_bpdu a;
struct sim_bpdu b;
printf("13. same designated port on two of ours: the receiving port ID decides\n");
reset_all();
links_set((1 << 3) | (1 << 4));
secs(1);
a = root_on(3);
b = root_on(4);
for (int i = 0; i < 3; i++) {
bpdu_in(&a);
bpdu_in(&b);
secs(2);
}
check(stp_root_port == 3, "equal port priority: the lower port number, port 4");
stp_pprio[4] = 0x40;
for (int i = 0; i < 3; i++) {
bpdu_in(&a);
bpdu_in(&b);
secs(2);
}
check(stp_root_port == 4, "a better priority on port 5 wins");
stp_pprio[4] = 0x80;
}
static void scen_inferior_info(void)
{
struct sim_bpdu r;
struct sim_bpdu peer = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 };
struct sim_bpdu third;
printf("14. worse information replaces stored only from the same sender\n");
reset_all();
links_set((1 << 0) | (1 << 8));
secs(1);
r = root_on(8);
memcpy(peer.root_mac, ROOT_MAC, 6);
memcpy(peer.br_mac, PEER_MAC, 6);
third = peer;
third.root_cost = 400000;
memcpy(third.br_mac, THIRD_MAC, 6);
for (int i = 0; i < 10; i++) {
bpdu_in(&r);
bpdu_in(&peer);
secs(2);
}
check_state(0, 1, "port 1 is discarding behind the better peer");
for (int i = 0; i < 2; i++) {
bpdu_in(&r);
bpdu_in(&peer);
bpdu_in(&third);
secs(1);
}
check(stp_dcost[0] == 2000, "a worse bridge on the segment does not overwrite the peer");
check_state(0, 1, "and port 1 keeps discarding");
peer.root_cost = 400000;
peer.flags = 0x0c;
bpdu_in(&peer);
check(stp_dcost[0] == 400000, "the peer's own worse information does replace it");
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
bpdu_in(&peer);
secs(2);
}
check_state(0, 3, "port 1 is designated and forwards again");
}
static void scen_root_role_bpdu(void)
{
struct sim_bpdu r;
struct sim_bpdu peer = {
.port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1, .flags = 0x08 | 0x30
};
printf("15. an RST BPDU sent by a root port is not designated information\n");
reset_all();
links_set((1 << 0) | (1 << 8));
secs(1);
r = root_on(8);
memcpy(peer.root_mac, ROOT_MAC, 6);
memcpy(peer.br_mac, PEER_MAC, 6);
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
bpdu_in(&peer);
secs(2);
}
check_state(0, 3, "port 1 keeps forwarding");
check(stp_root_port == 8, "the uplink stays the root port");
}
static void scen_alternate_takes_over(void)
{
struct sim_bpdu r;
struct sim_bpdu peer = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 };
printf("16. when the root port's information expires an alternate path takes over\n");
reset_all();
stp_pcost[8] = 2000;
links_set((1 << 0) | (1 << 8));
secs(1);
r = root_on(8);
memcpy(peer.root_mac, ROOT_MAC, 6);
memcpy(peer.br_mac, PEER_MAC, 6);
for (int i = 0; i < 12; i++) {
bpdu_in(&r);
bpdu_in(&peer);
secs(2);
}
check(stp_root_port == 8, "the 10G uplink is the root port");
for (int i = 0; i < 4; i++) {
bpdu_in(&peer);
secs(2);
}
check(stp_root_port == 0, "port 1 is the root port once port 9 went quiet");
check(memcmp(root_bridge.mac, ROOT_MAC, 6) == 0, "the root is still the same bridge");
check(root_bridge_cost == 22000, "the cost is the peer's plus port 1's");
stp_pcost[8] = 0;
}
static void scen_dbridge_tiebreak(void)
{
struct sim_bpdu via_peer = { .port = 1, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 };
struct sim_bpdu via_third;
printf("17. equal cost through two bridges: the lower bridge ID wins\n");
reset_all();
links_set((1 << 1) | (1 << 2));
secs(1);
memcpy(via_peer.root_mac, ROOT_MAC, 6);
memcpy(via_peer.br_mac, PEER_MAC, 6);
via_third = via_peer;
via_third.port = 2;
memcpy(via_third.br_mac, THIRD_MAC, 6);
via_third.br_prio = 0x70;
for (int i = 0; i < 12; i++) {
bpdu_in(&via_peer);
bpdu_in(&via_third);
secs(2);
}
check(stp_root_port == 2, "port 3, behind the bridge with priority 7000, is the root port");
check_state(1, 1, "port 2, behind 8000, is discarding");
}
static void scen_times_revert(void)
{
struct sim_bpdu r;
printf("18. back to our own times once we are the root again\n");
reset_all();
links_set((1 << 1) | (1 << 8));
secs(1);
r = root_on(8);
r.age = 3;
r.maxage = 12;
r.hello = 2;
r.fwd = 8;
for (int i = 0; i < 4; i++) {
bpdu_in(&r);
secs(2);
}
check(stp_root_port == 8, "port 9 is the root port");
secs(8);
check(stp_root_port == 0xff, "the switch is its own root again");
tx_frames[1] = 0;
secs(3);
check(tx_frames[1] > 0, "port 2 still sends");
check((uint8_t)last_tx[1].age == 0, "the message age is 0 from the root");
check((uint8_t)last_tx[1].age_max == 20, "max age is our own again");
check((uint8_t)last_tx[1].fwd_delay == 15, "forward delay is our own again");
}
static void ring_with_uplink_on_9(struct sim_bpdu *r, struct sim_bpdu *peer)
{
struct sim_bpdu p = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 };
reset_all();
stp_pcost[8] = 2000;
links_set((1 << 0) | (1 << 8));
secs(1);
*r = root_on(8);
memcpy(p.root_mac, ROOT_MAC, 6);
memcpy(p.br_mac, PEER_MAC, 6);
*peer = p;
for (int i = 0; i < 12; i++) {
bpdu_in(r);
bpdu_in(peer);
secs(2);
}
}
static void scen_tc_via_root_port(void)
{
struct sim_bpdu r;
struct sim_bpdu peer;
int waited = 0;
printf("19. a port that takes over as root port tells the root about it\n");
ring_with_uplink_on_9(&r, &peer);
check(stp_root_port == 8 && port_state(0) == 1, "port 1 starts as the blocked alternate");
check(tc_frames[0] == 0, "no TC on port 1 so far");
links_set(1 << 0);
while (port_state(0) != 3 && waited < 20 * STP_HZ) {
bpdu_in(&peer);
tick(STP_HZ / 2);
waited += STP_HZ / 2;
}
check(stp_root_port == 0, "port 1 is the root port after port 9 went down");
check_state(0, 3, "and forwards");
tick(2);
check(tc_frames[0] > 0, "port 1 sends a BPDU with TC toward the root right away");
check((last_tx[0].flags & 0x0c) == 0x08, "that BPDU carries the root port role");
stp_pcost[8] = 0;
}
static void scen_tc_window_rstp(void)
{
struct sim_bpdu r;
struct sim_bpdu peer;
int waited = 0;
int seen;
printf("20. in RSTP the TC window lasts hello time plus one second\n");
ring_with_uplink_on_9(&r, &peer);
links_set(1 << 0);
while (port_state(0) != 3 && waited < 20 * STP_HZ) {
bpdu_in(&peer);
tick(STP_HZ / 2);
waited += STP_HZ / 2;
}
for (int i = 0; i < 4; i++) {
bpdu_in(&peer);
secs(1);
}
seen = tc_frames[0];
for (int i = 0; i < 6; i++) {
bpdu_in(&peer);
secs(1);
}
check(seen > 0, "TC was sent");
check(tc_frames[0] == seen, "and stops once hello plus one second is over");
stp_pcost[8] = 0;
}
static void scen_tc_propagation(void)
{
struct sim_bpdu r;
int tc2;
int tc9;
struct sim_bpdu rtc;
printf("21. a TC from the root side goes out on the other ports, not back\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set((1 << 1) | (1 << 8));
secs(1);
r = root_on(8);
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 3, "designated port 2 forwards");
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(2);
}
memset(flush_count, 0, sizeof(flush_count));
tc2 = tc_frames[1];
tc9 = tc_frames[8];
rtc = r;
rtc.flags = 0x3d;
bpdu_in(&rtc);
tick(2);
check(tc_frames[1] > tc2, "port 2 passes the TC on at once");
check(flush_count[1] > 0, "port 2 forgets what it learned");
check(flush_count[8] == 0, "the port the TC came in on keeps its addresses");
check(tc_frames[8] == tc9, "no TC is echoed back to the root");
}
static void scen_tc_legacy_tcn(void)
{
struct sim_bpdu r;
struct sim_bpdu ack;
int quiet;
int n;
printf("22. with 802.1D neighbours the root port sends TCN until acknowledged\n");
reset_all();
stp_rstp = 0;
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set(1 << 8);
secs(1);
r = root_on(8);
r.legacy = 1;
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
secs(2);
}
check(stp_root_port == 8, "port 9 is the root port");
check(tcn_frames[8] > 0, "port 9 entering forwarding is itself reported with TCN");
ack = r;
ack.flags = 0x81;
bpdu_in(&ack);
quiet = tcn_frames[8];
for (int i = 0; i < 5; i++) {
bpdu_in(&r);
secs(2);
}
check(tcn_frames[8] == quiet, "acknowledged: no more TCN while nothing changes");
links_set((1 << 1) | (1 << 8));
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 3, "port 2 came up and forwards");
check(tcn_frames[8] > quiet, "port 9 sends TCN toward the root again");
bpdu_in(&ack);
n = tcn_frames[8];
for (int i = 0; i < 5; i++) {
bpdu_in(&r);
secs(2);
}
check(tcn_frames[8] == n, "and stops once the root acknowledged it");
}
static void scen_alt_forgets(void)
{
struct sim_bpdu r;
struct sim_bpdu peer = { .port = 0, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1 };
printf("23. a port that turns alternate forgets what it learned\n");
reset_all();
links_set((1 << 0) | (1 << 8));
secs(1);
r = root_on(8);
for (int i = 0; i < 10; i++) {
bpdu_in(&r);
secs(2);
}
check_state(0, 3, "port 1 forwards as designated");
memset(flush_count, 0, sizeof(flush_count));
memcpy(peer.root_mac, ROOT_MAC, 6);
memcpy(peer.br_mac, PEER_MAC, 6);
bpdu_in(&peer);
check_state(0, 1, "port 1 is blocked behind the better peer");
check(flush_count[0] > 0, "and its addresses are flushed");
}
static void scen_edge_no_tc(void)
{
struct sim_bpdu r;
uint16_t tc;
int tc9;
int tc2;
printf("24. an edge port coming up raises no topology change\n");
reset_all();
links_set((1 << 1) | (1 << 8));
secs(1);
r = root_on(8);
for (int i = 0; i < 10; i++) {
bpdu_in(&r);
secs(2);
}
for (int i = 0; i < 20; i++) {
bpdu_in(&r);
secs(2);
}
tc = stp_tc_count;
tc9 = tc_frames[8];
tc2 = tc_frames[1];
links_set((1 << 1) | (1 << 3) | (1 << 8));
for (int i = 0; i < 4; i++) {
bpdu_in(&r);
secs(2);
}
check_state(3, 3, "the host port forwards as an edge port");
check(stp_tc_count == tc, "the change counter does not move");
check(tc_frames[8] == tc9 && tc_frames[1] == tc2, "no TC is sent");
}
static void scen_link_loss_no_tc(void)
{
struct sim_bpdu r;
int tc3;
int tc9;
printf("25. losing a designated link flushes it but sends no TC\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
stp_pflags[2] &= ~STP_PF_AUTOEDGE;
links_set((1 << 1) | (1 << 2) | (1 << 8));
secs(1);
r = root_on(8);
for (int i = 0; i < 20; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 3, "port 2 forwards");
memset(flush_count, 0, sizeof(flush_count));
tc3 = tc_frames[2];
tc9 = tc_frames[8];
links_set((1 << 2) | (1 << 8));
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(1);
}
check(flush_count[1] > 0, "port 2's addresses are flushed");
check(tc_frames[2] == tc3 && tc_frames[8] == tc9, "no TC goes out");
}
static void scen_proposal_agreement(void)
{
struct sim_bpdu r;
struct sim_bpdu agree = {
.port = 1, .root_prio = 0x40, .root_cost = 22000, .br_prio = 0x80, .port_id = 3, .flags = 0x78
};
printf("26. a new designated port proposes and forwards as soon as the neighbour agrees\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set(1 << 8);
secs(1);
r = root_on(8);
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(2);
}
links_set((1 << 1) | (1 << 8));
tick(STP_HZ + STP_HZ / 2);
check_state(1, 1, "port 2 starts discarding");
check((last_tx[1].flags & 0x02) != 0, "and its BPDU carries the proposal flag");
check((last_tx[1].flags & 0x0c) == 0x0c, "with the designated role");
memcpy(agree.root_mac, ROOT_MAC, 6);
memcpy(agree.br_mac, PEER_MAC, 6);
bpdu_in(&agree);
tick(2);
check_state(1, 3, "port 2 forwards right after the agreement");
}
static void scen_no_agreement(void)
{
struct sim_bpdu r;
printf("27. without an agreement the port waits both forward delays\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
stp_pflags[2] &= ~STP_PF_AUTOEDGE;
stp_pp2p[2] = 2;
links_set(1 << 8);
secs(1);
r = root_on(8);
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(2);
}
links_set((1 << 1) | (1 << 2) | (1 << 8));
for (int i = 0; i < 5; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 1, "port 2 still discards after 10 s");
check((last_tx[2].flags & 0x02) == 0, "a port set to p2p off does not propose");
for (int i = 0; i < 4; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 2, "port 2 learns once the first forward delay is over");
for (int i = 0; i < 8; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 3, "and forwards after the second");
stp_pp2p[2] = 0;
}
static void scen_sync_on_proposal(void)
{
struct sim_bpdu r;
struct sim_bpdu prop;
printf("28. a proposal on the root port syncs the designated ports and is agreed\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set((1 << 1) | (1 << 3) | (1 << 8));
secs(1);
r = root_on(8);
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 3, "designated port 2 forwards");
check_state(3, 3, "edge port 4 forwards");
check_state(8, 3, "root port 9 forwards");
prop = r;
prop.flags = 0x3e;
bpdu_in(&prop);
tick(2);
check_state(1, 1, "port 2 is put back to discarding for the sync");
check((last_tx[1].flags & 0x02) != 0, "and proposes downstream");
check_state(3, 3, "the edge port is left alone");
check_state(8, 3, "the root port keeps forwarding");
check((last_tx[8].flags & 0x40) != 0, "the root port answers with an agreement");
check((last_tx[8].flags & 0x0c) == 0x08, "in a BPDU with the root role");
}
static void scen_rapid_failover(void)
{
struct sim_bpdu r;
struct sim_bpdu peer;
printf("29. when the root port goes down the alternate forwards at once\n");
ring_with_uplink_on_9(&r, &peer);
check(stp_root_port == 8 && port_state(0) == 1, "port 1 is the blocked alternate");
links_set(1 << 0);
for (int i = 0; i < 4; i++) {
bpdu_in(&peer);
tick(STP_HZ / 2);
}
check(stp_root_port == 0, "port 1 is the root port within two seconds");
check_state(0, 3, "and already forwards");
stp_pcost[8] = 0;
}
static void scen_rapid_reroot(void)
{
struct sim_bpdu r;
struct sim_bpdu peer;
int both = 0;
printf("30. the uplink coming back takes over without both ports forwarding\n");
ring_with_uplink_on_9(&r, &peer);
stp_pcost[8] = 2000;
links_set(1 << 0);
for (int i = 0; i < 6; i++) {
bpdu_in(&peer);
tick(STP_HZ / 2);
}
check(stp_root_port == 0 && port_state(0) == 3, "port 1 is the forwarding root port");
links_set((1 << 0) | (1 << 8));
for (int i = 0; i < 12; i++) {
struct sim_bpdu p = r;
p.flags = 0x3e;
bpdu_in(&p);
if (port_state(0) == 3 && port_state(8) == 3)
both++;
bpdu_in(&peer);
if (port_state(0) == 3 && port_state(8) == 3)
both++;
for (int k = 0; k < STP_HZ / 4; k++) {
tick(1);
if (port_state(0) == 3 && port_state(8) == 3)
both++;
}
}
check(stp_root_port == 8, "port 9 is the root port again");
check_state(8, 3, "and forwards within three seconds");
check_state(0, 1, "port 1 is back to discarding");
check(both == 0, "ports 1 and 9 never forwarded at the same time");
stp_pcost[8] = 0;
}
static void scen_agreement_other_root(void)
{
struct sim_bpdu r;
struct sim_bpdu agree = {
.port = 1, .root_prio = 0x40, .root_cost = 22000, .br_prio = 0x80, .port_id = 3, .flags = 0x78
};
printf("31. an agreement for a different root does not open a port\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set(1 << 8);
secs(1);
r = root_on(8);
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(2);
}
links_set((1 << 1) | (1 << 8));
tick(STP_HZ + STP_HZ / 2);
memcpy(agree.root_mac, THIRD_MAC, 6);
memcpy(agree.br_mac, PEER_MAC, 6);
bpdu_in(&agree);
tick(2);
check_state(1, 1, "port 2 keeps discarding");
}
static void scen_repeated_proposal(void)
{
struct sim_bpdu r;
struct sim_bpdu prop;
struct sim_bpdu agree = {
.port = 1, .root_prio = 0x40, .root_cost = 22000, .br_prio = 0x80, .port_id = 3, .flags = 0x78
};
int agreements = 0;
printf("32. a proposal repeated every hello syncs only once\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set((1 << 1) | (1 << 8));
secs(1);
r = root_on(8);
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 3, "designated port 2 forwards");
prop = r;
prop.flags = 0x3e;
bpdu_in(&prop);
tick(2);
check_state(1, 1, "the first proposal syncs port 2");
memcpy(agree.root_mac, ROOT_MAC, 6);
memcpy(agree.br_mac, PEER_MAC, 6);
bpdu_in(&agree);
tick(2);
check_state(1, 3, "port 2 forwards again after its own agreement");
for (int i = 0; i < 5; i++) {
int before = tx_frames[8];
bpdu_in(&prop);
tick(2);
if (tx_frames[8] > before && (last_tx[8].flags & 0x40))
agreements++;
secs(2);
check_state(1, 3, "a repeated proposal does not block port 2 again");
}
check(agreements == 5, "every repeated proposal is answered with an agreement");
}
static void scen_dispute(void)
{
struct sim_bpdu r;
struct sim_bpdu quiet = {
.port = 1, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .flags = 0x0c
};
struct sim_bpdu claim;
int opened = 0;
printf("33. a worse neighbour that claims to forward as designated is disputed\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set((1 << 1) | (1 << 8));
secs(1);
r = root_on(8);
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 3, "designated port 2 forwards");
memcpy(quiet.root_mac, ROOT_MAC, 6);
memcpy(quiet.br_mac, PEER_MAC, 6);
bpdu_in(&quiet);
tick(2);
check_state(1, 3, "worse designated info without the learning flag is no dispute");
claim = quiet;
claim.flags = 0x3c;
bpdu_in(&claim);
tick(2);
check_state(1, 1, "with the learning flag set port 2 goes back to discarding");
for (int i = 0; i < 12; i++) {
bpdu_in(&r);
bpdu_in(&claim);
for (int k = 0; k < 2 * STP_HZ; k++) {
tick(1);
if (port_state(1) == 3)
opened++;
}
}
check(opened == 0, "and never forwards while the dispute goes on");
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
bpdu_in(&quiet);
secs(2);
}
check_state(1, 3, "once the neighbour stops claiming, port 2 forwards again");
}
static void scen_learning_state(void)
{
struct sim_bpdu r;
struct sim_bpdu claim = {
.port = 1, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .flags = 0x3c
};
printf("34. without an agreement a port learns for one forward delay before it forwards\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set(1 << 8);
secs(1);
r = root_on(8);
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(2);
}
links_set((1 << 1) | (1 << 8));
for (int i = 0; i < 6; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 1, "port 2 discards for the first forward delay");
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 2, "then learns");
check((last_tx[1].flags & 0x30) == 0x10, "its BPDUs carry learning without forwarding");
memcpy(claim.root_mac, ROOT_MAC, 6);
memcpy(claim.br_mac, PEER_MAC, 6);
bpdu_in(&claim);
tick(2);
check_state(1, 1, "a dispute while learning sends it back to discarding");
for (int i = 0; i < 9; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 2, "after a full forward delay it learns again");
for (int i = 0; i < 8; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 3, "and forwards after the second forward delay");
}
static void scen_reply_to_inferior(void)
{
struct sim_bpdu r;
int n;
int guard = 0;
struct sim_bpdu worse = {
.port = 1, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .flags = 0x0c
};
struct sim_bpdu same_root_better = {
.port = 1, .root_prio = 0x40, .root_cost = 2000, .br_prio = 0x80, .port_id = 1, .flags = 0x0c
};
printf("35. a designated port answers worse information at once\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set((1 << 1) | (1 << 8));
secs(1);
r = root_on(8);
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 3, "designated port 2 forwards");
n = tx_frames[1];
while (tx_frames[1] == n && guard++ < 3 * STP_HZ)
tick(1);
check(tx_frames[1] > n, "port 2 sent its regular hello");
memcpy(worse.root_mac, ROOT_MAC, 6);
memcpy(worse.br_mac, PEER_MAC, 6);
n = tx_frames[1];
bpdu_in(&worse);
tick(2);
check(tx_frames[1] > n, "the worse BPDU is answered within two ticks, not at the next hello");
tick(STP_HZ / 2);
n = tx_frames[1];
memcpy(same_root_better.root_mac, ROOT_MAC, 6);
memcpy(same_root_better.br_mac, PEER_MAC, 6);
bpdu_in(&same_root_better);
tick(2);
check(tx_frames[1] == n, "better information is not answered");
}
static void scen_backup_role(void)
{
struct sim_bpdu r;
struct sim_bpdu own = { .port = 2, .root_prio = 0x40, .root_cost = 20000, .br_prio = 0x80, .port_id = 4 };
printf("36. a port hearing another port of this bridge is a blocked backup\n");
reset_all();
stp_pflags[2] &= ~STP_PF_AUTOEDGE;
stp_pflags[3] &= ~STP_PF_AUTOEDGE;
links_set((1 << 2) | (1 << 3) | (1 << 8));
secs(1);
r = root_on(8);
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
secs(2);
}
check_state(2, 3, "port 3 forwards");
check_state(3, 3, "port 4 forwards");
memcpy(own.root_mac, ROOT_MAC, 6);
memcpy(own.br_mac, uip_ethaddr.addr, 6);
bpdu_in(&own);
tick(2);
check_state(3, 1, "port 4, the worse of the two, is blocked");
check(((stp_backup >> 3) & 1) == 1, "and has the backup role");
check(((stp_backup >> 2) & 1) == 0, "port 3 is not a backup");
for (int i = 0; i < 20; i++) {
bpdu_in(&r);
secs(2);
}
check_state(3, 3, "once its own BPDUs stop coming back port 4 forwards again");
check(((stp_backup >> 3) & 1) == 0, "and is no longer a backup");
bpdu_in(&own);
tick(2);
check(((stp_backup >> 3) & 1) == 1, "a new loop makes it a backup again");
links_set((1 << 2) | (1 << 8));
secs(2);
check(((stp_backup >> 3) & 1) == 0, "losing the link clears the backup role");
}
static void migrate_to_stp(void)
{
struct sim_bpdu r;
struct sim_bpdu old = {
.port = 1, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .legacy = 1
};
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set((1 << 1) | (1 << 8));
secs(1);
r = root_on(8);
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(2);
}
check(last_tx[1].version == 2, "port 2 starts with RST BPDUs");
memcpy(old.root_mac, ROOT_MAC, 6);
memcpy(old.br_mac, PEER_MAC, 6);
bpdu_in(&old);
tick(2);
check(last_tx[1].version == 0 && last_tx[1].bpdu_type == 0, "after the 802.1D BPDU port 2 sends Config BPDUs");
check((last_tx[1].flags & 0x02) == 0, "without a proposal");
check(((stp_legacy >> 1) & 1) == 1 && ((stp_legacy >> 8) & 1) == 0,
"only port 2 is switched, not the root port");
}
static void scen_migrate_to_stp(void)
{
printf("37. a port that hears an 802.1D neighbour sends Config BPDUs\n");
migrate_to_stp();
}
static void scen_migrate_back(void)
{
struct sim_bpdu r;
struct sim_bpdu old = {
.port = 1, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .legacy = 1
};
struct sim_bpdu rst;
printf("38. mcheck and an RST neighbour bring the port back to RSTP\n");
migrate_to_stp();
r = root_on(8);
secs(4);
stp_port_mcheck(1);
tick(2);
check(last_tx[1].version == 2, "mcheck makes port 2 send RST BPDUs again");
memcpy(old.root_mac, ROOT_MAC, 6);
memcpy(old.br_mac, PEER_MAC, 6);
bpdu_in(&old);
tick(STP_HZ);
check(last_tx[1].version == 2, "an 802.1D BPDU within the migrate time does not switch it yet");
for (int i = 0; i < 4; i++) {
bpdu_in(&r);
tick(STP_HZ / 2);
}
tick(2 * STP_HZ);
check(last_tx[1].version == 0, "once the migrate time is over it falls back to STP");
secs(4);
rst = old;
rst.legacy = 0;
rst.flags = 0x0c;
bpdu_in(&rst);
tick(2);
check(last_tx[1].version == 2, "an RST BPDU after the migrate time switches it back to RSTP");
}
static void scen_legacy_root_tcn(void)
{
struct sim_bpdu r;
struct sim_bpdu ack;
int tcn;
printf("39. behind an 802.1D root port a topology change goes out as TCN\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set(1 << 8);
secs(1);
r = root_on(8);
r.legacy = 1;
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
secs(2);
}
check(stp_root_port == 8, "port 9 is the root port");
ack = r;
ack.flags = 0x81;
bpdu_in(&ack);
for (int i = 0; i < 5; i++) {
bpdu_in(&r);
secs(2);
}
tcn = tcn_frames[8];
links_set((1 << 1) | (1 << 8));
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
secs(2);
}
check_state(1, 3, "port 2 came up and forwards");
check(tcn_frames[8] > tcn, "port 9 reports the change with TCN");
check(last_tx[1].version == 2, "port 2, with no 802.1D neighbour, sends RST BPDUs");
}
static void scen_alternate_agrees(void)
{
struct sim_bpdu r;
struct sim_bpdu peer;
struct sim_bpdu prop;
int n;
printf("40. an alternate port answers a proposal with an agreement\n");
ring_with_uplink_on_9(&r, &peer);
stp_pflags[2] &= ~STP_PF_AUTOEDGE;
links_set((1 << 0) | (1 << 2) | (1 << 8));
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
bpdu_in(&peer);
secs(2);
}
check(stp_root_port == 8 && port_state(0) == 1, "port 1 is the blocked alternate");
check_state(2, 3, "designated port 3 forwards");
prop = peer;
prop.flags = 0x3e;
n = tx_frames[0];
bpdu_in(&prop);
tick(2);
check(tx_frames[0] > n, "port 1 answers at once");
check((last_tx[0].flags & 0x40) != 0, "with the agreement flag");
check((last_tx[0].flags & 0x0c) == 0x04, "in a BPDU with the alternate role");
check((last_tx[0].flags & 0x02) == 0, "and without a proposal of its own");
check_state(0, 1, "port 1 stays discarding");
check_state(8, 3, "the root port is not touched by the sync");
check_state(2, 1, "the first proposal syncs designated port 3");
for (int i = 0; i < 18; i++) {
bpdu_in(&r);
bpdu_in(&peer);
secs(2);
}
check_state(2, 3, "port 3 forwards again");
n = tx_frames[0];
bpdu_in(&prop);
tick(2);
check(tx_frames[0] > n && (last_tx[0].flags & 0x40), "a repeated proposal is answered again");
check_state(2, 3, "but does not sync port 3 a second time");
n = tx_frames[0];
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
bpdu_in(&peer);
secs(2);
}
check(tx_frames[0] == n, "without a proposal the alternate port stays silent");
stp_pcost[8] = 0;
}
static void scen_agreement_from_alternate(void)
{
struct sim_bpdu r;
struct sim_bpdu agree = {
.port = 1, .root_prio = 0x40, .root_cost = 22000, .br_prio = 0x80, .port_id = 3, .flags = 0x44
};
printf("41. an agreement from an alternate port opens a designated port\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set(1 << 8);
secs(1);
r = root_on(8);
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(2);
}
links_set((1 << 1) | (1 << 8));
tick(STP_HZ + STP_HZ / 2);
check_state(1, 1, "port 2 proposes from discarding");
memcpy(agree.root_mac, ROOT_MAC, 6);
memcpy(agree.br_mac, PEER_MAC, 6);
bpdu_in(&agree);
tick(2);
check_state(1, 3, "the agreement in an alternate-role BPDU opens it");
}
static void scen_timer_relation(void)
{
printf("42. bridge times must keep 2*(fwd-1) >= maxage >= 2*(hello+1)\n");
reset_all();
save_cmd = 1;
sim_cmd("stp fwd 30");
sim_cmd("stp maxage 40");
check(stp_fwddelay_s == 30 && stp_maxage_s == 40, "fwd 30 then maxage 40 is accepted");
check(err_status == ERR_OK, "an accepted value reports no error, so it is kept in the command history");
sim_cmd("stp fwd 20");
check(stp_fwddelay_s == 30, "fwd 20 under maxage 40 is refused");
check(err_status != ERR_OK, "a refused value reports an error, so it stays out of the command history");
sim_cmd("stp port 3 cost x");
check(err_status != ERR_OK, "a malformed command reports an error too");
sim_cmd("stp maxage 6");
sim_cmd("stp hello 3");
check(stp_maxage_s == 6 && stp_hello_s == 2, "hello 3 under maxage 6 is refused");
sim_cmd("stp fwd 4");
sim_cmd("stp maxage 7");
check(stp_fwddelay_s == 4 && stp_maxage_s == 6, "maxage 7 over fwd 4 is refused");
stp_defaults();
save_cmd = 0;
sim_cmd("stp maxage 40");
sim_cmd("stp fwd 30");
check(stp_maxage_s == 40 && stp_fwddelay_s == 30, "a saved config may pass through a broken order");
stp_setup();
check(stp_maxage_s == 40 && stp_fwddelay_s == 30, "and keeps its values when the result is consistent");
sim_cmd("stp fwd 10");
stp_setup();
check(stp_hello_s == 2 && stp_maxage_s == 20 && stp_fwddelay_s == 15,
"an inconsistent result falls back to 2/20/15 when STP starts");
save_cmd = 1;
}
static void scen_recent_root(void)
{
struct sim_bpdu r9;
struct sim_bpdu r3;
struct sim_bpdu agree = {
.port = 1, .root_prio = 0x40, .root_cost = 22000, .br_prio = 0x80, .port_id = 3, .flags = 0x78
};
printf("43. ports that were root a moment ago are held back when a new root port forwards\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
stp_pflags[2] &= ~STP_PF_AUTOEDGE;
links_set((1 << 1) | (1 << 8));
secs(1);
r9 = root_on(8);
for (int i = 0; i < 18; i++) {
bpdu_in(&r9);
secs(2);
}
check(stp_root_port == 8, "port 9 is the root port");
check_state(1, 3, "designated port 2 forwards");
stp_rrwhile[1] = 10 * STP_HZ;
stp_pcost[2] = 2000;
links_set((1 << 1) | (1 << 2) | (1 << 8));
secs(1);
r3 = root_on(2);
bpdu_in(&r3);
tick(2);
check(stp_root_port == 2, "port 3, the cheaper path, is the new root port");
check_state(2, 3, "and forwards at once");
check_state(8, 1, "the old root port 9 is discarding");
check_state(1, 1, "port 2, a recent root port too, is put back to discarding");
memcpy(agree.root_mac, ROOT_MAC, 6);
memcpy(agree.br_mac, PEER_MAC, 6);
bpdu_in(&agree);
tick(2);
check_state(1, 1, "an agreement does not open it while it is a recent root");
for (int i = 0; i < 6; i++) {
bpdu_in(&r3);
secs(2);
}
bpdu_in(&agree);
tick(2);
check_state(1, 3, "once that time is over the agreement opens it");
stp_pcost[2] = 0;
}
static void scen_recent_backup(void)
{
struct sim_bpdu r;
struct sim_bpdu r4;
printf("44. a port that was a backup a moment ago does not rush into forwarding as root\n");
reset_all();
links_set(1 << 8);
secs(1);
r = root_on(8);
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(2);
}
stp_pflags[3] &= ~STP_PF_AUTOEDGE;
stp_pcost[3] = 2000;
stp_rbwhile[3] = 3 * STP_HZ;
links_set((1 << 3) | (1 << 8));
secs(1);
r4 = root_on(3);
bpdu_in(&r4);
tick(2);
check(stp_root_port == 3, "port 4 is the new root port");
check_state(3, 1, "but stays discarding while it is a recent backup");
for (int i = 0; i < 4; i++) {
bpdu_in(&r4);
secs(1);
}
check_state(3, 3, "and forwards once that time is over");
stp_pcost[3] = 0;
}
static void scen_short_bpdu(void)
{
struct sim_bpdu r;
struct sim_bpdu c;
int n;
struct sim_bpdu tcn;
printf("45. BPDUs shorter than their type allows are ignored\n");
reset_all();
links_set(1 << 8);
secs(1);
r = root_on(8);
r.msg_len = 38;
bpdu_in(&r);
check(stp_root_port == 0xff, "an RST BPDU of 38 bytes is ignored");
r.msg_len = 0;
bpdu_in(&r);
check(stp_root_port == 8, "at 39 bytes it is taken");
reset_all();
links_set(1 << 8);
secs(1);
c = root_on(8);
c.legacy = 1;
c.msg_len = 37;
bpdu_in(&c);
check(stp_root_port == 0xff, "a Config BPDU of 37 bytes is ignored");
c.msg_len = 0;
bpdu_in(&c);
check(stp_root_port == 8, "at 38 bytes it is taken");
n = tx_frames[8];
tcn = c;
tcn.legacy = 2;
tcn.msg_len = 6;
bpdu_in(&tcn);
check(tx_frames[8] == n, "a TCN of 6 bytes gets no TCA");
tcn.msg_len = 0;
bpdu_in(&tcn);
check(tx_frames[8] > n, "a TCN of 7 bytes is acknowledged");
}
static void scen_port_roles(void)
{
struct sim_bpdu r;
struct sim_bpdu peer;
printf("46. the reported role covers every port, a port without link is disabled\n");
ring_with_uplink_on_9(&r, &peer);
check(stp_port_role(8) == 1, "port 9 is reported as root");
check(stp_port_role(0) == 3, "port 1 as alternate");
check(stp_port_role(4) == 0, "port 5, which has no link, as disabled");
links_set((1 << 0) | (1 << 4) | (1 << 8));
secs(2);
check(stp_port_role(4) == 2, "port 5 is designated once its link is up");
stp_pflags[4] &= ~STP_PF_ENABLED;
check(stp_port_role(4) == 0, "and disabled again when STP is off on it");
stp_pflags[4] |= STP_PF_ENABLED;
stp_pcost[8] = 0;
}
static void scen_no_tx_without_link(void)
{
struct sim_bpdu r;
printf("47. no BPDU goes out on a port without link\n");
reset_all();
links_set(1 << 8);
secs(1);
r = root_on(8);
for (int i = 0; i < 5; i++) {
bpdu_in(&r);
secs(2);
}
check(tx_frames[1] == 0, "port 2 without link sent nothing in ten seconds");
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set((1 << 1) | (1 << 8));
for (int i = 0; i < 2; i++) {
bpdu_in(&r);
secs(2);
}
check(tx_frames[1] > 0, "once its link is up it sends");
}
static void scen_counters(void)
{
struct sim_bpdu r;
int sent = 1;
int same = 1;
int any = 0;
struct sim_bpdu tcn;
int zero;
printf("48. per-port BPDU and TC counters, and stp clear\n");
reset_all();
stp_pflags[1] &= ~STP_PF_AUTOEDGE;
links_set((1 << 1) | (1 << 8));
r = root_on(8);
for (int i = 0; i < 40; i++) {
bpdu_in(&r);
secs(1);
}
check(stp_cnt[STP_CNT_RX][8] == 40 && stp_cnt[STP_CNT_RX][1] == 0,
"each BPDU taken in is counted on its own port");
check(stp_cnt[STP_CNT_TX][1] > 0 && stp_cnt[STP_CNT_TX][0] == 0,
"BPDUs sent are counted, none on a port without link");
for (int p = 0; p < NPORTS; p++)
sent &= stp_cnt[STP_CNT_TX][p] == (uint32_t)tx_frames[p];
check(sent, "BPDUs sent match the frames on the wire on every port");
check(stp_cnt[STP_CNT_TCRX][8] == 0, "a BPDU without the TC flag is not counted as TC");
for (int p = 0; p < NPORTS; p++) {
same &= stp_cnt[STP_CNT_TCTX][p] == (uint32_t)(tc_frames[p] + tcn_frames[p]);
any |= tc_frames[p] + tcn_frames[p];
}
check(any && same, "TC sent matches the TC and TCN frames on the wire");
r.flags = 0x3d;
bpdu_in(&r);
check(stp_cnt[STP_CNT_TCRX][8] == 1, "a BPDU with the TC flag is counted as TC");
tcn = r;
tcn.legacy = 2;
bpdu_in(&tcn);
check(stp_cnt[STP_CNT_TCRX][8] == 2 && stp_cnt[STP_CNT_RX][8] == 42, "and so is a TCN");
sim_cmd("stp clear");
zero = err_status == ERR_OK;
for (int k = 0; k < STP_CNT_N; k++)
for (int p = 0; p < NPORTS; p++)
zero &= stp_cnt[k][p] == 0;
check(zero, "stp clear zeroes every counter on every port");
}
static void scen_path_cost_method(void)
{
struct sim_bpdu r;
printf("49. short path costs follow 802.1D-1998\n");
reset_all();
links_set(1 << 8);
r = root_on(8);
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(1);
}
check(root_bridge_cost == 20000, "long method: a 1G root port costs 20000");
sim_cmd("stp pathcost short");
check(err_status == ERR_OK && stp_pcost_short == 1, "stp pathcost short is accepted");
for (int i = 0; i < 2; i++) {
bpdu_in(&r);
secs(1);
}
check(root_bridge_cost == 4, "short method: the same port costs 4");
sim_speed[8] = 4;
for (int i = 0; i < 2; i++) {
bpdu_in(&r);
secs(1);
}
check(root_bridge_cost == 2, "and a 10G port costs 2");
sim_cmd("stp pathcost medium");
check(err_status != ERR_OK && stp_pcost_short == 1, "an unknown method is refused");
sim_cmd("stp pathcost long");
sim_speed[8] = 2;
}
static void scen_bpdu_handling(void)
{
printf("50. BPDU handling while STP is off: flood or filter\n");
reset_all();
stp_enabled = 0;
l2mc_calls = 0;
stp_off();
check(l2mc_calls && (l2mc_pmask & PMASK_9) == PMASK_9, "by default BPDUs are flooded when STP stops");
l2mc_calls = 0;
sim_cmd("stp bpdu filter");
check(err_status == ERR_OK && l2mc_calls && l2mc_pmask == PMASK_CPU,
"filter keeps them on the CPU, at once while STP is off");
stp_enabled = 1;
stp_setup();
l2mc_calls = 0;
sim_cmd("stp bpdu flood");
check(l2mc_calls == 0, "while STP runs the setting only waits");
stp_enabled = 0;
stp_off();
check((l2mc_pmask & PMASK_9) == PMASK_9, "and applies when STP stops");
sim_cmd("stp bpdu drop");
check(err_status != ERR_OK, "an unknown handling is refused");
stp_enabled = 1;
}
static void scen_last_tc(void)
{
struct sim_bpdu tcn;
uint16_t before;
printf("51. time since the last topology change\n");
reset_all();
links_set(1 << 8);
tcn = root_on(8);
tcn.legacy = 2;
secs(4);
before = stp_tc_count;
bpdu_in(&tcn);
check(stp_tc_count != before, "a TCN moves the topology change counter");
secs(5);
check(stp_tc_secs >= 4 && stp_tc_secs <= 5, "the clock counts the seconds since then");
bpdu_in(&tcn);
secs(1);
check(stp_tc_secs <= 1, "and starts again at the next change");
}
static void scen_lag(void)
{
struct sim_bpdu before;
struct sim_bpdu b;
struct sim_bpdu c;
struct sim_bpdu t;
printf("52. a link aggregation group is one port of the tree\n");
reset_all();
links_set((1 << 1) | (1 << 2) | (1 << 8));
secs(1);
before = root_on(2);
bpdu_in(&before);
check(stp_root_port == 2 && stp_info_while[2], "port 3 hears the root before it joins a group");
sim_lag[0] = (1 << 1) | (1 << 2);
secs(1);
check(stp_info_while[2] == 0, "joining the group drops what the port heard on its own");
check(stp_ent_id(STP_LAG_BASE) && !stp_ent_id(1) && !stp_ent_id(2), "the group replaces its member ports");
check(port_state(1) == 1 && port_state(2) == 1, "both members start discarding together");
b = root_on(2);
bpdu_in(&b);
check(stp_root_port == STP_LAG_BASE, "a BPDU on either member reaches the group");
c = root_on(1);
c.root_cost = 4;
bpdu_in(&c);
check(stp_root_port == STP_LAG_BASE, "and the other member feeds the same port");
secs(31);
bpdu_in(&b);
check(port_state(1) == 3 && port_state(2) == 3, "both members forward as the root port");
sim_cmd("stp port 2 cost 5");
check(err_status != ERR_OK, "a member port cannot be set on its own");
sim_cmd("stp lag 1 cost 5");
check(err_status == ERR_OK && stp_pcost[STP_LAG_BASE] == 5, "the group takes the setting");
sim_cmd("stp lag 5 cost 5");
check(err_status != ERR_OK, "a group number past the last one is refused");
memset(tx_frames, 0, sizeof(tx_frames));
tcn_frames[1] = 0;
t = root_on(2);
t.legacy = 2;
bpdu_in(&t);
check(tx_frames[1] == 1 && tx_frames[2] == 0, "a reply leaves through the lowest member");
check(last_tx[1].port_id == 101, "with the group's own port id");
memset(flush_count, 0, sizeof(flush_count));
links_set((1 << 2) | (1 << 8));
secs(1);
check(port_state(2) == 3 && stp_root_port == STP_LAG_BASE, "losing one member keeps the group forwarding");
check(flush_count[2] == 0, "and is no topology change");
memset(tx_frames, 0, sizeof(tx_frames));
bpdu_in(&t);
check(tx_frames[1] == 0 && tx_frames[2] == 1, "replies move to the member that still has a link");
sim_lag[0] = 0;
links_set((1 << 1) | (1 << 2) | (1 << 8));
secs(1);
check(!stp_ent_id(STP_LAG_BASE) && stp_ent_id(1) && stp_ent_id(2), "dissolving the group gives the ports back");
check(stp_root_port != STP_LAG_BASE, "and the group stops being the root port");
check(port_state(1) == 1 && port_state(2) == 1, "the ports start over discarding");
}
static void scen_tc_needs_forwarding(void)
{
struct sim_bpdu r;
struct sim_bpdu worse = {
.port = 2, .root_prio = 0x40, .root_cost = 200000, .br_prio = 0x80, .port_id = 4, .flags = 0x3d
};
struct sim_bpdu tcn;
int tc;
printf("53. a topology change heard on a port that does not forward yet is not passed on\n");
reset_all();
stp_pflags[2] &= ~STP_PF_AUTOEDGE;
links_set((1 << 0) | (1 << 2));
secs(1);
r = root_on(0);
for (int i = 0; i < 3; i++) {
bpdu_in(&r);
secs(2);
}
check_state(0, 3, "root port 1 forwards");
check_state(2, 1, "port 3 is still discarding");
memcpy(worse.root_mac, ROOT_MAC, 6);
memcpy(worse.br_mac, PEER_MAC, 6);
memset(flush_count, 0, sizeof(flush_count));
tc = stp_tc_count;
bpdu_in(&worse);
tick(2);
check(stp_tc_count == tc, "a TC flag on the discarding port raises no topology change");
check(flush_count[0] == 0, "and the root port keeps its addresses");
tcn = worse;
tcn.legacy = 2;
memset(last_tx, 0, sizeof(last_tx));
bpdu_in(&tcn);
tick(2);
check(stp_tc_count == tc, "a TCN on the discarding port raises none either");
check((last_tx[2].flags & 0x80) == 0, "and is not acknowledged");
for (int i = 0; i < 16; i++) {
bpdu_in(&r);
secs(2);
}
check_state(2, 3, "port 3 forwards after both forward delays");
for (int i = 0; i < 2; i++) {
bpdu_in(&r);
secs(2);
}
tc = stp_tc_count;
bpdu_in(&worse);
tick(2);
check(stp_tc_count == tc + 1 && flush_count[0] > 0, "now the same TC is passed on");
}
static void scen_root_port_needs_link(void)
{
struct sim_bpdu r;
printf("54. a BPDU credited to a port without carrier does not pick the root port\n");
reset_all();
links_set(1 << 2);
secs(1);
r = root_on(0);
bpdu_in(&r);
tick(2);
check(stp_root_port == 0xff, "the bridge stays its own root");
links_set((1 << 0) | (1 << 2));
secs(1);
check(stp_root_port == 0, "once the carrier is seen the port becomes the root port");
check_state(0, 3, "and forwards");
}
static void follow_root(unsigned seconds)
{
struct sim_bpdu root = root_on(8);
while (seconds--) {
bpdu_in(&root);
secs(1);
}
}
static struct sim_bpdu loop_from_port8(void)
{
struct sim_bpdu own = {
.port = 0, .root_prio = 0x40, .root_cost = 20000,
.br_prio = 0x80, .port_id = 8
};
memcpy(own.root_mac, ROOT_MAC, 6);
memcpy(own.br_mac, uip_ethaddr.addr, 6);
return own;
}
static void loop_ports_ready(void)
{
reset_all();
links_set((1 << 0) | (1 << 7) | (1 << 8));
follow_root(5);
}
static void scen_loop_peer_edge_recovery(void)
{
struct sim_bpdu own = loop_from_port8();
int held = 1;
printf("55. a silent loop peer regains auto-edge when the loop hold expires\n");
loop_ports_ready();
check((stp_pflags[0] & STP_PF_OPEREDGE) && (stp_pflags[7] & STP_PF_OPEREDGE),
"both host-facing ports start as edge");
bpdu_in(&own);
check_state(7, 1, "a frame from port 8 returning on port 1 blocks port 8");
check(!(stp_pflags[7] & STP_PF_OPEREDGE) && stp_cnt[STP_CNT_RX][7] == 0,
"the held peer is non-edge even though it received no BPDU itself");
for (int i = 0; i < 20; i++) {
follow_root(2);
held &= port_state(7) == 1 && !(stp_pflags[7] & STP_PF_OPEREDGE);
bpdu_in(&own);
}
check(held, "repeated returned BPDUs keep the peer blocked and non-edge");
follow_root(14);
check_state(7, 1, "the peer waits its full initial forward delay");
check(!(stp_pflags[7] & STP_PF_OPEREDGE), "auto-edge cannot bypass the loop hold");
follow_root(14);
check_state(7, 2, "the peer then spends another forward delay learning");
check(!(stp_pflags[7] & STP_PF_OPEREDGE), "the learning peer is still non-edge");
follow_root(3);
check_state(7, 3, "the peer forwards once both forward delays have elapsed");
check((stp_pflags[7] & STP_PF_OPEREDGE) && stp_cnt[STP_CNT_RX][7] == 0,
"the silent peer regains edge with its RX counter still zero");
check(!(stp_pflags[0] & STP_PF_OPEREDGE) && stp_cnt[STP_CNT_RX][0] == 21,
"only the port that actually received the returned BPDUs counts and remembers them");
bpdu_in(&own);
check_state(7, 1, "a new returned BPDU blocks the recovered peer again");
check(!(stp_pflags[7] & STP_PF_OPEREDGE), "the new loop revokes the recovered edge status");
}
static void scen_loop_peer_heard_bpdu(void)
{
struct sim_bpdu own = loop_from_port8();
struct sim_bpdu peer = {
.port = 7, .root_prio = 0x40, .root_cost = 200000,
.br_prio = 0x90, .port_id = 1, .flags = 0x0c
};
printf("56. a loop peer that received a real BPDU remains non-edge\n");
loop_ports_ready();
bpdu_in(&own);
memcpy(peer.root_mac, ROOT_MAC, 6);
memcpy(peer.br_mac, PEER_MAC, 6);
bpdu_in(&peer);
follow_root(31);
check_state(7, 3, "the loop hold still expires after the full forward delays");
check(!(stp_pflags[7] & STP_PF_OPEREDGE) && stp_cnt[STP_CNT_RX][7] == 1,
"a real received BPDU prevents the peer from becoming edge");
}
static void scen_loop_peer_autoedge_off(void)
{
struct sim_bpdu own = loop_from_port8();
printf("57. loop recovery respects administratively disabled auto-edge\n");
reset_all();
stp_pflags[7] &= ~STP_PF_AUTOEDGE;
links_set((1 << 0) | (1 << 7) | (1 << 8));
follow_root(31);
bpdu_in(&own);
follow_root(31);
check_state(7, 3, "the peer forwards again after loop recovery");
check(!(stp_pflags[7] & STP_PF_OPEREDGE) && stp_cnt[STP_CNT_RX][7] == 0,
"zero RX does not override the disabled auto-edge setting");
}
/* Actual captured RouterOS bytes; only the redacted MAC octets are replaced. */
static const uint8_t mikrotik_wire[] = {
0x01, 0x80, 0xc2, 0x00, 0x00, 0x00, 0xd0, 0xea,
0x11, 0x12, 0x34, 0x56, 0x00, 0x27, 0x42, 0x42,
0x03, 0x00, 0x00, 0x02, 0x02, 0x3c, 0x10, 0x00,
0xd0, 0xea, 0x11, 0x12, 0x34, 0x56, 0x00, 0x00,
0x00, 0x00, 0x10, 0x00, 0xd0, 0xea, 0x11, 0x12,
0x34, 0x56, 0x80, 0x0a, 0x00, 0x00, 0x14, 0x00,
0x02, 0x00, 0x0f, 0x00, 0x00
};
static void bpdu_wire(uint8_t port, int tagged)
{
size_t body = 20;
memset(uip_buf, 0, sizeof(uip_buf));
memcpy(uip_buf, mikrotik_wire, 12);
uip_buf[12] = 0x88;
uip_buf[13] = 0x99;
uip_buf[14] = 0x04;
uip_buf[19] = port;
if (tagged) {
uip_buf[20] = 0x81;
uip_buf[23] = 1;
body += 4;
}
memcpy(uip_buf + body, mikrotik_wire + 12, sizeof(mikrotik_wire) - 12);
uip_len = body + sizeof(mikrotik_wire) - 12;
}
static void scen_mikrotik_wire_capture(void)
{
int attributed = 1;
printf("58. the captured RouterOS BPDU is parsed on every physical ingress port\n");
for (int tagged = 0; tagged < 2; tagged++) {
for (uint8_t port = 0; port < NPORTS; port++) {
reset_all();
links_set((1 << NPORTS) - 1);
secs(5);
bpdu_wire(port, tagged);
/* The upper source-word bits and trap reason are not port bits. */
uip_buf[15] = 0x04;
uip_buf[18] = 0xfe;
uip_buf[19] |= 0xa0;
stp_in();
attributed &= stp_root_port == port && root_bridge.prio == 0x10
&& stp_dpid[port] == 0x800a;
for (uint8_t p = 0; p < NPORTS; p++)
attributed &= stp_cnt[STP_CNT_RX][p] == (p == port)
&& !!(stp_pflags[p] & STP_PF_OPEREDGE) == (p != port);
}
}
check(attributed, "both VLAN layouts select the captured root and change RX and edge status only on the ingress port");
}
static void scen_port_id_panel(void)
{
struct sim_bpdu own = {
.port = 0, .root_prio = 0x40, .root_cost = 20000,
.br_prio = 0x80, .port_id = 10
};
printf("66. the port ID is the front panel number, not the chip port\n");
reset_all();
links_set((1 << 0) | (1 << 6) | (1 << 8));
follow_root(5);
check(last_tx[6].port_id == 10, "chip port 6 sends front panel number 10");
memcpy(own.root_mac, ROOT_MAC, 6);
memcpy(own.br_mac, uip_ethaddr.addr, 6);
bpdu_in(&own);
check_state(6, 1, "its BPDU coming back on another port blocks chip port 6");
}
int main(int argc, char **argv)
{
verbose = argc > 1 && argv[1][0] == '-' ? (argv[1][1] == 'd' ? 2 : 1) : 0;
scen_edge_ports();
scen_root_port();
scen_ring();
scen_ring_clears();
scen_cheaper_path();
scen_we_are_better();
scen_alt_survives_link_bounce();
scen_speed_cost();
scen_root_times();
scen_too_old();
scen_info_expiry();
scen_dport_tiebreak();
scen_rxport_tiebreak();
scen_inferior_info();
scen_root_role_bpdu();
scen_alternate_takes_over();
scen_dbridge_tiebreak();
scen_times_revert();
scen_tc_via_root_port();
scen_tc_window_rstp();
scen_tc_propagation();
scen_tc_legacy_tcn();
scen_alt_forgets();
scen_edge_no_tc();
scen_link_loss_no_tc();
scen_proposal_agreement();
scen_no_agreement();
scen_sync_on_proposal();
scen_rapid_failover();
scen_rapid_reroot();
scen_agreement_other_root();
scen_repeated_proposal();
scen_dispute();
scen_learning_state();
scen_reply_to_inferior();
scen_backup_role();
scen_migrate_to_stp();
scen_migrate_back();
scen_legacy_root_tcn();
scen_alternate_agrees();
scen_agreement_from_alternate();
scen_timer_relation();
scen_recent_root();
scen_recent_backup();
scen_short_bpdu();
scen_port_roles();
scen_no_tx_without_link();
scen_counters();
scen_path_cost_method();
scen_bpdu_handling();
scen_last_tc();
scen_lag();
scen_tc_needs_forwarding();
scen_root_port_needs_link();
scen_loop_peer_edge_recovery();
scen_loop_peer_heard_bpdu();
scen_loop_peer_autoedge_off();
scen_mikrotik_wire_capture();
scen_port_id_panel();
if (failures) {
printf("\n%d check(s) failed\n", failures);
return 1;
}
printf("\nall scenarios passed\n");
return 0;
}