Encapsulations - NormB/sipnab GitHub Wiki
Whether sipnab can read the SIP in your capture when something wraps it: a VLAN tag, an MPLS label, a tunnel. The first table answers that. The sections after it give:
- the exact link types, EtherTypes and tunnels,
- what sipnab reports when a frame does not decode.
| Your SIP travels inside⦠| From a capture file | From a live capture |
|---|---|---|
Ethernet, Linux cooked (any), raw IP, loopback, PPP or PPPoE |
yes | yes |
VLAN tags: 802.1Q, 802.1ad, QinQ, or legacy 0x9100
|
yes | yes |
| MPLS | yes, from an Ethernet capture. Not from a Linux cooked one | name the interface with -d, because the default any device produces a cooked capture |
| NSH, PBB or MACsec | yes, from an Ethernet capture. MACsec only when it protects integrity without encrypting | name the interface with -d and pass your own BPF filter that adds them, such as portrange 5060-5061 or ether proto 0x88e5 for MACsec |
| IP-in-IP, 6in4, GRE, GRE bridging, MPLS-in-IP or AH | yes | pass your own BPF filter that adds them, such as portrange 5060-5061 or ip proto 47 for GRE, because the generated one matches UDP and TCP ports only |
| GTP-U, VXLAN or GENEVE | yes | add --capture-tunnels
|
| Teredo or L2TPv2 | yes (L2TPv2 data messages only) | add --capture-tunnels=3544 or --capture-tunnels=1701
|
| ESP | only when NULL-encrypted | pass your own BPF filter that adds it, such as portrange 5060-5061 or ip proto 50
|
| UDP-encapsulated ESP, or L2TPv3 over UDP | no | no |
| anything else | no | no |
Whenever the answer is no, sipnab says so.
- It counts the frames it cannot decode in a
NOT DECODEDline at the end of the run. - The line gives the number that identifies each: the link type, the EtherType or the IP protocol.
- What sipnab says when it cannot read a frame shows that line.
sipnab drops nothing on this page silently. A frame it does not decode still increments a counter keyed by the number it did not recognize. That count reaches:
- the run summary,
-
--report, -
--json, -
/v1/stats, - MCP
capture_status, - the Prometheus capture family.
NOT DECODED: 3 of 3 frame(s) (100.0%) produced nothing and are in none of the
counts above. Reasons: unsupported link type 147 (3). NOTHING IN THIS CAPTURE
WAS READ β every frame failed to decode, so the totals above describe no
traffic whatsoever and a zero among them is not evidence of absence.
- A capture with no SIP and no undecodable frames is a finding: the SIP is not there.
- One with no SIP and a pile of undecodable frames means sipnab could not read it. That is a different problem.
Troubleshooting says what each reason means and what to do about it.
Over-eager decapsulation is worse than the silence it replaces.
- A silent drop loses a call.
- A false decapsulation invents one: inner addresses, inner ports, an inner dialog.
- It builds them from bytes that never described any of it.
- Nothing marks the flow fictional.
This bites hardest on port-keyed tunnels.
- A UDP port is not a protocol identity. 2152, 4789 and 6081 all occur as the ephemeral source port of ordinary RTP media.
- So every decapsulator validates structurally:
- reserved bits,
- version fields,
- length self-consistency,
- the shape of the first inner header.
- It declines the moment anything disagrees.
- Each port-keyed decoder has a test proving it rejects a realistic RTP packet arriving on its own port.
When the choice is between missing a tunnel and fabricating a flow, sipnab misses the tunnel. A miss stays visible. A fabrication does not.
| DLT | Name | Status |
|---|---|---|
| 1 | Ethernet | decoded |
| 0 | BSD loopback (DLT_NULL) |
decoded β address family in host byte order |
| 108 | OpenBSD loopback (DLT_LOOP) |
decoded β address family always big-endian |
| 12 | Raw IP (DLT_RAW) |
decoded β the version nibble picks v4 or v6 |
| 113 | Linux cooked v1 (SLL) |
decoded |
| 276 | Linux cooked v2 (SLL2) |
decoded |
| 9 | PPP (DLT_PPP) |
decoded β with or without RFC 1662 HDLC-like framing |
| 50 | PPP in HDLC-like framing (DLT_PPP_SERIAL) |
decoded |
| 51 | PPPoE session (DLT_PPP_ETHER) |
decoded |
| 228 | bare IPv4 (DLT_IPV4) |
decoded β sipnab checks the version nibble against the link type and rejects a mismatch |
| 229 | bare IPv6 (DLT_IPV6) |
decoded β same check, the other way |
| any other | β |
counted and named as unsupported link type N
|
Loopback is DLT_EN10MB on Linux but DLT_NULL on macOS and BSD.
That is why 0 and 108 matter for the common "SIP server listening on loopback" case.
- The parser dispatches on that closed set rather than on the raw number.
- The cheap
--coresshard key dispatches on the same one. - Adding a link type is a compile error in both until each has an arm. So the two walks cannot come to know different sets.
| EtherType | Protocol | Reference | Status |
|---|---|---|---|
0x0800 |
IPv4 | RFC 894 | decoded |
0x86DD |
IPv6 | RFC 2464 | decoded |
0x8100 |
C-VLAN tag | IEEE 802.1Q | skipped to reach IP |
0x88A8 |
S-VLAN tag | IEEE 802.1Q | skipped to reach IP |
0x9100 |
legacy QinQ | unregistered | skipped to reach IP |
0x8864 |
PPPoE Session | RFC 2516 | decapsulated |
0x8863 |
PPPoE Discovery | RFC 2516 | never decapsulated β counted and named like any EtherType sipnab does not walk |
0x8847 |
MPLS unicast | RFC 5332 | decapsulated |
0x8848 |
MPLS upstream-assigned | RFC 5332 | decapsulated |
0x894F |
NSH | RFC 8300 | decapsulated |
0x88E7 |
PBB I-TAG | IEEE 802.1Q | decapsulated to the customer frame |
0x88E5 |
MACsec | IEEE 802.1AE | see below |
| any other | β | β | counted and named with its hex value |
0x9100 is genuinely absent from the IEEE RA listing.
- It has no assignee and no protocol text. Every neighbor has both.
- No standards body ever registered it. It is legacy vendor usage, still common on older carrier gear.
- Supporting it concedes to deployed equipment rather than to any specification.
- Do not "correct" it away after checking a registry and finding nothing.
sipnab deliberately never decapsulates PPPoE Discovery. Its payload is TLV tags. So reading it as an IP header would report addresses the wire never carried.
The SecTAG's E and C bits distinguish confidentiality from integrity.
- sipnab decodes an integrity-protected frame that carries no encryption as the ordinary readable plaintext it is.
- Only a genuinely encrypted payload gets reported as encrypted.
- Treating all of
0x88E5as opaque would write off recoverable calls.
IEEE 802.1AE's own Annex C conformance vectors settled those bit positions. Figure 9-4 of the 2018 standard is defective. It labels two bits with names appearing nowhere else in the document.
The table above describes the Ethernet walk.
- A Linux cooked capture β
SLLorSLL2β runs a shorter one:- VLAN tags,
- then IPv4, IPv6 or PPPoE Session,
- and nothing else.
- That matters more than it sounds:
-
SLL2is what theanypseudo-device produces. -
anyis what sipnab opens on Linux when no-dnames an interface.
-
| EtherType | Ethernet (-d eth0) |
Cooked (any, -i any files) |
|---|---|---|
0x8100 / 0x88A8 / 0x9100 VLAN |
skipped to reach IP | skipped to reach IP |
0x0800 / 0x86DD IP |
decoded | decoded |
0x8864 PPPoE Session |
decapsulated | decapsulated |
0x8847 / 0x8848 MPLS |
decapsulated | not walked |
0x894F NSH |
decapsulated | not walked |
0x88E7 PBB I-TAG |
decapsulated | not walked |
0x88E5 MACsec |
decoded, or named as encrypted | not walked |
So SIP inside an MPLS label stack reaches the parser from -d eth0. It does not
reach it from the default device.
- A frame the cooked walk declines invents nothing. It fails to decode and joins the undecodable count with its own reason.
- But the remedy is the capture device rather than the filter.
- Name the interface when the link carries any of the bottom four.
The same asymmetry applies to a saved file.
- A capture somebody took with
tcpdump -i anycarries the cooked link type. - No sipnab flag can put back what the walk does not follow.
| Encapsulation | Key | Reference | Status |
|---|---|---|---|
| IP-in-IP / 6-in-4 | IP proto 4 / 41 | RFC 2003 / RFC 4213 | decoded |
| GRE | IP proto 47 | RFC 2784 | decoded |
| GRE Transparent Ethernet Bridging | GRE proto 0x6558
|
RFC 7637 section 3.2 | decoded |
| MPLS-in-IP | IP proto 137 | RFC 4023 | decoded |
| AH | IP proto 51 | RFC 4302 | traversed β AH authenticates without encrypting, so the payload is readable |
| ESP | IP proto 50 | RFC 4303 | decoded when NULL-encrypted β see below; otherwise named, never guessed |
| GTP-U | UDP 2152 | 3GPP TS 29.281 | decoded |
| VXLAN | UDP 4789 | RFC 7348 | decoded |
| GENEVE | UDP 6081 | RFC 8926 | decoded |
| Teredo | UDP 3544 | RFC 4380 | decoded |
| UDP-encapsulated ESP | UDP 4500 | RFC 3948 | encrypted β sipnab names it, never guesses |
| L2TPv2 | UDP 1701 | RFC 2661 | data messages only |
| L2TPv3 over UDP | UDP 1701 | RFC 3931 | refused β see below |
L2TPv3 over UDP is deliberately not decoded. RFC 3931 section 4.1 says:
The Session ID alone provides the necessary context for all further packet processing, including the presence, size, and value of the Cookie.
- The cookie runs to 0, 4 or 8 octets.
- Only the control channel carries that length, along with the pseudowire type.
- Guessing between those is precisely how a decapsulator invents a flow.
sipnab reads ESP only when the payload proves NULL encryption.
- An IMS core commonly protects the Gm interface between a phone and its P-CSCF with IPsec ESP.
- A lab or test network often runs it with NULL encryption (RFC 2410). The SIP then travels in the clear inside an ESP header and trailer.
Nothing in the ESP header says which. So sipnab reads the payload as NULL-encrypted only when all of these hold:
- an integrity check value of 12, 16, 24 or 32 octets,
- the default padding of RFC 4303 section 2.4 and 4-octet alignment,
- a next header of TCP or UDP,
- a TCP or UDP header that fits the recovered segment and whose checksum verifies.
- A UDP checksum of zero means "none".
- It passes over IPv4 only, as RFC 768 and RFC 8200 section 8.1 allow.
A frame that fails any of them joins the
NOT DECODED line as ESP not NULL-encrypted (IP protocol 50). sipnab
takes no keys for encrypted ESP. So capture inside the tunnel instead.
One shared budget bounds nesting.
- It covers every layer of a frame.
- So a frame combining MACsec, MPLS, GTP-U and IP-in-IP cannot walk further than one using a single encapsulation repeatedly.
- sipnab refuses an over-nested frame rather than following it.
Give sipnab no BPF expression of your own β the trailing argument, or a file
named by --bpf-file. It then generates one that matches SIP inside VLAN, QinQ,
PPPoE Session and MPLS as well as untagged traffic.
- Do not confuse that with
--filter. - That flag is sipnab's own matching language over messages sipnab already decoded, long after the kernel has made its decision.
UDP-tunneled SIP is not covered by default.
- BPF cannot parse a variable-length GTP-U header to reach the inner port.
- So covering those means capturing every packet on those ports.
- Use
--capture-tunnelsto opt in β see the CLI reference.
The encapsulated arm compiles on SLL and SLL2 as well as on Ethernet.
- It selects the encapsulation through libpcap's
ether proto. - libpcap resolves that to the right offset for each link type while compiling.
- So the kernel hands those frames up whichever device you opened.
- What sipnab makes of them afterwards still depends on the link type. An MPLS frame arrives on the default device and the cooked walk does not follow it, as the table above says.
Writing your own expression replaces the generated one whole.
- sipnab never edits it.
So the encapsulated arm goes away and
--capture-tunnelsturns inert. - Two warnings cover that:
- one when a port-based expression shows no sign of handling encapsulation,
- one naming
--capture-tunnelsas ignored when you passed it alongside a filter of your own.
One limit worth knowing before you rely on a live capture:
- On the encapsulated arm, an IPv4 header carrying options stays unmatched.
- A BPF index has to be constant, and the arm cannot multiply the IHL nibble.
- The untagged arm handles those.
On a live capture with no filter of your own, sipnab installs one built from
--portrange. It is not a bare portrange 5060-5061.
- That one matches the outer headers only.
- So on a tagged trunk, a PPPoE access link or an MPLS core it matches nothing.
- The kernel then discards the frames where no sipnab counter, metric or report can see them.
- You get "No SIP traffic found" on a link carrying calls.
The generated filter adds an encapsulated arm instead. It covers:
- one VLAN tag (802.1Q, 802.1ad or 0x9100),
- QinQ,
- PPPoE Session,
- VLAN over PPPoE,
- one or two MPLS labels,
- for IPv4 and IPv6, UDP and TCP.
The arm still demands a signaling port.
- So it matches more of the same traffic, not a new class of it.
- VLAN-tagged RTP reaches sipnab no more often than untagged RTP did.
It covers cooked captures too. So omitting -d costs the filter nothing.
The cooked walk still skips MPLS, though, as A cooked capture reaches fewer of them says.
The arm asks "does this frame carry an encapsulation?" through libpcap's
ether proto. That resolves to the right byte offset for whatever link type
the filter compiles against:
- offset 12 on Ethernet,
- 14 on Linux cooked v1,
- 0 on Linux cooked v2,
- a constant false on raw IP and the two loopback link types, which carry no protocol field at all.
The offsets come from measuring a capture of each type with tcpdump -d.
Asking the same question with a fixed ether[12:2] is the trap this avoids.
- That offset holds the EtherType on Ethernet.
- It holds part of the link-layer address on a cooked capture.
- So an arm written that way compiles, runs and matches nothing there: 1 of 11 encapsulated SIP frames on cooked v1 and cooked v2, against 11 of 11 on Ethernet.
- Cooked is what Linux gives you when you name no interface. So that shape would have left the default invocation blind.
Two limits worth knowing.
- On the encapsulated arm an IPv4 header carrying options stays unmatched.
- A BPF byte offset has to be a constant.
- So the arm cannot multiply the IHL nibble into the port offset the way libpcap's own
portrangedoes. - The untagged
portrangehandles those. So this costs you only IPv4-options traffic that is also encapsulated.
- One filter string serving three link types has to carry all three sets of
inner offsets.
- BPF offers no way to ask which link type it compiled against.
- Seven offsets get probed on every link type. Four of them belong to a different link header.
Those four can fire only when both of these hold:
- the frame already carries one of the six encapsulating protocols,
- its bytes at the wrong offset spell a complete IPv4-or-IPv6 header with a signaling port.
So the worst case is a stray tagged packet reaching userspace, where the
parser rejects it. Ordinary traffic never reaches those probes, because the
outer ether proto test is exact.
sipnab unwraps SIP carried over WebSocket
(RFC 7118) on the ports
--ws-portrange names.
- The ports are 80, 443, 8080 and 8443 unless you give a range.
- A range replaces that set.
- See
--ws-portrange.
sipnab reassembles IP fragments and TCP segments before it parses.
So a SIP message split across packets reads as one message.
--no-reassembly turns this off and parses every packet on its own.
sipnab reads STUN (RFC 5389) and TURN (RFC 5766) because they are the first link in a one-way-audio chain. It does not read them because it is an ICE agent. What it takes from them is what changes a diagnosis:
- whether an endpoint asked,
- whether anything answered,
- which address the answer named.
Three protocols share these ports. They separate cleanly on the two high bits of the first byte:
- STUN
00, - TURN ChannelData
01, - RTP
10.
So checking STUN before RTP cannot swallow media.
| Read | Why |
|---|---|
| Header: type, length, magic cookie, transaction ID | The parser checks the cookie before believing anything else, which is what makes it safe to offer it every UDP payload |
| Class and method |
Binding and TURN's Allocate, Refresh, Send, Data, CreatePermission, ChannelBind. An unknown method reports as its number rather than as "unknown", because the number is the thing to look up |
XOR-MAPPED-ADDRESS |
The answer the endpoint asked for, and what it then writes into its SDP |
XOR-RELAYED-ADDRESS, XOR-PEER-ADDRESS, LIFETIME
|
The TURN equivalents: the address a relay allocated, who a permission is about, and how long it lasts |
ERROR-CODE |
So a refusal reads as a refusal. A server that says no is reachable, which is a different fault from silence |
SOFTWARE |
Names the stack, which tells one vendor's retransmission pattern from the next |
MAPPED-ADDRESS (legacy) |
The pre-RFC5389 form. Servers older than the cookie still answer with it, and without reading it their successful response reads as no answer at all. When both forms arrive the XOR one wins, whatever order they come in |
ALTERNATE-SERVER |
So a 300 redirect names where it points instead of reading as a dead end |
REALM, NONCE
|
A 401 with a realm is an authentication challenge, not a blocked path, and each sends you somewhere different. sipnab records the nonce as present only: its value is a server-chosen opaque string that means nothing to an observer |
FINGERPRINT |
Verified. A CRC-32 over the message with no key involved, so a passive reader can check it honestly -- and it is what separates a real STUN message from a payload that merely carried the cookie bytes. Reported as verified, present-and-wrong, or absent, which stay distinct |
ICE USE-CANDIDATE, PRIORITY, ICE-CONTROLLING/ICE-CONTROLLED
|
The nomination is the finding: without it, an ICE exchange that converged and one that never did look alike. Both sides claiming controlling is a role conflict whose only other symptom is media that never starts |
TURN CHANNEL-NUMBER, REQUESTED-TRANSPORT
|
So sipnab can describe a relay path, not merely detect one |
| ChannelData framing | sipnab recognizes and unwraps it, so RTP relayed through TURN reaches reconstruction. Without that a relayed call reports as having no media β the same answer sipnab gives for a call that carried none, which are opposite findings |
sipnab tracks transactions.
- So it reports a request that never came back, along with the number of attempts.
- A retransmission is one unanswered question, not several.
- An error response counts as ANSWERED.
MESSAGE-INTEGRITY and MESSAGE-INTEGRITY-SHA256 decide whether a message is
authentic.
- A passive observer has no credentials to check them with.
- Reading them and reporting anything about them would be claiming a verification that did not happen.
- That is the same confident-wrong-answer this codebase refuses elsewhere.
sipnab reads USERNAME, REALM and NONCE for what they SAY.
- An authentication challenge is a different fault from a blocked path.
- sipnab never reads them as evidence that authentication succeeded.
sipnab is not an ICE agent. It does not:
- evaluate candidate pairs,
- compute priorities,
- decide nominations.
It reports what it saw on the wire.
An end-to-end test backs every "decoded" row for a tunnel, in
tests/tunnel_integration_test.rs.
- Each test carries a real INVITE through
parse_packetinto the SIP parser. - It asserts the method and Call-ID.
- MPLS, MPLS-in-IP, NSH, PBB, MACsec (integrity-only), VXLAN, GTP-U, GRE-TEB and AH each have one.
- PPPoE and the loopback link types have their own suites.
GENEVE, Teredo and L2TP rely on unit tests over crafted frames rather than an end-to-end INVITE.
- That is a weaker proof.
- This page says so rather than letting the table imply otherwise.
No measurement on this page comes from a live NIC.
The IEEE Registration Authority assigns EtherTypes, not IANA. RFC 9542 states it plainly:
Neither EtherTypes nor LSAPs are assigned by IANA; they are assigned by the IEEE Registration Authority.
The EtherType values on this page come from:
- the IEEE RA public listing at https://standards-oui.ieee.org/ethertype/eth.txt, retrieved 2026-08-04,
- cross-checked against IANA's informational mirror at https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml. That mirror's own header reads "Not assigned by IANA."
This is the one place the project's "when in doubt, use the RFC" rule does not resolve on its own.
- For EtherTypes the RFC hands the question to a different standards body.
- Use the IEEE RA listing for the value.
- Use the current RFC for the semantics.
Registries are no more infallible than RFCs, and both of these would mislead someone implementing from the listing alone.
-
0x8848carries the wrong protocol text.- The listing gives it
"8847: MPLS (multiprotocol label switching) label stack - unicast". - That is the
0x8847entry duplicated, wrong identifier and wrong cast. -
RFC 5332 assigns
0x8848to MPLS with an upstream-assigned label.
- The listing gives it
-
0x894Fcites a superseded draft,draft-ietf-sfc-nsh-18.- That draft became RFC 8300 in January 2018.
- That one is stale rather than wrong. The draft and the RFC define the same header.
- The two cases do not belong in the same bucket.
Why / how we know
sipnab used to answer a question it had not understood. Given a capture whose frames it could not decode, it reported:
sipnab: 49 packets captured, 0 SIP messages, 0 RTP packets across 0 streams
No SIP traffic found. Check that the capture contains SIP packets (typically UDP port 5060-5061).
That output was identical in two cases:
- sipnab had read every frame and found no SIP,
- or sipnab had failed to read a single frame of a capture full of INVITEs.
Two such captures were in this repository's own test corpus.
A missed encapsulation is now counted and named, with the number that identifies it: the link type, the EtherType, the IP protocol. See Troubleshooting for what each reason means.