diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp index 0585c09c71..4c58228c6d 100644 --- a/staticlibs/Android.bp +++ b/staticlibs/Android.bp @@ -139,6 +139,7 @@ java_library { srcs: [ "device/com/android/net/module/util/HexDump.java", "device/com/android/net/module/util/Ipv6Utils.java", + "device/com/android/net/module/util/PacketBuilder.java", "device/com/android/net/module/util/Struct.java", "device/com/android/net/module/util/structs/*.java", ], diff --git a/staticlibs/device/com/android/net/module/util/PacketBuilder.java b/staticlibs/device/com/android/net/module/util/PacketBuilder.java new file mode 100644 index 0000000000..e9ecb94dec --- /dev/null +++ b/staticlibs/device/com/android/net/module/util/PacketBuilder.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.net.module.util; + +import static android.system.OsConstants.IPPROTO_IP; +import static android.system.OsConstants.IPPROTO_TCP; + +import static com.android.net.module.util.IpUtils.ipChecksum; +import static com.android.net.module.util.IpUtils.tcpChecksum; +import static com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET; +import static com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET; +import static com.android.net.module.util.NetworkStackConstants.TCP_CHECKSUM_OFFSET; + +import android.net.MacAddress; + +import androidx.annotation.NonNull; + +import com.android.net.module.util.structs.EthernetHeader; +import com.android.net.module.util.structs.Ipv4Header; +import com.android.net.module.util.structs.TcpHeader; + +import java.io.IOException; +import java.net.Inet4Address; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * The class is used to build a packet. + * + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Layer 2 header (EthernetHeader) | (optional) + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Layer 3 header (Ipv4Header) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Layer 4 header (TcpHeader) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Payload | (optional) + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * Below is a sample code to build a packet. + * + * // Initialize builder + * final ByteBuffer buf = ByteBuffer.allocate(...); + * final PacketBuilder pb = new PacketBuilder(buf); + * // Write headers + * pb.writeL2Header(...); + * pb.writeIpHeader(...); + * pb.writeTcpHeader(...); + * // Write payload + * buf.putInt(...); + * buf.putShort(...); + * buf.putByte(...); + * // Finalize and use the packet + * pb.finalizePacket(); + * sendPacket(buf); + */ +public class PacketBuilder { + private final ByteBuffer mBuffer; + + private int mIpv4HeaderOffset = -1; + private int mTcpHeaderOffset = -1; + + public PacketBuilder(@NonNull ByteBuffer buffer) { + mBuffer = buffer; + } + + /** + * Write an ethernet header. + * + * @param srcMac source MAC address + * @param dstMac destination MAC address + * @param etherType ether type + */ + public void writeL2Header(MacAddress srcMac, MacAddress dstMac, short etherType) throws + IOException { + final EthernetHeader ethv4Header = new EthernetHeader(dstMac, srcMac, etherType); + try { + ethv4Header.writeToByteBuffer(mBuffer); + } catch (IllegalArgumentException | BufferOverflowException e) { + throw new IOException("Error writing to buffer: ", e); + } + } + + /** + * Write an IPv4 header. + * The IP header length and checksum are calculated and written back in #finalizePacket. + * + * @param tos type of service + * @param id the identification + * @param flagsAndFragmentOffset flags and fragment offset + * @param ttl time to live + * @param protocol protocol + * @param srcIp source IP address + * @param dstIp destination IP address + */ + public void writeIpv4Header(byte tos, short id, short flagsAndFragmentOffset, byte ttl, + byte protocol, @NonNull final Inet4Address srcIp, @NonNull final Inet4Address dstIp) + throws IOException { + mIpv4HeaderOffset = mBuffer.position(); + final Ipv4Header ipv4Header = new Ipv4Header(tos, + (short) 0 /* totalLength, calculate in #finalizePacket */, id, + flagsAndFragmentOffset, ttl, protocol, + (short) 0 /* checksum, calculate in #finalizePacket */, srcIp, dstIp); + + try { + ipv4Header.writeToByteBuffer(mBuffer); + } catch (IllegalArgumentException | BufferOverflowException e) { + throw new IOException("Error writing to buffer: ", e); + } + } + + /** + * Write a TCP header. + * The TCP header checksum is calculated and written back in #finalizePacket. + * + * @param srcPort source port + * @param dstPort destination port + * @param seq sequence number + * @param ack acknowledgement number + * @param tcpFlags tcp flags + * @param window window size + * @param urgentPointer urgent pointer + */ + public void writeTcpHeader(short srcPort, short dstPort, short seq, short ack, + byte tcpFlags, short window, short urgentPointer) throws IOException { + mTcpHeaderOffset = mBuffer.position(); + final TcpHeader tcpHeader = new TcpHeader(srcPort, dstPort, seq, ack, + (short) ((short) 0x5000 | ((byte) 0x3f & tcpFlags)) /* dataOffsetAndControlBits, + dataOffset is always 5(*4bytes) because options not supported */, window, + (short) 0 /* checksum, calculate in #finalizePacket */, + urgentPointer); + + try { + tcpHeader.writeToByteBuffer(mBuffer); + } catch (IllegalArgumentException | BufferOverflowException e) { + throw new IOException("Error writing to buffer: ", e); + } + } + + /** + * Finalize the packet. + * + * Call after writing L4 header (no payload) or payload to the buffer used by the builder. + * L3 header length, L3 header checksum and L4 header checksum are calculated and written back + * after finalization. + */ + @NonNull + public ByteBuffer finalizePacket() throws IOException { + if (mIpv4HeaderOffset < 0) { + // TODO: add support for IPv6 + throw new IOException("Packet is missing IPv4 header"); + } + + // Populate the IPv4 totalLength field. + mBuffer.putShort(mIpv4HeaderOffset + IPV4_LENGTH_OFFSET, + (short) (mBuffer.position() - mIpv4HeaderOffset)); + + // Populate the IPv4 header checksum field. + mBuffer.putShort(mIpv4HeaderOffset + IPV4_CHECKSUM_OFFSET, + ipChecksum(mBuffer, mIpv4HeaderOffset /* headerOffset */)); + + // Populate the TCP header checksum field. + if (mTcpHeaderOffset > 0) { + mBuffer.putShort(mTcpHeaderOffset + TCP_CHECKSUM_OFFSET, tcpChecksum(mBuffer, + mIpv4HeaderOffset /* ipOffset */, mTcpHeaderOffset /* transportOffset */, + mBuffer.position() - mTcpHeaderOffset /* transportLen */)); + } else { // TODO: add support for UDP + throw new IOException("Packet is missing TCP header"); + } + + mBuffer.flip(); + return mBuffer; + } + + /** + * Allocate bytebuffer for building the packet. + * + * @param hasEther has ethernet header. Set this flag to indicate that the packet has an + * ethernet header. + * @param l3proto the layer 3 protocol. Only {@code IPPROTO_IP} currently supported. + * @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} currently supported. + * @param payloadLen length of the payload. + */ + @NonNull + public static ByteBuffer allocate(boolean hasEther, int l3proto, int l4proto, int payloadLen) { + if (l3proto != IPPROTO_IP) { + // TODO: add support for IPv6 + throw new IllegalArgumentException("Unsupported layer 3 protocol " + l3proto); + } + + if (l4proto != IPPROTO_TCP) { + // TODO: add support for UDP + throw new IllegalArgumentException("Unsupported layer 4 protocol " + l4proto); + } + + if (payloadLen < 0) { + throw new IllegalArgumentException("Invalid payload length " + payloadLen); + } + + int packetLen = 0; + if (hasEther) packetLen += Struct.getSize(EthernetHeader.class); + packetLen += Struct.getSize(Ipv4Header.class); + packetLen += Struct.getSize(TcpHeader.class); + packetLen += payloadLen; + + return ByteBuffer.allocate(packetLen); + } +} diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java index f7151d7b17..378e4857a6 100644 --- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java +++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java @@ -89,9 +89,11 @@ public final class NetworkStackConstants { public static final int IPV4_MAX_MTU = 65_535; public static final int IPV4_HEADER_MIN_LEN = 20; public static final int IPV4_IHL_MASK = 0xf; + public static final int IPV4_LENGTH_OFFSET = 2; public static final int IPV4_FLAGS_OFFSET = 6; public static final int IPV4_FRAGMENT_MASK = 0x1fff; public static final int IPV4_PROTOCOL_OFFSET = 9; + public static final int IPV4_CHECKSUM_OFFSET = 10; public static final int IPV4_SRC_ADDR_OFFSET = 12; public static final int IPV4_DST_ADDR_OFFSET = 16; public static final int IPV4_ADDR_LEN = 4; @@ -163,6 +165,21 @@ public final class NetworkStackConstants { public static final byte PIO_FLAG_ON_LINK = (byte) (1 << 7); public static final byte PIO_FLAG_AUTONOMOUS = (byte) (1 << 6); + /** + * TCP constants. + * + * See also: + * - https://tools.ietf.org/html/rfc793 + */ + public static final int TCP_HEADER_MIN_LEN = 20; + public static final int TCP_CHECKSUM_OFFSET = 16; + public static final byte TCPHDR_FIN = (byte) (1 << 0); + public static final byte TCPHDR_SYN = (byte) (1 << 1); + public static final byte TCPHDR_RST = (byte) (1 << 2); + public static final byte TCPHDR_PSH = (byte) (1 << 3); + public static final byte TCPHDR_ACK = (byte) (1 << 4); + public static final byte TCPHDR_URG = (byte) (1 << 5); + /** * UDP constants. * diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java new file mode 100644 index 0000000000..275aa847d3 --- /dev/null +++ b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.net.module.util; + +import static android.system.OsConstants.IPPROTO_IP; +import static android.system.OsConstants.IPPROTO_TCP; + +import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4; +import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN; +import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK; +import static com.android.net.module.util.NetworkStackConstants.TCP_HEADER_MIN_LEN; +import static com.android.testutils.MiscAsserts.assertThrows; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import android.net.InetAddresses; +import android.net.MacAddress; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.structs.EthernetHeader; +import com.android.net.module.util.structs.Ipv4Header; +import com.android.net.module.util.structs.TcpHeader; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.net.Inet4Address; +import java.nio.ByteBuffer; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PacketBuilderTest { + private static final MacAddress SRC_MAC = MacAddress.fromString("11:22:33:44:55:66"); + private static final MacAddress DST_MAC = MacAddress.fromString("aa:bb:cc:dd:ee:ff"); + private static final Inet4Address IPV4_SRC_ADDR = addr("192.0.2.1"); + private static final Inet4Address IPV4_DST_ADDR = addr("198.51.100.1"); + private static final short SRC_PORT = 9876; + private static final short DST_PORT = 433; + private static final short SEQ_NO = 13579; + private static final short ACK_NO = 24680; + private static final byte TYPE_OF_SERVICE = 0; + private static final short ID = 27149; + private static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0 + private static final byte TIME_TO_LIVE = (byte) 0x40; + private static final short WINDOW = (short) 0x2000; + private static final short URGENT_POINTER = 0; + private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[] { + (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef + }); + + private static final byte[] TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR = + new byte[] { + // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff", + // type='IPv4') / + // scapy.IP(src="192.0.2.1", dst="198.51.100.1", + // tos=0, id=27149, flags='DF') / + // scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680, + // flags='A', window=8192, urgptr=0)) + // Ether header + (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd, + (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22, + (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66, + (byte) 0x08, (byte) 0x00, + // IPv4 header + (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x28, + (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00, + (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x8c, + (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01, + (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01, + // TCP header + (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1, + (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b, + (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68, + (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00, + (byte) 0xe5, (byte) 0xe5, (byte) 0x00, (byte) 0x00 + }; + + private static final byte[] TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR_DATA = + new byte[] { + // packet = (scapy.Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff", + // type='IPv4') / + // scapy.IP(src="192.0.2.1", dst="198.51.100.1", + // tos=0, id=27149, flags='DF') / + // scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680, + // flags='A', window=8192, urgptr=0) / + // b'\xde\xad\xbe\xef') + // Ether header + (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd, + (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22, + (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66, + (byte) 0x08, (byte) 0x00, + // IPv4 header + (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x2c, + (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00, + (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x88, + (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01, + (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01, + // TCP header + (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1, + (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b, + (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68, + (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00, + (byte) 0x48, (byte) 0x44, (byte) 0x00, (byte) 0x00, + // Data + (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef + }; + + private static final byte[] TEST_PACKET_IPV4HDR_TCPHDR = + new byte[] { + // packet = (scapy.IP(src="192.0.2.1", dst="198.51.100.1", + // tos=0, id=27149, flags='DF') / + // scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680, + // flags='A', window=8192, urgptr=0)) + // IPv4 header + (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x28, + (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00, + (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x8c, + (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01, + (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01, + // TCP header + (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1, + (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b, + (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68, + (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00, + (byte) 0xe5, (byte) 0xe5, (byte) 0x00, (byte) 0x00 + }; + + private static final byte[] TEST_PACKET_IPV4HDR_TCPHDR_DATA = + new byte[] { + // packet = (scapy.IP(src="192.0.2.1", dst="198.51.100.1", + // tos=0, id=27149, flags='DF') / + // scapy.TCP(sport=9876, dport=433, seq=13579, ack=24680, + // flags='A', window=8192, urgptr=0) / + // b'\xde\xad\xbe\xef') + // IPv4 header + (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0x2c, + (byte) 0x6a, (byte) 0x0d, (byte) 0x40, (byte) 0x00, + (byte) 0x40, (byte) 0x06, (byte) 0xe4, (byte) 0x88, + (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01, + (byte) 0xc6, (byte) 0x33, (byte) 0x64, (byte) 0x01, + // TCP header + (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1, + (byte) 0x00, (byte) 0x00, (byte) 0x35, (byte) 0x0b, + (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x68, + (byte) 0x50, (byte) 0x10, (byte) 0x20, (byte) 0x00, + (byte) 0x48, (byte) 0x44, (byte) 0x00, (byte) 0x00, + // Data + (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef + }; + + /** + * Build a TCPv4 packet which has ether header, IPv4 header, TCP header and data. + * The ethernet header and data are optional. Note that both source mac address and + * destination mac address are required for ethernet header. + * + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Layer 2 header (EthernetHeader) | (optional) + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Layer 3 header (Ipv4Header) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Layer 4 header (TcpHeader) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Payload | (optional) + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * @param srcMac source MAC address. used by L2 ether header. + * @param dstMac destination MAC address. used by L2 ether header. + * @param payload the payload. + */ + @NonNull + private ByteBuffer buildTcpv4Packet(@Nullable final MacAddress srcMac, + @Nullable final MacAddress dstMac, @Nullable final ByteBuffer payload) + throws Exception { + final boolean hasEther = (srcMac != null && dstMac != null); + final int payloadLen = (payload == null) ? 0 : payload.limit(); + final ByteBuffer buffer = PacketBuilder.allocate(hasEther, IPPROTO_IP, IPPROTO_TCP, + payloadLen); + final PacketBuilder packetBuilder = new PacketBuilder(buffer); + + if (hasEther) packetBuilder.writeL2Header(srcMac, dstMac, (short) ETHER_TYPE_IPV4); + packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET, + TIME_TO_LIVE, (byte) IPPROTO_TCP, IPV4_SRC_ADDR, IPV4_DST_ADDR); + packetBuilder.writeTcpHeader(SRC_PORT, DST_PORT, SEQ_NO, ACK_NO, + TCPHDR_ACK, WINDOW, URGENT_POINTER); + if (payload != null) { + buffer.put(payload); + // in case data might be reused by caller, restore the position and + // limit of bytebuffer. + payload.clear(); + } + + return packetBuilder.finalizePacket(); + } + + private void checkEtherHeader(final ByteBuffer actual) { + final EthernetHeader eth = Struct.parse(EthernetHeader.class, actual); + assertEquals(SRC_MAC, eth.srcMac); + assertEquals(DST_MAC, eth.dstMac); + assertEquals(ETHER_TYPE_IPV4, eth.etherType); + } + + private void checkIpv4Header(final boolean hasData, final ByteBuffer actual) { + final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, actual); + assertEquals(Ipv4Header.IPHDR_VERSION_IHL, ipv4Header.vi); + assertEquals(TYPE_OF_SERVICE, ipv4Header.tos); + assertEquals(ID, ipv4Header.id); + assertEquals(FLAGS_AND_FRAGMENT_OFFSET, ipv4Header.flagsAndFragmentOffset); + assertEquals(TIME_TO_LIVE, ipv4Header.ttl); + assertEquals(IPV4_SRC_ADDR, ipv4Header.srcIp); + assertEquals(IPV4_DST_ADDR, ipv4Header.dstIp); + + final int dataLength = hasData ? DATA.limit() : 0; + assertEquals(IPV4_HEADER_MIN_LEN + TCP_HEADER_MIN_LEN + dataLength, + ipv4Header.totalLength); + assertEquals((byte) IPPROTO_TCP, ipv4Header.protocol); + assertEquals(hasData ? (short) 0xe488 : (short) 0xe48c, ipv4Header.checksum); + } + + private void checkTcpv4Packet(final boolean hasEther, final boolean hasData, + final ByteBuffer actual) { + if (hasEther) { + checkEtherHeader(actual); + } + checkIpv4Header(hasData, actual); + + final TcpHeader tcpHeader = Struct.parse(TcpHeader.class, actual); + assertEquals(SRC_PORT, tcpHeader.srcPort); + assertEquals(DST_PORT, tcpHeader.dstPort); + assertEquals(SEQ_NO, tcpHeader.seq); + assertEquals(ACK_NO, tcpHeader.ack); + assertEquals((short) 0x5010 /* offset=5(*4bytes), control bits=ACK */, + tcpHeader.dataOffsetAndControlBits); + assertEquals(WINDOW, tcpHeader.window); + assertEquals(hasData ? (short) 0x4844 : (short) 0xe5e5, tcpHeader.checksum); + assertEquals(URGENT_POINTER, tcpHeader.urgentPointer); + } + + @Test + public void testBuildPacketEtherIPv4Tcp() throws Exception { + final ByteBuffer packet = buildTcpv4Packet(SRC_MAC, DST_MAC, null /* data */); + checkTcpv4Packet(true /* hasEther */, false /* hasData */, packet); + assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR, packet.array()); + } + + @Test + public void testBuildPacketEtherIPv4TcpData() throws Exception { + final ByteBuffer packet = buildTcpv4Packet(SRC_MAC, DST_MAC, DATA); + checkTcpv4Packet(true /* hasEther */, true /* hasData */, packet); + assertArrayEquals(TEST_PACKET_ETHERHDR_IPV4HDR_TCPHDR_DATA, + packet.array()); + } + + @Test + public void testBuildPacketIPv4Tcp() throws Exception { + final ByteBuffer packet = buildTcpv4Packet(null /* srcMac */, null /* dstMac */, + null /* data */); + checkTcpv4Packet(false /* hasEther */, false /* hasData */, packet); + assertArrayEquals(TEST_PACKET_IPV4HDR_TCPHDR, packet.array()); + } + + @Test + public void testBuildPacketIPv4TcpData() throws Exception { + final ByteBuffer packet = buildTcpv4Packet(null /* srcMac */, null /* dstMac */, DATA); + checkTcpv4Packet(false /* hasEther */, true /* hasData */, packet); + assertArrayEquals(TEST_PACKET_IPV4HDR_TCPHDR_DATA, packet.array()); + } + + @Test + public void testFinalizePacketWithoutIpv4Header() throws Exception { + final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */, IPPROTO_IP, + IPPROTO_TCP, 0 /* payloadLen */); + final PacketBuilder packetBuilder = new PacketBuilder(buffer); + packetBuilder.writeTcpHeader(SRC_PORT, DST_PORT, SEQ_NO, ACK_NO, + TCPHDR_ACK, WINDOW, URGENT_POINTER); + assertThrows("java.io.IOException: Packet is missing IPv4 header", IOException.class, + () -> packetBuilder.finalizePacket()); + } + + @Test + public void testFinalizePacketWithoutTcpHeader() throws Exception { + final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */, IPPROTO_IP, + IPPROTO_TCP, 0 /* payloadLen */); + final PacketBuilder packetBuilder = new PacketBuilder(buffer); + packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET, + TIME_TO_LIVE, (byte) IPPROTO_TCP, IPV4_SRC_ADDR, IPV4_DST_ADDR); + assertThrows("java.io.IOException: Packet is missing TCP headers", IOException.class, + () -> packetBuilder.finalizePacket()); + } + + @Test + public void testWriteL2HeaderToInsufficientBuffer() throws Exception { + final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1)); + assertThrows(IOException.class, + () -> packetBuilder.writeL2Header(SRC_MAC, DST_MAC, (short) ETHER_TYPE_IPV4)); + } + + @Test + public void testWriteIpv4HeaderToInsufficientBuffer() throws Exception { + final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1)); + assertThrows(IOException.class, + () -> packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET, + TIME_TO_LIVE, (byte) IPPROTO_TCP, IPV4_SRC_ADDR, IPV4_DST_ADDR)); + } + + @Test + public void testWriteTcpHeaderToInsufficientBuffer() throws Exception { + final PacketBuilder packetBuilder = new PacketBuilder(ByteBuffer.allocate(1)); + assertThrows(IOException.class, + () -> packetBuilder.writeTcpHeader(SRC_PORT, DST_PORT, SEQ_NO, ACK_NO, + TCPHDR_ACK, WINDOW, URGENT_POINTER)); + } + + private static Inet4Address addr(String addr) { + return (Inet4Address) InetAddresses.parseNumericAddress(addr); + } +}