/*
 * Copyright (C) 2017 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.server.connectivity;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.net.ConnectivityManager;
import android.net.IDnsResolver;
import android.net.INetd;
import android.net.InterfaceConfiguration;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.NetworkInfo;
import android.net.NetworkMisc;
import android.os.Handler;
import android.os.INetworkManagementService;
import android.os.test.TestLooper;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.server.ConnectivityService;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class Nat464XlatTest {

    static final String BASE_IFACE = "test0";
    static final String STACKED_IFACE = "v4-test0";
    static final LinkAddress ADDR = new LinkAddress("192.0.2.5/29");
    static final String NAT64_PREFIX = "64:ff9b::/96";
    static final int NETID = 42;

    @Mock ConnectivityService mConnectivity;
    @Mock NetworkMisc mMisc;
    @Mock IDnsResolver mDnsResolver;
    @Mock INetd mNetd;
    @Mock INetworkManagementService mNms;
    @Mock InterfaceConfiguration mConfig;
    @Mock NetworkAgentInfo mNai;

    TestLooper mLooper;
    Handler mHandler;

    Nat464Xlat makeNat464Xlat() {
        return new Nat464Xlat(mNai, mNetd, mDnsResolver, mNms) {
            @Override protected int getNetId() {
                return NETID;
            }
        };
    }

    @Before
    public void setUp() throws Exception {
        mLooper = new TestLooper();
        mHandler = new Handler(mLooper.getLooper());

        MockitoAnnotations.initMocks(this);

        mNai.linkProperties = new LinkProperties();
        mNai.linkProperties.setInterfaceName(BASE_IFACE);
        mNai.networkInfo = new NetworkInfo(null);
        mNai.networkInfo.setType(ConnectivityManager.TYPE_WIFI);
        when(mNai.connService()).thenReturn(mConnectivity);
        when(mNai.netMisc()).thenReturn(mMisc);
        when(mNai.handler()).thenReturn(mHandler);

        when(mNms.getInterfaceConfig(eq(STACKED_IFACE))).thenReturn(mConfig);
        when(mConfig.getLinkAddress()).thenReturn(ADDR);
    }

    private void assertRequiresClat(boolean expected, NetworkAgentInfo nai) {
        String msg = String.format("requiresClat expected %b for type=%d state=%s skip=%b "
                + "nat64Prefix=%s addresses=%s", expected, nai.networkInfo.getType(),
                nai.networkInfo.getDetailedState(),
                mMisc.skip464xlat, nai.linkProperties.getNat64Prefix(),
                nai.linkProperties.getLinkAddresses());
        assertEquals(msg, expected, Nat464Xlat.requiresClat(nai));
    }

    private void assertShouldStartClat(boolean expected, NetworkAgentInfo nai) {
        String msg = String.format("shouldStartClat expected %b for type=%d state=%s skip=%b "
                + "nat64Prefix=%s addresses=%s", expected, nai.networkInfo.getType(),
                nai.networkInfo.getDetailedState(),
                mMisc.skip464xlat, nai.linkProperties.getNat64Prefix(),
                nai.linkProperties.getLinkAddresses());
        assertEquals(msg, expected, Nat464Xlat.shouldStartClat(nai));
    }

    @Test
    public void testRequiresClat() throws Exception {
        final int[] supportedTypes = {
            ConnectivityManager.TYPE_MOBILE,
            ConnectivityManager.TYPE_WIFI,
            ConnectivityManager.TYPE_ETHERNET,
        };

        // NetworkInfo doesn't allow setting the State directly, but rather
        // requires setting DetailedState in order set State as a side-effect.
        final NetworkInfo.DetailedState[] supportedDetailedStates = {
            NetworkInfo.DetailedState.CONNECTED,
            NetworkInfo.DetailedState.SUSPENDED,
        };

        LinkProperties oldLp = new LinkProperties(mNai.linkProperties);
        for (int type : supportedTypes) {
            mNai.networkInfo.setType(type);
            for (NetworkInfo.DetailedState state : supportedDetailedStates) {
                mNai.networkInfo.setDetailedState(state, "reason", "extraInfo");

                mNai.linkProperties.setNat64Prefix(new IpPrefix("2001:db8:0:64::/96"));
                assertRequiresClat(false, mNai);
                assertShouldStartClat(false, mNai);

                mNai.linkProperties.addLinkAddress(new LinkAddress("fc00::1/64"));
                assertRequiresClat(false, mNai);
                assertShouldStartClat(false, mNai);

                mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
                assertRequiresClat(true, mNai);
                assertShouldStartClat(true, mNai);

                mMisc.skip464xlat = true;
                assertRequiresClat(false, mNai);
                assertShouldStartClat(false, mNai);

                mMisc.skip464xlat = false;
                assertRequiresClat(true, mNai);
                assertShouldStartClat(true, mNai);

                mNai.linkProperties.addLinkAddress(new LinkAddress("192.0.2.2/24"));
                assertRequiresClat(false, mNai);
                assertShouldStartClat(false, mNai);

                mNai.linkProperties.removeLinkAddress(new LinkAddress("192.0.2.2/24"));
                assertRequiresClat(true, mNai);
                assertShouldStartClat(true, mNai);

                mNai.linkProperties.setNat64Prefix(null);
                assertRequiresClat(true, mNai);
                assertShouldStartClat(false, mNai);

                mNai.linkProperties = new LinkProperties(oldLp);
            }
        }
    }

    @Test
    public void testNormalStartAndStop() throws Exception {
        Nat464Xlat nat = makeNat464Xlat();
        ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);

        nat.setNat64Prefix(new IpPrefix(NAT64_PREFIX));

        // Start clat.
        nat.start();

        verify(mNms).registerObserver(eq(nat));
        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));

        // Stacked interface up notification arrives.
        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
        mLooper.dispatchNext();

        verify(mNms).getInterfaceConfig(eq(STACKED_IFACE));
        verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
        assertFalse(c.getValue().getStackedLinks().isEmpty());
        assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
        assertRunning(nat);

        // Stop clat (Network disconnects, IPv4 addr appears, ...).
        nat.stop();

        verify(mNetd).clatdStop(eq(BASE_IFACE));
        verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
        verify(mNms).unregisterObserver(eq(nat));
        assertTrue(c.getValue().getStackedLinks().isEmpty());
        assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
        verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
        assertIdle(nat);

        // Stacked interface removed notification arrives and is ignored.
        nat.interfaceRemoved(STACKED_IFACE);
        mLooper.dispatchNext();

        verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
    }

    private void checkStartStopStart(boolean interfaceRemovedFirst) throws Exception {
        Nat464Xlat nat = makeNat464Xlat();
        ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
        InOrder inOrder = inOrder(mNetd, mConnectivity);

        nat.setNat64Prefix(new IpPrefix(NAT64_PREFIX));

        nat.start();

        inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));

        // Stacked interface up notification arrives.
        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
        mLooper.dispatchNext();

        inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
        assertFalse(c.getValue().getStackedLinks().isEmpty());
        assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
        assertRunning(nat);

        // ConnectivityService stops clat (Network disconnects, IPv4 addr appears, ...).
        nat.stop();

        inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE));

        inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
        assertTrue(c.getValue().getStackedLinks().isEmpty());
        assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
        assertIdle(nat);

        if (interfaceRemovedFirst) {
            // Stacked interface removed notification arrives and is ignored.
            nat.interfaceRemoved(STACKED_IFACE);
            mLooper.dispatchNext();
            nat.interfaceLinkStateChanged(STACKED_IFACE, false);
            mLooper.dispatchNext();
        }

        assertTrue(c.getValue().getStackedLinks().isEmpty());
        assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
        assertIdle(nat);
        inOrder.verifyNoMoreInteractions();

        nat.start();

        inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));

        if (!interfaceRemovedFirst) {
            // Stacked interface removed notification arrives and is ignored.
            nat.interfaceRemoved(STACKED_IFACE);
            mLooper.dispatchNext();
            nat.interfaceLinkStateChanged(STACKED_IFACE, false);
            mLooper.dispatchNext();
        }

        // Stacked interface up notification arrives.
        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
        mLooper.dispatchNext();

        inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
        assertFalse(c.getValue().getStackedLinks().isEmpty());
        assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
        assertRunning(nat);

        // ConnectivityService stops clat again.
        nat.stop();

        inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE));

        inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
        assertTrue(c.getValue().getStackedLinks().isEmpty());
        assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
        assertIdle(nat);

        inOrder.verifyNoMoreInteractions();
    }

    @Test
    public void testStartStopStart() throws Exception {
        checkStartStopStart(true);
    }

    @Test
    public void testStartStopStartBeforeInterfaceRemoved() throws Exception {
        checkStartStopStart(false);
    }

    @Test
    public void testClatdCrashWhileRunning() throws Exception {
        Nat464Xlat nat = makeNat464Xlat();
        ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);

        nat.setNat64Prefix(new IpPrefix(NAT64_PREFIX));

        nat.start();

        verify(mNms).registerObserver(eq(nat));
        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));

        // Stacked interface up notification arrives.
        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
        mLooper.dispatchNext();

        verify(mNms).getInterfaceConfig(eq(STACKED_IFACE));
        verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
        assertFalse(c.getValue().getStackedLinks().isEmpty());
        assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
        assertRunning(nat);

        // Stacked interface removed notification arrives (clatd crashed, ...).
        nat.interfaceRemoved(STACKED_IFACE);
        mLooper.dispatchNext();

        verify(mNetd).clatdStop(eq(BASE_IFACE));
        verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
        verify(mNms).unregisterObserver(eq(nat));
        verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
        assertTrue(c.getValue().getStackedLinks().isEmpty());
        assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
        assertIdle(nat);

        // ConnectivityService stops clat: no-op.
        nat.stop();

        verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
    }

    @Test
    public void testStopBeforeClatdStarts() throws Exception {
        Nat464Xlat nat = makeNat464Xlat();

        nat.setNat64Prefix(new IpPrefix(NAT64_PREFIX));

        nat.start();

        verify(mNms).registerObserver(eq(nat));
        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));

        // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
        nat.stop();

        verify(mNetd).clatdStop(eq(BASE_IFACE));
        verify(mNms).unregisterObserver(eq(nat));
        verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
        assertIdle(nat);

        // In-flight interface up notification arrives: no-op
        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
        mLooper.dispatchNext();

        // Interface removed notification arrives after stopClatd() takes effect: no-op.
        nat.interfaceRemoved(STACKED_IFACE);
        mLooper.dispatchNext();

        assertIdle(nat);

        verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
    }

    @Test
    public void testStopAndClatdNeverStarts() throws Exception {
        Nat464Xlat nat = makeNat464Xlat();

        nat.setNat64Prefix(new IpPrefix(NAT64_PREFIX));

        nat.start();

        verify(mNms).registerObserver(eq(nat));
        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));

        // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
        nat.stop();

        verify(mNetd).clatdStop(eq(BASE_IFACE));
        verify(mNms).unregisterObserver(eq(nat));
        verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
        assertIdle(nat);

        verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
    }

    static void assertIdle(Nat464Xlat nat) {
        assertTrue("Nat464Xlat was not IDLE", !nat.isStarted());
    }

    static void assertRunning(Nat464Xlat nat) {
        assertTrue("Nat464Xlat was not RUNNING", nat.isRunning());
    }
}
