diff --git a/examples/tap/main.go b/examples/tap/main.go index a5ac544..61a096a 100644 --- a/examples/tap/main.go +++ b/examples/tap/main.go @@ -12,6 +12,7 @@ import ( "time" "github.com/soypat/lneto/internal/ltesto" + "github.com/soypat/lneto/internet/pcap" ) func main() { @@ -39,6 +40,14 @@ func run() error { return err } defer sv.Close() + var cap pcap.PacketBreakdown + sv.OnTransfer(func(channel int, pkt []byte) { + captime := time.Now() + frames, err := cap.CaptureEthernet(nil, pkt, 0) + if err == nil { + fmt.Println(channel, captime.Format("15:04:05.000"), frames) + } + }) hwaddr, err := sv.HardwareAddress6() if err != nil { return err diff --git a/internal/ltesto/httptap.go b/internal/ltesto/httptap.go index 4787c68..897fa69 100644 --- a/internal/ltesto/httptap.go +++ b/internal/ltesto/httptap.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "log/slog" "net" "net/http" "net/netip" @@ -153,6 +152,7 @@ type HTTPTapServer struct { stack stack tap *internal.Tap buf []byte + onTx func(channel int, pkt []byte) tapfailed bool } @@ -162,6 +162,10 @@ type tapInfo struct { HardwareAddr string } +func (sv *HTTPTapServer) OnTransfer(cb func(channel int, pkt []byte)) { + sv.onTx = cb +} + func NewHTTPTapServer(iface string, ip netip.Prefix, mtu, queueOut, queueIn int) (*HTTPTapServer, error) { if mtu < minMTU { return nil, errors.New("too small MTU") @@ -176,15 +180,23 @@ func NewHTTPTapServer(iface string, ip netip.Prefix, mtu, queueOut, queueIn int) in: make(chan []byte, queueIn), } sv := http.NewServeMux() + taps := &HTTPTapServer{ + router: sv, + stack: s, + tap: tap, + buf: make([]byte, mtu), + } sv.HandleFunc("/send", func(w http.ResponseWriter, r *http.Request) { var data []byte err := json.NewDecoder(r.Body).Decode(&data) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } else { + if taps.onTx != nil { + taps.onTx(1, data) + } select { case s.out <- data: - slog.Info("http-send", slog.Int("plen", len(data))) default: http.Error(w, "outgoing packet queue full", http.StatusInternalServerError) } @@ -194,7 +206,6 @@ func NewHTTPTapServer(iface string, ip netip.Prefix, mtu, queueOut, queueIn int) select { case data := <-s.in: json.NewEncoder(w).Encode(data) - slog.Info("http-recv", slog.Int("plen", len(data))) default: json.NewEncoder(w).Encode("") // send empty string. } @@ -211,13 +222,8 @@ func NewHTTPTapServer(iface string, ip netip.Prefix, mtu, queueOut, queueIn int) } json.NewEncoder(w).Encode(info) }) - taps := HTTPTapServer{ - router: sv, - stack: s, - tap: tap, - buf: make([]byte, mtu), - } - return &taps, nil + + return taps, nil } func (sv *HTTPTapServer) HardwareAddress6() (hwaddr [6]byte, err error) { @@ -262,6 +268,9 @@ func (sv *HTTPTapServer) readTap() (int, error) { sv.tapfailed = true return n, err } else if n > 0 { + if sv.onTx != nil { + sv.onTx(0, buf[:n]) + } err = sv.stack.recv(buf[:n]) if err != nil { return n, err diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 9c9661f..50ddbee 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -1,8 +1,10 @@ package pcap +//go:generate stringer -type=FieldClass -linecomment -output stringers.go . import ( "encoding/binary" "errors" + "fmt" "math" "github.com/soypat/lneto" @@ -56,7 +58,7 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in case ethernet.TypeIPv4: dst, err = pc.CaptureIPv4(dst, pkt, end) default: - dst = append(dst, remainingFrameInfo(nil, FieldClassPayload, end, octet*len(pkt))) + dst = append(dst, remainingFrameInfo(etype, FieldClassPayload, end, octet*len(pkt))) } return dst, err } @@ -131,11 +133,13 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( } finfo.Fields = append(finfo.Fields, baseIPv4Fields[:]...) options := ifrm4.Options() - finfo.Fields = append(finfo.Fields, FrameField{ - Class: FieldClassOptions, - FrameBitOffset: 20 * octet, - BitLength: octet * len(options), - }) + if len(options) > 0 { + finfo.Fields = append(finfo.Fields, FrameField{ + Class: FieldClassOptions, + FrameBitOffset: 20 * octet, + BitLength: octet * len(options), + }) + } proto := ifrm4.Protocol() dst = append(dst, finfo) end := bitOffset + octet*ifrm4.HeaderLength() @@ -167,11 +171,13 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([ } finfo.Fields = append(finfo.Fields, baseTCPFields[:]...) options := tfrm.Options() - finfo.Fields = append(finfo.Fields, FrameField{ - Class: FieldClassOptions, - FrameBitOffset: 20 * octet, - BitLength: octet * len(options), - }) + if len(options) > 0 { + finfo.Fields = append(finfo.Fields, FrameField{ + Class: FieldClassOptions, + FrameBitOffset: 20 * octet, + BitLength: octet * len(options), + }) + } dst = append(dst, finfo) payload := tfrm.Payload() if len(payload) > 0 { @@ -274,7 +280,7 @@ func (frm Frame) FieldByClass(c FieldClass) (int, error) { } // FieldAsUint evaluates the field as a 64-bit integer. -func (frm *Frame) FieldAsUint(fieldIdx int, pkt []byte) (uint64, error) { +func (frm Frame) FieldAsUint(fieldIdx int, pkt []byte) (uint64, error) { const badUint64 = math.MaxUint64 if fieldIdx < 0 || fieldIdx >= len(frm.Fields) { return badUint64, errors.New("invalid field index") @@ -293,7 +299,7 @@ func (frm *Frame) FieldAsUint(fieldIdx int, pkt []byte) (uint64, error) { return v, nil } -func (frm *Frame) AppendField(dst []byte, fieldIdx int, pkt []byte) ([]byte, error) { +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") } @@ -349,6 +355,37 @@ func (frm *Frame) AppendField(dst []byte, fieldIdx int, pkt []byte) ([]byte, err return dst, nil } +func (frm Frame) String() string { + iopt, err := frm.FieldByClass(FieldClassOptions) + + hasOpts := "" + if err == nil { + hasOpts = fmt.Sprintf(" optlen=%d", (frm.Fields[iopt].BitLength+7)/8) + } + bitlen := frm.LenBits() + if bitlen%8 == 0 { + return fmt.Sprintf("%s len=%d%s", frm.Protocol, bitlen/8, hasOpts) + } + return fmt.Sprintf("%s bits=%d%s", frm.Protocol, bitlen, hasOpts) +} + +func (frm Frame) LenBits() (totalBitlen int) { + for i := range frm.Fields { + totalBitlen = max(totalBitlen, frm.Fields[i].FrameBitOffset+frm.Fields[i].BitLength) + } + return totalBitlen +} + +func (ff FrameField) String() string { + if ff.Class == FieldClassPayload { + return fmt.Sprintf("Payload len=%d", ff.BitLength/8) + } + if ff.Name != "" { + return fmt.Sprintf("%s (%s)", ff.Name, ff.Class.String()) + } + return ff.Class.String() +} + type FieldClass uint8 const ( diff --git a/internet/pcap/stringers.go b/internet/pcap/stringers.go new file mode 100644 index 0000000..0a5efb2 --- /dev/null +++ b/internet/pcap/stringers.go @@ -0,0 +1,35 @@ +// Code generated by "stringer -type=FieldClass -linecomment -output stringers.go ."; DO NOT EDIT. + +package pcap + +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[fieldClassUndefined-0] + _ = x[FieldClassSrc-1] + _ = x[FieldClassDst-2] + _ = x[FieldClassProto-3] + _ = x[FieldClassVersion-4] + _ = x[FieldClassType-5] + _ = x[FieldClassSize-6] + _ = x[FieldClassFlags-7] + _ = x[FieldClassID-8] + _ = x[FieldClassChecksum-9] + _ = x[FieldClassOptions-10] + _ = x[FieldClassPayload-11] + _ = x[FieldClassText-12] +} + +const _FieldClass_name = "undefinedsourcedestinationprotocolversiontypefield sizeflagsidentificationchecksumoptionspayloadtext" + +var _FieldClass_index = [...]uint8{0, 9, 15, 26, 34, 41, 45, 55, 60, 74, 82, 89, 96, 100} + +func (i FieldClass) String() string { + if i >= FieldClass(len(_FieldClass_index)-1) { + return "FieldClass(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _FieldClass_name[_FieldClass_index[i]:_FieldClass_index[i+1]] +}