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