← Back to Blog
Engineering Notes Documentation

Dante in Oracle Cloud

Dante consults an ancient oracle beside a column and digital SOCKS5 routes.

I originally set up Dante in Oracle Cloud to experiment with forwarding WireGuard handshake packets over SOCKS5. It was a useful little server to have, so I wrote down the steps for anyone trying the same thing. The original 2022 setup used Ubuntu 20.04 and Dante 1.4.2; the configuration below targets the Ubuntu 24.04 LTS package instead.

There are two details worth getting right at the start: keep the proxy restricted before it is reachable, and test UDP separately from TCP. A web request through SOCKS can work perfectly while the UDP relay is still unusable.

Install Ubuntu Instance

Before you begin

You need an OCI account, an Ubuntu instance you can administer over SSH, and a client whose public source IPv4 address you know. Oracle’s current Free Tier terms, resource availability and console pricing determine whether your chosen resources are free. Check the instance shape, traffic allowance and current pricing before creating the server.

Create the Ubuntu instance

Use Oracle’s instance creation guide to choose your compartment, Ubuntu 24.04 image and suitable shape. For a directly reachable proxy, arrange a public IPv4 address, a public subnet and an internet-gateway route. Supply your SSH public key and keep the private key on your client. The wizard can use an existing VCN; do not assume it always creates a new one or that every default is appropriate.

ssh -i <your-private-key-file> ubuntu@<proxy-public-ip>

Enable the intended network access

Use the client’s actual public source address, represented here by 203.0.113.10/32, in both OCI and guest rules. This is a documentation address: replace it. If the client changes networks or uses another VPN, the source seen by the proxy can change too.

In the applicable OCI security list or NSG, allow TCP destination port 1080 and UDP destination ports 40000–45000 from that source. Prefer stateful rules for this example. The UDP allowance is a separate incoming flow, not a consequence of opening the TCP control connection. Check all applicable rules for existing broader access; adding a narrow rule does not cancel a broader one.

Allow the proxy’s required outbound TCP/UDP traffic, DNS resolution and return traffic to the client. If you use stateless rules, permit both directions explicitly; a matching stateless rule takes precedence over stateful tracking. Retain required ICMP/path-MTU handling and the instance’s SSH/platform rules. OCI permission does not replace the guest firewall.

Install and configure Dante SOCKS5 proxy

Install with the service held back

On a fresh systemd-based Ubuntu host, mask the service before installing the package so installation cannot start a listener with an unfinished policy. If you are replacing an existing setup, stop it deliberately first and preserve its configuration before proceeding.

sudo systemctl mask danted.service
sudo apt update
sudo apt install dante-server
danted -v
dpkg-query -W dante-server
ip -4 address
ip -4 route get 1.1.1.1

The Ubuntu 24.04 package used for the checks here is 1.4.3+dfsg-1; it reports Dante 1.4.3. Read the output on your own host rather than substituting the newest upstream version into package instructions. In the example the cloud-facing interface is ens3. Use the interface following dev in the route output, and check its address; a public OCI address is normally mapped to the VNIC’s private address, not configured directly on the guest.

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

This keeps a copy without repeatedly overwriting the same backup. Keep it until the revised setup and your rollback procedure have been checked.

Limit access before starting Dante

This remains a personal IPv4 proxy, with both TCP CONNECT and UDP ASSOCIATE available to an explicitly allowed user and source. It is not an open proxy, an anonymity guarantee or an encrypted transport. SOCKS username/password authentication sends credentials in cleartext. Use a trusted encrypted path that also carries UDP, or a suitable encrypted proxy transport, when the client-to-proxy network is not trusted. Encrypting the WireGuard payload does not encrypt the separate SOCKS password exchange.

Limit by IP address

Replace 203.0.113.10/32 everywhere below with the address the server actually sees. For more clients, add deliberate client and SOCKS rules as well as matching network policy. Do not temporarily replace the source with 0.0.0.0/0 just to make an unexplained failure disappear.

