Skip to content

Fix checksum calculation #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 115 additions & 17 deletions src/main/java/com/github/hirsivaja/ip/IpUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.</p>
*
* <p>Algorithm:</p>
* <ol>
* <li>Sum all 16-bit words in the data</li>
* <li>Add any odd byte as the high byte of a 16-bit word</li>
* <li>Fold carries from bits 16-31 into bits 0-15 until no more carries</li>
* <li>Return the one's complement of the result</li>
* </ol>
*
* <p>This checksum is used by IPv4 headers, TCP, UDP, ICMP, and other
* Internet protocols as specified in their respective RFCs.</p>
*
* @param data the byte array to compute the checksum for
* @return the 16-bit Internet checksum
* @throws IllegalArgumentException if data is null
* @see <a href="https://datatracker.ietf.org/doc/html/rfc1071">RFC 1071</a>
*/
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.
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* @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);
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/github/hirsivaja/ip/icmp/IcmpPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ public void encode(ByteBuffer out) {
message.encode(out);
}

/**
* Constructs the data for ICMP checksum calculation as specified in RFC 792.
*
* <p>ICMP checksum is calculated over the entire ICMP message:</p>
* <ol>
* <li><b>ICMP Type</b> - 1 byte</li>
* <li><b>ICMP Code</b> - 1 byte</li>
* <li><b>ICMP Checksum</b> - 2 bytes (set to zero during calculation)</li>
* <li><b>ICMP Message Data</b> - Variable length, depends on ICMP type</li>
* </ol>
*
* <p><b>Important:</b> ICMP does NOT use a pseudo-header. The checksum covers
* only the ICMP header and data, not any IP header information.</p>
*
* @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 <a href="https://datatracker.ietf.org/doc/html/rfc792">RFC 792</a>
*/
private static byte[] getChecksumData(IcmpMessage message, short checksum) {
ByteBuffer checksumBuf = ByteBuffer.allocate(message.getLength());
checksumBuf.put(message.getType().getType());
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/github/hirsivaja/ip/icmpv6/Icmpv6Payload.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,35 @@ public void encode(ByteBuffer out) {
message.encode(out);
}

/**
* Constructs the data for ICMPv6 checksum calculation as specified in RFC 4443.
*
* <p>ICMPv6 checksum is calculated over the concatenation of:</p>
* <ol>
* <li><b>IPv6 Pseudo-header</b> - 40 bytes, provides protection against misrouted packets
* <ul>
* <li>Source Address (16 bytes)</li>
* <li>Destination Address (16 bytes)</li>
* <li>ICMPv6 Length (4 bytes)</li>
* <li>Zero padding (3 bytes)</li>
* <li>Next Header = 58 (ICMPv6) (1 byte)</li>
* </ul>
* </li>
* <li><b>ICMPv6 Type</b> - 1 byte</li>
* <li><b>ICMPv6 Code</b> - 1 byte</li>
* <li><b>ICMPv6 Checksum</b> - 2 bytes (set to zero during calculation)</li>
* <li><b>ICMPv6 Message Data</b> - Variable length, depends on ICMPv6 type</li>
* </ol>
*
* <p><b>Important:</b> Unlike ICMP, ICMPv6 requires a pseudo-header for checksum calculation.
* The checksum is mandatory for all ICMPv6 messages.</p>
*
* @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 <a href="https://datatracker.ietf.org/doc/html/rfc4443">RFC 4443</a>
*/
private static byte[] getChecksumData(Ipv6Header header, Icmpv6Message message, short checksum) {
ByteBuffer checksumBuf = ByteBuffer.allocate(Ipv6Header.HEADER_LEN + message.getLength());
checksumBuf.put(header.getPseudoHeader());
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/github/hirsivaja/ip/igmp/IgmpPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ public void encode(ByteBuffer out) {
message.encode(out);
}

/**
* Constructs the data for IGMP checksum calculation as specified in RFC 3376.
*
* <p>IGMP checksum is calculated over the entire IGMP message:</p>
* <ol>
* <li><b>IGMP Type</b> - 1 byte (Query, Report, etc.)</li>
* <li><b>IGMP Code</b> - 1 byte (Max Response Time for queries)</li>
* <li><b>IGMP Checksum</b> - 2 bytes (set to zero during calculation)</li>
* <li><b>IGMP Message Data</b> - Variable length, depends on IGMP type and version</li>
* </ol>
*
* <p><b>Important:</b> 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).</p>
*
* @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 <a href="https://datatracker.ietf.org/doc/html/rfc3376">RFC 3376</a>
*/
private static byte[] getChecksumData(IgmpMessage message, short checksum) {
ByteBuffer checksumBuf = ByteBuffer.allocate(message.getLength());
checksumBuf.put(message.getType().getType());
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/com/github/hirsivaja/ip/tcp/TcpMessagePayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>TCP checksum is calculated over the concatenation of:</p>
* <ol>
* <li><b>Pseudo-header</b> - Provides protection against misrouted packets
* <ul>
* <li>IPv4: Source IP (4) + Dest IP (4) + Zero (1) + Protocol (1) + TCP Length (2) = 12 bytes</li>
* <li>IPv6: Source IP (16) + Dest IP (16) + TCP Length (4) + Zero (3) + Protocol (1) = 40 bytes</li>
* </ul>
* </li>
* <li><b>TCP Header</b> - 20 bytes minimum (with checksum field zeroed)</li>
* <li><b>TCP Payload</b> - Variable length data</li>
* </ol>
*
* <p>The checksum field in the TCP header must be set to zero before calling this method.</p>
*
* @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 <a href="https://datatracker.ietf.org/doc/html/rfc9293#section-3.1">RFC 9293 Section 3.1</a>
*/
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());
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/github/hirsivaja/ip/udp/UdpMessagePayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>UDP checksum is calculated over the concatenation of:</p>
* <ol>
* <li><b>Pseudo-header</b> - Provides protection against misrouted packets
* <ul>
* <li>IPv4: Source IP (4) + Dest IP (4) + Zero (1) + Protocol (1) + UDP Length (2) = 12 bytes</li>
* <li>IPv6: Source IP (16) + Dest IP (16) + UDP Length (4) + Zero (3) + Protocol (1) = 40 bytes</li>
* </ul>
* </li>
* <li><b>UDP Header</b> - 8 bytes (with checksum field zeroed)</li>
* <li><b>UDP Payload</b> - Variable length data</li>
* </ol>
*
* <p>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.</p>
*
* @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 <a href="https://datatracker.ietf.org/doc/html/rfc768">RFC 768</a>
*/
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());
Expand Down
Loading