diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp index 93a6d916dd..82b7413fbf 100644 --- a/tests/cts/net/Android.bp +++ b/tests/cts/net/Android.bp @@ -39,6 +39,7 @@ java_defaults { static_libs: [ "FrameworksNetCommonTests", + "TestNetworkStackLib", "core-tests-support", "compatibility-device-util-axt", "cts-net-utils", diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionDigitalSignatureTest.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionDigitalSignatureTest.java new file mode 100644 index 0000000000..9be1dc72cf --- /dev/null +++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionDigitalSignatureTest.java @@ -0,0 +1,211 @@ +/* + * 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 android.net.ipsec.ike.cts; + +import android.net.InetAddresses; +import android.net.LinkAddress; +import android.net.ipsec.ike.IkeDerAsn1DnIdentification; +import android.net.ipsec.ike.IkeSession; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.IkeTrafficSelector; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.net.ipsec.ike.testutils.CertUtils; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.util.ArrayList; +import java.util.Arrays; + +import javax.security.auth.x500.X500Principal; + +/** + * Explicitly test setting up transport mode Child SA so that devices do not have + * FEATURE_IPSEC_TUNNELS will be test covered. Tunnel mode Child SA setup has been tested in + * IkeSessionPskTest and authentication method is orthogonal to Child mode. + */ +@RunWith(AndroidJUnit4.class) +public class IkeSessionDigitalSignatureTest extends IkeSessionTestBase { + private static final int EXPECTED_AUTH_REQ_FRAG_COUNT = 3; + + private static final String IKE_INIT_RESP = + "46B8ECA1E0D72A18BF3FA1C2CB1EE86F21202220000000000000015022000030" + + "0000002C010100040300000C0100000C800E0100030000080300000503000008" + + "0200000400000008040000022800008800020000328451C8A976CE69E407966A" + + "50D7320C4197A15A07267CE1B16BAFF9BDBBDEC1FDCDAAF7175ADF9AA8DB55DB" + + "2D70C012D01D914C4EDEF6E8B226868EA1D01B2ED0C4C5C86E6BFE566010EC0C" + + "33BA1C93666430B88BDA0470D82CC4F4416F49E3E361E3017C9F27811A66718B" + + "389E1800915D776D59AA528A7E1D1B7815D35144290000249FE8FABE7F43D917" + + "CE370DE2FD9C22BBC082951AC26C1BA26DE795470F2C25BC2900001C00004004" + + "AE388EC86D6D1A470D44142D01AB2E85A7AC14182900001C0000400544A235A4" + + "171C884286B170F48FFC181DB428D87D290000080000402E290000100000402F" + + "00020003000400050000000800004014"; + private static final String IKE_AUTH_RESP_FRAG_1 = + "46B8ECA1E0D72A18BF3FA1C2CB1EE86F3520232000000001000004E0240004C4" + + "00010002DF6750A2D1D5675006F9F6230BB886FFD20CFB973FD04963CFD7A528" + + "560598C58CC44178B2FCBBBBB271387AC81A664B7E7F1055B912F8C686E287C9" + + "D31684C66339151AB86DA3CF1DA664052FA97687634558A1E9E6B37E16A86BD1" + + "68D76DA5E2E1E0B7E98EB662D80D542307015D2BF134EBBBE425D6954FE8C2C4" + + "D31D16C16AA0521C3C481F873ECF25BB8B05AC6083775C1821CAAB1E35A3955D" + + "85ACC599574142E1DD5B262D6E5365CBF6EBE92FFCC16BC29EC3239456F3B202" + + "492551C0F6D752ADCCA56D506D50CC8809EF6BC56EAD005586F7168F76445FD3" + + "1366CC62D32C0C19B28210B8F813F97CD6A447C3857EFD6EC483DDA8ACD9870E" + + "5A21B9C66F0FA44496C0C3D05E8859A1A4CFC88155D0C411BABC13033DD41FA4" + + "AF08CE7734A146687F374F95634D1F26843203CA1FFD05CA3EB150CEA02FBF14" + + "712B7A1C9BC7616A086E7FCA059E7D64EFF98DB895B32F8F7002762AF7D12F23" + + "31E9DD25174C4CE273E5392BBB48F50B7A3E0187181216265F6A4FC7B91BE0AB" + + "C601A580149D4B07411AE99DDB1944B977E86ADC9746605C60A92B569EEFAFFC" + + "3A888D187B75D8F13249689FC28EBCD62B5E03AF171F3A561F0DEA3B1A75F531" + + "971157DCE1E7BC6E7789FF3E8156015BC9C521EFE48996B41471D33BF09864E4" + + "2436E8D7EB6218CDE7716DA754A924B123A63E25585BF27F4AC043A0C4AECE38" + + "BB59DD62F5C0EC657206A76CED1BD26262237DA1CA6815435992A825758DEBEC" + + "DDF598A22B8242AC4E34E70704DBA7B7B73DC3E067C1C98764F8791F84C99156" + + "947D1FFC875F36FCE24B89369C1B5BF1D4C999DCA28E72A528D0E0163C66C067" + + "E71B5E0025C13DA93313942F9EDA230B3ADC254821A4CB1A5DC9D0C5F4DC4E8E" + + "CE46B7B8C72D3C5923C9B30DF1EF7B4EDEDA8BD05C86CA0162AE1BF8F277878E" + + "607401BAA8F06E3EA873FA4C137324C4E0699277CDF649FE7F0F01945EE25FA7" + + "0E4A89737E58185B11B4CB52FD5B0497D3E3CD1CEE7B1FBB3E969DB6F4C324A1" + + "32DC6A0EA21F41332435FD99140C286F8ABBBA926953ADBEED17D30AAD953909" + + "1347EF6D87163D6B1FF32D8B11FFB2E69FAEE7FE913D3826FBA7F9D11E0E3C57" + + "27625B37D213710B5DD8965DAEFD3F491E8C029E2BF361039949BADEC31D60AC" + + "355F26EE41339C03CC9D9B01C3C7F288F0E9D6DFEE78231BDA9AC10FED135913" + + "2836B1A17CE060742B7E5B738A7177CCD59F70337BA251409C377A0FA5333204" + + "D8622BA8C06DE0BEF4F32B6D4D77BE9DE977445D8A2A08C5C38341CB7974FBFB" + + "22C8F983A7D6CEF068DDB2281E6673453521C831C1826861005AE5F37649BC64" + + "0A6360B23284861441A440F1C5AADE1AB53CA63DB17F4C314D493C4C44DE5F20" + + "75E084D080F92791F30BDD88373D50AB5A07BC72B0E7FFFA593103964E55603E" + + "F7FEB7CA0762A1A7B86B6CCAD88CD6CBC7C6935D21F5F06B2700588A2530E619" + + "DA1648AC809F3DDF56ACE5951737568FFEC7E2AB1AA0AE01B03A7F5A29CE73C0" + + "5D2801B17CAAD0121082E9952FAB16BA1C386336C62D4CF3A5019CF61609433E" + + "1C083237D47C4CF575097F7BF9000EF6B6C497A44E6480154A35669AD276BF05" + + "6CC730B4E5962B6AF96CC6D236AE85CEFDA6877173F72D2F614F6696D1F9DF07" + + "E107758B0978F69BC9DBE0CCBF252C40A3FDF7CE9104D3344F7B73593CCD73E0"; + private static final String IKE_AUTH_RESP_FRAG_2 = + "46B8ECA1E0D72A18BF3FA1C2CB1EE86F3520232000000001000000F0000000D4" + + "00020002155211EA41B37BC5F20568A6AE57038EEE208F94F9B444004F1EF391" + + "2CABFCF857B9CD95FAAA9489ED10A3F5C93510820E22E23FC55ED8049E067D72" + + "3645C00E1E08611916CE72D7F0A84123B63A8F3B9E78DBBE39967B7BB074AF4D" + + "BF2178D991EDBDD01908A14A266D09236DB963B14AC33D894F0F83A580209EFD" + + "61875BB56273AA336C22D6A4D890B93E0D42435667830CC32E4F608500E18569" + + "3E6C1D88C0B5AE427333C86468E3474DAA4D1506AAB2A4021309A33DD759D0D0" + + "A8C98BF7FBEA8109361A9F194D0FD756"; + private static final String DELETE_IKE_RESP = + "46B8ECA1E0D72A18BF3FA1C2CB1EE86F2E202520000000020000004C00000030" + + "342842D8DA37C8EFB92ED37C4FBB23CBDC90445137D6A0AF489F9F03641DBA9D" + + "02F6F59FD8A7A78C7261CEB8"; + + // Using IPv4 for transport mode Child SA. IPv6 is currently infeasible because the IKE server + // that generates the test vectors is running in an IPv4 only network. + private static final IkeTrafficSelector TRANSPORT_MODE_INBOUND_TS = + new IkeTrafficSelector( + MIN_PORT, + MAX_PORT, + InetAddresses.parseNumericAddress("172.58.35.103"), + InetAddresses.parseNumericAddress("172.58.35.103")); + + // TODO(b/157510502): Add test for IKE Session setup with transport mode Child in IPv6 network + + private static final String LOCAL_ID_ASN1_DN = + "CN=client.test.ike.android.net, O=Android, C=US"; + private static final String REMOTE_ID_ASN1_DN = + "CN=server.test.ike.android.net, O=Android, C=US"; + + private static X509Certificate sServerCaCert; + private static X509Certificate sClientEndCert; + private static X509Certificate sClientIntermediateCaCertOne; + private static X509Certificate sClientIntermediateCaCertTwo; + private static RSAPrivateKey sClientPrivateKey; + + @BeforeClass + public static void setUpCertsBeforeClass() throws Exception { + sServerCaCert = CertUtils.createCertFromPemFile("server-a-self-signed-ca.pem"); + sClientEndCert = CertUtils.createCertFromPemFile("client-a-end-cert.pem"); + sClientIntermediateCaCertOne = + CertUtils.createCertFromPemFile("client-a-intermediate-ca-one.pem"); + sClientIntermediateCaCertTwo = + CertUtils.createCertFromPemFile("client-a-intermediate-ca-two.pem"); + sClientPrivateKey = CertUtils.createRsaPrivateKeyFromKeyFile("client-a-private-key.key"); + } + + private IkeSession openIkeSessionWithRemoteAddress(InetAddress remoteAddress) { + IkeSessionParams ikeParams = + new IkeSessionParams.Builder(sContext) + .setNetwork(mTunNetwork) + .setServerHostname(remoteAddress.getHostAddress()) + .addSaProposal(SaProposalTest.buildIkeSaProposalWithNormalModeCipher()) + .addSaProposal(SaProposalTest.buildIkeSaProposalWithCombinedModeCipher()) + .setLocalIdentification( + new IkeDerAsn1DnIdentification(new X500Principal(LOCAL_ID_ASN1_DN))) + .setRemoteIdentification( + new IkeDerAsn1DnIdentification( + new X500Principal(REMOTE_ID_ASN1_DN))) + .setAuthDigitalSignature( + sServerCaCert, + sClientEndCert, + Arrays.asList( + sClientIntermediateCaCertOne, sClientIntermediateCaCertTwo), + sClientPrivateKey) + .build(); + + return new IkeSession( + sContext, + ikeParams, + buildTransportModeChildParamsWithTs( + TRANSPORT_MODE_INBOUND_TS, TRANSPORT_MODE_OUTBOUND_TS), + mUserCbExecutor, + mIkeSessionCallback, + mFirstChildSessionCallback); + } + + @Test + public void testIkeSessionSetupAndChildSessionSetupWithTransportMode() throws Exception { + // Open IKE Session + IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress); + performSetupIkeAndFirstChildBlocking( + IKE_INIT_RESP, + EXPECTED_AUTH_REQ_FRAG_COUNT /* expectedReqPktCnt */, + true /* expectedAuthUseEncap */, + IKE_AUTH_RESP_FRAG_1, + IKE_AUTH_RESP_FRAG_2); + + // IKE INIT and IKE AUTH takes two exchanges. Message ID starts from 2 + int expectedMsgId = 2; + + verifyIkeSessionSetupBlocking(); + verifyChildSessionSetupBlocking( + mFirstChildSessionCallback, + Arrays.asList(TRANSPORT_MODE_INBOUND_TS), + Arrays.asList(TRANSPORT_MODE_OUTBOUND_TS), + new ArrayList()); + IpSecTransformCallRecord firstTransformRecordA = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + IpSecTransformCallRecord firstTransformRecordB = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + verifyCreateIpSecTransformPair(firstTransformRecordA, firstTransformRecordB); + + // Close IKE Session + ikeSession.close(); + performCloseIkeBlocking(expectedMsgId++, DELETE_IKE_RESP); + verifyCloseIkeAndChildBlocking(firstTransformRecordA, firstTransformRecordB); + } +} diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionMschapV2Test.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionMschapV2Test.java new file mode 100644 index 0000000000..cb771276dc --- /dev/null +++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionMschapV2Test.java @@ -0,0 +1,220 @@ +/* + * 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 android.net.ipsec.ike.cts; + +import android.net.InetAddresses; +import android.net.LinkAddress; +import android.net.eap.EapSessionConfig; +import android.net.ipsec.ike.IkeFqdnIdentification; +import android.net.ipsec.ike.IkeSession; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.IkeTrafficSelector; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.net.ipsec.ike.testutils.CertUtils; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Explicitly test setting up transport mode Child SA so that devices do not have + * FEATURE_IPSEC_TUNNELS will be test covered. Tunnel mode Child SA setup has been tested in + * IkeSessionPskTest and authentication method is orthogonal to Child mode. + */ +@RunWith(AndroidJUnit4.class) +public class IkeSessionMschapV2Test extends IkeSessionTestBase { + private static final String IKE_INIT_RESP = + "46B8ECA1E0D72A1873F643FF94D249A921202220000000000000015022000030" + + "0000002C010100040300000C0100000C800E0080030000080300000203000008" + + "0200000200000008040000022800008800020000CC6E71E67E32CED6BCE33FBD" + + "A74113867E3FA3AE21C7C9AB44A7F8835DF602BFD6F6528B67FEE39821232380" + + "C99E8FFC0A5D767F8F38906DA41946C2299DF18C15FA69BAC08D3EDB32E8C8CA" + + "28431831561C04CB0CDE393F817151CD8DAF7A311838411F1C39BFDB5EBCF6A6" + + "1DF66DEB067362649D64607D599B56C4227819D0290000241197004CF31AD00F" + + "5E0C92E198488D8A2B6F6A25C82762AA49F565BCE9D857D72900001C00004004" + + "A0D98FEABBFB92A6C0976EE83D2AACFCCF969A6B2900001C0000400575EBF73F" + + "8EE5CC73917DE9D3F91FCD4A16A0444D290000080000402E290000100000402F" + + "00020003000400050000000800004014"; + private static final String IKE_AUTH_RESP_1_FRAG_1 = + "46B8ECA1E0D72A1873F643FF94D249A93520232000000001000004E0240004C4" + + "00010002C4159CB756773B3F1911F4595107BC505D7A28C72F05182966076679" + + "CA68ED92E4BC5CD441C9CB315F2F449A8A521CAFED3C5F285E295FC3791D3415" + + "E3BACF66A08410DF4E35F7D88FE40DA28851C91C77A6549E186AC1B7846DF3FA" + + "0A347A5ABBCAEE19E70F0EE5966DC6242A115F29523709302EDAD2E36C8F0395" + + "CF5C42EC2D2898ECDD8A6AEDD686A70B589A981558667647F32F41E0D8913E94" + + "A6693F53E59EA8938037F562CF1DC5E6E2CDC630B5FFB08949E3172249422F7D" + + "EA069F9BAD5F96E48BADC7164A9269669AD0DF295A80C54D1D23CEA3F28AC485" + + "86D2A9850DA23823037AB7D1577B7B2364C92C36B84238357129EB4A64D33310" + + "B95DCD50CD53E78C32EFE7DC1627D9432E9BFDEE130045DE967B19F92A9D1270" + + "F1E2C6BFBAA56802F3E63510578EF1ECB6872852F286EEC790AA1FE0CAF391CB" + + "E276554922713BA4770CFE71E23F043DC620E22CC02A74F60725D18331B7F2C9" + + "276EB6FBB7CBDAA040046D7ECBE1A5D7064E04E542807C5101B941D1C81B9D5E" + + "90347B22BD4E638E2EDC98E369B51AA29BDB2CF8AA610D4B893EB83A4650717C" + + "38B4D145EE939C18DCEDF6C79933CEB3D7C116B1F188DF9DDD560951B54E4A7D" + + "80C999A32AB02BF39D7B498DAD36F1A5CBE2F64557D6401AE9DD6E0CEADA3F90" + + "540FE9114BB6B8719C9064796354F4A180A6600CAD092F8302564E409B71ACB7" + + "590F19B3AC88E7A606C718D0B97F7E4B4830F11D851C59F2255846DA22E2C805" + + "0CA2AF2ACF3B6C769D11B75B5AC9AB82ED3D90014994B1BF6FED58FBEF2D72EF" + + "8BDFE51F9A101393A7CA1ACF78FAEBF3E3CC25E09407D1E14AF351A159A13EE3" + + "9B919BA8B49942792E7527C2FB6D418C4DF427669A4BF5A1AFBBB973BAF17918" + + "9C9D520CAC2283B89A539ECE785EBE48FBB77D880A17D55C84A51F46068A4B87" + + "FF48FEEE50E1E034CC8AFF5DA92105F55EC4823E67BDFE942CA8BE0DAECBBD52" + + "E8AAF306049DC6C4CF87D987B0AC54FCE92E6AE8507965AAAC6AB8BD3405712F" + + "EE170B70BC64BDCBD86D80C7AAAF341131F9A1210D7430B17218413AE1363183" + + "5C98FA2428B1E9E987ADC9070E232310A28F4C3163E18366FFB112BADD7C5E0F" + + "D13093A7C1428F87856BA0A7E46955589ACA267CE7A04320C4BCDBB60C672404" + + "778F8D511AAB09349DAB482445D7F606F28E7FBBB18FC0F4EC0AF04F44C282F9" + + "39C6E3B955C84DADEA350667236583069B74F492D600127636FA31F63E560851" + + "2FC28B8EA5B4D01D110990B6EA46B9C2E7C7C856C240EF7A8147BA2C4344B85A" + + "453C862024B5B6814D13CDEAEF7683D539BB50CAFFC0416F269F2F9EDEC5FA30" + + "022FD7B4B186CD2020E7ED8D81ED90822EDD8B76F840DD68F09694CFF9B4F33E" + + "11DF4E601A4212881A6D4E9259001705C41E9E23D18A7F3D4A3463649A38211A" + + "5A90D0F17739A677C74E23F31C01D60B5A0F1E6A4D44FED9D25BF1E63418E1FC" + + "0B19F6F4B71DE53C62B14B82279538A82DD4BE19AB6E00AFC20F124AAB7DF21A" + + "42259BE4F40EC69B16917256F23E2C37376311D62E0A3A0EF8C2AD0C090221D5" + + "C5ECA08F08178A4D31FFDB150C609827D18AD83C7B0A43AEE0406BD3FB494B53" + + "A279FDD6447E234C926AD8CE47FFF779BB45B1FC8457C6E7D257D1359959D977" + + "CEF6906A3367DC4D454993EFDC6F1EA94E17EB3DCB00A289346B4CFD7F19B16E"; + private static final String IKE_AUTH_RESP_1_FRAG_2 = + "46B8ECA1E0D72A1873F643FF94D249A935202320000000010000008000000064" + + "00020002C61F66025E821A5E69A4DE1F591A2C32C983C3154A5003660137D685" + + "A5262B9FDF5EDC699DE4D8BD38F549E3CBD12024B45B4C86561C36C3EED839DA" + + "9860C6AA0B764C662D08F1B6A98F68CF6E3038F737C0B415AD8A8B7D702BD92A"; + private static final String IKE_AUTH_RESP_2 = + "46B8ECA1E0D72A1873F643FF94D249A92E202320000000020000008C30000070" + + "62B90C2229FD23025BC2FD7FE6341E9EE04B17264CD619BCE18975A5F88BE438" + + "D4AD4A5310057255AF568C293A29B10107E3EE3675C10AA2B26404D90C0528CC" + + "F7605A86C96A1F2635CCC6CFC90EE65E5C2A2262EB33FE520EB708423A83CB63" + + "274ECCBB102AF5DF35742657"; + private static final String IKE_AUTH_RESP_3 = + "46B8ECA1E0D72A1873F643FF94D249A92E202320000000030000004C30000030" + + "AB52C3C80123D3432C05AF457CE93C352395F73E861CD49561BA528CFE68D17D" + + "78BBF6FC41E81C2B9EA051A2"; + private static final String IKE_AUTH_RESP_4 = + "46B8ECA1E0D72A1873F643FF94D249A92E20232000000004000000CC270000B0" + + "8D3342A7AB2666AC754F4B55C5C6B1A61255E62FBCA53D5CDEEDE60DADB7915C" + + "7F962076A58BF7D39A05ED1B60FF349B6DE311AF7CEBC72B4BB9723A728A5D3E" + + "9E508B2D7A11843D279B56ADA07E608D61F5CA7638F10372A440AD1DCE44E190" + + "7B7B7A68B126EBBB86638D667D5B528D233BA8D32D7E0FAC4E1448E87396EEE6" + + "0985B79841E1229D7962AACFD8F872722EC8D5B19D4C82D6C4ADCB276127A1A7" + + "3FC84CDF85B2299BC96B64AC"; + private static final String DELETE_IKE_RESP = + "46B8ECA1E0D72A1873F643FF94D249A92E202520000000050000004C00000030" + + "622CE06C8CB132AA00567E9BC83F58B32BD7DB5130C76E385B306434DA227361" + + "D50CC19D408A8D4F36F9697F"; + + // This value is align with the test vectors hex that are generated in an IPv4 environment + private static final IkeTrafficSelector TRANSPORT_MODE_INBOUND_TS = + new IkeTrafficSelector( + MIN_PORT, + MAX_PORT, + InetAddresses.parseNumericAddress("172.58.35.67"), + InetAddresses.parseNumericAddress("172.58.35.67")); + + private static final EapSessionConfig EAP_CONFIG = + new EapSessionConfig.Builder() + .setEapIdentity(EAP_IDENTITY) + .setEapMsChapV2Config(EAP_MSCHAPV2_USERNAME, EAP_MSCHAPV2_PASSWORD) + .build(); + + private static X509Certificate sServerCaCert; + + @BeforeClass + public static void setUpCertBeforeClass() throws Exception { + sServerCaCert = CertUtils.createCertFromPemFile("server-a-self-signed-ca.pem"); + } + + private IkeSession openIkeSessionWithRemoteAddress(InetAddress remoteAddress) { + IkeSessionParams ikeParams = + new IkeSessionParams.Builder(sContext) + .setNetwork(mTunNetwork) + .setServerHostname(remoteAddress.getHostAddress()) + .addSaProposal(SaProposalTest.buildIkeSaProposalWithNormalModeCipher()) + .addSaProposal(SaProposalTest.buildIkeSaProposalWithCombinedModeCipher()) + .setLocalIdentification(new IkeFqdnIdentification(LOCAL_HOSTNAME)) + .setRemoteIdentification(new IkeFqdnIdentification(REMOTE_HOSTNAME)) + .setAuthEap(sServerCaCert, EAP_CONFIG) + .build(); + return new IkeSession( + sContext, + ikeParams, + buildTransportModeChildParamsWithTs( + TRANSPORT_MODE_INBOUND_TS, TRANSPORT_MODE_OUTBOUND_TS), + mUserCbExecutor, + mIkeSessionCallback, + mFirstChildSessionCallback); + } + + @Test + public void testIkeSessionSetupAndChildSessionSetupWithTransportMode() throws Exception { + // Open IKE Session + IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress); + int expectedMsgId = 0; + mTunUtils.awaitReqAndInjectResp( + IKE_DETERMINISTIC_INITIATOR_SPI, + expectedMsgId++, + false /* expectedUseEncap */, + IKE_INIT_RESP); + + mTunUtils.awaitReqAndInjectResp( + IKE_DETERMINISTIC_INITIATOR_SPI, + expectedMsgId++, + true /* expectedUseEncap */, + IKE_AUTH_RESP_1_FRAG_1, + IKE_AUTH_RESP_1_FRAG_2); + + mTunUtils.awaitReqAndInjectResp( + IKE_DETERMINISTIC_INITIATOR_SPI, + expectedMsgId++, + true /* expectedUseEncap */, + IKE_AUTH_RESP_2); + mTunUtils.awaitReqAndInjectResp( + IKE_DETERMINISTIC_INITIATOR_SPI, + expectedMsgId++, + true /* expectedUseEncap */, + IKE_AUTH_RESP_3); + mTunUtils.awaitReqAndInjectResp( + IKE_DETERMINISTIC_INITIATOR_SPI, + expectedMsgId++, + true /* expectedUseEncap */, + IKE_AUTH_RESP_4); + + verifyIkeSessionSetupBlocking(); + verifyChildSessionSetupBlocking( + mFirstChildSessionCallback, + Arrays.asList(TRANSPORT_MODE_INBOUND_TS), + Arrays.asList(TRANSPORT_MODE_OUTBOUND_TS), + new ArrayList()); + IpSecTransformCallRecord firstTransformRecordA = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + IpSecTransformCallRecord firstTransformRecordB = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + verifyCreateIpSecTransformPair(firstTransformRecordA, firstTransformRecordB); + + // Close IKE Session + ikeSession.close(); + performCloseIkeBlocking(expectedMsgId++, DELETE_IKE_RESP); + verifyCloseIkeAndChildBlocking(firstTransformRecordA, firstTransformRecordB); + } +} diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionPskTest.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionPskTest.java index fb93398b26..0509fc0c92 100644 --- a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionPskTest.java +++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionPskTest.java @@ -16,39 +16,34 @@ package android.net.ipsec.ike.cts; -import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_FRAGMENTATION; +import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED; +import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_INTERNAL_ADDRESS_FAILURE; import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_NO_PROPOSAL_CHOSEN; -import static android.system.OsConstants.AF_INET; -import static android.system.OsConstants.AF_INET6; - -import static com.android.internal.util.HexDump.hexStringToByteArray; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import android.net.ipsec.ike.ChildSessionConfiguration; +import android.net.LinkAddress; import android.net.ipsec.ike.IkeFqdnIdentification; import android.net.ipsec.ike.IkeSession; -import android.net.ipsec.ike.IkeSessionConfiguration; -import android.net.ipsec.ike.IkeSessionConnectionInfo; import android.net.ipsec.ike.IkeSessionParams; -import android.net.ipsec.ike.TunnelModeChildSessionParams; -import android.net.ipsec.ike.exceptions.IkeException; import android.net.ipsec.ike.exceptions.IkeProtocolException; import android.platform.test.annotations.AppModeFull; import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import java.net.InetAddress; +import java.util.ArrayList; import java.util.Arrays; @RunWith(AndroidJUnit4.class) -@AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps") +@AppModeFull(reason = "MANAGE_IPSEC_TUNNELS permission can't be granted to instant apps") public class IkeSessionPskTest extends IkeSessionTestBase { // Test vectors for success workflow private static final String SUCCESS_IKE_INIT_RESP = @@ -89,16 +84,6 @@ public class IkeSessionPskTest extends IkeSessionTestBase { + "9352D71100777B00ABCC6BD7DBEA697827FFAAA48DF9A54D1D68161939F5DC8" + "6743A7CEB2BE34AC00095A5B8"; - private static final long IKE_INIT_SPI = Long.parseLong("46B8ECA1E0D72A18", 16); - - private static final TunnelModeChildSessionParams CHILD_PARAMS = - new TunnelModeChildSessionParams.Builder() - .addSaProposal(SaProposalTest.buildChildSaProposalWithNormalModeCipher()) - .addSaProposal(SaProposalTest.buildChildSaProposalWithCombinedModeCipher()) - .addInternalAddressRequest(AF_INET) - .addInternalAddressRequest(AF_INET6) - .build(); - private IkeSession openIkeSessionWithRemoteAddress(InetAddress remoteAddress) { IkeSessionParams ikeParams = new IkeSessionParams.Builder(sContext) @@ -113,110 +98,159 @@ public class IkeSessionPskTest extends IkeSessionTestBase { return new IkeSession( sContext, ikeParams, - CHILD_PARAMS, + buildTunnelModeChildSessionParams(), mUserCbExecutor, mIkeSessionCallback, mFirstChildSessionCallback); } + @BeforeClass + public static void setUpTunnelPermissionBeforeClass() throws Exception { + // Under normal circumstances, the MANAGE_IPSEC_TUNNELS appop would be auto-granted, and + // a standard permission is insufficient. So we shell out the appop, to give us the + // right appop permissions. + setAppOp(OP_MANAGE_IPSEC_TUNNELS, true); + } + + // This method is guaranteed to run in subclasses and will run after subclasses' @AfterClass + // methods. + @AfterClass + public static void tearDownTunnelPermissionAfterClass() throws Exception { + setAppOp(OP_MANAGE_IPSEC_TUNNELS, false); + } + @Test public void testIkeSessionSetupAndChildSessionSetupWithTunnelMode() throws Exception { if (!hasTunnelsFeature()) return; // Open IKE Session IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress); - int expectedMsgId = 0; - mTunUtils.awaitReqAndInjectResp( - IKE_INIT_SPI, - expectedMsgId++, - false /* expectedUseEncap */, - hexStringToByteArray(SUCCESS_IKE_INIT_RESP)); + performSetupIkeAndFirstChildBlocking(SUCCESS_IKE_INIT_RESP, SUCCESS_IKE_AUTH_RESP); - mTunUtils.awaitReqAndInjectResp( - IKE_INIT_SPI, - expectedMsgId++, - true /* expectedUseEncap */, - hexStringToByteArray(SUCCESS_IKE_AUTH_RESP)); + // IKE INIT and IKE AUTH takes two exchanges. Message ID starts from 2 + int expectedMsgId = 2; - // Verify opening IKE Session - IkeSessionConfiguration ikeConfig = mIkeSessionCallback.awaitIkeConfig(); - assertNotNull(ikeConfig); - assertEquals(EXPECTED_REMOTE_APP_VERSION_EMPTY, ikeConfig.getRemoteApplicationVersion()); - assertTrue(ikeConfig.getRemoteVendorIds().isEmpty()); - assertTrue(ikeConfig.getPcscfServers().isEmpty()); - assertTrue(ikeConfig.isIkeExtensionEnabled(EXTENSION_TYPE_FRAGMENTATION)); + verifyIkeSessionSetupBlocking(); + verifyChildSessionSetupBlocking( + mFirstChildSessionCallback, + Arrays.asList(TUNNEL_MODE_INBOUND_TS), + Arrays.asList(TUNNEL_MODE_OUTBOUND_TS), + Arrays.asList(EXPECTED_INTERNAL_LINK_ADDR)); - IkeSessionConnectionInfo ikeConnectInfo = ikeConfig.getIkeSessionConnectionInfo(); - assertNotNull(ikeConnectInfo); - assertEquals(mLocalAddress, ikeConnectInfo.getLocalAddress()); - assertEquals(mRemoteAddress, ikeConnectInfo.getRemoteAddress()); - assertEquals(mTunNetwork, ikeConnectInfo.getNetwork()); - - // Verify opening first Child Session - ChildSessionConfiguration firstChildConfig = mFirstChildSessionCallback.awaitChildConfig(); - assertNotNull(firstChildConfig); - assertEquals( - Arrays.asList(EXPECTED_INBOUND_TS), firstChildConfig.getInboundTrafficSelectors()); - assertEquals(Arrays.asList(DEFAULT_V4_TS), firstChildConfig.getOutboundTrafficSelectors()); - assertEquals( - Arrays.asList(EXPECTED_INTERNAL_LINK_ADDR), - firstChildConfig.getInternalAddresses()); - assertTrue(firstChildConfig.getInternalSubnets().isEmpty()); - assertTrue(firstChildConfig.getInternalDnsServers().isEmpty()); - assertTrue(firstChildConfig.getInternalDhcpServers().isEmpty()); - - assertNotNull(mFirstChildSessionCallback.awaitNextCreatedIpSecTransform()); - assertNotNull(mFirstChildSessionCallback.awaitNextCreatedIpSecTransform()); + IpSecTransformCallRecord firstTransformRecordA = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + IpSecTransformCallRecord firstTransformRecordB = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + verifyCreateIpSecTransformPair(firstTransformRecordA, firstTransformRecordB); // Open additional Child Session TestChildSessionCallback additionalChildCb = new TestChildSessionCallback(); - ikeSession.openChildSession(CHILD_PARAMS, additionalChildCb); + ikeSession.openChildSession(buildTunnelModeChildSessionParams(), additionalChildCb); mTunUtils.awaitReqAndInjectResp( - IKE_INIT_SPI, + IKE_DETERMINISTIC_INITIATOR_SPI, expectedMsgId++, true /* expectedUseEncap */, - hexStringToByteArray(SUCCESS_CREATE_CHILD_RESP)); + SUCCESS_CREATE_CHILD_RESP); // Verify opening additional Child Session - ChildSessionConfiguration additionalChildConfig = additionalChildCb.awaitChildConfig(); - assertNotNull(additionalChildConfig); - assertEquals( - Arrays.asList(EXPECTED_INBOUND_TS), firstChildConfig.getInboundTrafficSelectors()); - assertEquals(Arrays.asList(DEFAULT_V4_TS), firstChildConfig.getOutboundTrafficSelectors()); - assertTrue(additionalChildConfig.getInternalAddresses().isEmpty()); - assertTrue(additionalChildConfig.getInternalSubnets().isEmpty()); - assertTrue(additionalChildConfig.getInternalDnsServers().isEmpty()); - assertTrue(additionalChildConfig.getInternalDhcpServers().isEmpty()); - - assertNotNull(additionalChildCb.awaitNextCreatedIpSecTransform()); - assertNotNull(additionalChildCb.awaitNextCreatedIpSecTransform()); + verifyChildSessionSetupBlocking( + additionalChildCb, + Arrays.asList(TUNNEL_MODE_INBOUND_TS), + Arrays.asList(TUNNEL_MODE_OUTBOUND_TS), + new ArrayList()); + IpSecTransformCallRecord additionalTransformRecordA = + additionalChildCb.awaitNextCreatedIpSecTransform(); + IpSecTransformCallRecord additionalTransformRecordB = + additionalChildCb.awaitNextCreatedIpSecTransform(); + verifyCreateIpSecTransformPair(additionalTransformRecordA, additionalTransformRecordB); // Close additional Child Session ikeSession.closeChildSession(additionalChildCb); mTunUtils.awaitReqAndInjectResp( - IKE_INIT_SPI, + IKE_DETERMINISTIC_INITIATOR_SPI, expectedMsgId++, true /* expectedUseEncap */, - hexStringToByteArray(SUCCESS_DELETE_CHILD_RESP)); + SUCCESS_DELETE_CHILD_RESP); - assertNotNull(additionalChildCb.awaitNextDeletedIpSecTransform()); - assertNotNull(additionalChildCb.awaitNextDeletedIpSecTransform()); + verifyDeleteIpSecTransformPair( + additionalChildCb, additionalTransformRecordA, additionalTransformRecordB); additionalChildCb.awaitOnClosed(); // Close IKE Session ikeSession.close(); - mTunUtils.awaitReqAndInjectResp( - IKE_INIT_SPI, - expectedMsgId++, - true /* expectedUseEncap */, - hexStringToByteArray(SUCCESS_DELETE_IKE_RESP)); + performCloseIkeBlocking(expectedMsgId++, SUCCESS_DELETE_IKE_RESP); + verifyCloseIkeAndChildBlocking(firstTransformRecordA, firstTransformRecordB); + } - assertNotNull(mFirstChildSessionCallback.awaitNextDeletedIpSecTransform()); - assertNotNull(mFirstChildSessionCallback.awaitNextDeletedIpSecTransform()); - mFirstChildSessionCallback.awaitOnClosed(); - mIkeSessionCallback.awaitOnClosed(); + @Test + public void testIkeSessionSetupAndChildSessionSetupWithTunnelModeV6() throws Exception { + if (!hasTunnelsFeature()) return; - // TODO: verify created and deleted IpSecTransform pair and their directions + final String ikeInitResp = + "46B8ECA1E0D72A186F7B6C2CEB77EB9021202220000000000000011822000030" + + "0000002C010100040300000C0100000C800E0100030000080300000C03000008" + + "0200000500000008040000022800008800020000DABAA04B38B491E2403F2125" + + "96ECF1C8EF7B1DC19A422FDD46E1756C826BB3A16404361B775D9950577B5CDF" + + "6AAA1642BD1427BDA8BC55354A97C1025E19C1E2EE2DF8A0C9406E545D829F52" + + "75695008E3B742984B8DD1770F3514213B0DF3EE8B199416DF200D248115C057" + + "1C193E4F96802E5EF48DD99CAC251882A8F7CCC329000024BC6F0F1D3653C2C7" + + "679E02CDB6A3B32B2FEE9AF52F0326D4D9AE073D56CE8922290000080000402E" + + "290000100000402F00020003000400050000000800004014"; + final String ikeAuthResp = + "46B8ECA1E0D72A186F7B6C2CEB77EB902E202320000000010000015024000134" + + "4D115AFDCDAD0310760BB664EB7D405A340869AD6EDF0AAEAD0663A9253DADCB" + + "73EBE5CD29D4FA1CDEADE0B94391B5C4CF77BCC1596ACE3CE6A7891E44888FA5" + + "46632C0EF4E6193C023C9DC59142C37D1C49D6EF5CD324EC6FC35C89E1721C78" + + "91FDCDB723D8062709950F4AA9273D26A54C9C7E86862DBC15F7B6641D2B9BAD" + + "E55069008201D12968D97B537B1518FE87B0FFA03C3EE6012C06721B1E2A3F68" + + "92108BC4A4F7063F7F94562D8B60F291A1377A836CF12BCDA7E15C1A8F3C77BB" + + "6DB7F2C833CCE4CDDED7506536621A3356CE2BC1874E7B1A1A9B447D7DF6AB09" + + "638B8AD94A781B28BB91B514B611B24DF8E8A047A10AE27BBF15C754D3D2F792" + + "D3E1CCADDAE934C98AE53A8FC3419C88AFF0355564F82A629C998012DA7BB704" + + "5307270DF326377E3E1994476902035B"; + final String deleteIkeResp = + "46B8ECA1E0D72A186F7B6C2CEB77EB902E202520000000020000005000000034" + + "CF15C299F35688E5140A48B61C95F004121BF8236201415E5CD45BA41AAB16D4" + + "90B44B9E6D5D92B5B97D24196A58C73F"; + + mLocalAddress = IPV6_ADDRESS_LOCAL; + mRemoteAddress = IPV6_ADDRESS_REMOTE; + + // Teardown current test network that uses IPv4 address and set up new network with IPv6 + // address. + tearDownTestNetwork(); + setUpTestNetwork(mLocalAddress); + + // Open IKE Session + IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress); + performSetupIkeAndFirstChildBlocking( + ikeInitResp, + 1 /* expectedAuthReqPktCnt */, + false /* expectedAuthUseEncap */, + ikeAuthResp); + + // Local request message ID starts from 2 because there is one IKE_INIT message and a single + // IKE_AUTH message. + int expectedMsgId = 2; + + verifyIkeSessionSetupBlocking(); + verifyChildSessionSetupBlocking( + mFirstChildSessionCallback, + Arrays.asList(TUNNEL_MODE_INBOUND_TS_V6), + Arrays.asList(TUNNEL_MODE_OUTBOUND_TS_V6), + Arrays.asList(EXPECTED_INTERNAL_LINK_ADDR_V6), + Arrays.asList(EXPECTED_DNS_SERVERS_ONE, EXPECTED_DNS_SERVERS_TWO)); + + IpSecTransformCallRecord firstTransformRecordA = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + IpSecTransformCallRecord firstTransformRecordB = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + verifyCreateIpSecTransformPair(firstTransformRecordA, firstTransformRecordB); + + // Close IKE Session + ikeSession.close(); + performCloseIkeBlocking(expectedMsgId++, false /* expectedUseEncap */, deleteIkeResp); + verifyCloseIkeAndChildBlocking(firstTransformRecordA, firstTransformRecordB); } @Test @@ -225,18 +259,7 @@ public class IkeSessionPskTest extends IkeSessionTestBase { // Open IKE Session IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress); - int expectedMsgId = 0; - mTunUtils.awaitReqAndInjectResp( - IKE_INIT_SPI, - expectedMsgId++, - false /* expectedUseEncap */, - hexStringToByteArray(SUCCESS_IKE_INIT_RESP)); - - mTunUtils.awaitReqAndInjectResp( - IKE_INIT_SPI, - expectedMsgId++, - true /* expectedUseEncap */, - hexStringToByteArray(SUCCESS_IKE_AUTH_RESP)); + performSetupIkeAndFirstChildBlocking(SUCCESS_IKE_INIT_RESP, SUCCESS_IKE_AUTH_RESP); ikeSession.kill(); mFirstChildSessionCallback.awaitOnClosed(); @@ -245,29 +268,94 @@ public class IkeSessionPskTest extends IkeSessionTestBase { @Test public void testIkeInitFail() throws Exception { - String ikeInitFailRespHex = + final String ikeInitFailRespHex = "46B8ECA1E0D72A180000000000000000292022200000000000000024000000080000000E"; // Open IKE Session IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress); int expectedMsgId = 0; mTunUtils.awaitReqAndInjectResp( - IKE_INIT_SPI, + IKE_DETERMINISTIC_INITIATOR_SPI, expectedMsgId++, false /* expectedUseEncap */, - hexStringToByteArray(ikeInitFailRespHex)); + ikeInitFailRespHex); mFirstChildSessionCallback.awaitOnClosed(); - IkeException exception = mIkeSessionCallback.awaitOnClosedException(); - assertNotNull(exception); - assertTrue(exception instanceof IkeProtocolException); - IkeProtocolException protocolException = (IkeProtocolException) exception; + IkeProtocolException protocolException = + (IkeProtocolException) mIkeSessionCallback.awaitOnClosedException(); assertEquals(ERROR_TYPE_NO_PROPOSAL_CHOSEN, protocolException.getErrorType()); assertArrayEquals(EXPECTED_PROTOCOL_ERROR_DATA_NONE, protocolException.getErrorData()); } - // TODO(b/155821007): Verify rekey process and handling IKE_AUTH failure + @Test + public void testIkeAuthHandlesAuthFailNotification() throws Exception { + final String ikeInitRespHex = + "46B8ECA1E0D72A18CF94CE3159486F002120222000000000000001502200" + + "00300000002C010100040300000C0100000C800E01000300000803000005" + + "0300000802000004000000080400000228000088000200001821AA854691" + + "FA3292DF710F0AC149ACBD0CB421608B8796C1912AF04C5B4B23936FDEC4" + + "7CB640E3EAFB56BBB562825E87AF68B40E4BAB80A49BAD44407450A4195A" + + "1DD54BD99F48D28C9F0FBA315A3401C1C3C4AD55911F514A8DF2D2467C46" + + "A73DDC1452AE81336E0F0D5EC896D2E7A77628AF2F9089F48943399DF216" + + "EFCD2900002418D2B7E4E6AF0FEFF5962CF8D68F7793B1293FEDE13331D4" + + "AB0CE9436C2EE1EC2900001C0000400457BD9AEF5B362A83DD7F3DDAA4A9" + + "9B6B4041DAF32900001C000040055A81893582701E44D4B6729A22FE06DE" + + "82A03A36290000080000402E290000100000402F00020003000400050000" + + "000800004014"; + final String ikeAuthFailRespHex = + "46B8ECA1E0D72A18CF94CE3159486F002E202320000000010000004C2900" + + "00301B9E4C8242D3BE62E7F0A537FE8B92C6EAB7153105DA421DCE43A06D" + + "AB6E4808BAC0CA1DAD6ADD0A126A41BD"; - // TODO(b/155821007): Test creating transport mode Child SA + // Open IKE Session + IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress); + performSetupIkeAndFirstChildBlocking(ikeInitRespHex, ikeAuthFailRespHex); + + mFirstChildSessionCallback.awaitOnClosed(); + IkeProtocolException protocolException = + (IkeProtocolException) mIkeSessionCallback.awaitOnClosedException(); + assertEquals(ERROR_TYPE_AUTHENTICATION_FAILED, protocolException.getErrorType()); + assertArrayEquals(EXPECTED_PROTOCOL_ERROR_DATA_NONE, protocolException.getErrorData()); + } + + @Test + public void testIkeAuthHandlesFirstChildCreationFail() throws Exception { + final String ikeInitRespHex = + "46B8ECA1E0D72A182B300285DA19E6452120222000000000000001502200" + + "00300000002C010100040300000C0100000C800E01000300000803000005" + + "0300000802000004000000080400000228000088000200005C9DE629981F" + + "DB1FC45DB6CCF15D076C1F51BD9F63C771DC089F05CCDE6247965D15C616" + + "C7B5A62342491715E4D1FEA19326477D24143E8E56AB6AD93F54B19BC32A" + + "44BC0A5B5632E57D0A3C43E466E1547D8E4EF65EA4B864A348161666E229" + + "84975A486251A17C4F096A6D5CF3DB83874B70324A31AA7ADDE2D73BADD8" + + "238029000024CF06260F7C4923295E7C91F2B8479212892DA7A519A0322F" + + "F5B2BF570B92972B2900001C00004004C7ACC2C7D58CF8C9F5E953993AF4" + + "6CAC976635B42900001C00004005B64B190DFE7BDE8B9B1475EDE67B63D6" + + "F1DBBF44290000080000402E290000100000402F00020003000400050000" + + "000800004014"; + final String ikeAuthCreateChildFailHex = + "46B8ECA1E0D72A182B300285DA19E6452E202320000000010000008C2400" + + "0070386FC9CCC67495A17915D0544390A2963A769F4A42C6FA668CEEC07F" + + "EC0C87D681DE34267023DD394F1401B5A563E71002C0CE0928D0ABC0C4570" + + "E39C2EDEF820F870AB71BD70A3F3EB5C96CA294B6D3F01677690DCF9F8CFC" + + "9584650957573502BA83E32F18207A9ADEB1FA"; + + // Open IKE Session + IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress); + performSetupIkeAndFirstChildBlocking(ikeInitRespHex, ikeAuthCreateChildFailHex); + + // Even though the child creation failed, the authentication succeeded, so the IKE Session's + // onOpened() callback is still expected + verifyIkeSessionSetupBlocking(); + + // Verify Child Creation failed + IkeProtocolException protocolException = + (IkeProtocolException) mFirstChildSessionCallback.awaitOnClosedException(); + assertEquals(ERROR_TYPE_INTERNAL_ADDRESS_FAILURE, protocolException.getErrorType()); + assertArrayEquals(EXPECTED_PROTOCOL_ERROR_DATA_NONE, protocolException.getErrorData()); + + ikeSession.kill(); + mIkeSessionCallback.awaitOnClosed(); + } } diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionRekeyTest.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionRekeyTest.java new file mode 100644 index 0000000000..f954fcd031 --- /dev/null +++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionRekeyTest.java @@ -0,0 +1,265 @@ +/* + * 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 android.net.ipsec.ike.cts; + +import static com.android.internal.util.HexDump.hexStringToByteArray; + +import android.net.InetAddresses; +import android.net.LinkAddress; +import android.net.ipsec.ike.IkeFqdnIdentification; +import android.net.ipsec.ike.IkeSession; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.IkeTrafficSelector; +import android.net.ipsec.ike.cts.IkeTunUtils.PortPair; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Explicitly test transport mode Child SA so that devices without FEATURE_IPSEC_TUNNELS can be test + * covered. Tunnel mode Child SA setup has been tested in IkeSessionPskTest. Rekeying process is + * independent from Child SA mode. + */ +@RunWith(AndroidJUnit4.class) +public class IkeSessionRekeyTest extends IkeSessionTestBase { + // This value is align with the test vectors hex that are generated in an IPv4 environment + private static final IkeTrafficSelector TRANSPORT_MODE_INBOUND_TS = + new IkeTrafficSelector( + MIN_PORT, + MAX_PORT, + InetAddresses.parseNumericAddress("172.58.35.40"), + InetAddresses.parseNumericAddress("172.58.35.40")); + + private IkeSession openIkeSessionWithRemoteAddress(InetAddress remoteAddress) { + IkeSessionParams ikeParams = + new IkeSessionParams.Builder(sContext) + .setNetwork(mTunNetwork) + .setServerHostname(remoteAddress.getHostAddress()) + .addSaProposal(SaProposalTest.buildIkeSaProposalWithNormalModeCipher()) + .addSaProposal(SaProposalTest.buildIkeSaProposalWithCombinedModeCipher()) + .setLocalIdentification(new IkeFqdnIdentification(LOCAL_HOSTNAME)) + .setRemoteIdentification(new IkeFqdnIdentification(REMOTE_HOSTNAME)) + .setAuthPsk(IKE_PSK) + .build(); + return new IkeSession( + sContext, + ikeParams, + buildTransportModeChildParamsWithTs( + TRANSPORT_MODE_INBOUND_TS, TRANSPORT_MODE_OUTBOUND_TS), + mUserCbExecutor, + mIkeSessionCallback, + mFirstChildSessionCallback); + } + + private byte[] buildInboundPkt(PortPair outPktSrcDestPortPair, String inboundDataHex) + throws Exception { + // Build inbound packet by flipping the outbound packet addresses and ports + return IkeTunUtils.buildIkePacket( + mRemoteAddress, + mLocalAddress, + outPktSrcDestPortPair.dstPort, + outPktSrcDestPortPair.srcPort, + true /* useEncap */, + hexStringToByteArray(inboundDataHex)); + } + + @Test + public void testRekeyIke() throws Exception { + final String ikeInitResp = + "46B8ECA1E0D72A1866B5248CF6C7472D21202220000000000000015022000030" + + "0000002C010100040300000C0100000C800E0100030000080300000C03000008" + + "0200000500000008040000022800008800020000920D3E830E7276908209212D" + + "E5A7F2A48706CFEF1BE8CB6E3B173B8B4E0D8C2DC626271FF1B13A88619E569E" + + "7B03C3ED2C127390749CDC7CDC711D0A8611E4457FFCBC4F0981B3288FBF58EA" + + "3E8B70E27E76AE70117FBBCB753660ADDA37EB5EB3A81BED6A374CCB7E132C2A" + + "94BFCE402DC76B19C158B533F6B1F2ABF01ACCC329000024B302CA2FB85B6CF4" + + "02313381246E3C53828D787F6DFEA6BD62D6405254AEE6242900001C00004004" + + "7A1682B06B58596533D00324886EF1F20EF276032900001C00004005BF633E31" + + "F9984B29A62E370BB2770FC09BAEA665290000080000402E290000100000402F" + + "00020003000400050000000800004014"; + final String ikeAuthResp = + "46B8ECA1E0D72A1866B5248CF6C7472D2E20232000000001000000F0240000D4" + + "10166CA8647F56123DE74C17FA5E256043ABF73216C812EE32EE1BB01EAF4A82" + + "DC107AB3ADBFEE0DEA5EEE10BDD5D43178F4C975C7C775D252273BB037283C7F" + + "236FE34A6BCE4833816897075DB2055B9FFD66DFA45A0A89A8F70AFB59431EED" + + "A20602FB614369D12906D3355CF7298A5D25364ABBCC75A9D88E0E6581449FCD" + + "4E361A39E00EFD1FD0A69651F63DB46C12470226AA21BA5EFF48FAF0B6DDF61C" + + "B0A69392CE559495EEDB4D1C1D80688434D225D57210A424C213F7C993D8A456" + + "38153FBD194C5E247B592D1D048DB4C8"; + final String rekeyIkeCreateReq = + "46B8ECA1E0D72A1866B5248CF6C7472D2E202400000000000000013021000114" + + "13743670039E308A8409BA5FD47B67F956B36FEE88AC3B70BB5D789B8218A135" + + "1B3D83E260E87B3EDB1BF064F09D4DC2611AEDBC99951B4B2DE767BD4AA2ACC3" + + "3653549CFC66B75869DF003CDC9A137A9CC27776AD5732B34203E74BE8CA4858" + + "1D5C0D9C9CA52D680EB299B4B21C7FA25FFEE174D57015E0FF2EAED653AAD95C" + + "071ABE269A8C2C9FBC1188E07550EB992F910D4CA9689E44BA66DE0FABB2BDF9" + + "8DD377186DBB25EF9B68B027BB2A27981779D8303D88D7CE860010A42862D50B" + + "1E0DBFD3D27C36F14809D7F493B2B96A65534CF98B0C32AD5219AD77F681AC04" + + "9D5CB89A0230A91A243FA7F16251B0D9B4B65E7330BEEAC9663EF4578991EAC8" + + "46C19EBB726E7D113F1D0D601102C05E"; + final String rekeyIkeDeleteReq = + "46B8ECA1E0D72A1866B5248CF6C7472D2E20250000000001000000502A000034" + + "02E40C0C7B1ED977729F705BB9B643FAC513A1070A6EB28ECD2AEA8A441ADC05" + + "7841382A7967BBF116AE52496590B2AD"; + final String deleteIkeReq = + "7D3DEDC65407D1FC9361C8CF8C47162A2E20250800000000000000502A000034" + + "201915C9E4E9173AA9EE79F3E02FE2D4954B22085C66D164762C34D347C16E9F" + + "FC5F7F114428C54D8D915860C57B1BC1"; + final long newIkeDeterministicInitSpi = Long.parseLong("7D3DEDC65407D1FC", 16); + + // Open IKE Session + IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress); + PortPair localRemotePorts = performSetupIkeAndFirstChildBlocking(ikeInitResp, ikeAuthResp); + + // Local request message ID starts from 2 because there is one IKE_INIT message and a single + // IKE_AUTH message. + int expectedReqMsgId = 2; + int expectedRespMsgId = 0; + + verifyIkeSessionSetupBlocking(); + verifyChildSessionSetupBlocking( + mFirstChildSessionCallback, + Arrays.asList(TRANSPORT_MODE_INBOUND_TS), + Arrays.asList(TRANSPORT_MODE_OUTBOUND_TS), + new ArrayList()); + IpSecTransformCallRecord firstTransformRecordA = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + IpSecTransformCallRecord firstTransformRecordB = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + verifyCreateIpSecTransformPair(firstTransformRecordA, firstTransformRecordB); + + // Inject rekey IKE requests + mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, rekeyIkeCreateReq)); + mTunUtils.awaitResp( + IKE_DETERMINISTIC_INITIATOR_SPI, expectedRespMsgId++, true /* expectedUseEncap */); + mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, rekeyIkeDeleteReq)); + mTunUtils.awaitResp( + IKE_DETERMINISTIC_INITIATOR_SPI, expectedRespMsgId++, true /* expectedUseEncap */); + + // IKE has been rekeyed, reset message IDs + expectedReqMsgId = 0; + expectedRespMsgId = 0; + + // Inject delete IKE request + mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, deleteIkeReq)); + mTunUtils.awaitResp( + newIkeDeterministicInitSpi, expectedRespMsgId++, true /* expectedUseEncap */); + + verifyDeleteIpSecTransformPair( + mFirstChildSessionCallback, firstTransformRecordA, firstTransformRecordB); + mFirstChildSessionCallback.awaitOnClosed(); + mIkeSessionCallback.awaitOnClosed(); + } + + @Test + public void testRekeyTransportModeChildSa() throws Exception { + final String ikeInitResp = + "46B8ECA1E0D72A18CECD871146CF83A121202220000000000000015022000030" + + "0000002C010100040300000C0100000C800E0100030000080300000C03000008" + + "0200000500000008040000022800008800020000C4904458957746BCF1C12972" + + "1D4E19EB8A584F78DE673053396D167CE0F34552DBC69BA63FE7C673B4CF4A99" + + "62481518EE985357876E8C47BAAA0DBE9C40AE47B12E52165874703586E8F786" + + "045F72EEEB238C5D1823352BED44B71B3214609276ADC0B3D42DAC820168C4E2" + + "660730DAAC92492403288805EBB9053F1AB060DA290000242D9364ACB93519FF" + + "8F8B019BAA43A40D699F59714B327B8382216EF427ED52282900001C00004004" + + "06D91438A0D6B734E152F76F5CC55A72A2E38A0A2900001C000040052EFF78B3" + + "55B37F3CE75AFF26C721B050F892C0D6290000080000402E290000100000402F" + + "00020003000400050000000800004014"; + final String ikeAuthResp = + "46B8ECA1E0D72A18CECD871146CF83A12E20232000000001000000F0240000D4" + + "A17BC258BA2714CF536663639DD5F665A60C75E93557CD5141990A8CEEDD2017" + + "93F5B181C8569FBCD6C2A00198EC2B62D42BEFAC016B8B6BF6A7BC9CEDE3413A" + + "6C495A6B8EC941864DC3E08F57D015EA6520C4B05884960B85478FCA53DA5F17" + + "9628BB1097DA77461C71837207A9EB80720B3E6E661816EE4E14AC995B5E8441" + + "A4C3F9097CC148142BA300076C94A23EC4ADE82B1DD2B121F7E9102860A8C3BF" + + "58DDC207285A3176E924C44DE820322524E1AA438EFDFBA781B36084AED80846" + + "3B77FCED9682B6E4E476408EF3F1037E"; + final String rekeyChildCreateReq = + "46B8ECA1E0D72A18CECD871146CF83A12E202400000000000000015029000134" + + "319D74B6B155B86942143CEC1D29D21F073F24B7BEDC9BFE0F0FDD8BDB5458C0" + + "8DB93506E1A43DD0640FE7370C97F9B34FF4EC9B2DB7257A87B75632301FB68A" + + "86B54871249534CA3D01C9BEB127B669F46470E1C8AAF72574C3CEEC15B901CF" + + "5A0D6ADAE59C3CA64AC8C86689C860FAF9500E608DFE63F2DCD30510FD6FFCD5" + + "A50838574132FD1D069BCACD4C7BAF45C9B1A7689FAD132E3F56DBCFAF905A8C" + + "4145D4BA1B74A54762F8F43308D94DE05649C49D885121CE30681D51AC1E3E68" + + "AB82F9A19B99579AFE257F32DBD1037814DA577379E4F42DEDAC84502E49C933" + + "9EA83F6F5DB4401B660CB1681B023B8603D205DFDD1DE86AD8DE22B6B754F30D" + + "05EAE81A709C2CEE81386133DC3DC7B5EF8F166E48E54A0722DD0C64F4D00638" + + "40F272144C47F6ECED72A248180645DB"; + final String rekeyChildDeleteReq = + "46B8ECA1E0D72A18CECD871146CF83A12E20250000000001000000502A000034" + + "02D98DAF0432EBD991CA4F2D89C1E0EFABC6E91A3327A85D8914FB2F1485BE1B" + + "8D3415D548F7CE0DC4224E7E9D0D3355"; + final String deleteIkeReq = + "46B8ECA1E0D72A18CECD871146CF83A12E20250000000002000000502A000034" + + "095041F4026B4634F04B0AB4F9349484F7BE9AEF03E3733EEE293330043B75D2" + + "ABF5F965ED51127629585E1B1BBA787F"; + + // Open IKE Session + IkeSession ikeSession = openIkeSessionWithRemoteAddress(mRemoteAddress); + PortPair localRemotePorts = performSetupIkeAndFirstChildBlocking(ikeInitResp, ikeAuthResp); + + // IKE INIT and IKE AUTH takes two exchanges. Local request message ID starts from 2 + int expectedReqMsgId = 2; + int expectedRespMsgId = 0; + + verifyIkeSessionSetupBlocking(); + verifyChildSessionSetupBlocking( + mFirstChildSessionCallback, + Arrays.asList(TRANSPORT_MODE_INBOUND_TS), + Arrays.asList(TRANSPORT_MODE_OUTBOUND_TS), + new ArrayList()); + IpSecTransformCallRecord oldTransformRecordA = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + IpSecTransformCallRecord oldTransformRecordB = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + verifyCreateIpSecTransformPair(oldTransformRecordA, oldTransformRecordB); + + // Inject rekey Child requests + mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, rekeyChildCreateReq)); + mTunUtils.awaitResp( + IKE_DETERMINISTIC_INITIATOR_SPI, expectedRespMsgId++, true /* expectedUseEncap */); + mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, rekeyChildDeleteReq)); + mTunUtils.awaitResp( + IKE_DETERMINISTIC_INITIATOR_SPI, expectedRespMsgId++, true /* expectedUseEncap */); + + // Verify IpSecTransforms are renewed + IpSecTransformCallRecord newTransformRecordA = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + IpSecTransformCallRecord newTransformRecordB = + mFirstChildSessionCallback.awaitNextCreatedIpSecTransform(); + verifyCreateIpSecTransformPair(newTransformRecordA, newTransformRecordB); + verifyDeleteIpSecTransformPair( + mFirstChildSessionCallback, oldTransformRecordA, oldTransformRecordB); + + // Inject delete IKE request + mTunUtils.injectPacket(buildInboundPkt(localRemotePorts, deleteIkeReq)); + mTunUtils.awaitResp( + IKE_DETERMINISTIC_INITIATOR_SPI, expectedRespMsgId++, true /* expectedUseEncap */); + + verifyDeleteIpSecTransformPair( + mFirstChildSessionCallback, newTransformRecordA, newTransformRecordB); + mFirstChildSessionCallback.awaitOnClosed(); + mIkeSessionCallback.awaitOnClosed(); + } +} diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionTestBase.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionTestBase.java index 279d088b3c..2458b25e33 100644 --- a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionTestBase.java +++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeSessionTestBase.java @@ -15,7 +15,13 @@ package android.net.ipsec.ike.cts; -import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS; +import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_FRAGMENTATION; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import android.annotation.NonNull; import android.app.AppOpsManager; @@ -23,6 +29,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.net.ConnectivityManager; import android.net.InetAddresses; +import android.net.IpSecManager; import android.net.IpSecTransform; import android.net.LinkAddress; import android.net.Network; @@ -33,7 +40,11 @@ import android.net.ipsec.ike.ChildSessionCallback; import android.net.ipsec.ike.ChildSessionConfiguration; import android.net.ipsec.ike.IkeSessionCallback; import android.net.ipsec.ike.IkeSessionConfiguration; +import android.net.ipsec.ike.IkeSessionConnectionInfo; import android.net.ipsec.ike.IkeTrafficSelector; +import android.net.ipsec.ike.TransportModeChildSessionParams; +import android.net.ipsec.ike.TunnelModeChildSessionParams; +import android.net.ipsec.ike.cts.IkeTunUtils.PortPair; import android.net.ipsec.ike.cts.TestNetworkUtils.TestNetworkCallback; import android.net.ipsec.ike.exceptions.IkeException; import android.net.ipsec.ike.exceptions.IkeProtocolException; @@ -55,6 +66,11 @@ import org.junit.runner.RunWith; import java.net.Inet4Address; import java.net.InetAddress; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -79,13 +95,39 @@ abstract class IkeSessionTestBase extends IkeTestBase { // Package-wide common expected results that will be shared by all IKE/Child SA creation tests static final String EXPECTED_REMOTE_APP_VERSION_EMPTY = ""; static final byte[] EXPECTED_PROTOCOL_ERROR_DATA_NONE = new byte[0]; + + static final InetAddress EXPECTED_DNS_SERVERS_ONE = + InetAddresses.parseNumericAddress("8.8.8.8"); + static final InetAddress EXPECTED_DNS_SERVERS_TWO = + InetAddresses.parseNumericAddress("8.8.4.4"); + static final InetAddress EXPECTED_INTERNAL_ADDR = InetAddresses.parseNumericAddress("198.51.100.10"); static final LinkAddress EXPECTED_INTERNAL_LINK_ADDR = new LinkAddress(EXPECTED_INTERNAL_ADDR, IP4_PREFIX_LEN); - static final IkeTrafficSelector EXPECTED_INBOUND_TS = + static final InetAddress EXPECTED_INTERNAL_ADDR_V6 = + InetAddresses.parseNumericAddress("2001:db8::2"); + static final LinkAddress EXPECTED_INTERNAL_LINK_ADDR_V6 = + new LinkAddress(EXPECTED_INTERNAL_ADDR_V6, IP6_PREFIX_LEN); + + static final IkeTrafficSelector TUNNEL_MODE_INBOUND_TS = new IkeTrafficSelector( MIN_PORT, MAX_PORT, EXPECTED_INTERNAL_ADDR, EXPECTED_INTERNAL_ADDR); + static final IkeTrafficSelector TUNNEL_MODE_OUTBOUND_TS = DEFAULT_V4_TS; + static final IkeTrafficSelector TUNNEL_MODE_INBOUND_TS_V6 = + new IkeTrafficSelector( + MIN_PORT, MAX_PORT, EXPECTED_INTERNAL_ADDR_V6, EXPECTED_INTERNAL_ADDR_V6); + static final IkeTrafficSelector TUNNEL_MODE_OUTBOUND_TS_V6 = DEFAULT_V6_TS; + + // This value is align with the test vectors hex that are generated in an IPv4 environment + static final IkeTrafficSelector TRANSPORT_MODE_OUTBOUND_TS = + new IkeTrafficSelector( + MIN_PORT, + MAX_PORT, + InetAddresses.parseNumericAddress("10.138.0.2"), + InetAddresses.parseNumericAddress("10.138.0.2")); + + static final long IKE_DETERMINISTIC_INITIATOR_SPI = Long.parseLong("46B8ECA1E0D72A18", 16); // Static state to reduce setup/teardown static Context sContext = InstrumentationRegistry.getContext(); @@ -124,19 +166,12 @@ abstract class IkeSessionTestBase extends IkeTestBase { .getUiAutomation() .adoptShellPermissionIdentity(); sTNM = sContext.getSystemService(TestNetworkManager.class); - - // Under normal circumstances, the MANAGE_IPSEC_TUNNELS appop would be auto-granted, and - // a standard permission is insufficient. So we shell out the appop, to give us the - // right appop permissions. - setAppOp(OP_MANAGE_IPSEC_TUNNELS, true); } // This method is guaranteed to run in subclasses and will run after subclasses' @AfterClass // methods. @AfterClass public static void tearDownPermissionAfterClass() throws Exception { - setAppOp(OP_MANAGE_IPSEC_TUNNELS, false); - InstrumentationRegistry.getInstrumentation() .getUiAutomation() .dropShellPermissionIdentity(); @@ -159,7 +194,7 @@ abstract class IkeSessionTestBase extends IkeTestBase { } void setUpTestNetwork(InetAddress localAddr) throws Exception { - int prefixLen = localAddr instanceof Inet4Address ? IP4_PREFIX_LEN : IP4_PREFIX_LEN; + int prefixLen = localAddr instanceof Inet4Address ? IP4_PREFIX_LEN : IP6_PREFIX_LEN; TestNetworkInterface testIface = sTNM.createTunInterface(new LinkAddress[] {new LinkAddress(localAddr, prefixLen)}); @@ -179,7 +214,7 @@ abstract class IkeSessionTestBase extends IkeTestBase { mTunFd.close(); } - private static void setAppOp(int appop, boolean allow) { + static void setAppOp(int appop, boolean allow) { String opName = AppOpsManager.opToName(appop); for (String pkg : new String[] {"com.android.shell", sContext.getPackageName()}) { String cmd = @@ -231,6 +266,78 @@ abstract class IkeSessionTestBase extends IkeTestBase { } } + TransportModeChildSessionParams buildTransportModeChildParamsWithTs( + IkeTrafficSelector inboundTs, IkeTrafficSelector outboundTs) { + return new TransportModeChildSessionParams.Builder() + .addSaProposal(SaProposalTest.buildChildSaProposalWithCombinedModeCipher()) + .addSaProposal(SaProposalTest.buildChildSaProposalWithNormalModeCipher()) + .addInboundTrafficSelectors(inboundTs) + .addOutboundTrafficSelectors(outboundTs) + .build(); + } + + TunnelModeChildSessionParams buildTunnelModeChildSessionParams() { + return new TunnelModeChildSessionParams.Builder() + .addSaProposal(SaProposalTest.buildChildSaProposalWithNormalModeCipher()) + .addSaProposal(SaProposalTest.buildChildSaProposalWithCombinedModeCipher()) + .addInternalAddressRequest(AF_INET) + .addInternalAddressRequest(AF_INET6) + .build(); + } + + PortPair performSetupIkeAndFirstChildBlocking(String ikeInitRespHex, String... ikeAuthRespHexes) + throws Exception { + return performSetupIkeAndFirstChildBlocking( + ikeInitRespHex, + 1 /* expectedAuthReqPktCnt */, + true /*expectedAuthUseEncap*/, + ikeAuthRespHexes); + } + + PortPair performSetupIkeAndFirstChildBlocking( + String ikeInitRespHex, boolean expectedAuthUseEncap, String... ikeAuthRespHexes) + throws Exception { + return performSetupIkeAndFirstChildBlocking( + ikeInitRespHex, + 1 /* expectedAuthReqPktCnt */, + expectedAuthUseEncap, + ikeAuthRespHexes); + } + + PortPair performSetupIkeAndFirstChildBlocking( + String ikeInitRespHex, + int expectedAuthReqPktCnt, + boolean expectedAuthUseEncap, + String... ikeAuthRespHexes) + throws Exception { + mTunUtils.awaitReqAndInjectResp( + IKE_DETERMINISTIC_INITIATOR_SPI, + 0 /* expectedMsgId */, + false /* expectedUseEncap */, + ikeInitRespHex); + + byte[] ikeAuthReqPkt = + mTunUtils + .awaitReqAndInjectResp( + IKE_DETERMINISTIC_INITIATOR_SPI, + 1 /* expectedMsgId */, + expectedAuthUseEncap, + expectedAuthReqPktCnt, + ikeAuthRespHexes) + .get(0); + return IkeTunUtils.getSrcDestPortPair(ikeAuthReqPkt); + } + + void performCloseIkeBlocking(int expectedMsgId, String deleteIkeRespHex) throws Exception { + performCloseIkeBlocking(expectedMsgId, true /* expectedUseEncap*/, deleteIkeRespHex); + } + + void performCloseIkeBlocking( + int expectedMsgId, boolean expectedUseEncap, String deleteIkeRespHex) throws Exception { + mTunUtils.awaitReqAndInjectResp( + IKE_DETERMINISTIC_INITIATOR_SPI, expectedMsgId, expectedUseEncap, deleteIkeRespHex); + } + /** Testing callback that allows caller to block current thread until a method get called */ static class TestIkeSessionCallback implements IkeSessionCallback { private CompletableFuture mFutureIkeConfig = @@ -370,6 +477,109 @@ abstract class IkeSessionTestBase extends IkeTestBase { this.ipSecTransform = ipSecTransform; this.direction = direction; } + + @Override + public int hashCode() { + return Objects.hash(ipSecTransform, direction); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof IpSecTransformCallRecord)) return false; + + IpSecTransformCallRecord record = (IpSecTransformCallRecord) o; + return ipSecTransform.equals(record.ipSecTransform) && direction == record.direction; + } + } + + void verifyIkeSessionSetupBlocking() throws Exception { + IkeSessionConfiguration ikeConfig = mIkeSessionCallback.awaitIkeConfig(); + assertNotNull(ikeConfig); + assertEquals(EXPECTED_REMOTE_APP_VERSION_EMPTY, ikeConfig.getRemoteApplicationVersion()); + assertTrue(ikeConfig.getRemoteVendorIds().isEmpty()); + assertTrue(ikeConfig.getPcscfServers().isEmpty()); + assertTrue(ikeConfig.isIkeExtensionEnabled(EXTENSION_TYPE_FRAGMENTATION)); + + IkeSessionConnectionInfo ikeConnectInfo = ikeConfig.getIkeSessionConnectionInfo(); + assertNotNull(ikeConnectInfo); + assertEquals(mLocalAddress, ikeConnectInfo.getLocalAddress()); + assertEquals(mRemoteAddress, ikeConnectInfo.getRemoteAddress()); + assertEquals(mTunNetwork, ikeConnectInfo.getNetwork()); + } + + void verifyChildSessionSetupBlocking( + TestChildSessionCallback childCallback, + List expectedInboundTs, + List expectedOutboundTs, + List expectedInternalAddresses) + throws Exception { + verifyChildSessionSetupBlocking( + childCallback, + expectedInboundTs, + expectedOutboundTs, + expectedInternalAddresses, + new ArrayList() /* expectedDnsServers */); + } + + void verifyChildSessionSetupBlocking( + TestChildSessionCallback childCallback, + List expectedInboundTs, + List expectedOutboundTs, + List expectedInternalAddresses, + List expectedDnsServers) + throws Exception { + ChildSessionConfiguration childConfig = childCallback.awaitChildConfig(); + assertNotNull(childConfig); + assertEquals(expectedInboundTs, childConfig.getInboundTrafficSelectors()); + assertEquals(expectedOutboundTs, childConfig.getOutboundTrafficSelectors()); + assertEquals(expectedInternalAddresses, childConfig.getInternalAddresses()); + assertEquals(expectedDnsServers, childConfig.getInternalDnsServers()); + assertTrue(childConfig.getInternalSubnets().isEmpty()); + assertTrue(childConfig.getInternalDhcpServers().isEmpty()); + } + + void verifyCloseIkeAndChildBlocking( + IpSecTransformCallRecord expectedTransformRecordA, + IpSecTransformCallRecord expectedTransformRecordB) + throws Exception { + verifyDeleteIpSecTransformPair( + mFirstChildSessionCallback, expectedTransformRecordA, expectedTransformRecordB); + mFirstChildSessionCallback.awaitOnClosed(); + mIkeSessionCallback.awaitOnClosed(); + } + + static void verifyCreateIpSecTransformPair( + IpSecTransformCallRecord transformRecordA, IpSecTransformCallRecord transformRecordB) { + IpSecTransform transformA = transformRecordA.ipSecTransform; + IpSecTransform transformB = transformRecordB.ipSecTransform; + + assertNotNull(transformA); + assertNotNull(transformB); + + Set expectedDirections = new HashSet<>(); + expectedDirections.add(IpSecManager.DIRECTION_IN); + expectedDirections.add(IpSecManager.DIRECTION_OUT); + + Set resultDirections = new HashSet<>(); + resultDirections.add(transformRecordA.direction); + resultDirections.add(transformRecordB.direction); + + assertEquals(expectedDirections, resultDirections); + } + + static void verifyDeleteIpSecTransformPair( + TestChildSessionCallback childCb, + IpSecTransformCallRecord expectedTransformRecordA, + IpSecTransformCallRecord expectedTransformRecordB) { + Set expectedTransforms = new HashSet<>(); + expectedTransforms.add(expectedTransformRecordA); + expectedTransforms.add(expectedTransformRecordB); + + Set resultTransforms = new HashSet<>(); + resultTransforms.add(childCb.awaitNextDeletedIpSecTransform()); + resultTransforms.add(childCb.awaitNextDeletedIpSecTransform()); + + assertEquals(expectedTransforms, resultTransforms); } /** Package private method to check if device has IPsec tunnels feature */ @@ -377,7 +587,5 @@ abstract class IkeSessionTestBase extends IkeTestBase { return sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS); } - // TODO(b/148689509): Verify IKE Session setup using EAP and digital-signature-based auth - // TODO(b/148689509): Verify hostname based creation } diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java index f07c710ba9..c70e5372ec 100644 --- a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java +++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTestBase.java @@ -54,7 +54,7 @@ abstract class IkeTestBase { static final int SUB_ID = 1; static final byte[] EAP_IDENTITY = "test@android.net".getBytes(); static final String NETWORK_NAME = "android.net"; - static final String EAP_MSCHAPV2_USERNAME = "username"; + static final String EAP_MSCHAPV2_USERNAME = "mschapv2user"; static final String EAP_MSCHAPV2_PASSWORD = "password"; static final Inet4Address IPV4_ADDRESS_LOCAL = diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTunUtils.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTunUtils.java index f52b88ba3a..41cbf0baa1 100644 --- a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTunUtils.java +++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/IkeTunUtils.java @@ -26,6 +26,8 @@ import static android.net.ipsec.ike.cts.PacketUtils.UDP_HDRLEN; import static android.net.ipsec.ike.cts.PacketUtils.UdpHeader; import static android.system.OsConstants.IPPROTO_UDP; +import static com.android.internal.util.HexDump.hexStringToByteArray; + import static org.junit.Assert.fail; import android.os.ParcelFileDescriptor; @@ -34,7 +36,10 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; public class IkeTunUtils extends TunUtils { private static final int PORT_LEN = 2; @@ -42,63 +47,132 @@ public class IkeTunUtils extends TunUtils { private static final int NON_ESP_MARKER_LEN = 4; private static final byte[] NON_ESP_MARKER = new byte[NON_ESP_MARKER_LEN]; - private static final int IKE_HEADER_LEN = 28; private static final int IKE_INIT_SPI_OFFSET = 0; + private static final int IKE_FIRST_PAYLOAD_OFFSET = 16; private static final int IKE_IS_RESP_BYTE_OFFSET = 19; private static final int IKE_MSG_ID_OFFSET = 20; + private static final int IKE_HEADER_LEN = 28; + private static final int IKE_FRAG_NUM_OFFSET = 32; + private static final int IKE_PAYLOAD_TYPE_SKF = 53; + + private static final int RSP_FLAG_MASK = 0x20; public IkeTunUtils(ParcelFileDescriptor tunFd) { super(tunFd); } /** - * Await the expected IKE request and inject an IKE response. + * Await the expected IKE request inject an IKE response (or a list of response fragments) * - * @param respIkePkt IKE response packet without IP/UDP headers or NON ESP MARKER. + * @param ikeRespDataFragmentsHex IKE response hex (or a list of response fragments) without + * IP/UDP headers or NON ESP MARKER. */ public byte[] awaitReqAndInjectResp( - long expectedInitIkeSpi, int expectedMsgId, boolean expectedUseEncap, byte[] respIkePkt) + long expectedInitIkeSpi, + int expectedMsgId, + boolean expectedUseEncap, + String... ikeRespDataFragmentsHex) throws Exception { - byte[] request = - awaitIkePacket( + return awaitReqAndInjectResp( expectedInitIkeSpi, expectedMsgId, - false /* expectedResp */, - expectedUseEncap); + expectedUseEncap, + 1 /* expectedReqPktCnt */, + ikeRespDataFragmentsHex) + .get(0); + } + + /** + * Await the expected IKE request (or the list of IKE request fragments) and inject an IKE + * response (or a list of response fragments) + * + * @param ikeRespDataFragmentsHex IKE response hex (or a list of response fragments) without + * IP/UDP headers or NON ESP MARKER. + */ + public List awaitReqAndInjectResp( + long expectedInitIkeSpi, + int expectedMsgId, + boolean expectedUseEncap, + int expectedReqPktCnt, + String... ikeRespDataFragmentsHex) + throws Exception { + List reqList = new ArrayList<>(expectedReqPktCnt); + if (expectedReqPktCnt == 1) { + // Expecting one complete IKE packet + byte[] req = + awaitIkePacket( + (pkt) -> { + return isExpectedIkePkt( + pkt, + expectedInitIkeSpi, + expectedMsgId, + false /* expectedResp */, + expectedUseEncap); + }); + reqList.add(req); + } else { + // Expecting "expectedReqPktCnt" number of request fragments + for (int i = 0; i < expectedReqPktCnt; i++) { + // IKE Fragment number always starts from 1 + int expectedFragNum = i + 1; + byte[] req = + awaitIkePacket( + (pkt) -> { + return isExpectedIkeFragPkt( + pkt, + expectedInitIkeSpi, + expectedMsgId, + false /* expectedResp */, + expectedUseEncap, + expectedFragNum); + }); + reqList.add(req); + } + } + + // All request fragments have the same addresses and ports + byte[] request = reqList.get(0); // Build response header by flipping address and port InetAddress srcAddr = getAddress(request, false /* shouldGetSource */); InetAddress dstAddr = getAddress(request, true /* shouldGetSource */); int srcPort = getPort(request, false /* shouldGetSource */); int dstPort = getPort(request, true /* shouldGetSource */); + for (String resp : ikeRespDataFragmentsHex) { + byte[] response = + buildIkePacket( + srcAddr, + dstAddr, + srcPort, + dstPort, + expectedUseEncap, + hexStringToByteArray(resp)); + injectPacket(response); + } - byte[] response = - buildIkePacket(srcAddr, dstAddr, srcPort, dstPort, expectedUseEncap, respIkePkt); - injectPacket(response); - return request; + return reqList; } - private byte[] awaitIkePacket( - long expectedInitIkeSpi, - int expectedMsgId, - boolean expectedResp, - boolean expectedUseEncap) + /** Await the expected IKE response */ + public byte[] awaitResp(long expectedInitIkeSpi, int expectedMsgId, boolean expectedUseEncap) throws Exception { + return awaitIkePacket( + (pkt) -> { + return isExpectedIkePkt( + pkt, + expectedInitIkeSpi, + expectedMsgId, + true /* expectedResp*/, + expectedUseEncap); + }); + } + + private byte[] awaitIkePacket(Predicate pktVerifier) throws Exception { long endTime = System.currentTimeMillis() + TIMEOUT; int startIndex = 0; synchronized (mPackets) { while (System.currentTimeMillis() < endTime) { - byte[] ikePkt = - getFirstMatchingPacket( - (pkt) -> { - return isIke( - pkt, - expectedInitIkeSpi, - expectedMsgId, - expectedResp, - expectedUseEncap); - }, - startIndex); + byte[] ikePkt = getFirstMatchingPacket(pktVerifier, startIndex); if (ikePkt != null) { return ikePkt; // We've found the packet we're looking for. } @@ -112,51 +186,51 @@ public class IkeTunUtils extends TunUtils { } } - String direction = expectedResp ? "response" : "request"; - fail( - "No such IKE " - + direction - + " found with Initiator SPI " - + expectedInitIkeSpi - + " and message ID " - + expectedMsgId); + fail("No matching packet found"); } throw new IllegalStateException( "Hit an impossible case where fail() didn't throw an exception"); } - private static boolean isIke( + private static boolean isExpectedIkePkt( byte[] pkt, long expectedInitIkeSpi, int expectedMsgId, boolean expectedResp, boolean expectedUseEncap) { - int ipProtocolOffset = 0; - int ikeOffset = 0; - if (isIpv6(pkt)) { - // IPv6 UDP expectedUseEncap not supported by kernels; assume non-expectedUseEncap. - ipProtocolOffset = IP6_PROTO_OFFSET; - ikeOffset = IP6_HDRLEN + UDP_HDRLEN; - } else { - // Use default IPv4 header length (assuming no options) - ipProtocolOffset = IP4_PROTO_OFFSET; - ikeOffset = IP4_HDRLEN + UDP_HDRLEN; - - if (expectedUseEncap) { - if (hasNonEspMarker(pkt)) { - ikeOffset += NON_ESP_MARKER_LEN; - } else { - return false; - } - } - } + int ipProtocolOffset = isIpv6(pkt) ? IP6_PROTO_OFFSET : IP4_PROTO_OFFSET; + int ikeOffset = getIkeOffset(pkt, expectedUseEncap); return pkt[ipProtocolOffset] == IPPROTO_UDP - && areSpiAndMsgIdEqual( + && expectedUseEncap == hasNonEspMarker(pkt) + && isExpectedSpiAndMsgId( pkt, ikeOffset, expectedInitIkeSpi, expectedMsgId, expectedResp); } + private static boolean isExpectedIkeFragPkt( + byte[] pkt, + long expectedInitIkeSpi, + int expectedMsgId, + boolean expectedResp, + boolean expectedUseEncap, + int expectedFragNum) { + return isExpectedIkePkt( + pkt, expectedInitIkeSpi, expectedMsgId, expectedResp, expectedUseEncap) + && isExpectedFragNum(pkt, getIkeOffset(pkt, expectedUseEncap), expectedFragNum); + } + + private static int getIkeOffset(byte[] pkt, boolean useEncap) { + if (isIpv6(pkt)) { + // IPv6 UDP expectedUseEncap not supported by kernels; assume non-expectedUseEncap. + return IP6_HDRLEN + UDP_HDRLEN; + } else { + // Use default IPv4 header length (assuming no options) + int ikeOffset = IP4_HDRLEN + UDP_HDRLEN; + return useEncap ? ikeOffset + NON_ESP_MARKER_LEN : ikeOffset; + } + } + private static boolean hasNonEspMarker(byte[] pkt) { ByteBuffer buffer = ByteBuffer.wrap(pkt); int ikeOffset = IP4_HDRLEN + UDP_HDRLEN; @@ -170,23 +244,81 @@ public class IkeTunUtils extends TunUtils { return Arrays.equals(NON_ESP_MARKER, nonEspMarker); } - private static boolean areSpiAndMsgIdEqual( + private static boolean isExpectedSpiAndMsgId( byte[] pkt, int ikeOffset, - long expectedIkeInitSpi, + long expectedInitIkeSpi, int expectedMsgId, boolean expectedResp) { if (pkt.length <= ikeOffset + IKE_HEADER_LEN) return false; ByteBuffer buffer = ByteBuffer.wrap(pkt); buffer.get(new byte[ikeOffset]); // Skip IP, UDP header (and NON_ESP_MARKER) + buffer.mark(); // Mark this position so that later we can reset back here - // Check message ID. + // Check SPI + buffer.get(new byte[IKE_INIT_SPI_OFFSET]); + long initSpi = buffer.getLong(); + if (expectedInitIkeSpi != initSpi) { + return false; + } + + // Check direction + buffer.reset(); + buffer.get(new byte[IKE_IS_RESP_BYTE_OFFSET]); + byte flagsByte = buffer.get(); + boolean isResp = ((flagsByte & RSP_FLAG_MASK) != 0); + if (expectedResp != isResp) { + return false; + } + + // Check message ID + buffer.reset(); buffer.get(new byte[IKE_MSG_ID_OFFSET]); - int msgId = buffer.getInt(); - return expectedMsgId == msgId; - // TODO: Check SPI and packet direction + // Both the expected message ID and the packet's msgId are signed integers, so directly + // compare them. + int msgId = buffer.getInt(); + if (expectedMsgId != msgId) { + return false; + } + + return true; + } + + private static boolean isExpectedFragNum(byte[] pkt, int ikeOffset, int expectedFragNum) { + ByteBuffer buffer = ByteBuffer.wrap(pkt); + buffer.get(new byte[ikeOffset]); + buffer.mark(); // Mark this position so that later we can reset back here + + // Check if it is a fragment packet + buffer.get(new byte[IKE_FIRST_PAYLOAD_OFFSET]); + int firstPayload = Byte.toUnsignedInt(buffer.get()); + if (firstPayload != IKE_PAYLOAD_TYPE_SKF) { + return false; + } + + // Check fragment number + buffer.reset(); + buffer.get(new byte[IKE_FRAG_NUM_OFFSET]); + int fragNum = Short.toUnsignedInt(buffer.getShort()); + return expectedFragNum == fragNum; + } + + public static class PortPair { + public final int srcPort; + public final int dstPort; + + public PortPair(int sourcePort, int destinationPort) { + srcPort = sourcePort; + dstPort = destinationPort; + } + } + + public static PortPair getSrcDestPortPair(byte[] outboundIkePkt) throws Exception { + return new PortPair( + getPort(outboundIkePkt, true /* shouldGetSource */), + getPort(outboundIkePkt, false /* shouldGetSource */)); } private static InetAddress getAddress(byte[] pkt, boolean shouldGetSource) throws Exception { @@ -210,7 +342,7 @@ public class IkeTunUtils extends TunUtils { return Short.toUnsignedInt(buffer.getShort()); } - private static byte[] buildIkePacket( + public static byte[] buildIkePacket( InetAddress srcAddr, InetAddress dstAddr, int srcPort, diff --git a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java index cb1d8269d7..5539dbca23 100644 --- a/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java +++ b/tests/cts/net/ipsec/src/android/net/ipsec/ike/cts/TunUtils.java @@ -47,7 +47,7 @@ public class TunUtils { private static final String TAG = TunUtils.class.getSimpleName(); private static final int DATA_BUFFER_LEN = 4096; - static final int TIMEOUT = 100; + static final int TIMEOUT = 500; static final int IP4_PROTO_OFFSET = 9; static final int IP6_PROTO_OFFSET = 6; diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalApiTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalApiTest.kt new file mode 100644 index 0000000000..40d0ca65c7 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/CaptivePortalApiTest.kt @@ -0,0 +1,271 @@ +/* + * 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 android.net.cts + +import android.Manifest.permission.MANAGE_TEST_NETWORKS +import android.Manifest.permission.NETWORK_SETTINGS +import android.content.Context +import android.net.ConnectivityManager +import android.net.EthernetManager +import android.net.InetAddresses +import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL +import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED +import android.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkRequest +import android.net.TestNetworkInterface +import android.net.TestNetworkManager +import android.net.Uri +import android.net.dhcp.DhcpDiscoverPacket +import android.net.dhcp.DhcpPacket +import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE +import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_DISCOVER +import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_REQUEST +import android.net.dhcp.DhcpRequestPacket +import android.net.shared.Inet4AddressUtils.getBroadcastAddress +import android.net.shared.Inet4AddressUtils.getPrefixMaskAsInet4Address +import android.os.Build +import android.os.HandlerThread +import android.platform.test.annotations.AppModeFull +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 +import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity +import com.android.compatibility.common.util.ThrowingRunnable +import com.android.server.util.NetworkStackConstants.IPV4_ADDR_ANY +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DhcpClientPacketFilter +import com.android.testutils.DhcpOptionFilter +import com.android.testutils.RecorderCallback.CallbackEntry +import com.android.testutils.TapPacketReader +import com.android.testutils.TestableNetworkCallback +import fi.iki.elonen.NanoHTTPD +import org.junit.After +import org.junit.Assume.assumeFalse +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.net.Inet4Address +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.TimeUnit +import kotlin.reflect.KClass +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail + +private const val MAX_PACKET_LENGTH = 1500 +private const val TEST_TIMEOUT_MS = 10_000L + +private const val TEST_LEASE_TIMEOUT_SECS = 3600 * 12 +private const val TEST_PREFIX_LENGTH = 24 + +private const val TEST_LOGIN_URL = "https://login.capport.android.com" +private const val TEST_VENUE_INFO_URL = "https://venueinfo.capport.android.com" +private const val TEST_DOMAIN_NAME = "lan" +private const val TEST_MTU = 1500.toShort() + +@AppModeFull(reason = "Instant apps cannot create test networks") +@RunWith(AndroidJUnit4::class) +class CaptivePortalApiTest { + @JvmField + @Rule + val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q) + + private val context by lazy { InstrumentationRegistry.getInstrumentation().context } + private val tnm by lazy { context.assertHasService(TestNetworkManager::class.java) } + private val eth by lazy { context.assertHasService(EthernetManager::class.java) } + private val cm by lazy { context.assertHasService(ConnectivityManager::class.java) } + + private val handlerThread = HandlerThread(CaptivePortalApiTest::class.simpleName) + private val serverIpAddr = InetAddresses.parseNumericAddress("192.0.2.222") as Inet4Address + private val clientIpAddr = InetAddresses.parseNumericAddress("192.0.2.111") as Inet4Address + private val httpServer = HttpServer() + private val ethRequest = NetworkRequest.Builder() + // ETHERNET|TEST transport networks do not have NET_CAPABILITY_TRUSTED + .removeCapability(NET_CAPABILITY_TRUSTED) + .addTransportType(TRANSPORT_ETHERNET).build() + private val ethRequestCb = TestableNetworkCallback() + + private lateinit var iface: TestNetworkInterface + private lateinit var reader: TapPacketReader + private lateinit var capportUrl: Uri + + private var testSkipped = false + + @Before + fun setUp() { + // This test requires using a tap interface as the default ethernet interface: skip if there + // is already an ethernet interface connected. + testSkipped = eth.isAvailable() + assumeFalse(testSkipped) + + // Register a request so the network does not get torn down + cm.requestNetwork(ethRequest, ethRequestCb) + runAsShell(NETWORK_SETTINGS, MANAGE_TEST_NETWORKS) { + eth.setIncludeTestInterfaces(true) + // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor + // does not go out of scope, which would cause it to close the underlying FileDescriptor + // in its finalizer. + iface = tnm.createTapInterface() + } + + handlerThread.start() + reader = TapPacketReader( + handlerThread.threadHandler, + iface.fileDescriptor.fileDescriptor, + MAX_PACKET_LENGTH) + handlerThread.threadHandler.post { reader.start() } + httpServer.start() + + // Pad the listening port to make sure it is always of length 5. This ensures the URL has + // always the same length so the test can use constant IP and UDP header lengths. + // The maximum port number is 65535 so a length of 5 is always enough. + capportUrl = Uri.parse("http://localhost:${httpServer.listeningPort}/testapi.html?par=val") + } + + @After + fun tearDown() { + if (testSkipped) return + cm.unregisterNetworkCallback(ethRequestCb) + + runAsShell(NETWORK_SETTINGS) { eth.setIncludeTestInterfaces(false) } + + httpServer.stop() + handlerThread.threadHandler.post { reader.stop() } + handlerThread.quitSafely() + + iface.fileDescriptor.close() + } + + @Test + fun testApiCallbacks() { + // Handle the DHCP handshake that includes the capport API URL + val discover = reader.assertDhcpPacketReceived( + DhcpDiscoverPacket::class, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_DISCOVER) + reader.sendResponse(makeOfferPacket(discover.clientMac, discover.transactionId)) + + val request = reader.assertDhcpPacketReceived( + DhcpRequestPacket::class, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_REQUEST) + assertEquals(discover.transactionId, request.transactionId) + assertEquals(clientIpAddr, request.mRequestedIp) + reader.sendResponse(makeAckPacket(request.clientMac, request.transactionId)) + + // Expect a request to the capport API + val capportReq = httpServer.recordedRequests.poll(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS) + assertNotNull(capportReq, "The device did not fetch captive portal API data within timeout") + assertEquals(capportUrl.path, capportReq.uri) + assertEquals(capportUrl.query, capportReq.queryParameterString) + + // Expect network callbacks with capport info + val testCb = TestableNetworkCallback(TEST_TIMEOUT_MS) + // LinkProperties do not contain captive portal info if the callback is registered without + // NETWORK_SETTINGS permissions. + val lp = runAsShell(NETWORK_SETTINGS) { + cm.registerNetworkCallback(ethRequest, testCb) + + try { + val ncCb = testCb.eventuallyExpect { + it.caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) + } + testCb.eventuallyExpect { + it.network == ncCb.network && it.lp.captivePortalData != null + }.lp + } finally { + cm.unregisterNetworkCallback(testCb) + } + } + + assertEquals(capportUrl, lp.captivePortalApiUrl) + with(lp.captivePortalData) { + assertNotNull(this) + assertTrue(isCaptive) + assertEquals(Uri.parse(TEST_LOGIN_URL), userPortalUrl) + assertEquals(Uri.parse(TEST_VENUE_INFO_URL), venueInfoUrl) + } + } + + private fun makeOfferPacket(clientMac: ByteArray, transactionId: Int) = + DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_L2, transactionId, + false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr, + clientMac, TEST_LEASE_TIMEOUT_SECS, + getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH), + getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH), + listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */, + serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */, + TEST_MTU, capportUrl.toString()) + + private fun makeAckPacket(clientMac: ByteArray, transactionId: Int) = + DhcpPacket.buildAckPacket(DhcpPacket.ENCAP_L2, transactionId, + false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr, + clientIpAddr /* requestClientIp */, clientMac, TEST_LEASE_TIMEOUT_SECS, + getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH), + getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH), + listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */, + serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */, + TEST_MTU, false /* rapidCommit */, capportUrl.toString()) + + private fun parseDhcpPacket(bytes: ByteArray) = DhcpPacket.decodeFullPacket( + bytes, MAX_PACKET_LENGTH, DhcpPacket.ENCAP_L2) +} + +/** + * A minimal HTTP server running on localhost (loopback), on a random available port. + * + * The server records each request in [recordedRequests] and will not serve any further request + * until the last one is removed from the queue for verification. + */ +private class HttpServer : NanoHTTPD("localhost", 0 /* auto-select the port */) { + val recordedRequests = ArrayBlockingQueue(1 /* capacity */) + + override fun serve(session: IHTTPSession): Response { + recordedRequests.offer(session) + return newFixedLengthResponse(""" + |{ + | "captive": true, + | "user-portal-url": "$TEST_LOGIN_URL", + | "venue-info-url": "$TEST_VENUE_INFO_URL" + |} + """.trimMargin()) + } +} + +private fun TapPacketReader.assertDhcpPacketReceived( + packetType: KClass, + timeoutMs: Long, + type: Byte +): T { + val packetBytes = popPacket(timeoutMs, DhcpClientPacketFilter() + .and(DhcpOptionFilter(DHCP_MESSAGE_TYPE, type))) + ?: fail("${packetType.simpleName} not received within timeout") + val packet = DhcpPacket.decodeFullPacket(packetBytes, packetBytes.size, DhcpPacket.ENCAP_L2) + assertTrue(packetType.isInstance(packet), + "Expected ${packetType.simpleName} but got ${packet.javaClass.simpleName}") + return packetType.java.cast(packet) +} + +private fun Context.assertHasService(manager: Class): T { + return getSystemService(manager) ?: fail("Service $manager not found") +} + +/** + * Wrapper around runWithShellPermissionIdentity with kotlin-like syntax. + */ +private fun runAsShell(vararg permissions: String, task: () -> T): T { + var ret: T? = null + runWithShellPermissionIdentity(ThrowingRunnable { ret = task() }, *permissions) + return ret ?: fail("ThrowingRunnable was not run") +}