Limit by username

Create a separate credential account and set its password interactively. The daemon’s unprivileged identity is separate from this account. A non-login shell limits shell access; keep SSH authorization for the account under your SSH policy as well.

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

The username method checks the system account database. Authentication alone does not restrict the proxy to your new account: the configuration also needs user: danteuser. Otherwise another valid system identity may match a permissive rule.

Write the restricted configuration

Edit /etc/danted.conf and install this policy after replacing the interface and source address. Keep the address-family lines before the interface bindings. clientmethod: none means there is no pre-SOCKS authentication step; socksmethod: username still requires SOCKS authentication. The explicit command list allows CONNECT and UDP ASSOCIATE, while leaving BIND unavailable.

Show the Dante configuration
# Ubuntu dante-server: replace ens3 and the documentation-only client address.
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
}

# Keep this public-internet proxy away from local/VCN/metadata destinations.
socks block { from: 0.0.0.0/0 to: 0.0.0.0/8 command: connect udpassociate }
socks block { from: 0.0.0.0/0 to: 10.0.0.0/8 command: connect udpassociate }
socks block { from: 0.0.0.0/0 to: 100.64.0.0/10 command: connect udpassociate }
socks block { from: 0.0.0.0/0 to: 127.0.0.0/8 command: connect udpassociate }
socks block { from: 0.0.0.0/0 to: 169.254.0.0/16 command: connect udpassociate }
socks block { from: 0.0.0.0/0 to: 172.16.0.0/12 command: connect udpassociate }
socks block { from: 0.0.0.0/0 to: 192.168.0.0/16 command: connect udpassociate }
socks block { from: 0.0.0.0/0 to: 224.0.0.0/3 command: connect udpassociate }

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

The destination blocks keep this public-internet example away from common local, private, shared-address, metadata and multicast/reserved destinations. They are not a complete egress policy for every network: add any other VCN or sensitive ranges you use, and prefer an explicit destination allowlist when the proxy has only one job. Rules are evaluated in order. Keep the blocks before the pass rule. The udpreply rule permits replies for the associated client; it does not create an independent UDP association.

The udp.portrange setting selects client-facing relay ports. It does not constrain the separate source ports used for traffic from Dante to the destination. See the Dante configuration reference for the rule and port semantics.

Configure the guest firewall

Dante accepts and creates local sockets, so this setup uses the host’s INPUT/OUTPUT policy. It does not need Linux packet forwarding or a WireGuard-style masquerade rule. The path has four parts:

TrafficRequired permission
Client → proxy controlAllowed source → TCP 1080
Client → advertised UDP relayAllowed source → UDP 40000–45000
Proxy → targetOutbound traffic to the intended destination and service
Target → proxy → clientReturn traffic for those flows, including the proxy’s replies to the client

If you manage the host directly with iptables, the following adds the two client-facing allowances without duplicating an exact matching rule. Replace the source first and review the existing chain order. This is an additive operation, not a firewall transaction. It assumes your existing stateful return and OUTPUT rules already permit the remaining legs.

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-control -j ACCEPT
allow -s "$CLIENT_CIDR" -p udp --dport 40000:45000 -m comment --comment ntkernel-dante-udp -j ACCEPT
SH

Do not flush the ruleset or mix this recipe with a policy owned by UFW, firewalld or another manager. A narrow ACCEPT does not remove older broad ACCEPT rules. Express the policy through its existing owner and preserve OCI storage/platform access. On a host whose complete ruleset you manage with iptables-persistent, install that package and review the rules before saving them:

sudo apt install iptables-persistent
sudo iptables -vnL INPUT --line-numbers
sudo netfilter-persistent save

Saving snapshots all current managed rules. Avoid capturing temporary Docker, VPN or unrelated service rules into a second persistence mechanism. Verify both firewall and service behavior after a planned reboot; enabling the service alone does not establish persistence of the network policy.

