Issue regarding the service

Hi,

I have enroll in ziti and and service is active in the identity detail screen i am showing the services and ports belongs to service i want to hit the service and it should open in the browser i am sharing the code please let me know that wnet wrong in present ziti tunnel in gihub is not showing service open in the browser.

package com.intrusion.endpoint.ui.screens
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.intrusion.endpoint.IntrusionShieldApp
import com.intrusion.endpoint.model.TunnelModel
import com.intrusion.endpoint.net.dns.DnsLookupManager
import com.intrusion.endpoint.ui.theme.*
import com.intrusion.endpoint.ui.viewmodel.ZitiInfoViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.*
import org.openziti.tunnel.Service

data class ServiceInterceptInfo(
    val ports: List<Pair<Int, Int>>,
    val protocols: List<String>
)
fun resolveBrowsableUrl(service: Service): List<String> {
    val intercept = service.config["intercept.v1"]?.jsonObject
    val addresses = intercept?.get("addresses")?.jsonArray?.mapNotNull {
        it.jsonPrimitive.contentOrNull
    } ?: return emptyList()

    val baseHost = addresses.firstOrNull { !it.startsWith("*.") } ?: service.name

    val ports = extractServiceInterceptInfo(service.config)?.ports?.map { it.first } ?: listOf(443)

    return ports.map { port ->
        val scheme = if (port == 443 || port == 9443) "https" else "http"
        if ((port == 443 && scheme == "https") || (port == 80 && scheme == "http")) {
            "$scheme://$baseHost"
        } else {
            "$scheme://$baseHost:$port"
        }
    }
}



fun isIpInZitiSubnet(ip: String?): Boolean {
    if (ip == null) return false
    val parts = ip.split(".").mapNotNull { it.toIntOrNull() }
    if (parts.size != 4) return false

    val first = parts[0]
    val second = parts[1]

    // Block 127.x.x.x as it's loopback/blocked
    if (first == 127) return false

    // Accept 100.64.0.0/10 and 10.x.x.x
    return (first == 100 && second in 64..127) || (first == 10)
}

// Extracts all (low, high) port ranges per service/hostname
fun extractServiceInterceptInfo(config: Map<String, JsonElement>): ServiceInterceptInfo? {
    val intercept = config["intercept.v1"]?.jsonObject ?: return null

    val portRanges = intercept["portRanges"]?.jsonArray?.mapNotNull {
        val obj = it.jsonObject
        val low = obj["low"]?.jsonPrimitive?.intOrNull
        val high = obj["high"]?.jsonPrimitive?.intOrNull
        if (low != null && high != null) Pair(low, high) else null
    } ?: emptyList()

    val protocols = intercept["protocols"]?.jsonArray?.mapNotNull {
        it.jsonPrimitive.contentOrNull
    } ?: emptyList()

    return ServiceInterceptInfo(ports = portRanges, protocols = protocols)
}



