Good Old SOCKS5: Why It Finally Needed TLS After Three Decades

By | August 10, 2026

Some technologies arrive with a bang, spend a few years at the peak of popularity, and then gradually disappear as something newer takes their place. SOCKS5 is different.

It appeared in 1996, when HTTPS was still far from widespread, containers belonged to the future, and cloud services sounded more like science fiction than infrastructure. Since then, almost everything around it has changed: NAT and IPv6 became commonplace, TLS became ubiquitous, HTTP/2 and HTTP/3 appeared, QUIC moved into production, and containers, cloud platforms, and Zero Trust architectures became part of everyday infrastructure. SOCKS5 itself, however, has barely changed, and it still does its job remarkably well.

OpenSSH still uses it for dynamic port forwarding, while corporate proxies, automation systems, testing tools, VPN clients, and countless users rely on it whenever traffic needs to be routed through a remote server. That is probably why I have always had a soft spot for SOCKS5: there is very little unnecessary complexity in it. It simply acts as an intermediary that helps an application reach a destination through another server, without trying to replace a VPN or become a universal networking protocol.

Last year, I wrote about ProxiFyre, a small Windows tool that transparently routes traffic from selected applications through SOCKS5 proxies. Many applications either do not support SOCKS5 at all or require explicit proxy configuration, so ProxiFyre handles this at the operating-system level. Applications continue working normally while routing decisions are made transparently underneath.

Since then, the project has grown considerably. IPv6 support was added, along with process exclusions, LAN bypass, substantially reworked UDP handling, and many internal improvements that most users will probably never notice. While working on those changes, however, another aspect of SOCKS5 kept bothering me.

The protocol has survived several generations of networking technology and is still effectively a de facto standard, yet the most commonly used SOCKS5 authentication method still sends the password across the network in cleartext. Once I started looking at that problem more closely, it became clear that the password was only part of the story.

The more interesting question was why, after all these years, we have continued protecting SOCKS5 from the outside with SSH, VPNs, stunnel, and similar mechanisms instead of simply giving SOCKS5 itself a secure transport.

To answer that, we need to go back to where SOCKS5 started.

Back to 1996

The Internet of the mid-1990s looked very different from the one we know today. Most SOCKS servers were deployed inside corporate networks, universities, and other organizations where the local network itself was generally considered trusted. The primary concern was controlling access between networks, not protecting every byte exchanged between a SOCKS client and its proxy.

HTTPS was only beginning to appear. SSL 3.0 represented the current generation of secure transport, while TLS 1.0 would not be standardized until several years later. VPNs were associated much more with expensive corporate infrastructure than with a small virtual server somewhere on the Internet.

The SOCKS5 designers were therefore solving a different problem. They needed a simple and general protocol capable of relaying both TCP and UDP traffic, supporting multiple authentication mechanisms, and traversing network boundaries without imposing its own transport-security model. If confidentiality was required, the expectation was that it could be provided elsewhere.

That is why RFC 1928 defines an authentication-method negotiation mechanism rather than an encrypted transport. The actual authentication methods are described separately, and the one that eventually became ubiquitous is defined in RFC 1929: username/password authentication.

The exchange is simple. A client establishes a TCP connection to the SOCKS5 server, the two sides select an authentication method, and the client sends its username and password. RFC 1929 explicitly points out the obvious weakness: the password is transmitted in cleartext, so the method is unsuitable where passive network monitoring is possible.

This produced an interesting compromise. SOCKS5 gained an authentication mechanism simple enough to be implemented almost everywhere, but the specification itself warned that using it over an untrusted network was unsafe. More secure authentication mechanisms did exist, yet they never achieved anything close to the same level of adoption.

Why GSSAPI Never Became the Answer

SOCKS5 was designed from the beginning to support more than username/password authentication. The main secure alternative was GSSAPI, defined for SOCKS5 in RFC 1961 and commonly paired with Kerberos.

Instead of handing a password directly to the SOCKS server, the client authenticates through an existing security infrastructure. Depending on the negotiated protection level, GSSAPI can also provide integrity and confidentiality for subsequent protocol exchanges.

For a corporate network in the late 1990s, this made sense. If an organization already operated Kerberos infrastructure, SOCKS5 could become another service within the same authentication environment. Users did not need separate proxy credentials, and administrators did not need another independent password database.

The typical SOCKS5 deployment changed considerably over time. Today, a SOCKS5 server might just as easily be running on a small VPS, a home server, or a cloud VM where no Kerberos infrastructure exists.

