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