// Finds port ranges by matching service name to hostname or wildcard
fun findPortRangesForService(serviceName: String, portsMap: Map<String, List<Pair<Int, Int>>>): List<Pair<Int, Int>> {
    return portsMap.entries.find { (host, _) ->
        host == serviceName || (host.startsWith("*.") && serviceName.endsWith(host.removePrefix("*.")))
    }?.value ?: emptyList()
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun IdentityDetailsScreen(
    tunnelModel: TunnelModel,
    identityId: String,
    zitiInfoViewModel: ZitiInfoViewModel,
    dnsLookupManager: DnsLookupManager,
    onBack: () -> Unit
) {
    var identity by remember { mutableStateOf(tunnelModel.identity(identityId)) }
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()
    var showDialog by remember { mutableStateOf(false) }
    var shouldDeleteIdentity by remember { mutableStateOf(false) }
    val snackbarHostState = remember { SnackbarHostState() }

    val servicesState = remember { mutableStateOf(emptyList<org.openziti.tunnel.Service>()) }
    val isEnabledState = remember { mutableStateOf(false) }


    DisposableEffect(identity) {
        val servicesLiveData = identity?.services()
        val servicesObserver = androidx.lifecycle.Observer<List<org.openziti.tunnel.Service>> {
            servicesState.value = it ?: emptyList()
        }
        servicesLiveData?.observeForever(servicesObserver)

        val enabledLiveData = identity?.enabled()
        val enabledObserver = androidx.lifecycle.Observer<Boolean> {
            isEnabledState.value = it == true
        }
        enabledLiveData?.observeForever(enabledObserver)

        onDispose {
            servicesLiveData?.removeObserver(servicesObserver)
            enabledLiveData?.removeObserver(enabledObserver)
        }
    }

    if (identity == null) {
        LaunchedEffect(Unit) { onBack() }
        return
    }

   /* LaunchedEffect(shouldDeleteIdentity) {
        if (shouldDeleteIdentity) {
            val name = identity?.name()?.value ?: "Identity"
            identity?.delete()
            zitiInfoViewModel.clearIdentity(context)
            identity = null
            onBack()
        }
    }*/
    LaunchedEffect(shouldDeleteIdentity) {
        if (shouldDeleteIdentity) {
            try {
                identity?.setEnabled(false)
                delay(2000) // give it time to cleanup routes/interfaces
                identity?.delete()
            } catch (e: Exception) {
                Log.e("Ziti", "Delete failed", e)
            } finally {
                zitiInfoViewModel.clearIdentity(context)
                onBack() // ⬅️ move before identity = null
                identity = null
            }
        }
    }



    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Ziti Identity", color = IntzWhite) },
                navigationIcon = {
                    IconButton(onClick = onBack) {
                        Icon(Icons.Default.ArrowBack, contentDescription = "Back", tint = Color.White)
                    }
                },
                colors = TopAppBarDefaults.topAppBarColors(containerColor = IntzBlue300)
            )
        },
        containerColor = IntzBase500,
        snackbarHost = { SnackbarHost(snackbarHostState) }
    ) { padding ->
        Column(
            modifier = Modifier
                .padding(padding)
                .padding(16.dp)
                .verticalScroll(rememberScrollState())
        ) {
            Text(identity!!.id, color = IntzWhite, fontSize = 20.sp, fontWeight = FontWeight.Bold)

            Spacer(modifier = Modifier.height(12.dp))
            Text("Network", color = IntzGray300, fontSize = 12.sp)
            Text(identity!!.controllers()?.value.toString(), color = IntzWhite, fontSize = 14.sp)

            Spacer(modifier = Modifier.height(12.dp))
            Text("Status", color = IntzGray300, fontSize = 12.sp)
            Text(if (isEnabledState.value) "Active" else "Inactive", color = IntzWhite, fontSize = 14.sp)

            Spacer(modifier = Modifier.height(24.dp))

            Row(
                modifier = Modifier.fillMaxWidth(),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text("Services", color = IntzWhite, fontSize = 16.sp, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
                Text(
                    "FORGET THIS IDENTITY",
                    color = IntzBlue300,
                    fontSize = 12.sp,
                    modifier = Modifier.clickable { showDialog = true }
                )
            }

            Spacer(modifier = Modifier.height(8.dp))

            servicesState.value.forEach { service ->
                val interceptInfo = extractServiceInterceptInfo(service.config)

                val portsText = interceptInfo?.ports?.joinToString(", ") {
                    if (it.first == it.second) "${it.first}" else "${it.first}-${it.second}"
                } ?: "N/A"

                val protocolText = interceptInfo?.protocols?.joinToString(", ") ?: "N/A"

              /*  val url = "https://${service.name}"*/
                val urls = resolveBrowsableUrl(service)

                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(vertical = 6.dp)

                        .clickable(enabled = isEnabledState.value) {
                            coroutineScope.launch(Dispatchers.IO) {

                                val workingUrl = urls.firstOrNull { url ->
                                    try {
                                        val uri = Uri.parse(url)
                                        val host = uri.host ?: return@firstOrNull false
                                        val originalPort = uri.port.takeIf { it != -1 } ?: if (url.startsWith("https")) 443 else 80

                                        val results = dnsLookupManager.lookup(host)
                                        val resolvedIp = results?.firstOrNull()?.address?.hostAddress
                                        Log.d("ZitiDNS", "Resolved $host to $resolvedIp")

                                        val interceptInfo = extractServiceInterceptInfo(service.config)
                                        val allowedPorts = interceptInfo?.ports ?: emptyList()
                                        val portAllowed = allowedPorts.any { (low, high) -> originalPort in low..high }

                                        return@firstOrNull isIpInZitiSubnet(resolvedIp) && portAllowed
                                    } catch (e: Exception) {
                                        Log.e("ZitiDNS", "Error checking $url", e)
                                        return@firstOrNull false
                                    }
                                }

                                val finalUrl = workingUrl?.let { url ->
                                    val uri = Uri.parse(url)
                                    val originalPort = uri.port
                                    // Force port 443 if original is non-standard like 9443, 8443 etc.
                                    if (originalPort != -1 && originalPort != 443) {
                                        uri.buildUpon()
                                            .scheme("https")
                                            .encodedAuthority("${uri.host}:443")
                                            .build()
                                            .toString()
                                    } else url
                                }

                                if (finalUrl != null) {
                                    withContext(Dispatchers.Main) {
                                        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(finalUrl))
                                        intent.resolveActivity(context.packageManager)?.let {
                                            context.startActivity(intent)
                                        } ?: Toast.makeText(context, "No browser found to open $finalUrl", Toast.LENGTH_SHORT).show()
                                    }
                                } else {
                                    withContext(Dispatchers.Main) {
                                        Toast.makeText(context, "Ziti is not intercepting any known URL for this service", Toast.LENGTH_SHORT).show()
                                    }
                                }
                            }
                        }
                ) {
                    Text(
                        text = service.name + if (isEnabledState.value) " (Tap to open)" else " (Blocked)",
                        color = if (isEnabledState.value) Color.Yellow else IntzWhite,
                        fontSize = 14.sp
                    )
                    Text("Ziti Ports: $portsText", color = IntzGray300, fontSize = 12.sp)
                    Text("Protocols: $protocolText", color = IntzGray300, fontSize = 12.sp)
                    Text(
                        text = if (isEnabledState.value) "ZTNA Access: Allowed" else "ZTNA Access: Blocked",
                        color = if (isEnabledState.value) Color.Green else Color.Red,
                        fontSize = 12.sp,
                        fontWeight = FontWeight.Medium
                    )
                    Divider(color = IntzGray300.copy(alpha = 0.4f), thickness = 0.5.dp)
                }
            }


            Spacer(modifier = Modifier.height(24.dp))
        }
    }


    if (showDialog) {
        AlertDialog(
            onDismissRequest = { showDialog = false },
            title = { Text("Confirm") },
            text = { Text("Are you sure you want to delete this identity?") },
            confirmButton = {
                TextButton(onClick = {
                    showDialog = false
                    shouldDeleteIdentity = true
                }) {
                    Text("Yes")
                }
            },
            dismissButton = {
                TextButton(onClick = { showDialog = false }) {
                    Text("Cancel")
                }
            },
            icon = { Icon(Icons.Default.ArrowBack, contentDescription = null) },
            containerColor = MaterialTheme.colorScheme.surface
        )
    }
}
package com.intrusion.endpoint.model