Deploying a KDC, creating service principals and keytabs, configuring DNS, and preparing client systems simply to secure a single proxy is difficult to justify. In a corporate domain those pieces may already exist, but for a personal server or a small project they introduce far more infrastructure than the proxy itself requires.

Client support presents another problem. In most applications, SOCKS5 authentication still comes down to the familiar username and password fields, sometimes accompanied by a no-auth option. GSSAPI support is considerably less common and often depends on additional operating-system or application configuration.

The result is that SOCKS5 has had a more sophisticated authentication mechanism for decades, yet most real-world deployments still use RFC 1929. The problem was never the complete absence of a secure alternative; it was that the available alternative fit enterprise identity infrastructure far better than the simple SOCKS5 deployments that later became common.

What was missing was a way to retain the universally supported username/password model while protecting it without requiring an entire Kerberos environment.

We Have Always Protected SOCKS5 From the Outside

Authentication is only part of the problem. With traditional SOCKS5, the CONNECT command, destination information, and subsequent TCP stream between the client and proxy receive no additional transport protection either.

If the application uses TLS itself, as HTTPS does, its application payload is already encrypted. From SOCKS5’s perspective, however, this is still just a byte stream to relay. Application-level encryption does not protect the SOCKS5 authentication exchange or protocol metadata, and it does nothing for application protocols that are not already encrypted.

Over the years, we have developed plenty of ways to secure the client-to-proxy path. We can put SOCKS5 through an SSH tunnel, establish a VPN first, or use stunnel or another TLS wrapper. All of these approaches are mature, well understood, and perfectly valid.

Their architecture is essentially the same:

Application
     ↓
  SOCKS5
     ↓
TLS / SSH / VPN
     ↓
  Network

SOCKS5 remains unchanged while another protocol provides the secure transport around it. This works so well that over time it became the natural way to deploy SOCKS5 across an untrusted network.

That eventually led me to a fairly simple question: if the only thing we need is a protected transport between the SOCKS5 client and server, why not establish TLS directly before starting the SOCKS5 exchange?

My First Thought Was stunnel

The obvious solution already existed: run stunnel on the client and server and send a normal SOCKS5 connection through the resulting TLS tunnel.

On the client side, ProxiFyre would connect to a local stunnel port. The encrypted connection would then travel to the remote server, where another stunnel instance would forward it to an ordinary SOCKS5 service. SOCKS5 itself would remain completely unchanged.

For an existing SOCKS5 deployment, this may actually be the fastest way to add TLS. Issue a certificate, configure forwarding on both sides, and run the additional services. It is predictable and proven.

For ProxiFyre, however, the architecture felt unnecessarily indirect. Instead of connecting to the actual proxy, ProxiFyre would connect to a local intermediary, requiring another configuration file and another process whose state had to be managed.

The arrangement becomes particularly awkward when several SOCKS5 servers are involved. Each remote server needs a corresponding local TLS listener, and the ProxiFyre configuration contains local ports rather than the actual proxy endpoints.

Information about one logical connection also becomes divided between two applications. ProxiFyre knows which routing rule matched and which proxy should be used, but it sees only a local endpoint such as 127.0.0.1:10001. The remote hostname, certificate validation, TLS settings, and related diagnostics live elsewhere in the stunnel configuration.

For a generic TLS wrapper, that separation is perfectly reasonable. In this particular case, though, the wrapper has only one purpose: protecting a TCP connection that ProxiFyre already establishes and manages.

If ProxiFyre already knows where it wants to connect, there is little reason it cannot perform the TLS handshake itself, validate the server certificate, and then begin the normal SOCKS5 protocol on the secured stream.

SOCKS5 over TLS

That is essentially how the new transport works. ProxiFyre first establishes the TCP connection to the proxy and then performs a TLS handshake through the Windows SChannel stack. Only after TLS is established does it send the SOCKS5 greeting and begin authentication.

The protocol itself does not change. There are no new SOCKS5 commands and no custom authentication mechanism. Standard SOCKS5 messages are simply carried over a TLS-protected stream.

The resulting sequence looks like this:

TCP connection to proxy
          ↓
     TLS handshake
          ↓
 Certificate validation
          ↓
    SOCKS5 greeting
          ↓
     Authentication
          ↓
        CONNECT
          ↓
   TCP data transfer

