diff --git a/src/main/java/com/github/hirsivaja/ip/IpUtils.java b/src/main/java/com/github/hirsivaja/ip/IpUtils.java index 235eaba..155d8e8 100644 --- a/src/main/java/com/github/hirsivaja/ip/IpUtils.java +++ b/src/main/java/com/github/hirsivaja/ip/IpUtils.java @@ -6,52 +6,150 @@ public class IpUtils { private static final Logger logger = Logger.getLogger("IpUtils"); - private IpUtils() {} + private IpUtils() { + } + + /** + * Calculates the Internet checksum as defined in RFC 1071. + * + *

The Internet checksum is computed as the 16-bit one's complement of the + * one's complement sum of all 16-bit words in the data. This implementation + * properly handles multiple carries through iterative folding.

+ * + *

Algorithm:

+ *
    + *
  1. Sum all 16-bit words in the data
  2. + *
  3. Add any odd byte as the high byte of a 16-bit word
  4. + *
  5. Fold carries from bits 16-31 into bits 0-15 until no more carries
  6. + *
  7. Return the one's complement of the result
  8. + *
+ * + *

This checksum is used by IPv4 headers, TCP, UDP, ICMP, and other + * Internet protocols as specified in their respective RFCs.

+ * + * @param data the byte array to compute the checksum for + * @return the 16-bit Internet checksum + * @throws IllegalArgumentException if data is null + * @see RFC 1071 + */ public static short calculateInternetChecksum(byte[] data) { + if (data == null) { + throw new IllegalArgumentException("Data cannot be null"); + } + ByteBuffer buf = ByteBuffer.wrap(data); long sum = 0; - while(buf.hasRemaining()) { - if(buf.remaining() > 1) { + + // Sum all 16-bit words (network byte order - big endian) + while (buf.hasRemaining()) { + if (buf.remaining() > 1) { + // Complete 16-bit word - ByteBuffer.getShort() reads in big-endian (network) order sum += buf.getShort() & 0xFFFF; } else { - sum += buf.get() << 8 & 0xFFFF; + // Odd byte: treat as high byte of 16-bit word (pad with zero) + sum += (buf.get() & 0xFF) << 8; } } - return (short) ((~((sum & 0xFFFF) + (sum >> 16))) & 0xFFFF); + + // Fold carries until no more carries exist (RFC 1071 end-around carry) + while ((sum & 0xFFFF0000) != 0) { + sum = (sum & 0xFFFF) + (sum >>> 16); + } + + // Return one's complement + return (short) (~sum & 0xFFFF); } + /** + * Verifies the Internet checksum of data that includes the checksum field. + * + *

This method verifies that the checksum embedded in the data is correct. + * The checksum field in the data should contain the actual checksum value. + * When calculated over the entire data (including the checksum field), + * the result should be zero for valid data.

+ * + * @param checksumData the byte array containing data with embedded checksum + * @return true if the checksum is valid (calculation yields zero), false otherwise + * @throws IllegalArgumentException if checksumData is null + * @see #calculateInternetChecksum(byte[]) + */ public static boolean verifyInternetChecksum(byte[] checksumData) { return verifyInternetChecksum(checksumData, (short) 0); } - public static boolean verifyInternetChecksum(byte[] checksumData, short actual) { - short expected = calculateInternetChecksum(checksumData); - if(expected != actual) { - logger.warning("CRC mismatch!"); + /** + * Verifies the Internet checksum by comparing the calculated checksum with expected value. + * + *

This method calculates the checksum of the provided data and compares it + * with the expected checksum value. The data should NOT include the checksum field + * when using this method.

+ * + * @param checksumData the byte array to verify (without checksum field) + * @param expected the expected checksum value to compare against + * @return true if the calculated checksum matches the expected value, false otherwise + * @throws IllegalArgumentException if checksumData is null + * @see #calculateInternetChecksum(byte[]) + */ + public static boolean verifyInternetChecksum(byte[] checksumData, short expected) { + short calculated = calculateInternetChecksum(checksumData); + if (calculated != expected) { + logger.warning("Checksum mismatch! Expected: 0x" + + Integer.toHexString(expected & 0xFFFF) + + ", Calculated: 0x" + + Integer.toHexString(calculated & 0xFFFF)); } - return expected == actual; + return calculated == expected; } + /** + * Ensures the Internet checksum of data that includes the checksum field is valid. + * + *

This method verifies that the checksum embedded in the data is correct + * and throws an exception if validation fails. The checksum field in the data + * should contain the actual checksum value. When calculated over the entire data + * (including the checksum field), the result should be zero for valid data.

+ * + * @param checksumData the byte array containing data with embedded checksum + * @throws IllegalArgumentException if the checksum is invalid or checksumData is null + * @see #verifyInternetChecksum(byte[]) + */ public static void ensureInternetChecksum(byte[] checksumData) { ensureInternetChecksum(checksumData, (short) 0); } - public static void ensureInternetChecksum(byte[] checksumData, short actual) { - short expected = calculateInternetChecksum(checksumData); - if(expected != actual) { - logger.log(Level.FINEST, "Checksum mismatch! Expected checksum {0}. Actual checksum {1}", new Object[]{expected, actual}); - throw new IllegalArgumentException("Checksum does not match!"); + /** + * Ensures the Internet checksum matches the expected value. + * + *

This method calculates the checksum of the provided data and compares it + * with the expected checksum value, throwing an exception if they don't match. + * The data should NOT include the checksum field when using this method.

+ * + * @param checksumData the byte array to verify (without checksum field) + * @param expected the expected checksum value + * @throws IllegalArgumentException if the checksum doesn't match expected value or checksumData is null + * @see #calculateInternetChecksum(byte[]) + */ + public static void ensureInternetChecksum(byte[] checksumData, short expected) { + short calculated = calculateInternetChecksum(checksumData); + if (calculated != expected) { + logger.log(Level.FINEST, "Checksum mismatch! Expected: 0x{0}, Calculated: 0x{1}", + new Object[]{Integer.toHexString(expected & 0xFFFF), + Integer.toHexString(calculated & 0xFFFF)}); + throw new IllegalArgumentException("Checksum does not match! Expected: 0x" + + Integer.toHexString(expected & 0xFFFF) + + ", Calculated: 0x" + + Integer.toHexString(calculated & 0xFFFF)); } } public static byte[] parseHexBinary(String hexString) { - if(hexString.length() % 2 == 1) { + if (hexString.length() % 2 == 1) { hexString = "0" + hexString; } char[] chars = hexString.toCharArray(); byte[] bytes = new byte[chars.length / 2]; - for(int i = 0, j = 0; i < chars.length; i += 2, j++) { + for (int i = 0, j = 0; i < chars.length; i += 2, j++) { int a = Character.digit(chars[i], 16) << 4; int b = Character.digit(chars[i + 1], 16); bytes[j] = (byte) (a | b); diff --git a/src/main/java/com/github/hirsivaja/ip/icmp/IcmpPayload.java b/src/main/java/com/github/hirsivaja/ip/icmp/IcmpPayload.java index c30852a..a41d0be 100644 --- a/src/main/java/com/github/hirsivaja/ip/icmp/IcmpPayload.java +++ b/src/main/java/com/github/hirsivaja/ip/icmp/IcmpPayload.java @@ -25,6 +25,25 @@ public void encode(ByteBuffer out) { message.encode(out); } + /** + * Constructs the data for ICMP checksum calculation as specified in RFC 792. + * + *

ICMP checksum is calculated over the entire ICMP message:

+ *
    + *
  1. ICMP Type - 1 byte
  2. + *
  3. ICMP Code - 1 byte
  4. + *
  5. ICMP Checksum - 2 bytes (set to zero during calculation)
  6. + *
  7. ICMP Message Data - Variable length, depends on ICMP type
  8. + *
+ * + *

Important: ICMP does NOT use a pseudo-header. The checksum covers + * only the ICMP header and data, not any IP header information.

+ * + * @param message the ICMP message containing type, code, and data + * @param checksum the checksum value (typically 0 for calculation, actual value for verification) + * @return byte array containing ICMP type + code + checksum + message data for checksum calculation + * @see RFC 792 + */ private static byte[] getChecksumData(IcmpMessage message, short checksum) { ByteBuffer checksumBuf = ByteBuffer.allocate(message.getLength()); checksumBuf.put(message.getType().getType()); diff --git a/src/main/java/com/github/hirsivaja/ip/icmpv6/Icmpv6Payload.java b/src/main/java/com/github/hirsivaja/ip/icmpv6/Icmpv6Payload.java index ff30b15..e5e1030 100644 --- a/src/main/java/com/github/hirsivaja/ip/icmpv6/Icmpv6Payload.java +++ b/src/main/java/com/github/hirsivaja/ip/icmpv6/Icmpv6Payload.java @@ -26,6 +26,35 @@ public void encode(ByteBuffer out) { message.encode(out); } + /** + * Constructs the data for ICMPv6 checksum calculation as specified in RFC 4443. + * + *

ICMPv6 checksum is calculated over the concatenation of:

+ *
    + *
  1. IPv6 Pseudo-header - 40 bytes, provides protection against misrouted packets + * + *
  2. + *
  3. ICMPv6 Type - 1 byte
  4. + *
  5. ICMPv6 Code - 1 byte
  6. + *
  7. ICMPv6 Checksum - 2 bytes (set to zero during calculation)
  8. + *
  9. ICMPv6 Message Data - Variable length, depends on ICMPv6 type
  10. + *
+ * + *

Important: Unlike ICMP, ICMPv6 requires a pseudo-header for checksum calculation. + * The checksum is mandatory for all ICMPv6 messages.

+ * + * @param header the IPv6 header providing the pseudo-header + * @param message the ICMPv6 message containing type, code, and data + * @param checksum the checksum value (typically 0 for calculation, actual value for verification) + * @return byte array containing pseudo-header + ICMPv6 type + code + checksum + message data + * @see RFC 4443 + */ private static byte[] getChecksumData(Ipv6Header header, Icmpv6Message message, short checksum) { ByteBuffer checksumBuf = ByteBuffer.allocate(Ipv6Header.HEADER_LEN + message.getLength()); checksumBuf.put(header.getPseudoHeader()); diff --git a/src/main/java/com/github/hirsivaja/ip/igmp/IgmpPayload.java b/src/main/java/com/github/hirsivaja/ip/igmp/IgmpPayload.java index d2aac36..483398b 100644 --- a/src/main/java/com/github/hirsivaja/ip/igmp/IgmpPayload.java +++ b/src/main/java/com/github/hirsivaja/ip/igmp/IgmpPayload.java @@ -25,6 +25,26 @@ public void encode(ByteBuffer out) { message.encode(out); } + /** + * Constructs the data for IGMP checksum calculation as specified in RFC 3376. + * + *

IGMP checksum is calculated over the entire IGMP message:

+ *
    + *
  1. IGMP Type - 1 byte (Query, Report, etc.)
  2. + *
  3. IGMP Code - 1 byte (Max Response Time for queries)
  4. + *
  5. IGMP Checksum - 2 bytes (set to zero during calculation)
  6. + *
  7. IGMP Message Data - Variable length, depends on IGMP type and version
  8. + *
+ * + *

Important: IGMP does NOT use a pseudo-header. The checksum covers + * only the IGMP header and data, similar to ICMP. This applies to all IGMP + * versions (v1, v2, v3).

+ * + * @param message the IGMP message containing type, code, and data + * @param checksum the checksum value (typically 0 for calculation, actual value for verification) + * @return byte array containing IGMP type + code + checksum + message data for checksum calculation + * @see RFC 3376 + */ private static byte[] getChecksumData(IgmpMessage message, short checksum) { ByteBuffer checksumBuf = ByteBuffer.allocate(message.getLength()); checksumBuf.put(message.getType().getType()); diff --git a/src/main/java/com/github/hirsivaja/ip/tcp/TcpMessagePayload.java b/src/main/java/com/github/hirsivaja/ip/tcp/TcpMessagePayload.java index 9bbbcfb..fb10bb5 100644 --- a/src/main/java/com/github/hirsivaja/ip/tcp/TcpMessagePayload.java +++ b/src/main/java/com/github/hirsivaja/ip/tcp/TcpMessagePayload.java @@ -36,6 +36,29 @@ public int getLength() { return header.getLength() + TcpHeader.TCP_HEADER_LEN + payload.length; } + /** + * Constructs the data for TCP checksum calculation as specified in RFC 9293. + * + *

TCP checksum is calculated over the concatenation of:

+ *
    + *
  1. Pseudo-header - Provides protection against misrouted packets + * + *
  2. + *
  3. TCP Header - 20 bytes minimum (with checksum field zeroed)
  4. + *
  5. TCP Payload - Variable length data
  6. + *
+ * + *

The checksum field in the TCP header must be set to zero before calling this method.

+ * + * @param header the IP header (IPv4 or IPv6) providing the pseudo-header + * @param tcpHeader the TCP header with checksum field zeroed + * @param payload the TCP payload data + * @return byte array containing pseudo-header + TCP header + payload for checksum calculation + * @see RFC 9293 Section 3.1 + */ private static byte[] getChecksumData(IpHeader header, TcpHeader tcpHeader, byte[] payload) { ByteBuffer checksumBuf = ByteBuffer.allocate(header.getPseudoHeaderLength() + TcpHeader.TCP_HEADER_LEN + payload.length); checksumBuf.put(header.getPseudoHeader()); diff --git a/src/main/java/com/github/hirsivaja/ip/udp/UdpMessagePayload.java b/src/main/java/com/github/hirsivaja/ip/udp/UdpMessagePayload.java index 8db1c78..6c77848 100644 --- a/src/main/java/com/github/hirsivaja/ip/udp/UdpMessagePayload.java +++ b/src/main/java/com/github/hirsivaja/ip/udp/UdpMessagePayload.java @@ -38,6 +38,30 @@ public int getLength() { return header.getLength() + UdpHeader.UDP_HEADER_LEN + payload.length; } + /** + * Constructs the data for UDP checksum calculation as specified in RFC 768. + * + *

UDP checksum is calculated over the concatenation of:

+ *
    + *
  1. Pseudo-header - Provides protection against misrouted packets + * + *
  2. + *
  3. UDP Header - 8 bytes (with checksum field zeroed)
  4. + *
  5. UDP Payload - Variable length data
  6. + *
+ * + *

The checksum field in the UDP header must be set to zero before calling this method. + * UDP checksum is optional for IPv4 but mandatory for IPv6.

+ * + * @param header the IP header (IPv4 or IPv6) providing the pseudo-header + * @param udpHeader the UDP header with checksum field zeroed + * @param payload the UDP payload data + * @return byte array containing pseudo-header + UDP header + payload for checksum calculation + * @see RFC 768 + */ private static byte[] getChecksumData(IpHeader header, UdpHeader udpHeader, byte[] payload) { ByteBuffer checksumBuf = ByteBuffer.allocate(header.getPseudoHeaderLength() + UdpHeader.UDP_HEADER_LEN + payload.length); checksumBuf.put(header.getPseudoHeader()); diff --git a/src/test/java/com/github/hirsivaja/ip/InternetChecksumTest.java b/src/test/java/com/github/hirsivaja/ip/InternetChecksumTest.java new file mode 100644 index 0000000..63bf2ea --- /dev/null +++ b/src/test/java/com/github/hirsivaja/ip/InternetChecksumTest.java @@ -0,0 +1,189 @@ +package com.github.hirsivaja.ip; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; + +@RunWith(Enclosed.class) +public class InternetChecksumTest { + + public static class InternetChecksumSingleTest { + + @Test + public void simpleChecksumTest() { + String data = "20010DB8000001"; + short expectedChecksum = (short) (0xD146 & 0xFFFF); + + byte[] bytes = IpUtils.parseHexBinary(data); + short calculatedChecksum = IpUtils.calculateInternetChecksum(bytes); + System.out.println("calculatedChecksum = " + calculatedChecksum + " (0x" + Integer.toHexString(calculatedChecksum & 0xFFFF).toUpperCase() + ")"); + System.out.println("expectedChecksum = " + expectedChecksum + " (0x" + Integer.toHexString(expectedChecksum & 0xFFFF).toUpperCase() + ")"); + Assert.assertEquals(expectedChecksum, calculatedChecksum); + + } + } + + + @RunWith(Parameterized.class) + public static class InternetChecksumParamTest { + + private final String testName; + private final String inputHex; + private final String fullDataHex; + private final short expectedChecksum; + + public InternetChecksumParamTest(String testName, String inputHex, String fullDataHex, + short expectedChecksum) { + this.testName = testName; + this.inputHex = inputHex; + this.fullDataHex = fullDataHex; + this.expectedChecksum = expectedChecksum; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection data() { + return Arrays.asList(new Object[][]{ + { + "Basic test", + "E34F2396442799F3", + "E34F2396442799F31AFF", + (short) 0x1AFF, + }, + { + "Second basic test", + "0001F203F4F5F6F7", + "0001F203F4F5F6F7220D", + (short) 0x220D + }, + + // Multiple carries test cases + { + "Two 0xFFFF values causing multiple carries", + "FFFFFFFF", + "FFFFFFFF0000", + (short) 0x0000, + }, + { + "Pattern causing carry propagation", + "FFFF0001", + "FFFF0001FFFE", + (short) 0xFFFE, + }, + + // IPv4 header examples + { + "IPv4 header with correct checksum should verify to 0", + "4500002B50A340007F06C894C0A83801AC1E3DCD", + "4500002B50A340007F06C894C0A83801AC1E3DCD", + (short) 0x0000, + }, + { + "IPv4 header without checksum field", + "4500002B50A340007F060000C0A83801AC1E3DCD", + "4500002B50A340007F06C894C0A83801AC1E3DCD", + (short) 0xC894, + }, + + // IPv6 pseudo-header test cases + { + "IPv6 pseudo-header for ICMPv6", + "20010DB8000000000000000000000001" + // Source: 2001:db8::1 + "20010DB8000000000000000000000002" + // Dest: 2001:db8::2 + "0000000800000000003A", // Length=8, zeros, next header=58 + "20010DB8000000000000000000000001" + + "20010DB8000000000000000000000002" + + "0000000800000000003A" + "A448", // With checksum + (short) 0xA448 + }, + { + "IPv6 pseudo-header with correct checksum should verify to 0", + "20010DB8000000000000000000000001" + // Source: 2001:db8::1 + "20010DB8000000000000000000000002" + // Dest: 2001:db8::2 + "0000000800000000003A" + "A448", // Length=8, zeros, next header=58, checksum=0xA448 + "20010DB8000000000000000000000001" + + "20010DB8000000000000000000000002" + + "0000000800000000003A" + "A448", // With checksum should verify to 0 + (short) 0x0000, + }, + + // Edge cases + { + "All zeros should produce 0xFFFF checksum", + "0000000000000000", + "0000000000000000FFFF", + (short) 0xFFFF, + }, + { + "Single 0xFF byte (odd length)", + "FF", + "FF0000FF", + (short) 0x00FF, + } + }); + } + + @Test + public void calculateChecksumTest() { + byte[] data = IpUtils.parseHexBinary(inputHex); + short actualChecksum = IpUtils.calculateInternetChecksum(data); + + Assert.assertEquals( + String.format("Failed for test '%s': expected 0x%04X, got 0x%04X", + testName, expectedChecksum & 0xFFFF, actualChecksum & 0xFFFF), + expectedChecksum, + actualChecksum + ); + } + + @Test + public void verifyChecksumTest() { + byte[] data = IpUtils.parseHexBinary(inputHex); + short calculatedChecksum = IpUtils.calculateInternetChecksum(data); + + Assert.assertTrue( + String.format("Verification failed for test '%s'", testName), + IpUtils.verifyInternetChecksum(data, calculatedChecksum) + ); + } + + @Test + public void verifyChecksumWithFullDataTest() { + byte[] fullData = IpUtils.parseHexBinary(fullDataHex); + + Assert.assertTrue( + String.format("Full data verification failed for test '%s'", testName), + IpUtils.verifyInternetChecksum(fullData) + ); + } + + @Test + public void ensureChecksumTest() { + byte[] data = IpUtils.parseHexBinary(inputHex); + short calculatedChecksum = IpUtils.calculateInternetChecksum(data); + + try { + IpUtils.ensureInternetChecksum(data, calculatedChecksum); + } catch (IllegalArgumentException e) { + Assert.fail(String.format("ensureInternetChecksum failed for test '%s': %s", + testName, e.getMessage())); + } + } + + @Test + public void ensureChecksumWithFullDataTest() { + byte[] fullData = IpUtils.parseHexBinary(fullDataHex); + + try { + IpUtils.ensureInternetChecksum(fullData); + } catch (IllegalArgumentException e) { + Assert.fail(String.format("ensureInternetChecksum with full data failed for test '%s': %s", + testName, e.getMessage())); + } + } + } +} \ No newline at end of file