From 407f4a08f11f2574737ad3036ab04f755497cf56 Mon Sep 17 00:00:00 2001 From: soypat Date: Tue, 21 Oct 2025 21:46:44 -0300 Subject: [PATCH] enhance xnet StackAsync test and add non-linux stubs for Tap and Bridge --- internal/tap.go | 2 +- internal/tap_nolinux.go | 68 ++++++++++++++++++++++++ internet/pcap/capture.go | 19 +++++-- tcp/control.go | 3 ++ x/xnet/xnet_test.go | 111 +++++++++++++++++++++++++++++++-------- 5 files changed, 177 insertions(+), 26 deletions(-) create mode 100644 internal/tap_nolinux.go diff --git a/internal/tap.go b/internal/tap.go index 62dd488..535a6bb 100644 --- a/internal/tap.go +++ b/internal/tap.go @@ -1,4 +1,4 @@ -//go:build linux && !baremetal +//go:build linux && !tinygo package internal diff --git a/internal/tap_nolinux.go b/internal/tap_nolinux.go new file mode 100644 index 0000000..5706298 --- /dev/null +++ b/internal/tap_nolinux.go @@ -0,0 +1,68 @@ +//go:build !linux || tinygo + +package internal + +import ( + "errors" + "net/netip" +) + +type Tap struct { +} + +func NewTap(name string, ip netip.Prefix) (*Tap, error) { + return nil, errors.ErrUnsupported +} + +func (tap *Tap) IPMask() (netip.Prefix, error) { + return netip.Prefix{}, errors.ErrUnsupported +} +func (tap *Tap) Read(b []byte) (int, error) { + return -1, errors.ErrUnsupported +} +func (tap *Tap) Write(b []byte) (int, error) { + return -1, errors.ErrUnsupported +} +func (tap *Tap) Close() error { + return errors.ErrUnsupported +} +func (tap *Tap) MTU() (int, error) { + return -1, errors.ErrUnsupported +} +func (tap *Tap) HardwareAddress6() (hw [6]byte, err error) { + return hw, errors.ErrUnsupported +} + +type Bridge struct { +} + +func NewBridge(name string) (*Bridge, error) { + return nil, errors.ErrUnsupported +} +func (br *Bridge) Write(frame []byte) (int, error) { + return -1, errors.ErrUnsupported +} +func (br *Bridge) Read(frame []byte) (int, error) { + return -1, errors.ErrUnsupported +} +func (br *Bridge) Close() error { + return errors.ErrUnsupported +} +func (tap *Bridge) MTU() (int, error) { + return -1, errors.ErrUnsupported +} +func (tap *Bridge) HardwareAddress6() (hw [6]byte, err error) { + return hw, errors.ErrUnsupported +} + +func (br *Bridge) SetHardwareAddress6(hw [6]byte) error { + return errors.ErrUnsupported +} + +func (br *Bridge) IPMask() (netip.Prefix, error) { + return netip.Prefix{}, errors.ErrUnsupported +} + +func (br *Bridge) Addr() (netip.Addr, error) { + return netip.Addr{}, errors.ErrUnsupported +} diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index 07f63af..4c62c5f 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -22,6 +22,16 @@ import ( const unknownPayloadProto = "payload?" +var ( + ErrFieldByClassNotFound = errors.New("pcap: field by class not found") +) + +type proto string + +const ( + ProtoEthernet proto = "Ethernet" +) + type PacketBreakdown struct { hdr httpraw.Header dmsg dns.Message @@ -42,7 +52,7 @@ func (pc *PacketBreakdown) CaptureEthernet(dst []Frame, pkt []byte, bitOffset in } finfo := Frame{ - Protocol: "Ethernet", + Protocol: ProtoEthernet, PacketBitOffset: bitOffset, } finfo.Fields = append(finfo.Fields, baseEthernetFields[:]...) @@ -208,7 +218,8 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( end := bitOffset + octet*ifrm4.HeaderLength() var protoErrs []error var crc lneto.CRC791 - if proto == lneto.IPProtoTCP { + switch proto { + case lneto.IPProtoTCP: ifrm4.CRCWriteTCPPseudo(&crc) tfrm, err := tcp.NewFrame(ifrm4.Payload()) if err == nil { @@ -224,7 +235,7 @@ func (pc *PacketBreakdown) CaptureIPv4(dst []Frame, pkt []byte, bitOffset int) ( protoErrs = append(protoErrs, &crcError16{protocol: "ipv4+tcp", want: wantSum, got: gotSum}) } } - } else if proto == lneto.IPProtoUDP { + case lneto.IPProtoUDP: ifrm4.CRCWriteUDPPseudo(&crc) ufrm, err := udp.NewFrame(ifrm4.Payload()) if err == nil { @@ -504,7 +515,7 @@ func (frm Frame) FieldByClass(c FieldClass) (int, error) { } } if selected < 0 { - return -1, errors.New("field by class not found") + return -1, ErrFieldByClassNotFound } if multiple && frm.Fields[selected].Name != "" { return -1, errors.New("multiple classes found and none have empty name") diff --git a/tcp/control.go b/tcp/control.go index f2c7a0c..ce2e465 100644 --- a/tcp/control.go +++ b/tcp/control.go @@ -202,6 +202,9 @@ func (tcb *ControlBlock) PendingSegment(payloadLen int) (_ Segment, ok bool) { } payloadLen = int(maxPayload) } + if payloadLen > 0 { + pending |= FlagPSH // By default ensure all data flushed to destination application immediately on receive. + } if established { pending |= FlagACK // ACK is always set in established state. Not in RFC9293 but somehow expected? diff --git a/x/xnet/xnet_test.go b/x/xnet/xnet_test.go index 64cf166..adb062d 100644 --- a/x/xnet/xnet_test.go +++ b/x/xnet/xnet_test.go @@ -2,10 +2,12 @@ package xnet import ( "bytes" + "errors" "net/netip" "testing" "github.com/soypat/lneto" + "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/internet/pcap" "github.com/soypat/lneto/tcp" ) @@ -140,16 +142,16 @@ func (tst *tester) TCPExchange(expect tcpExpectExchange, stack1, stack2 *StackAs t := tst.t buf := tst.buf nodata := expect.WantFlags == 0 - var n int - var err error + var src, dst *StackAsync switch expect.SourceIdx { case 0: - n, err = stack1.Encapsulate(buf[:], 0) + src, dst = stack1, stack2 case 1: - n, err = stack2.Encapsulate(buf[:], 0) + src, dst = stack2, stack1 default: panic("OOB") } + n, err := src.Encapsulate(buf[:], 0) if err != nil { t.Fatal(err) } else if n == 0 { @@ -158,38 +160,105 @@ func (tst *tester) TCPExchange(expect tcpExpectExchange, stack1, stack2 *StackAs } t.Error("zero bits sent") } + + tst.buf = tst.buf[:n] + defer func() { + tst.buf = tst.buf[:cap(tst.buf)] + }() tst.frmbuf, err = tst.cap.CaptureEthernet(tst.frmbuf[:0], buf[:n], 0) if err != nil { t.Fatal(err) } - tfrm := getProtoFrame(tst.frmbuf, lneto.IPProtoTCP) - if tfrm == nil { - t.Fatal("where's the TCP?") + srcEth := src.HardwareAddress() + dstEth := dst.HardwareAddress() + if !bytes.Equal(srcEth[:], tst.getData(pcap.ProtoEthernet, pcap.FieldClassSrc)) { + t.Errorf("mismatched ethernet src addr %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassSrc)) } - fidx, _ := tfrm.FieldByClass(pcap.FieldClassFlags) - flags, _ := tfrm.FieldAsUint(fidx, buf[:n]) - tflags := tcp.Flags(flags) - var payload []byte - fidx, _ = tfrm.FieldByClass(pcap.FieldClassPayload) - if fidx > 0 { - fieldPayload := tfrm.Fields[fidx] - payload = buf[fieldPayload.FrameBitOffset*8:] + if !bytes.Equal(dstEth[:], tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst)) { + t.Errorf("mismatched ethernet dst addr %x", tst.getData(pcap.ProtoEthernet, pcap.FieldClassDst)) } + if tst.getInt(ethernet.TypeIPv4, pcap.FieldClassVersion) != 4 { + t.Errorf("did not get IP version=4, got=%d", tst.getInt(ethernet.TypeIPv4, pcap.FieldClassVersion)) + } + srcAddr := src.Addr() + dstAddr := dst.Addr() + if !bytes.Equal(srcAddr.AsSlice(), tst.getData(ethernet.TypeIPv4, pcap.FieldClassSrc)) { + t.Errorf("mismatched ip src addr %d", tst.getData(ethernet.TypeIPv4, pcap.FieldClassSrc)) + } + if !bytes.Equal(dstAddr.AsSlice(), tst.getData(ethernet.TypeIPv4, pcap.FieldClassDst)) { + t.Errorf("mismatched ip dst addr %d", tst.getData(ethernet.TypeIPv4, pcap.FieldClassDst)) + } + tflags := tcp.Flags(tst.getInt(lneto.IPProtoTCP, pcap.FieldClassFlags)) + payload := tst.getPayload(lneto.IPProtoTCP) if !bytes.Equal(payload, expect.WantData) { t.Errorf("mismatched data sent, \nwant=%q\ngot=%q\n", expect.WantData, payload) } if tflags != expect.WantFlags { t.Errorf("expected flags %s, got %s", expect.WantFlags.String(), tflags.String()) } - switch expect.SourceIdx { - case 0: - err = stack2.Demux(buf[:], 0) - case 1: - err = stack1.Demux(buf[:], 0) - } + err = dst.Demux(buf[:], 0) if err != nil { t.Fatal(err) } + for i := range buf[:n] { + buf[i] = 0 // Set data sent to zero. + } +} + +func (tst *tester) getPayload(proto any) []byte { + tst.t.Helper() + i := 0 + for i = 0; i < len(tst.frmbuf); i++ { + if tst.frmbuf[i].Protocol == proto { + if i < len(tst.frmbuf)-1 { + frm := &tst.frmbuf[i+1] + bitOff := frm.PacketBitOffset + if bitOff%8 != 0 { + tst.t.Fatalf("proto %s bitoffset not multiple of 8: %d", proto, bitOff) + } + return tst.buf[bitOff/8:] + } + } + } + return tst.getData(proto, pcap.FieldClassPayload) +} + +func (tst *tester) getData(proto any, field pcap.FieldClass) []byte { + tst.t.Helper() + frm := getProtoFrame(tst.frmbuf, proto) + if frm == nil { + tst.t.Fatalf("no frame for proto %s found in %s", proto, tst.frmbuf) + } + fidx, err := frm.FieldByClass(field) + if err != nil { + if errors.Is(err, pcap.ErrFieldByClassNotFound) { + return nil + } + tst.t.Fatal(err) + } + bitoff := frm.PacketBitOffset + frm.Fields[fidx].FrameBitOffset + bitlen := frm.Fields[fidx].BitLength + if bitoff%8 != 0 || bitlen%8 != 0 { + tst.t.Fatal("frame bitlength not multiple of 8") + } + return tst.buf[bitoff/8 : bitoff/8+bitlen/8] +} + +func (tst *tester) getInt(proto any, field pcap.FieldClass) uint64 { + tst.t.Helper() + frm := getProtoFrame(tst.frmbuf, proto) + if frm == nil { + tst.t.Fatalf("no frame for proto %s found in %s", proto, tst.frmbuf) + } + fidx, err := frm.FieldByClass(field) + if err != nil { + tst.t.Fatal(err) + } + v, err := frm.FieldAsUint(fidx, tst.buf) + if err != nil { + tst.t.Fatal(err) + } + return v } func getProtoFrame(frms []pcap.Frame, proto any) *pcap.Frame {