I chose SChannel because ProxiFyre is Windows-only and already relies heavily on operating-system networking facilities. Using the native Windows TLS implementation avoids shipping OpenSSL or another cryptographic library while leaving certificate validation, encryption, and TLS negotiation to the operating system.

The current implementation negotiates TLS 1.2. Enabling SOCKS5-over-TLS requires only a few additional configuration parameters:

{
  "appNames": ["chrome"],
  "socks5ProxyEndpoint": "proxy.example.com:443",
  "username": "alice",
  "password": "your-password",
  "socks5Transport": "TLS",
  "tlsServerName": "proxy.example.com",
  "supportedProtocols": ["TCP", "UDP"],
  "supportedAddressFamilies": ["IPv4", "IPv6"]
}

socks5Transport selects TLS instead of the default plain TCP transport, while tlsServerName specifies the hostname used for SNI and certificate validation. When it is identical to the host in socks5ProxyEndpoint, ProxiFyre can determine it automatically.

For publicly trusted certificates, standard chain and hostname validation is used. Private or self-signed certificates can instead be authenticated by specifying the exact SHA-256 leaf-certificate fingerprint through tlsPinnedSha256. There is also a tlsAllowInvalidCertificate option, but disabling certificate validation should normally be limited to temporary diagnostics rather than used as a deployment configuration.

Existing configurations require no migration because ordinary SOCKS5 over TCP remains the default.

One current transport limitation is worth mentioning here: the SOCKS5 server endpoint itself must be reachable over IPv4, either as an IPv4 literal or through a hostname with an A record. The destinations being proxied can still be IPv4, IPv6, or both, provided the SOCKS5 server supports the corresponding address types.

With TLS enabled, the SOCKS5 credentials, protocol commands, destination metadata, and the complete TCP relay between ProxiFyre and the proxy are protected. Applications remain unaware of any of this; they continue opening normal network connections while ProxiFyre transparently handles process matching, proxy selection, TLS, SOCKS5 authentication, and relay setup underneath.

For TCP, that largely solves the problem. UDP is different.

UDP Is a Little More Complicated

The most important limitation is straightforward: TLS protects the SOCKS5 control connection, but it does not encrypt the separate SOCKS5 UDP relay traffic.

SOCKS5 handles UDP differently from TCP. The client first establishes the usual TCP connection to the proxy and sends UDP ASSOCIATE. The server then creates a UDP relay and returns its address and port. The TCP connection remains open as the control channel while the actual datagrams travel separately over UDP.

When SOCKS5-over-TLS is enabled, ProxiFyre protects that control connection. The SOCKS5 greeting, username and password, UDP ASSOCIATE request, and the server’s response all travel inside TLS.

The UDP datagrams themselves continue to use the standard SOCKS5 UDP relay format and therefore remain outside that TLS stream.

It might seem natural to simply push UDP datagrams through the existing TLS connection, but that would no longer behave like normal SOCKS5 UDP. The datagrams would effectively be encapsulated inside TCP and inherit retransmission, ordering, and head-of-line blocking characteristics that UDP applications are specifically designed to avoid.

A separate secure datagram transport based on DTLS or QUIC would be another possibility, but it would require a new protocol extension and matching support at both ends. At that point we would no longer be talking about ordinary SOCKS5 compatibility.

ProxiFyre therefore takes the conservative approach. TCP CONNECT traffic is carried completely inside TLS, while UDP ASSOCIATE uses TLS to protect authentication and control-plane negotiation but retains the standard SOCKS5 UDP relay.

For QUIC and other protocols that already encrypt their own payloads, the application data remains cryptographically protected by the application protocol itself. The SOCKS5 UDP header and flow characteristics, however, are still visible between ProxiFyre and the proxy. Applications sending plaintext UDP should not treat SOCKS5-over-TLS as encryption for that traffic.

This distinction is important, but the practical improvement is still substantial: credentials, SOCKS5 negotiation, and TCP traffic can now be protected without another client-side service or external TLS wrapper.

Once the client side was working, however, another practical requirement appeared. A conventional SOCKS5 server expects a SOCKS greeting immediately after accepting the TCP connection; it does not expect a TLS handshake first. To use the new transport directly, the server therefore needs corresponding TLS-listener support.

The Client Is Ready. What About the Server?

I wanted the server side to retain normal SOCKS5 behavior: TCP CONNECT, UDP ASSOCIATE, username/password authentication, IPv4 and IPv6 destinations, and flexible access-control rules. The difference would be that TLS belonged to the listener itself instead of being provided by another service in front of it.

