Identify incomming connectin ziti IP address

I am building a zitified windows RDP proxy. I need to find out what is the ziti ip of incomming connection.

Setup: Ubuntu host(runs ziti controller/router and also tunneler, etc) and windows 11 pro virtualbox vm(runs zitified proxy program).

ziti service “rdp” with intercept config 99.12.99.12 with port 1212

Ubuntu tunneler identity has #rdp.dial attr

Windows vm rdp.json has #rdp.bind attr

Problem is that conn does not return IP address of incomming connection:

		log.Printf("remote addr: %s", conn.RemoteAddr())
		log.Printf("local addr: %s", conn.LocalAddr().String())
// Output:
// remote addr: ziti-edge-router connId=2147483648, logical=ziti-sdk[router=tls:ziti-edge-router:80]
// locla addr: zitiConn connId=2147483648 svcId= sourceIdentity=admin1@gmail.com

However, when i call windows api - WTSEnumerateSessionsW and WTSQuerySessionInformationW i get this output:

2025/09/18 14:35:55 ------------------------------------------
<unsupported-family> / <none> -> 0
100.64.0.1 / carlo -> 1
<unsupported-family> / <none> -> 3
<unsupported-family> / <none> -> 65536

and on my ubuntu witch hold ziti controller, router, tunneler, etc, i have:

$ ip addr show | grep 100.64
inet 100.64.0.1/32 scope global ziti0

So windows session list knows the ziti IP.

Questions:

  1. How can i know incoming connections IP address

  2. if i have multiple incoming connections trough ziti, will the ip address always be the same 100.64.0.1 or will each have a unique IP address.

Full example code:

package main

import (
	"io"
	"log"
	"net"
	"sync"
	"time"

	"github.com/openziti/sdk-golang/ziti"
)

const (
	targetAddr = "127.0.0.1:3389" // local RDP service
)

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

	// Get identity config
	cfg, err := ziti.NewConfigFromFile("rdp.json")
	if err != nil {
		panic(err)
	}

	// Get service name (defaults to "chat")
	serviceName := "rdp"

	options := ziti.ListenOptions{
		ConnectTimeout: 5 * time.Minute,
		MaxConnections: 3,
	}
	ctx, err := ziti.NewContext(cfg)

	if err != nil {
		panic(err)
	}

	listener, err := ctx.ListenWithOptions(serviceName, &options)
	if err != nil {
		panic(err)
	}

	for {
		conn, err := listener.Accept()
		if err != nil {
			panic(err)
		}

		log.Printf("network: %s", conn.LocalAddr().Network())
		log.Printf("remote addr: %s", conn.RemoteAddr())
		log.Printf("local addr: %s", conn.LocalAddr().String())
		go handleClient(conn)
	}

}

func handleClient(client net.Conn) {
	defer client.Close()
	log.Printf("incoming from %s", client.RemoteAddr())

	backend, err := net.Dial("tcp", targetAddr)
	if err != nil {
		log.Printf("dial backend %s: %v", targetAddr, err)
		return
	}
	defer backend.Close()

	// copy data in both directions
	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		_, err := io.Copy(backend, client) // client -> backend
		if err != nil {
			log.Printf("copy client->backend: %v", err)
		}
		// signal EOF to backend write side if possible
		if tc, ok := backend.(*net.TCPConn); ok {
			tc.CloseWrite()
		}
	}()

	go func() {
		defer wg.Done()
		_, err := io.Copy(client, backend) // backend -> client
		if err != nil {
			log.Printf("copy backend->client: %v", err)
		}
		// signal EOF to client write side if possible
		if tc, ok := client.(*net.TCPConn); ok {
			tc.CloseWrite()
		}
	}()

	wg.Wait()
	log.Printf("connection closed for %s", client.RemoteAddr())
	/// we end screen control here
}

Note, use GitHub - openziti/sdk-golang: Ziti SDK for Golang v1.2.2. when i tried v1.2.4, it has the session issue from my previous post.

Hi @CarlosHleb, that sounds cool! I hope you blog about it when you're done!!! :slight_smile:

Let's level set on this because I'm not sure I understand what this even means? ziti doesn't have "ips". I THINK you mean you want the identity of the machine connecting? That would make sense to me. That or you want the remote IP of the machine that connected to your RDP proxy? I think we need to clarify that in order to help you here...

A diagram would also help me understand too. My initial reaction is that you will want to build into your proxy something that uses the context available in our SDKs during dial. Specifically in DialOptions there's an AppData block. That's how our tunnelers do this sort of thing.

Also worth noting is that we are in the process of making a zitified proxy similar to this which is coming soon to the go sdk/router. You might be able to have a look at how we're doing that to gain insights.

Not the identity connecting, but the IP(ex. 100.64.0.1). I need a way to identify witch RDP sessions come from ziti and witch dont.

If i will have multiple people(2 devices with tunnelers installed), will they all show up in sessions list with ip: 100.64.0.1?

i tried adding “sourceIp”: “$src_ip“ to my intercept config, i still dont see the IP :confused:

That would be helpful!

Yeah, i could, would have to ask my boss first :smiley:

