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