import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.openziti.tunnel.Dump
import org.openziti.tunnel.Enroll
import org.openziti.tunnel.Event
import org.openziti.tunnel.ExtAuthResult
import org.openziti.tunnel.ExternalAuth
import org.openziti.tunnel.Keychain
import org.openziti.tunnel.LoadIdentity
import org.openziti.tunnel.OnOffCommand
import org.openziti.tunnel.RefreshIdentity
import org.openziti.tunnel.Service
import org.openziti.tunnel.SetUpstreamDNS
import org.openziti.tunnel.Tunnel
import org.openziti.tunnel.Upstream
import org.openziti.tunnel.ZitiConfig
import org.openziti.tunnel.ZitiID
import org.openziti.tunnel.toPEM
import org.pcap4j.packet.IpPacket
import java.net.InetAddress
import java.net.URI
import java.security.KeyStore.PrivateKeyEntry
import java.security.cert.X509Certificate
import java.util.Locale
import java.util.concurrent.CompletableFuture

open class TunnelModel(
    val tunnel: Tunnel,
    val context: () -> Context
) : ViewModel() {
    val Context.prefs: DataStore<Preferences> by preferencesDataStore("tunnel")
    val identitiesDir = context().getDir("identities", Context.MODE_PRIVATE)
    private val _zitiDomains = mutableSetOf<String>()
    val zitiDomains: Set<String> get() = _zitiDomains
    val NAMESERVER = stringPreferencesKey("nameserver")
    val zitiDNS = context().prefs.data.map {
        it[NAMESERVER] ?: "100.64.0.2"
    }
    val RANGE = stringPreferencesKey("range")
    val zitiRange = context().prefs.data.map {
        it[RANGE] ?: "100.64.0.0/10"
    }

    fun setDNS(server: String?, range: String?) = viewModelScope.launch {
        context().prefs.edit { settings ->
            settings[NAMESERVER] = server ?: DEFAULT_DNS
            settings[RANGE] = range ?: DEFAULT_RANGE
        }
    }

    // TODO: If we need this for ziti, then we must change this function as it emits every second, even if unused in UI.
    // Consider switching to `StateFlow` with `SharingStarted.WhileSubscribed`.
    /*
    data class NetworkStats(val up: Double, val down: Double)
    fun stats() = flow {
        while (true) {
            emit(NetworkStats(tunnel.getUpRate(), tunnel.getDownRate()))
            delay(1_000)
        }
    }.asLiveData()
    */

    fun identities(): LiveData<List<Identity>> = identitiesData
    private val identitiesData = MutableLiveData<List<Identity>>()
    private val identities = mutableMapOf<String, Identity>()
    private val identitiesMutex = Mutex()

    fun identity(id: String): Identity? = runBlocking { identitiesMutex.withLock { identities[id] } }
    init {
        viewModelScope.launch(Dispatchers.IO) {
            val dns = zitiDNS.first()
            val range = zitiRange.first()
            Log.i("tunnel", "setting dns[$dns] and range[$range]")
            tunnel.setupDNS(dns, range)
            tunnel.start()
        }
        viewModelScope.launch {
            tunnel.events().collect { processEvent(it) }
        }

        // Load configurations from identity files.
        val configs = mutableMapOf<String, ZitiConfig>()
        val idFiles = identitiesDir.listFiles() ?: emptyArray()
        idFiles.forEach {
            Log.i(TAG, "loading identity from file[$it]")
            val cfg = Json.decodeFromString<ZitiConfig>(it.readText())
            configs[cfg.identifier] = cfg
        }

        val loadedKeys = configs.mapNotNull { it.value.id.key?.removePrefix("keychain:") }
        val aliases = Keychain.store.aliases().toList().filter { !loadedKeys.contains(it) }
        for (alias in aliases) {
            loadConfigFromKeyStore(alias)?.let { cfg ->
                Log.i(TAG, "migrating identity from keychain[$alias]")
                val uri = URI(alias)
                val id = uri.userInfo ?: uri.path.removePrefix("/")
                val json = Json.encodeToString<ZitiConfig>(cfg)
                identitiesDir.resolve(cfg.identifier).outputStream().use {
                    it.write(json.toByteArray())
                }
                configs[id] = cfg
            }
        }

        // Launch loadIdentity for each configuration asynchronously.
        for ((id, cfg) in configs) {
            viewModelScope.launch {
                loadIdentity(id, cfg)
            }
        }
    }

    private fun loadConfigFromKeyStore(alias: String): ZitiConfig? {
        if (!Keychain.store.containsAlias(alias)) return null
        if (!alias.startsWith("ziti://")) return null

        val entry = Keychain.store.getEntry(alias, null)
        if (entry !is PrivateKeyEntry) return null

        val uri = URI(alias)
        val ctrl = "https://${uri.host}:${uri.port}"
        val id = uri.userInfo ?: uri.path.removePrefix("/")

        val idCerts = Keychain.store.getCertificateChain(alias)
        val pem = idCerts.map { it as X509Certificate }
            .joinToString(transform = X509Certificate::toPEM, separator = "")
        val caCerts = Keychain.store.aliases().toList().filter { it.startsWith("ziti:$id/") }
            .map { Keychain.store.getCertificate(it) as X509Certificate}
            .joinToString(transform = X509Certificate::toPEM, separator = "")

        return ZitiConfig(
            controller = ctrl,
            controllers = listOf(ctrl),
            id = ZitiID(cert = pem, key = "keychain:${alias}", ca = caCerts)
        )
    }

    private fun disabledKey(id: String) = booleanPreferencesKey("$id.disabled")

    private suspend fun loadIdentity(id: String, cfg: ZitiConfig) {
        val disabled = withContext(Dispatchers.IO) {
            context().prefs.data.map {
                it[disabledKey(id)] ?: false
            }.first()
        }
        Log.i(TAG, "loading identity[$id] disabled[$disabled]")
        val cmd = LoadIdentity(id, cfg, disabled)
        tunnel.processCmd(cmd).handleAsync { json: JsonElement?, ex: Throwable? ->
            if (ex != null) {
                Log.w(TAG, "failed to execute", ex)
            } else {
                viewModelScope.launch {
                    identitiesMutex.withLock {
                        identities[id] = Identity(id, cfg, this@TunnelModel, !disabled)
                        identitiesData.postValue(identities.values.toList())
                    }
                    Log.i(TAG, "load result[$id]: $json")
                }
            }
        }
    }

    private fun processEvent(ev: Event) {
        Log.d(TAG, "received event[$ev]")
        viewModelScope.launch {
            identitiesMutex.withLock {
                identities[ev.identifier]?.processEvent(ev)
                    ?: Log.w(TAG, "no identity for event[$ev]")
            }
        }
    }
    fun getZitiServiceHostnames(): List<String> {
        val id = currentId() ?: return emptyList()
        val identity = identity(id) ?: return emptyList()

        return identity.services().value
            ?.mapNotNull { it.config["intercept.v1"]?.jsonObject?.get("addresses") }
            ?.flatMap { array -> array.jsonArray.mapNotNull { it.jsonPrimitive.contentOrNull } }
            ?.distinct()
            ?: emptyList()
    }

    fun setUpstreamDNS(servers: List<String>): CompletableFuture<Unit> {
        val cmd = SetUpstreamDNS(servers.map { Upstream(it) })
        return tunnel.processCmd(cmd).thenApply { }
    }

    fun enroll(jwt: String): CompletableFuture<ZitiConfig?> {
        val cmd = Enroll(jwt = jwt, useKeychain = true)
        val future = tunnel.processCmd(cmd).thenApply {
            Json.decodeFromJsonElement<ZitiConfig>(it!!)
        }

        future.thenApply { cfg ->
            val cfgJson = Json.encodeToString<ZitiConfig>(cfg)
            identitiesDir.resolve(cfg.identifier).outputStream().use { output ->
                output.write(cfgJson.toByteArray())
            }
            // Launch the identity load asynchronously.
            viewModelScope.launch {
                loadIdentity(cfg.identifier, cfg)
            }
            // Return cfg so that the future's type is maintained.
            cfg
        }.exceptionally { error ->
            Log.e(TAG, "enrollment failed", error)
            null
        }
        return future
    }

    fun dumpIdentity(id: String): CompletableFuture<String> =
        tunnel.processCmd(Dump(id)).thenApply { json ->
            if (json is JsonObject && json[id] is JsonPrimitive) {
                json[id]?.jsonPrimitive?.content
            } else {
                json?.toString() ?: "no data"
            }
        }

    internal fun refreshIdentity(id: String) =
        tunnel.processCmd(RefreshIdentity(id)).thenAccept { }

    internal fun useJWTSigner(id: String, signer: String?) =
        tunnel.processCmd(ExternalAuth(id, signer)).thenApply {
            Json.decodeFromJsonElement<ExtAuthResult>(it!!)
        }

    internal fun enableIdentity(id: String, on: Boolean): CompletableFuture<Unit> {
        val disabledKey = disabledKey(id)
        viewModelScope.launch(Dispatchers.IO) {
            context().prefs.edit {
                it[disabledKey] = !on
            }
        }
        return tunnel.processCmd(OnOffCommand(id, on)).thenApply { }
    }

    internal fun deleteIdentity(identifier: String, key: String?) {
        viewModelScope.launch {
            identitiesMutex.withLock {
                identities.remove(identifier)
                identitiesData.postValue(identities.values.toList())
            }
        }
        val uri = URI(identifier)
        val id = uri.userInfo ?: uri.path.removePrefix("/")
        key?.let {
            runCatching { Keychain.store.deleteEntry(it) }
                .onFailure { Log.w(TAG, "failed to remove entry", it) }
        }
        runCatching { identitiesDir.resolve(id).delete() }
            .onFailure { Log.w(TAG, "failed to remove config", it) }
        val caCerts = Keychain.store.aliases().toList().filter { it.startsWith("ziti:$id/") }
        caCerts.forEach {
            Keychain.store.runCatching { deleteEntry(it) }
        }
        viewModelScope.launch(Dispatchers.IO) {
            context().prefs.edit {
                val prefKey = disabledKey(identifier)
                if (it.contains(prefKey)) it.remove(prefKey)
            }
        }
    }
    fun currentId(): String? {
        return identities.values.firstOrNull()?.id
    }
    fun isZitiModeActiveBlocking(): Boolean {
        val id = currentId() ?: return false
        return identity(id)?.enabled()?.value == true
    }
    fun getInterceptIpForDomain(domain: String): InetAddress? {
        val id = currentId() ?: return null
        val identity = identity(id) ?: return null
        val services = identity.services().value ?: return null

        for (service in services) {
            // Check if service name matches domain or wildcard match
            val serviceName = service.name.lowercase()
            if (serviceName == domain.lowercase() ||
                (serviceName.startsWith("*.") && domain.endsWith(serviceName.removePrefix("*.")))) {

                val hostConfig = service.config["host.v1"]?.jsonObject
                val addressString = hostConfig?.get("address")?.jsonPrimitive?.contentOrNull

                if (!addressString.isNullOrEmpty()) {
                    try {
                        return InetAddress.getByName(addressString)
                    } catch (e: Exception) {
                        // Log error if IP parsing fails
                    }
                }
            }
        }
        return null
    }
    fun getZitiDnsMap(): Map<String, String> {
        val map = mutableMapOf<String, String>()
        identity(currentId() ?: return emptyMap())?.services()?.value?.forEach { service ->
            val interceptCfg = service.config["intercept.v1"]?.jsonObject
            val hostCfg = service.config["host.v1"]?.jsonObject
            val ip = hostCfg?.get("address")?.jsonPrimitive?.contentOrNull ?: return@forEach
            val addresses = interceptCfg?.get("addresses")?.jsonArray?.mapNotNull {
                it.jsonPrimitive.contentOrNull
            } ?: return@forEach
            addresses.forEach { domain ->
                map[domain] = ip
            }
        }
        return map
    }

    private fun isZitiIntercept(hostname: String): Boolean {
        val lower = hostname.lowercase(Locale.ROOT)
        return zitiDomains.any { domain ->
            when {
                domain.startsWith("*.") -> lower.endsWith(domain.removePrefix("*"))
                else -> lower == domain
            }
        }
    }
    fun updateIntercepts(services: List<Service>) {
        _zitiDomains.clear()

        services.forEach { service ->
            val interceptConfig = service.config["intercept.v1"] as? Map<*, *> ?: return@forEach
            val addresses = interceptConfig["addresses"] as? List<*> ?: return@forEach

            addresses.forEach { addr ->
                if (addr is String) _zitiDomains.add(addr)
            }
        }

        Log.d("TunnelModel", "Ziti intercept domains: $_zitiDomains")
    }
    fun isHostnamePartOfActiveZitiServices(hostname: String): Boolean {
        val id = currentId() ?: return false
        val identity = identity(id) ?: return false
        if (identity.enabled().value != true) return false

        val services = identity.services().value ?: emptyList()

        // Check if hostname matches any service intercept address or hostname
        return services.any { service ->
            val intercept = service.config["intercept.v1"]?.jsonObject ?: return@any false

            val addresses = intercept["addresses"]?.jsonArray?.mapNotNull {
                it.jsonPrimitive.contentOrNull
            } ?: emptyList()
            Log.d("TunnelModel", "Addresses in intercept: $addresses")
            // Match exact or wildcard hosts - simple example:
            addresses.any { addr ->
                if (addr.startsWith("*.")) {
                    hostname.endsWith(addr.removePrefix("*.")) // wildcard match
                } else {
                    hostname.equals(addr, ignoreCase = true)
                }
            }
        }
    }

    companion object {
        const val TAG = "model"
        const val DEFAULT_DNS = "100.64.0.2"
        const val DEFAULT_RANGE = "100.64.0.0/10"
    }

}
if you packetrouter i will share that too 

Please help to check this . Regards, Yamini

1 Like