← Back to Blog
Engineering Notes Documentation

Configuring Dante as a SOCKS5 Proxy for Cloudflare WARP on Ubuntu 24

Dante and Virgil in an engraving-inspired collage with a selected UDP route.

A proxy for one job can be much easier to reason about than a general-purpose SOCKS server. In this case, the job is to carry a WireGuard-based WARP connection through a chosen server, while refusing ordinary web connections and UDP traffic to other destinations. This is the server-side half of the setup I described in September 2025; the instructions below use Ubuntu 24.04 LTS and its Dante package.

The important detail is that SOCKS5 UDP has two separate connections to get right: a TCP control connection and a UDP relay. A listening port, a successful login or packets leaving the server each prove only one part of the path.

SOCKS-capable WireGuard client -> Dante on Ubuntu 24.04 -> selected WARP WireGuard endpoint

Keep the scope narrow

You need a client that implements SOCKS5 UDP ASSOCIATE, such as a suitably configured WireSock client, and a valid WARP WireGuard profile. This article does not create that profile or install Cloudflare’s client on the proxy server. Keep the WireGuard peer’s endpoint and Dante’s allowed destination in agreement.

This configuration allows the selected endpoint 162.159.192.1:2408. Use the endpoint assigned to your WARP WireGuard profile and keep the Dante and firewall rules consistent with it. Cloudflare’s current firewall documentation distinguishes consumer WARP addresses from Cloudflare One addresses and distinguishes WireGuard from MASQUE. This UDP/2408 recipe covers a WireGuard profile with a matching endpoint; it does not cover MASQUE, endpoint fallback or all traffic needed by Cloudflare’s official applications.

The example is IPv4-only and allows one client source and one destination. Replace 203.0.113.10/32 with the client’s source address as seen by the server. Replace ens3 with the server interface you actually intend to use. If you select a different endpoint, change it consistently in the configuration, network policy and checks below. Keep SSH and provider/platform access intact while making network changes.

Step 1. Install Dante and create an authentication account

On a fresh systemd-based Ubuntu installation, hold the service masked until its configuration and firewall rules are ready. For an existing installation, plan the interruption, stop the old service deliberately and preserve its configuration first. Masking an already-running service does not stop it.

sudo systemctl mask danted.service
sudo apt update
sudo apt install dante-server iproute2 curl tcpdump
danted -v
dpkg-query -W dante-server

The Ubuntu 24.04 package used for the configuration checks is 1.4.3+dfsg-1, reporting Dante 1.4.3. Check your installed version rather than assuming it matches the latest upstream release.

sudo useradd --system --no-create-home --shell /usr/sbin/nologin danteuser
sudo passwd danteuser

Set a separate proxy password interactively. The non-login shell limits shell access, but it is not a complete SSH authorization policy. Keep this account out of SSH access under your existing SSH policy. Dante’s privileged and unprivileged process identities are separate from this authentication account.

Find the interface used for the route to the selected endpoint. Looking for the token after dev works with both a directly connected route and the usual gateway form; a fixed field number does not. This is the selected route to that address, which need not be the system’s default route.

#!/usr/bin/env bash
set -euo pipefail
IFACE=$(ip -o -4 route get 162.159.192.1 | awk '{for (i=1; i<NF; i++) if ($i=="dev") {print $(i+1); exit}}')
[ -n "$IFACE" ] || { echo 'No route interface found' >&2; exit 1; }
printf '%s\n' "$IFACE"

Check the resulting interface and its IPv4 address with ip -4 address show. An interface name does not imply a public address. On an OCI instance, for example, the public IPv4 address is mapped to a private VNIC address. That distinction matters when the client receives Dante’s UDP relay address.

sudo cp --archive --backup=numbered /etc/danted.conf /etc/danted.conf.before-warp
sudoedit /etc/danted.conf

Keep the backup until both the revised setup and its rollback procedure have been checked. Numbered backups avoid silently replacing the only earlier copy when repeating the command.

Step 2. Configure Dante for one endpoint

Install the following policy after replacing the source address and interface. The address-family settings precede the interface bindings. clientmethod: none disables a separate pre-SOCKS authentication step; socksmethod: username still requires SOCKS username/password authentication.

Show the restricted Dante configuration
# Replace ens3, the documentation-only client address and the selected endpoint.
logoutput: syslog
internal.protocol: ipv4
external.protocol: ipv4
internal: ens3 port = 1080
external: ens3
clientmethod: none
socksmethod: username
user.privileged: root
user.unprivileged: nobody