The Accept implementation of the edge listener doesn’t try to bind the address to the net.Conn’s that it returns. It’s technically possible to do that and we considered it at one point, but doing so would invite failure cases that might be unexpected for most users (e.g. the source address isn’t available on the receiving host (EADDRNOTAVAIL).

We could revisit that decision, but you can see the source IP of the intercepted connection if you use listener.AcceptEdge() to receive connections. AcceptEdge returns an edge.Conn, which is a net.Conn that also has a GetAppData() method (among others). GetAppData returns application-specific metadata for the connection. When the dialing application is an OpenZiti tunneler, the application data is a JSON string that includes the source /destIP and port of the intercepted connection as well as the value that you specified in intercept.v1 (with variables resolved). You can see how we use AcceptEdge and conn.GetAppData in our edge router here. The tunneler app data json keys are here.

AcceptEdge() - very helpful. when i log GetAppData() i also see the same 100… ip address(ran this test locally with vm):

{"connType":null,"dst_protocol":"tcp","dst_ip":"12.99.12.99","dst_port":"1212","src_protocol":"tcp","src_ip":"100.64.0.1","src_port":"55478","source_addr":"100.64.0.1"}

Another question regarding plain Accept().I just ran a test from a different machine.

Setup:

  • 1 server hosting zitified rdp proxy
  • Ubuntu running tunneler with rdp.dial
  • inside ubuntu - windows vm running tunneler with rdp.dial

i connected from both tunneler machines to zitified rdp proxy, they both got the same IP:

        <unsupported-family> / <none> -> 0
        <unsupported-family> / <none> -> 1
        100.64.0.1 / Anotherser -> 3
        100.64.0.1 / User -> 4
        <unsupported-family> / <none> -> 65536

Can i reliably trust that any incomming rdp sessions(trough ziti proxy), will all have this `100.64.0.1` ip?

Generally no, you can’t count on a specific IP.

  • src_ip will be the IP that the intercepting tunneler observed from the peer of the intercepted connection. If you’re always intercepting local connections with Ziti Desktop Edge for Windows (or ziti-edge-tunnel), src_ip will be the IP address that’s assigned to the tun interface that’s reading the intercepted packets. By default this is 100.64.0.1, but it can be changed with configuration so you probably don’t want to rely on it.
  • source_addr is derived from the intercept.v1 sourceIp field, with variables resolved. You’re using the “$src_ip” variable here, which explains why you see the same value for source_addr and src_ip in the app data. You could put other values/variables in the sourceIp field though. For example you could make this a hard-coded string like “1.2.3.4” if that suits your purpose. Another variable, “$tunneler_id.name” resolves to the name of the openziti identity that the intercepting tunneler was using. So if you named your identities as IP addresses, you’d see those IPs in source_addr.

One more question.

Will src_ip always be equal to the ip address i see in windows session list?

100.64.0.1 / Anotherser -> 3

I’m not sure… Mainly because I’m not very handy in Windows. Where does the session list come from? I’m assuming you run a command on the host that has RDP server?

i am getting session list from wtsapi32.dll(WTSEnumerateSessionsW and WTSQuerySessionInformationW procedures).

It’s not a command, but a dll call from golang.

Ahh, sorry to ask that question which you provided an answer for in your initial post to this thread. I’m still not 100% sure where the session list gets those IPs from. Which info class (WTS_INFO_CLASS) are you requesting? For now I’ll assume you’re getting the IP from WTS_CLIENT_ADDRESS.

If so, the remarks in that doc seem to suggest that the value isn’t a reliable way to identify the client. Honestly I’m a little surprised that the RDP client would choose to use the Ziti TUN interface IP as the client address. It doesn’t seem like it’s possible to control what ends up in the session client address (e.g. through rdp configuration).

Going back to your original question here:

Do you really just need to know whether an RDP connection came through OpenZiti or the LAN? I’m guessing no, because your proxy already knows this by virtue of having received a connection with the OpenZiti SDK.

So you probably need to know precisely which OpenZiti identity originated the connection when it comes through OpenZiti. Does that need to be in the form of an IP address? If so you’ll need to use a trick like I mentioned above using “$tunneler_id.name” in the intercept.v1 sourceIp field and naming the identities as IP addresses.

If you don’t need to identify your OpenZiti clients by IP address then you could probably get away with calling SourceIdentifier() on the edge.Conn that’s returned from AcceptEdge. That wouldn’t even require setting the sourceIp field in your service’s intercept configuration.

Yes, i do. Not only that, i have edge cases where there can be multiple RDP sessions coming trough ziti on a single machine.

No, i don’t care about source identity.

My task is to map witch RDP sessions came from ziti(there can be many RDP sessions, some from ziti and some from not ziti).

By reliable you mean RDP client can send witch ever IP he wants, right? But in most normal cases that ip should match src_ip, right?

It’s hard do know for sure, but the rdp client probably looks at its own connection to the “rdp server” (which is actually the intercepting tunneler… shhh :slight_smile: ) to get the client address. So actually it isn’t that surprising to see the tunneler’s tun IP being used as the client address.

But the other reason that client address might be misleading:

The client network address is also not available in the following cases:

  • The connection is established through a Remote Desktop Gateway.
  • The connection is originated by the Microsoft Remote Desktop app that is available in the Store.

You probably know if those scenarios apply to you.