Validate, then start the service

Use -V for a configuration-only check. Once the source, account, destinations and both firewall layers are correct, unmask, restart and enable the service. Restart applies your edited file even if an older daemon was already running.

sudo danted -V -f /etc/danted.conf
sudo systemctl unmask danted.service
sudo systemctl restart danted.service
sudo systemctl enable danted.service
systemctl status danted.service
sudo ss -lntp
sudo journalctl -u danted.service -n 50 --no-pager

A successful syntax check does not verify authentication, firewall reachability or UDP delivery. Perform the tests below from the intended client before considering the setup usable.

Test TCP and UDP separately

A TCP smoke test

Curl can test an authenticated SOCKS CONNECT request. With --proxy-user and no password in the argument, it prompts for the proxy password. The hostname option asks the proxy to resolve the target name. This avoids putting credentials in a URL, command history or process arguments.

curl --fail --show-error --socks5-hostname <proxy-public-ip>:1080 \
  --proxy-user danteuser https://example.com/

That verifies a TCP request, not UDP ASSOCIATE. Also check that omitting credentials fails, a wrong password fails, and another system account is not authorized. Use your own server address and a unique proxy password.

A UDP round-trip test

Save this standard-library Python diagnostic as udp-probe.py on the client. It authenticates, keeps the TCP control connection open, prints the relay address/port, sends an example.com DNS query through the UDP relay and checks the response framing, source and transaction ID. The test sends real UDP traffic; it is still a DNS probe, not a WireGuard-client interoperability test.

Show the SOCKS5 UDP diagnostic
#!/usr/bin/env python3
"""IPv4 SOCKS5 username/password UDP DNS round-trip diagnostic.

The TCP control connection stays open until the UDP reply is checked.
--relay-host is an explicit operator override, never an automatic NAT guess.
"""
import argparse
import getpass
import ipaddress
import json
import os
import socket
import struct


def read_exact(sock, count):
    data = b''
    while len(data) < count:
        part = sock.recv(count - len(data))
        if not part:
            raise ConnectionError('SOCKS control connection closed')
        data += part
    return data


def authenticate(sock, user, password):
    user, password = user.encode('utf-8'), password.encode('utf-8')
    if not (1 <= len(user) <= 255 and 1 <= len(password) <= 255):
        raise ValueError('Username/password must each be 1..255 encoded bytes')
    sock.sendall(b'\x05\x01\x02')
    if read_exact(sock, 2) != b'\x05\x02':
        raise PermissionError('Server did not select username/password')
    sock.sendall(b'\x01' + bytes([len(user)]) + user + bytes([len(password)]) + password)
    if read_exact(sock, 2) != b'\x01\x00':
        raise PermissionError('SOCKS authentication failed')


def request(sock, command, host, port):
    sock.sendall(bytes([5, command, 0, 1]) + socket.inet_aton(host) + struct.pack('!H', port))
    version, result, reserved, atyp = read_exact(sock, 4)
    if version != 5 or reserved != 0 or result != 0:
        raise ConnectionError(f'SOCKS request failed: version={version}, reply={result}')
    if atyp != 1:
        raise ValueError('This IPv4 diagnostic requires an IPv4 relay reply')
    return socket.inet_ntoa(read_exact(sock, 4)), struct.unpack('!H', read_exact(sock, 2))[0]