client pass {
    from: 203.0.113.10/32 to: 0.0.0.0/0
    log: connect disconnect error
}
client block {
    from: 0.0.0.0/0 to: 0.0.0.0/0
    log: connect error
}

socks pass {
    from: 203.0.113.10/32
    to: 162.159.192.1/32 port = 2408
    command: udpassociate
    protocol: udp
    proxyprotocol: socks_v5
    socksmethod: username
    user: danteuser
    udp.portrange: 40000-45000
    log: connect disconnect error
}
socks pass {
    from: 162.159.192.1/32 port = 2408
    to: 203.0.113.10/32
    command: udpreply
    protocol: udp
    log: connect disconnect error
}
socks block {
    from: 0.0.0.0/0 to: 0.0.0.0/0
    log: connect error
}

The username method authenticates a system account. The additional user: danteuser is the authorization check that limits use to this account. Without it, another valid system user can match the same pass rule. Both the allowed source and the allowed identity must match.

The UDP rules permit datagrams only to the selected IP and port, and permit replies from that endpoint for an existing association. CONNECT and BIND remain blocked. This restricts where the user can send packets; it does not inspect their payload or prove that those bytes are WireGuard. Using a literal address makes the destination policy easy to inspect. A hostname-based policy also needs resolution and request/reply testing.

The udp.portrange setting constrains the relay ports used between the client and Dante to 40000–45000. It does not constrain Dante’s separate outgoing source port towards the endpoint. These are different sockets, with different firewall requirements. The Dante configuration manual describes the rule and port-range semantics.

Make all four network legs reachable

Dante is a user-space relay; this setup does not require turning the server into an IP router or adding a WireGuard-style masquerade rule. It does require the host firewall and any provider firewall to allow the complete path:

LegPermission to arrange
Client → Dante TCP controlAllowed client source → TCP 1080, with replies
Client → Dante UDP relayAllowed client source → advertised relay address and UDP 40000–45000, with replies
Dante → selected endpointOutbound UDP to 162.159.192.1:2408 from Dante’s external socket
Endpoint → Dante external socketUDP return traffic from 162.159.192.1:2408, then Dante relays it to the client

Opening TCP 1080 does not open the UDP relay. The client’s first UDP datagram is a new flow; an established-traffic rule for the TCP connection cannot admit it. A stateful rule may cover replies to the outbound endpoint traffic, but the client-facing UDP allowance still has to exist. With stateless policy, arrange both directions explicitly. For OCI, check the applicable security lists and NSGs as well as the guest firewall.

If you directly manage this dedicated server’s firewall with iptables, the following adds the two client-facing allowances without duplicating exact matching rules. Replace the source first and review existing chain order. It assumes the remaining stateful return and OUTPUT permissions in the table already exist; it is not a complete firewall policy or an atomic firewall transaction.

sudo bash <<'SH'
set -eu
CLIENT_CIDR=203.0.113.10/32
allow() {
    if iptables -w 5 -C INPUT "$@" 2>/dev/null; then return; else rc=$?; fi
    [ "$rc" -eq 1 ] || exit "$rc"
    iptables -w 5 -I INPUT 1 "$@"
}
allow -s "$CLIENT_CIDR" -p tcp --dport 1080 -m comment --comment ntkernel-dante-warp-control -j ACCEPT
allow -s "$CLIENT_CIDR" -p udp --dport 40000:45000 -m comment --comment ntkernel-dante-warp-udp -j ACCEPT
SH

Use the existing firewall owner. Do not flush the ruleset or mix these commands into a policy owned by UFW, firewalld or another manager. A narrow ACCEPT does not cancel an older broad ACCEPT. Persist the intended policy through its existing manager, preserving unrelated rules, and verify it after a planned reboot. Enabling Dante at boot does not make temporary firewall rules persistent.

Treat the advertised relay address as part of the setup

In the UDP ASSOCIATE reply, BND.ADDR and BND.PORT tell the client where to send SOCKS-encapsulated UDP datagrams. Binding the listener to a real interface avoids some wildcard-address problems, but the address still has to be reachable from that client.

Behind a provider NAT, Dante can advertise the interface’s private IPv4 address. A public TCP connection can therefore succeed while UDP fails. Check the actual reply in a client diagnostic log or a private packet capture, including the returned port. Do not assume that WireSock or another client substitutes the control server’s address: confirm the behavior of the exact client/version you use. A public relay mapping must carry the selected UDP range and preserve the port mapping expected by the client. A plain SSH TCP local-forward does not carry this SOCKS UDP flow.

