Skip to content

Commit c0eb3c5

Browse files
committed
feat: add 802.1Q VLAN tag support to ethernet, improve IPv6 address compression
1 parent 8a09187 commit c0eb3c5

3 files changed

Lines changed: 127 additions & 8 deletions

File tree

ipparse/l2/ethernet.moon

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,46 @@
2828
:bidirectional = require"ipparse.fun"
2929
:format, pack: sp, unpack: su = require "ipparse.lib.pack_compat"
3030
{:need_bytes} = require "ipparse"
31+
{:band} = require "ipparse.lib.bit_compat"
3132
unpack or= table.unpack
3233

34+
--- EtherType for IEEE 802.1Q VLAN-tagged frames.
35+
ETH_P_8021Q = 0x8100
36+
3337
--- Packs the Ethernet frame fields into a binary string.
34-
-- Constructs the binary representation of the Ethernet frame, including destination MAC, source MAC, EtherType, and optional payload data.
38+
-- If the `vlan` field is set and non-zero, inserts a 4-byte 802.1Q tag (TPID=0x8100, TCI=VID)
39+
-- between source MAC and EtherType. Otherwise produces a plain Ethernet frame.
3540
-- @tparam table self The Ethernet frame object.
3641
-- @treturn string The packed Ethernet frame as a binary string.
37-
pack = => sp("c6 c6 >H", @dst, @src, @protocol) .. "#{@data or ''}"
42+
pack = =>
43+
if @vlan and @vlan != 0
44+
sp("c6 c6 >HHH", @dst, @src, ETH_P_8021Q, @vlan, @protocol) .. "#{@data or ''}"
45+
else
46+
sp("c6 c6 >H", @dst, @src, @protocol) .. "#{@data or ''}"
3847

3948
_mt =
4049
--- Converts the Ethernet frame object to a binary string.
4150
-- @treturn string Binary string representing the Ethernet frame.
4251
__tostring: pack
4352

4453
--- Parses an Ethernet frame header from a data string.
45-
-- Extracts the destination MAC, source MAC, EtherType, and calculates offsets for the payload.
54+
-- Transparently handles 802.1Q VLAN-tagged frames: if EtherType is 0x8100, the 4-byte tag is
55+
-- consumed and the `vlan` field (VID, 12-bit) is set on the result; `protocol` reflects the
56+
-- inner EtherType and `data_off` points past the full header (18 bytes instead of 14).
4657
-- @tparam string self The binary string containing the Ethernet frame.
4758
-- @tparam[opt=1] number off Offset in the data string to start parsing from. Defaults to 1.
48-
-- @treturn table A table containing the Ethernet header fields: `dst` (destination MAC), `src` (source MAC), `protocol` (EtherType), `off` (input offset), `data_off` (offset after header).
49-
-- @treturn number The offset after the Ethernet header (data_off).
59+
-- @treturn table Fields: `dst`, `src`, `protocol` (inner EtherType), `vlan` (nil if untagged),
60+
-- `off` (input offset), `data_off` (offset of payload).
61+
-- @treturn number The offset after the header.
5062
parse = (off=1) =>
5163
return nil, off unless need_bytes @, off, 14
5264
dst, src, protocol, data_off = su "c6 c6 >H", @, off
53-
setmetatable({:dst, :src, :protocol, :off, :data_off}, _mt), data_off
65+
vlan = nil
66+
if protocol == ETH_P_8021Q
67+
return nil, off unless need_bytes @, data_off, 4
68+
tci, protocol, data_off = su ">HH", @, data_off
69+
vlan = band tci, 0xFFF
70+
setmetatable({:dst, :src, :protocol, :vlan, :off, :data_off}, _mt), data_off
5471

5572
--- Creates a new instance of the Ethernet frame object and sets its metatable.
5673
-- @tparam table self The Ethernet frame object.
@@ -81,4 +98,4 @@ proto =
8198
IP4: 0x800
8299
proto = bidirectional proto
83100

84-
:parse, :new, :pack, :proto, :mac2s, :s2mac
101+
:parse, :new, :pack, :proto, :mac2s, :s2mac, :ETH_P_8021Q

ipparse/l3/ip6.moon

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,61 @@ parse_ip6 = =>
118118
address
119119

