/*
 * Copyright (C) 2020 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 com.android.net.module.util.netlink;

import static android.system.OsConstants.AF_INET6;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * A NetlinkMessage subclass for RTM_NEWNDUSEROPT messages.
 */
public class NduseroptMessage extends NetlinkMessage {
    public static final int STRUCT_SIZE = 16;

    static final int NDUSEROPT_SRCADDR = 1;

    /** The address family. Presumably always AF_INET6. */
    public final byte family;
    /**
     * The total length in bytes of the options that follow this structure.
     * Actually a 16-bit unsigned integer.
     */
    public final int opts_len;
    /** The interface index on which the options were received. */
    public final int ifindex;
    /** The ICMP type of the packet that contained the options. */
    public final byte icmp_type;
    /** The ICMP code of the packet that contained the options. */
    public final byte icmp_code;

    /**
     * ND option that was in this message.
     * Even though the length field is called "opts_len", the kernel only ever sends one option per
     * message. It is unlikely that this will ever change as it would break existing userspace code.
     * But if it does, we can simply update this code, since userspace is typically newer than the
     * kernel.
     */
    @Nullable
    public final NdOption option;

    /** The IP address that sent the packet containing the option. */
    public final InetAddress srcaddr;

    @VisibleForTesting
    public NduseroptMessage(@NonNull final StructNlMsgHdr header, byte family, int optslen,
            int ifindex, byte icmptype, byte icmpcode, @NonNull final NdOption option,
            final InetAddress srcaddr) {
        super(header);
        this.family = family;
        this.opts_len = optslen;
        this.ifindex = ifindex;
        this.icmp_type = icmptype;
        this.icmp_code = icmpcode;
        this.option = option;
        this.srcaddr = srcaddr;
    }

    NduseroptMessage(@NonNull StructNlMsgHdr header, @NonNull ByteBuffer buf)
            throws UnknownHostException {
        super(header);

        // The structure itself.
        buf.order(ByteOrder.nativeOrder());  // Restored in the finally clause inside parse().
        final int start = buf.position();
        family = buf.get();
        buf.get();  // Skip 1 byte of padding.
        opts_len = Short.toUnsignedInt(buf.getShort());
        ifindex = buf.getInt();
        icmp_type = buf.get();
        icmp_code = buf.get();
        buf.position(buf.position() + 6);  // Skip 6 bytes of padding.

        // The ND option.
        // Ensure we don't read past opts_len even if the option length is invalid.
        // Note that this check is not really necessary since if the option length is not valid,
        // this struct won't be very useful to the caller.
        //
        // It's safer to pass the slice of original ByteBuffer to just parse the ND option field,
        // although parsing ND option might throw exception or return null, it won't break the
        // original ByteBuffer position.
        buf.order(ByteOrder.BIG_ENDIAN);
        try {
            final ByteBuffer slice = buf.slice();
            slice.limit(opts_len);
            option = NdOption.parse(slice);
        } finally {
            // Advance buffer position according to opts_len in the header. ND option length might
            // be incorrect in the malformed packet.
            int newPosition = start + STRUCT_SIZE + opts_len;
            if (newPosition >= buf.limit()) {
                throw new IllegalArgumentException("ND option extends past end of buffer");
            }
            buf.position(newPosition);
        }

        // The source address attribute.
        StructNlAttr nla = StructNlAttr.parse(buf);
        if (nla == null || nla.nla_type != NDUSEROPT_SRCADDR || nla.nla_value == null) {
            throw new IllegalArgumentException("Invalid source address in ND useropt");
        }
        if (family == AF_INET6) {
            // InetAddress.getByAddress only looks at the ifindex if the address type needs one.
            srcaddr = Inet6Address.getByAddress(null /* hostname */, nla.nla_value, ifindex);
        } else {
            srcaddr = InetAddress.getByAddress(nla.nla_value);
        }
    }

    /**
     * Parses a StructNduseroptmsg from a {@link ByteBuffer}.
     *
     * @param header the netlink message header.
     * @param buf The buffer from which to parse the option. The buffer's byte order must be
     *            {@link java.nio.ByteOrder#BIG_ENDIAN}.
     * @return the parsed option, or {@code null} if the option could not be parsed successfully
     *         (for example, if it was truncated, or if the prefix length code was wrong).
     */
    @Nullable
    public static NduseroptMessage parse(@NonNull StructNlMsgHdr header, @NonNull ByteBuffer buf) {
        if (buf == null || buf.remaining() < STRUCT_SIZE) return null;
        ByteOrder oldOrder = buf.order();
        try {
            return new NduseroptMessage(header, buf);
        } catch (IllegalArgumentException | UnknownHostException | BufferUnderflowException e) {
            // Not great, but better than throwing an exception that might crash the caller.
            // Convention in this package is that null indicates that the option was truncated, so
            // callers must already handle it.
            return null;
        } finally {
            buf.order(oldOrder);
        }
    }

    @Override
    public String toString() {
        return String.format("Nduseroptmsg(family:%d, opts_len:%d, ifindex:%d, icmp_type:%d, "
                + "icmp_code:%d, srcaddr: %s, %s)",
                family, opts_len, ifindex, Byte.toUnsignedInt(icmp_type),
                Byte.toUnsignedInt(icmp_code), srcaddr.getHostAddress(), option);
    }
}
