• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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 com.android.server.net;
18 
19 import android.util.Log;
20 
21 import java.io.IOException;
22 import java.nio.BufferUnderflowException;
23 import java.nio.ByteBuffer;
24 import java.util.Arrays;
25 
26 public class HeaderCompressionUtils {
27     private static final String TAG = "L2capHeaderCompressionUtils";
28     private static final int IPV6_HEADER_SIZE = 40;
29 
decodeIpv6Address(ByteBuffer buffer, int mode, boolean isMulticast)30     private static byte[] decodeIpv6Address(ByteBuffer buffer, int mode, boolean isMulticast)
31             throws BufferUnderflowException, IOException {
32         // Mode is equivalent between SAM and DAM; however, isMulticast only applies to DAM.
33         final byte[] address = new byte[16];
34         // If multicast bit is set, mix it in the mode, so that the lower two bits represent the
35         // address mode, and the upper bit represents multicast compression.
36         switch ((isMulticast ? 0b100 : 0) | mode) {
37             case 0b000: // 128 bits. The full address is carried in-line.
38             case 0b100:
39                 buffer.get(address);
40                 break;
41             case 0b001: // 64 bits. The first 64-bits of the fe80:: address are elided.
42                 address[0] = (byte) 0xfe;
43                 address[1] = (byte) 0x80;
44                 buffer.get(address, 8 /*off*/, 8 /*len*/);
45                 break;
46             case 0b010: // 16 bits. fe80::ff:fe00:XXXX, where XXXX are the bits carried in-line
47                 address[0] = (byte) 0xfe;
48                 address[1] = (byte) 0x80;
49                 address[11] = (byte) 0xff;
50                 address[12] = (byte) 0xfe;
51                 buffer.get(address, 14 /*off*/, 2 /*len*/);
52                 break;
53             case 0b011: // 0 bits. The address is fully elided and derived from BLE MAC address
54                 // Note that on Android, the BLE MAC addresses are not exposed via the API;
55                 // therefore, this compression mode cannot be supported.
56                 throw new IOException("Address cannot be fully elided");
57             case 0b101: // 48 bits. The address takes the form ffXX::00XX:XXXX:XXXX.
58                 address[0] = (byte) 0xff;
59                 address[1] = buffer.get();
60                 buffer.get(address, 11 /*off*/, 5 /*len*/);
61                 break;
62             case 0b110: // 32 bits. The address takes the form ffXX::00XX:XXXX
63                 address[0] = (byte) 0xff;
64                 address[1] = buffer.get();
65                 buffer.get(address, 13 /*off*/, 3 /*len*/);
66                 break;
67             case 0b111: // 8 bits. The address takes the form ff02::00XX.
68                 address[0] = (byte) 0xff;
69                 address[1] = (byte) 0x02;
70                 address[15] = buffer.get();
71                 break;
72         }
73         return address;
74     }
75 
76     /**
77      * Performs 6lowpan header decompression in place.
78      *
79      * Note that the passed in buffer must have enough capacity for successful decompression.
80      *
81      * @param bytes The buffer containing the packet.
82      * @param len The size of the packet
83      * @return decompressed size or zero
84      * @throws BufferUnderflowException if an illegal packet is encountered.
85      * @throws IOException if an unsupported option is encountered.
86      */
decompress6lowpan(byte[] bytes, int len)87     public static int decompress6lowpan(byte[] bytes, int len)
88             throws BufferUnderflowException, IOException {
89         // Note that ByteBuffer's default byte order is big endian.
90         final ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
91         inBuffer.limit(len);
92 
93         // LOWPAN_IPHC base encoding:
94         //   0   1   2   3   4   5   6   7 | 8   9  10  11  12  13  14  15
95         // +---+---+---+---+---+---+---+---|---+---+---+---+---+---+---+---+
96         // | 0 | 1 | 1 |  TF   |NH | HLIM  |CID|SAC|  SAM  | M |DAC|  DAM  |
97         // +---+---+---+---+---+---+---+---|---+---+---+---+---+---+---+---+
98         final int iphc1 = inBuffer.get() & 0xff;
99         final int iphc2 = inBuffer.get() & 0xff;
100         // Dispatch must start with 0b011.
101         if ((iphc1 & 0xe0) != 0x60) {
102             throw new IOException("LOWPAN_IPHC does not start with 011");
103         }
104 
105         final int tf = (iphc1 >> 3) & 3;         // Traffic class
106         final boolean nh = (iphc1 & 4) != 0;     // Next header
107         final int hlim = iphc1 & 3;              // Hop limit
108         final boolean cid = (iphc2 & 0x80) != 0; // Context identifier extension
109         final boolean sac = (iphc2 & 0x40) != 0; // Source address compression
110         final int sam = (iphc2 >> 4) & 3;        // Source address mode
111         final boolean m = (iphc2 & 8) != 0;      // Multicast compression
112         final boolean dac = (iphc2 & 4) != 0;    // Destination address compression
113         final int dam = iphc2 & 3;               // Destination address mode
114 
115         final ByteBuffer ipv6Header = ByteBuffer.allocate(IPV6_HEADER_SIZE);
116 
117         final int trafficClass;
118         final int flowLabel;
119         switch (tf) {
120             case 0b00: // ECN + DSCP + 4-bit Pad + Flow Label (4 bytes)
121                 trafficClass = inBuffer.get() & 0xff;
122                 flowLabel = (inBuffer.get() & 0x0f) << 16
123                         | (inBuffer.get() & 0xff) << 8
124                         | (inBuffer.get() & 0xff);
125                 break;
126             case 0b01: // ECN + 2-bit Pad + Flow Label (3 bytes), DSCP is elided.
127                 final int firstByte = inBuffer.get() & 0xff;
128                 //     0     1     2     3     4     5     6     7
129                 // +-----+-----+-----+-----+-----+-----+-----+-----+
130                 // |          DS FIELD, DSCP           | ECN FIELD |
131                 // +-----+-----+-----+-----+-----+-----+-----+-----+
132                 // rfc6282 does not explicitly state what value to use for DSCP, assuming 0.
133                 trafficClass = firstByte >> 6;
134                 flowLabel = (firstByte & 0x0f) << 16
135                         | (inBuffer.get() & 0xff) << 8
136                         | (inBuffer.get() & 0xff);
137                 break;
138             case 0b10: // ECN + DSCP (1 byte), Flow Label is elided.
139                 trafficClass = inBuffer.get() & 0xff;
140                 // rfc6282 does not explicitly state what value to use, assuming 0.
141                 flowLabel = 0;
142                 break;
143             case 0b11: // Traffic Class and Flow Label are elided.
144                 // rfc6282 does not explicitly state what value to use, assuming 0.
145                 trafficClass = 0;
146                 flowLabel = 0;
147                 break;
148             default:
149                 // This cannot happen. Crash if it does.
150                 throw new IllegalStateException("Illegal TF value");
151         }
152 
153         // Write version, traffic class, and flow label
154         final int versionTcFlowLabel = (6 << 28) | (trafficClass << 20) | flowLabel;
155         ipv6Header.putInt(versionTcFlowLabel);
156 
157         // Payload length is still unknown. Use 0 for now.
158         ipv6Header.putShort((short) 0);
159 
160         // We do not use UDP or extension header compression, therefore the next header
161         // cannot be compressed.
162         if (nh) throw new IOException("Next header cannot be compressed");
163         // Write next header
164         ipv6Header.put(inBuffer.get());
165 
166         final byte hopLimit;
167         switch (hlim) {
168             case 0b00: // The Hop Limit field is carried in-line.
169                 hopLimit = inBuffer.get();
170                 break;
171             case 0b01: // The Hop Limit field is compressed and the hop limit is 1.
172                 hopLimit = 1;
173                 break;
174             case 0b10: // The Hop Limit field is compressed and the hop limit is 64.
175                 hopLimit = 64;
176                 break;
177             case 0b11: // The Hop Limit field is compressed and the hop limit is 255.
178                 hopLimit = (byte) 255;
179                 break;
180             default:
181                 // This cannot happen. Crash if it does.
182                 throw new IllegalStateException("Illegal HLIM value");
183         }
184         ipv6Header.put(hopLimit);
185 
186         if (cid) throw new IOException("Context based compression not supported");
187         if (sac) throw new IOException("Context based compression not supported");
188         if (dac) throw new IOException("Context based compression not supported");
189 
190         // Write source address
191         ipv6Header.put(decodeIpv6Address(inBuffer, sam, false /* isMulticast */));
192 
193         // Write destination address
194         ipv6Header.put(decodeIpv6Address(inBuffer, dam, m));
195 
196         // Go back and fix up payloadLength
197         final short payloadLength = (short) inBuffer.remaining();
198         ipv6Header.putShort(4, payloadLength);
199 
200         // Done! Check that 40 bytes were written.
201         if (ipv6Header.position() != IPV6_HEADER_SIZE) {
202             // This indicates a bug in our code -> crash.
203             throw new IllegalStateException("Faulty decompression wrote less than 40 bytes");
204         }
205 
206         // Ensure there is enough room in the buffer
207         final int packetLength = payloadLength + IPV6_HEADER_SIZE;
208         if (bytes.length < packetLength) {
209             throw new IOException("Decompressed packet exceeds buffer size");
210         }
211 
212         // Move payload bytes back to make room for the header
213         inBuffer.limit(packetLength);
214         System.arraycopy(bytes, inBuffer.position(), bytes, IPV6_HEADER_SIZE, payloadLength);
215         // Copy IPv6 header to the beginning of the buffer.
216         inBuffer.position(0);
217         ipv6Header.flip();
218         inBuffer.put(ipv6Header);
219 
220         return packetLength;
221     }
222 
223     /**
224      * Performs 6lowpan header compression in place.
225      *
226      * @param bytes The buffer containing the packet.
227      * @param len The size of the packet
228      * @return compressed size or zero
229      * @throws BufferUnderflowException if an illegal packet is encountered.
230      * @throws IOException if an unsupported option is encountered.
231      */
compress6lowpan(byte[] bytes, final int len)232     public static int compress6lowpan(byte[] bytes, final int len)
233             throws BufferUnderflowException, IOException {
234         // Compression only happens on egress, i.e. the packet is read from the tun fd.
235         // This means that this code can be a bit more lenient.
236         if (len < 40) {
237             Log.wtf(TAG, "Encountered short (<40 byte) packet");
238             return 0;
239         }
240 
241         // Note that ByteBuffer's default byte order is big endian.
242         final ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
243         inBuffer.limit(len);
244 
245         // Check that the packet is an IPv6 packet
246         final int versionTcFlowLabel = inBuffer.getInt() & 0xffffffff;
247         if ((versionTcFlowLabel >> 28) != 6) {
248             return 0;
249         }
250 
251         // Check that the payload length matches the packet length - 40.
252         int payloadLength = inBuffer.getShort();
253         if (payloadLength != len - IPV6_HEADER_SIZE) {
254             throw new IOException("Encountered packet with payload length mismatch");
255         }
256 
257         // Implements rfc 6282 6lowpan header compression using iphc 0110 0000 0000 0000 (all
258         // fields are carried inline).
259         inBuffer.position(0);
260         inBuffer.put((byte) 0x60);
261         inBuffer.put((byte) 0x00);
262         final byte trafficClass = (byte) ((versionTcFlowLabel >> 20) & 0xff);
263         inBuffer.put(trafficClass);
264         final byte flowLabelMsb = (byte) ((versionTcFlowLabel >> 16) & 0x0f);
265         final short flowLabelLsb = (short) (versionTcFlowLabel & 0xffff);
266         inBuffer.put(flowLabelMsb);
267         // Note: the next putShort overrides the payload length. This is WAI as the payload length
268         // is reconstructed via L2CAP packet length.
269         inBuffer.putShort(flowLabelLsb);
270 
271         // Since the iphc (2 bytes) matches the payload length that was elided (2 bytes), the length
272         // of the packet did not change.
273         return len;
274     }
275 }
276