Python SDK with IPv6

Hello community,

I’m having trouble getting the Python SDK with Flask to work with IPv6, the GitHub example using waitress doesn’t seem to support it.

( ziti-sdk-py/sample/flask-of-ziti/helloFlazk.py at main · openziti/ziti-sdk-py · GitHub )

I tried multiple things including :

  • Using the GitHub example with IPv6 localhost

  • Only using Flask without waitress, and trying to run it on IPv6

  • Setting flask on ipv4 localhost and hoping Ziti would manage the routing

  • Creating my own IPv6 socket that Ziti would use

I also couldn’t find valuable infos on the forum.
I guess I’m once again missing the obvious, just getting into Ziti and it’s quite hard to grasp everything at first !

Any idea would greatly help. Best regards

can you provide some code examples for the things you tried?

Also, do you really need IPv6? When you bind on Ziti network there is no actual socket listening on IPv4/6. the address is only use for mapping to a ziti service

This my android code here in identity detail screen on click of service I need to browse it but it is giving unable to connect


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 resolveBrowsableUrl(service: org.openziti.tunnel.Service): String {
    val intercept = service.config["intercept.v1"]?.jsonObject
    val addresses = intercept?.get("addresses")?.jsonArray?.mapNotNull {
        it.jsonPrimitive.contentOrNull
    }

    val url = addresses?.firstOrNull()?.replace(" ", "-")?.lowercase()
        ?: service.name.replace(" ", "-").lowercase()
    // Prefer non-wildcard public-looking domain
    val preferred = addresses!!.firstOrNull {
        !it.startsWith("*.") && it.contains('.') && !it.endsWith(".zta")
    }
    return "https://${preferred ?: service.name}"

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

    return (parts[0] == 100 && parts[1] in 64..127) ||  // 100.64.0.0/10
            (parts[0] == 10)                             // allow 10.x.x.x too
}*/

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) {
                            try {
                                val resolved = InetAddress.getByName(url.removePrefix("https://"))
                                Log.d("Ziti-DNS", "Resolved IP for gitlab.intrusion.com = ${resolved.hostAddress}")

                                if (isEnabledState.value) {
                                    if (resolved.hostAddress.startsWith("127.")) {
                                        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                                        context.startActivity(intent)
                                    } else {
                                        Toast.makeText(context, "Ziti not intercepting $url", Toast.LENGTH_LONG).show()
                                    }
                                }
                               *//* val intent = Intent(Intent.ACTION_VIEW).apply {
                                    data = android.net.Uri.parse(url)
                                }
                                context.startActivity(intent)*//*
                            } catch (e: Exception) {
                                Toast.makeText(context, "Failed to open $url", Toast.LENGTH_SHORT).show()
                            }
                        }*//*.clickable(enabled = isEnabledState.value) {
                            coroutineScope.launch(Dispatchers.IO) {
                                try {
                                    val host = url.removePrefix("https://").removeSuffix("/")
                                    val results = dnsLookupManager.lookup(host)
                                    val resolved = results?.firstOrNull()?.address

                                    Log.d("Ziti-DNS", "Resolved IP for $host = ${resolved?.hostAddress ?: "null"}")

                                    if (resolved?.hostAddress?.startsWith("100.127.") == true) {
                                        val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                                        browserIntent.resolveActivity(context.packageManager)?.let {
                                            context.startActivity(browserIntent)
                                        } ?: withContext(Dispatchers.Main) {
                                            Toast.makeText(context, "No browser found to open $url", Toast.LENGTH_SHORT).show()
                                        }
                                    } else {
                                        withContext(Dispatchers.Main) {
                                            Toast.makeText(context, "Ziti not intercepting $host", Toast.LENGTH_SHORT).show()
                                        }
                                    }
                                } catch (e: Exception) {
                                    withContext(Dispatchers.Main) {
                                        Toast.makeText(context, "Failed to resolve/open: $url", Toast.LENGTH_SHORT).show()
                                    }
                                    Log.e("Ziti-DNS", "Error resolving $url", e)
                                }
                            }
                        }*/.*clickable*(enabled = isEnabledState.value) **{**
 **                           **urls.*forEach* **{** url **->**
 **                               **coroutineScope.*launch*(Dispatchers.IO) **{**
 **                                           **try {
                                                val host = Uri.parse(url).*host* ?: return@launch
                                                val results = dnsLookupManager.lookup(host)
                                                Log.d("DNS", "gitlab.intrusion.com resolved to: ${results?.*firstOrNull*()?.address}")
                                                val resolvedIp = results?.*firstOrNull*()?.address?.*hostAddress*
 *                                               *val interceptInfo = *extractServiceInterceptInfo*(service.config)
                                                val port = Uri.parse(url).*port*.*takeIf* **{ it** != -1 **}**
 **                                                   **?: if (url.*startsWith*("https")) 443 else 80

                                                val allowedPorts = interceptInfo?.ports ?: *emptyList*()
                                                val portInIntercept = allowedPorts.*any* **{** (low, high) **->** port in low..high **}**
 **                                               **if (*isIpInZitiSubnet*(resolvedIp) && portInIntercept) {
                                                    val intent = Intent(Intent.*ACTION_VIEW*, Uri.parse(url))
                                                    intent.resolveActivity(context.*packageManager*)?.*let* **{**
 **                                                       **context.startActivity(intent)
                                                    **}** ?: withContext(Dispatchers.Main) **{**
 **                                                       **Toast.makeText(context, "No browser found to open $url", Toast.*LENGTH_SHORT*).show()
                                                    **}**
 **                                               **} else {
                                                    withContext(Dispatchers.Main) **{**
 **                                                       **Toast.makeText(context, "Ziti not intercepting $host:$port", Toast.*LENGTH_SHORT*).show()
                                                    **}**
 **                                               **}
                                            } catch (e: Exception) {
                                                withContext(Dispatchers.Main) **{**
 **                                                   **Toast.makeText(context, "Failed to resolve/open $url", Toast.*LENGTH_SHORT*).show()
                                                **}**
 **                                               **Log.e("Ziti-DNS", "Error resolving $url", e)
                                            }
                                        **}**

 **                           }**

 **                           **/*  coroutineScope.launch(Dispatchers.IO) {
                                  try {
                                    //  val host = url.removePrefix("https://").removeSuffix("/")
                                      val results = dnsLookupManager.lookup(host)
                                      val resolvedIp = results?.firstOrNull()?.address?.hostAddress
                                      Log.d("ZitiStatus", "Identity enabled: ${isEnabledState.value}, IP: $resolvedIp")
                                       val interceptInfo = extractServiceInterceptInfo(service.config)
                                      val allowedPorts = interceptInfo?.ports ?: emptyList()
                                      val targetPorts = listOf(443, 80, 8443)
                                      val portInIntercept = targetPorts.any { port ->
                                          allowedPorts.any { (low, high) -> port in low..high }
                                      }

                                      if (isIpInZitiSubnet(resolvedIp) && portInIntercept) {
                                          val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                                          browserIntent.resolveActivity(context.packageManager)?.let {
                                              context.startActivity(browserIntent)
                                          } ?: withContext(Dispatchers.Main) {
                                              Toast.makeText(context, "No browser found to open $url", Toast.LENGTH_SHORT).show()
                                          }
                                      } else {
                                          withContext(Dispatchers.Main) {
                                              Toast.makeText(context, "Ziti not intercepting $host on required ports", Toast.LENGTH_SHORT).show()
                                          }
                                      }
                                  } catch (e: Exception) {
                                      withContext(Dispatchers.Main) {
                                          Toast.makeText(context, "Failed to resolve/open: $url", Toast.LENGTH_SHORT).show()
                                      }
                                      Log.e("Ziti-DNS", "Error resolving $url", e)
                                  }
                              }*/
                        **}**

 **               **) **{**
 **                   **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
        )
    }
}

does this belong to a different thread?

Can you help me with any suggestion?

Disclaimer: This message and any attachments are intended solely for the use of the recipient and may contain privileged and confidential information. If you are not the intended recipient or an authorized representative of the intended recipient, please be advised that any review, dissemination, or copying of this communication is strictly prohibited. If you have received this communication in error, please notify us immediately by email and delete this message and any attachments from your system.