diff --git a/examples/xnet/main.go b/examples/xnet/main.go index 2061de0..21e8a55 100644 --- a/examples/xnet/main.go +++ b/examples/xnet/main.go @@ -42,6 +42,7 @@ func run() (err error) { flagRequestedIP = "" flagDoNTP = false flagHTTPGet = false + flagNoPcap = false ) flag.BoolVar(&flagHTTPGet, "httpget", flagHTTPGet, "Do an HTTP GET request ") flag.StringVar(&flagInterface, "i", flagInterface, "Interface to use. Either tap* or the name of an existing interface to bridge to.") @@ -49,6 +50,7 @@ func run() (err error) { flag.StringVar(&flagHostToResolve, "host", flagHostToResolve, "Hostname to resolve via DNS.") flag.StringVar(&flagRequestedIP, "addr", flagRequestedIP, "IP address to request via DHCP.") flag.BoolVar(&flagDoNTP, "ntp", flagDoNTP, "Do NTP round and print result time") + flag.BoolVar(&flagNoPcap, "nopcap", flagNoPcap, "Disable pcap logging.") flag.Parse() fmt.Println("softrand", softRand) _, err = dns.NewName(flagHostToResolve) @@ -108,18 +110,37 @@ func run() (err error) { buf := make([]byte, mtu) var cap pcap.PacketBreakdown var frames []pcap.Frame + pf := pcap.Formatter{ + FilterClasses: []pcap.FieldClass{pcap.FieldClassFlags, pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassAddress}, + } + var pfbuf []byte + logFrames := func(context string, pkt []byte) error { + if flagNoPcap { + return nil + } + frames, err = cap.CaptureEthernet(frames[:0], pkt, 0) + if err != nil { + return err + } + pfbuf = append(pfbuf[:0], context...) + pfbuf = append(pfbuf, ' ', '[') + pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt) + pfbuf = append(pfbuf, ']', '\n') + if err != nil { + return err + } + _, err = os.Stdout.Write(pfbuf) + return err + } for { clear(buf) nwrite, err := stack.Encapsulate(buf[:], -1, 0) if err != nil { - fmt.Println("ERR:ENCAPSULATE", err) + log.Println("ERR:ENCAPSULATE", err) } else if nwrite > 0 { - frames, err = cap.CaptureEthernet(frames[:0], buf[:nwrite], 0) + err = logFrames("OUT", buf[:nwrite]) if err != nil { - log.Println("ERR capture", err) - } - if len(frames) > 0 { - fmt.Println("OUT", frames) + log.Println("ERR:OUTLOG", err) } n, err := iface.Write(buf[:nwrite]) if err != nil { @@ -137,8 +158,10 @@ func run() (err error) { err = stack.Demux(buf[:nread], 0) if err != nil { if !errors.Is(err, lneto.ErrPacketDrop) { - frames, err = cap.CaptureEthernet(frames[:0], buf[:nread], 0) - log.Println("groutine demux:", err, frames) + err = logFrames("IN", buf[:nread]) + if err != nil { + log.Println("ERR:INLOG", err) + } } } } diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 759a3e8..b5a1c7e 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -3,14 +3,9 @@ package pcap //go:generate stringer -type=FieldClass -linecomment -output stringers.go . import ( "encoding/binary" - "encoding/hex" "errors" "fmt" "math" - "net/netip" - "slices" - "strconv" - "strings" "github.com/soypat/lneto" "github.com/soypat/lneto/arp" @@ -538,6 +533,7 @@ func (frm Frame) FieldAsUint(fieldIdx int, pkt []byte) (uint64, error) { return fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.RightAligned) } +// AppendField appends the binary on-the-wire representation of the field and aligns the field so it starts at the first bit of appended data. func (frm Frame) AppendField(dst []byte, fieldIdx int, pkt []byte) ([]byte, error) { if fieldIdx < 0 || fieldIdx >= len(frm.Fields) { return dst, errors.New("invalid field index") @@ -616,129 +612,6 @@ func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool) return dst, nil } -type Formatter struct { - FieldSep string - FrameSep string - FilterClasses []FieldClass - buf []byte -} - -func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte, err error) { - sep := f.FrameSep - if sep == "" { - sep = " | " - } - for ifrm := range frms { - if ifrm != 0 { - dst = append(dst, sep...) - } - dst, err = f.FormatFrame(dst, frms[ifrm], pkt) - if err != nil { - return dst, err - } - } - return dst, nil -} - -func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, err error) { - sep := f.FieldSep - if sep == "" { - sep = "; " // default field separator - } - bitlen := frm.LenBits() - if bitlen%8 == 0 { - dst = fmt.Appendf(dst, "%s len=%d", frm.Protocol, bitlen/8) - } else { - dst = fmt.Appendf(dst, "%s bitlen=%d", frm.Protocol, bitlen) - } - - for ifield := range frm.Fields { - field := frm.Fields[ifield] - if f.filterField(field) { - continue - } - dst = append(dst, sep...) - if field.Class == FieldClassFlags && frm.Protocol == lneto.IPProtoTCP { - dst = append(dst, "flags="...) - v, err := fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.RightAligned) - if err != nil { - return dst, err - } - dst = tcp.Flags(v).AppendFormat(dst) - continue - } - dst, err = f.formatField(dst, frm.PacketBitOffset, field, pkt) - if err != nil { - return dst, err - } - } - return dst, nil -} - -func (f *Formatter) filterField(field FrameField) bool { - return f.FilterClasses != nil && !slices.Contains(f.FilterClasses, field.Class) -} - -func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) { - return f.formatField(dst, pktStartOff, field, pkt) -} - -func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) { - name := field.Name - if name == "" { - name = field.Class.String() - } - hasSpaces := strings.IndexByte(name, ' ') >= 0 - if hasSpaces { - dst = append(dst, '(') - } - dst = append(dst, name...) - if hasSpaces { - dst = append(dst, ')') - } - dst = append(dst, '=') - f.buf, err = appendField(f.buf[:0], pkt, field.FrameBitOffset+pktStartOff, field.BitLength, field.RightAligned) - if err != nil { - return dst, err - } - fieldBitStart := pktStartOff + field.FrameBitOffset - switch field.Class { - default: - fallthrough - case FieldClassChecksum, FieldClassID, FieldClassFlags, FieldClassOptions, FieldClassAddress: - // Binary data to be printed as hexadecimal. - dst = append(dst, "0x"...) - dst = hex.AppendEncode(dst, f.buf) - case FieldClassDst, FieldClassSrc, FieldClassSize: - // IP, MAC addresses and ports. - if field.BitLength <= 16 { - v, err := fieldAsUint(pkt, fieldBitStart, field.BitLength, field.RightAligned) - if err != nil { - return dst, err - } - dst = strconv.AppendUint(dst, v, 10) - } else if field.BitLength == 4*8 { - dst = netip.AddrFrom4([4]byte(f.buf)).AppendTo(dst) - } else if field.BitLength == 6*8 { - for i := range f.buf { - if i != 0 { - dst = append(dst, ':') - } - if f.buf[i] < 16 { - dst = append(dst, '0') - } - dst = strconv.AppendUint(dst, uint64(f.buf[i]), 16) - } - } else if field.BitLength == 16*8 { - dst = netip.AddrFrom16([16]byte(f.buf)).AppendTo(dst) - } else { - dst = append(dst, "0x"...) - dst = hex.AppendEncode(dst, f.buf) - } - } - return dst, err -} - func (frm Frame) String() string { return string(frm.AppendString(nil)) } @@ -787,7 +660,7 @@ const ( FieldClassProto // protocol FieldClassVersion // version FieldClassType // type - FieldClassSize // field size + FieldClassSize // size FieldClassFlags // flags FieldClassID // identification FieldClassChecksum // checksum @@ -795,6 +668,8 @@ const ( FieldClassPayload // payload FieldClassText // text FieldClassAddress // address + // FieldClassBinaryText represents long stretches of binary data such as BOOTP DHCPv4 field. + FieldClassBinaryText // binary-text ) const octet = 8 @@ -1122,7 +997,7 @@ var baseDHCPv4Fields = [...]FrameField{ }, { Name: "BOOTP", - Class: FieldClassAddress, + Class: FieldClassBinaryText, FrameBitOffset: (28 + 16) * octet, BitLength: (dhcpv4.OptionsOffset - (28 + 16)) * octet, }, diff --git a/internet/pcap/format.go b/internet/pcap/format.go new file mode 100644 index 0000000..e006454 --- /dev/null +++ b/internet/pcap/format.go @@ -0,0 +1,142 @@ +package pcap + +import ( + "encoding/hex" + "fmt" + "net/netip" + "slices" + "strconv" + "strings" + + "github.com/soypat/lneto" + "github.com/soypat/lneto/tcp" +) + +type Formatter struct { + FieldSep string + FrameSep string + FilterClasses []FieldClass + buf []byte +} + +// FormatFrames appends the formatted frame data to the destination buffer according the Formatter state. +// Is equivalent to [Formatter.FormatFrame] called on each Frame with the FrameSep inserted between frames. +func (f *Formatter) FormatFrames(dst []byte, frms []Frame, pkt []byte) (_ []byte, err error) { + sep := f.FrameSep + if sep == "" { + sep = " | " + } + for ifrm := range frms { + if ifrm != 0 { + dst = append(dst, sep...) + } + dst, err = f.FormatFrame(dst, frms[ifrm], pkt) + if err != nil { + return dst, err + } + } + return dst, nil +} + +// FormatFrame +func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, err error) { + sep := f.FieldSep + if sep == "" { + sep = "; " // default field separator + } + bitlen := frm.LenBits() + if bitlen%8 == 0 { + dst = fmt.Appendf(dst, "%s len=%d", frm.Protocol, bitlen/8) + } else { + dst = fmt.Appendf(dst, "%s bitlen=%d", frm.Protocol, bitlen) + } + + for ifield := range frm.Fields { + field := frm.Fields[ifield] + if f.filterField(field) { + continue + } + dst = append(dst, sep...) + if field.Class == FieldClassFlags && frm.Protocol == lneto.IPProtoTCP { + // TCP flags pretty print special case. + dst = append(dst, "flags="...) + v, err := fieldAsUint(pkt, frm.PacketBitOffset+field.FrameBitOffset, field.BitLength, field.RightAligned) + if err != nil { + return dst, err + } + dst = tcp.Flags(v).AppendFormat(dst) + continue + } + dst, err = f.formatField(dst, frm.PacketBitOffset, field, pkt) + if err != nil { + return dst, err + } + } + return dst, nil +} + +func (f *Formatter) filterField(field FrameField) bool { + return f.FilterClasses != nil && !slices.Contains(f.FilterClasses, field.Class) +} + +func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) { + return f.formatField(dst, pktStartOff, field, pkt) +} + +func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) { + name := field.Name + if name == "" { + name = field.Class.String() + } + hasSpaces := strings.IndexByte(name, ' ') >= 0 + if hasSpaces { + dst = append(dst, '(') + } + dst = append(dst, name...) + if hasSpaces { + dst = append(dst, ')') + } + dst = append(dst, '=') + f.buf, err = appendField(f.buf[:0], pkt, field.FrameBitOffset+pktStartOff, field.BitLength, field.RightAligned) + if err != nil { + return dst, err + } + fieldBitStart := pktStartOff + field.FrameBitOffset + switch field.Class { + default: + fallthrough + case FieldClassChecksum, FieldClassID, FieldClassFlags, FieldClassOptions: + // Binary data to be printed as hexadecimal. + dst = append(dst, "0x"...) + dst = hex.AppendEncode(dst, f.buf) + case FieldClassText: + dst = strconv.AppendQuote(dst, string(f.buf)) + case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress: + // IP, MAC addresses and ports. + if field.BitLength <= 16 { + v, err := fieldAsUint(pkt, fieldBitStart, field.BitLength, field.RightAligned) + if err != nil { + return dst, err + } + dst = strconv.AppendUint(dst, v, 10) + } else if field.BitLength == 4*8 { + dst = netip.AddrFrom4([4]byte(f.buf)).AppendTo(dst) + } else if field.BitLength == 6*8 { + for i := range f.buf { + if i != 0 { + dst = append(dst, ':') + } + if f.buf[i] < 16 { + dst = append(dst, '0') + } + dst = strconv.AppendUint(dst, uint64(f.buf[i]), 16) + } + } else if field.BitLength == 16*8 { + dst = netip.AddrFrom16([16]byte(f.buf)).AppendTo(dst) + } else { + dst = append(dst, "0x"...) + dst = hex.AppendEncode(dst, f.buf) + } + } + return dst, err +} diff --git a/internet/pcap/stringers.go b/internet/pcap/stringers.go index cd8a9be..3af28c7 100644 --- a/internet/pcap/stringers.go +++ b/internet/pcap/stringers.go @@ -22,11 +22,12 @@ func _() { _ = x[FieldClassPayload-11] _ = x[FieldClassText-12] _ = x[FieldClassAddress-13] + _ = x[FieldClassBinaryText-14] } -const _FieldClass_name = "undefinedsourcedestinationprotocolversiontypefield sizeflagsidentificationchecksumoptionspayloadtextaddress" +const _FieldClass_name = "undefinedsourcedestinationprotocolversiontypesizeflagsidentificationchecksumoptionspayloadtextaddressbinary-text" -var _FieldClass_index = [...]uint8{0, 9, 15, 26, 34, 41, 45, 55, 60, 74, 82, 89, 96, 100, 107} +var _FieldClass_index = [...]uint8{0, 9, 15, 26, 34, 41, 45, 49, 54, 68, 76, 83, 90, 94, 101, 112} func (i FieldClass) String() string { if i >= FieldClass(len(_FieldClass_index)-1) {