def probe(proxy, port, user, password, dns='1.1.1.1', relay_host=None, bind_address=None):
    dns = str(ipaddress.IPv4Address(dns))
    source = (bind_address, 0) if bind_address else None
    with socket.create_connection((proxy, port), timeout=5, source_address=source) as control:
        authenticate(control, user, password)
        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp:
            udp.settimeout(5)
            udp.bind((control.getsockname()[0], 0))
            relay = request(control, 3, '0.0.0.0', 0)
            effective_host = relay_host or relay[0]
            if effective_host == '0.0.0.0':
                raise ValueError('Wildcard relay address: inspect the path and supply --relay-host explicitly')
            effective = (socket.gethostbyname(effective_host), relay[1])
            print(json.dumps({'advertised_relay': relay, 'udp_destination': effective}), flush=True)
            transaction = os.urandom(2)
            query = transaction + b'\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00' + b'\x07example\x03com\x00\x00\x01\x00\x01'
            udp.sendto(b'\x00\x00\x00\x01' + socket.inet_aton(dns) + b'\x00\x35' + query, effective)
            packet, sender = udp.recvfrom(65535)
            if sender != effective or len(packet) < 22 or packet[:4] != b'\x00\x00\x00\x01':
                raise ValueError('Unexpected UDP sender, framing, fragmentation or address family')
            remote = socket.inet_ntoa(packet[4:8]), struct.unpack('!H', packet[8:10])[0]
            answer = packet[10:]
            if remote != (dns, 53) or answer[:2] != transaction or not answer[2] & 0x80:
                raise ValueError('UDP reply does not match this DNS transaction')
            return {'udp_round_trip': True, 'dns_reply_bytes': len(answer), 'dns_rcode': answer[3] & 15,
                    'advertised_relay': relay, 'udp_destination': effective}


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('proxy')
    parser.add_argument('--port', type=int, default=1080)
    parser.add_argument('--user', default='danteuser')
    parser.add_argument('--dns', default='1.1.1.1', help='IPv4 DNS resolver reachable through the proxy')
    parser.add_argument('--relay-host', help='Explicit reachable relay IPv4 address or hostname; port is unchanged')
    parser.add_argument('--bind-address', help='Optional local IPv4 source address for the client')
    args = parser.parse_args()
    try:
        result = probe(args.proxy, args.port, args.user, getpass.getpass('SOCKS password: '),
                       args.dns, args.relay_host, args.bind_address)
    except (OSError, ValueError) as error:
        parser.exit(1, f'{error}\n')
    print(json.dumps(result))
python3 udp-probe.py <proxy-public-ip> --user danteuser

A matching reply produces "udp_round_trip": true. The DNS response code is reported separately: receiving an error response can still demonstrate a working UDP round trip. Use --dns to select a resolver allowed by your policy. A timeout needs investigation at each of the four legs, rather than being treated as proof that all SOCKS functionality is broken.

Check the advertised relay address

With UDP ASSOCIATE, the server returns BND.ADDR and BND.PORT. The client must send SOCKS-framed datagrams to that relay, and the association lasts only while its TCP control connection remains open. Binding Dante to ens3 can advertise its private VNIC address, even though the TCP connection arrived through a public address.

Inspect the printed relay before sending credentials or traffic through a different path. A private address unreachable from the client needs a deliberate solution: a routed private/UDP-capable encrypted path, or a client with a tested public-relay override for that exact mapping. The diagnostic’s --relay-host option explicitly changes only the address used for UDP, retaining the returned port. Use it only after verifying that the chosen public IP and port map to that relay. It is not automatic SOCKS behavior or a guarantee that an arbitrary OCI/NAT topology will work. Do not add the mapped public address to the guest merely to make it bind.

A plain SSH local forward for TCP 1080 carries the control connection, but does not carry the separate SOCKS UDP datagrams. Likewise, a successful local lab test does not verify OCI public-IP mapping or a particular Windows client’s relay-address handling.

Keep the useful checks close by

If TCP works and UDP does not, start with the advertised relay and the client-facing UDP rule, then check destination egress and tracked replies. If authentication succeeds but requests are refused, check the explicit user, source and destination rules. Use the service journal and firewall counters, with passwords removed from any shared output. Finally, test the actual WireGuard client and endpoint you intend to use: relaying a DNS packet does not establish that a tunnel works or that its traffic will evade a network filter.