Openziti golang sdk - impossible to map incomming conn to api-session

Basic setup, simple ziti binder app, and intercept config and i access it trough chrome browser.

The issue is that edge.Conn does not provide a way to map the connection to api session. I does provide information about connId and circuitId, but openziti management api does not provide a way to map connid or circuitId with api session.

Why could this be useful? Ziti api session provides information of public IP address used, witch could be useful for most users.

Why not map by identity - from what i understand, 1 identity can have multiple api-sessions.

code i checked:

log.Print(edge.RemoteAddr())
log.Print("zitiIdentity: ", edge.SourceIdentifier())
log.Print("AppData: ", string(edge.GetAppData()))
log.Print("CircuitId: ", edge.GetCircuitId())
log.Print("state: ", edge.GetState())
log.Print("id: ", edge.Id())
log.Print("local addr: ", edge.LocalAddr())

logs:

2025/10/03 11:12:19 main.go:42: ziti-edge-router connId=2147483653, logical=ziti-sdk[router=tls:ziti-edge-router:80]
2025/10/03 11:12:19 main.go:43: zitiIdentity: admin1@gmail.com
2025/10/03 11:12:19 main.go:44: AppData: {"connType":null,"dst_protocol":"tcp","dst_hostname":"myapi.ziti","dst_ip":"100.64.0.7","dst_port":"443","src_protocol":"tcp","src_ip":"100.64.0.1","src_port":"55756"}
2025/10/03 11:12:19 main.go:45: CircuitId: 7RnlBvjJXfH8sPKKML6HWd
2025/10/03 11:12:19 main.go:46: state: {"circuitId":"7RnlBvjJXfH8sPKKML6HWd","closed":false,"encrypted":true,"encryptionRequired":true,"id":2147483653,"marker":"sjFrdC9e\u0000","readFIN":false,"sentFIN":false,"serviceName":""}
2025/10/03 11:12:19 main.go:47: id: 2147483653
2025/10/03 11:12:19 main.go:48: local addr: zitiConn connId=2147483653 svcId= sourceIdentity=admin1@gmail.com
[GIN] 2025/10/03 - 11:12:19 | 200 |     117.657µs |                 | GET      "/"

Any comment on this?

Hi @CarlosHleb ,
Apologies, I missed your initial post. The first thing to figure out is if you need the correlation data in the SDK, or if you're OK getting it from the controller.

From the SDK perspective, any given Context only has one api session at a time. The CtrlClt instance inside the Context tracks the api session. There's not currently a clean way to get it from the Context directly, but we could add it, if there's interest.

From the controller perspective the circuit has a field called Client. For edge circuits, that maps to the service session id. From there you can get to the api session id.

If you're mostly interested for tracing and tracking, events are probably your best bet:

Example

{
  "namespace": "apiSession",
  "event_src_id": "ctrl_client",
  "timestamp": "2025-10-09T13:40:28.556879991-04:00",
  "event_type": "created",
  "id": "e1b85b4e-45df-40df-8a6f-ccd7d4b3d4a1",
  "type": "jwt",
  "token": "",
  "identity_id": "yFr5juCltT",
  "ip_address": "127.0.0.1:36690"
}

{
  "namespace": "session",
  "event_src_id": "ctrl_client",
  "timestamp": "2025-10-09T13:40:28.56554556-04:00",
  "event_type": "created",
  "session_type": "Dial",
  "provider": "jwt",
  "id": "03dcf503-4763-46e6-bc1f-899d60ad90d4",
  "api_session_id": "e1b85b4e-45df-40df-8a6f-ccd7d4b3d4a1",
  "identity_id": "yFr5juCltT",
  "service_id": "3OMXuX9Wo1ZxMLOl6PtQzc"
}

{
  "namespace": "circuit",
  "event_src_id": "ctrl_client",
  "timestamp": "2025-10-09T13:40:28.577710008-04:00",
  "version": 2,
  "event_type": "created",
  "circuit_id": "6PwGyB6XospJNgAx2HCNHa",
  "client_id": "03dcf503-4763-46e6-bc1f-899d60ad90d4",
  "service_id": "3OMXuX9Wo1ZxMLOl6PtQzc",
  "terminator_id": "5SSYTuBHxHVstIHmWEBMuB",
  "instance_id": "",
  "creation_timespan": 543832,
  "path": {
    "nodes": [
      "L9IJNc4uz"
    ],
    "links": null,
    "ingress_id": "1RWAF0DlcdRYKv5y58ThHE",
    "egress_id": "74ZpqT0om062yZS5LZdbGa"
  },
  "link_count": 0,
  "path_cost": 262140,
  "tags": {
    "clientId": "yFr5juCltT",
    "hostId": "z-3dOhCp0T",
    "serviceId": "3OMXuX9Wo1ZxMLOl6PtQzc"
  }
}

Let me know if that's helpful. I'm not sure I entirely understand your use case, but I'm happy to clarify things.

Paul

My use-case:
i have an api written in golang that binds to ziti service.

User runs tunneler on his matchine and accesses that api trough a browser(intercept config).

When api receives a request, i want to store the public IP of the user that has visited my site.

My idea:

the ziti identity that api uses to bind to api service has ziti admin priveleges. I would like to, somehow, get the users public IP by calling management api - api sessions endpoint(public ip is available there).

