From e4cc80aaca1767a669cad9d822c49e007ef7be65 Mon Sep 17 00:00:00 2001 From: Wade Simmons Date: Tue, 16 Jun 2026 13:04:21 -0400 Subject: [PATCH] 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 --- examples/config.yml | 2 +- iputil/packet.go | 265 +++++++++++++++++++++++++++++++++++++++--- iputil/packet_test.go | 170 ++++++++++++++++++++++++++- 3 files changed, 416 insertions(+), 21 deletions(-) diff --git a/examples/config.yml b/examples/config.yml index 6c7fb489..4f7fd1e7 100644 --- a/examples/config.yml +++ b/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 diff --git a/iputil/packet.go b/iputil/packet.go index b18e5244..e5b9f70f 100644 --- a/iputil/packet.go +++ b/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 +} diff --git a/iputil/packet_test.go b/iputil/packet_test.go index e1d0d95d..81644c5e 100644 --- a/iputil/packet_test.go +++ b/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 +}