From 55bc765d922994274dea59ffefbc3ac52f3c4f49 Mon Sep 17 00:00:00 2001 From: soypat Date: Sun, 1 Jun 2025 13:04:43 -0300 Subject: [PATCH] begin adding packet capture functionality --- arp/definitions.go | 1 + arp/frame.go | 4 +- internal/ltesto/capture.go | 423 +++++++++++++++++++++++++++++++++++++ tcp/frame.go | 2 +- 4 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 internal/ltesto/capture.go diff --git a/arp/definitions.go b/arp/definitions.go index da82f16..f6925ae 100644 --- a/arp/definitions.go +++ b/arp/definitions.go @@ -14,6 +14,7 @@ var ( errARPBufferFull = errors.New("ARP client need handling:too many ops pending") errShortARP = errors.New("packet too short to be ARP") errARPUnsupported = errors.New("ARP not supprortedf") + errLargeSizes = errors.New("size of ARP protocol+hardware is unusually large") ) // Operation represents the type of ARP packet, either request or reply/response. diff --git a/arp/frame.go b/arp/frame.go index ea92adb..d7f2aa3 100644 --- a/arp/frame.go +++ b/arp/frame.go @@ -138,9 +138,11 @@ func (afrm Frame) SwapTargetSender() { func (afrm Frame) ValidateSize(v *lneto.Validator) { _, hlen := afrm.Hardware() _, ilen := afrm.Protocol() - minLen := 8 + 2*(hlen+ilen) + minLen := 8 + 2*(int(hlen)+int(ilen)) if len(afrm.buf) < int(minLen) { v.AddError(errShortARP) + } else if minLen > 255 { + v.AddError(errLargeSizes) // We don't want a uint8 overflow somewhere. This is probably a maliciously crafted packet. } } diff --git a/internal/ltesto/capture.go b/internal/ltesto/capture.go new file mode 100644 index 0000000..b3242cd --- /dev/null +++ b/internal/ltesto/capture.go @@ -0,0 +1,423 @@ +package ltesto + +import ( + "errors" + + "github.com/soypat/lneto" + "github.com/soypat/lneto/arp" + "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/http/httpraw" + "github.com/soypat/lneto/ipv4" + "github.com/soypat/lneto/tcp" +) + +type PacketBreakdown struct { + hdr httpraw.Header + vld lneto.Validator +} + +func (pc *PacketBreakdown) CaptureEthernet(dst []FrameInfo, pkt []byte, bitOffset int) ([]FrameInfo, error) { + if bitOffset%8 != 0 { + return dst, errors.New("Ethernet must be parsed at byte boundary") + } + efrm, err := ethernet.NewFrame(pkt[bitOffset/8:]) + if err != nil { + return dst, err + } + efrm.ValidateSize(pc.validator()) + if pc.validator().HasError() { + return dst, pc.validator().Err() + } + + finfo := FrameInfo{ + Protocol: "Ethernet", + PacketBitOffset: bitOffset, + } + finfo.Fields = append(finfo.Fields, baseEthernetFields[:]...) + etype := efrm.EtherTypeOrSize() + end := 14*octet + bitOffset + if etype.IsSize() { + finfo.Fields[len(finfo.Fields)-1].Class = classSize + dst = append(dst, finfo) + dst = append(dst, remainingFrameInfo("Ethernet payload", classPayload, end, octet*len(pkt))) + return dst, nil + } + dst = append(dst, finfo) + if efrm.IsVLAN() { + finfo.Fields = append(finfo.Fields, FrameField{Name: "VLAN Tag", Class: classType, FrameBitOffset: end, BitLength: 2 * octet}) + dst = append(dst, remainingFrameInfo("Ethernet VLAN", classPayload, end+2*octet, octet*len(pkt))) + return dst, nil + } + switch etype { + case ethernet.TypeARP: + case ethernet.TypeIPv4: + dst, err = pc.CaptureIPv4(dst, pkt, end) + } + return dst, err +} + +func (pc *PacketBreakdown) CaptureARP(dst []FrameInfo, pkt []byte, bitOffset int) ([]FrameInfo, error) { + if bitOffset%8 != 0 { + return dst, errors.New("ARP must be parsed at byte boundary") + } + afrm, err := arp.NewFrame(pkt[bitOffset/8:]) + if err != nil { + return dst, err + } + afrm.ValidateSize(pc.validator()) + if pc.validator().HasError() { + return dst, pc.validator().Err() + } + + finfo := FrameInfo{ + Protocol: ethernet.TypeARP, + PacketBitOffset: bitOffset, + } + + const varstart = 8 * octet + finfo.Fields = append(finfo.Fields, baseARPFields[:]...) + _, hlen := afrm.Hardware() + _, plen := afrm.Protocol() + finfo.Fields = append(finfo.Fields, + FrameField{ + Name: "Sender hardware address", + Class: classSrc, + FrameBitOffset: varstart, + BitLength: int(hlen) * octet, + }, + FrameField{ + Name: "Sender protocol address", + Class: classSrc, + FrameBitOffset: int(hlen)*octet + varstart, + BitLength: int(plen) * octet, + }, + FrameField{ + Name: "Target hardware address", + Class: classSrc, + FrameBitOffset: int(hlen+plen)*octet + varstart, + BitLength: int(hlen) * octet, + }, + FrameField{ + Name: "Target protocol address", + Class: classSrc, + FrameBitOffset: (2*int(hlen)+int(plen))*octet + varstart, + BitLength: int(plen) * octet, + }, + ) + dst = append(dst, finfo) + return dst, nil +} + +func (pc *PacketBreakdown) CaptureIPv4(dst []FrameInfo, pkt []byte, bitOffset int) ([]FrameInfo, error) { + if bitOffset%8 != 0 { + return dst, errors.New("IPv4 must be parsed at byte boundary") + } + ifrm4, err := ipv4.NewFrame(pkt[bitOffset/8:]) + if err != nil { + return dst, err + } + ifrm4.ValidateSize(pc.validator()) + if pc.validator().HasError() { + return dst, pc.validator().Err() + } + finfo := FrameInfo{ + Protocol: ethernet.TypeIPv4, + PacketBitOffset: bitOffset, + } + finfo.Fields = append(finfo.Fields, baseIPv4Fields[:]...) + options := ifrm4.Options() + finfo.Fields = append(finfo.Fields, FrameField{ + Class: classOptions, + FrameBitOffset: 20 * octet, + BitLength: octet * len(options), + }) + proto := ifrm4.Protocol() + dst = append(dst, finfo) + end := bitOffset + octet*ifrm4.HeaderLength() + switch proto { + case lneto.IPProtoTCP: + dst, err = pc.CaptureTCP(dst, pkt, end) + default: + dst = append(dst, remainingFrameInfo(proto, 0, end, octet*len(pkt))) + } + return dst, err +} + +func (pc *PacketBreakdown) CaptureTCP(dst []FrameInfo, pkt []byte, bitOffset int) ([]FrameInfo, error) { + if bitOffset%8 != 0 { + return dst, errors.New("TCP must be parsed at byte boundary") + } + tfrm, err := tcp.NewFrame(pkt[bitOffset/8:]) + if err != nil { + return dst, err + } + tfrm.ValidateSize(pc.validator()) + if pc.validator().HasError() { + return dst, pc.validator().Err() + } + end := bitOffset + octet*tfrm.HeaderLength() + finfo := FrameInfo{ + Protocol: lneto.IPProtoTCP, + PacketBitOffset: bitOffset, + } + finfo.Fields = append(finfo.Fields, baseTCPFields[:]...) + options := tfrm.Options() + finfo.Fields = append(finfo.Fields, FrameField{ + Class: classOptions, + FrameBitOffset: 20 * octet, + BitLength: octet * len(options), + }) + dst = append(dst, finfo) + payload := tfrm.Payload() + if len(payload) > 0 { + dst, err = pc.CaptureHTTP(dst, pkt, end) + if err != nil { + dst = append(dst, remainingFrameInfo(nil, classPayload, end, len(pkt))) + } + } + return dst, nil +} + +func (pc *PacketBreakdown) CaptureHTTP(dst []FrameInfo, pkt []byte, bitOffset int) ([]FrameInfo, error) { + if bitOffset%8 != 0 { + return nil, errors.New("HTTP must be parsed at byte boundary") + } + const asResponse = true + const asRequest = false + httpData := pkt[bitOffset/8:] + pc.hdr.Reset(httpData) + err := pc.hdr.Parse(asResponse) + if err == nil { + dst = append(dst, remainingFrameInfo("HTTP Response", classText, bitOffset, len(pkt))) + return dst, nil + } + pc.hdr.Reset(httpData) + err = pc.hdr.Parse(asRequest) + if err == nil { + dst = append(dst, remainingFrameInfo(string(pc.hdr.Protocol()), classText, bitOffset, len(pkt))) + return dst, nil + } + return dst, err +} + +func (pc *PacketBreakdown) validator() *lneto.Validator { + return &pc.vld +} + +type FrameField struct { + Name string + Class FieldClass + FrameBitOffset int + BitLength int + SubFields []FrameField +} + +type FrameInfo struct { + Protocol any + Fields []FrameField + PacketBitOffset int +} + +type FieldClass uint16 + +const ( + _ FieldClass = iota + classSrc // Source + classDst // Destination + classProto // Protocol + classType // Type + classSize // Field Size + classFlags // Flags + classID // Identification + classChecksum // Checksum + classOptions // Options + classPayload // Payload + classText // Text +) + +const octet = 8 + +var baseEthernetFields = [...]FrameField{ + { + Class: classDst, + FrameBitOffset: 0, + BitLength: 6 * octet, + }, + { + Class: classSrc, + FrameBitOffset: 6 * octet, + BitLength: 6 * octet, + }, + { + Class: classProto, + FrameBitOffset: 12 * octet, + BitLength: 2 * octet, + }, +} + +var baseARPFields = [...]FrameField{ + { + Name: "Hardware type", + Class: classType, + FrameBitOffset: 0, + BitLength: 2 * octet, + }, + { + Name: "Protocol type", + Class: classType, + FrameBitOffset: 2 * octet, + BitLength: 2 * octet, + }, + { + Name: "Hardware size", + Class: classSize, + FrameBitOffset: 4 * octet, + BitLength: 1 * octet, + }, + { + Name: "Protocol size", + Class: classSize, + FrameBitOffset: 5 * octet, + BitLength: 1 * octet, + }, + { + Name: "Opcode", + Class: classType, + FrameBitOffset: 6 * octet, + BitLength: 2 * octet, + }, +} + +var baseIPv4Fields = [...]FrameField{ + { + Name: "Version", + Class: classType, + FrameBitOffset: 0, + BitLength: 4, + }, + { + Name: "Header Length", + Class: classSize, + FrameBitOffset: 4, + BitLength: 4, + }, + { + Name: "Differentiated services", + Class: classFlags, + FrameBitOffset: 1 * octet, + BitLength: 1 * octet, + }, + { + Name: "Total Length", + Class: classSize, + FrameBitOffset: 2 * octet, + BitLength: 2 * octet, + }, + { + Class: classID, + FrameBitOffset: 4 * octet, + BitLength: 2 * octet, + }, + { + Class: classID, + FrameBitOffset: 4 * octet, + BitLength: 2 * octet, + }, + { + Class: classFlags, + FrameBitOffset: 6 * octet, + BitLength: 2 * octet, + }, + { + Name: "Time to live", + FrameBitOffset: 8 * octet, + BitLength: 1 * octet, + }, + { + Class: classProto, + FrameBitOffset: 9 * octet, + BitLength: 1 * octet, + }, + { + Class: classChecksum, + FrameBitOffset: 10 * octet, + BitLength: 2 * octet, + }, + { + Class: classSrc, + FrameBitOffset: 12 * octet, + BitLength: 4 * octet, + }, + { + Class: classDst, + FrameBitOffset: 16 * octet, + BitLength: 4 * octet, + }, +} + +var baseTCPFields = [...]FrameField{ + { + Name: "Source port", + Class: classSrc, + FrameBitOffset: 0, + BitLength: 2 * octet, + }, + { + Name: "Destination port", + Class: classSrc, + FrameBitOffset: 2 * octet, + BitLength: 2 * octet, + }, + { + Name: "Sequence number", + Class: classID, + FrameBitOffset: 4 * octet, + BitLength: 4 * octet, + }, + { + Name: "Acknowledgement number", + Class: classID, + FrameBitOffset: 8 * octet, + BitLength: 4 * octet, + }, + { + Name: "Header length", + Class: classID, + FrameBitOffset: 12 * octet, + BitLength: 4, + }, + { + Class: classFlags, + FrameBitOffset: 12*octet + 4, + BitLength: 12, + }, + { + Name: "Window", + Class: classSize, + FrameBitOffset: 14 * octet, + BitLength: 2 * octet, + }, + { + Class: classChecksum, + FrameBitOffset: 16 * octet, + BitLength: 2 * octet, + }, + { + Name: "Urgent pointer", + Class: 0, + FrameBitOffset: 18 * octet, + BitLength: 2 * octet, + }, +} + +func remainingFrameInfo(proto any, class FieldClass, pktBitOffset, pktBitLen int) FrameInfo { + return FrameInfo{ + Protocol: "Ethernet data payload", + PacketBitOffset: pktBitOffset, + Fields: []FrameField{ + { + Class: class, + BitLength: pktBitLen - pktBitOffset, + }}, + } +} diff --git a/tcp/frame.go b/tcp/frame.go index f396b52..62c44f3 100644 --- a/tcp/frame.go +++ b/tcp/frame.go @@ -97,7 +97,7 @@ func (tfrm Frame) SetOffsetAndFlags(offset uint8, flags Flags) { // HeaderLength uses Offset field to calculate the total length of // the TCP header including options. Performs no validation. -func (tfrm Frame) HeaderLength() (tcpWords int) { +func (tfrm Frame) HeaderLength() (lengthInBytes int) { offset, _ := tfrm.OffsetAndFlags() return 4 * int(offset) }