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
17package android.net.dhcp;
18
19import static android.net.dhcp.DhcpPacket.*;
20import static org.junit.Assert.assertEquals;
21import static org.junit.Assert.assertNotNull;
22import static org.junit.Assert.assertNull;
23import static org.junit.Assert.assertTrue;
24import static org.junit.Assert.fail;
25
26import android.net.DhcpResults;
27import android.net.LinkAddress;
28import android.net.NetworkUtils;
29import android.net.metrics.DhcpErrorEvent;
30import android.support.test.runner.AndroidJUnit4;
31import android.support.test.filters.SmallTest;
32import android.system.OsConstants;
33
34import com.android.internal.util.HexDump;
35
36import java.net.Inet4Address;
37import java.nio.ByteBuffer;
38import java.util.ArrayList;
39import java.util.Arrays;
40import java.util.Random;
41
42import org.junit.Before;
43import org.junit.Test;
44import org.junit.runner.RunWith;
45
46@RunWith(AndroidJUnit4.class)
47@SmallTest
48public class DhcpPacketTest {
49
50    private static Inet4Address SERVER_ADDR = v4Address("192.0.2.1");
51    private static Inet4Address CLIENT_ADDR = v4Address("192.0.2.234");
52    // Use our own empty address instead of Inet4Address.ANY or INADDR_ANY to ensure that the code
53    // doesn't use == instead of equals when comparing addresses.
54    private static Inet4Address ANY = (Inet4Address) v4Address("0.0.0.0");
55
56    private static byte[] CLIENT_MAC = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
57
58    private static final Inet4Address v4Address(String addrString) throws IllegalArgumentException {
59        return (Inet4Address) NetworkUtils.numericToInetAddress(addrString);
60    }
61
62    @Before
63    public void setUp() {
64        DhcpPacket.testOverrideVendorId = "android-dhcp-???";
65        DhcpPacket.testOverrideHostname = "android-01234567890abcde";
66    }
67
68    class TestDhcpPacket extends DhcpPacket {
69        private byte mType;
70        // TODO: Make this a map of option numbers to bytes instead.
71        private byte[] mDomainBytes, mVendorInfoBytes, mLeaseTimeBytes, mNetmaskBytes;
72
73        public TestDhcpPacket(byte type, Inet4Address clientIp, Inet4Address yourIp) {
74            super(0xdeadbeef, (short) 0, clientIp, yourIp, INADDR_ANY, INADDR_ANY,
75                  CLIENT_MAC, true);
76            mType = type;
77        }
78
79        public TestDhcpPacket(byte type) {
80            this(type, INADDR_ANY, CLIENT_ADDR);
81        }
82
83        public TestDhcpPacket setDomainBytes(byte[] domainBytes) {
84            mDomainBytes = domainBytes;
85            return this;
86        }
87
88        public TestDhcpPacket setVendorInfoBytes(byte[] vendorInfoBytes) {
89            mVendorInfoBytes = vendorInfoBytes;
90            return this;
91        }
92
93        public TestDhcpPacket setLeaseTimeBytes(byte[] leaseTimeBytes) {
94            mLeaseTimeBytes = leaseTimeBytes;
95            return this;
96        }
97
98        public TestDhcpPacket setNetmaskBytes(byte[] netmaskBytes) {
99            mNetmaskBytes = netmaskBytes;
100            return this;
101        }
102
103        public ByteBuffer buildPacket(int encap, short unusedDestUdp, short unusedSrcUdp) {
104            ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
105            fillInPacket(encap, CLIENT_ADDR, SERVER_ADDR,
106                         DHCP_CLIENT, DHCP_SERVER, result, DHCP_BOOTREPLY, false);
107            return result;
108        }
109
110        public void finishPacket(ByteBuffer buffer) {
111            addTlv(buffer, DHCP_MESSAGE_TYPE, mType);
112            if (mDomainBytes != null) {
113                addTlv(buffer, DHCP_DOMAIN_NAME, mDomainBytes);
114            }
115            if (mVendorInfoBytes != null) {
116                addTlv(buffer, DHCP_VENDOR_INFO, mVendorInfoBytes);
117            }
118            if (mLeaseTimeBytes != null) {
119                addTlv(buffer, DHCP_LEASE_TIME, mLeaseTimeBytes);
120            }
121            if (mNetmaskBytes != null) {
122                addTlv(buffer, DHCP_SUBNET_MASK, mNetmaskBytes);
123            }
124            addTlvEnd(buffer);
125        }
126
127        // Convenience method.
128        public ByteBuffer build() {
129            // ENCAP_BOOTP packets don't contain ports, so just pass in 0.
130            ByteBuffer pkt = buildPacket(ENCAP_BOOTP, (short) 0, (short) 0);
131            pkt.flip();
132            return pkt;
133        }
134    }
135
136    private void assertDomainAndVendorInfoParses(
137            String expectedDomain, byte[] domainBytes,
138            String expectedVendorInfo, byte[] vendorInfoBytes) throws Exception {
139        ByteBuffer packet = new TestDhcpPacket(DHCP_MESSAGE_TYPE_OFFER)
140                .setDomainBytes(domainBytes)
141                .setVendorInfoBytes(vendorInfoBytes)
142                .build();
143        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP);
144        assertEquals(expectedDomain, offerPacket.mDomainName);
145        assertEquals(expectedVendorInfo, offerPacket.mVendorInfo);
146    }
147
148    @Test
149    public void testDomainName() throws Exception {
150        byte[] nullByte = new byte[] { 0x00 };
151        byte[] twoNullBytes = new byte[] { 0x00, 0x00 };
152        byte[] nonNullDomain = new byte[] {
153            (byte) 'g', (byte) 'o', (byte) 'o', (byte) '.', (byte) 'g', (byte) 'l'
154        };
155        byte[] trailingNullDomain = new byte[] {
156            (byte) 'g', (byte) 'o', (byte) 'o', (byte) '.', (byte) 'g', (byte) 'l', 0x00
157        };
158        byte[] embeddedNullsDomain = new byte[] {
159            (byte) 'g', (byte) 'o', (byte) 'o', 0x00, 0x00, (byte) 'g', (byte) 'l'
160        };
161        byte[] metered = "ANDROID_METERED".getBytes("US-ASCII");
162
163        byte[] meteredEmbeddedNull = metered.clone();
164        meteredEmbeddedNull[7] = (char) 0;
165
166        byte[] meteredTrailingNull = metered.clone();
167        meteredTrailingNull[meteredTrailingNull.length - 1] = (char) 0;
168
169        assertDomainAndVendorInfoParses("", nullByte, "\u0000", nullByte);
170        assertDomainAndVendorInfoParses("", twoNullBytes, "\u0000\u0000", twoNullBytes);
171        assertDomainAndVendorInfoParses("goo.gl", nonNullDomain, "ANDROID_METERED", metered);
172        assertDomainAndVendorInfoParses("goo", embeddedNullsDomain,
173                                        "ANDROID\u0000METERED", meteredEmbeddedNull);
174        assertDomainAndVendorInfoParses("goo.gl", trailingNullDomain,
175                                        "ANDROID_METERE\u0000", meteredTrailingNull);
176    }
177
178    private void assertLeaseTimeParses(boolean expectValid, Integer rawLeaseTime,
179            long leaseTimeMillis, byte[] leaseTimeBytes) throws Exception {
180        TestDhcpPacket testPacket = new TestDhcpPacket(DHCP_MESSAGE_TYPE_OFFER);
181        if (leaseTimeBytes != null) {
182            testPacket.setLeaseTimeBytes(leaseTimeBytes);
183        }
184        ByteBuffer packet = testPacket.build();
185        DhcpPacket offerPacket = null;
186
187        if (!expectValid) {
188            try {
189                offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP);
190                fail("Invalid packet parsed successfully: " + offerPacket);
191            } catch (ParseException expected) {
192            }
193            return;
194        }
195
196        offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP);
197        assertNotNull(offerPacket);
198        assertEquals(rawLeaseTime, offerPacket.mLeaseTime);
199        DhcpResults dhcpResults = offerPacket.toDhcpResults();  // Just check this doesn't crash.
200        assertEquals(leaseTimeMillis, offerPacket.getLeaseTimeMillis());
201    }
202
203    @Test
204    public void testLeaseTime() throws Exception {
205        byte[] noLease = null;
206        byte[] tooShortLease = new byte[] { 0x00, 0x00 };
207        byte[] tooLongLease = new byte[] { 0x00, 0x00, 0x00, 60, 0x01 };
208        byte[] zeroLease = new byte[] { 0x00, 0x00, 0x00, 0x00 };
209        byte[] tenSecondLease = new byte[] { 0x00, 0x00, 0x00, 10 };
210        byte[] oneMinuteLease = new byte[] { 0x00, 0x00, 0x00, 60 };
211        byte[] fiveMinuteLease = new byte[] { 0x00, 0x00, 0x01, 0x2c };
212        byte[] oneDayLease = new byte[] { 0x00, 0x01, 0x51, (byte) 0x80 };
213        byte[] maxIntPlusOneLease = new byte[] { (byte) 0x80, 0x00, 0x00, 0x01 };
214        byte[] infiniteLease = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
215
216        assertLeaseTimeParses(true, null, 0, noLease);
217        assertLeaseTimeParses(false, null, 0, tooShortLease);
218        assertLeaseTimeParses(false, null, 0, tooLongLease);
219        assertLeaseTimeParses(true, 0, 60 * 1000, zeroLease);
220        assertLeaseTimeParses(true, 10, 60 * 1000, tenSecondLease);
221        assertLeaseTimeParses(true, 60, 60 * 1000, oneMinuteLease);
222        assertLeaseTimeParses(true, 300, 300 * 1000, fiveMinuteLease);
223        assertLeaseTimeParses(true, 86400, 86400 * 1000, oneDayLease);
224        assertLeaseTimeParses(true, -2147483647, 2147483649L * 1000, maxIntPlusOneLease);
225        assertLeaseTimeParses(true, DhcpPacket.INFINITE_LEASE, 0, infiniteLease);
226    }
227
228    private void checkIpAddress(String expected, Inet4Address clientIp, Inet4Address yourIp,
229                                byte[] netmaskBytes) throws Exception {
230        checkIpAddress(expected, DHCP_MESSAGE_TYPE_OFFER, clientIp, yourIp, netmaskBytes);
231        checkIpAddress(expected, DHCP_MESSAGE_TYPE_ACK, clientIp, yourIp, netmaskBytes);
232    }
233
234    private void checkIpAddress(String expected, byte type,
235                                Inet4Address clientIp, Inet4Address yourIp,
236                                byte[] netmaskBytes) throws Exception {
237        ByteBuffer packet = new TestDhcpPacket(type, clientIp, yourIp)
238                .setNetmaskBytes(netmaskBytes)
239                .build();
240        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP);
241        DhcpResults results = offerPacket.toDhcpResults();
242
243        if (expected != null) {
244            LinkAddress expectedAddress = new LinkAddress(expected);
245            assertEquals(expectedAddress, results.ipAddress);
246        } else {
247            assertNull(results);
248        }
249    }
250
251    @Test
252    public void testIpAddress() throws Exception {
253        byte[] slash11Netmask = new byte[] { (byte) 0xff, (byte) 0xe0, 0x00, 0x00 };
254        byte[] slash24Netmask = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00 };
255        byte[] invalidNetmask = new byte[] { (byte) 0xff, (byte) 0xfb, (byte) 0xff, 0x00 };
256        Inet4Address example1 = v4Address("192.0.2.1");
257        Inet4Address example2 = v4Address("192.0.2.43");
258
259        // A packet without any addresses is not valid.
260        checkIpAddress(null, ANY, ANY, slash24Netmask);
261
262        // ClientIP is used iff YourIP is not present.
263        checkIpAddress("192.0.2.1/24", example2, example1, slash24Netmask);
264        checkIpAddress("192.0.2.43/11", example2, ANY, slash11Netmask);
265        checkIpAddress("192.0.2.43/11", ANY, example2, slash11Netmask);
266
267        // Invalid netmasks are ignored.
268        checkIpAddress(null, example2, ANY, invalidNetmask);
269
270        // If there is no netmask, implicit netmasks are used.
271        checkIpAddress("192.0.2.43/24", ANY, example2, null);
272    }
273
274    private void assertDhcpResults(String ipAddress, String gateway, String dnsServersString,
275            String domains, String serverAddress, String vendorInfo, int leaseDuration,
276            boolean hasMeteredHint, int mtu, DhcpResults dhcpResults) throws Exception {
277        assertEquals(new LinkAddress(ipAddress), dhcpResults.ipAddress);
278        assertEquals(v4Address(gateway), dhcpResults.gateway);
279
280        String[] dnsServerStrings = dnsServersString.split(",");
281        ArrayList dnsServers = new ArrayList();
282        for (String dnsServerString : dnsServerStrings) {
283            dnsServers.add(v4Address(dnsServerString));
284        }
285        assertEquals(dnsServers, dhcpResults.dnsServers);
286
287        assertEquals(domains, dhcpResults.domains);
288        assertEquals(v4Address(serverAddress), dhcpResults.serverAddress);
289        assertEquals(vendorInfo, dhcpResults.vendorInfo);
290        assertEquals(leaseDuration, dhcpResults.leaseDuration);
291        assertEquals(hasMeteredHint, dhcpResults.hasMeteredHint());
292        assertEquals(mtu, dhcpResults.mtu);
293    }
294
295    @Test
296    public void testOffer1() throws Exception {
297        // TODO: Turn all of these into golden files. This will probably require using
298        // android.support.test.InstrumentationRegistry for obtaining a Context object
299        // to read such golden files, along with an appropriate Android.mk.
300        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
301            // IP header.
302            "451001480000000080118849c0a89003c0a89ff7" +
303            // UDP header.
304            "004300440134dcfa" +
305            // BOOTP header.
306            "02010600c997a63b0000000000000000c0a89ff70000000000000000" +
307            // MAC address.
308            "30766ff2a90c00000000000000000000" +
309            // Server name.
310            "0000000000000000000000000000000000000000000000000000000000000000" +
311            "0000000000000000000000000000000000000000000000000000000000000000" +
312            // File.
313            "0000000000000000000000000000000000000000000000000000000000000000" +
314            "0000000000000000000000000000000000000000000000000000000000000000" +
315            "0000000000000000000000000000000000000000000000000000000000000000" +
316            "0000000000000000000000000000000000000000000000000000000000000000" +
317            // Options
318            "638253633501023604c0a89003330400001c200104fffff0000304c0a89ffe06080808080808080404" +
319            "3a0400000e103b040000189cff00000000000000000000"));
320
321        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
322        assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
323        DhcpResults dhcpResults = offerPacket.toDhcpResults();
324        assertDhcpResults("192.168.159.247/20", "192.168.159.254", "8.8.8.8,8.8.4.4",
325                null, "192.168.144.3", null, 7200, false, 0, dhcpResults);
326    }
327
328    @Test
329    public void testOffer2() throws Exception {
330        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
331            // IP header.
332            "450001518d0600004011144dc0a82b01c0a82bf7" +
333            // UDP header.
334            "00430044013d9ac7" +
335            // BOOTP header.
336            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
337            // MAC address.
338            "30766ff2a90c00000000000000000000" +
339            // Server name.
340            "0000000000000000000000000000000000000000000000000000000000000000" +
341            "0000000000000000000000000000000000000000000000000000000000000000" +
342            // File.
343            "0000000000000000000000000000000000000000000000000000000000000000" +
344            "0000000000000000000000000000000000000000000000000000000000000000" +
345            "0000000000000000000000000000000000000000000000000000000000000000" +
346            "0000000000000000000000000000000000000000000000000000000000000000" +
347            // Options
348            "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
349            "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff"));
350
351        assertEquals(337, packet.limit());
352        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
353        assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
354        DhcpResults dhcpResults = offerPacket.toDhcpResults();
355        assertDhcpResults("192.168.43.247/24", "192.168.43.1", "192.168.43.1",
356                null, "192.168.43.1", "ANDROID_METERED", 3600, true, 0, dhcpResults);
357        assertTrue(dhcpResults.hasMeteredHint());
358    }
359
360    @Test
361    public void testBadIpPacket() throws Exception {
362        final byte[] packet = HexDump.hexStringToByteArray(
363            // IP header.
364            "450001518d0600004011144dc0a82b01c0a82bf7");
365
366        try {
367            DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
368        } catch (DhcpPacket.ParseException expected) {
369            assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode);
370            return;
371        }
372        fail("Dhcp packet parsing should have failed");
373    }
374
375    @Test
376    public void testBadDhcpPacket() throws Exception {
377        final byte[] packet = HexDump.hexStringToByteArray(
378            // IP header.
379            "450001518d0600004011144dc0a82b01c0a82bf7" +
380            // UDP header.
381            "00430044013d9ac7" +
382            // BOOTP header.
383            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000");
384
385        try {
386            DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
387        } catch (DhcpPacket.ParseException expected) {
388            assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode);
389            return;
390        }
391        fail("Dhcp packet parsing should have failed");
392    }
393
394    @Test
395    public void testBadTruncatedOffer() throws Exception {
396        final byte[] packet = HexDump.hexStringToByteArray(
397            // IP header.
398            "450001518d0600004011144dc0a82b01c0a82bf7" +
399            // UDP header.
400            "00430044013d9ac7" +
401            // BOOTP header.
402            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
403            // MAC address.
404            "30766ff2a90c00000000000000000000" +
405            // Server name.
406            "0000000000000000000000000000000000000000000000000000000000000000" +
407            "0000000000000000000000000000000000000000000000000000000000000000" +
408            // File, missing one byte
409            "0000000000000000000000000000000000000000000000000000000000000000" +
410            "0000000000000000000000000000000000000000000000000000000000000000" +
411            "0000000000000000000000000000000000000000000000000000000000000000" +
412            "00000000000000000000000000000000000000000000000000000000000000");
413
414        try {
415            DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
416        } catch (DhcpPacket.ParseException expected) {
417            assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode);
418            return;
419        }
420        fail("Dhcp packet parsing should have failed");
421    }
422
423    @Test
424    public void testBadOfferWithoutACookie() throws Exception {
425        final byte[] packet = HexDump.hexStringToByteArray(
426            // IP header.
427            "450001518d0600004011144dc0a82b01c0a82bf7" +
428            // UDP header.
429            "00430044013d9ac7" +
430            // BOOTP header.
431            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
432            // MAC address.
433            "30766ff2a90c00000000000000000000" +
434            // Server name.
435            "0000000000000000000000000000000000000000000000000000000000000000" +
436            "0000000000000000000000000000000000000000000000000000000000000000" +
437            // File.
438            "0000000000000000000000000000000000000000000000000000000000000000" +
439            "0000000000000000000000000000000000000000000000000000000000000000" +
440            "0000000000000000000000000000000000000000000000000000000000000000" +
441            "0000000000000000000000000000000000000000000000000000000000000000"
442            // No options
443            );
444
445        try {
446            DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
447        } catch (DhcpPacket.ParseException expected) {
448            assertDhcpErrorCodes(DhcpErrorEvent.DHCP_NO_COOKIE, expected.errorCode);
449            return;
450        }
451        fail("Dhcp packet parsing should have failed");
452    }
453
454    @Test
455    public void testOfferWithBadCookie() throws Exception {
456        final byte[] packet = HexDump.hexStringToByteArray(
457            // IP header.
458            "450001518d0600004011144dc0a82b01c0a82bf7" +
459            // UDP header.
460            "00430044013d9ac7" +
461            // BOOTP header.
462            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
463            // MAC address.
464            "30766ff2a90c00000000000000000000" +
465            // Server name.
466            "0000000000000000000000000000000000000000000000000000000000000000" +
467            "0000000000000000000000000000000000000000000000000000000000000000" +
468            // File.
469            "0000000000000000000000000000000000000000000000000000000000000000" +
470            "0000000000000000000000000000000000000000000000000000000000000000" +
471            "0000000000000000000000000000000000000000000000000000000000000000" +
472            "0000000000000000000000000000000000000000000000000000000000000000" +
473            // Bad cookie
474            "DEADBEEF3501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
475            "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff");
476
477        try {
478            DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
479        } catch (DhcpPacket.ParseException expected) {
480            assertDhcpErrorCodes(DhcpErrorEvent.DHCP_BAD_MAGIC_COOKIE, expected.errorCode);
481            return;
482        }
483        fail("Dhcp packet parsing should have failed");
484    }
485
486    private void assertDhcpErrorCodes(int expected, int got) {
487        assertEquals(Integer.toHexString(expected), Integer.toHexString(got));
488    }
489
490    @Test
491    public void testTruncatedOfferPackets() throws Exception {
492        final byte[] packet = HexDump.hexStringToByteArray(
493            // IP header.
494            "450001518d0600004011144dc0a82b01c0a82bf7" +
495            // UDP header.
496            "00430044013d9ac7" +
497            // BOOTP header.
498            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
499            // MAC address.
500            "30766ff2a90c00000000000000000000" +
501            // Server name.
502            "0000000000000000000000000000000000000000000000000000000000000000" +
503            "0000000000000000000000000000000000000000000000000000000000000000" +
504            // File.
505            "0000000000000000000000000000000000000000000000000000000000000000" +
506            "0000000000000000000000000000000000000000000000000000000000000000" +
507            "0000000000000000000000000000000000000000000000000000000000000000" +
508            "0000000000000000000000000000000000000000000000000000000000000000" +
509            // Options
510            "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
511            "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff");
512
513        for (int len = 0; len < packet.length; len++) {
514            try {
515                DhcpPacket.decodeFullPacket(packet, len, ENCAP_L3);
516            } catch (ParseException e) {
517                if (e.errorCode == DhcpErrorEvent.PARSING_ERROR) {
518                    fail(String.format("bad truncated packet of length %d", len));
519                }
520            }
521        }
522    }
523
524    @Test
525    public void testRandomPackets() throws Exception {
526        final int maxRandomPacketSize = 512;
527        final Random r = new Random();
528        for (int i = 0; i < 10000; i++) {
529            byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
530            r.nextBytes(packet);
531            try {
532                DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3);
533            } catch (ParseException e) {
534                if (e.errorCode == DhcpErrorEvent.PARSING_ERROR) {
535                    fail("bad packet: " + HexDump.toHexString(packet));
536                }
537            }
538        }
539    }
540
541    private byte[] mtuBytes(int mtu) {
542        // 0x1a02: option 26, length 2. 0xff: no more options.
543        if (mtu > Short.MAX_VALUE - Short.MIN_VALUE) {
544            throw new IllegalArgumentException(
545                String.format("Invalid MTU %d, must be 16-bit unsigned", mtu));
546        }
547        String hexString = String.format("1a02%04xff", mtu);
548        return HexDump.hexStringToByteArray(hexString);
549    }
550
551    private void checkMtu(ByteBuffer packet, int expectedMtu, byte[] mtuBytes) throws Exception {
552        if (mtuBytes != null) {
553            packet.position(packet.capacity() - mtuBytes.length);
554            packet.put(mtuBytes);
555            packet.clear();
556        }
557        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
558        assertTrue(offerPacket instanceof DhcpOfferPacket);  // Implicitly checks it's non-null.
559        DhcpResults dhcpResults = offerPacket.toDhcpResults();
560        assertDhcpResults("192.168.159.247/20", "192.168.159.254", "8.8.8.8,8.8.4.4",
561                null, "192.168.144.3", null, 7200, false, expectedMtu, dhcpResults);
562    }
563
564    @Test
565    public void testMtu() throws Exception {
566        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
567            // IP header.
568            "451001480000000080118849c0a89003c0a89ff7" +
569            // UDP header.
570            "004300440134dcfa" +
571            // BOOTP header.
572            "02010600c997a63b0000000000000000c0a89ff70000000000000000" +
573            // MAC address.
574            "30766ff2a90c00000000000000000000" +
575            // Server name.
576            "0000000000000000000000000000000000000000000000000000000000000000" +
577            "0000000000000000000000000000000000000000000000000000000000000000" +
578            // File.
579            "0000000000000000000000000000000000000000000000000000000000000000" +
580            "0000000000000000000000000000000000000000000000000000000000000000" +
581            "0000000000000000000000000000000000000000000000000000000000000000" +
582            "0000000000000000000000000000000000000000000000000000000000000000" +
583            // Options
584            "638253633501023604c0a89003330400001c200104fffff0000304c0a89ffe06080808080808080404" +
585            "3a0400000e103b040000189cff00000000"));
586
587        checkMtu(packet, 0, null);
588        checkMtu(packet, 0, mtuBytes(1501));
589        checkMtu(packet, 1500, mtuBytes(1500));
590        checkMtu(packet, 1499, mtuBytes(1499));
591        checkMtu(packet, 1280, mtuBytes(1280));
592        checkMtu(packet, 0, mtuBytes(1279));
593        checkMtu(packet, 0, mtuBytes(576));
594        checkMtu(packet, 0, mtuBytes(68));
595        checkMtu(packet, 0, mtuBytes(Short.MIN_VALUE));
596        checkMtu(packet, 0, mtuBytes(Short.MAX_VALUE + 3));
597        checkMtu(packet, 0, mtuBytes(-1));
598    }
599
600    @Test
601    public void testBadHwaddrLength() throws Exception {
602        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
603            // IP header.
604            "450001518d0600004011144dc0a82b01c0a82bf7" +
605            // UDP header.
606            "00430044013d9ac7" +
607            // BOOTP header.
608            "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" +
609            // MAC address.
610            "30766ff2a90c00000000000000000000" +
611            // Server name.
612            "0000000000000000000000000000000000000000000000000000000000000000" +
613            "0000000000000000000000000000000000000000000000000000000000000000" +
614            // File.
615            "0000000000000000000000000000000000000000000000000000000000000000" +
616            "0000000000000000000000000000000000000000000000000000000000000000" +
617            "0000000000000000000000000000000000000000000000000000000000000000" +
618            "0000000000000000000000000000000000000000000000000000000000000000" +
619            // Options
620            "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" +
621            "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff"));
622        String expectedClientMac = "30766FF2A90C";
623
624        final int hwAddrLenOffset = 20 + 8 + 2;
625        assertEquals(6, packet.get(hwAddrLenOffset));
626
627        // Expect the expected.
628        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
629        assertNotNull(offerPacket);
630        assertEquals(6, offerPacket.getClientMac().length);
631        assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac()));
632
633        // Reduce the hardware address length and verify that it shortens the client MAC.
634        packet.flip();
635        packet.put(hwAddrLenOffset, (byte) 5);
636        offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
637        assertNotNull(offerPacket);
638        assertEquals(5, offerPacket.getClientMac().length);
639        assertEquals(expectedClientMac.substring(0, 10),
640                HexDump.toHexString(offerPacket.getClientMac()));
641
642        packet.flip();
643        packet.put(hwAddrLenOffset, (byte) 3);
644        offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
645        assertNotNull(offerPacket);
646        assertEquals(3, offerPacket.getClientMac().length);
647        assertEquals(expectedClientMac.substring(0, 6),
648                HexDump.toHexString(offerPacket.getClientMac()));
649
650        // Set the the hardware address length to 0xff and verify that we a) don't treat it as -1
651        // and crash, and b) hardcode it to 6.
652        packet.flip();
653        packet.put(hwAddrLenOffset, (byte) -1);
654        offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
655        assertNotNull(offerPacket);
656        assertEquals(6, offerPacket.getClientMac().length);
657        assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac()));
658
659        // Set the the hardware address length to a positive invalid value (> 16) and verify that we
660        // hardcode it to 6.
661        packet.flip();
662        packet.put(hwAddrLenOffset, (byte) 17);
663        offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
664        assertNotNull(offerPacket);
665        assertEquals(6, offerPacket.getClientMac().length);
666        assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac()));
667    }
668
669    @Test
670    public void testPadAndOverloadedOptionsOffer() throws Exception {
671        // A packet observed in the real world that is interesting for two reasons:
672        //
673        // 1. It uses pad bytes, which we previously didn't support correctly.
674        // 2. It uses DHCP option overloading, which we don't currently support (but it doesn't
675        //    store any information in the overloaded fields).
676        //
677        // For now, we just check that it parses correctly.
678        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
679            // Ethernet header.
680            "b4cef6000000e80462236e300800" +
681            // IP header.
682            "4500014c00000000ff11741701010101ac119876" +
683            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
684            "004300440138ae5a" +
685            // BOOTP header.
686            "020106000fa0059f0000000000000000ac1198760000000000000000" +
687            // MAC address.
688            "b4cef600000000000000000000000000" +
689            // Server name.
690            "ff00000000000000000000000000000000000000000000000000000000000000" +
691            "0000000000000000000000000000000000000000000000000000000000000000" +
692            // File.
693            "ff00000000000000000000000000000000000000000000000000000000000000" +
694            "0000000000000000000000000000000000000000000000000000000000000000" +
695            "0000000000000000000000000000000000000000000000000000000000000000" +
696            "0000000000000000000000000000000000000000000000000000000000000000" +
697            // Options
698            "638253633501023604010101010104ffff000033040000a8c03401030304ac1101010604ac110101" +
699            "0000000000000000000000000000000000000000000000ff000000"));
700
701        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
702        assertTrue(offerPacket instanceof DhcpOfferPacket);
703        DhcpResults dhcpResults = offerPacket.toDhcpResults();
704        assertDhcpResults("172.17.152.118/16", "172.17.1.1", "172.17.1.1",
705                null, "1.1.1.1", null, 43200, false, 0, dhcpResults);
706    }
707
708    @Test
709    public void testBug2111() throws Exception {
710        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
711            // IP header.
712            "4500014c00000000ff119beac3eaf3880a3f5d04" +
713            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
714            "0043004401387464" +
715            // BOOTP header.
716            "0201060002554812000a0000000000000a3f5d040000000000000000" +
717            // MAC address.
718            "00904c00000000000000000000000000" +
719            // Server name.
720            "0000000000000000000000000000000000000000000000000000000000000000" +
721            "0000000000000000000000000000000000000000000000000000000000000000" +
722            // File.
723            "0000000000000000000000000000000000000000000000000000000000000000" +
724            "0000000000000000000000000000000000000000000000000000000000000000" +
725            "0000000000000000000000000000000000000000000000000000000000000000" +
726            "0000000000000000000000000000000000000000000000000000000000000000" +
727            // Options.
728            "638253633501023604c00002fe33040000bfc60104fffff00003040a3f50010608c0000201c0000202" +
729            "0f0f646f6d61696e3132332e636f2e756b0000000000ff00000000"));
730
731        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3);
732        assertTrue(offerPacket instanceof DhcpOfferPacket);
733        DhcpResults dhcpResults = offerPacket.toDhcpResults();
734        assertDhcpResults("10.63.93.4/20", "10.63.80.1", "192.0.2.1,192.0.2.2",
735                "domain123.co.uk", "192.0.2.254", null, 49094, false, 0, dhcpResults);
736    }
737
738    @Test
739    public void testBug2136() throws Exception {
740        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
741            // Ethernet header.
742            "bcf5ac000000d0c7890000000800" +
743            // IP header.
744            "4500014c00000000ff119beac3eaf3880a3f5d04" +
745            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
746            "0043004401387574" +
747            // BOOTP header.
748            "0201060163339a3000050000000000000a209ecd0000000000000000" +
749            // MAC address.
750            "bcf5ac00000000000000000000000000" +
751            // Server name.
752            "0000000000000000000000000000000000000000000000000000000000000000" +
753            "0000000000000000000000000000000000000000000000000000000000000000" +
754            // File.
755            "0000000000000000000000000000000000000000000000000000000000000000" +
756            "0000000000000000000000000000000000000000000000000000000000000000" +
757            "0000000000000000000000000000000000000000000000000000000000000000" +
758            "0000000000000000000000000000000000000000000000000000000000000000" +
759            // Options.
760            "6382536335010236040a20ff80330400001c200104fffff00003040a20900106089458413494584135" +
761            "0f0b6c616e63732e61632e756b000000000000000000ff00000000"));
762
763        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
764        assertTrue(offerPacket instanceof DhcpOfferPacket);
765        assertEquals("BCF5AC000000", HexDump.toHexString(offerPacket.getClientMac()));
766        DhcpResults dhcpResults = offerPacket.toDhcpResults();
767        assertDhcpResults("10.32.158.205/20", "10.32.144.1", "148.88.65.52,148.88.65.53",
768                "lancs.ac.uk", "10.32.255.128", null, 7200, false, 0, dhcpResults);
769    }
770
771    @Test
772    public void testUdpServerAnySourcePort() throws Exception {
773        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
774            // Ethernet header.
775            "9cd917000000001c2e0000000800" +
776            // IP header.
777            "45a00148000040003d115087d18194fb0a0f7af2" +
778            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
779            // NOTE: The server source port is not the canonical port 67.
780            "C29F004401341268" +
781            // BOOTP header.
782            "02010600d628ba8200000000000000000a0f7af2000000000a0fc818" +
783            // MAC address.
784            "9cd91700000000000000000000000000" +
785            // Server name.
786            "0000000000000000000000000000000000000000000000000000000000000000" +
787            "0000000000000000000000000000000000000000000000000000000000000000" +
788            // File.
789            "0000000000000000000000000000000000000000000000000000000000000000" +
790            "0000000000000000000000000000000000000000000000000000000000000000" +
791            "0000000000000000000000000000000000000000000000000000000000000000" +
792            "0000000000000000000000000000000000000000000000000000000000000000" +
793            // Options.
794            "6382536335010236040a0169fc3304000151800104ffff000003040a0fc817060cd1818003d1819403" +
795            "d18180060f0777766d2e6564751c040a0fffffff000000"));
796
797        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
798        assertTrue(offerPacket instanceof DhcpOfferPacket);
799        assertEquals("9CD917000000", HexDump.toHexString(offerPacket.getClientMac()));
800        DhcpResults dhcpResults = offerPacket.toDhcpResults();
801        assertDhcpResults("10.15.122.242/16", "10.15.200.23",
802                "209.129.128.3,209.129.148.3,209.129.128.6",
803                "wvm.edu", "10.1.105.252", null, 86400, false, 0, dhcpResults);
804    }
805
806    @Test
807    public void testUdpInvalidDstPort() throws Exception {
808        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
809            // Ethernet header.
810            "9cd917000000001c2e0000000800" +
811            // IP header.
812            "45a00148000040003d115087d18194fb0a0f7af2" +
813            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
814            // NOTE: The destination port is a non-DHCP port.
815            "0043aaaa01341268" +
816            // BOOTP header.
817            "02010600d628ba8200000000000000000a0f7af2000000000a0fc818" +
818            // MAC address.
819            "9cd91700000000000000000000000000" +
820            // Server name.
821            "0000000000000000000000000000000000000000000000000000000000000000" +
822            "0000000000000000000000000000000000000000000000000000000000000000" +
823            // File.
824            "0000000000000000000000000000000000000000000000000000000000000000" +
825            "0000000000000000000000000000000000000000000000000000000000000000" +
826            "0000000000000000000000000000000000000000000000000000000000000000" +
827            "0000000000000000000000000000000000000000000000000000000000000000" +
828            // Options.
829            "6382536335010236040a0169fc3304000151800104ffff000003040a0fc817060cd1818003d1819403" +
830            "d18180060f0777766d2e6564751c040a0fffffff000000"));
831
832        try {
833            DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
834            fail("Packet with invalid dst port did not throw ParseException");
835        } catch (ParseException expected) {}
836    }
837
838    @Test
839    public void testMultipleRouters() throws Exception {
840        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
841            // Ethernet header.
842            "fc3d93000000" + "081735000000" + "0800" +
843            // IP header.
844            "45000148c2370000ff117ac2c0a8bd02ffffffff" +
845            // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation).
846            "0043004401343beb" +
847            // BOOTP header.
848            "0201060027f518e20000800000000000c0a8bd310000000000000000" +
849            // MAC address.
850            "fc3d9300000000000000000000000000" +
851            // Server name.
852            "0000000000000000000000000000000000000000000000000000000000000000" +
853            "0000000000000000000000000000000000000000000000000000000000000000" +
854            // File.
855            "0000000000000000000000000000000000000000000000000000000000000000" +
856            "0000000000000000000000000000000000000000000000000000000000000000" +
857            "0000000000000000000000000000000000000000000000000000000000000000" +
858            "0000000000000000000000000000000000000000000000000000000000000000" +
859            // Options.
860            "638253633501023604c0abbd023304000070803a04000038403b04000062700104ffffff00" +
861            "0308c0a8bd01ffffff0006080808080808080404ff000000000000"));
862
863        DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2);
864        assertTrue(offerPacket instanceof DhcpOfferPacket);
865        assertEquals("FC3D93000000", HexDump.toHexString(offerPacket.getClientMac()));
866        DhcpResults dhcpResults = offerPacket.toDhcpResults();
867        assertDhcpResults("192.168.189.49/24", "192.168.189.1", "8.8.8.8,8.8.4.4",
868                null, "192.171.189.2", null, 28800, false, 0, dhcpResults);
869    }
870
871    @Test
872    public void testDiscoverPacket() throws Exception {
873        short secs = 7;
874        int transactionId = 0xdeadbeef;
875        byte[] hwaddr = {
876                (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a
877        };
878
879        ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
880                DhcpPacket.ENCAP_L2, transactionId, secs, hwaddr,
881                false /* do unicast */, DhcpClient.REQUESTED_PARAMS);
882
883        byte[] headers = new byte[] {
884            // Ethernet header.
885            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
886            (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a,
887            (byte) 0x08, (byte) 0x00,
888            // IP header.
889            (byte) 0x45, (byte) 0x10, (byte) 0x01, (byte) 0x56,
890            (byte) 0x00, (byte) 0x00, (byte) 0x40, (byte) 0x00,
891            (byte) 0x40, (byte) 0x11, (byte) 0x39, (byte) 0x88,
892            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
893            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
894            // UDP header.
895            (byte) 0x00, (byte) 0x44, (byte) 0x00, (byte) 0x43,
896            (byte) 0x01, (byte) 0x42, (byte) 0x6a, (byte) 0x4a,
897            // BOOTP.
898            (byte) 0x01, (byte) 0x01, (byte) 0x06, (byte) 0x00,
899            (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef,
900            (byte) 0x00, (byte) 0x07, (byte) 0x00, (byte) 0x00,
901            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
902            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
903            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
904            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
905            (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b,
906            (byte) 0xb1, (byte) 0x7a
907        };
908        byte[] options = new byte[] {
909            // Magic cookie 0x63825363.
910            (byte) 0x63, (byte) 0x82, (byte) 0x53, (byte) 0x63,
911            // Message type DISCOVER.
912            (byte) 0x35, (byte) 0x01, (byte) 0x01,
913            // Client identifier Ethernet, da:01:19:5b:b1:7a.
914            (byte) 0x3d, (byte) 0x07,
915                    (byte) 0x01,
916                    (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a,
917            // Max message size 1500.
918            (byte) 0x39, (byte) 0x02, (byte) 0x05, (byte) 0xdc,
919            // Version "android-dhcp-???".
920            (byte) 0x3c, (byte) 0x10,
921                    'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 'h', 'c', 'p', '-', '?', '?', '?',
922            // Hostname "android-01234567890abcde"
923            (byte) 0x0c, (byte) 0x18,
924                    'a', 'n', 'd', 'r', 'o', 'i', 'd', '-',
925                    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e',
926            // Requested parameter list.
927            (byte) 0x37, (byte) 0x0a,
928                DHCP_SUBNET_MASK,
929                DHCP_ROUTER,
930                DHCP_DNS_SERVER,
931                DHCP_DOMAIN_NAME,
932                DHCP_MTU,
933                DHCP_BROADCAST_ADDRESS,
934                DHCP_LEASE_TIME,
935                DHCP_RENEWAL_TIME,
936                DHCP_REBINDING_TIME,
937                DHCP_VENDOR_INFO,
938            // End options.
939            (byte) 0xff,
940            // Our packets are always of even length. TODO: find out why and possibly fix it.
941            (byte) 0x00
942        };
943        byte[] expected = new byte[DhcpPacket.MIN_PACKET_LENGTH_L2 + options.length];
944        assertTrue((expected.length & 1) == 0);
945        System.arraycopy(headers, 0, expected, 0, headers.length);
946        System.arraycopy(options, 0, expected, DhcpPacket.MIN_PACKET_LENGTH_L2, options.length);
947
948        byte[] actual = new byte[packet.limit()];
949        packet.get(actual);
950        String msg =
951                "Expected:\n  " + Arrays.toString(expected) +
952                "\nActual:\n  " + Arrays.toString(actual);
953        assertTrue(msg, Arrays.equals(expected, actual));
954    }
955}
956