120120
--- Converts a binary IPv6 address to a readable string.
121+
-- Compresses consecutive zero groups with :: according to RFC 5952.
121122
-- @tparam string self The binary IPv6 address.
122123
-- @treturn string IPv6 address as a string in the format "xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx".
123124
ip62s = =>
124-
format "%x:%x:%x:%x:%x:%x:%x:%x", su ">HHHH HHHH", @
125+
parts = { su ">HHHH HHHH", @ }
126+
127+
-- Find the longest run of consecutive zeros
128+
max_zero_start = 1
129+
max_zero_len = 0
130+
zero_start = 1
131+
zero_len = 0
132+
133+
for i = 1, 8
134+
if parts[i] == 0
135+
if zero_len == 0
136+
zero_start = i
137+
zero_len += 1
138+
else
139+
if zero_len > max_zero_len
140+
max_zero_start = zero_start
141+
max_zero_len = zero_len
142+
zero_len = 0
143+
144+
-- Check if the last run is the longest
145+
if zero_len > max_zero_len
146+
max_zero_start = zero_start
147+
max_zero_len = zero_len
148+
149+
-- Only compress if we have at least 2 consecutive zeros
150+
if max_zero_len >= 2
151+
-- Build the compressed address in two parts
152+
before = {}
153+
after = {}
154+
155+
for i = 1, 8
156+
if i < max_zero_start
157+
table.insert before, format "%x", parts[i]
158+
elseif i >= max_zero_start + max_zero_len
159+
table.insert after, format "%x", parts[i]
160+
161+
before_str = table.concat before, ":"
162+
after_str = table.concat after, ":"
163+
164+
-- Handle edge cases
165+
if #before == 0 and #after == 0
166+
return "::"
167+
elseif #before == 0
168+
return "::" .. after_str
169+
elseif #after == 0
170+
return before_str .. "::"
171+
else
172+
return before_str .. "::" .. after_str
173+
else
174+
-- No compression needed
175+
format "%x:%x:%x:%x:%x:%x:%x:%x", unpack parts
125176

126177
--- Converts a readable IPv6 address string to binary format.
127178
-- @tparam string self The IPv6 address as a string in the format "xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx".

ipparse/tests/l2/test_ethernet.moon

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,55 @@ test "new + tostring round-trip", ->
6363
assert parsed.dst == "\xaa\xbb\xcc\xdd\xee\xff", "round-trip dst mismatch"
6464
assert parsed.src == "\x00\x11\x22\x33\x44\x55", "round-trip src mismatch"
6565
assert parsed.protocol == 0x0800, "round-trip protocol mismatch"
66+
67+
-- Build an 18-byte 802.1Q-tagged frame: same MACs, VLAN 6, inner proto=0x0800
68+
eth_vlan_raw = sp("c6c6>HHH", "\xaa\xbb\xcc\xdd\xee\xff", "\x00\x11\x22\x33\x44\x55", 0x8100, 6, 0x0800)
69+
70+
test "parse detects 802.1Q tag and extracts vlan", ->
71+
frame, next_off = eth.parse eth_vlan_raw, 1
72+
assert frame.vlan == 6, "vlan should be 6, got #{frame.vlan}"
73+
74+
test "parse 802.1Q: inner protocol is correct", ->
75+
frame, _ = eth.parse eth_vlan_raw, 1
76+
assert frame.protocol == 0x0800, "inner protocol should be 0x0800, got #{frame.protocol}"
77+
78+
test "parse 802.1Q: data_off is 19 (18-byte header + 1-based)", ->
79+
frame, next_off = eth.parse eth_vlan_raw, 1
80+
assert frame.data_off == 19, "data_off should be 19, got #{frame.data_off}"
81+
assert next_off == 19, "next_off should be 19, got #{next_off}"
82+
83+
test "parse untagged frame: vlan is nil", ->
84+
frame, _ = eth.parse eth_raw, 1
85+
assert frame.vlan == nil, "vlan should be nil for untagged frame, got #{frame.vlan}"
86+
87+
test "new with vlan: tostring produces 802.1Q frame", ->
88+
frame = eth.new {
89+
dst: "\xaa\xbb\xcc\xdd\xee\xff"
90+
src: "\x00\x11\x22\x33\x44\x55"
91+
protocol: 0x0800
92+
vlan: 6
93+
}
94+
assert tostring(frame) == eth_vlan_raw, "VLAN-tagged frame bytes mismatch"
95+
96+
test "new with vlan=0: tostring produces plain frame (no tag)", ->
97+
frame = eth.new {
98+
dst: "\xaa\xbb\xcc\xdd\xee\xff"
99+
src: "\x00\x11\x22\x33\x44\x55"
100+
protocol: 0x0800
101+
vlan: 0
102+
}
103+
assert tostring(frame) == eth_raw, "vlan=0 should produce plain frame"
104+
105+
test "new + tostring round-trip with vlan", ->
106+
frame = eth.new {
107+
dst: "\xaa\xbb\xcc\xdd\xee\xff"
108+
src: "\x00\x11\x22\x33\x44\x55"
109+
protocol: 0x0800
110+
vlan: 42
111+
}
112+
parsed, _ = eth.parse tostring(frame), 1
113+
assert parsed.vlan == 42, "round-trip vlan mismatch: got #{parsed.vlan}"
114+
assert parsed.protocol == 0x0800, "round-trip protocol mismatch"
115+
assert parsed.dst == "\xaa\xbb\xcc\xdd\xee\xff", "round-trip dst mismatch"
116+
66117
util.summary "l2/ethernet"

0 commit comments

Comments
 (0)