/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.net.ipsec.ike; import android.annotation.IntDef; import android.annotation.NonNull; import android.net.InetAddresses; import android.net.ipsec.ike.exceptions.InvalidSyntaxException; import android.os.PersistableBundle; import android.util.ArraySet; import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.util.Objects; /** * IkeTrafficSelector represents a Traffic Selector of a Child Session. * *
Traffic Selectors specify addresses that are acceptable within the IPsec SA. * *
Callers can propose {@link IkeTrafficSelector}s when building a {@link ChildSessionParams} and
 * receive the negotiated {@link IkeTrafficSelector}s via a {@link ChildSessionConfiguration}.
 *
 * @see RFC 7296, Internet Key Exchange
 *     Protocol Version 2 (IKEv2)
 */
public final class IkeTrafficSelector {
    // IpProtocolId consists of standard IP Protocol IDs.
    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({IP_PROTOCOL_ID_UNSPEC, IP_PROTOCOL_ID_ICMP, IP_PROTOCOL_ID_TCP, IP_PROTOCOL_ID_UDP})
    public @interface IpProtocolId {}
    // Zero value is re-defined by IKE to indicate that all IP protocols are acceptable.
    /** @hide */
    @VisibleForTesting static final int IP_PROTOCOL_ID_UNSPEC = 0;
    /** @hide */
    @VisibleForTesting static final int IP_PROTOCOL_ID_ICMP = 1;
    /** @hide */
    @VisibleForTesting static final int IP_PROTOCOL_ID_TCP = 6;
    /** @hide */
    @VisibleForTesting static final int IP_PROTOCOL_ID_UDP = 17;
    private static final ArraySet Android platform does not support port-based routing. The port range negotiation is only
     * informational.
     *
     * @param startPort the smallest port number allowed by this Traffic Selector.
     * @param endPort the largest port number allowed by this Traffic Selector.
     * @param startingAddress the smallest address included in this Traffic Selector.
     * @param endingAddress the largest address included in this Traffic Selector.
     */
    public IkeTrafficSelector(
            int startPort,
            int endPort,
            @NonNull InetAddress startingAddress,
            @NonNull InetAddress endingAddress) {
        this(getTsType(startingAddress), startPort, endPort, startingAddress, endingAddress);
    }
    private static int getTsType(InetAddress address) {
        if (address instanceof Inet4Address) {
            return TRAFFIC_SELECTOR_TYPE_IPV4_ADDR_RANGE;
        }
        return TRAFFIC_SELECTOR_TYPE_IPV6_ADDR_RANGE;
    }
    /**
     * Construct an instance of IkeTrafficSelector for building an outbound IKE message.
     *
     * @param tsType the Traffic Selector type.
     * @param startPort the smallest port number allowed by this Traffic Selector.
     * @param endPort the largest port number allowed by this Traffic Selector.
     * @param startingAddress the smallest address included in this Traffic Selector.
     * @param endingAddress the largest address included in this Traffic Selector.
     * @hide
     */
    public IkeTrafficSelector(
            @TrafficSelectorType int tsType,
            int startPort,
            int endPort,
            InetAddress startingAddress,
            InetAddress endingAddress) {
        this.tsType = tsType;
        this.ipProtocolId = IP_PROTOCOL_ID_UNSPEC;
        switch (tsType) {
            case TRAFFIC_SELECTOR_TYPE_IPV4_ADDR_RANGE:
                this.selectorLength = TRAFFIC_SELECTOR_IPV4_LEN;
                if (!(startingAddress instanceof Inet4Address)
                        || !(endingAddress instanceof Inet4Address)) {
                    throw new IllegalArgumentException(
                            "Invalid address range: TS_IPV4_ADDR_RANGE requires IPv4 addresses.");
                }
                break;
            case TRAFFIC_SELECTOR_TYPE_IPV6_ADDR_RANGE:
                this.selectorLength = TRAFFIC_SELECTOR_IPV6_LEN;
                if (!(startingAddress instanceof Inet6Address)
                        || !(endingAddress instanceof Inet6Address)) {
                    throw new IllegalArgumentException(
                            "Invalid address range: TS_IPV6_ADDR_RANGE requires IPv6 addresses.");
                }
                break;
            default:
                throw new IllegalArgumentException("Unrecognized Traffic Selector type.");
        }
        if (compareInetAddressTo(startingAddress, endingAddress) > 0) {
            throw new IllegalArgumentException("Received invalid address range.");
        }
        if (!isPortRangeValid(startPort, endPort)) {
            throw new IllegalArgumentException(
                    "Invalid port range. startPort: " + startPort + " endPort: " + endPort);
        }
        this.startPort = startPort;
        this.endPort = endPort;
        this.startingAddress = startingAddress;
        this.endingAddress = endingAddress;
    }
    /**
     * Constructs this object by deserializing a PersistableBundle
     *
     * @hide
     */
    @NonNull
    public static IkeTrafficSelector fromPersistableBundle(@NonNull PersistableBundle in) {
        Objects.requireNonNull(in, "PersistableBundle not provided");
        int startPort = in.getInt(START_PORT_KEY);
        int endPort = in.getInt(END_PORT_KEY);
        InetAddress startingAddress =
                InetAddresses.parseNumericAddress(in.getString(START_ADDRESS_KEY));
        Objects.requireNonNull(in, "startAddress not provided");
        InetAddress endingAddress =
                InetAddresses.parseNumericAddress(in.getString(END_ADDRESS_KEY));
        Objects.requireNonNull(in, "endAddress not provided");
        return new IkeTrafficSelector(startPort, endPort, startingAddress, endingAddress);
    }
    /**
     * Serializes this object to a PersistableBundle
     *
     * @hide
     */
    @NonNull
    public PersistableBundle toPersistableBundle() {
        final PersistableBundle result = new PersistableBundle();
        result.putInt(START_PORT_KEY, startPort);
        result.putInt(END_PORT_KEY, endPort);
        result.putString(START_ADDRESS_KEY, startingAddress.getHostAddress());
        result.putString(END_ADDRESS_KEY, endingAddress.getHostAddress());
        return result;
    }
    /**
     * Decode IkeTrafficSelectors from inbound Traffic Selector Payload.
     *
     *  This method is only called by IkeTsPayload when decoding inbound IKE message.
     *
     * @param numTs number or Traffic Selectors
     * @param tsBytes encoded byte array of Traffic Selectors
     * @return an array of decoded IkeTrafficSelectors
     * @throws InvalidSyntaxException if received bytes are malformed.
     * @hide
     */
    public static IkeTrafficSelector[] decodeIkeTrafficSelectors(int numTs, byte[] tsBytes)
            throws InvalidSyntaxException {
        IkeTrafficSelector[] tsArray = new IkeTrafficSelector[numTs];
        ByteBuffer inputBuffer = ByteBuffer.wrap(tsBytes);
        try {
            for (int i = 0; i < numTs; i++) {
                int tsType = Byte.toUnsignedInt(inputBuffer.get());
                switch (tsType) {
                    case TRAFFIC_SELECTOR_TYPE_IPV4_ADDR_RANGE:
                        tsArray[i] = decodeTrafficSelector(inputBuffer,
                                TRAFFIC_SELECTOR_TYPE_IPV4_ADDR_RANGE);
                        break;
                    case TRAFFIC_SELECTOR_TYPE_IPV6_ADDR_RANGE:
                        tsArray[i] = decodeTrafficSelector(inputBuffer,
                                TRAFFIC_SELECTOR_TYPE_IPV6_ADDR_RANGE);
                        break;
                    default:
                        throw new InvalidSyntaxException(
                                "Invalid Traffic Selector type: " + tsType);
                }
            }
        } catch (BufferOverflowException e) {
            // Throw exception if any Traffic Selector has invalid length.
            throw new InvalidSyntaxException(e);
        }
        if (inputBuffer.remaining() != 0) {
            throw new InvalidSyntaxException(
                    "Unexpected trailing characters of Traffic Selectors.");
        }
        return tsArray;
    }
    // Decode Traffic Selector from a ByteBuffer. A BufferOverflowException will be thrown and
    // caught by method caller if operation reaches the input ByteBuffer's limit.
    private static IkeTrafficSelector decodeTrafficSelector(ByteBuffer inputBuffer, int tsType)
            throws InvalidSyntaxException {
        // Decode and validate IP Protocol ID
        int ipProtocolId = Byte.toUnsignedInt(inputBuffer.get());
        if (!IP_PROTOCOL_ID_SET.contains(ipProtocolId)) {
            throw new InvalidSyntaxException("Invalid IP Protocol ID.");
        }
        // Decode and validate Selector Length
        boolean isTsIpv4 = tsType == TRAFFIC_SELECTOR_TYPE_IPV4_ADDR_RANGE;
        int expectedTsLen = isTsIpv4 ? TRAFFIC_SELECTOR_IPV4_LEN : TRAFFIC_SELECTOR_IPV6_LEN;
        int tsLength = Short.toUnsignedInt(inputBuffer.getShort());
        if (expectedTsLen != tsLength) {
            throw new InvalidSyntaxException("Invalid Traffic Selector Length.");
        }
        // Decode and validate ports
        int startPort = Short.toUnsignedInt(inputBuffer.getShort());
        int endPort = Short.toUnsignedInt(inputBuffer.getShort());
        if (!isPortRangeValid(startPort, endPort)) {
            throw new InvalidSyntaxException(
                    "Received invalid port range. startPort: "
                            + startPort
                            + " endPort: "
                            + endPort);
        }
        // Decode and validate IP addresses
        int expectedAddrLen = isTsIpv4 ? IPV4_ADDR_LEN : IPV6_ADDR_LEN;
        byte[] startAddressBytes = new byte[expectedAddrLen];
        byte[] endAddressBytes = new byte[expectedAddrLen];
        inputBuffer.get(startAddressBytes);
        inputBuffer.get(endAddressBytes);
        try {
            InetAddress startAddress = InetAddress.getByAddress(startAddressBytes);
            InetAddress endAddress = InetAddress.getByAddress(endAddressBytes);
            boolean isStartAddrIpv4 = startAddress instanceof Inet4Address;
            boolean isEndAddrIpv4 = endAddress instanceof Inet4Address;
            if (isTsIpv4 != isStartAddrIpv4 || isTsIpv4 != isEndAddrIpv4) {
                throw new InvalidSyntaxException("Invalid IP address family");
            }
            // Validate address range.
            if (compareInetAddressTo(startAddress, endAddress) > 0) {
                throw new InvalidSyntaxException("Received invalid IP address range.");
            }
            return new IkeTrafficSelector(
                    tsType,
                    ipProtocolId,
                    expectedTsLen,
                    startPort,
                    endPort,
                    startAddress,
                    endAddress);
        } catch (ClassCastException | UnknownHostException | IllegalArgumentException e) {
            throw new InvalidSyntaxException(e);
        }
    }
    // Validate port range.
    private static boolean isPortRangeValid(int startPort, int endPort) {
        return (startPort >= PORT_NUMBER_MIN
                && startPort <= PORT_NUMBER_MAX
                && endPort >= PORT_NUMBER_MIN
                && endPort <= PORT_NUMBER_MAX
                && startPort <= endPort);
    }
    // Compare two InetAddresses. Return -1 if the first input is smaller; 1 if the second input is
    // smaller; 0 if two addresses are equal.
    // TODO: Consider moving it to the platform code in the future./
    private static int compareInetAddressTo(InetAddress leftAddress, InetAddress rightAddress) {
        byte[] leftAddrBytes = leftAddress.getAddress();
        byte[] rightAddrBytes = rightAddress.getAddress();
        if (leftAddrBytes.length != rightAddrBytes.length) {
            throw new IllegalArgumentException("Two addresses are different types.");
        }
        for (int i = 0; i < leftAddrBytes.length; i++) {
            int unsignedByteLeft = Byte.toUnsignedInt(leftAddrBytes[i]);
            int unsignedByteRight = Byte.toUnsignedInt(rightAddrBytes[i]);
            int result = Integer.compare(unsignedByteLeft, unsignedByteRight);
            if (result != 0) return result;
        }
        return 0;
    }
    /**
     * Check if the input IkeTrafficSelector is a subset of this instance.
     *
     * @param ts the provided IkeTrafficSelector to check.
     * @return true if the input IkeTrafficSelector is a subset of this instance, otherwise false.
     * @hide
     */
    public boolean contains(IkeTrafficSelector ts) {
        if (tsType == ts.tsType
                && ipProtocolId == ts.ipProtocolId
                && startPort <= ts.startPort
                && endPort >= ts.endPort
                && compareInetAddressTo(startingAddress, ts.startingAddress) <= 0
                && compareInetAddressTo(endingAddress, ts.endingAddress) >= 0) {
            return true;
        }
        return false;
    }
    /** @hide */
    @Override
    public int hashCode() {
        return Objects.hash(
                tsType,
                ipProtocolId,
                selectorLength,
                startPort,
                endPort,
                startingAddress,
                endingAddress);
    }
    /** @hide */
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof IkeTrafficSelector)) return false;
        IkeTrafficSelector other = (IkeTrafficSelector) o;
        if (tsType != other.tsType
                || ipProtocolId != other.ipProtocolId
                || startPort != other.startPort
                || endPort != other.endPort) {
            return false;
        }
        switch (tsType) {
            case TRAFFIC_SELECTOR_TYPE_IPV4_ADDR_RANGE:
                return (((Inet4Address) startingAddress)
                                .equals((Inet4Address) other.startingAddress)
                        && ((Inet4Address) endingAddress)
                                .equals((Inet4Address) other.endingAddress));
            case TRAFFIC_SELECTOR_TYPE_IPV6_ADDR_RANGE:
                return (((Inet6Address) startingAddress)
                                .equals((Inet6Address) other.startingAddress)
                        && ((Inet6Address) endingAddress)
                                .equals((Inet6Address) other.endingAddress));
            default:
                throw new UnsupportedOperationException("Unrecognized TS type");
        }
    }
    /**
     * Encode traffic selector to ByteBuffer.
     *
     *  This method will be only called by IkeTsPayload for building an outbound IKE message.
     *
     * @param byteBuffer destination ByteBuffer that stores encoded traffic selector.
     * @hide
     */
    public void encodeToByteBuffer(ByteBuffer byteBuffer) {
        byteBuffer
                .put((byte) tsType)
                .put((byte) ipProtocolId)
                .putShort((short) selectorLength)
                .putShort((short) startPort)
                .putShort((short) endPort)
                .put(startingAddress.getAddress())
                .put(endingAddress.getAddress());
    }
}