diff --git a/CMakeLists.txt b/CMakeLists.txt index bbabf46..879cbe2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,7 @@ add_library(libef STATIC src/ef-igmp.c src/ef-ipv4.c src/ef-ipv6.c + src/ef-llc.c src/ef-mld.c src/ef-mrp.c src/ef-oam.c @@ -82,6 +83,7 @@ add_library(libef STATIC src/ef-profinet.c src/ef-ptp.c src/ef-rate.c + src/ef-stp.c src/ef-sv.c src/ef-txring.c src/ef-udp.c @@ -127,6 +129,8 @@ add_executable(ef-tests test/test-mld-zero-width.cxx test/test-rate-limit.cxx test/test-rx-ign.cxx + test/test-llc.cxx + test/test-stp.cxx ) target_compile_options(ef-tests PRIVATE ${EF_SANITIZE_FLAGS}) diff --git a/fuzz/corpus-argc-frame/eth_llc_stp b/fuzz/corpus-argc-frame/eth_llc_stp new file mode 100644 index 0000000..71c4226 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_llc_stp differ diff --git a/fuzz/corpus-argc-frame/eth_llc_stp_rst b/fuzz/corpus-argc-frame/eth_llc_stp_rst new file mode 100644 index 0000000..b91a490 Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_llc_stp_rst differ diff --git a/fuzz/corpus-argc-frame/eth_llc_stp_tcn b/fuzz/corpus-argc-frame/eth_llc_stp_tcn new file mode 100644 index 0000000..b25bcab Binary files /dev/null and b/fuzz/corpus-argc-frame/eth_llc_stp_tcn differ diff --git a/fuzz/ef.dict b/fuzz/ef.dict index 65f33e8..f64004a 100644 --- a/fuzz/ef.dict +++ b/fuzz/ef.dict @@ -46,6 +46,14 @@ "mrp_topo" "mrp_tst" +# Headers LLC +"llc" + +# Headers STP +"stp" +"stp_rst" +"stp_tcn" + # Headers PTP "ptp-announce" "ptp-follow-up" @@ -168,6 +176,31 @@ "hdr-flagField" "hdr-logMessageInterval" +# Field names LLC +"dsap" +"ssap" +"ctrl" + +# Field names STP +"tc" +"proposal" +"role" +"learning" +"forwarding" +"agreement" +"tcack" +"root_prio" +"root_mac" +"root_cost" +"bridge_prio" +"bridge_mac" +"port" +"msg_age" +"max_age" +"hello" +"fwd_delay" +"v1_len" + # Field names OAM "opcode" "mel" diff --git a/src/ef-llc.c b/src/ef-llc.c new file mode 100644 index 0000000..dd5fd22 --- /dev/null +++ b/src/ef-llc.c @@ -0,0 +1,81 @@ +#include +#include "ef.h" + +// IEEE 802.2 Logical Link Control. An LLC header is preceded by a length and +// not by an EtherType, so we compute the 802.3 length and write it into the +// "et" field of the header below us -- eth, or the outermost VLAN tag, both +// of which carry that field last. +// +// This works because frame_to_buf() runs frame_fill_defaults from the top of +// the stack downwards: by the time eth_fill_defaults() calls +// ether_type_fill_defaults(), et->val is already set and is left alone. +static int llc_fill_defaults(struct frame *f, int stack_idx) { + char buf[16]; + int i, len = 0; + hdr_t *ll; + field_t *et; + + if (stack_idx < 1) + return 0; + + ll = f->stack[stack_idx - 1]; + et = find_field(ll, "et"); + + // An explicit "et" on the command line wins over the computed length. + if (!et || et->val) + return 0; + + for (i = stack_idx; i < f->stack_size; ++i) + len += f->stack[i]->size; + + // 802.3 clause 3.2.6: a type/length value above 1500 is not a length, + // and from 1536 up it is read as an EtherType. Refuse to emit one rather + // than silently write a frame the receiver will misparse. + if (len > 1500) { + pe("llc: 802.3 length %d exceeds 1500, set \"et\" explicitly\n", + len); + return -1; + } + + snprintf(buf, 16, "%d", len); + buf[15] = 0; + et->val = parse_bytes(buf, 2); + + return 0; +} + +static field_t LLC_FIELDS[] = { + { .name = "dsap", + .help = "Destination Service Access Point, e.g. 0x42 for STP", + .bit_width = 8 }, + { .name = "ssap", + .help = "Source Service Access Point, e.g. 0x42 for STP", + .bit_width = 8 }, + { .name = "ctrl", + .help = "Control, one byte for U-format frames, e.g. 0x03 for UI", + .bit_width = 8 }, +}; + +static hdr_t HDR_LLC = { + .name = "llc", + .help = "IEEE 802.2 LLC, sets the 802.3 length below it, e.g. llc stp", + .fields = LLC_FIELDS, + .fields_size = sizeof(LLC_FIELDS) / sizeof(LLC_FIELDS[0]), + .frame_fill_defaults = llc_fill_defaults, + .parser = hdr_parse_fields, +}; + +void llc_init() { + def_offset(&HDR_LLC); + def_val(&HDR_LLC, "dsap", "0x42"); + def_val(&HDR_LLC, "ssap", "0x42"); + def_val(&HDR_LLC, "ctrl", "0x03"); + + hdr_tmpls[HDR_TMPL_LLC] = &HDR_LLC; +} + +void llc_uninit() { + uninit_frame_data(&HDR_LLC); + + hdr_tmpls[HDR_TMPL_LLC] = 0; +} diff --git a/src/ef-stp.c b/src/ef-stp.c new file mode 100644 index 0000000..55bd8eb --- /dev/null +++ b/src/ef-stp.c @@ -0,0 +1,300 @@ +#include "ef.h" + +// IEEE 802.1D-1998 clause 9.3 Bridge Protocol Data Units. A BPDU rides on an +// 802.2 LLC header (DSAP/SSAP 0x42, control 0x03), so the usual invocation is +// "eth ... llc stp ...". +// +// The four timer fields are carried in units of 1/256 second on the wire and +// are exposed as the raw 16-bit wire value -- a test asserting on the exact +// octets must be able to write them. +// +// The flags octet is modelled both as a whole byte and as the eight single-bit +// fields of 802.1Q table 13-5. A configuration BPDU uses only the two outer +// ones; the six between them belong to the rapid protocol, which carries the +// proposal/agreement handshake and the sender's port role there. +// +// Configuration, RST and TCN are three separate header templates rather than +// one header with the trailing fields suppressed. The MRP precedent (four +// templates in ef-mrp.c) fits better here than the shrink-the-header trick of +// ef-sv.c and ef-mld.c: those shrink a *trailing optional* part of one +// message, whereas each of these is a different message. Suppressing 31 of 35 +// fields would also leave every "stp" field name valid on a TCN command line, +// silently ignored. +// +// The same reasoning extends to the RST BPDU, from the other direction. Its +// body is 36 octets: the configuration BPDU's 35 followed by the Version 1 +// Length, transmitted as 0 (802.1Q clause 14.4, which also sets the receive +// rule -- version 2, type 0x02 and 36 or more octets). Growing HDR_STP by a +// byte when "ver 2" is given is not available: a frame_fill_defaults hook can +// shrink a header, as ef-sv.c does, but frame_mask_to_buf() never runs those +// hooks, so the value buf and the mask buf would disagree on the header's +// size. Nor is a permanently 36-byte HDR_STP an option -- that would make +// every configuration BPDU malformed. + +static field_t STP_FIELDS[] = { + { .name = "proto", + .help = "Protocol identifier, 0x0000 for STP", + .bit_width = 16 }, + { .name = "ver", + .help = "Protocol version identifier, 0 for STP; for RSTP use stp_rst", + .bit_width = 8 }, + { .name = "type", + .help = "BPDU type, 0x00 for configuration", + .bit_width = 8 }, + { .name = "flags", + .help = "Flags, see the eight single-bit fields below", + .bit_width = 8 }, + { .name = "tc", + .help = "Topology change flag, bit 0 of flags", + .bit_width = 1, + .bit_offset = 39, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "proposal", + .help = "Proposal flag, bit 1 of flags", + .bit_width = 1, + .bit_offset = 38, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "role", + .help = "Port role, bits 2-3 of flags: 0 unknown, 1 alternate or " + "backup, 2 root, 3 designated", + .bit_width = 2, + .bit_offset = 36, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "learning", + .help = "Learning flag, bit 4 of flags", + .bit_width = 1, + .bit_offset = 35, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "forwarding", + .help = "Forwarding flag, bit 5 of flags", + .bit_width = 1, + .bit_offset = 34, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "agreement", + .help = "Agreement flag, bit 6 of flags", + .bit_width = 1, + .bit_offset = 33, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "tcack", + .help = "Topology change acknowledgement flag, bit 7 of flags", + .bit_width = 1, + .bit_offset = 32, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "root_prio", + .help = "Root identifier priority, e.g. 0x8000", + .bit_width = 16 }, + { .name = "root_mac", + .help = "Root identifier MAC address, e.g. 00:11:22:33:44:00", + .bit_width = 48 }, + { .name = "root_cost", + .help = "Root path cost", + .bit_width = 32 }, + { .name = "bridge_prio", + .help = "Bridge identifier priority, e.g. 0x8000", + .bit_width = 16 }, + { .name = "bridge_mac", + .help = "Bridge identifier MAC address, e.g. 00:11:22:33:44:00", + .bit_width = 48 }, + { .name = "port", + .help = "Port identifier, e.g. 0x8001", + .bit_width = 16 }, + { .name = "msg_age", + .help = "Message age in units of 1/256 second, e.g. 0x0100 for 1 s", + .bit_width = 16 }, + { .name = "max_age", + .help = "Max age in units of 1/256 second, e.g. 0x1400 for 20 s", + .bit_width = 16 }, + { .name = "hello", + .help = "Hello time in units of 1/256 second, e.g. 0x0200 for 2 s", + .bit_width = 16 }, + { .name = "fwd_delay", + .help = "Forward delay in units of 1/256 second, e.g. 0x0f00 for 15 s", + .bit_width = 16 }, +}; + +static hdr_t HDR_STP = { + .name = "stp", + .help = "STP configuration BPDU, e.g. llc stp root_prio 0x8000 tc 1", + .fields = STP_FIELDS, + .fields_size = sizeof(STP_FIELDS) / sizeof(STP_FIELDS[0]), + .parser = hdr_parse_fields, +}; + +static field_t STP_RST_FIELDS[] = { + { .name = "proto", + .help = "Protocol identifier, 0x0000 for STP", + .bit_width = 16 }, + { .name = "ver", + .help = "Protocol version identifier, 2 for RSTP", + .bit_width = 8 }, + { .name = "type", + .help = "BPDU type, 0x02 for an RST BPDU", + .bit_width = 8 }, + { .name = "flags", + .help = "Flags, see the eight single-bit fields below", + .bit_width = 8 }, + { .name = "tc", + .help = "Topology change flag, bit 0 of flags", + .bit_width = 1, + .bit_offset = 39, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "proposal", + .help = "Proposal flag, bit 1 of flags", + .bit_width = 1, + .bit_offset = 38, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "role", + .help = "Port role, bits 2-3 of flags: 0 unknown, 1 alternate or " + "backup, 2 root, 3 designated", + .bit_width = 2, + .bit_offset = 36, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "learning", + .help = "Learning flag, bit 4 of flags", + .bit_width = 1, + .bit_offset = 35, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "forwarding", + .help = "Forwarding flag, bit 5 of flags", + .bit_width = 1, + .bit_offset = 34, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "agreement", + .help = "Agreement flag, bit 6 of flags", + .bit_width = 1, + .bit_offset = 33, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "tcack", + .help = "Topology change acknowledgement flag, bit 7 of flags", + .bit_width = 1, + .bit_offset = 32, + .rx_match_skip = 1, + .overlay = 1 }, + { .name = "root_prio", + .help = "Root identifier priority, e.g. 0x8000", + .bit_width = 16 }, + { .name = "root_mac", + .help = "Root identifier MAC address, e.g. 00:11:22:33:44:00", + .bit_width = 48 }, + { .name = "root_cost", + .help = "Root path cost", + .bit_width = 32 }, + { .name = "bridge_prio", + .help = "Bridge identifier priority, e.g. 0x8000", + .bit_width = 16 }, + { .name = "bridge_mac", + .help = "Bridge identifier MAC address, e.g. 00:11:22:33:44:00", + .bit_width = 48 }, + { .name = "port", + .help = "Port identifier, e.g. 0x8001", + .bit_width = 16 }, + { .name = "msg_age", + .help = "Message age in units of 1/256 second, e.g. 0x0100 for 1 s", + .bit_width = 16 }, + { .name = "max_age", + .help = "Max age in units of 1/256 second, e.g. 0x1400 for 20 s", + .bit_width = 16 }, + { .name = "hello", + .help = "Hello time in units of 1/256 second, e.g. 0x0200 for 2 s", + .bit_width = 16 }, + { .name = "fwd_delay", + .help = "Forward delay in units of 1/256 second, e.g. 0x0f00 for 15 s", + .bit_width = 16 }, + { .name = "v1_len", + .help = "Version 1 length, transmitted as 0", + .bit_width = 8 }, +}; + +static hdr_t HDR_STP_RST = { + .name = "stp_rst", + .help = "RST BPDU, e.g. llc stp_rst role 3 proposal 1 forwarding 1", + .fields = STP_RST_FIELDS, + .fields_size = sizeof(STP_RST_FIELDS) / sizeof(STP_RST_FIELDS[0]), + .parser = hdr_parse_fields, +}; + +static field_t STP_TCN_FIELDS[] = { + { .name = "proto", + .help = "Protocol identifier, 0x0000 for STP", + .bit_width = 16 }, + { .name = "ver", + .help = "Protocol version identifier, 0 for STP", + .bit_width = 8 }, + { .name = "type", + .help = "BPDU type, 0x80 for topology change notification", + .bit_width = 8 }, +}; + +static hdr_t HDR_STP_TCN = { + .name = "stp_tcn", + .help = "STP topology change notification BPDU, e.g. llc stp_tcn", + .fields = STP_TCN_FIELDS, + .fields_size = sizeof(STP_TCN_FIELDS) / sizeof(STP_TCN_FIELDS[0]), + .parser = hdr_parse_fields, +}; + +void stp_init() { + // Defaults describe a root bridge advertising the IEEE 802.1D-1998 + // table 8-3 recommended timer values: max age 20 s, hello 2 s and + // forward delay 15 s, each scaled by 256. + def_offset(&HDR_STP); + def_val(&HDR_STP, "proto", "0x0000"); + def_val(&HDR_STP, "ver", "0x00"); + def_val(&HDR_STP, "type", "0x00"); + def_val(&HDR_STP, "flags", "0x00"); + def_val(&HDR_STP, "root_prio", "0x8000"); + def_val(&HDR_STP, "root_cost", "0"); + def_val(&HDR_STP, "bridge_prio", "0x8000"); + def_val(&HDR_STP, "port", "0x8001"); + def_val(&HDR_STP, "msg_age", "0x0000"); + def_val(&HDR_STP, "max_age", "0x1400"); + def_val(&HDR_STP, "hello", "0x0200"); + def_val(&HDR_STP, "fwd_delay", "0x0f00"); + + def_offset(&HDR_STP_RST); + def_val(&HDR_STP_RST, "proto", "0x0000"); + def_val(&HDR_STP_RST, "ver", "0x02"); + def_val(&HDR_STP_RST, "type", "0x02"); + def_val(&HDR_STP_RST, "flags", "0x00"); + def_val(&HDR_STP_RST, "root_prio", "0x8000"); + def_val(&HDR_STP_RST, "root_cost", "0"); + def_val(&HDR_STP_RST, "bridge_prio", "0x8000"); + def_val(&HDR_STP_RST, "port", "0x8001"); + def_val(&HDR_STP_RST, "msg_age", "0x0000"); + def_val(&HDR_STP_RST, "max_age", "0x1400"); + def_val(&HDR_STP_RST, "hello", "0x0200"); + def_val(&HDR_STP_RST, "fwd_delay", "0x0f00"); + def_val(&HDR_STP_RST, "v1_len", "0x00"); + + def_offset(&HDR_STP_TCN); + def_val(&HDR_STP_TCN, "proto", "0x0000"); + def_val(&HDR_STP_TCN, "ver", "0x00"); + def_val(&HDR_STP_TCN, "type", "0x80"); + + hdr_tmpls[HDR_TMPL_STP] = &HDR_STP; + hdr_tmpls[HDR_TMPL_STP_RST] = &HDR_STP_RST; + hdr_tmpls[HDR_TMPL_STP_TCN] = &HDR_STP_TCN; +} + +void stp_uninit() { + uninit_frame_data(&HDR_STP); + uninit_frame_data(&HDR_STP_RST); + uninit_frame_data(&HDR_STP_TCN); + + hdr_tmpls[HDR_TMPL_STP] = 0; + hdr_tmpls[HDR_TMPL_STP_RST] = 0; + hdr_tmpls[HDR_TMPL_STP_TCN] = 0; +} diff --git a/src/ef.c b/src/ef.c index 2d7060f..1642e30 100644 --- a/src/ef.c +++ b/src/ef.c @@ -349,6 +349,7 @@ int hdr_parse_fields(frame_t *frame, struct hdr *hdr, int offset, if (field_ignore) { field_ignore = 0; f->rx_match_skip = 1; + f->rx_match_ign = 1; i += 1; continue; } @@ -369,6 +370,7 @@ int hdr_parse_fields(frame_t *frame, struct hdr *hdr, int offset, if (strcmp(argv[i], "ign") == 0 || strcmp(argv[i], "ignore") == 0) { frame->has_mask = 1; f->rx_match_skip = 1; + f->rx_match_ign = 1; i += 1; continue; } @@ -397,6 +399,7 @@ int hdr_parse_fields(frame_t *frame, struct hdr *hdr, int offset, i += 1; } f->rx_match_skip = 0; + f->rx_match_ign = 0; } return i; @@ -447,7 +450,22 @@ static int hdr_copy_to_buf_(hdr_t *hdr, size_t offset, buf_t *buf, int mask) { } } - return i; + // An overlay field (a flag bit inside a flags byte) that was given "ign" + // must take its bits back out of the byte around it, which the loop + // above may have masked in. Only overlays do this: elsewhere, overlapping + // fields are alternative encodings and an ignored one must not erase the + // bits of another that is compared. + for (i = 0, f = hdr->fields; mask && i < hdr->fields_size; ++i, ++f) { + if (!f->overlay || !f->rx_match_ign) + continue; + + maskb = balloc(BIT_TO_BYTE(f->bit_width)); + hdr_write_field(buf, offset, f, maskb); + bfree(maskb); + maskb = 0; + } + + return hdr->fields_size; } int hdr_copy_to_buf_mask(hdr_t *hdr, size_t offset, buf_t *buf) { @@ -595,6 +613,8 @@ void profinet_init(); void opcua_init(); void coap_init(); void sv_init(); +void llc_init(); +void stp_init(); void init() __attribute__ ((constructor)); void init() { @@ -617,6 +637,8 @@ void init() { opcua_init(); coap_init(); sv_init(); + llc_init(); + stp_init(); } void ifh_uninit(); @@ -638,6 +660,8 @@ void profinet_uninit(); void opcua_uninit(); void coap_uninit(); void sv_uninit(); +void llc_uninit(); +void stp_uninit(); void uninit() __attribute__ ((destructor)); void uninit() { @@ -660,5 +684,7 @@ void uninit() { opcua_uninit(); coap_uninit(); sv_uninit(); + llc_uninit(); + stp_uninit(); } diff --git a/src/ef.h b/src/ef.h index cfbeb5a..5c8540c 100644 --- a/src/ef.h +++ b/src/ef.h @@ -163,6 +163,13 @@ typedef struct field { const char *name; const char *help; int rx_match_skip; + // Set when the field itself was given "ign". rx_match_skip alone only + // keeps a field from adding to the rx mask; an overlay field also has to + // take its bits back out of the field it sits inside. + int rx_match_ign; + // The field is a view of bits that another field of the header already + // covers, e.g. a flag bit inside a flags byte. See hdr_copy_to_buf_mask. + int overlay; int bit_width; int bit_offset; buf_t *def; @@ -307,6 +314,10 @@ typedef enum { HDR_TMPL_COAP_OPTIONS, HDR_TMPL_COAP_PARMS, HDR_TMPL_SV, + HDR_TMPL_LLC, + HDR_TMPL_STP, + HDR_TMPL_STP_RST, + HDR_TMPL_STP_TCN, HDR_TMPL_SIZE, } hdr_tmpl_t; diff --git a/test/test-llc.cxx b/test/test-llc.cxx new file mode 100644 index 0000000..c91a3ff --- /dev/null +++ b/test/test-llc.cxx @@ -0,0 +1,137 @@ +#include "ef.h" +#include "ef-test.h" +#include "catch_single_include.hxx" + +// An 802.2 LLC header is preceded by a length, not an EtherType. The tests +// below pin the length llc computes into the header below it, and the cases +// where it must keep its hands off that field. + +TEST_CASE("llc: 802.3 length counts llc plus everything above it", "[llc]") { + struct { + const char *what; + std::vector spec; + uint8_t hi, lo; + } cases[] = { + // llc(3) on its own + {"bare llc", {"eth", "llc"}, 0x00, 0x03}, + // llc(3) + 4 bytes of payload + {"llc + payload", {"eth", "llc", "data", "hex", "deadbeef"}, + 0x00, 0x07}, + // llc(3) + 46 bytes of payload, past the 60-byte pad boundary + {"llc + long payload", + {"eth", "llc", "data", "hex", + "000102030405060708090a0b0c0d0e0f" + "101112131415161718191a1b1c1d1e1f" + "202122232425262728292a2b2c2d"}, + 0x00, 0x31}, + }; + + for (auto &c : cases) { + SECTION(c.what) { + frame_t *f = parse_frame_wrap(c.spec); + REQUIRE(f != nullptr); + + buf_t *b = frame_to_buf(f); + REQUIRE(b != nullptr); + + // The length replaces the EtherType in the eth header, bytes 12:13. + CHECK(b->data[12] == c.hi); + CHECK(b->data[13] == c.lo); + + bfree(b); + frame_free(f); + } + } +} + +TEST_CASE("llc: default dsap/ssap/ctrl are the STP SAPs", "[llc]") { + frame_t *f = parse_frame_wrap({"eth", "llc"}); + REQUIRE(f != nullptr); + + buf_t *b = frame_to_buf(f); + REQUIRE(b != nullptr); + + CHECK(b->data[14] == 0x42); + CHECK(b->data[15] == 0x42); + CHECK(b->data[16] == 0x03); + + bfree(b); + frame_free(f); +} + +TEST_CASE("llc: an explicit et on the eth header wins", "[llc]") { + frame_t *f = parse_frame_wrap({"eth", "et", "0x1234", "llc"}); + REQUIRE(f != nullptr); + + buf_t *b = frame_to_buf(f); + REQUIRE(b != nullptr); + + // llc_fill_defaults must not overwrite a value the user asked for. + CHECK(b->data[12] == 0x12); + CHECK(b->data[13] == 0x34); + + bfree(b); + frame_free(f); +} + +TEST_CASE("llc: the length lands in the outermost vlan tag", "[llc]") { + frame_t *f = parse_frame_wrap({"eth", "ctag", "vid", "100", "llc", + "data", "hex", "deadbeef"}); + REQUIRE(f != nullptr); + + buf_t *b = frame_to_buf(f); + REQUIRE(b != nullptr); + + // eth.et becomes the ctag TPID, and the ctag's own "et" field at bytes + // 16:17 carries the 802.3 length of llc(3) + payload(4). + CHECK(b->data[12] == 0x81); + CHECK(b->data[13] == 0x00); + CHECK(b->data[16] == 0x00); + CHECK(b->data[17] == 0x07); + + bfree(b); + frame_free(f); +} + +TEST_CASE("llc: a length past 1500 is refused, not misencoded", "[llc]") { + // 1497 bytes of payload is the most an llc(3) frame can carry. One more + // would put 1501 in the length field, and 1536 or more reads as an + // EtherType; 65538 does not even fit in the field. + frame_t *max = parse_frame_wrap({"eth", "llc", "data", "repeat", "1497", + "0"}); + frame_t *over = parse_frame_wrap({"eth", "llc", "data", "repeat", "1498", + "0"}); + frame_t *wrap = parse_frame_wrap({"eth", "llc", "data", "repeat", + "65535", "0"}); + REQUIRE(max != nullptr); + REQUIRE(over != nullptr); + REQUIRE(wrap != nullptr); + + buf_t *b = frame_to_buf(max); + REQUIRE(b != nullptr); + CHECK(b->data[12] == 0x05); + CHECK(b->data[13] == 0xdc); + bfree(b); + + CHECK(frame_to_buf(over) == nullptr); + CHECK(frame_to_buf(wrap) == nullptr); + + frame_free(max); + frame_free(over); + frame_free(wrap); +} + +TEST_CASE("llc: an explicit et lifts the length limit", "[llc]") { + // A test that wants the malformed frame on purpose can still have it. + frame_t *f = parse_frame_wrap({"eth", "et", "0x0602", "llc", + "data", "repeat", "1500", "0"}); + REQUIRE(f != nullptr); + + buf_t *b = frame_to_buf(f); + REQUIRE(b != nullptr); + CHECK(b->data[12] == 0x06); + CHECK(b->data[13] == 0x02); + + bfree(b); + frame_free(f); +} diff --git a/test/test-stp.cxx b/test/test-stp.cxx new file mode 100644 index 0000000..bb07ce0 --- /dev/null +++ b/test/test-stp.cxx @@ -0,0 +1,513 @@ +#include "ef.h" +#include "ef-test.h" +#include "catch_single_include.hxx" + +#include + +// IEEE 802.1D-1998 clause 9.3 BPDUs. A BPDU is not an Ethernet II frame: the +// type/length field carries a length, followed by an 802.2 LLC header and the +// BPDU body. These tests pin the byte layout of both BPDU types. + +// RAII guard to set NO_PAD for the duration of a scope, as in +// test-padding.cxx. +struct NoPadGuard { + NoPadGuard() { NO_PAD = 1; } + ~NoPadGuard() { NO_PAD = 0; } +}; + +// ef pads short frames to 60 bytes. Append the zeros rather than spelling them +// out, so the expectation says "this body, then padding" and cannot drift. +static std::string padded(const std::string &body) { + size_t bytes = body.size() / 2; + REQUIRE(body.size() % 2 == 0); + REQUIRE(bytes <= 60); + return body + std::string((60 - bytes) * 2, '0'); +} + +// The frame a real bridge emits, byte for byte. +static const char *bpdu_config_body = + "0180c20000000011223344550026" // eth, 802.3 length 38 + "424203" // llc dsap/ssap/ctrl + "0000" "00" "00" "00" // proto, ver, type, flags + "8000" "001122334400" // root id + "00000000" // root path cost + "8000" "001122334400" // bridge id + "8001" // port id + "0000" "1400" "0200" "0f00"; // msg age, max age, hello, fwd + +TEST_CASE("stp: config BPDU byte layout matches a real bridge", "[stp]") { + frame_t *f = parse_frame_wrap({ + "eth", "dmac", "01:80:c2:00:00:00", "smac", "00:11:22:33:44:55", + "llc", + "stp", "root_mac", "00:11:22:33:44:00", + "bridge_mac", "00:11:22:33:44:00", + }); + REQUIRE(f != nullptr); + + // hexstr() frees the buf it is handed. + CHECK(hexstr(frame_to_buf(f)) == padded(bpdu_config_body)); + + frame_free(f); +} + +TEST_CASE("stp: every config field can be set explicitly", "[stp]") { + frame_t *f = parse_frame_wrap({ + "eth", "dmac", "01:80:c2:00:00:00", "smac", "00:11:22:33:44:55", + "llc", "dsap", "0x42", "ssap", "0x42", "ctrl", "0x03", + "stp", "proto", "0x0000", "ver", "0", "type", "0", "flags", "0", + "root_prio", "0x8000", "root_mac", "00:11:22:33:44:00", + "root_cost", "0", + "bridge_prio", "0x8000", "bridge_mac", "00:11:22:33:44:00", + "port", "0x8001", + "msg_age", "0x0000", "max_age", "0x1400", + "hello", "0x0200", "fwd_delay", "0x0f00", + }); + REQUIRE(f != nullptr); + + // Spelling out every field must land on the same bytes as the defaults. + CHECK(hexstr(frame_to_buf(f)) == padded(bpdu_config_body)); + + frame_free(f); +} + +TEST_CASE("stp: tc and tcack are bit 0 and bit 7 of flags", "[stp]") { + frame_t *tc = parse_frame_wrap({"eth", "llc", "stp", "tc", "1"}); + frame_t *ack = parse_frame_wrap({"eth", "llc", "stp", "tcack", "1"}); + frame_t *both = parse_frame_wrap({"eth", "llc", "stp", "flags", "0x81"}); + REQUIRE(tc != nullptr); + REQUIRE(ack != nullptr); + REQUIRE(both != nullptr); + + buf_t *b_tc = frame_to_buf(tc); + buf_t *b_ack = frame_to_buf(ack); + buf_t *b_both = frame_to_buf(both); + REQUIRE(b_tc != nullptr); + REQUIRE(b_ack != nullptr); + REQUIRE(b_both != nullptr); + + // flags sits at eth(14) + llc(3) + proto/ver/type(4) = offset 21. + CHECK(b_tc->data[21] == 0x01); + CHECK(b_ack->data[21] == 0x80); + CHECK(b_both->data[21] == 0x81); + + bfree(b_tc); + bfree(b_ack); + bfree(b_both); + frame_free(tc); + frame_free(ack); + frame_free(both); +} + +// The six bits between tc and tcack, which only the rapid protocol sets. +// 802.1Q table 13-5, read against the flags octet at offset 21. +TEST_CASE("stp: every flag bit lands in its own bit of the flags octet", + "[stp]") { + struct { const char *field; const char *value; uint8_t expect; } cases[] = { + { "tc", "1", 0x01 }, + { "proposal", "1", 0x02 }, + { "role", "1", 0x04 }, // alternate or backup + { "role", "2", 0x08 }, // root + { "role", "3", 0x0c }, // designated + { "learning", "1", 0x10 }, + { "forwarding", "1", 0x20 }, + { "agreement", "1", 0x40 }, + { "tcack", "1", 0x80 }, + }; + + for (auto &c : cases) { + frame_t *f = parse_frame_wrap({"eth", "llc", "stp", c.field, c.value}); + REQUIRE(f != nullptr); + + buf_t *b = frame_to_buf(f); + REQUIRE(b != nullptr); + + // Only the named bit may be set, so the whole octet is the expectation. + INFO("stp " << c.field << " " << c.value); + CHECK(b->data[21] == c.expect); + + bfree(b); + frame_free(f); + } +} + +TEST_CASE("stp: a designated port proposing sets four flag bits", "[stp]") { + // What a designated port offers at the start of the clause 13.35 + // handshake: proposal, role designated, learning and forwarding. + frame_t *named = parse_frame_wrap({ + "eth", "llc", + "stp", "proposal", "1", "role", "3", + "learning", "1", "forwarding", "1", + }); + frame_t *byte = parse_frame_wrap({"eth", "llc", "stp", "flags", "0x3e"}); + REQUIRE(named != nullptr); + REQUIRE(byte != nullptr); + + buf_t *b_named = frame_to_buf(named); + buf_t *b_byte = frame_to_buf(byte); + REQUIRE(b_named != nullptr); + REQUIRE(b_byte != nullptr); + + // Naming the bits and spelling out the byte must agree. + CHECK(b_named->data[21] == 0x3e); + CHECK(bequal(b_named, b_byte) == 1); + + bfree(b_named); + bfree(b_byte); + frame_free(named); + frame_free(byte); +} + +TEST_CASE("stp: an unnamed flag bit stays out of the rx mask", "[stp]") { + // Each bit alias overlays a byte that is itself a field, so an alias + // carries rx_match_skip and only enters the mask when it is named. + // Without that, "flags ign" could not narrow the byte at all. + frame_t *one = parse_frame_wrap({ + "eth", "llc", "stp", "flags", "ign", "agreement", "1", + }); + frame_t *two = parse_frame_wrap({ + "eth", "llc", "stp", "flags", "ign", "agreement", "1", "role", "3", + }); + REQUIRE(one != nullptr); + REQUIRE(two != nullptr); + + buf_t *m_one = frame_mask_to_buf(one); + buf_t *m_two = frame_mask_to_buf(two); + REQUIRE(m_one != nullptr); + REQUIRE(m_two != nullptr); + + CHECK(m_one->data[21] == 0x40); // agreement alone + CHECK(m_two->data[21] == 0x4c); // agreement and the two role bits + + bfree(m_one); + bfree(m_two); + frame_free(one); + frame_free(two); +} + +TEST_CASE("stp: ignoring a flag bit takes it out of the rx mask", "[stp]") { + // "flags" is itself in the mask unless ignored, so an ignored bit alias + // has to clear its own bits from it rather than merely add nothing. + struct { const char *field; uint8_t expect; } cases[] = { + { "tc", 0xfe }, + { "proposal", 0xfd }, + { "role", 0xf3 }, + { "learning", 0xef }, + { "forwarding", 0xdf }, + { "agreement", 0xbf }, + { "tcack", 0x7f }, + }; + + for (const char *hdr : {"stp", "stp_rst"}) { + for (auto &c : cases) { + frame_t *f = parse_frame_wrap({"eth", "llc", hdr, c.field, "ign"}); + REQUIRE(f != nullptr); + CHECK(f->has_mask == 1); + + buf_t *mask = frame_mask_to_buf(f); + REQUIRE(mask != nullptr); + + INFO(hdr << " " << c.field << " ign"); + CHECK(mask->data[21] == c.expect); + // The rest of the BPDU is still compared. + CHECK(mask->data[20] == 0xff); + CHECK(mask->data[22] == 0xff); + + bfree(mask); + frame_free(f); + } + } +} + +TEST_CASE("stp: a BPDU differing only in an ignored bit still matches", + "[stp]") { + // "tc ign" must accept a BPDU whatever its tc bit, which it cannot do if + // the flags byte around it is still compared in full. + frame_t *expect = parse_frame_wrap({"eth", "llc", "stp", "tc", "ign"}); + frame_t *rx_tc = parse_frame_wrap({"eth", "llc", "stp", "tc", "1"}); + frame_t *rx_ack = parse_frame_wrap({"eth", "llc", "stp", "tcack", "1"}); + REQUIRE(expect != nullptr); + REQUIRE(rx_tc != nullptr); + REQUIRE(rx_ack != nullptr); + + buf_t *b_expect = frame_to_buf(expect); + buf_t *mask = frame_mask_to_buf(expect); + buf_t *b_tc = frame_to_buf(rx_tc); + buf_t *b_ack = frame_to_buf(rx_ack); + REQUIRE(b_expect != nullptr); + REQUIRE(mask != nullptr); + REQUIRE(b_tc != nullptr); + REQUIRE(b_ack != nullptr); + + CHECK(bequal_mask(b_tc, b_expect, mask, 0) == 1); + // Ignoring tc must not loosen the other seven bits. + CHECK(bequal_mask(b_ack, b_expect, mask, 0) == 0); + + bfree(b_expect); + bfree(mask); + bfree(b_tc); + bfree(b_ack); + frame_free(expect); + frame_free(rx_tc); + frame_free(rx_ack); +} + +TEST_CASE("stp: an ignored bit clears the mask whatever the field order", + "[stp]") { + // flags comes before tc in the field list, but the bit must come out of + // an explicitly-set flags byte however the command line orders them. + frame_t *a = parse_frame_wrap({"eth", "llc", "stp", "flags", "0x81", + "tc", "ign"}); + frame_t *b = parse_frame_wrap({"eth", "llc", "stp", "tc", "ign", + "flags", "0x81"}); + REQUIRE(a != nullptr); + REQUIRE(b != nullptr); + + buf_t *m_a = frame_mask_to_buf(a); + buf_t *m_b = frame_mask_to_buf(b); + REQUIRE(m_a != nullptr); + REQUIRE(m_b != nullptr); + + CHECK(m_a->data[21] == 0xfe); + CHECK(m_b->data[21] == 0xfe); + + bfree(m_a); + bfree(m_b); + frame_free(a); + frame_free(b); +} + +TEST_CASE("stp: ignoring an overlapping non-overlay field changes nothing", + "[stp]") { + // IFH fields overlap as alternative encodings of the same bits, not as + // views of a wider field. Ignoring one there must not erase the mask of + // another that is compared: b1-rew-op stays compared in full. + frame_t *f = parse_frame_wrap({"ifh-oc1", "ign", "b1-rew-op", "0", + "b0-masq-port", "ign", "eth"}); + REQUIRE(f != nullptr); + + buf_t *mask = frame_mask_to_buf(f); + REQUIRE(mask != nullptr); + + CHECK(mask->data[0] == 0x3f); + CHECK(mask->data[1] == 0xe0); + + bfree(mask); + frame_free(f); +} + +TEST_CASE("stp: TCN BPDU is a four-byte body", "[stp]") { + frame_t *f = parse_frame_wrap({ + "eth", "dmac", "01:80:c2:00:00:00", "smac", "00:11:22:33:44:55", + "llc", "stp_tcn", + }); + REQUIRE(f != nullptr); + + CHECK(hexstr(frame_to_buf(f)) == + padded("0180c20000000011223344550007" // eth, 802.3 length 7 + "424203" // llc + "00000080")); // proto, ver, type=TCN + + frame_free(f); +} + +// An RST BPDU, 802.1Q clause 14.4: version 2, type 0x02, and a 36-octet body +// -- the configuration BPDU's 35 plus the Version 1 Length, which is 0. +static const char *bpdu_rst_body = + "0180c20000000011223344550027" // eth, 802.3 length 39 + "424203" // llc dsap/ssap/ctrl + "0000" "02" "02" "00" // proto, ver=2, type=RST, flags + "8000" "001122334400" // root id + "00000000" // root path cost + "8000" "001122334400" // bridge id + "8001" // port id + "0000" "1400" "0200" "0f00" // msg age, max age, hello, fwd + "00"; // version 1 length + +TEST_CASE("stp: RST BPDU body is 36 octets, one more than a config BPDU", + "[stp]") { + frame_t *rst = parse_frame_wrap({ + "eth", "dmac", "01:80:c2:00:00:00", "smac", "00:11:22:33:44:55", + "llc", + "stp_rst", "root_mac", "00:11:22:33:44:00", + "bridge_mac", "00:11:22:33:44:00", + }); + REQUIRE(rst != nullptr); + + CHECK(hexstr(frame_to_buf(rst)) == padded(bpdu_rst_body)); + + frame_free(rst); +} + +TEST_CASE("stp: the RST body is one octet longer than the config body", + "[stp]") { + // Padding would hide the difference, and so would comparing against a + // hand-written expectation: measure both bodies on the same frame stack. + NoPadGuard no_pad; + + frame_t *rst = parse_frame_wrap({"eth", "llc", "stp_rst"}); + frame_t *cfg = parse_frame_wrap({"eth", "llc", "stp"}); + REQUIRE(rst != nullptr); + REQUIRE(cfg != nullptr); + + buf_t *b_rst = frame_to_buf(rst); + buf_t *b_cfg = frame_to_buf(cfg); + REQUIRE(b_rst != nullptr); + REQUIRE(b_cfg != nullptr); + + // eth(14) + llc(3) + body. + CHECK(b_cfg->size - 17 == 35); + CHECK(b_rst->size - 17 == 36); + + // The 802.3 length counts the LLC header plus the body, so it moves too. + CHECK(b_cfg->data[12] == 0x00); + CHECK(b_cfg->data[13] == 0x26); + CHECK(b_rst->data[12] == 0x00); + CHECK(b_rst->data[13] == 0x27); + + bfree(b_rst); + bfree(b_cfg); + frame_free(rst); + frame_free(cfg); +} + +TEST_CASE("stp: every RST field can be set explicitly", "[stp]") { + frame_t *f = parse_frame_wrap({ + "eth", "dmac", "01:80:c2:00:00:00", "smac", "00:11:22:33:44:55", + "llc", "dsap", "0x42", "ssap", "0x42", "ctrl", "0x03", + "stp_rst", "proto", "0x0000", "ver", "2", "type", "0x02", "flags", "0", + "root_prio", "0x8000", "root_mac", "00:11:22:33:44:00", + "root_cost", "0", + "bridge_prio", "0x8000", "bridge_mac", "00:11:22:33:44:00", + "port", "0x8001", + "msg_age", "0x0000", "max_age", "0x1400", + "hello", "0x0200", "fwd_delay", "0x0f00", + "v1_len", "0", + }); + REQUIRE(f != nullptr); + + // Spelling out every field must land on the same bytes as the defaults. + CHECK(hexstr(frame_to_buf(f)) == padded(bpdu_rst_body)); + + frame_free(f); +} + +TEST_CASE("stp: RST flags sit in the same octet as a config BPDU's", "[stp]") { + // The clause 13.35 handshake on the wire. flags is at eth(14) + llc(3) + + // proto/ver/type(4) = offset 21, unmoved by the extra trailing octet. + frame_t *propose = parse_frame_wrap({ + "eth", "llc", + "stp_rst", "proposal", "1", "role", "3", + "learning", "1", "forwarding", "1", + }); + frame_t *agree = parse_frame_wrap({ + "eth", "llc", "stp_rst", "agreement", "1", "role", "2", + }); + REQUIRE(propose != nullptr); + REQUIRE(agree != nullptr); + + buf_t *b_propose = frame_to_buf(propose); + buf_t *b_agree = frame_to_buf(agree); + REQUIRE(b_propose != nullptr); + REQUIRE(b_agree != nullptr); + + // A designated port proposing, learning and forwarding. + CHECK(b_propose->data[21] == 0x3e); + // A root port agreeing. + CHECK(b_agree->data[21] == 0x48); + + bfree(b_propose); + bfree(b_agree); + frame_free(propose); + frame_free(agree); +} + +TEST_CASE("stp: a config BPDU can be matched field by field", "[stp]") { + // What an "rx" command builds: a value buf and a mask with every + // specified field set to all-ones. Mirrors ef-args.c:355-362. + frame_t *exact = parse_frame_wrap({ + "eth", "dmac", "01:80:c2:00:00:00", "smac", "00:11:22:33:44:55", + "llc", "stp", "tc", "1", + }); + frame_t *masked = parse_frame_wrap({ + "eth", "dmac", "01:80:c2:00:00:00", "smac", "00:11:22:33:44:55", + "llc", "stp", "flags", "ign", "tc", "1", + }); + REQUIRE(exact != nullptr); + REQUIRE(masked != nullptr); + CHECK(masked->has_mask == 1); + + buf_t *b_exact = frame_to_buf(exact); + buf_t *b_masked = frame_to_buf(masked); + buf_t *mask = frame_mask_to_buf(masked); + REQUIRE(b_exact != nullptr); + REQUIRE(b_masked != nullptr); + REQUIRE(mask != nullptr); + + // "flags ign tc 1" must still pin tc, so only bit 0 of the flags byte + // is compared and the received value must have it set. The other seven + // bit aliases carry rx_match_skip, so an unnamed one adds nothing. + CHECK(mask->data[21] == 0x01); + CHECK(bequal_mask(b_exact, b_masked, mask, 0) == 1); + + bfree(b_exact); + bfree(b_masked); + bfree(mask); + frame_free(exact); + frame_free(masked); +} + +TEST_CASE("stp: an RST BPDU can be matched field by field", "[stp]") { + // The same as the config BPDU case, on the 36-octet template, where the + // mask must reach the trailing v1_len octet as well. + frame_t *exact = parse_frame_wrap({ + "eth", "dmac", "01:80:c2:00:00:00", "smac", "00:11:22:33:44:55", + "llc", "stp_rst", "agreement", "1", "role", "2", + }); + frame_t *masked = parse_frame_wrap({ + "eth", "dmac", "01:80:c2:00:00:00", "smac", "00:11:22:33:44:55", + "llc", "stp_rst", "flags", "ign", "agreement", "1", + }); + frame_t *other = parse_frame_wrap({ + "eth", "dmac", "01:80:c2:00:00:00", "smac", "00:11:22:33:44:55", + "llc", "stp_rst", "role", "2", + }); + REQUIRE(exact != nullptr); + REQUIRE(masked != nullptr); + REQUIRE(other != nullptr); + CHECK(masked->has_mask == 1); + + buf_t *b_exact = frame_to_buf(exact); + buf_t *b_masked = frame_to_buf(masked); + buf_t *b_other = frame_to_buf(other); + buf_t *mask = frame_mask_to_buf(masked); + REQUIRE(b_exact != nullptr); + REQUIRE(b_masked != nullptr); + REQUIRE(b_other != nullptr); + REQUIRE(mask != nullptr); + + // Padding would fill a short mask out to 60 bytes with ones, so only an + // unpadded mask shows that it really ends at v1_len, the last octet of + // eth(14) + llc(3) + 36, and that v1_len is compared. + { + NoPadGuard no_pad; + buf_t *unpadded = frame_mask_to_buf(masked); + REQUIRE(unpadded != nullptr); + CHECK(unpadded->size == 53); + CHECK(unpadded->data[52] == 0xff); + bfree(unpadded); + } + + // Only agreement is compared in the flags octet, so a root port + // agreeing matches and one that does not agree is rejected. + CHECK(mask->data[21] == 0x40); + CHECK(bequal_mask(b_exact, b_masked, mask, 0) == 1); + CHECK(bequal_mask(b_other, b_masked, mask, 0) == 0); + + bfree(b_exact); + bfree(b_masked); + bfree(b_other); + bfree(mask); + frame_free(exact); + frame_free(masked); + frame_free(other); +}