SOCKS username/password authentication sends the password without transport encryption. WireGuard encrypts its own payload, not this separate SOCKS exchange. Use a trusted encrypted client-to-proxy path that carries both TCP and UDP, or an appropriately secured proxy transport, when that network is untrusted. Keep captures and diagnostic logs private.

Step 3. Start & verify Dante

1. Check the configuration without starting a listener

sudo danted -V -f /etc/danted.conf

-V verifies the configuration and exits. The -d1 option runs a foreground debug server and can conflict with a service already using the same listener. A successful parse is useful, but does not test passwords, network rules or packet delivery.

2. Apply the file and enable the service

sudo systemctl unmask danted.service
sudo systemctl restart danted.service
sudo systemctl enable danted.service
sudo systemctl --no-pager status danted.service
sudo ss -4 -ltnp 'sport = :1080'
sudo journalctl -u danted.service -n 50 --no-pager

Restart applies the edited configuration even when an older daemon was already running. enable --now alone does not reload such a daemon. Expect an IPv4 TCP listener on the intended interface; the per-client UDP relay appears after UDP ASSOCIATE, so an idle server need not have a relay socket waiting in that range.

Sanity checks

Confirm that ordinary SOCKS TCP forwarding is denied

Run this from the allowed client and enter the proxy password when curl prompts. Replace YOUR_SERVER_IP with the control server’s reachable address. Using a password prompt avoids placing it in a URL, shell history or process arguments.

curl --verbose --socks5-hostname YOUR_SERVER_IP:1080 \
  --proxy-user danteuser https://example.com/

Authentication should succeed and the SOCKS CONNECT request should then be denied. A timeout, bad-password error or connection refusal proves something different; merely seeing curl fail is not enough. Also check that an incorrect password fails, another system user cannot relay, and a source outside the allowlist cannot negotiate. Curl is a negative TCP test here, not a UDP or WARP test.

Verify UDP with the actual intended client

  • Start the SOCKS-capable WireGuard client with the valid profile and matching endpoint. Keep the SOCKS TCP control connection open; the UDP association depends on it.
  • Inspect the advertised relay address/port and verify that client datagrams reach that socket. Then check outbound UDP to the selected endpoint and replies back through Dante.
  • Look for a fresh WireGuard handshake and successful application traffic through the intended tunnel. A UDP ASSOCIATE success reply or an outbound packet by itself does not prove this.
  • Test a disallowed destination and a different destination port: neither should be relayed. Repeat the reachability checks after any client network or server firewall change.
sudo journalctl -u danted.service -f

The configuration was checked with Dante 1.4.3 using a local UDP echo fixture, including denied identities, sources, commands and destinations. That verifies the relay policy; a current Cloudflare handshake, WireSock interoperability and your public/NAT path still need to be checked with your own profile and client.

(Optional) Quick packet view

Use the interface printed by the route check, for example ens3. This filter shows the endpoint leg only; inspect the client-facing relay separately when diagnosing a missing first UDP packet.

sudo tcpdump -ni ens3 'udp and host 162.159.192.1 and port 2408'

A capture can distinguish requests from replies, but encrypted UDP packets alone do not establish a successful tunnel. Avoid posting captures containing SOCKS authentication or unrelated traffic.

Step 4. Troubleshooting notes

  • Configuration parser error: check the installed version and use the literal address with /32. Keep address-family settings before interface bindings. Run the syntax-only check again after edits.
  • Login succeeds but the client times out: inspect the returned relay address, separate new-flow UDP ingress, relay-port mapping and both firewall layers before changing authentication policy.
  • Outbound endpoint packets with no replies: check the selected profile/endpoint, provider path, OUTPUT/return rules and remote service availability. Opening arbitrary destinations on the proxy is not a diagnosis.
  • Replies reach the server but not the client: inspect the endpoint-restricted udpreply rule, active TCP association and client-facing UDP return path.
  • It works until a reboot or network change: check persistent service/firewall configuration and the client source address the server now sees.

Conclusion & next steps

The useful result is a small, explicit policy: one client source, one authorized identity, UDP to one selected endpoint, and a reachable relay path. Each restriction has a test, so a failure is easier to locate without temporarily turning the server into a general proxy.

Choosing the proxy’s location can change the path into Cloudflare, but it does not guarantee a particular WARP egress country, anonymity or a streaming catalogue. Cloudflare’s WARP FAQ describes location-dependent routing and compatibility limits. Treat any region or service result as a measured outcome of that particular setup, not a property promised by this configuration.