diff --git a/dns/dns.go b/dns/dns.go index 88a53b0..34f86d8 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -346,6 +346,8 @@ func (r *Resource) Reset() { r.data = r.data[:0] } +func (r *Resource) Header() ResourceHeader { return r.header } + func (r *Resource) RawData() []byte { length := r.header.Length if int(length) > len(r.data) { diff --git a/examples/httptap/main.go b/examples/httptap/main.go index d393e8e..ab9eb29 100644 --- a/examples/httptap/main.go +++ b/examples/httptap/main.go @@ -85,8 +85,9 @@ func run() error { } defer sv.Close() var cap pcap.PacketBreakdown + cap.SubfieldLimit = 30 // For big DNS or DHCP. pf := pcap.Formatter{ - FilterClasses: []pcap.FieldClass{pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassSize, pcap.FieldClassFlags}, + FilterClasses: []pcap.FieldClass{pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassSize, pcap.FieldClassFlags, pcap.FieldClassDNSName}, } var pfbuf []byte sv.OnTransfer(func(channel int, pkt []byte) { diff --git a/examples/xcurl/main.go b/examples/xcurl/main.go index 593d7f0..df0a87b 100644 --- a/examples/xcurl/main.go +++ b/examples/xcurl/main.go @@ -136,9 +136,11 @@ func run() (err error) { lastAction := time.Now() buf := make([]byte, math.MaxUint16) // Generic-receive Offload (GRO) can aggregate packets. var cap pcap.PacketBreakdown + cap.SubfieldLimit = 30 // For chunky DNS. var frames []pcap.Frame pf := pcap.Formatter{ - FilterClasses: []pcap.FieldClass{pcap.FieldClassFlags, pcap.FieldClassOperation, pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassAddress, pcap.FieldClassTimestamp}, + FilterClasses: []pcap.FieldClass{pcap.FieldClassFlags, pcap.FieldClassOperation, pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassAddress, pcap.FieldClassTimestamp, pcap.FieldClassDNSName}, + SubfieldLimit: 30, } var pfbuf []byte logFrames := func(context string, pkt []byte) error { diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index aa16185..98f7552 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -384,14 +384,14 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int) } finfo := reclaimFrame(&dst, "ICMP", bitOffset, baseICMPv4Fields[:]) - + tp := ifrm.Type() // Add type-specific fields. - switch ifrm.Type() { + switch tp { case icmpv4.TypeEcho, icmpv4.TypeEchoReply: finfo.Fields = append(finfo.Fields, icmpv4EchoFields[:]...) if len(icmpData) > 8 { finfo.Fields = append(finfo.Fields, FrameField{ - Name: "Data", + Name: tp.String(), Class: FieldClassPayload, FrameBitOffset: 8 * octet, BitLength: (len(icmpData) - 8) * octet, @@ -439,26 +439,202 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([ } dnsData := pkt[bitOffset/8:] pc.dmsg.LimitResourceDecoding(4, 4, 4, 4) - off, incomplete, err := pc.dmsg.Decode(dnsData) + _, incomplete, err := pc.dmsg.Decode(dnsData) if err != nil && !incomplete { return dst, err } debuglog("pcap:dns-decode") - finfo := reclaimFrame(&dst, "DNS", bitOffset, nil) + hdr, _ := dns.NewFrame(dnsData) + finfo := reclaimFrame(&dst, "DNS", bitOffset, baseDNSFields[:]) if incomplete { finfo.Errors = append(finfo.Errors, ErrLimitExceeded) } - field := internal.SliceReclaim(&finfo.Fields) - *field = FrameField{ - Name: "Data", - FrameBitOffset: 0, - BitLength: int(off) * octet, - SubFields: field.SubFields[:0], // Reuse subfields. + if pc.SubfieldLimit <= 0 { + debuglog("pcap:dns-done") + return dst, nil } + + wireOff := dns.SizeHeader + + // Questions section: walk all QDCount wire records to keep wireOff correct, + // but only emit SubFields for the decoded ones. + nq := int(hdr.QDCount()) + if nq > 0 { + sectionStart := wireOff + qfield := internal.SliceReclaim(&finfo.Fields) + *qfield = FrameField{Name: "Questions", Class: fieldClassDNSResource, SubFields: qfield.SubFields[:0], Flags: FlagContainer} + decoded := pc.dmsg.Questions + for i := range nq { + nameStart := wireOff + wireOff, err = dnsSkipName(dnsData, wireOff) + if err != nil { + break + } + nameEnd := wireOff + wireOff += 4 // Type(2) + Class(2) + if i < len(decoded) && len(qfield.SubFields)+3 <= pc.SubfieldLimit { + qfield.SubFields = append(qfield.SubFields, FrameField{ + Name: "Name", + Class: FieldClassDNSName, + FrameBitOffset: nameStart * octet, + BitLength: (nameEnd - nameStart) * octet, + }, FrameField{ + Name: "Type", + Class: FieldClassOperation, + FrameBitOffset: nameEnd * octet, + BitLength: 2 * octet, + }, FrameField{ + Name: "Class", + Class: FieldClassOperation, + FrameBitOffset: (nameEnd + 2) * octet, + BitLength: 2 * octet, + }) + } + } + qfield.FrameBitOffset = sectionStart * octet + qfield.BitLength = (wireOff - sectionStart) * octet + } + + wireOff = pc.appendDNSResources(finfo, "Answers", dnsData, pc.dmsg.Answers, int(hdr.ANCount()), wireOff) + wireOff = pc.appendDNSResources(finfo, "Authorities", dnsData, pc.dmsg.Authorities, int(hdr.NSCount()), wireOff) + wireOff = pc.appendDNSResources(finfo, "Additionals", dnsData, pc.dmsg.Additionals, int(hdr.ARCount()), wireOff) + _ = wireOff + debuglog("pcap:dns-done") return dst, nil } +// appendDNSResources adds a section FrameField with per-record SubFields to finfo. +// It walks all `total` wire records to keep wireOff accurate, but only emits SubFields +// for the decoded slice entries while nFields < pc.SubfieldLimit. +func (pc *PacketBreakdown) appendDNSResources(finfo *Frame, name string, dnsData []byte, decoded []dns.Resource, total, wireOff int) int { + if total == 0 { + return wireOff + } + var err error + sectionStart := wireOff + rfield := internal.SliceReclaim(&finfo.Fields) + *rfield = FrameField{Name: name, Class: fieldClassDNSResource, SubFields: rfield.SubFields[:0], Flags: FlagContainer} + for i := range total { + nameStart := wireOff + wireOff, err = dnsSkipName(dnsData, wireOff) + if err != nil || wireOff+10 > len(dnsData) { + break + } + nameEnd := wireOff + dataLen := int(binary.BigEndian.Uint16(dnsData[nameEnd+8:])) + wireOff += 10 + dataLen // Type(2)+Class(2)+TTL(4)+Length(2)+Data + if wireOff > len(dnsData) { + break + } + if i < len(decoded) && len(rfield.SubFields)+6 <= pc.SubfieldLimit { + rfield.SubFields = append(rfield.SubFields, FrameField{ + Name: "Name", + Class: FieldClassDNSName, + FrameBitOffset: nameStart * octet, + BitLength: (nameEnd - nameStart) * octet, + }, FrameField{ + Name: "Type", + Class: FieldClassOperation, + FrameBitOffset: nameEnd * octet, + BitLength: 2 * octet, + }, FrameField{ + Name: "Class", + Class: FieldClassOperation, + FrameBitOffset: (nameEnd + 2) * octet, + BitLength: 2 * octet, + }, FrameField{ + Name: "TTL", + Class: FieldClassID, + FrameBitOffset: (nameEnd + 4) * octet, + BitLength: 4 * octet, + }, FrameField{ + Name: "Length", + Class: FieldClassSize, + FrameBitOffset: (nameEnd + 8) * octet, + BitLength: 2 * octet, + }, FrameField{ + Name: "Data", + Class: FieldClassAddress, + FrameBitOffset: (nameEnd + 10) * octet, + BitLength: dataLen * octet, + }) + } + } + rfield.FrameBitOffset = sectionStart * octet + rfield.BitLength = (wireOff - sectionStart) * octet + if err != nil { + finfo.Errors = append(finfo.Errors, err) + } + return wireOff +} + +// dnsAppendDottedName walks a DNS name in wire format starting at off within +// dnsMsg, follows compression pointers, and appends the dotted representation +// (e.g. "example.com") to dst. Returns dst unchanged on error. +func dnsAppendDottedName(dst, dnsMsg []byte, off int) []byte { + // TODO: somehow move this to dns package. dns.Name.Append ? dns.Name is the wire format... + first := true + for ptr := 0; off < len(dnsMsg) && ptr <= 10; { + c := dnsMsg[off] + off++ + switch c & 0xc0 { + case 0x00: + if c == 0 { + return dst // null terminator: done + } + end := off + int(c) + if end > len(dnsMsg) { + return dst + } + if !first { + dst = append(dst, '.') + } + dst = append(dst, dnsMsg[off:end]...) + off = end + first = false + case 0xc0: + if off >= len(dnsMsg) { + return dst + } + off = int(c&0x3f)<<8 | int(dnsMsg[off]) + ptr++ // guard against pointer loops + default: + return dst // reserved label type + } + } + return dst +} + +// dnsSkipName advances off past a DNS name in wire format without allocating. +// Compression pointers are followed and consume 2 bytes, terminating the name. +func dnsSkipName(b []byte, off int) (int, error) { + for { + if off >= len(b) { + return off, lneto.ErrTruncatedFrame + } + c := b[off] + off++ + switch c & 0xc0 { + case 0x00: + if c == 0 { + return off, nil // null terminator + } + off += int(c) // skip label bytes + if off > len(b) { + return off, lneto.ErrTruncatedFrame + } + case 0xc0: + if off >= len(b) { + return off, lneto.ErrTruncatedFrame + } + return off + 1, nil // compression pointer: 2 bytes total, always terminal + default: + return off, lneto.ErrInvalidField // reserved label type + } + } +} + func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) { if bitOffset%8 != 0 { return dst, errNotByteAligned @@ -656,6 +832,9 @@ type Flags uint32 const ( FlagRightAligned Flags = 1 << iota FlagLegacy + // FlagContainer is used for [FrameField]s whose SubFields represent + // the entirety of the FrameField's data. i.e: DNS Questions/Answers. + FlagContainer ) func (ff Flags) IsLegacy() bool { return ff&FlagLegacy != 0 } @@ -855,10 +1034,13 @@ const ( FieldClassBinaryText // binary-text FieldClassOperation // op FieldClassTimestamp // timestamp + FieldClassDNSName // dns name ) const octet = 8 +const fieldClassDNSResource = fieldClassUndefined + var baseEthernetFields = [...]FrameField{ { Class: FieldClassDst, diff --git a/internet/pcap/capture_test.go b/internet/pcap/capture_test.go index 9b750c9..fc877a3 100644 --- a/internet/pcap/capture_test.go +++ b/internet/pcap/capture_test.go @@ -10,6 +10,7 @@ import ( "github.com/soypat/lneto" "github.com/soypat/lneto/dhcpv4" + "github.com/soypat/lneto/dns" "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/http/httpraw" "github.com/soypat/lneto/internal/ltesto" @@ -486,9 +487,91 @@ func ExampleFormatter_dhcp() { // (Hostname string)="myhost" } -func writeOpt(dst []byte, opt dhcpv4.OptNum, data ...byte) int { - dst[0] = byte(opt) - dst[1] = byte(len(data)) - copy(dst[2:], data) - return 2 + len(data) +func ExampleFormatter_dns() { + const ( + ethSize = 14 + ipv4Size = 20 + udpSize = 8 + ) + + // Build a DNS query for "example.com" A record. + var msg dns.Message + msg.Questions = []dns.Question{ + {Name: dns.MustNewName("example.com"), Type: dns.TypeA, Class: dns.ClassINET}, + {Name: dns.MustNewName("temu.com"), Type: dns.TypeAAAA, Class: dns.ClassANY}, + } + msg.Answers = []dns.Resource{ + dns.NewResource(dns.MustNewName("abc.com"), dns.TypeALL, dns.ClassANY, 64, []byte{10, 0, 11, 1}), + dns.NewResource(dns.MustNewName("123.com"), dns.TypeA, dns.ClassINET, 64, []byte{20, 0, 22, 2}), + } + + dnsPayload, err := msg.AppendTo(nil, 0x1234, dns.NewClientHeaderFlags(dns.OpCodeQuery, true)) + if err != nil { + fmt.Println("dns encode error:", err) + return + } + + pkt := make([]byte, ethSize+ipv4Size+udpSize+len(dnsPayload)) + + efrm, _ := ethernet.NewFrame(pkt) + *efrm.DestinationHardwareAddr() = [6]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x01} + *efrm.SourceHardwareAddr() = [6]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x02} + efrm.SetEtherType(ethernet.TypeIPv4) + + ifrm, _ := ipv4.NewFrame(pkt[ethSize:]) + ifrm.SetVersionAndIHL(4, 5) + ifrm.SetTTL(64) + ifrm.SetProtocol(lneto.IPProtoUDP) + ifrm.SetTotalLength(uint16(ipv4Size + udpSize + len(dnsPayload))) + ifrm.SetCRC(ifrm.CalculateHeaderCRC()) + + ufrm, _ := udp.NewFrame(pkt[ethSize+ipv4Size:]) + ufrm.SetSourcePort(58200) + ufrm.SetDestinationPort(dns.ServerPort) + ufrm.SetLength(uint16(udpSize + len(dnsPayload))) + + copy(pkt[ethSize+ipv4Size+udpSize:], dnsPayload) + + var cap PacketBreakdown + cap.SubfieldLimit = 20 + frames, err := cap.CaptureEthernet(nil, pkt, 0) + if err != nil { + fmt.Println("capture error:", err) + return + } + + var fmtr Formatter + fmtr.SubfieldLimit = cap.SubfieldLimit + fmtr.FrameSep = "\n" + fmtr.FieldSep = "; " + fmtr.SubfieldSep = "\n\t" + out, err := fmtr.FormatFrames(nil, frames, pkt) + if err != nil { + fmt.Println("format error:", err) + return + } + fmt.Println(string(out)) + // Output: + // Ethernet len=14; destination=00:00:00:00:00:01; source=00:00:00:00:00:02; protocol=0x0800 + // IPv4 len=20; version=0x04; (Header Length)=5; (Type of Service)=0x00; (Total Length)=117; identification=0x0000; flags=0x0000; (Time to live)=0x40; protocol=0x11; checksum=0x7a79; source=0.0.0.0; destination=0.0.0.0 + // UDP len=8; (Source port)=58200; (Destination port)=53; size=97; checksum=0x0000 + // DNS len=89; identification=0x1234; flags=0x0100; Questions=2; Answers=2; Authorities=0; Additionals=0; Questions + // Name=example.com + // Type=1 + // Class=1 + // Name=temu.com + // Type=28 + // Class=255; Answers + // Name=abc.com + // Type=255 + // Class=255 + // TTL=0x00000040 + // Length=4 + // Data=10.0.11.1 + // Name=123.com + // Type=1 + // Class=1 + // TTL=0x00000040 + // Length=4 + // Data=20.0.22.2 } diff --git a/internet/pcap/format.go b/internet/pcap/format.go index 02bb1cb..f9d816c 100644 --- a/internet/pcap/format.go +++ b/internet/pcap/format.go @@ -105,12 +105,12 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er } func (f *Formatter) filterField(field FrameField) bool { - return f.FilterClasses != nil && !slices.Contains(f.FilterClasses, field.Class) || + return f.FilterClasses != nil && field.Flags&FlagContainer == 0 && !slices.Contains(f.FilterClasses, field.Class) || (field.Flags.IsLegacy() && !f.DisableLegacyFilter) } func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) { - printOnlySubfields := field.Class == FieldClassOptions && len(field.SubFields) > 0 + printOnlySubfields := (field.Flags&FlagContainer != 0 || field.Class == FieldClassOptions) && len(field.SubFields) > 0 if !printOnlySubfields { dst, err = f.formatField(dst, pktStartOff, field, pkt) } else { @@ -119,7 +119,10 @@ func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, p if f.SubfieldLimit > 0 && len(field.SubFields) > 0 { sep := f.subfieldSep() lim := min(len(field.SubFields), f.SubfieldLimit) - for i := 0; err == nil && i < lim && !f.filterField(field.SubFields[i]); i++ { + for i := 0; err == nil && i < lim; i++ { + if f.filterField(field.SubFields[i]) { + continue + } dst = append(dst, sep...) // Notice we only format subfields one level low dst, err = f.formatField(dst, pktStartOff, field.SubFields[i], pkt) @@ -143,9 +146,13 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p dst = append(dst, ')') } if field.BitLength == 0 { + // We do not print empty nor container fields. return dst, nil } dst = append(dst, '=') + if field.Flags&FlagContainer != 0 { + return dst, nil + } f.mubuf.Lock() defer f.mubuf.Unlock() f.buf, err = appendField(f.buf[:0], pkt, field.FrameBitOffset+pktStartOff, field.BitLength, field.Flags.IsRightAligned()) @@ -177,6 +184,12 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p dst = strconv.AppendQuote(dst, unsafe.String(&f.buf[0], len(f.buf))) } debuglog("pcap:fmtfield:text-done") + case FieldClassDNSName: + // Walk the DNS message from pktStartOff to resolve the name, following + // compression pointers that reference earlier bytes in the DNS message. + dnsMsg := pkt[pktStartOff/8:] + nameOff := field.FrameBitOffset / 8 + dst = dnsAppendDottedName(dst, dnsMsg, nameOff) case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation: // IP, MAC addresses and ports. if field.BitLength <= 16 { @@ -221,7 +234,7 @@ func (f *Formatter) fieldSep() string { func (f *Formatter) subfieldSep() string { sep := f.SubfieldSep if sep == "" { - sep = "_" // default sub-field separator + sep = "," // default sub-field separator } return sep } diff --git a/ipv4/icmpv4/icmpv4.go b/ipv4/icmpv4/icmpv4.go index 6dee3fb..f2d145e 100644 --- a/ipv4/icmpv4/icmpv4.go +++ b/ipv4/icmpv4/icmpv4.go @@ -6,6 +6,8 @@ import ( "github.com/soypat/lneto" ) +//go:generate stringer -type=Type,CodeDestinationUnreachable,CodeRedirect -linecomment -output stringers.go + const ( sizeHeader = 8 ) @@ -33,7 +35,7 @@ const ( type CodeTimeExceeded uint8 const ( - CodeExceededInTransit CodeTimeExceeded = iota // TTL exceeded in transit + CodeExceededInTransit CodeTimeExceeded = iota // TTL exceeded CodeFragmentReassembly // fragment reassembly time exceeded ) diff --git a/ipv4/icmpv4/stringers.go b/ipv4/icmpv4/stringers.go new file mode 100644 index 0000000..5481f09 --- /dev/null +++ b/ipv4/icmpv4/stringers.go @@ -0,0 +1,93 @@ +// Code generated by "stringer -type=Type,CodeDestinationUnreachable,CodeRedirect -linecomment -output stringers.go"; DO NOT EDIT. + +package icmpv4 + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TypeEchoReply-0] + _ = x[TypeEcho-8] + _ = x[TypeDestinationUnreachable-3] + _ = x[TypeSourceQuench-4] + _ = x[TypeRedirect-5] + _ = x[TypeTimeExceeded-11] + _ = x[TypeParameterProblem-12] + _ = x[TypeTimestamp-13] + _ = x[TypeTimestampReply-14] + _ = x[TypeInfoRequest-15] + _ = x[TypeInfoRequestReply-16] +} + +const ( + _Type_name_0 = "echo reply" + _Type_name_1 = "destination unreachablesource quenchredirect" + _Type_name_2 = "echo" + _Type_name_3 = "time exceededparameter problemtimestamptimestamp replyinformation requestinformation request reply" +) + +var ( + _Type_index_1 = [...]uint8{0, 23, 36, 44} + _Type_index_3 = [...]uint8{0, 13, 30, 39, 54, 73, 98} +) + +func (i Type) String() string { + switch { + case i == 0: + return _Type_name_0 + case 3 <= i && i <= 5: + i -= 3 + return _Type_name_1[_Type_index_1[i]:_Type_index_1[i+1]] + case i == 8: + return _Type_name_2 + case 11 <= i && i <= 16: + i -= 11 + return _Type_name_3[_Type_index_3[i]:_Type_index_3[i+1]] + default: + return "Type(" + strconv.FormatInt(int64(i), 10) + ")" + } +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[CodeNetUnreachable-0] + _ = x[CodeHostUnreachable-1] + _ = x[CodeProtoUnreachable-2] + _ = x[CodePortUnreachable-3] + _ = x[CodeFragNeededAndDFSet-4] + _ = x[CodeSourceRouteFailed-5] +} + +const _CodeDestinationUnreachable_name = "net unreachablehost unreachableprotocol unreachableport unreachablefragmentation needed and DF setsource route failed" + +var _CodeDestinationUnreachable_index = [...]uint8{0, 15, 31, 51, 67, 98, 117} + +func (i CodeDestinationUnreachable) String() string { + if i >= CodeDestinationUnreachable(len(_CodeDestinationUnreachable_index)-1) { + return "CodeDestinationUnreachable(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _CodeDestinationUnreachable_name[_CodeDestinationUnreachable_index[i]:_CodeDestinationUnreachable_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[CodeRedirectForNetwork-0] + _ = x[CodeRedirectForHost-1] + _ = x[CodeRedirectForToSAndNetwork-2] + _ = x[CodeRedirectToSAndHost-3] +} + +const _CodeRedirect_name = "redirect for networkredirect for hostredirect for ToS+networkredirect for ToS+host" + +var _CodeRedirect_index = [...]uint8{0, 20, 37, 61, 82} + +func (i CodeRedirect) String() string { + if i >= CodeRedirect(len(_CodeRedirect_index)-1) { + return "CodeRedirect(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _CodeRedirect_name[_CodeRedirect_index[i]:_CodeRedirect_index[i+1]] +}