Back to Blog
Technical May 10, 2026

Single-Port Architecture: Multiplexing Protocols with ALPN

A technical deep dive into how we use TLS ALPN to serve HTTPS, gRPC, reverse tunnels, and SSH all over a single port, simplifying deployment and firewall rules.

When building a modern infrastructure access platform, minimizing the surface area of your deployment is critical. Every exposed port requires firewall rules, load balancer configuration, and operational oversight.

For a platform like Mezite, which manages SSH sessions, API traffic, and reverse tunnels, the naive approach requires multiple open ports. You need one port for the web UI (HTTPS), one for the agent reverse tunnels, one for the cluster tunnels, and perhaps another for the gRPC authentication service. This complexity creates friction for teams trying to deploy the software in locked-down environments.

We take a different approach. We route everything—HTTPS, gRPC, SSH proxying, and custom reverse tunnels—through a single port (443). To accomplish this without writing a monolithic protocol parser, we exploit a powerful feature built into modern TLS: Application-Layer Protocol Negotiation (ALPN).

The ALPN Handshake

ALPN is a TLS extension originally designed to negotiate HTTP/2 connections over HTTPS. During the initial TLS handshake, the client sends a ClientHello message that includes a list of application protocols it supports (e.g., ["h2", "http/1.1"]). The server inspects this list, selects a protocol it also supports, and returns the selection in the ServerHello.

Once the TLS handshake finishes, both sides know exactly which protocol to speak over the encrypted tunnel, before a single byte of application data is sent.

While web browsers use ALPN to choose HTTP versions, there is no restriction on the protocol names you can negotiate. In Mezite, we define custom ALPN identifiers for our internal protocols:

server/alpn/alpn.go go
const (
ProtocolHTTP2         = "h2"
ProtocolSSH           = "mezite-ssh"
ProtocolTunnel        = "mezite-tunnel"
ProtocolClusterTunnel = "mezite-cluster-tunnel"
ProtocolAuth          = "mezite-auth"
)

When a Mezite agent (mezd) connects to the central proxy, it advertises mezite-tunnel in its ClientHello. When the CLI (msh) makes an API request, it advertises mezite-auth.

Building a Protocol Multiplexer in Go

To route these connections effectively, we need to intercept the TLS handshake and dispatch the resulting net.Conn to the correct internal server (the HTTPS server, the gRPC server, etc.).

Go’s standard crypto/tls library makes this surprisingly elegant. Instead of statically defining the NextProtos in our tls.Config, we use the GetConfigForClient callback. This function executes exactly when the server receives the ClientHello.

server/proxy/mux.go (Simplified) go
// Set up GetConfigForClient to handle ALPN negotiation dynamically.
tlsConfig.GetConfigForClient = func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
  cfg := baseCfg.Clone()

  // Check if the client offers any protocol we know about
  for _, proto := range hello.SupportedProtos {
      if m.isKnownProtocol(proto) {
          cfg.NextProtos = []string{proto}
          return cfg, nil
      }
  }

  // Client offered no ALPN or unknown protocols — default to h2 (HTTPS)
  cfg.NextProtos = nil
  return cfg, nil
}

This callback lets us dynamically constrain the server’s negotiated protocol based on what the client offered. If a standard web browser connects, it asks for h2 or http/1.1. Since it does not ask for mezite-tunnel, our callback falls back to standard HTTPS routing.

Virtual Listeners

Once the TLS handshake completes, our multiplexer inspects the negotiated protocol on the connection state and routes the connection to a virtual listener.

A virtual listener in Go is simply an implementation of the net.Listener interface that reads from a channel instead of an OS socket.

server/proxy/mux.go (Simplified) go
func (m *ProtocolMux) handleConn(conn net.Conn) {
  tlsConn := tls.Server(conn, m.tlsConfig)
  if err := tlsConn.Handshake(); err != nil {
      _ = conn.Close()
      return
  }

  // Inspect the negotiated protocol
  alpn := tlsConn.ConnectionState().NegotiatedProtocol

  // Look up the virtual listener for this protocol
  if handler, ok := m.handlers[alpn]; ok {
      handler.ch <- tlsConn
      return
  }

  // Route unknown ALPN to the default HTTPS handler
  if handler, ok := m.handlers[ALPNProtocolHTTP2]; ok {
      handler.ch <- tlsConn
      return
  }

  _ = tlsConn.Close()
}

When we start our internal services, we do not bind them to actual TCP ports. We bind them to these virtual ALPN listeners.

server/proxy/service.go (Simplified) go
// The gRPC server listens on the virtual "mezite-auth" listener
authListener := mux.Listen(alpn.ProtocolAuth)
go grpcServer.Serve(authListener)

// The tunnel manager listens on the virtual "mezite-tunnel" listener
tunnelListener := mux.Listen(alpn.ProtocolTunnel)
go tunnelManager.Serve(tunnelListener)

The Operational Win

By pushing routing down to the TLS layer via ALPN, we gain several significant operational advantages.

First, deploying Mezite becomes trivial. You open port 443 on your firewall and point a wildcard DNS record at your server. That is it. There are no secondary proxy ports or obscure tunnel ports to manage.

Second, it plays perfectly with modern infrastructure like Kubernetes Ingress controllers and cloud load balancers. Many Layer 4 load balancers support passing TLS traffic directly to the backend (“TCP Passthrough”). Because the routing decision happens at the edge of the Go binary rather than at the load balancer, you avoid complex routing rules in your cloud provider.

Third, it provides a unified security posture. Every single byte entering the Mezite proxy is wrapped in TLS. Whether it is an SSH session or an internal agent heartbeat, it benefits from the same cryptographic guarantees, the same certificate management, and the same robust Go TLS stack.

Complexity in networking often stems from fighting the protocol. By leaning into ALPN, we keep the architecture simple, the attack surface small, and the deployment painless.


MT

Mezite Team

Engineering