1
0
Эх сурвалжийг харах

add IPv6 reject packet generation (#1766)

* add IPv6 reject packet generation (ICMPv6 Destination Unreachable and TCP RST)

* use ICMPv6 code 1 (administratively prohibited) and cap body at 1000 bytes

* cleanup, use ICMP error code 13 for ipv4

* better docs

* cleanup
Wade Simmons 4 сар өмнө
parent
commit
e4cc80aaca
3 өөрчлөгдсөн 416 нэмэгдсэн , 21 устгасан
  1. 1 1
      examples/config.yml
  2. 246 19
      iputil/packet.go
  3. 169 1
      iputil/packet_test.go

+ 1 - 1
examples/config.yml

@@ -397,7 +397,7 @@ firewall:
   #   `drop` (default): silently drop the packet.
   #   `reject`: send a reject reply.
   #     - For TCP, this will be a RST "Connection Reset" packet.
-  #     - For other protocols, this will be an ICMP port unreachable packet.
+  #     - For other protocols, this will be an ICMP "Destination unreachable: Communication administratively prohibited" packet.
   outbound_action: drop
   inbound_action: drop
 

+ 246 - 19
iputil/packet.go

@@ -4,26 +4,50 @@ import (
 	"encoding/binary"
 
 	"golang.org/x/net/ipv4"
+	"golang.org/x/net/ipv6"
 )
 
 const (
-	// Need 96 bytes for the largest reject packet:
+	// MaxIPv4RejectPacketSize is the largest IPv4 reject packet:
 	// - 20 byte ipv4 header
 	// - 8 byte icmpv4 header
 	// - 68 byte body (60 byte max orig ipv4 header + 8 byte orig icmpv4 header)
-	MaxRejectPacketSize = ipv4.HeaderLen + 8 + 60 + 8
+	maxIPv4RejectPacketSize = ipv4.HeaderLen + 8 + 60 + 8
+
+	// MaxRejectPacketSize is sized for the largest possible reject packet (IPv6):
+	// - 40 byte ipv6 header
+	// - 8 byte icmpv6 header
+	// - up to 1000 byte body (original packet, possibly truncated. We want to stay
+	//   under the MTU with Nebula overhead included)
+	maxIPv6RejectPacketSize = ipv6.HeaderLen + 8 + 1000
+
+	MaxRejectPacketSize = maxIPv6RejectPacketSize
 )
 
 func CreateRejectPacket(packet []byte, out []byte) []byte {
-	if len(packet) < ipv4.HeaderLen || int(packet[0]>>4) != ipv4.Version {
+	if len(packet) < 1 {
 		return nil
 	}
 
-	switch packet[9] {
-	case 6: // tcp
-		return ipv4CreateRejectTCPPacket(packet, out)
+	version := int(packet[0] >> 4)
+	switch version {
+	case ipv4.Version:
+		if len(packet) < ipv4.HeaderLen {
+			return nil
+		}
+		switch packet[9] {
+		case 6: // tcp
+			return ipv4CreateRejectTCPPacket(packet, out)
+		default:
+			return ipv4CreateRejectICMPPacket(packet, out)
+		}
+	case ipv6.Version:
+		if len(packet) < ipv6.HeaderLen {
+			return nil
+		}
+		return ipv6CreateRejectPacket(packet, out)
 	default:
-		return ipv4CreateRejectICMPPacket(packet, out)
+		return nil
 	}
 }
 
@@ -36,10 +60,7 @@ func ipv4CreateRejectICMPPacket(packet []byte, out []byte) []byte {
 	}
 
 	// ICMP reply includes original header and first 8 bytes of the packet
-	packetLen := len(packet)
-	if packetLen > ihl+8 {
-		packetLen = ihl + 8
-	}
+	packetLen := min(len(packet), ihl+8)
 
 	outLen := ipv4.HeaderLen + 8 + packetLen
 	if outLen > cap(out) {
@@ -71,14 +92,14 @@ func ipv4CreateRejectICMPPacket(packet []byte, out []byte) []byte {
 
 	// ICMP Destination Unreachable
 	icmpOut := out[ipv4.HeaderLen:]
-	icmpOut[0] = 3 // type (Destination unreachable)
-	icmpOut[1] = 3 // code (Port unreachable error)
-	icmpOut[2] = 0 // checksum
-	icmpOut[3] = 0 //  .
-	icmpOut[4] = 0 // unused
-	icmpOut[5] = 0 //  .
-	icmpOut[6] = 0 //  .
-	icmpOut[7] = 0 //  .
+	icmpOut[0] = 3  // type (Destination unreachable)
+	icmpOut[1] = 13 // code (Communication administratively prohibited)
+	icmpOut[2] = 0  // checksum
+	icmpOut[3] = 0  //  .
+	icmpOut[4] = 0  // unused
+	icmpOut[5] = 0  //  .
+	icmpOut[6] = 0  //  .
+	icmpOut[7] = 0  //  .
 
 	// Copy original IP header and first 8 bytes as body
 	copy(icmpOut[8:], packet[:packetLen])
@@ -165,6 +186,197 @@ func ipv4CreateRejectTCPPacket(packet []byte, out []byte) []byte {
 	return out
 }
 
+func ipv6CreateRejectPacket(packet []byte, out []byte) []byte {
+	proto := ipv6FindUpperProtocol(packet)
+	switch proto {
+	case 6: // tcp
+		return ipv6CreateRejectTCPPacket(packet, out)
+	default:
+		return ipv6CreateRejectICMPPacket(packet, out)
+	}
+}
+
+func ipv6FindUpperProtocol(packet []byte) uint8 {
+	nextHeader := packet[6]
+	offset := ipv6.HeaderLen
+
+	for {
+		switch nextHeader {
+		case 0, 43, 60: // Hop-by-Hop, Routing, Destination
+			if len(packet) < offset+2 {
+				return nextHeader
+			}
+			nextHeader = packet[offset]
+			offset += int(packet[offset+1]+1) << 3
+
+		case 44: // Fragment
+			if len(packet) < offset+8 {
+				return nextHeader
+			}
+			nextHeader = packet[offset]
+			offset += 8
+
+		case 51: // AH
+			if len(packet) < offset+2 {
+				return nextHeader
+			}
+			nextHeader = packet[offset]
+			offset += int(packet[offset+1]+2) << 2
+
+		default:
+			return nextHeader
+		}
+	}
+}
+
+func ipv6CreateRejectICMPPacket(packet []byte, out []byte) []byte {
+	// Include as much of the original packet as possible, up to 1000 bytes,
+	// so the response fits comfortably within any tunnel MTU.
+	packetLen := min(len(packet), 1000)
+
+	outLen := ipv6.HeaderLen + 8 + packetLen
+	if outLen > cap(out) {
+		return nil
+	}
+
+	out = out[:outLen]
+
+	// IPv6 header
+	ipHdr := out[0:ipv6.HeaderLen]
+	ipHdr[0] = ipv6.Version << 4 // version, traffic class (high bits)
+	ipHdr[1] = 0                 // traffic class (low bits), flow label (high bits)
+	ipHdr[2] = 0                 // flow label
+	ipHdr[3] = 0                 // flow label
+
+	payloadLen := uint16(outLen - ipv6.HeaderLen)
+	binary.BigEndian.PutUint16(ipHdr[4:], payloadLen) // payload length
+	ipHdr[6] = 58                                     // next header (ICMPv6)
+	ipHdr[7] = 64                                     // hop limit
+
+	// Swap dest / src IPs (each 16 bytes, src at 8, dst at 24)
+	copy(ipHdr[8:24], packet[24:40])
+	copy(ipHdr[24:40], packet[8:24])
+
+	// ICMPv6 Destination Unreachable
+	icmpOut := out[ipv6.HeaderLen:]
+	icmpOut[0] = 1 // type (Destination Unreachable)
+	icmpOut[1] = 1 // code (Communication with destination administratively prohibited)
+	icmpOut[2] = 0 // checksum
+	icmpOut[3] = 0 //  .
+	icmpOut[4] = 0 // unused
+	icmpOut[5] = 0 //  .
+	icmpOut[6] = 0 //  .
+	icmpOut[7] = 0 //  .
+
+	copy(icmpOut[8:], packet[:packetLen])
+
+	// ICMPv6 checksum uses a pseudo-header
+	csum := ipv6PseudoheaderChecksum(ipHdr[8:24], ipHdr[24:40], 58, uint32(payloadLen))
+	binary.BigEndian.PutUint16(icmpOut[2:], tcpipChecksum(icmpOut, csum))
+
+	return out
+}
+
+func ipv6CreateRejectTCPPacket(packet []byte, out []byte) []byte {
+	const tcpLen = 20
+
+	offset := ipv6FindUpperProtocolOffset(packet)
+	if len(packet) < offset+tcpLen {
+		return nil
+	}
+
+	outLen := ipv6.HeaderLen + tcpLen
+	if outLen > cap(out) {
+		return nil
+	}
+
+	out = out[:outLen]
+
+	// IPv6 header
+	ipHdr := out[0:ipv6.HeaderLen]
+	ipHdr[0] = ipv6.Version << 4 // version, traffic class (high bits)
+	ipHdr[1] = 0                 // traffic class (low bits), flow label (high bits)
+	ipHdr[2] = 0                 // flow label
+	ipHdr[3] = 0                 // flow label
+
+	binary.BigEndian.PutUint16(ipHdr[4:], tcpLen) // payload length
+	ipHdr[6] = 6                                  // next header (TCP)
+	ipHdr[7] = 64                                 // hop limit
+
+	// Swap dest / src IPs
+	copy(ipHdr[8:24], packet[24:40])
+	copy(ipHdr[24:40], packet[8:24])
+
+	// TCP RST
+	tcpIn := packet[offset:]
+	var ackSeq, seq uint32
+	outFlags := byte(0b00000100) // RST
+
+	inAck := tcpIn[13]&0b00010000 != 0
+	if inAck {
+		seq = binary.BigEndian.Uint32(tcpIn[8:])
+	} else {
+		inSyn := uint32((tcpIn[13] & 0b00000010) >> 1)
+		inFin := uint32(tcpIn[13] & 0b00000001)
+		ackSeq = binary.BigEndian.Uint32(tcpIn[4:]) + inSyn + inFin + uint32(len(tcpIn)) - uint32(tcpIn[12]>>4)<<2
+		outFlags |= 0b00010000 // ACK
+	}
+
+	tcpOut := out[ipv6.HeaderLen:]
+	// Swap dest / src ports
+	copy(tcpOut[0:2], tcpIn[2:4])
+	copy(tcpOut[2:4], tcpIn[0:2])
+	binary.BigEndian.PutUint32(tcpOut[4:], seq)
+	binary.BigEndian.PutUint32(tcpOut[8:], ackSeq)
+	tcpOut[12] = (tcpLen >> 2) << 4 // data offset, reserved, NS
+	tcpOut[13] = outFlags           // CWR, ECE, URG, ACK, PSH, RST, SYN, FIN
+	tcpOut[14] = 0                  // window size
+	tcpOut[15] = 0                  //  .
+	tcpOut[16] = 0                  // checksum
+	tcpOut[17] = 0                  //  .
+	tcpOut[18] = 0                  // URG Pointer
+	tcpOut[19] = 0                  //  .
+
+	// Calculate checksum with IPv6 pseudo-header
+	csum := ipv6PseudoheaderChecksum(ipHdr[8:24], ipHdr[24:40], 6, tcpLen)
+	binary.BigEndian.PutUint16(tcpOut[16:], tcpipChecksum(tcpOut, csum))
+
+	return out
+}
+
+func ipv6FindUpperProtocolOffset(packet []byte) int {
+	nextHeader := packet[6]
+	offset := ipv6.HeaderLen
+
+	for {
+		switch nextHeader {
+		case 0, 43, 60: // Hop-by-Hop, Routing, Destination
+			if len(packet) < offset+2 {
+				return offset
+			}
+			nextHeader = packet[offset]
+			offset += int(packet[offset+1]+1) << 3
+
+		case 44: // Fragment
+			if len(packet) < offset+8 {
+				return offset
+			}
+			nextHeader = packet[offset]
+			offset += 8
+
+		case 51: // AH
+			if len(packet) < offset+2 {
+				return offset
+			}
+			nextHeader = packet[offset]
+			offset += int(packet[offset+1]+2) << 2
+
+		default:
+			return offset
+		}
+	}
+}
+
 func CreateICMPEchoResponse(packet, out []byte) []byte {
 	// Return early if this is not a simple ICMP Echo Request
 	//TODO: make constants out of these
@@ -236,3 +448,18 @@ func ipv4PseudoheaderChecksum(src, dst []byte, proto, length uint32) (csum uint3
 	csum += length >> 16
 	return csum
 }
+
+// based on:
+// - https://github.com/google/gopacket/blob/v1.1.19/layers/tcpip.go#L37-L48
+func ipv6PseudoheaderChecksum(src, dst []byte, proto, length uint32) (csum uint32) {
+	for i := 0; i < 16; i += 2 {
+		csum += uint32(src[i]) << 8
+		csum += uint32(src[i+1])
+		csum += uint32(dst[i]) << 8
+		csum += uint32(dst[i+1])
+	}
+	csum += proto
+	csum += length & 0xffff
+	csum += length >> 16
+	return csum
+}

+ 169 - 1
iputil/packet_test.go

@@ -1,11 +1,13 @@
 package iputil
 
 import (
+	"encoding/binary"
 	"net"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 	"golang.org/x/net/ipv4"
+	"golang.org/x/net/ipv6"
 )
 
 func Test_CreateRejectPacket(t *testing.T) {
@@ -43,7 +45,7 @@ func Test_CreateRejectPacket(t *testing.T) {
 	}
 	b = append(b, []byte{0, 3, 0, 4, 0, 0, 0, 0}...)
 
-	expectedLen = MaxRejectPacketSize
+	expectedLen = maxIPv4RejectPacketSize
 	out = make([]byte, MaxRejectPacketSize)
 	rejectPacket = CreateRejectPacket(b, out)
 	assert.NotNil(t, rejectPacket)
@@ -71,3 +73,169 @@ func Test_CreateRejectPacket(t *testing.T) {
 	assert.NotNil(t, rejectPacket)
 	assert.Len(t, rejectPacket, expectedLen)
 }
+
+func makeIPv6Packet(src, dst net.IP, nextHeader uint8, payload []byte) []byte {
+	b := make([]byte, ipv6.HeaderLen+len(payload))
+	b[0] = ipv6.Version << 4
+	binary.BigEndian.PutUint16(b[4:], uint16(len(payload)))
+	b[6] = nextHeader
+	b[7] = 64
+	copy(b[8:24], src.To16())
+	copy(b[24:40], dst.To16())
+	copy(b[ipv6.HeaderLen:], payload)
+	return b
+}
+
+func Test_CreateRejectPacketIPv6_ICMP(t *testing.T) {
+	src := net.ParseIP("fd00::1")
+	dst := net.ParseIP("fd00::2")
+
+	// Small UDP packet: entire original included in body
+	udpPayload := make([]byte, 20)
+	udpPayload[0] = 0x00 // src port high
+	udpPayload[1] = 0x50 // src port low (80)
+	udpPayload[2] = 0x01 // dst port high
+	udpPayload[3] = 0xBB // dst port low (443)
+	packet := makeIPv6Packet(src, dst, 17, udpPayload)
+
+	out := make([]byte, MaxRejectPacketSize)
+	rejectPacket := CreateRejectPacket(packet, out)
+	assert.NotNil(t, rejectPacket)
+
+	// Small packet fits entirely: 40 (ipv6 hdr) + 8 (icmpv6 hdr) + 60 (original)
+	expectedLen := ipv6.HeaderLen + 8 + len(packet)
+	assert.Len(t, rejectPacket, expectedLen)
+
+	// Verify version
+	assert.Equal(t, byte(ipv6.Version<<4), rejectPacket[0]&0xf0)
+	// Verify next header is ICMPv6 (58)
+	assert.Equal(t, byte(58), rejectPacket[6])
+	// Verify src/dst are swapped
+	assert.Equal(t, dst.To16(), net.IP(rejectPacket[8:24]))
+	assert.Equal(t, src.To16(), net.IP(rejectPacket[24:40]))
+	// Verify ICMPv6 type=1 (Dest Unreachable), code=1 (Administratively prohibited)
+	assert.Equal(t, byte(1), rejectPacket[ipv6.HeaderLen])
+	assert.Equal(t, byte(1), rejectPacket[ipv6.HeaderLen+1])
+	// Verify entire original packet is included in body
+	assert.Equal(t, packet, rejectPacket[ipv6.HeaderLen+8:])
+
+	// Large packet: body is truncated to 1000 bytes
+	largePkt := makeIPv6Packet(src, dst, 17, make([]byte, 1200))
+	rejectPacket = CreateRejectPacket(largePkt, out)
+	assert.NotNil(t, rejectPacket)
+	assert.Len(t, rejectPacket, ipv6.HeaderLen+8+1000)
+	assert.Equal(t, largePkt[:1000], rejectPacket[ipv6.HeaderLen+8:])
+}
+
+func Test_CreateRejectPacketIPv6_TCP(t *testing.T) {
+	src := net.ParseIP("fd00::1")
+	dst := net.ParseIP("fd00::2")
+
+	// TCP SYN packet (next header 6)
+	tcpPayload := make([]byte, 20)
+	tcpPayload[0] = 0x00                             // src port high
+	tcpPayload[1] = 0x50                             // src port low (80)
+	tcpPayload[2] = 0x01                             // dst port high
+	tcpPayload[3] = 0xBB                             // dst port low (443)
+	binary.BigEndian.PutUint32(tcpPayload[4:], 1000) // seq
+	binary.BigEndian.PutUint32(tcpPayload[8:], 0)    // ack seq
+	tcpPayload[12] = (20 >> 2) << 4                  // data offset
+	tcpPayload[13] = 0b00000010                      // SYN flag
+
+	packet := makeIPv6Packet(src, dst, 6, tcpPayload)
+
+	out := make([]byte, MaxRejectPacketSize)
+	rejectPacket := CreateRejectPacket(packet, out)
+	assert.NotNil(t, rejectPacket)
+
+	// Expected: 40 (ipv6 hdr) + 20 (tcp RST)
+	expectedLen := ipv6.HeaderLen + 20
+	assert.Len(t, rejectPacket, expectedLen)
+
+	// Verify version
+	assert.Equal(t, byte(ipv6.Version<<4), rejectPacket[0]&0xf0)
+	// Verify next header is TCP (6)
+	assert.Equal(t, byte(6), rejectPacket[6])
+	// Verify src/dst are swapped
+	assert.Equal(t, dst.To16(), net.IP(rejectPacket[8:24]))
+	assert.Equal(t, src.To16(), net.IP(rejectPacket[24:40]))
+	// Verify ports are swapped
+	tcpOut := rejectPacket[ipv6.HeaderLen:]
+	assert.Equal(t, uint16(443), binary.BigEndian.Uint16(tcpOut[0:2]))
+	assert.Equal(t, uint16(80), binary.BigEndian.Uint16(tcpOut[2:4]))
+	// RST+ACK flags (since input was SYN without ACK)
+	assert.Equal(t, byte(0b00010100), tcpOut[13])
+	// ack_seq = original seq (1000) + SYN (1) + FIN (0) + segment data (0)
+	assert.Equal(t, uint32(1001), binary.BigEndian.Uint32(tcpOut[8:]))
+}
+
+func Test_CreateRejectPacketIPv6_TCPWithACK(t *testing.T) {
+	src := net.ParseIP("fd00::1")
+	dst := net.ParseIP("fd00::2")
+
+	// TCP packet with ACK set
+	tcpPayload := make([]byte, 20)
+	tcpPayload[0] = 0x00
+	tcpPayload[1] = 0x50
+	tcpPayload[2] = 0x01
+	tcpPayload[3] = 0xBB
+	binary.BigEndian.PutUint32(tcpPayload[4:], 1000) // seq
+	binary.BigEndian.PutUint32(tcpPayload[8:], 2000) // ack seq
+	tcpPayload[12] = (20 >> 2) << 4                  // data offset
+	tcpPayload[13] = 0b00010000                      // ACK flag
+
+	packet := makeIPv6Packet(src, dst, 6, tcpPayload)
+
+	out := make([]byte, MaxRejectPacketSize)
+	rejectPacket := CreateRejectPacket(packet, out)
+	assert.NotNil(t, rejectPacket)
+
+	tcpOut := rejectPacket[ipv6.HeaderLen:]
+	// RST only (no ACK) since input had ACK
+	assert.Equal(t, byte(0b00000100), tcpOut[13])
+	// seq = original ack_seq
+	assert.Equal(t, uint32(2000), binary.BigEndian.Uint32(tcpOut[4:]))
+}
+
+func Test_CreateRejectPacketIPv6_TooShort(t *testing.T) {
+	// Packet too short to be valid IPv6
+	out := make([]byte, MaxRejectPacketSize)
+	assert.Nil(t, CreateRejectPacket([]byte{0x60}, out))
+	assert.Nil(t, CreateRejectPacket(make([]byte, 39), out))
+}
+
+func Test_CreateRejectPacketIPv6_ExtensionHeaders(t *testing.T) {
+	src := net.ParseIP("fd00::1")
+	dst := net.ParseIP("fd00::2")
+
+	// IPv6 + Hop-by-Hop extension header + TCP
+	hopByHop := []byte{
+		6,    // next header: TCP
+		0,    // length (8 bytes total)
+		0, 0, // padding
+		0, 0, 0, 0,
+	}
+	tcpPayload := make([]byte, 20)
+	tcpPayload[0] = 0x00
+	tcpPayload[1] = 0x50
+	tcpPayload[2] = 0x01
+	tcpPayload[3] = 0xBB
+	binary.BigEndian.PutUint32(tcpPayload[4:], 1000)
+	binary.BigEndian.PutUint32(tcpPayload[8:], 2000)
+	tcpPayload[12] = (20 >> 2) << 4
+	tcpPayload[13] = 0b00010000 // ACK
+
+	payload := append(hopByHop, tcpPayload...)
+	packet := makeIPv6Packet(src, dst, 0, payload) // next header 0 = Hop-by-Hop
+
+	out := make([]byte, MaxRejectPacketSize)
+	rejectPacket := CreateRejectPacket(packet, out)
+	assert.NotNil(t, rejectPacket)
+
+	// Should produce TCP RST
+	expectedLen := ipv6.HeaderLen + 20
+	assert.Len(t, rejectPacket, expectedLen)
+	assert.Equal(t, byte(6), rejectPacket[6]) // next header is TCP
+	tcpOut := rejectPacket[ipv6.HeaderLen:]
+	assert.Equal(t, byte(0b00000100), tcpOut[13]) // RST only
+}