My first thought was naturally Dante. It is a mature and well-known SOCKS server with flexible rules, TCP and UDP support, and several authentication mechanisms. I have used it for years, so its configuration model was already familiar.

Dante solves the traditional SOCKS-server problem extremely well, but it does not provide the integrated TLS listener needed for this particular design. I therefore needed a server with a slightly different focus: establish the secure transport first, and then run a normal SOCKS5 session inside it.

And That’s How Alighieri Appeared

The project actually began somewhere else. I originally received a request to add a lightweight SOCKS5 server to WireSock Secure Connect that could remain bound to the VPN tunnel. The scope gradually expanded until it made more sense to turn the server into an independent project.

That project became Alighieri, a SOCKS5 server written from scratch in Rust for Windows and Linux. The name is, of course, a reference to Dante, and the configuration language is intentionally influenced by Dante’s rule-oriented model.

Alighieri focuses on SOCKS5 and implements its two most useful relay commands: CONNECT for TCP and UDP ASSOCIATE for UDP. It uses Tokio for asynchronous I/O and a deny-by-default access-control model built around client and socks rules.

Rules are evaluated in order and can match client addresses, destinations, ports, protocols, commands, and authentication methods. Username/password authentication is retained for compatibility with ordinary SOCKS5 clients, while server-side credentials can be stored using Argon2id hashes.

Most importantly for this use case, Alighieri can expose a TLS-wrapped SOCKS5 listener. Clients establish TLS first and then perform an otherwise standard SOCKS5 exchange inside the protected connection.

For public deployments, the server can use an existing certificate and private key or obtain a certificate automatically. Its built-in ACME support uses the TLS-ALPN-01 challenge, allowing Let’s Encrypt validation directly through the TLS listener on port 443. This avoids both a separate HTTP listener on port 80 and integration with a DNS-provider API, while certificate renewal happens automatically.

Alighieri has since gained the operational features I would expect from a server intended to run continuously: native Windows Service support, systemd integration, structured text and JSON logs, Prometheus-style metrics, connection and bandwidth controls, DNS policies, configuration hot reload, and a local configuration wizard.

It remains a standalone SOCKS5 server rather than a ProxiFyre-specific backend, so ordinary SOCKS5 clients can use it as well.

Dante and Alighieri Solve Different Problems

Alighieri is not intended to be a universal replacement for Dante. Dante has been developed and deployed for many years and supports a much broader collection of traditional SOCKS features, including SOCKS4, BIND, GSSAPI, PAM integration, and libsocks.

Alighieri deliberately has a narrower focus: SOCKS5, TCP CONNECT, UDP ASSOCIATE, Windows and Linux support, integrated TLS, and a relatively modern operational model. Keeping that scope limited makes it possible to avoid pulling in features that are useful in traditional Unix environments but unnecessary for the original problem.

The platform model is also different. Alighieri uses the same codebase on Linux and Windows and can run as a systemd service, native Windows Service, container, or ordinary console process. That makes it convenient in mixed environments where the same proxy configuration and access-control model should be used across platforms.

If an environment depends on Kerberos, PAM, SOCKS4, BIND, or Dante’s client library, Dante remains the obvious choice. If the requirement is a focused SOCKS5 server with CONNECT, UDP ASSOCIATE, Windows/Linux support, and an integrated TLS listener, Alighieri is aimed directly at that scenario.

What It Looks Like in Practice

A public Alighieri server can operate with a single TLS listener on port 443. With ACME enabled, the same listener handles the TLS-ALPN-01 validation challenge, obtains the Let’s Encrypt certificate, stores the ACME state locally, and renews the certificate automatically.

The domain must resolve to the server, and incoming TCP port 443 must be reachable by Let’s Encrypt during validation.

A simple configuration might look like this:

internal: 0.0.0.0:443
external: 0.0.0.0

socksmethod: username
userlist: /etc/alighieri/users

tls.acme.domains: proxy.example.com
tls.acme.email: [email protected]
tls.acme.cache: /var/lib/alighieri/acme

logoutput: stdout
logformat: text

dns.deny: private linklocal loopback reserved

client pass "clients" { }

socks block "deny-loopback-v4" {
    to: 127.0.0.0/8
}

socks block "deny-loopback-v6" {
    to: ::1/128
}

socks pass "internet" {
    protocol: tcp udp
    command: connect udpassociate
}

