diff --git a/iputil/packet.go b/iputil/packet.go index e5b9f70f..1cf895ce 100644 --- a/iputil/packet.go +++ b/iputil/packet.go @@ -378,6 +378,21 @@ func ipv6FindUpperProtocolOffset(packet []byte) int { } func CreateICMPEchoResponse(packet, out []byte) []byte { + if len(packet) < 1 { + return nil + } + + switch packet[0] >> 4 { + case 4: + return createICMPv4EchoResponse(packet, out) + case 6: + return createICMPv6EchoResponse(packet, out) + default: + return nil + } +} + +func createICMPv4EchoResponse(packet, out []byte) []byte { // Return early if this is not a simple ICMP Echo Request //TODO: make constants out of these if !(len(packet) >= 28 && len(packet) <= 9001 && packet[0] == 0x45 && packet[9] == 0x01 && packet[20] == 0x08) { @@ -411,6 +426,43 @@ func CreateICMPEchoResponse(packet, out []byte) []byte { return out } +func createICMPv6EchoResponse(packet, out []byte) []byte { + // IPv6 header (40 bytes) + ICMPv6 header (8 bytes minimum) + if len(packet) < ipv6.HeaderLen+8 || len(packet) > 9001 { + return nil + } + + // Next Header must be ICMPv6 (58) + if packet[6] != 58 { + return nil + } + + // ICMPv6 type must be Echo Request (128) + if packet[ipv6.HeaderLen] != 128 { + return nil + } + + out = out[:len(packet)] + copy(out, packet) + + // Swap src/dst addresses (bytes 8-23 and 24-39) + copy(out[8:24], packet[24:40]) + copy(out[24:40], packet[8:24]) + + // Change ICMPv6 type to Echo Reply (129) + icmp := out[ipv6.HeaderLen:] + icmp[0] = 129 + icmp[2] = 0 + icmp[3] = 0 + + // ICMPv6 checksum uses a pseudo-header with src, dst, length, and next header + payloadLen := uint32(len(icmp)) + csum := ipv6PseudoheaderChecksum(out[8:24], out[24:40], 58, payloadLen) + binary.BigEndian.PutUint16(icmp[2:], tcpipChecksum(icmp, csum)) + + return out +} + // calculates the TCP/IP checksum defined in rfc1071. The passed-in // csum is any initial checksum data that's already been computed. // diff --git a/iputil/packet_test.go b/iputil/packet_test.go index 81644c5e..346d4dd3 100644 --- a/iputil/packet_test.go +++ b/iputil/packet_test.go @@ -239,3 +239,106 @@ func Test_CreateRejectPacketIPv6_ExtensionHeaders(t *testing.T) { tcpOut := rejectPacket[ipv6.HeaderLen:] assert.Equal(t, byte(0b00000100), tcpOut[13]) // RST only } + +func TestCreateICMPEchoResponse_IPv4(t *testing.T) { + // Build a simple IPv4 ICMP Echo Request + packet := make([]byte, 28) + packet[0] = 0x45 // version 4, IHL 5 + binary.BigEndian.PutUint16(packet[2:], uint16(28)) // total length + packet[8] = 64 // TTL + packet[9] = 1 // protocol ICMP + copy(packet[12:16], net.IPv4(10, 0, 0, 1).To4()) // src + copy(packet[16:20], net.IPv4(10, 0, 0, 2).To4()) // dst + packet[20] = 8 // ICMP Echo Request + + out := make([]byte, len(packet)) + result := CreateICMPEchoResponse(packet, out) + assert.NotNil(t, result) + assert.Equal(t, byte(0x45), result[0]) + // src/dst swapped + assert.Equal(t, net.IPv4(10, 0, 0, 2).To4(), net.IP(result[12:16])) + assert.Equal(t, net.IPv4(10, 0, 0, 1).To4(), net.IP(result[16:20])) + // ICMP Echo Reply + assert.Equal(t, byte(0), result[20]) +} + +func TestCreateICMPEchoResponse_IPv6(t *testing.T) { + src := net.ParseIP("fd00::1").To16() + dst := net.ParseIP("fd00::2").To16() + + // Build an IPv6 ICMPv6 Echo Request packet + // IPv6 header (40 bytes) + ICMPv6 (8 bytes) + packet := make([]byte, 48) + packet[0] = 0x60 // version 6 + payloadLen := uint16(8) // ICMPv6 header only + binary.BigEndian.PutUint16(packet[4:], payloadLen) + packet[6] = 58 // Next Header: ICMPv6 + packet[7] = 64 // Hop Limit + copy(packet[8:24], src) // src address + copy(packet[24:40], dst) // dst address + + // ICMPv6 Echo Request + icmp := packet[40:] + icmp[0] = 128 // type: Echo Request + icmp[1] = 0 // code + binary.BigEndian.PutUint16(icmp[4:], 1) // identifier + binary.BigEndian.PutUint16(icmp[6:], 1) // sequence number + + // Compute correct checksum for the request + csum := ipv6PseudoheaderChecksum(src, dst, 58, uint32(payloadLen)) + binary.BigEndian.PutUint16(icmp[2:], tcpipChecksum(icmp, csum)) + + out := make([]byte, len(packet)) + result := CreateICMPEchoResponse(packet, out) + assert.NotNil(t, result) + + // Version should still be 6 + assert.Equal(t, byte(6), result[0]>>4) + // src/dst swapped + assert.Equal(t, dst, net.IP(result[8:24])) + assert.Equal(t, src, net.IP(result[24:40])) + // ICMPv6 Echo Reply type + assert.Equal(t, byte(129), result[40]) + + // Verify checksum is valid (tcpipChecksum returns 0 when data+checksum is correct) + respIcmp := result[40:] + verifyCsum := ipv6PseudoheaderChecksum(result[8:24], result[24:40], 58, uint32(payloadLen)) + assert.Equal(t, uint16(0), tcpipChecksum(respIcmp, verifyCsum)) +} + +func TestCreateICMPEchoResponse_IPv6_NotEchoRequest(t *testing.T) { + src := net.ParseIP("fd00::1").To16() + dst := net.ParseIP("fd00::2").To16() + + packet := make([]byte, 48) + packet[0] = 0x60 + binary.BigEndian.PutUint16(packet[4:], 8) + packet[6] = 58 + packet[7] = 64 + copy(packet[8:24], src) + copy(packet[24:40], dst) + + // ICMPv6 type 1 (Destination Unreachable) - not Echo Request + packet[40] = 1 + + out := make([]byte, len(packet)) + result := CreateICMPEchoResponse(packet, out) + assert.Nil(t, result) +} + +func TestCreateICMPEchoResponse_IPv6_NotICMPv6(t *testing.T) { + src := net.ParseIP("fd00::1").To16() + dst := net.ParseIP("fd00::2").To16() + + packet := make([]byte, 48) + packet[0] = 0x60 + binary.BigEndian.PutUint16(packet[4:], 8) + packet[6] = 6 // TCP, not ICMPv6 + packet[7] = 64 + copy(packet[8:24], src) + copy(packet[24:40], dst) + + out := make([]byte, len(packet)) + result := CreateICMPEchoResponse(packet, out) + assert.Nil(t, result) +}