wgengine/packet: refactor and expose UDP header marshaling (#408)
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
This commit is contained in:

committed by
GitHub

parent
5e0ff494a5
commit
059b1d10bb
@ -7,75 +7,39 @@ package packet
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/strbuilder"
|
||||
)
|
||||
|
||||
type IPProto int
|
||||
|
||||
const (
|
||||
Junk IPProto = iota
|
||||
Fragment
|
||||
ICMP
|
||||
UDP
|
||||
TCP
|
||||
)
|
||||
|
||||
// RFC1858: prevent overlapping fragment attacks.
|
||||
const minFrag = 60 + 20 // max IPv4 header + basic TCP header
|
||||
|
||||
func (p IPProto) String() string {
|
||||
switch p {
|
||||
case Fragment:
|
||||
return "Frag"
|
||||
case ICMP:
|
||||
return "ICMP"
|
||||
case UDP:
|
||||
return "UDP"
|
||||
case TCP:
|
||||
return "TCP"
|
||||
default:
|
||||
return "Junk"
|
||||
}
|
||||
}
|
||||
|
||||
// IP is an IPv4 address.
|
||||
type IP uint32
|
||||
|
||||
// NewIP converts a standard library IP address into an IP.
|
||||
// It panics if b is not an IPv4 address.
|
||||
func NewIP(b net.IP) IP {
|
||||
b4 := b.To4()
|
||||
if b4 == nil {
|
||||
panic(fmt.Sprintf("To4(%v) failed", b))
|
||||
}
|
||||
return IP(binary.BigEndian.Uint32(b4))
|
||||
}
|
||||
|
||||
func (ip IP) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip))
|
||||
}
|
||||
|
||||
// ICMP types.
|
||||
const (
|
||||
ICMPEchoReply = 0x00
|
||||
ICMPEchoRequest = 0x08
|
||||
ICMPUnreachable = 0x03
|
||||
ICMPTimeExceeded = 0x0b
|
||||
)
|
||||
|
||||
const (
|
||||
TCPSyn = 0x02
|
||||
TCPAck = 0x10
|
||||
TCPSynAck = TCPSyn | TCPAck
|
||||
)
|
||||
|
||||
type QDecode struct {
|
||||
b []byte // Packet buffer that this decodes
|
||||
subofs int // byte offset of IP subprotocol
|
||||
var (
|
||||
get16 = binary.BigEndian.Uint16
|
||||
get32 = binary.BigEndian.Uint32
|
||||
|
||||
put16 = binary.BigEndian.PutUint16
|
||||
put32 = binary.BigEndian.PutUint32
|
||||
)
|
||||
|
||||
// ParsedPacket is a minimal decoding of a packet suitable for use in filters.
|
||||
type ParsedPacket struct {
|
||||
// b is the byte buffer that this decodes.
|
||||
b []byte
|
||||
// subofs is the offset of IP subprotocol.
|
||||
subofs int
|
||||
// dataofs is the offset of IP subprotocol payload.
|
||||
dataofs int
|
||||
// length is the total length of the packet.
|
||||
// This is not the same as len(b) because b can have trailing zeros.
|
||||
length int
|
||||
|
||||
IPProto IPProto // IP subprotocol (UDP, TCP, etc)
|
||||
SrcIP IP // IP source address
|
||||
@ -85,9 +49,12 @@ type QDecode struct {
|
||||
TCPFlags uint8 // TCP flags (SYN, ACK, etc)
|
||||
}
|
||||
|
||||
func (q *QDecode) String() string {
|
||||
if q.IPProto == Junk {
|
||||
return "Junk{}"
|
||||
func (q *ParsedPacket) String() string {
|
||||
switch q.IPProto {
|
||||
case IPv6:
|
||||
return "IPv6{???}"
|
||||
case Unknown:
|
||||
return "Unknown{???}"
|
||||
}
|
||||
sb := strbuilder.Get()
|
||||
sb.WriteString(q.IPProto.String())
|
||||
@ -117,7 +84,7 @@ func ipChecksum(b []byte) uint16 {
|
||||
i := 0
|
||||
n := len(b)
|
||||
for n >= 2 {
|
||||
ac += uint32(binary.BigEndian.Uint16(b[i : i+2]))
|
||||
ac += uint32(get16(b[i : i+2]))
|
||||
n -= 2
|
||||
i += 2
|
||||
}
|
||||
@ -130,71 +97,44 @@ func ipChecksum(b []byte) uint16 {
|
||||
return uint16(^ac)
|
||||
}
|
||||
|
||||
var put16 = binary.BigEndian.PutUint16
|
||||
var put32 = binary.BigEndian.PutUint32
|
||||
|
||||
// GenICMP returns the bytes of an ICMP packet.
|
||||
// If payload is too short or too long, it returns nil.
|
||||
func GenICMP(srcIP, dstIP IP, ipid uint16, icmpType, icmpCode uint8, payload []byte) []byte {
|
||||
if len(payload) < 4 {
|
||||
return nil
|
||||
}
|
||||
if len(payload) > 65535-24 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sz := 24 + len(payload)
|
||||
out := make([]byte, 24+len(payload))
|
||||
out[0] = 0x45 // IPv4, 20-byte header
|
||||
out[1] = 0x00 // DHCP, ECN
|
||||
put16(out[2:4], uint16(sz))
|
||||
put16(out[4:6], ipid)
|
||||
put16(out[6:8], 0) // flags, offset
|
||||
out[8] = 64 // TTL
|
||||
out[9] = 0x01 // ICMPv4
|
||||
// out[10:12] = 0x00 // blank IP header checksum
|
||||
put32(out[12:16], uint32(srcIP))
|
||||
put32(out[16:20], uint32(dstIP))
|
||||
|
||||
out[20] = icmpType
|
||||
out[21] = icmpCode
|
||||
//out[22:24] = 0x00 // blank ICMP checksum
|
||||
copy(out[24:], payload)
|
||||
|
||||
put16(out[10:12], ipChecksum(out[0:20]))
|
||||
put16(out[22:24], ipChecksum(out))
|
||||
return out
|
||||
}
|
||||
|
||||
// An extremely simple packet decoder for basic IPv4 packet types.
|
||||
// Decode extracts data from the packet in b into q.
|
||||
// It performs extremely simple packet decoding for basic IPv4 packet types.
|
||||
// It extracts only the subprotocol id, IP addresses, and (if any) ports,
|
||||
// and shouldn't need any memory allocation.
|
||||
func (q *QDecode) Decode(b []byte) {
|
||||
func (q *ParsedPacket) Decode(b []byte) {
|
||||
q.b = nil
|
||||
|
||||
if len(b) < 20 {
|
||||
q.IPProto = Junk
|
||||
return
|
||||
}
|
||||
// Check that it's IPv4.
|
||||
// TODO(apenwarr): consider IPv6 support
|
||||
if ((b[0] & 0xF0) >> 4) != 4 {
|
||||
q.IPProto = Junk
|
||||
if len(b) < ipHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
n := int(binary.BigEndian.Uint16(b[2:4]))
|
||||
if len(b) < n {
|
||||
// Check that it's IPv4.
|
||||
// TODO(apenwarr): consider IPv6 support
|
||||
switch (b[0] & 0xF0) >> 4 {
|
||||
case 4:
|
||||
q.IPProto = IPProto(b[9])
|
||||
// continue
|
||||
case 6:
|
||||
q.IPProto = IPv6
|
||||
return
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
q.length = int(get16(b[2:4]))
|
||||
if len(b) < q.length {
|
||||
// Packet was cut off before full IPv4 length.
|
||||
q.IPProto = Junk
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
// If it's valid IPv4, then the IP addresses are valid
|
||||
q.SrcIP = IP(binary.BigEndian.Uint32(b[12:16]))
|
||||
q.DstIP = IP(binary.BigEndian.Uint32(b[16:20]))
|
||||
q.SrcIP = IP(get32(b[12:16]))
|
||||
q.DstIP = IP(get32(b[16:20]))
|
||||
|
||||
q.subofs = int((b[0] & 0x0F) * 4)
|
||||
q.subofs = int((b[0] & 0x0F) << 2)
|
||||
sub := b[q.subofs:]
|
||||
|
||||
// We don't care much about IP fragmentation, except insofar as it's
|
||||
@ -207,57 +147,56 @@ func (q *QDecode) Decode(b []byte) {
|
||||
// A "perfectly correct" implementation would have to reassemble
|
||||
// fragments before deciding what to do. But the truth is there's
|
||||
// zero reason to send such a short first fragment, so we can treat
|
||||
// it as Junk. We can also treat any subsequent fragment that starts
|
||||
// at such a low offset as Junk.
|
||||
fragFlags := binary.BigEndian.Uint16(b[6:8])
|
||||
// it as Unknown. We can also treat any subsequent fragment that starts
|
||||
// at such a low offset as Unknown.
|
||||
fragFlags := get16(b[6:8])
|
||||
moreFrags := (fragFlags & 0x20) != 0
|
||||
fragOfs := fragFlags & 0x1FFF
|
||||
if fragOfs == 0 {
|
||||
// This is the first fragment
|
||||
if moreFrags && len(sub) < minFrag {
|
||||
// Suspiciously short first fragment, dump it.
|
||||
log.Printf("junk1!\n")
|
||||
q.IPProto = Junk
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
// otherwise, this is either non-fragmented (the usual case)
|
||||
// or a big enough initial fragment that we can read the
|
||||
// whole subprotocol header.
|
||||
proto := b[9]
|
||||
switch proto {
|
||||
case 1: // ICMPv4
|
||||
if len(sub) < 8 {
|
||||
q.IPProto = Junk
|
||||
switch q.IPProto {
|
||||
case ICMP:
|
||||
if len(sub) < icmpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.IPProto = ICMP
|
||||
q.SrcPort = 0
|
||||
q.DstPort = 0
|
||||
q.b = b
|
||||
q.dataofs = q.subofs + icmpHeaderLength
|
||||
return
|
||||
case 6: // TCP
|
||||
if len(sub) < 20 {
|
||||
q.IPProto = Junk
|
||||
case TCP:
|
||||
if len(sub) < tcpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.IPProto = TCP
|
||||
q.SrcPort = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.DstPort = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.SrcPort = get16(sub[0:2])
|
||||
q.DstPort = get16(sub[2:4])
|
||||
q.TCPFlags = sub[13] & 0x3F
|
||||
q.b = b
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
case 17: // UDP
|
||||
if len(sub) < 8 {
|
||||
q.IPProto = Junk
|
||||
case UDP:
|
||||
if len(sub) < udpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.IPProto = UDP
|
||||
q.SrcPort = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.DstPort = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.SrcPort = get16(sub[0:2])
|
||||
q.DstPort = get16(sub[2:4])
|
||||
q.b = b
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
return
|
||||
default:
|
||||
q.IPProto = Junk
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@ -265,7 +204,7 @@ func (q *QDecode) Decode(b []byte) {
|
||||
if fragOfs < minFrag {
|
||||
// First frag was suspiciously short, so we can't
|
||||
// trust the followup either.
|
||||
q.IPProto = Junk
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
// otherwise, we have to permit the fragment to slide through.
|
||||
@ -279,29 +218,59 @@ func (q *QDecode) Decode(b []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a subset of the IP subprotocol section.
|
||||
func (q *QDecode) Sub(begin, n int) []byte {
|
||||
func (q *ParsedPacket) IPHeader() IPHeader {
|
||||
ipid := get16(q.b[4:6])
|
||||
return IPHeader{
|
||||
IPID: ipid,
|
||||
IPProto: q.IPProto,
|
||||
SrcIP: q.SrcIP,
|
||||
DstIP: q.DstIP,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *ParsedPacket) ICMPHeader() ICMPHeader {
|
||||
return ICMPHeader{
|
||||
IPHeader: q.IPHeader(),
|
||||
Type: ICMPType(q.b[q.subofs+0]),
|
||||
Code: ICMPCode(q.b[q.subofs+1]),
|
||||
}
|
||||
}
|
||||
|
||||
func (q *ParsedPacket) UDPHeader() UDPHeader {
|
||||
return UDPHeader{
|
||||
IPHeader: q.IPHeader(),
|
||||
SrcPort: q.SrcPort,
|
||||
DstPort: q.DstPort,
|
||||
}
|
||||
}
|
||||
|
||||
// Sub returns the IP subprotocol section.
|
||||
func (q *ParsedPacket) Sub(begin, n int) []byte {
|
||||
return q.b[q.subofs+begin : q.subofs+begin+n]
|
||||
}
|
||||
|
||||
// Payload returns the payload of the IP subprotocol section.
|
||||
func (q *ParsedPacket) Payload() []byte {
|
||||
return q.b[q.dataofs:q.length]
|
||||
}
|
||||
|
||||
// Trim trims the buffer to its IPv4 length.
|
||||
// Sometimes packets arrive from an interface with extra bytes on the end.
|
||||
// This removes them.
|
||||
func (q *QDecode) Trim() []byte {
|
||||
n := binary.BigEndian.Uint16(q.b[2:4])
|
||||
return q.b[:n]
|
||||
func (q *ParsedPacket) Trim() []byte {
|
||||
return q.b[:q.length]
|
||||
}
|
||||
|
||||
// IsTCPSyn reports whether q is a TCP SYN packet (i.e. the
|
||||
// first packet in a new connection).
|
||||
func (q *QDecode) IsTCPSyn() bool {
|
||||
// IsTCPSyn reports whether q is a TCP SYN packet
|
||||
// (i.e. the first packet in a new connection).
|
||||
func (q *ParsedPacket) IsTCPSyn() bool {
|
||||
return (q.TCPFlags & TCPSynAck) == TCPSyn
|
||||
}
|
||||
|
||||
// IsError reports whether q is an IPv4 ICMP "Error" packet.
|
||||
func (q *QDecode) IsError() bool {
|
||||
func (q *ParsedPacket) IsError() bool {
|
||||
if q.IPProto == ICMP && len(q.b) >= q.subofs+8 {
|
||||
switch q.b[q.subofs] {
|
||||
switch ICMPType(q.b[q.subofs]) {
|
||||
case ICMPUnreachable, ICMPTimeExceeded:
|
||||
return true
|
||||
}
|
||||
@ -310,28 +279,23 @@ func (q *QDecode) IsError() bool {
|
||||
}
|
||||
|
||||
// IsEchoRequest reports whether q is an IPv4 ICMP Echo Request.
|
||||
func (q *QDecode) IsEchoRequest() bool {
|
||||
func (q *ParsedPacket) IsEchoRequest() bool {
|
||||
if q.IPProto == ICMP && len(q.b) >= q.subofs+8 {
|
||||
return q.b[q.subofs] == ICMPEchoRequest && q.b[q.subofs+1] == 0
|
||||
return ICMPType(q.b[q.subofs]) == ICMPEchoRequest &&
|
||||
ICMPCode(q.b[q.subofs+1]) == ICMPNoCode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEchoRequest reports whether q is an IPv4 ICMP Echo Response.
|
||||
func (q *QDecode) IsEchoResponse() bool {
|
||||
func (q *ParsedPacket) IsEchoResponse() bool {
|
||||
if q.IPProto == ICMP && len(q.b) >= q.subofs+8 {
|
||||
return q.b[q.subofs] == ICMPEchoReply && q.b[q.subofs+1] == 0
|
||||
return ICMPType(q.b[q.subofs]) == ICMPEchoReply &&
|
||||
ICMPCode(q.b[q.subofs+1]) == ICMPNoCode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// EchoResponse returns an IPv4 ICMP echo reply to the request in q.
|
||||
func (q *QDecode) EchoRespond() []byte {
|
||||
icmpid := binary.BigEndian.Uint16(q.Sub(4, 2))
|
||||
b := q.Trim()
|
||||
return GenICMP(q.DstIP, q.SrcIP, icmpid, ICMPEchoReply, 0, b[q.subofs+4:])
|
||||
}
|
||||
|
||||
func Hexdump(b []byte) string {
|
||||
out := new(strings.Builder)
|
||||
for i := 0; i < len(b); i += 16 {
|
||||
|
Reference in New Issue
Block a user