feat(android): set search-domain on VPN configuration (#8436)

On Android, we can use
[`addSearchDomain`](https://developer.android.com/reference/android/net/VpnService.Builder#addSearchDomain(java.lang.String))
to configure the search domain list for our VPN tunnel.

Thankfully, this gets applied to the system resolver without any other
hackery involved (unlike for Apple in #8421), and most apps use the
system resolver for queries. The one exception to this are some network
utilities like AndroDNS and Fing.

Tested to work fine in Termux using `github.io` as the search domain,
which responds to ICMP echoes to any subdomain:



<img width="420" alt="Screenshot 2025-03-13 at 10 19 41 PM"
src="https://github.com/user-attachments/assets/e156e644-08a8-4ab6-b49a-91ef92aabafd"
/>


Related #8410

---------

Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
Jamil
2025-03-13 23:54:44 -05:00
committed by GitHub
parent eb195861c2
commit 7e196683a1
3 changed files with 31 additions and 2 deletions

View File

@@ -58,6 +58,7 @@ class TunnelService : VpnService() {
var tunnelIpv4Address: String? = null
var tunnelIpv6Address: String? = null
private var tunnelDnsAddresses: MutableList<String> = mutableListOf()
private var tunnelSearchDomain: String? = null
private var tunnelRoutes: MutableList<Cidr> = mutableListOf()
private var _tunnelResources: List<Resource> = emptyList()
private var _tunnelState: State = State.DOWN
@@ -116,6 +117,7 @@ class TunnelService : VpnService() {
addressIPv4: String,
addressIPv6: String,
dnsAddresses: String,
searchDomain: String?,
routes4JSON: String,
routes6JSON: String,
) {
@@ -124,6 +126,7 @@ class TunnelService : VpnService() {
val routes4 = moshi.adapter<MutableList<Cidr>>().fromJson(routes4JSON)!!
val routes6 = moshi.adapter<MutableList<Cidr>>().fromJson(routes6JSON)!!
tunnelSearchDomain = searchDomain
tunnelIpv4Address = addressIPv4
tunnelIpv6Address = addressIPv6
tunnelRoutes.clear()
@@ -207,6 +210,10 @@ class TunnelService : VpnService() {
addDnsServer(dns)
}
tunnelSearchDomain?.let {
addSearchDomain(it)
}
addAddress(tunnelIpv4Address!!, 32)
addAddress(tunnelIpv6Address!!, 128)
}.establish()

View File

@@ -6,6 +6,7 @@ interface ConnlibCallback {
addressIPv4: String,
addressIPv6: String,
dnsAddresses: String,
searchDomain: String?,
routes4JSON: String,
routes6JSON: String,
)

View File

@@ -171,7 +171,7 @@ impl Callbacks for CallbackHandler {
tunnel_address_v4: Ipv4Addr,
tunnel_address_v6: Ipv6Addr,
dns_addresses: Vec<IpAddr>,
_search_domain: Option<DomainName>,
search_domain: Option<DomainName>,
route_list_4: Vec<Ipv4Network>,
route_list_6: Vec<Ipv6Network>,
) {
@@ -194,6 +194,16 @@ impl Callbacks for CallbackHandler {
name: "dns_addresses",
source,
})?;
let search_domain = search_domain
.map(|domain| {
env.new_string(domain.to_string())
.map_err(|source| CallbackError::NewStringFailed {
name: "search_domain",
source,
})
})
.transpose()?
.unwrap_or_default();
let route_list_4 = env
.new_string(serde_json::to_string(&V4RouteList::new(route_list_4))?)
.map_err(|source| CallbackError::NewStringFailed {
@@ -211,11 +221,12 @@ impl Callbacks for CallbackHandler {
env.call_method(
&self.callback_handler,
name,
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
&[
JValue::from(&tunnel_address_v4),
JValue::from(&tunnel_address_v6),
JValue::from(&dns_addresses),
JValue::from(&search_domain),
JValue::from(&route_list_4),
JValue::from(&route_list_6),
],
@@ -566,3 +577,13 @@ fn install_rustls_crypto_provider() {
tracing::debug!("Skipping install of crypto provider because we already have one.");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_jstring_is_null() {
assert!(JString::default().is_null())
}
}