mirror of
https://github.com/bitwarden/android.git
synced 2026-11-09 10:34:18 -06:00
[PM-46454] fix: surface local network permission recovery after IPv6 connection timeouts (#7581)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
co-authored by
Matt Van Horn
parent
63d1e6457b
commit
acb080aff4
@@ -6,9 +6,16 @@ import com.bitwarden.network.provider.PermissionProvider
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
|
||||
/**
|
||||
* Mask and prefix for the first byte of IPv6 unique local addresses (fc00::/7).
|
||||
*/
|
||||
private const val IPV6_UNIQUE_LOCAL_MASK: Int = 0xFE
|
||||
private const val IPV6_UNIQUE_LOCAL_PREFIX: Int = 0xFC
|
||||
|
||||
/**
|
||||
* Interceptor responsible for determining if the destination of the network request is on the
|
||||
* local network or not.
|
||||
@@ -39,5 +46,15 @@ private fun Interceptor.Chain.isLocalRequest(): Boolean {
|
||||
// A security exception has occurred, lets be safe and assume it's a local request.
|
||||
return true
|
||||
}
|
||||
return address.isSiteLocalAddress
|
||||
return address.isSiteLocalAddress || address.isLocalIpv6Address()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is an IPv6 link-local (fe80::/10) or unique local (fc00::/7) address.
|
||||
*/
|
||||
private fun InetAddress.isLocalIpv6Address(): Boolean {
|
||||
if (this !is Inet6Address) return false
|
||||
val isUniqueLocal =
|
||||
(address.first().toInt() and IPV6_UNIQUE_LOCAL_MASK) == IPV6_UNIQUE_LOCAL_PREFIX
|
||||
return isLinkLocalAddress || isUniqueLocal
|
||||
}
|
||||
|
||||
+64
@@ -15,6 +15,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
|
||||
@@ -101,6 +102,54 @@ class PermissionInterceptorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `intercept should acquire permission and throw when request targets local IPv6 address without permission`() {
|
||||
val errorMessage = "Local network access required"
|
||||
every { permissionProvider.hasLocalNetworkAccessPermission } returns false
|
||||
every { permissionProvider.errorMessageString } returns errorMessage
|
||||
every { permissionProvider.acquireLocalNetworkAccessPermission() } just runs
|
||||
listOf(
|
||||
"fd00::1" to ipv6Address(first = 0xFD, second = 0x00),
|
||||
"fc12::1" to ipv6Address(first = 0xFC, second = 0x12),
|
||||
"fe80::1" to ipv6Address(first = 0xFE, second = 0x80),
|
||||
).forEach { (host, address) ->
|
||||
every { InetAddress.getByName(host) } returns address
|
||||
val chain = FakeInterceptorChain(
|
||||
request = Request.Builder().url("http://[$host]/api").build(),
|
||||
)
|
||||
|
||||
val exception = assertThrows(LocalNetworkAccessException::class.java) {
|
||||
interceptor.intercept(chain)
|
||||
}
|
||||
|
||||
assertEquals(errorMessage, exception.message)
|
||||
}
|
||||
verify(exactly = 3) {
|
||||
permissionProvider.hasLocalNetworkAccessPermission
|
||||
permissionProvider.acquireLocalNetworkAccessPermission()
|
||||
permissionProvider.errorMessageString
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should proceed when request targets a public IPv6 address`() {
|
||||
every { permissionProvider.hasLocalNetworkAccessPermission } returns false
|
||||
val host = "2001:db8::1"
|
||||
every { InetAddress.getByName(host) } returns ipv6Address(first = 0x20, second = 0x01)
|
||||
val chain = FakeInterceptorChain(
|
||||
request = Request.Builder().url("http://[$host]/api").build(),
|
||||
)
|
||||
|
||||
val response = interceptor.intercept(chain)
|
||||
|
||||
assertEquals(200, response.code)
|
||||
verify(exactly = 0) {
|
||||
permissionProvider.acquireLocalNetworkAccessPermission()
|
||||
permissionProvider.errorMessageString
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `intercept should rethrow UnknownHostException when DNS resolution fails`() {
|
||||
every { permissionProvider.hasLocalNetworkAccessPermission } returns false
|
||||
@@ -144,4 +193,19 @@ class PermissionInterceptorTest {
|
||||
permissionProvider.errorMessageString
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [Inet6Address] whose first two bytes are [first] and [second] and whose last
|
||||
* byte is 1.
|
||||
*/
|
||||
private fun ipv6Address(first: Int, second: Int): Inet6Address =
|
||||
Inet6Address.getByAddress(
|
||||
null,
|
||||
ByteArray(size = 16).apply {
|
||||
this[0] = first.toByte()
|
||||
this[1] = second.toByte()
|
||||
this[15] = 1
|
||||
},
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user