mirror of
https://github.com/soypat/lneto.git
synced 2026-08-16 04:43:29 +00:00
66a1aec593
* begin adding better fuzz test * finish adding working fuzzer * implement mutateipv4 * fuzzer finds a panic in ICMP client on receive empty payload * rename fuzz tests to reflect fuzz methodology
440 lines
11 KiB
Go
440 lines
11 KiB
Go
package xnet
|
|
|
|
import (
|
|
"net/netip"
|
|
"testing"
|
|
|
|
"github.com/soypat/lneto"
|
|
"github.com/soypat/lneto/ethernet"
|
|
"github.com/soypat/lneto/http/httpraw"
|
|
"github.com/soypat/lneto/internal/ltesto"
|
|
"github.com/soypat/lneto/ipv4"
|
|
"github.com/soypat/lneto/tcp"
|
|
"github.com/soypat/lneto/udp"
|
|
)
|
|
|
|
func FuzzStackPacketHTTP(f *testing.F) {
|
|
const MTU = 1500
|
|
const seed = 1
|
|
var buf [MTU + ethernet.MaxOverheadSize]byte
|
|
s1, s2, c1, c2 := newTCPStacks(f, seed, MTU)
|
|
var hdr httpraw.Header
|
|
err := s1.ListenTCP(c1, 80)
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
err = s2.DialTCP(c2, 1337, netip.AddrPortFrom(s1.Addr(), c1.LocalPort()))
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
hdr.SetMethod("GET")
|
|
hdr.SetProtocol("HTTP/1.1")
|
|
hdr.SetRequestURI("/")
|
|
data := hdr.AppendHeaders(nil)
|
|
|
|
pktnum := 0
|
|
written := false
|
|
closed := false
|
|
for {
|
|
n1, err := s1.EgressEthernet(buf[:])
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
if n1 > 0 {
|
|
err = s2.IngressEthernet(buf[:n1])
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
f.Add(pktnum, buf[:n1])
|
|
pktnum++
|
|
if !written && c2.State() >= tcp.StateEstablished {
|
|
_, err = c2.Write(data)
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
written = true
|
|
}
|
|
}
|
|
n2, err := s2.EgressEthernet(buf[:])
|
|
if n2 > 0 {
|
|
pktnum++
|
|
err = s1.IngressEthernet(buf[:n2])
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
f.Add(pktnum, buf[:n2])
|
|
}
|
|
if n1 == 0 && n2 == 0 {
|
|
if !closed {
|
|
c2.Close()
|
|
closed = true
|
|
continue
|
|
}
|
|
break // No more data to send
|
|
}
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, pktnum int, a []byte) {
|
|
var buf [MTU + ethernet.MaxOverheadSize]byte
|
|
s1, s2, c1, c2 := newTCPStacks(t, seed, MTU)
|
|
err = s1.EnableICMP(true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = s2.EnableICMP(true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err := s1.ListenTCP(c1, 80)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = s2.DialTCP(c2, 1337, netip.AddrPortFrom(s1.Addr(), c1.LocalPort()))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pkt := 0
|
|
written := false
|
|
closed := false
|
|
const maxpkts = 100
|
|
for {
|
|
n1, err := s1.EgressEthernet(buf[:])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n1 > 0 {
|
|
if pkt == pktnum {
|
|
n1 = copy(buf[:], a)
|
|
fixIPTCPCRCs(buf[:n1])
|
|
}
|
|
s2.IngressEthernet(buf[:n1])
|
|
pkt++
|
|
if !written && c2.State() >= tcp.StateEstablished {
|
|
c2.Write(data)
|
|
written = true
|
|
}
|
|
}
|
|
n2, err := s2.EgressEthernet(buf[:])
|
|
if n2 > 0 {
|
|
if pkt == pktnum {
|
|
n2 = copy(buf[:], a)
|
|
fixIPTCPCRCs(buf[:n2])
|
|
}
|
|
pkt++
|
|
s1.IngressEthernet(buf[:n2])
|
|
}
|
|
if n1 == 0 && n2 == 0 {
|
|
if !closed {
|
|
if c1.BufferedInput() > 0 {
|
|
var hdr httpraw.Header
|
|
n, _ := c1.Read(buf[:])
|
|
hdr.ReadFromBytes(buf[:n])
|
|
hdr.TryParse(false)
|
|
}
|
|
c2.Close()
|
|
closed = true
|
|
continue
|
|
}
|
|
break // No more data to send
|
|
}
|
|
if pkt > maxpkts {
|
|
panic("infinite retransmission loop")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// fixIPTCPCRCs corrects CRCs of IP and TCP headers so that
|
|
// fuzzed packets are not discarded 99.9999% of the time.
|
|
func fixIPTCPCRCs(pkt []byte) (fixable bool) {
|
|
efrm, err := ethernet.NewFrame(pkt)
|
|
if err != nil || efrm.EtherTypeOrSize() != ethernet.TypeIPv4 {
|
|
return false
|
|
}
|
|
ifrm, err := ipv4.NewFrame(efrm.Payload())
|
|
if err != nil {
|
|
return false
|
|
}
|
|
v, ihl := ifrm.VersionAndIHL()
|
|
tl := ifrm.TotalLength()
|
|
if v != 4 || ihl < 5 || tl < uint16(ihl)*4 || int(tl) > len(pkt) {
|
|
return false // Invalid frame
|
|
}
|
|
var crc lneto.CRC791
|
|
ifrm.SetCRC(0)
|
|
ifrm.CRCWriteHeader(&crc)
|
|
ifrm.SetCRC(crc.Sum16())
|
|
if ifrm.Protocol() != lneto.IPProtoTCP {
|
|
return false
|
|
}
|
|
IPpayload := ifrm.Payload()
|
|
tfrm, err := tcp.NewFrame(IPpayload)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
crc.Reset()
|
|
ifrm.CRCWriteTCPPseudo(&crc)
|
|
// Zero the CRC field so its value does not add to the final result.
|
|
tfrm.SetCRC(0)
|
|
crcValue := crc.PayloadSum16(IPpayload)
|
|
tfrm.SetCRC(crcValue)
|
|
return true
|
|
}
|
|
|
|
func FuzzStackSeeded(f *testing.F) {
|
|
f.Add(int64(1), int64(2), int64(3))
|
|
var pmut ltesto.PacketMut
|
|
f.Fuzz(func(t *testing.T, seed1, seed2, seedAction int64) {
|
|
if seed1 == 0 {
|
|
seed1++
|
|
}
|
|
if seed2 == 0 {
|
|
seed2++
|
|
}
|
|
const mtu = 1500
|
|
const mfl = mtu + 14 // frame length includes ethernet header
|
|
var buf [mfl]byte
|
|
var s1, s2 StackAsync
|
|
v1, v2 := byte(seed1), byte(seed2)
|
|
cfg1 := StackConfig{
|
|
Hostname: "s1",
|
|
StaticAddress: netip.AddrFrom4([4]byte{1, 0, 0, v1}),
|
|
RandSeed: seed1,
|
|
MaxActiveTCPPorts: 1,
|
|
MaxActiveUDPPorts: 1,
|
|
ICMPQueueLimit: 1 + int(v1%4),
|
|
MTU: mtu,
|
|
HardwareAddress: [6]byte{0x1, 0, 0, 0, 0, v1},
|
|
AcceptMulticast: v1%2 == 0,
|
|
}
|
|
err := s1.Reset(cfg1)
|
|
if err != nil {
|
|
t.Fatal(err, cfg1)
|
|
}
|
|
cfg2 := StackConfig{
|
|
Hostname: "s2",
|
|
StaticAddress: netip.AddrFrom4([4]byte{1, 0, 0, v2}),
|
|
RandSeed: seed2,
|
|
MaxActiveTCPPorts: 1,
|
|
MaxActiveUDPPorts: 1,
|
|
ICMPQueueLimit: 1 + int(v2%4),
|
|
MTU: mtu,
|
|
HardwareAddress: [6]byte{0x2, 0, 0, 0, 0, v2},
|
|
AcceptMulticast: v2%2 == 0,
|
|
}
|
|
err = s2.Reset(cfg2)
|
|
if err != nil {
|
|
t.Fatal(err, cfg2)
|
|
}
|
|
const maxActions = 100
|
|
const (
|
|
actionUDP = iota
|
|
actionTCP
|
|
actionICMP
|
|
actionARP
|
|
actionNone
|
|
actionLim
|
|
)
|
|
const (
|
|
pingMinPayload = 8
|
|
port1 = 8080
|
|
port2 = 80
|
|
bufsize = 64
|
|
)
|
|
var udp1, udp2 udp.Conn
|
|
var tcp1, tcp2 tcp.Conn
|
|
err = tcp1.Configure(tcp.ConnConfig{
|
|
RxBuf: make([]byte, bufsize),
|
|
TxBuf: make([]byte, bufsize),
|
|
TxPacketQueueSize: 1 + int(s1.Prand32())%10,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = tcp2.Configure(tcp.ConnConfig{
|
|
RxBuf: make([]byte, bufsize),
|
|
TxBuf: make([]byte, bufsize),
|
|
TxPacketQueueSize: 1 + int(s2.Prand32())%10,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = udp1.Configure(udp.ConnConfig{
|
|
RxBuf: make([]byte, bufsize),
|
|
TxBuf: make([]byte, bufsize),
|
|
RxQueueSize: int(1 + s1.Prand32()%10),
|
|
TxQueueSize: int(1 + s1.Prand32()%10),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = udp2.Configure(udp.ConnConfig{
|
|
RxBuf: make([]byte, bufsize),
|
|
TxBuf: make([]byte, bufsize),
|
|
RxQueueSize: int(1 + s2.Prand32()%10),
|
|
TxQueueSize: int(1 + s2.Prand32()%10),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
icmpEnabled := false
|
|
udpOrder := 0
|
|
betsAreOff := false // When a packet is mutated all bets on which error can be returned are off.
|
|
for i := 0; i < maxActions; i++ {
|
|
action1 := s1.Prand32()
|
|
switch action1 % actionLim {
|
|
case actionTCP:
|
|
state1 := tcp1.State()
|
|
state2 := tcp2.State()
|
|
if state1 == 0 && state2 == 0 {
|
|
err = s1.DialTCP(&tcp1, port1, netip.AddrPortFrom(s2.Addr(), port2))
|
|
if err != nil {
|
|
t.Fatal(i, err)
|
|
}
|
|
err = s2.ListenTCP(&tcp2, port2)
|
|
if err != nil {
|
|
t.Fatal(i, err)
|
|
}
|
|
} else if state1 == tcp.StateEstablished && state2 == tcp.StateEstablished {
|
|
// For now just close after established.
|
|
if s1.Prand32()%2 == 0 {
|
|
tcp1.Close()
|
|
} else {
|
|
tcp2.Close()
|
|
}
|
|
}
|
|
case actionUDP:
|
|
// Ensure connections open.
|
|
if !udp1.IsOpen() {
|
|
err = s1.DialUDP(&udp1, port1, netip.AddrPortFrom(s2.Addr(), port2))
|
|
if err != nil {
|
|
t.Fatal(i, err)
|
|
}
|
|
}
|
|
if !udp2.IsOpen() {
|
|
err = s2.DialUDP(&udp2, port2, netip.AddrPortFrom(s1.Addr(), port1))
|
|
if err != nil {
|
|
t.Fatal(i, err)
|
|
}
|
|
}
|
|
udpOrder++
|
|
action := s1.Prand32() % 8
|
|
switch action {
|
|
case 0:
|
|
if udp1.FreeOutput() > 0 {
|
|
udp1.Write([]byte{byte(udpOrder)})
|
|
}
|
|
case 1:
|
|
if udp1.BufferedInput() > 0 {
|
|
udp1.Read(buf[:])
|
|
}
|
|
case 2:
|
|
if udp2.FreeOutput() > 0 {
|
|
udp2.Write([]byte{byte(udpOrder)})
|
|
}
|
|
case 3:
|
|
if udp2.BufferedInput() > 0 {
|
|
udp2.Read(buf[:])
|
|
}
|
|
case 4:
|
|
udp1.Close()
|
|
case 5:
|
|
udp2.Close()
|
|
}
|
|
case actionICMP:
|
|
if !icmpEnabled {
|
|
err = s1.EnableICMP(true)
|
|
if err != nil {
|
|
t.Fatal(i, err)
|
|
}
|
|
err = s2.EnableICMP(true)
|
|
if err != nil {
|
|
t.Fatal(i, err)
|
|
}
|
|
icmpEnabled = true
|
|
}
|
|
if s1.Prand32()%2 == 0 {
|
|
s1.icmp.Reset()
|
|
_, err = s1.icmp.PingStart(s2.Addr().As4(), buf[:pingMinPayload], pingMinPayload+uint16(s1.Prand32())%pingMinPayload)
|
|
if err != nil {
|
|
t.Fatal(i, err)
|
|
}
|
|
} else {
|
|
s2.icmp.Reset()
|
|
_, err = s2.icmp.PingStart(s1.Addr().As4(), buf[:pingMinPayload], pingMinPayload+uint16(s2.Prand32())%pingMinPayload)
|
|
if err != nil {
|
|
t.Fatal(i, err)
|
|
}
|
|
}
|
|
case actionARP:
|
|
action := s1.Prand32() % 6
|
|
switch action {
|
|
case 0: // s1 queries s2 address.
|
|
s1.StartResolveHardwareAddress6(s2.Addr())
|
|
case 1: // s2 queries s1 address.
|
|
s2.StartResolveHardwareAddress6(s1.Addr())
|
|
case 2: // s1 checks query result for s2.
|
|
s1.ResultResolveHardwareAddress6(s2.Addr())
|
|
case 3: // s2 checks query result for s1.
|
|
s2.ResultResolveHardwareAddress6(s1.Addr())
|
|
case 4: // s1 discards pending query.
|
|
s1.DiscardResolveHardwareAddress6(s2.Addr())
|
|
case 5: // s2 discards pending query.
|
|
s2.DiscardResolveHardwareAddress6(s1.Addr())
|
|
}
|
|
}
|
|
// Exchange data while checking stack does not enter runaway infinite frame send loop.
|
|
first, second := &s1, &s2
|
|
if s1.Prand32()%2 == 0 {
|
|
first, second = second, first
|
|
}
|
|
// TODO(soypat): add specialized packet mutation by detecting protocol and modifying specific packet fields.
|
|
const maxConsecutivePackets = 6
|
|
mut := s1.Prand32()
|
|
|
|
for k := 0; k < maxConsecutivePackets; k++ {
|
|
n, err := first.EgressEthernet(buf[:])
|
|
if err != nil {
|
|
t.Fatal(i, k, err)
|
|
} else if n > 0 {
|
|
if mut&1 == 1 {
|
|
pmut.MutateEthernet(buf[:n], int64(s1.Prand32())|int64(s1.Prand32())<<32, int64(s1.Prand32())|int64(s1.Prand32())<<32)
|
|
betsAreOff = true
|
|
}
|
|
err = second.IngressEthernet(buf[:n])
|
|
if err != nil && !betsAreOff && err != lneto.ErrPacketDrop && err != lneto.ErrExhausted {
|
|
t.Fatal(i, k, err)
|
|
}
|
|
mut >>= 1
|
|
}
|
|
n, err = second.EgressEthernet(buf[:])
|
|
if err != nil {
|
|
t.Fatal(i, k, err)
|
|
} else if n > 0 {
|
|
if mut&1 == 1 {
|
|
pmut.MutateEthernet(buf[:n], int64(s1.Prand32())|int64(s1.Prand32())<<32, int64(s1.Prand32())|int64(s1.Prand32())<<32)
|
|
betsAreOff = true
|
|
}
|
|
mut >>= 1
|
|
err = first.IngressEthernet(buf[:n])
|
|
if err != nil && !betsAreOff && err != lneto.ErrPacketDrop && err != lneto.ErrExhausted {
|
|
t.Fatal(i, k, err)
|
|
}
|
|
}
|
|
}
|
|
n, err := first.EgressEthernet(buf[:])
|
|
if err != nil {
|
|
t.Fatal(i, "expected no errors after maxconsecutive", err)
|
|
} else if n > 0 {
|
|
t.Fatal(i, "expected no more data after max consecutive")
|
|
}
|
|
n, err = second.EgressEthernet(buf[:])
|
|
if err != nil {
|
|
t.Fatal(i, "expected no errors after maxconsecutive", err)
|
|
} else if n > 0 {
|
|
t.Fatal(i, "expected no more data after max consecutive")
|
|
}
|
|
}
|
|
})
|
|
}
|