Skip to content

Commit

Permalink
Handle IPv6 extensions headers
Browse files Browse the repository at this point in the history
And add parsePacket unit test
  • Loading branch information
uablrek committed Jul 9, 2024
1 parent db089bc commit 169cc67
Show file tree
Hide file tree
Showing 4 changed files with 387 additions and 24 deletions.
103 changes: 79 additions & 24 deletions pkg/networkpolicy/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/hex"
"fmt"
"net"
"syscall"

v1 "k8s.io/api/core/v1"
)
Expand All @@ -20,6 +21,9 @@ type packet struct {
payload []byte
}

var ErrorTooShort = fmt.Errorf("packet too short")
var ErrorCorrupted = fmt.Errorf("packet corrupted")

func (p packet) String() string {
return fmt.Sprintf("[%d] %s:%d %s:%d %s\n%s", p.id, p.srcIP.String(), p.srcPort, p.dstIP.String(), p.dstPort, p.proto, hex.Dump(p.payload))
}
Expand All @@ -29,59 +33,110 @@ func (p packet) String() string {
// https://github.com/golang/net/blob/master/ipv4/header.go
func parsePacket(b []byte) (packet, error) {
t := packet{}
if b == nil {
return t, fmt.Errorf("empty payload")
if len(b) < 20 {
// 20 is the minimum length of an IPv4 header (IPv6 is 40)
return t, ErrorTooShort
}
version := int(b[0] >> 4)
// initialize variables
var hdrlen, protocol int
var protocol, l4offset, nxtHeader, payloadOffset int
switch version {
case 4:
t.family = v1.IPv4Protocol
hdrlen = int(b[0]&0x0f) << 2
if len(b) < hdrlen+4 {
return t, fmt.Errorf("payload to short, received %d expected at least %d", len(b), hdrlen+4)
hdrlen := int(b[0]&0x0f) * 4 // (header length in 32-bit words)
if hdrlen < 20 {
return t, ErrorCorrupted
}
l4offset = hdrlen
if l4offset >= len(b) {
return t, ErrorTooShort
}
t.srcIP = net.IPv4(b[12], b[13], b[14], b[15])
t.dstIP = net.IPv4(b[16], b[17], b[18], b[19])
protocol = int(b[9])
// IPv4 fragments:
// Since the conntracker is always used in K8s, IPv4 fragments
// will never be passed via the nfqueue. Packets are
// re-assembled by the kernel. Please see:
// https://unix.stackexchange.com/questions/650790/unwanted-defragmentation-of-forwarded-ipv4-packets
case 6:
t.family = v1.IPv6Protocol
hdrlen = 40
if len(b) < hdrlen+4 {
return t, fmt.Errorf("payload to short, received %d expected at least %d", len(b), hdrlen+4)
if len(b) < 48 {
// 40 is the minimum length of an IPv6 header, and 8 is
// the minimum lenght of an extension or L4 header
return t, ErrorTooShort
}
t.srcIP = make(net.IP, net.IPv6len)
copy(t.srcIP, b[8:24])
t.dstIP = make(net.IP, net.IPv6len)
copy(t.dstIP, b[24:40])
// NextHeader (not extension headers supported)
protocol = int(b[6])
// Handle extension headers.
nxtHeader = int(b[6])
l4offset = 40
for nxtHeader == syscall.IPPROTO_DSTOPTS || nxtHeader == syscall.IPPROTO_HOPOPTS || nxtHeader == syscall.IPPROTO_ROUTING {
// These headers have a lenght in 8-octet units, not
// including the first 8 octets
nxtHeader = int(b[l4offset])
l4offset += (8 + int(b[l4offset+1])*8)
// Now l4offset points to either another extension header,
// or an L4 header. So we must have at least 8 byte data
// after this (minimum extension header size)
if (l4offset + 8) >= len(b) {
return t, ErrorTooShort
}
}
if nxtHeader == syscall.IPPROTO_FRAGMENT {
// Only the first fragment has the L4 header
fragOffset := int(binary.BigEndian.Uint16(b[l4offset+2 : l4offset+4]))
if fragOffset&0xfff8 == 0 {
nxtHeader = int(b[l4offset])
l4offset += 8
// Here it's assumed that the fragment is the last
// extension header before the L4 header. But this is
// only a *recommendation*.
// TODO: handle extension headers in non-recommended order
} else {
// Not first fragment, we have no L4 header and the
// payload begins after this header
l4offset = -1
}
}
protocol = nxtHeader
default:
return t, fmt.Errorf("unknown versions %d", version)
return t, fmt.Errorf("unknown version %d", version)
}

var dataOffset int
havePorts := true
switch protocol {
case 6:
case syscall.IPPROTO_TCP:
t.proto = v1.ProtocolTCP
dataOffset = int(b[hdrlen+12] >> 4) // data offset
case 17:
dataOffset := int(b[l4offset+12]>>4) * 4
if dataOffset < 20 {
return t, ErrorCorrupted
}
payloadOffset = l4offset + dataOffset
case syscall.IPPROTO_UDP:
t.proto = v1.ProtocolUDP
dataOffset = hdrlen + 8 // data starts after
case 132:
payloadOffset = l4offset + 8
case syscall.IPPROTO_SCTP:
t.proto = v1.ProtocolSCTP
dataOffset = hdrlen + 8
payloadOffset = l4offset + 8
default:
return t, fmt.Errorf("unknown protocol %d", protocol)
}
// TCP, UDP and SCTP srcPort and dstPort are the first 4 bytes after the IP header
t.srcPort = int(binary.BigEndian.Uint16(b[hdrlen : hdrlen+2]))
t.dstPort = int(binary.BigEndian.Uint16(b[hdrlen+2 : hdrlen+4]))
if payloadOffset > len(b) {
// We must have a complete L4 header, however payload may be zero-lengh
return t, ErrorTooShort
}
if havePorts {
t.srcPort = int(binary.BigEndian.Uint16(b[l4offset : l4offset+2]))
t.dstPort = int(binary.BigEndian.Uint16(b[l4offset+2 : l4offset+4]))
}
// Obtain the offset of the payload
// TODO allow to filter by the payload
if len(b) >= hdrlen+dataOffset {
t.payload = b[hdrlen+dataOffset:]
if payloadOffset > 0 {
// Don't set t.payload for protocols we don't know
t.payload = b[payloadOffset:]
}
return t, nil
}
Loading

0 comments on commit 169cc67

Please sign in to comment.