socksmethod: username requires username/password authentication, and userlist points to the credential database. Alighieri can generate Argon2id-backed entries itself, so plaintext passwords do not need to be stored in the file.

The client and socks rules are processed from top to bottom, with the first matching rule taking effect. Requests that do not match an allow rule are denied. In this example, loopback destinations are blocked explicitly, while the DNS policy rejects several classes of unsafe resolved addresses.

A user can be created and the resulting configuration validated before the server starts:

sudo alighieri user add alice --userlist /etc/alighieri/users
sudo alighieri --check /etc/alighieri/alighieri.conf

The user-management command prompts for the password and stores its hash, while --check parses and validates the configuration without opening the listener.

On the ProxiFyre side, a corresponding configuration can be as simple as:

{
  "logLevel": "Info",
  "bypassLan": true,

  "proxies": [
    {
      "appNames": [""],

      "socks5ProxyEndpoint": "proxy.example.com:443",
      "username": "alice",
      "password": "your-password",

      "socks5Transport": "TLS",
      "tlsServerName": "proxy.example.com",

      "supportedProtocols": [
        "TCP",
        "UDP"
      ],
      "supportedAddressFamilies": [
        "IPv4",
        "IPv6"
      ]
    }
  ],

  "excludes": [
    "wireguard.exe",
    "openvpn.exe"
  ]
}

The empty string in appNames acts as a catch-all for processes that have not matched an earlier proxy rule and are not listed in excludes. Catch-all entries should therefore be placed after more specific proxy rules.

Once ProxiFyre is running, applications continue opening ordinary TCP and UDP connections. ProxiFyre determines the originating process, applies the appropriate rule, establishes TLS to Alighieri, and performs SOCKS5 authentication followed by either CONNECT or UDP ASSOCIATE.

The application sees none of this. From its perspective, it is simply communicating with the network in the normal way.

What Else Changed in ProxiFyre

TLS was the main motivation for this article, but several other changes are worth mentioning because they have made ProxiFyre considerably more practical as a general-purpose traffic redirection tool.

The excludes configuration makes it possible to define a catch-all proxy while explicitly keeping selected applications on the direct connection. This is particularly useful when ProxiFyre is combined with another VPN because the VPN’s carrier process can be excluded to prevent its outer tunnel traffic from being captured recursively.

bypassLan solves another practical problem. When most application traffic is sent through a remote proxy, connections to a router, NAS, printer, development server, or another local resource should usually remain on the local network. LAN bypass handles that automatically for the supported local address ranges.

IPv6 destination support was another important addition. Each proxy can specify supportedAddressFamilies, and unsupported families are blocked rather than silently allowed to escape directly. This is important because an IPv4-only proxy configuration should not accidentally turn IPv6 into a bypass path.

There are a couple of limitations worth keeping in mind. As mentioned earlier, the proxy endpoint itself currently requires IPv4 reachability even when IPv6 destinations are being relayed. Fragmented IPv6 UDP datagrams are also passed through rather than redirected, because safely rewriting only part of a fragmented datagram would risk corruption; applications that emit such traffic therefore need to take that behavior into account.

UDP handling in general has been reworked substantially. Associations are established asynchronously, initial datagrams can be queued while negotiation completes, and an association closed by the server can be recreated automatically. Handling of dynamic relay endpoints and temporary socket failures has also been improved, which is particularly useful with QUIC and HTTP/3 traffic.

A large amount of work has gone into changes that are less visible from the outside: asynchronous lifetime handling, shutdown races, SOCKS5 packet validation, timeout behavior, cleanup, process matching, and configuration diagnostics. None of these make particularly exciting screenshots, but collectively they are the difference between an experiment and something that can run continuously.

Final Thoughts

This started with a simple goal: protect SOCKS5 username/password authentication without deploying another TLS wrapper. The result was broader than I initially expected. ProxiFyre gained a native SOCKS5-over-TLS transport, while Alighieri emerged as an independent SOCKS5 server capable of terminating that TLS connection directly.

What I like about the result is that SOCKS5 itself does not need to change. There is no new command set, no custom authentication protocol, and no attempt to reinvent the proxy protocol. We simply establish a secure transport first and then run ordinary SOCKS5 over it.

Perhaps that is also part of the reason SOCKS5 has survived for so long. It is simple enough that we do not need to redesign it every time the surrounding network changes.

After three decades, it just needed a secure ride.

Leave a Reply

Your email address will not be published. Required fields are marked *