Capture engine streams dumpcap pcapng into verbatim, hashed segments with per-segment tshark summaries; display filters, PDML dissection with verified byte ranges, export and import. Tests run in a Debian trixie container.
108 lines
2.9 KiB
Go
108 lines
2.9 KiB
Go
package capture
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/xml"
|
|
"errors"
|
|
"strconv"
|
|
)
|
|
|
|
// Node is one line of the protocol tree sent to the browser.
|
|
type Node struct {
|
|
Label string `json:"label"`
|
|
Field string `json:"field,omitempty"`
|
|
Pos int `json:"pos"`
|
|
Size int `json:"size"`
|
|
Verified bool `json:"verified"` // bytes at pos..pos+size are proven to belong to this field
|
|
Children []Node `json:"children,omitempty"`
|
|
}
|
|
|
|
type pdmlNode struct {
|
|
XMLName xml.Name
|
|
Name string `xml:"name,attr"`
|
|
Show string `xml:"show,attr"`
|
|
Showname string `xml:"showname,attr"`
|
|
Value string `xml:"value,attr"`
|
|
Unmasked string `xml:"unmaskedvalue,attr"`
|
|
Pos string `xml:"pos,attr"`
|
|
Size string `xml:"size,attr"`
|
|
Hide string `xml:"hide,attr"`
|
|
Children []pdmlNode `xml:",any"`
|
|
}
|
|
|
|
// ParsePDML converts tshark PDML for a single packet into a tree. frame holds
|
|
// the original captured bytes, used to verify each field's byte range.
|
|
func ParsePDML(b []byte, frame []byte) ([]Node, error) {
|
|
var doc struct {
|
|
Packets []struct {
|
|
Protos []pdmlNode `xml:",any"`
|
|
} `xml:"packet"`
|
|
}
|
|
d := xml.NewDecoder(bytes.NewReader(b))
|
|
d.Strict = false
|
|
if err := d.Decode(&doc); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(doc.Packets) == 0 {
|
|
return nil, errors.New("packet not found")
|
|
}
|
|
var out []Node
|
|
for _, p := range doc.Packets[0].Protos {
|
|
if p.Name == "geninfo" || p.Hide == "yes" {
|
|
continue
|
|
}
|
|
n, _ := convert(p, frame)
|
|
out = append(out, n)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// convert returns the node and whether any field in its subtree contradicts the frame bytes.
|
|
func convert(p pdmlNode, frame []byte) (Node, bool) {
|
|
n := Node{Field: p.Name}
|
|
n.Label = p.Showname
|
|
if n.Label == "" {
|
|
n.Label = p.Show
|
|
}
|
|
if n.Label == "" {
|
|
n.Label = p.Name
|
|
}
|
|
n.Pos, _ = strconv.Atoi(p.Pos)
|
|
n.Size, _ = strconv.Atoi(p.Size)
|
|
inRange := n.Size > 0 && n.Pos >= 0 && n.Pos+n.Size <= len(frame)
|
|
raw := p.Value
|
|
if p.Unmasked != "" {
|
|
raw = p.Unmasked // bitfields: value is the masked number, unmaskedvalue the bytes
|
|
}
|
|
contradicts := false
|
|
if v, err := hex.DecodeString(raw); err == nil && len(v) > 0 && len(v) == n.Size {
|
|
if inRange && bytes.Equal(v, frame[n.Pos:n.Pos+n.Size]) {
|
|
n.Verified = true
|
|
} else {
|
|
contradicts = true
|
|
}
|
|
}
|
|
matched, mismatched := false, contradicts
|
|
for _, c := range p.Children {
|
|
if c.Hide == "yes" {
|
|
continue
|
|
}
|
|
cn, cm := convert(c, frame)
|
|
if c.XMLName.Local != "proto" { // a nested (e.g. reassembled) protocol is judged on its own
|
|
matched = matched || cn.Verified
|
|
mismatched = mismatched || cm
|
|
}
|
|
n.Children = append(n.Children, cn)
|
|
}
|
|
// Protocols carry no value: trust their range only if their own fields verify
|
|
// against the frame bytes and none contradict it (rules out reassembled sources).
|
|
if raw == "" && inRange && matched && !mismatched {
|
|
n.Verified = true
|
|
}
|
|
if p.Name == "frame" && inRange {
|
|
n.Verified = true
|
|
}
|
|
return n, mismatched
|
|
}
|