Is there another way, except events, to do it, it would be helpful to know.

Do you mean changing existing source code of ziti controller? or do you mean calling ziti management/edge endpoints? I don’t plan to change the existing ziti controller code.

client_id Who the circuit was created for. Usually an edge session id.

This looks closer to what i need, but other options would be helpful.

Heres an example code i use:

package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"log"
	"net"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/openziti/sdk-golang/ziti"
	"github.com/openziti/sdk-golang/ziti/edge"
)

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)

	// load identity
	cfg, err := ziti.NewConfigFromFile("api-binder.json")
	if err != nil {
		panic(err)
	}
	ctx, err := ziti.NewContext(cfg)
	if err != nil {
		panic(err)
	}

	// listen on a Ziti service (instead of a TCP port)
	listener, err := ctx.Listen("api")
	if err != nil {
		panic(err)
	}
	defer listener.Close()

	// create Gin router
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		val := c.Request.Context().Value("zitiConn")
		if val != nil {
			if edge, ok := val.(edge.Conn); ok {
				log.Print(edge.RemoteAddr())
				log.Print("zitiIdentity: ", edge.SourceIdentifier())
				log.Print("AppData: ", string(edge.GetAppData()))
				log.Print("CircuitId: ", edge.GetCircuitId())
				log.Print("state: ", edge.GetState())
				log.Print("id: ", edge.Id())
				log.Print("local addr: ", edge.LocalAddr())
			} else {
				log.Print("not okey")
			}
		}

		c.JSON(200, gin.H{"msg": "hello over ziti"})
	})
	r.POST("/echo", func(c *gin.Context) {
		body, _ := c.GetRawData()
		c.Data(200, "application/octet-stream", body)
	})

	srv := &http.Server{
		Handler: r,
		ConnContext: func(ctx context.Context, c net.Conn) context.Context {
			// If TLS, unwrap it
			c1 := ctx.Value("CtrlClt")
			log.Print("CtrlClt: ", c1)
			if tlsConn, ok := c.(*tls.Conn); ok {
				if underlying := tlsConn.NetConn(); underlying != nil {
					if edgeConn, ok := underlying.(edge.Conn); ok {
						return context.WithValue(ctx, "zitiConn", edgeConn)
					}
				}
			}

			// If it’s a direct Ziti connection (no TLS)
			if edgeConn, ok := c.(edge.Conn); ok {
				return context.WithValue(ctx, "zitiConn", edgeConn)
			}

			log.Print("connection was neither edge.Conn nor tls->edge.Conn")
			return ctx
		},
	}

	fmt.Println("Gin HTTP server is running over Ziti service: my-http-service")
	if err := srv.ServeTLS(listener, "server.crt", "server.key"); err != nil {
		panic(err)
	}
}

1 Like

Ok, let's dig in a little more, there's some areas I'm still not clear on.

Let me recap, and see if I understand things correctly:
You've got:

                       /---> controller <---\
                      /         ^            \
                     /          |             \
browser -> local tunneler -> fabric -> host app (Go SDK app using Gin)
  1. You'd like the IP address of the local tunneler made available to the host application.
  2. You want the IP address as it's known to the controller, which may be different than the actual IP because of network address translation (NAT) and may also be different than the IP as it's known to the router it's connected through (again because of NAT) or because of use of multiple interfaces.

Questions:

  1. Why do you intend to do with the IP? If you're just going to track it to a log, then events seem like they'd be ideal for your use case. We have api session events as well as connect events which will have the various IP both at the controller and the router. There are also circuit events, which are generated for each dial attempt. You can have your own application which takes the event logs and enriches them or does whatever manipulation you need to get data in the final form you need.

You can even stream events to your host application if it has access to the management API. There's a web-socket API where you can register and stream the events. I'm not sure I'd go down this route, though, it feels too complicated and error-prone.

  1. Why the IP address and not the identity? We've talked about adding the dialing identity to the dial information in the router or controller to make it available to the host application. Since you have a tunneler on the other side, I'd think that identity would be a better identifier than the IP address. If you know the identity, you can always dig up the IP if you need it later. If the identity id was available on the dial and/or edge connection, would that work?

Thank you,
Paul

Hello,

Thank you for response.

Your notes:

  1. So you are telling me that, if controller/router is on remote server and a user connects from his matchine, running tunneler, the ip address in api sessions can be other than the public one? From my tests, it shows public ip. Screenshot attached.

Question answers:

  1. It’s for logging who accesses what and when.

Where can i find documentation for streaming events to my service(websocket), i have looked here, havent found anything related to that:

  1. I am going to log both the identity used and public ip address. What if 1 identity gets stolen/leaked and then 2 computers, running tunnelers(using the same identity), connect to our api, its usefull to log the IP. If it’s a Chinese IP, that would instantly look suspicious in our UI.

Thank you,

Karlis

Hi @CarlosHleb

  1. The address should only be different if the client is using NAT. If that's the case, it will show up as the ip address of the NAT gateway. In this scenario, multiple clients could be in the same private subnet and would show up as the same IP address to the controller (assuming the controller is not in the same subnet.
  2. The ziti CLI, written in Go, is a streaming event client. You can see the code here: ziti/ziti/cmd/fabric/stream_events.go at main · openziti/ziti · GitHub

Cheers,
Paul