1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.dhcp;
18 
19 import static android.net.dhcp.DhcpPacket.DHCP_BROADCAST_ADDRESS;
20 import static android.net.dhcp.DhcpPacket.DHCP_DNS_SERVER;
21 import static android.net.dhcp.DhcpPacket.DHCP_DOMAIN_NAME;
22 import static android.net.dhcp.DhcpPacket.DHCP_LEASE_TIME;
23 import static android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_ACK;
24 import static android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_OFFER;
25 import static android.net.dhcp.DhcpPacket.DHCP_MTU;
26 import static android.net.dhcp.DhcpPacket.DHCP_REBINDING_TIME;
27 import static android.net.dhcp.DhcpPacket.DHCP_RENEWAL_TIME;
28 import static android.net.dhcp.DhcpPacket.DHCP_ROUTER;
29 import static android.net.dhcp.DhcpPacket.DHCP_SUBNET_MASK;
30 import static android.net.dhcp.DhcpPacket.DHCP_VENDOR_INFO;
31 import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP;
32 import static android.net.dhcp.DhcpPacket.ENCAP_L2;
33 import static android.net.dhcp.DhcpPacket.ENCAP_L3;
34 import static android.net.dhcp.DhcpPacket.INADDR_ANY;
35 import static android.net.dhcp.DhcpPacket.INFINITE_LEASE;
36 import static android.net.dhcp.DhcpPacket.ParseException;
37 
38 import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
39 import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
40 
41 import static org.junit.Assert.assertEquals;
42 import static org.junit.Assert.assertNotNull;
43 import static org.junit.Assert.assertNull;
44 import static org.junit.Assert.assertTrue;
45 import static org.junit.Assert.fail;
46 
47 import android.annotation.Nullable;
48 import android.net.DhcpResults;
49 import android.net.InetAddresses;
50 import android.net.LinkAddress;
51 import android.net.metrics.DhcpErrorEvent;
52 
53 import androidx.test.filters.SmallTest;
54 import androidx.test.runner.AndroidJUnit4;
55 
56 import com.android.internal.util.HexDump;
57 
58 import org.junit.After;
59 import org.junit.Before;
60 import org.junit.Test;
61 import org.junit.runner.RunWith;
62 
63 import java.io.ByteArrayOutputStream;
64 import java.net.Inet4Address;
65 import java.nio.ByteBuffer;
66 import java.nio.charset.Charset;
67 import java.util.ArrayList;
68 import java.util.Arrays;
69 import java.util.Collections;
70 import java.util.Random;
71 
72 @RunWith(AndroidJUnit4.class)
73 @SmallTest
74 public class DhcpPacketTest {
75 
76     private static final Inet4Address SERVER_ADDR = v4Address("192.0.2.1");
77     private static final Inet4Address CLIENT_ADDR = v4Address("192.0.2.234");
78     private static final int PREFIX_LENGTH = 22;
79     private static final Inet4Address NETMASK = getPrefixMaskAsInet4Address(PREFIX_LENGTH);
80     private static final Inet4Address BROADCAST_ADDR = getBroadcastAddress(
81             SERVER_ADDR, PREFIX_LENGTH);
82     private static final String HOSTNAME = "testhostname";
83     private static final String CAPTIVE_PORTAL_API_URL = "https://example.com/capportapi";
84     private static final short MTU = 1500;
85     // Use our own empty address instead of IPV4_ADDR_ANY or INADDR_ANY to ensure that the code
86     // doesn't use == instead of equals when comparing addresses.
87     private static final Inet4Address ANY = v4Address("0.0.0.0");
88     private static final byte[] TEST_EMPTY_OPTIONS_SKIP_LIST = new byte[0];
89     private static final int TEST_IPV6_ONLY_WAIT_S = 1800; // 30 min
90 
91     private static final byte[] CLIENT_MAC = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
92 
v4Address(String addrString)93     private static final Inet4Address v4Address(String addrString) throws IllegalArgumentException {
94         return (Inet4Address) InetAddresses.parseNumericAddress(addrString);
95     }
96 
97     @Before
setUp()98     public void setUp() {
99         DhcpPacket.sTestOverrideVendorId = "android-dhcp-???";
100     }
101 
102     @After
tearDown()103     public void tearDown() {
104         DhcpPacket.sTestOverrideVendorId = null;
105     }
106 
107     class TestDhcpPacket extends DhcpPacket {
108         private byte mType;
109         // TODO: Make this a map of option numbers to bytes instead.
110         private byte[] mDomainBytes, mVendorInfoBytes, mLeaseTimeBytes, mNetmaskBytes;
111 
TestDhcpPacket(byte type, Inet4Address clientIp, Inet4Address yourIp)112         public TestDhcpPacket(byte type, Inet4Address clientIp, Inet4Address yourIp) {
113             super(0xdeadbeef, (short) 0, clientIp, yourIp, INADDR_ANY, INADDR_ANY,
114                   CLIENT_MAC, true);
115             mType = type;
116         }
117 
TestDhcpPacket(byte type)118         public TestDhcpPacket(byte type) {
119             this(type, INADDR_ANY, CLIENT_ADDR);
120         }
121 
setDomainBytes(byte[] domainBytes)122         public TestDhcpPacket setDomainBytes(byte[] domainBytes) {
123             mDomainBytes = domainBytes;
124             return this;
125         }
126 
setVendorInfoBytes(byte[] vendorInfoBytes)127         public TestDhcpPacket setVendorInfoBytes(byte[] vendorInfoBytes) {
128             mVendorInfoBytes = vendorInfoBytes;
129             return this;
130         }
131 
setLeaseTimeBytes(byte[] leaseTimeBytes)132         public TestDhcpPacket setLeaseTimeBytes(byte[] leaseTimeBytes) {
133             mLeaseTimeBytes = leaseTimeBytes;
134             return this;
135         }
136 
setNetmaskBytes(byte[] netmaskBytes)137         public TestDhcpPacket setNetmaskBytes(byte[] netmaskBytes) {
138             mNetmaskBytes = netmaskBytes;
139             return this;
140         }
141 
buildPacket(int encap, short unusedDestUdp, short unusedSrcUdp)142         public ByteBuffer buildPacket(int encap, short unusedDestUdp, short unusedSrcUdp) {
143             ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
144             fillInPacket(encap, CLIENT_ADDR, SERVER_ADDR,
145                          DHCP_CLIENT, DHCP_SERVER, result, DHCP_BOOTREPLY, false);
146             return result;
147         }
148 
finishPacket(ByteBuffer buffer)149         public void finishPacket(ByteBuffer buffer) {
150             addTlv(buffer, DHCP_MESSAGE_TYPE, mType);
151             if (mDomainBytes != null) {
152                 addTlv(buffer, DHCP_DOMAIN_NAME, mDomainBytes);
153             }
154             if (mVendorInfoBytes != null) {
155                 addTlv(buffer, DHCP_VENDOR_INFO, mVendorInfoBytes);
156             }
157             if (mLeaseTimeBytes != null) {
158                 addTlv(buffer, DHCP_LEASE_TIME, mLeaseTimeBytes);
159             }
160             if (mNetmaskBytes != null) {
161                 addTlv(buffer, DHCP_SUBNET_MASK, mNetmaskBytes);
162             }
163             addTlvEnd(buffer);
164         }
165 
166         // Convenience method.
build()167         public ByteBuffer build() {
168             // ENCAP_BOOTP packets don't contain ports, so just pass in 0.
169             ByteBuffer pkt = buildPacket(ENCAP_BOOTP, (short) 0, (short) 0);
170             pkt.flip();
171             return pkt;
172         }
173     }
174 
assertDomainAndVendorInfoParses( String expectedDomain, byte[] domainBytes, String expectedVendorInfo, byte[] vendorInfoBytes)175     private void assertDomainAndVendorInfoParses(
176             String expectedDomain, byte[] domainBytes,
177             String expectedVendorInfo, byte[] vendorInfoBytes) throws Exception {
178         ByteBuffer packet = new TestDhcpPacket(DHCP_MESSAGE_TYPE_OFFER)
179                 .setDomainBytes(domainBytes)
180                 .setVendorInfoBytes(vendorInfoBytes)
181                 .build();
182         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP,
183                 TEST_EMPTY_OPTIONS_SKIP_LIST);
184         assertEquals(expectedDomain, offerPacket.mDomainName);
185         assertEquals(expectedVendorInfo, offerPacket.mVendorInfo);
186     }
187 
188     @Test
testDomainName()189     public void testDomainName() throws Exception {
190         byte[] nullByte = new byte[] { 0x00 };
191         byte[] twoNullBytes = new byte[] { 0x00, 0x00 };
192         byte[] nonNullDomain = new byte[] {
193             (byte) 'g', (byte) 'o', (byte) 'o', (byte) '.', (byte) 'g', (byte) 'l'
194         };
195         byte[] trailingNullDomain = new byte[] {
196             (byte) 'g', (byte) 'o', (byte) 'o', (byte) '.', (byte) 'g', (byte) 'l', 0x00
197         };
198         byte[] embeddedNullsDomain = new byte[] {
199             (byte) 'g', (byte) 'o', (byte) 'o', 0x00, 0x00, (byte) 'g', (byte) 'l'
200         };
201         byte[] metered = "ANDROID_METERED".getBytes("US-ASCII");
202 
203         byte[] meteredEmbeddedNull = metered.clone();
204         meteredEmbeddedNull[7] = (char) 0;
205 
206         byte[] meteredTrailingNull = metered.clone();
207         meteredTrailingNull[meteredTrailingNull.length - 1] = (char) 0;
208 
209         assertDomainAndVendorInfoParses("", nullByte, "\u0000", nullByte);
210         assertDomainAndVendorInfoParses("", twoNullBytes, "\u0000\u0000", twoNullBytes);
211         assertDomainAndVendorInfoParses("goo.gl", nonNullDomain, "ANDROID_METERED", metered);
212         assertDomainAndVendorInfoParses("goo", embeddedNullsDomain,
213                                         "ANDROID\u0000METERED", meteredEmbeddedNull);
214         assertDomainAndVendorInfoParses("goo.gl", trailingNullDomain,
215                                         "ANDROID_METERE\u0000", meteredTrailingNull);
216     }
217 
assertLeaseTimeParses(boolean expectValid, Integer rawLeaseTime, long leaseTimeMillis, byte[] leaseTimeBytes)218     private void assertLeaseTimeParses(boolean expectValid, Integer rawLeaseTime,
219             long leaseTimeMillis, byte[] leaseTimeBytes) throws Exception {
220         TestDhcpPacket testPacket = new TestDhcpPacket(DHCP_MESSAGE_TYPE_OFFER);
221         if (leaseTimeBytes != null) {
222             testPacket.setLeaseTimeBytes(leaseTimeBytes);
223         }
224         ByteBuffer packet = testPacket.build();
225         DhcpPacket offerPacket = null;
226 
227         if (!expectValid) {
228             try {
229                 offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP,
230                         TEST_EMPTY_OPTIONS_SKIP_LIST);
231                 fail("Invalid packet parsed successfully: " + offerPacket);
232             } catch (ParseException expected) {
233             }
234             return;
235         }
236 
237         offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP,
238                 TEST_EMPTY_OPTIONS_SKIP_LIST);
239         assertNotNull(offerPacket);
240         assertEquals(rawLeaseTime, offerPacket.mLeaseTime);
241         DhcpResults dhcpResults = offerPacket.toDhcpResults();  // Just check this doesn't crash.
242         assertEquals(leaseTimeMillis,
243                 offerPacket.getLeaseTimeMillis(DhcpPacket.DEFAULT_MINIMUM_LEASE));
244     }
245 
246     @Test
testLeaseTime()247     public void testLeaseTime() throws Exception {
248         byte[] noLease = null;
249         byte[] tooShortLease = new byte[] { 0x00, 0x00 };
250         byte[] tooLongLease = new byte[] { 0x00, 0x00, 0x00, 60, 0x01 };
251         byte[] zeroLease = new byte[] { 0x00, 0x00, 0x00, 0x00 };
252         byte[] tenSecondLease = new byte[] { 0x00, 0x00, 0x00, 10 };
253         byte[] oneMinuteLease = new byte[] { 0x00, 0x00, 0x00, 60 };
254         byte[] fiveMinuteLease = new byte[] { 0x00, 0x00, 0x01, 0x2c };
255         byte[] oneDayLease = new byte[] { 0x00, 0x01, 0x51, (byte) 0x80 };
256         byte[] maxIntPlusOneLease = new byte[] { (byte) 0x80, 0x00, 0x00, 0x01 };
257         byte[] infiniteLease = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
258 
259         assertLeaseTimeParses(true, null, 0, noLease);
260         assertLeaseTimeParses(false, null, 0, tooShortLease);
261         assertLeaseTimeParses(false, null, 0, tooLongLease);
262         assertLeaseTimeParses(true, 0, 60 * 1000, zeroLease);
263         assertLeaseTimeParses(true, 10, 60 * 1000, tenSecondLease);
264         assertLeaseTimeParses(true, 60, 60 * 1000, oneMinuteLease);
265         assertLeaseTimeParses(true, 300, 300 * 1000, fiveMinuteLease);
266         assertLeaseTimeParses(true, 86400, 86400 * 1000, oneDayLease);
267         assertLeaseTimeParses(true, -2147483647, 2147483649L * 1000, maxIntPlusOneLease);
268         assertLeaseTimeParses(true, DhcpPacket.INFINITE_LEASE, 0, infiniteLease);
269     }
270 
checkIpAddress(String expected, Inet4Address clientIp, Inet4Address yourIp, byte[] netmaskBytes)271     private void checkIpAddress(String expected, Inet4Address clientIp, Inet4Address yourIp,
272                                 byte[] netmaskBytes) throws Exception {
273         checkIpAddress(expected, DHCP_MESSAGE_TYPE_OFFER, clientIp, yourIp, netmaskBytes);
274         checkIpAddress(expected, DHCP_MESSAGE_TYPE_ACK, clientIp, yourIp, netmaskBytes);
275     }
276 
checkIpAddress(String expected, byte type, Inet4Address clientIp, Inet4Address yourIp, byte[] netmaskBytes)277     private void checkIpAddress(String expected, byte type,
278                                 Inet4Address clientIp, Inet4Address yourIp,
279                                 byte[] netmaskBytes) throws Exception {
280         ByteBuffer packet = new TestDhcpPacket(type, clientIp, yourIp)
281                 .setNetmaskBytes(netmaskBytes)
282                 .build();
283         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP,
284                 TEST_EMPTY_OPTIONS_SKIP_LIST);
285         DhcpResults results = offerPacket.toDhcpResults();
286 
287         if (expected != null) {
288             LinkAddress expectedAddress = new LinkAddress(expected);
289             assertEquals(expectedAddress, results.ipAddress);
290         } else {
291             assertNull(results);
292         }
293     }
294 
295     @Test
testIpAddress()296     public void testIpAddress() throws Exception {
297         byte[] slash11Netmask = new byte[] { (byte) 0xff, (byte) 0xe0, 0x00, 0x00 };
298         byte[] slash24Netmask = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00 };
299         byte[] invalidNetmask = new byte[] { (byte) 0xff, (byte) 0xfb, (byte) 0xff, 0x00 };
300         Inet4Address example1 = v4Address("192.0.2.1");
301         Inet4Address example2 = v4Address("192.0.2.43");
302 
303         // A packet without any addresses is not valid.
304         checkIpAddress(null, ANY, ANY, slash24Netmask);
305 
306         // ClientIP is used iff YourIP is not present.
307         checkIpAddress("192.0.2.1/24", example2, example1, slash24Netmask);
308         checkIpAddress("192.0.2.43/11", example2, ANY, slash11Netmask);
309         checkIpAddress("192.0.2.43/11", ANY, example2, slash11Netmask);
310 
311         // Invalid netmasks are ignored.
312         checkIpAddress(null, example2, ANY, invalidNetmask);
313 
314         // If there is no netmask, implicit netmasks are used.
315         checkIpAddress("192.0.2.43/24", ANY, example2, null);
316     }
317 
assertDhcpResults(String ipAddress, String gateway, String dnsServersString, String domains, String serverAddress, String serverHostName, String vendorInfo, int leaseDuration, boolean hasMeteredHint, int mtu, DhcpResults dhcpResults)318     private void assertDhcpResults(String ipAddress, String gateway, String dnsServersString,
319             String domains, String serverAddress, String serverHostName, String vendorInfo,
320             int leaseDuration, boolean hasMeteredHint, int mtu, DhcpResults dhcpResults)
321                     throws Exception {
322         assertEquals(new LinkAddress(ipAddress), dhcpResults.ipAddress);
323         assertEquals(v4Address(gateway), dhcpResults.gateway);
324 
325         String[] dnsServerStrings = dnsServersString.split(",");
326         ArrayList dnsServers = new ArrayList();
327         for (String dnsServerString : dnsServerStrings) {
328             dnsServers.add(v4Address(dnsServerString));
329         }
330         assertEquals(dnsServers, dhcpResults.dnsServers);
331 
332         assertEquals(domains, dhcpResults.domains);
333         assertEquals(v4Address(serverAddress), dhcpResults.serverAddress);
334         assertEquals(serverHostName, dhcpResults.serverHostName);
335         assertEquals(vendorInfo, dhcpResults.vendorInfo);
336         assertEquals(leaseDuration, dhcpResults.leaseDuration);
337         assertEquals(hasMeteredHint, dhcpResults.hasMeteredHint());
338         assertEquals(mtu, dhcpResults.mtu);
339     }
340 
341     @Test
testOffer1()342     public void testOffer1() throws Exception {
343         // TODO: Turn all of these into golden files. This will probably require using
344         // androidx.test.InstrumentationRegistry for obtaining a Context object
345         // to read such golden files, along with an appropriate Android.mk.
346         // CHECKSTYLE:OFF Generated code
347         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
348             // IP header.
349             "451001480000000080118849c0a89003c0a89ff7" +
350             // UDP header.
351             "004300440134dcfa" +
352             // BOOTP header.
353             "02010600c997a63b0000000000000000c0a89ff70000000000000000" +
354             // MAC address.
355             "30766ff2a90c00000000000000000000" +
356             // Server name.
357             "0000000000000000000000000000000000000000000000000000000000000000" +
358             "0000000000000000000000000000000000000000000000000000000000000000" +
359             // File.
360             "0000000000000000000000000000000000000000000000000000000000000000" +
361             "0000000000000000000000000000000000000000000000000000000000000000" +
362             "0000000000000000000000000000000000000000000000000000000000000000" +
363             "0000000000000000000000000000000000000000000000000000000000000000" +
364             // Options
365             "638253633501023604c0a89003330400001c200104fffff0000304c0a89ffe06080808080808080404" +
366             "3a0400000e103b040000189cff00000000000000000000"));
367         // CHECKSTYLE:ON Generated code
368 
369         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
370                 TEST_EMPTY_OPTIONS_SKIP_LIST);
371         assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
372         DhcpResults dhcpResults = offerPacket.toDhcpResults();
373         assertDhcpResults("192.168.159.247/20", "192.168.159.254", "8.8.8.8,8.8.4.4",
374                 null, "192.168.144.3", "", null, 7200, false, 0, dhcpResults);
375     }
376 
377     @Test
testOffer2()378     public void testOffer2() throws Exception {
379         // CHECKSTYLE:OFF Generated code
380         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
381             // IP header.
382             "450001518d0600004011144dc0a82b01c0a82bf7" +
383             // UDP header.
384             "00430044013d9ac7" +
385             // BOOTP header.
386             "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
387             // MAC address.
388             "30766ff2a90c00000000000000000000" +
389             // Server name ("dhcp.android.com" plus invalid "AAAA" after null terminator).
390             "646863702e616e64726f69642e636f6d00000000000000000000000000000000" +
391             "0000000000004141414100000000000000000000000000000000000000000000" +
392             // File.
393             "0000000000000000000000000000000000000000000000000000000000000000" +
394             "0000000000000000000000000000000000000000000000000000000000000000" +
395             "0000000000000000000000000000000000000000000000000000000000000000" +
396             "0000000000000000000000000000000000000000000000000000000000000000" +
397             // Options
398             "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
399             "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff"));
400         // CHECKSTYLE:ON Generated code
401 
402         assertEquals(337, packet.limit());
403         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
404                 TEST_EMPTY_OPTIONS_SKIP_LIST);
405         assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
406         DhcpResults dhcpResults = offerPacket.toDhcpResults();
407         assertDhcpResults("192.168.43.247/24", "192.168.43.1", "192.168.43.1",
408                 null, "192.168.43.1", "dhcp.android.com", "ANDROID_METERED", 3600, true, 0,
409                 dhcpResults);
410         assertTrue(dhcpResults.hasMeteredHint());
411     }
412 
runCapportOptionTest(boolean enabled)413     private void runCapportOptionTest(boolean enabled) throws Exception {
414         // CHECKSTYLE:OFF Generated code
415         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
416                 // IP header.
417                 "450001518d0600004011144dc0a82b01c0a82bf7" +
418                 // UDP header
419                 "00430044013d9ac7" +
420                 // BOOTP header
421                 "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
422                 // MAC address.
423                 "30766ff2a90c00000000000000000000" +
424                 // Server name ("dhcp.android.com" plus invalid "AAAA" after null terminator).
425                 "646863702e616e64726f69642e636f6d00000000000000000000000000000000" +
426                 "0000000000004141414100000000000000000000000000000000000000000000" +
427                 // File.
428                 "0000000000000000000000000000000000000000000000000000000000000000" +
429                 "0000000000000000000000000000000000000000000000000000000000000000" +
430                 "0000000000000000000000000000000000000000000000000000000000000000" +
431                 "0000000000000000000000000000000000000000000000000000000000000000" +
432                 // Options
433                 "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
434                 "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544721d" +
435                 "68747470733a2f2f706f7274616c6170692e6578616d706c652e636f6dff"));
436         // CHECKSTYLE:ON Generated code
437 
438         final DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
439                 enabled ? TEST_EMPTY_OPTIONS_SKIP_LIST
440                         : new byte[] { DhcpPacket.DHCP_CAPTIVE_PORTAL });
441         assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
442         final DhcpResults dhcpResults = offerPacket.toDhcpResults();
443         final String testUrl = enabled ? "https://portalapi.example.com" : null;
444         assertEquals(testUrl, dhcpResults.captivePortalApiUrl);
445     }
446 
447     @Test
testCapportOption()448     public void testCapportOption() throws Exception {
449         runCapportOptionTest(true /* enabled */);
450     }
451 
452     @Test
testCapportOption_Disabled()453     public void testCapportOption_Disabled() throws Exception {
454         runCapportOptionTest(false /* enabled */);
455     }
456 
457     @Test
testCapportOption_Invalid()458     public void testCapportOption_Invalid() throws Exception {
459         // CHECKSTYLE:OFF Generated code
460         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
461                 // IP header.
462                 "450001518d0600004011144dc0a82b01c0a82bf7" +
463                 // UDP header
464                 "00430044013d9ac7" +
465                 // BOOTP header
466                 "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
467                 // MAC address.
468                 "30766ff2a90c00000000000000000000" +
469                 // Server name ("dhcp.android.com" plus invalid "AAAA" after null terminator).
470                 "646863702e616e64726f69642e636f6d00000000000000000000000000000000" +
471                 "0000000000004141414100000000000000000000000000000000000000000000" +
472                 // File.
473                 "0000000000000000000000000000000000000000000000000000000000000000" +
474                 "0000000000000000000000000000000000000000000000000000000000000000" +
475                 "0000000000000000000000000000000000000000000000000000000000000000" +
476                 "0000000000000000000000000000000000000000000000000000000000000000" +
477                 // Options
478                 "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
479                 "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544" +
480                 // Option 114 (0x72, capport), length 10 (0x0a)
481                 "720a" +
482                 // バグ-com in UTF-8, plus the ff byte that marks the end of options.
483                 "e38390e382b02d636f6dff"));
484         // CHECKSTYLE:ON Generated code
485 
486         final DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
487                 TEST_EMPTY_OPTIONS_SKIP_LIST);
488         assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
489         final DhcpResults dhcpResults = offerPacket.toDhcpResults();
490         // Output URL will be garbled because some characters do not exist in the target charset,
491         // but the parser should not crash.
492         assertTrue(dhcpResults.captivePortalApiUrl.length() > 0);
493     }
494 
runIPv6OnlyPreferredOption(boolean enabled)495     private void runIPv6OnlyPreferredOption(boolean enabled) throws Exception {
496         // CHECKSTYLE:OFF Generated code
497         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
498                 // IP header.
499                 "45100158000040004011B5CEC0A80164C0A80102" +
500                 // UDP header
501                 "004300440144CE63" +
502                 // BOOTP header
503                 "02010600B8BF41E60000000000000000C0A80102C0A8016400000000" +
504                 // MAC address.
505                 "22B3614EE01200000000000000000000" +
506                 // Server name and padding.
507                 "0000000000000000000000000000000000000000000000000000000000000000" +
508                 "0000000000000000000000000000000000000000000000000000000000000000" +
509                 // File.
510                 "0000000000000000000000000000000000000000000000000000000000000000" +
511                 "0000000000000000000000000000000000000000000000000000000000000000" +
512                 "0000000000000000000000000000000000000000000000000000000000000000" +
513                 "0000000000000000000000000000000000000000000000000000000000000000" +
514                 // Options
515                 "638253633501023604C0A80164330400000E103A04000007083B0400000C4E01" +
516                 "04FFFFFF001C04C0A801FF0304C0A801640604C0A801640C0C74657374686F73" +
517                 "746E616D651A0205DC" +
518                 // Option 108 (0x6c, IPv6-Only preferred option), length 4 (0x04), 1800s
519                 "6C0400000708" +
520                 // End of options.
521                 "FF"));
522         // CHECKSTYLE:ON Generated code
523 
524         final DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
525                 enabled ? TEST_EMPTY_OPTIONS_SKIP_LIST
526                         : new byte[] { DhcpPacket.DHCP_IPV6_ONLY_PREFERRED });
527         assertTrue(offerPacket instanceof DhcpOfferPacket);
528         assertEquals(offerPacket.mIpv6OnlyWaitTime,
529                 enabled ? new Integer(TEST_IPV6_ONLY_WAIT_S) : null);
530     }
531 
532     @Test
testIPv6OnlyPreferredOption()533     public void testIPv6OnlyPreferredOption() throws Exception {
534         runIPv6OnlyPreferredOption(true /* enabled */);
535     }
536 
537     @Test
testIPv6OnlyPreferredOption_Disable()538     public void testIPv6OnlyPreferredOption_Disable() throws Exception {
539         runIPv6OnlyPreferredOption(false /* enabled */);
540     }
541 
542     @Test
testBadIpPacket()543     public void testBadIpPacket() throws Exception {
544         final byte[] packet = HexDump.hexStringToByteArray(
545             // IP header.
546             "450001518d0600004011144dc0a82b01c0a82bf7");
547 
548         try {
549             DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3,
550                     TEST_EMPTY_OPTIONS_SKIP_LIST);
551         } catch (DhcpPacket.ParseException expected) {
552             assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode);
553             return;
554         }
555         fail("Dhcp packet parsing should have failed");
556     }
557 
558     @Test
testBadDhcpPacket()559     public void testBadDhcpPacket() throws Exception {
560         final byte[] packet = HexDump.hexStringToByteArray(
561             // IP header.
562             "450001518d0600004011144dc0a82b01c0a82bf7" +
563             // UDP header.
564             "00430044013d9ac7" +
565             // BOOTP header.
566             "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000");
567 
568         try {
569             DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
570         } catch (DhcpPacket.ParseException expected) {
571             assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode);
572             return;
573         }
574         fail("Dhcp packet parsing should have failed");
575     }
576 
577     @Test
testBadTruncatedOffer()578     public void testBadTruncatedOffer() throws Exception {
579         final byte[] packet = HexDump.hexStringToByteArray(
580             // IP header.
581             "450001518d0600004011144dc0a82b01c0a82bf7" +
582             // UDP header.
583             "00430044013d9ac7" +
584             // BOOTP header.
585             "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
586             // MAC address.
587             "30766ff2a90c00000000000000000000" +
588             // Server name.
589             "0000000000000000000000000000000000000000000000000000000000000000" +
590             "0000000000000000000000000000000000000000000000000000000000000000" +
591             // File, missing one byte
592             "0000000000000000000000000000000000000000000000000000000000000000" +
593             "0000000000000000000000000000000000000000000000000000000000000000" +
594             "0000000000000000000000000000000000000000000000000000000000000000" +
595             "00000000000000000000000000000000000000000000000000000000000000");
596 
597         try {
598             DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
599         } catch (DhcpPacket.ParseException expected) {
600             assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode);
601             return;
602         }
603         fail("Dhcp packet parsing should have failed");
604     }
605 
606     @Test
testBadOfferWithoutACookie()607     public void testBadOfferWithoutACookie() throws Exception {
608         final byte[] packet = HexDump.hexStringToByteArray(
609             // IP header.
610             "450001518d0600004011144dc0a82b01c0a82bf7" +
611             // UDP header.
612             "00430044013d9ac7" +
613             // BOOTP header.
614             "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
615             // MAC address.
616             "30766ff2a90c00000000000000000000" +
617             // Server name.
618             "0000000000000000000000000000000000000000000000000000000000000000" +
619             "0000000000000000000000000000000000000000000000000000000000000000" +
620             // File.
621             "0000000000000000000000000000000000000000000000000000000000000000" +
622             "0000000000000000000000000000000000000000000000000000000000000000" +
623             "0000000000000000000000000000000000000000000000000000000000000000" +
624             "0000000000000000000000000000000000000000000000000000000000000000"
625             // No options
626             );
627 
628         try {
629             DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
630         } catch (DhcpPacket.ParseException expected) {
631             assertDhcpErrorCodes(DhcpErrorEvent.DHCP_NO_COOKIE, expected.errorCode);
632             return;
633         }
634         fail("Dhcp packet parsing should have failed");
635     }
636 
637     @Test
testOfferWithBadCookie()638     public void testOfferWithBadCookie() throws Exception {
639         final byte[] packet = HexDump.hexStringToByteArray(
640             // IP header.
641             "450001518d0600004011144dc0a82b01c0a82bf7" +
642             // UDP header.
643             "00430044013d9ac7" +
644             // BOOTP header.
645             "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
646             // MAC address.
647             "30766ff2a90c00000000000000000000" +
648             // Server name.
649             "0000000000000000000000000000000000000000000000000000000000000000" +
650             "0000000000000000000000000000000000000000000000000000000000000000" +
651             // File.
652             "0000000000000000000000000000000000000000000000000000000000000000" +
653             "0000000000000000000000000000000000000000000000000000000000000000" +
654             "0000000000000000000000000000000000000000000000000000000000000000" +
655             "0000000000000000000000000000000000000000000000000000000000000000" +
656             // Bad cookie
657             "DEADBEEF3501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
658             "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff");
659 
660         try {
661             DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
662         } catch (DhcpPacket.ParseException expected) {
663             assertDhcpErrorCodes(DhcpErrorEvent.DHCP_BAD_MAGIC_COOKIE, expected.errorCode);
664             return;
665         }
666         fail("Dhcp packet parsing should have failed");
667     }
668 
assertDhcpErrorCodes(int expected, int got)669     private void assertDhcpErrorCodes(int expected, int got) {
670         assertEquals(Integer.toHexString(expected), Integer.toHexString(got));
671     }
672 
673     @Test
testTruncatedOfferPackets()674     public void testTruncatedOfferPackets() throws Exception {
675         final byte[] packet = HexDump.hexStringToByteArray(
676             // IP header.
677             "450001518d0600004011144dc0a82b01c0a82bf7" +
678             // UDP header.
679             "00430044013d9ac7" +
680             // BOOTP header.
681             "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
682             // MAC address.
683             "30766ff2a90c00000000000000000000" +
684             // Server name.
685             "0000000000000000000000000000000000000000000000000000000000000000" +
686             "0000000000000000000000000000000000000000000000000000000000000000" +
687             // File.
688             "0000000000000000000000000000000000000000000000000000000000000000" +
689             "0000000000000000000000000000000000000000000000000000000000000000" +
690             "0000000000000000000000000000000000000000000000000000000000000000" +
691             "0000000000000000000000000000000000000000000000000000000000000000" +
692             // Options
693             "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
694             "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff");
695 
696         for (int len = 0; len < packet.length; len++) {
697             try {
698                 DhcpPacket.decodeFullPacket(packet, len, ENCAP_L3);
699             } catch (ParseException e) {
700                 if (e.errorCode == DhcpErrorEvent.PARSING_ERROR) {
701                     fail(String.format("bad truncated packet of length %d", len));
702                 }
703             }
704         }
705     }
706 
707     @Test
testRandomPackets()708     public void testRandomPackets() throws Exception {
709         final int maxRandomPacketSize = 512;
710         final Random r = new Random();
711         for (int i = 0; i < 10000; i++) {
712             byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
713             r.nextBytes(packet);
714             try {
715                 DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
716             } catch (ParseException e) {
717                 if (e.errorCode == DhcpErrorEvent.PARSING_ERROR) {
718                     fail("bad packet: " + HexDump.toHexString(packet));
719                 }
720             }
721         }
722     }
723 
mtuBytes(int mtu)724     private byte[] mtuBytes(int mtu) {
725         // 0x1a02: option 26, length 2. 0xff: no more options.
726         if (mtu > Short.MAX_VALUE - Short.MIN_VALUE) {
727             throw new IllegalArgumentException(
728                 String.format("Invalid MTU %d, must be 16-bit unsigned", mtu));
729         }
730         String hexString = String.format("1a02%04xff", mtu);
731         return HexDump.hexStringToByteArray(hexString);
732     }
733 
checkMtu(ByteBuffer packet, int expectedMtu, byte[] mtuBytes)734     private void checkMtu(ByteBuffer packet, int expectedMtu, byte[] mtuBytes) throws Exception {
735         if (mtuBytes != null) {
736             packet.position(packet.capacity() - mtuBytes.length);
737             packet.put(mtuBytes);
738             packet.clear();
739         }
740         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
741                 TEST_EMPTY_OPTIONS_SKIP_LIST);
742         assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
743         DhcpResults dhcpResults = offerPacket.toDhcpResults();
744         assertDhcpResults("192.168.159.247/20", "192.168.159.254", "8.8.8.8,8.8.4.4",
745                 null, "192.168.144.3", "", null, 7200, false, expectedMtu, dhcpResults);
746     }
747 
748     @Test
testMtu()749     public void testMtu() throws Exception {
750         // CHECKSTYLE:OFF Generated code
751         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
752             // IP header.
753             "451001480000000080118849c0a89003c0a89ff7" +
754             // UDP header.
755             "004300440134dcfa" +
756             // BOOTP header.
757             "02010600c997a63b0000000000000000c0a89ff70000000000000000" +
758             // MAC address.
759             "30766ff2a90c00000000000000000000" +
760             // Server name.
761             "0000000000000000000000000000000000000000000000000000000000000000" +
762             "0000000000000000000000000000000000000000000000000000000000000000" +
763             // File.
764             "0000000000000000000000000000000000000000000000000000000000000000" +
765             "0000000000000000000000000000000000000000000000000000000000000000" +
766             "0000000000000000000000000000000000000000000000000000000000000000" +
767             "0000000000000000000000000000000000000000000000000000000000000000" +
768             // Options
769             "638253633501023604c0a89003330400001c200104fffff0000304c0a89ffe06080808080808080404" +
770             "3a0400000e103b040000189cff00000000"));
771         // CHECKSTYLE:ON Generated code
772 
773         checkMtu(packet, 0, null);
774         checkMtu(packet, 0, mtuBytes(1501));
775         checkMtu(packet, 1500, mtuBytes(1500));
776         checkMtu(packet, 1499, mtuBytes(1499));
777         checkMtu(packet, 1280, mtuBytes(1280));
778         checkMtu(packet, 0, mtuBytes(1279));
779         checkMtu(packet, 0, mtuBytes(576));
780         checkMtu(packet, 0, mtuBytes(68));
781         checkMtu(packet, 0, mtuBytes(Short.MIN_VALUE));
782         checkMtu(packet, 0, mtuBytes(Short.MAX_VALUE + 3));
783         checkMtu(packet, 0, mtuBytes(-1));
784     }
785 
786     @Test
testExplicitClientId()787     public void testExplicitClientId() throws Exception {
788         final byte[] clientId = new byte[] {
789                 0x01 /* CLIENT_ID_ETH */, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 };
790 
791         // CHECKSTYLE:OFF Generated code
792         final byte[] packet = HexDump.hexStringToByteArray(
793                 // IP header.
794                 "450001518d0600004011144dc0a82b01c0a82bf7" +
795                 // UDP header
796                 "00430044013d9ac7" +
797                 // BOOTP header
798                 "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
799                 // MAC address.
800                 "30766ff2a90c00000000000000000000" +
801                 // Server name ("dhcp.android.com" plus invalid "AAAA" after null terminator).
802                 "646863702e616e64726f69642e636f6d00000000000000000000000000000000" +
803                 "0000000000004141414100000000000000000000000000000000000000000000" +
804                 // File.
805                 "0000000000000000000000000000000000000000000000000000000000000000" +
806                 "0000000000000000000000000000000000000000000000000000000000000000" +
807                 "0000000000000000000000000000000000000000000000000000000000000000" +
808                 "0000000000000000000000000000000000000000000000000000000000000000" +
809                 // Options
810                 "638253633501013d0701010203040506390205dc3c0e616e64726f69642d6468" +
811                 "63702d52370a0103060f1a1c333a3b2bff00");
812         // CHECKSTYLE:ON Generated code
813 
814         final DhcpPacket discoverPacket = DhcpPacket.decodeFullPacket(packet,
815                 packet.length, ENCAP_L3);
816         assertTrue(discoverPacket instanceof DhcpDiscoverPacket);
817         assertTrue(discoverPacket.hasExplicitClientId());
818         assertTrue(Arrays.equals(discoverPacket.mClientId, clientId));
819     }
820 
821     @Test
testBadHwaddrLength()822     public void testBadHwaddrLength() throws Exception {
823         // CHECKSTYLE:OFF Generated code
824         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
825             // IP header.
826             "450001518d0600004011144dc0a82b01c0a82bf7" +
827             // UDP header.
828             "00430044013d9ac7" +
829             // BOOTP header.
830             "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
831             // MAC address.
832             "30766ff2a90c00000000000000000000" +
833             // Server name.
834             "0000000000000000000000000000000000000000000000000000000000000000" +
835             "0000000000000000000000000000000000000000000000000000000000000000" +
836             // File.
837             "0000000000000000000000000000000000000000000000000000000000000000" +
838             "0000000000000000000000000000000000000000000000000000000000000000" +
839             "0000000000000000000000000000000000000000000000000000000000000000" +
840             "0000000000000000000000000000000000000000000000000000000000000000" +
841             // Options
842             "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
843             "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff"));
844         // CHECKSTYLE:ON Generated code
845         String expectedClientMac = "30766FF2A90C";
846 
847         final int hwAddrLenOffset = 20 + 8 + 2;
848         assertEquals(6, packet.get(hwAddrLenOffset));
849 
850         // Expect the expected.
851         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
852                 TEST_EMPTY_OPTIONS_SKIP_LIST);
853         assertNotNull(offerPacket);
854         assertEquals(6, offerPacket.getClientMac().length);
855         assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac()));
856 
857         // Reduce the hardware address length and verify that it shortens the client MAC.
858         packet.flip();
859         packet.put(hwAddrLenOffset, (byte) 5);
860         offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3, TEST_EMPTY_OPTIONS_SKIP_LIST);
861         assertNotNull(offerPacket);
862         assertEquals(5, offerPacket.getClientMac().length);
863         assertEquals(expectedClientMac.substring(0, 10),
864                 HexDump.toHexString(offerPacket.getClientMac()));
865 
866         packet.flip();
867         packet.put(hwAddrLenOffset, (byte) 3);
868         offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3, TEST_EMPTY_OPTIONS_SKIP_LIST);
869         assertNotNull(offerPacket);
870         assertEquals(3, offerPacket.getClientMac().length);
871         assertEquals(expectedClientMac.substring(0, 6),
872                 HexDump.toHexString(offerPacket.getClientMac()));
873 
874         // Set the the hardware address length to 0xff and verify that we a) don't treat it as -1
875         // and crash, and b) hardcode it to 6.
876         packet.flip();
877         packet.put(hwAddrLenOffset, (byte) -1);
878         offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3, TEST_EMPTY_OPTIONS_SKIP_LIST);
879         assertNotNull(offerPacket);
880         assertEquals(6, offerPacket.getClientMac().length);
881         assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac()));
882 
883         // Set the the hardware address length to a positive invalid value (> 16) and verify that we
884         // hardcode it to 6.
885         packet.flip();
886         packet.put(hwAddrLenOffset, (byte) 17);
887         offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3, TEST_EMPTY_OPTIONS_SKIP_LIST);
888         assertNotNull(offerPacket);
889         assertEquals(6, offerPacket.getClientMac().length);
890         assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac()));
891     }
892 
893     @Test
testPadAndOverloadedOptionsOffer()894     public void testPadAndOverloadedOptionsOffer() throws Exception {
895         // A packet observed in the real world that is interesting for two reasons:
896         //
897         // 1. It uses pad bytes, which we previously didn't support correctly.
898         // 2. It uses DHCP option overloading, which we don't currently support (but it doesn't
899         //    store any information in the overloaded fields).
900         //
901         // For now, we just check that it parses correctly.
902         // CHECKSTYLE:OFF Generated code
903         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
904             // Ethernet header.
905             "b4cef6000000e80462236e300800" +
906             // IP header.
907             "4500014c00000000ff11741701010101ac119876" +
908             // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
909             "004300440138ae5a" +
910             // BOOTP header.
911             "020106000fa0059f0000000000000000ac1198760000000000000000" +
912             // MAC address.
913             "b4cef600000000000000000000000000" +
914             // Server name.
915             "ff00000000000000000000000000000000000000000000000000000000000000" +
916             "0000000000000000000000000000000000000000000000000000000000000000" +
917             // File.
918             "ff00000000000000000000000000000000000000000000000000000000000000" +
919             "0000000000000000000000000000000000000000000000000000000000000000" +
920             "0000000000000000000000000000000000000000000000000000000000000000" +
921             "0000000000000000000000000000000000000000000000000000000000000000" +
922             // Options
923             "638253633501023604010101010104ffff000033040000a8c03401030304ac1101010604ac110101" +
924             "0000000000000000000000000000000000000000000000ff000000"));
925         // CHECKSTYLE:ON Generated code
926 
927         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2,
928                 TEST_EMPTY_OPTIONS_SKIP_LIST);
929         assertTrue(offerPacket instanceof DhcpOfferPacket);
930         DhcpResults dhcpResults = offerPacket.toDhcpResults();
931         assertDhcpResults("172.17.152.118/16", "172.17.1.1", "172.17.1.1",
932                 null, "1.1.1.1", "", null, 43200, false, 0, dhcpResults);
933     }
934 
935     @Test
testBug2111()936     public void testBug2111() throws Exception {
937         // CHECKSTYLE:OFF Generated code
938         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
939             // IP header.
940             "4500014c00000000ff119beac3eaf3880a3f5d04" +
941             // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
942             "0043004401387464" +
943             // BOOTP header.
944             "0201060002554812000a0000000000000a3f5d040000000000000000" +
945             // MAC address.
946             "00904c00000000000000000000000000" +
947             // Server name.
948             "0000000000000000000000000000000000000000000000000000000000000000" +
949             "0000000000000000000000000000000000000000000000000000000000000000" +
950             // File.
951             "0000000000000000000000000000000000000000000000000000000000000000" +
952             "0000000000000000000000000000000000000000000000000000000000000000" +
953             "0000000000000000000000000000000000000000000000000000000000000000" +
954             "0000000000000000000000000000000000000000000000000000000000000000" +
955             // Options.
956             "638253633501023604c00002fe33040000bfc60104fffff00003040a3f50010608c0000201c0000202" +
957             "0f0f646f6d61696e3132332e636f2e756b0000000000ff00000000"));
958         // CHECKSTYLE:ON Generated code
959 
960         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
961                 TEST_EMPTY_OPTIONS_SKIP_LIST);
962         assertTrue(offerPacket instanceof DhcpOfferPacket);
963         DhcpResults dhcpResults = offerPacket.toDhcpResults();
964         assertDhcpResults("10.63.93.4/20", "10.63.80.1", "192.0.2.1,192.0.2.2",
965                 "domain123.co.uk", "192.0.2.254", "", null, 49094, false, 0, dhcpResults);
966     }
967 
968     @Test
testBug2136()969     public void testBug2136() throws Exception {
970         // CHECKSTYLE:OFF Generated code
971         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
972             // Ethernet header.
973             "bcf5ac000000d0c7890000000800" +
974             // IP header.
975             "4500014c00000000ff119beac3eaf3880a3f5d04" +
976             // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
977             "0043004401387574" +
978             // BOOTP header.
979             "0201060163339a3000050000000000000a209ecd0000000000000000" +
980             // MAC address.
981             "bcf5ac00000000000000000000000000" +
982             // Server name.
983             "0000000000000000000000000000000000000000000000000000000000000000" +
984             "0000000000000000000000000000000000000000000000000000000000000000" +
985             // File.
986             "0000000000000000000000000000000000000000000000000000000000000000" +
987             "0000000000000000000000000000000000000000000000000000000000000000" +
988             "0000000000000000000000000000000000000000000000000000000000000000" +
989             "0000000000000000000000000000000000000000000000000000000000000000" +
990             // Options.
991             "6382536335010236040a20ff80330400001c200104fffff00003040a20900106089458413494584135" +
992             "0f0b6c616e63732e61632e756b000000000000000000ff00000000"));
993         // CHECKSTYLE:ON Generated code
994 
995         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2,
996                 TEST_EMPTY_OPTIONS_SKIP_LIST);
997         assertTrue(offerPacket instanceof DhcpOfferPacket);
998         assertEquals("BCF5AC000000", HexDump.toHexString(offerPacket.getClientMac()));
999         DhcpResults dhcpResults = offerPacket.toDhcpResults();
1000         assertDhcpResults("10.32.158.205/20", "10.32.144.1", "148.88.65.52,148.88.65.53",
1001                 "lancs.ac.uk", "10.32.255.128", "", null, 7200, false, 0, dhcpResults);
1002     }
1003 
1004     @Test
testUdpServerAnySourcePort()1005     public void testUdpServerAnySourcePort() throws Exception {
1006         // CHECKSTYLE:OFF Generated code
1007         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
1008             // Ethernet header.
1009             "9cd917000000001c2e0000000800" +
1010             // IP header.
1011             "45a00148000040003d115087d18194fb0a0f7af2" +
1012             // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
1013             // NOTE: The server source port is not the canonical port 67.
1014             "C29F004401341268" +
1015             // BOOTP header.
1016             "02010600d628ba8200000000000000000a0f7af2000000000a0fc818" +
1017             // MAC address.
1018             "9cd91700000000000000000000000000" +
1019             // Server name.
1020             "0000000000000000000000000000000000000000000000000000000000000000" +
1021             "0000000000000000000000000000000000000000000000000000000000000000" +
1022             // File.
1023             "0000000000000000000000000000000000000000000000000000000000000000" +
1024             "0000000000000000000000000000000000000000000000000000000000000000" +
1025             "0000000000000000000000000000000000000000000000000000000000000000" +
1026             "0000000000000000000000000000000000000000000000000000000000000000" +
1027             // Options.
1028             "6382536335010236040a0169fc3304000151800104ffff000003040a0fc817060cd1818003d1819403" +
1029             "d18180060f0777766d2e6564751c040a0fffffff000000"));
1030         // CHECKSTYLE:ON Generated code
1031 
1032         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2,
1033                 TEST_EMPTY_OPTIONS_SKIP_LIST);
1034         assertTrue(offerPacket instanceof DhcpOfferPacket);
1035         assertEquals("9CD917000000", HexDump.toHexString(offerPacket.getClientMac()));
1036         DhcpResults dhcpResults = offerPacket.toDhcpResults();
1037         assertDhcpResults("10.15.122.242/16", "10.15.200.23",
1038                 "209.129.128.3,209.129.148.3,209.129.128.6",
1039                 "wvm.edu", "10.1.105.252", "", null, 86400, false, 0, dhcpResults);
1040     }
1041 
1042     @Test
testUdpInvalidDstPort()1043     public void testUdpInvalidDstPort() throws Exception {
1044         // CHECKSTYLE:OFF Generated code
1045         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
1046             // Ethernet header.
1047             "9cd917000000001c2e0000000800" +
1048             // IP header.
1049             "45a00148000040003d115087d18194fb0a0f7af2" +
1050             // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
1051             // NOTE: The destination port is a non-DHCP port.
1052             "0043aaaa01341268" +
1053             // BOOTP header.
1054             "02010600d628ba8200000000000000000a0f7af2000000000a0fc818" +
1055             // MAC address.
1056             "9cd91700000000000000000000000000" +
1057             // Server name.
1058             "0000000000000000000000000000000000000000000000000000000000000000" +
1059             "0000000000000000000000000000000000000000000000000000000000000000" +
1060             // File.
1061             "0000000000000000000000000000000000000000000000000000000000000000" +
1062             "0000000000000000000000000000000000000000000000000000000000000000" +
1063             "0000000000000000000000000000000000000000000000000000000000000000" +
1064             "0000000000000000000000000000000000000000000000000000000000000000" +
1065             // Options.
1066             "6382536335010236040a0169fc3304000151800104ffff000003040a0fc817060cd1818003d1819403" +
1067             "d18180060f0777766d2e6564751c040a0fffffff000000"));
1068         // CHECKSTYLE:ON Generated code
1069 
1070         try {
1071             DhcpPacket.decodeFullPacket(packet, ENCAP_L2, TEST_EMPTY_OPTIONS_SKIP_LIST);
1072             fail("Packet with invalid dst port did not throw ParseException");
1073         } catch (ParseException expected) {}
1074     }
1075 
1076     @Test
testMultipleRouters()1077     public void testMultipleRouters() throws Exception {
1078         // CHECKSTYLE:OFF Generated code
1079         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
1080             // Ethernet header.
1081             "fc3d93000000" + "081735000000" + "0800" +
1082             // IP header.
1083             "45000148c2370000ff117ac2c0a8bd02ffffffff" +
1084             // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
1085             "0043004401343beb" +
1086             // BOOTP header.
1087             "0201060027f518e20000800000000000c0a8bd310000000000000000" +
1088             // MAC address.
1089             "fc3d9300000000000000000000000000" +
1090             // Server name.
1091             "0000000000000000000000000000000000000000000000000000000000000000" +
1092             "0000000000000000000000000000000000000000000000000000000000000000" +
1093             // File.
1094             "0000000000000000000000000000000000000000000000000000000000000000" +
1095             "0000000000000000000000000000000000000000000000000000000000000000" +
1096             "0000000000000000000000000000000000000000000000000000000000000000" +
1097             "0000000000000000000000000000000000000000000000000000000000000000" +
1098             // Options.
1099             "638253633501023604c0abbd023304000070803a04000038403b04000062700104ffffff00" +
1100             "0308c0a8bd01ffffff0006080808080808080404ff000000000000"));
1101         // CHECKSTYLE:ON Generated code
1102 
1103         DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2,
1104                 TEST_EMPTY_OPTIONS_SKIP_LIST);
1105         assertTrue(offerPacket instanceof DhcpOfferPacket);
1106         assertEquals("FC3D93000000", HexDump.toHexString(offerPacket.getClientMac()));
1107         DhcpResults dhcpResults = offerPacket.toDhcpResults();
1108         assertDhcpResults("192.168.189.49/24", "192.168.189.1", "8.8.8.8,8.8.4.4",
1109                 null, "192.171.189.2", "", null, 28800, false, 0, dhcpResults);
1110     }
1111 
1112     @Test
testDiscoverPacket()1113     public void testDiscoverPacket() throws Exception {
1114         final short secs = 7;
1115         final int transactionId = 0xdeadbeef;
1116         final byte[] hwaddr = {
1117                 (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a
1118         };
1119         final String testHostname = "android-01234567890abcde";
1120 
1121         ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
1122                 DhcpPacket.ENCAP_L2, transactionId, secs, hwaddr,
1123                 false /* do unicast */, DhcpClient.DEFAULT_REQUESTED_PARAMS,
1124                 false /* rapid commit */, testHostname, null /* customized DHCP options */);
1125 
1126         final byte[] headers = new byte[] {
1127             // Ethernet header.
1128             (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
1129             (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a,
1130             (byte) 0x08, (byte) 0x00,
1131             // IP header.
1132             (byte) 0x45, (byte) 0x10, (byte) 0x01, (byte) 0x56,
1133             (byte) 0x00, (byte) 0x00, (byte) 0x40, (byte) 0x00,
1134             (byte) 0x40, (byte) 0x11, (byte) 0x39, (byte) 0x88,
1135             (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
1136             (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
1137             // UDP header.
1138             (byte) 0x00, (byte) 0x44, (byte) 0x00, (byte) 0x43,
1139             (byte) 0x01, (byte) 0x42, (byte) 0x6a, (byte) 0x4a,
1140             // BOOTP.
1141             (byte) 0x01, (byte) 0x01, (byte) 0x06, (byte) 0x00,
1142             (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef,
1143             (byte) 0x00, (byte) 0x07, (byte) 0x00, (byte) 0x00,
1144             (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
1145             (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
1146             (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
1147             (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
1148             (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b,
1149             (byte) 0xb1, (byte) 0x7a
1150         };
1151         final byte[] options = new byte[] {
1152             // Magic cookie 0x63825363.
1153             (byte) 0x63, (byte) 0x82, (byte) 0x53, (byte) 0x63,
1154             // Message type DISCOVER.
1155             (byte) 0x35, (byte) 0x01, (byte) 0x01,
1156             // Client identifier Ethernet, da:01:19:5b:b1:7a.
1157             (byte) 0x3d, (byte) 0x07,
1158                     (byte) 0x01,
1159                     (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a,
1160             // Max message size 1500.
1161             (byte) 0x39, (byte) 0x02, (byte) 0x05, (byte) 0xdc,
1162             // Version "android-dhcp-???".
1163             (byte) 0x3c, (byte) 0x10,
1164                     'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 'h', 'c', 'p', '-', '?', '?', '?',
1165             // Hostname "android-01234567890abcde"
1166             (byte) 0x0c, (byte) 0x18,
1167                     'a', 'n', 'd', 'r', 'o', 'i', 'd', '-',
1168                     '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e',
1169             // Requested parameter list.
1170             (byte) 0x37, (byte) 0x0a,
1171                 DHCP_SUBNET_MASK,
1172                 DHCP_ROUTER,
1173                 DHCP_DNS_SERVER,
1174                 DHCP_DOMAIN_NAME,
1175                 DHCP_MTU,
1176                 DHCP_BROADCAST_ADDRESS,
1177                 DHCP_LEASE_TIME,
1178                 DHCP_RENEWAL_TIME,
1179                 DHCP_REBINDING_TIME,
1180                 DHCP_VENDOR_INFO,
1181             // End options.
1182             (byte) 0xff,
1183             // Our packets are always of even length. TODO: find out why and possibly fix it.
1184             (byte) 0x00
1185         };
1186         final byte[] expected = new byte[DhcpPacket.MIN_PACKET_LENGTH_L2 + options.length];
1187         assertTrue((expected.length & 1) == 0);
1188         assertEquals(DhcpPacket.MIN_PACKET_LENGTH_L2,
1189                 headers.length + 10 /* client hw addr padding */ + 64 /* sname */ + 128 /* file */);
1190         System.arraycopy(headers, 0, expected, 0, headers.length);
1191         System.arraycopy(options, 0, expected, DhcpPacket.MIN_PACKET_LENGTH_L2, options.length);
1192 
1193         final byte[] actual = new byte[packet.limit()];
1194         packet.get(actual);
1195         String msg = "Expected:\n  " + Arrays.toString(expected) + "\nActual:\n  "
1196                 + Arrays.toString(actual);
1197         assertTrue(msg, Arrays.equals(expected, actual));
1198     }
1199 
checkBuildOfferPacket(int leaseTimeSecs, @Nullable String hostname)1200     public void checkBuildOfferPacket(int leaseTimeSecs, @Nullable String hostname)
1201             throws Exception {
1202         final int renewalTime = (int) (Integer.toUnsignedLong(leaseTimeSecs) / 2);
1203         final int rebindingTime = (int) (Integer.toUnsignedLong(leaseTimeSecs) * 875 / 1000);
1204         final int transactionId = 0xdeadbeef;
1205 
1206         final ByteBuffer packet = DhcpPacket.buildOfferPacket(
1207                 DhcpPacket.ENCAP_BOOTP, transactionId, false /* broadcast */,
1208                 SERVER_ADDR, INADDR_ANY /* relayIp */, CLIENT_ADDR /* yourIp */,
1209                 CLIENT_MAC, leaseTimeSecs, NETMASK /* netMask */,
1210                 BROADCAST_ADDR /* bcAddr */, Collections.singletonList(SERVER_ADDR) /* gateways */,
1211                 Collections.singletonList(SERVER_ADDR) /* dnsServers */,
1212                 SERVER_ADDR /* dhcpServerIdentifier */, null /* domainName */, hostname,
1213                 false /* metered */, MTU, CAPTIVE_PORTAL_API_URL);
1214 
1215         ByteArrayOutputStream bos = new ByteArrayOutputStream();
1216         // BOOTP headers
1217         bos.write(new byte[] {
1218                 (byte) 0x02, (byte) 0x01, (byte) 0x06, (byte) 0x00,
1219                 (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef,
1220                 (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
1221                 // ciaddr
1222                 (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
1223         });
1224         // yiaddr
1225         bos.write(CLIENT_ADDR.getAddress());
1226         // siaddr
1227         bos.write(SERVER_ADDR.getAddress());
1228         // giaddr
1229         bos.write(INADDR_ANY.getAddress());
1230         // chaddr
1231         bos.write(CLIENT_MAC);
1232 
1233         // Padding
1234         bos.write(new byte[202]);
1235 
1236         // Options
1237         bos.write(new byte[]{
1238                 // Magic cookie 0x63825363.
1239                 (byte) 0x63, (byte) 0x82, (byte) 0x53, (byte) 0x63,
1240                 // Message type OFFER.
1241                 (byte) 0x35, (byte) 0x01, (byte) 0x02,
1242         });
1243         // Server ID
1244         bos.write(new byte[] { (byte) 0x36, (byte) 0x04 });
1245         bos.write(SERVER_ADDR.getAddress());
1246         // Lease time
1247         bos.write(new byte[] { (byte) 0x33, (byte) 0x04 });
1248         bos.write(intToByteArray(leaseTimeSecs));
1249         if (leaseTimeSecs != INFINITE_LEASE) {
1250             // Renewal time
1251             bos.write(new byte[]{(byte) 0x3a, (byte) 0x04});
1252             bos.write(intToByteArray(renewalTime));
1253             // Rebinding time
1254             bos.write(new byte[]{(byte) 0x3b, (byte) 0x04});
1255             bos.write(intToByteArray(rebindingTime));
1256         }
1257         // Subnet mask
1258         bos.write(new byte[] { (byte) 0x01, (byte) 0x04 });
1259         bos.write(NETMASK.getAddress());
1260         // Broadcast address
1261         bos.write(new byte[] { (byte) 0x1c, (byte) 0x04 });
1262         bos.write(BROADCAST_ADDR.getAddress());
1263         // Router
1264         bos.write(new byte[] { (byte) 0x03, (byte) 0x04 });
1265         bos.write(SERVER_ADDR.getAddress());
1266         // Nameserver
1267         bos.write(new byte[] { (byte) 0x06, (byte) 0x04 });
1268         bos.write(SERVER_ADDR.getAddress());
1269         // Hostname
1270         if (hostname != null) {
1271             bos.write(new byte[]{(byte) 0x0c, (byte) hostname.length()});
1272             bos.write(hostname.getBytes(Charset.forName("US-ASCII")));
1273         }
1274         // MTU
1275         bos.write(new byte[] { (byte) 0x1a, (byte) 0x02 });
1276         bos.write(shortToByteArray(MTU));
1277         // capport URL. Option 114 = 0x72
1278         bos.write(new byte[] { (byte) 0x72, (byte) CAPTIVE_PORTAL_API_URL.length() });
1279         bos.write(CAPTIVE_PORTAL_API_URL.getBytes(Charset.forName("US-ASCII")));
1280         // End options.
1281         bos.write(0xff);
1282 
1283         if ((bos.size() & 1) != 0) {
1284             bos.write(0x00);
1285         }
1286 
1287         final byte[] expected = bos.toByteArray();
1288         final byte[] actual = new byte[packet.limit()];
1289         packet.get(actual);
1290         final String msg = "Expected:\n  " + HexDump.dumpHexString(expected) +
1291                 "\nActual:\n  " + HexDump.dumpHexString(actual);
1292         assertTrue(msg, Arrays.equals(expected, actual));
1293     }
1294 
1295     @Test
testOfferPacket()1296     public void testOfferPacket() throws Exception {
1297         checkBuildOfferPacket(3600, HOSTNAME);
1298         checkBuildOfferPacket(Integer.MAX_VALUE, HOSTNAME);
1299         checkBuildOfferPacket(0x80000000, HOSTNAME);
1300         checkBuildOfferPacket(INFINITE_LEASE, HOSTNAME);
1301         checkBuildOfferPacket(3600, null);
1302     }
1303 
1304     @Test
testInvalidLengthIpv6OnlyPreferredOption()1305     public void testInvalidLengthIpv6OnlyPreferredOption() throws Exception {
1306         // CHECKSTYLE:OFF Generated code
1307         final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
1308                 // IP header.
1309                 "45100158000040004011B5CEC0A80164C0A80102" +
1310                 // UDP header
1311                 "004300440144CE63" +
1312                 // BOOTP header
1313                 "02010600B8BF41E60000000000000000C0A80102C0A8016400000000" +
1314                 // MAC address.
1315                 "22B3614EE01200000000000000000000" +
1316                 // Server name and padding.
1317                 "0000000000000000000000000000000000000000000000000000000000000000" +
1318                 "0000000000000000000000000000000000000000000000000000000000000000" +
1319                 // File.
1320                 "0000000000000000000000000000000000000000000000000000000000000000" +
1321                 "0000000000000000000000000000000000000000000000000000000000000000" +
1322                 "0000000000000000000000000000000000000000000000000000000000000000" +
1323                 "0000000000000000000000000000000000000000000000000000000000000000" +
1324                 // Options
1325                 "638253633501023604C0A80164330400000E103A04000007083B0400000C4E01" +
1326                 "04FFFFFF001C04C0A801FF0304C0A801640604C0A801640C0C74657374686F73" +
1327                 "746E616D651A0205DC" +
1328                 // Option 108 (0x6c, IPv6-Only preferred option), length 8 (0x08)
1329                 "6C080102030405060708" +
1330                 // End of options.
1331                 "FF"));
1332         // CHECKSTYLE:ON Generated code
1333 
1334         final DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
1335                 TEST_EMPTY_OPTIONS_SKIP_LIST);
1336         // rfc8925#section-3.1: The client MUST ignore the IPv6-Only Preferred option if the length
1337         // field value is not 4.
1338         assertTrue(offerPacket instanceof DhcpOfferPacket);
1339         assertEquals(offerPacket.mIpv6OnlyWaitTime, null);
1340     }
1341 
intToByteArray(int val)1342     private static byte[] intToByteArray(int val) {
1343         return ByteBuffer.allocate(4).putInt(val).array();
1344     }
1345 
shortToByteArray(short val)1346     private static byte[] shortToByteArray(short val) {
1347         return ByteBuffer.allocate(2).putShort(val).array();
1348     }
1349 }
1350