How IAP Desktop protects TCP tunnels

Posted on

In the last post, we looked at the risks of local port forwarding and how it’s difficult to prevent malicious users from connecting to other user’s TCP tunnels in a multi-user environment.

Before version 2.7, the TCP tunnels created by IAP Desktop were as prone to being hijacked by other users as tunnels created by SSH or gcloud compute start-iap-tunnel. But as of version 2.7, attempting to connect to any of IAP Desktop’s tunnels by using mstsc or any other program fails and causes a warning to be emitted to IAP Desktop’s log file:

Warning: 0 : Connection from 127.0.0.1:59941 rejected by policy

Tracing connections to processes

When you connect to a remote desktop in IAP Desktop, the app creates an IAP TCP tunnel in the background. The tunnel is necessary because the Microsoft RDP ActiveX control requires an IP address and port to connect to. In an ideal world, the control would accept an IStream interface (or something similar) so that the hosting application could manage the connection – but that is not the case.

Like SSH and gcloud, IAP Desktop binds the port used for the TCP tunnel to 127.0.0.1 so that only local clients – or more precisely, only processes from the local machine – can connect. But restricting access to local processes is still too generous – the only process that should be allowed to connect to IAP Desktop’s ports is the IAP Desktop process itself. And not just any IAP Desktop process for that matter, but the exact same process that hosts the port!

As it turns out, finding out which process owns a port is not a secret: We can query this information from the Windows kernel by using the GetTcpTable2 function – no elevation required. The function returns the list of open TCP connections and contains the following data for each connection:

  • State
  • Local IP address and port
  • Remote IP address and port
  • Owning process ID

Relay policies

Before IAP Desktop accepts a connection to one of its TCP tunnels, it checks the client against what it calls the SameProcessRelayPolicy. This policy takes the remote port of the connection, calls GetTcpTable2 and then finds the entry that tracks the outgoing connection for this port.

Note that for TCP connections from 127.0.0.1 to 127.0.0.1, the table actually contains two entries – one tracking the outgoing connection and one tracking the incoming connection. The outgoing connection is tagged with the client’s process ID while the incoming connection is tagged with the server’s process ID.

Once we have the right table entry, the admission check is as simple as comparing the client’s process ID with the current process ID:

public bool IsClientAllowed(IPEndPoint remote)
{
  //
  // NB. For connections from localhost, there are two
  // entries in the table - one tracking the outgoing
  // connection and one tracking the incoming connection.
  //
  // To find out the client process id, we need to find
  // the entry for the outgoing connection.
  //
  var tcpTableEntry = TcpTable.GetTcpTable2()
      .Where(e => e.LocalEndpoint.Equals(remote));
  //
  // Only permit access if the originating process is
  // the current process.
  //
  return remote.Address.Equals(IPAddress.Loopback) &&
      tcpTableEntry.Any() &&
      tcpTableEntry.First().ProcessId == Process.GetCurrentProcess().Id;
}

If the check fails, the client is considered unauthorized and IAP Desktop resets the connection by closing the socket. In the case of mstsc, this causes the connection error:

Connection failure

If the check succeeds, IAP Desktop will start relaying incoming data to the TCP tunnel and vice versa.

Note that the code snippet above uses a helper class TcpTable which abstracts the gory details of P/Invoke’ing GetTcpTable2. You can find the source code for this class in the IAP Desktop GitHub repository.

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home