Your own SOCKS5 on a bare server: ssh -D, Dante, and 3proxy — three ways to do the same thing

SOCKS5 — the most boring and the most resilient way to give one application another IP. It does not encrypt traffic, does not bring up an interface, does not require root rights on the client, and is understood by everyone: in a browser via an extension, curl, git, Telegram, Python script, any sing-box or Xray acting as the outbound. When you need to “go somewhere else” from a specific program, rather than tunneling the entire laptop, SOCKS5 usually proves to be the right tool.

The task is one, but there are three bare-server implementations, and they are fundamentally different. Below are all three, with line-by-line configs, authentication, systemd units, and DNS leakage checks. Plus a separate discussion of why a passwordless proxy stops being yours in about a day.


Diagram based on man ssh(1), danted.conf(5) and README 3proxy

Info:

Who needs this. You have a VPS in the right country and SSH access to it. You want individual programs to go to the network through it — without VPN for the entire machine, without changing routes, without administrator rights on the client. Everything else is implementation details.


Variant 1. ssh -D: thirty seconds and not a single config

The OpenSSH client can act as a SOCKS server by itself. The manual page states this directly: “Specifies a local “dynamic” application-level port forwarding… Currently the SOCKS4 and SOCKS5 protocols are supported, and ssh will act as a SOCKS server”.

A key point that is often misunderstood: the port is listened to by your local machine, not the server. The server only handles outbound connections. You don’t need to install a daemon on the VPS — regular sshd there is enough.

ssh -D 127.0.0.1:1080 -N -C user@vps.example.com

What’s inside:

Flag What it does
-D 127.0.0.1:1080 start a SOCKS server on the local port 1080, listen on loopback only
-N “Do not execute a remote command” — do not run a shell, only forwarding
-C compress all traffic; useful on a narrow channel, detrimental on a fast one
-f go to the background (for manual startup; not needed for systemd)
Warning:

The address in -D must be specified. ssh -D 1080 in most builds listens on localhost only, but behavior depends on GatewayPorts and build options — and when you decide to let your wife’s laptop access it too and add -D 0.0.0.0:1080, you’ll get a passwordless SOCKS proxy accessible to the entire local network. For access from other machines, it’s better to forward the port again rather than open it.

To keep the tunnel from dying

Bare ssh -D collapses at the slightest interruption. There are two complementary ways to fix it.

First — make SSH notice when the connection is lost. Second — restart the process. The man page for autossh(1) explicitly recommends the first: “you may wish to explore using the ServerAliveInterval and ServerAliveCountMax options to have the SSH client exit if it finds itself no longer connected to the server. In many ways this may be a better solution than the monitoring port”.

Therefore, in a setup with autossh, the monitoring port is disabled (-M 0 — “Setting the monitor port to 0 turns the monitoring function off, and autossh will only restart ssh upon ssh’s exit”), and resilience is tied to keepalive:

# /etc/systemd/system/socks-tunnel.service
[Unit]
Description=SOCKS5 via SSH to vps.example.com
After=network-online.target
Wants=network-online.target

[Service]
User=proxyuser
Environment=AUTOSSH_GATETIME=0
ExecStart=/usr/bin/autossh -M 0 -N -T \
  -o "ServerAliveInterval=15" \
  -o "ServerAliveCountMax=3" \
  -o "ExitOnForwardFailure=yes" \
  -o "StrictHostKeyChecking=yes" \
  -i /home/proxyuser/.ssh/id_ed25519 \
  -D 127.0.0.1:1080 user@vps.example.com
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

AUTOSSH_GATETIME=0 here is not cosmetic: by default autossh considers the connection successful only after 30 seconds of life, and with zero it also ignores the failure of the first start — exactly what you need when the system boots and the network may come up a second later.

ExitOnForwardFailure=yes is also not trivial. Without it, ssh may happily establish a connection where port 1080 is already taken by something else, and you’ll be puzzled for a long time why the proxy works but goes somewhere else.

Success:

When this is enough. One user, one laptop, SSH key authentication already set up. No authentication to the SOCKS proxy is needed at all: to use the proxy you just need access to your local machine’s localhost, which you already have. This is the safest configuration of the three — simply because it listens nowhere from the outside.


Variant 2. Dante: a daemon that knows about system users

Dante (sockd) — the canonical SOCKS implementation from Inferno Nettverk. The current version is 1.4.4; in Debian 13 and Ubuntu the package is dante-server, the config lives in /etc/danted.conf (upstream by default uses /etc/sockd.conf — don’t mix them when reading the official docs).

apt update && apt install -y dante-server

Minimal working config with password and subnet restriction:

# /etc/danted.conf

logoutput: /var/log/danted.log

# address to listen for clients
internal: 0.0.0.0 port = 1080
# which interface to go out through
external: eth0

# under whose name to run after startup
user.privileged: root
user.notprivileged: nobody

# client authentication method for SOCKS
socksmethod: username

# ── rules about who can connect
client pass {
    from: 0.0.0.0/0 to: 0.0.0.0/0
    log: error connect disconnect
}

# ── rules about what can be done inside a session
socks pass {
    from: 0.0.0.0/0 to: 0.0.0.0/0
    command: connect
    socksmethod: username
    user: socksuser
    log: error connect
}

Two levels of rules — the main feature of Dante, and what trips people up most. client decides whether to accept a TCP connection at all. socks decides what is allowed inside the established SOCKS session. Missing socks pass — the client will connect but get denial on the first request.

Names to know:

Directive Allowed values (per danted.conf(5))
socksmethod none, username, gssapi, pam.any, pam.address, pam.username, rfc931, bsdauth
command bind, connect, udpassociate, bindreply, udpreply
log connect, disconnect, data, error, ioop, tcpinfo
logoutput syslog[/facility], stdout, stderr, file name or their combination
external.rotation none (default), route, same-same

socksmethod: username means authenticating against the system user database. Create a user who has nothing to do in the system:

useradd -r -s /usr/sbin/nologin socksuser
passwd socksuser
Error:

Do not leave socksmethod: none in a config that listens on 0.0.0.0. That is exactly the configuration scanners find fastest: an open SOCKS5 on port 1080 — a standard target for mass scans. If authentication gets in the way (for example, the client can’t use it), close access at the level client pass { from: } and with a firewall — but don’t leave both open.

Source restrictions are written directly into the rule:

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

Order matters: rules are evaluated top to bottom, the first match wins. An explicit block at the end is good practice, even if the default is already “deny”.

Starting and testing:

systemctl enable --now danted
systemctl status danted
tail -f /var/log/danted.log

Variant 3. 3proxy: your own users and ACL without system accounts

3proxy — a single C binary, around since the early 2000s. The latest release is 0.9.8 from August 7, 2024; it includes IMAPv4 proxy and STARTTLS support. It can be built from source or installed as ready-made deb/rpm packages from releases on GitHub.

# build from source
git clone https://github.com/3proxy/3proxy
cd 3proxy
ln -s Makefile.Linux Makefile
make
make install

Configuration — sequential, not declarative: directives are applied as the file is read, and the order of lines changes the meaning.

# /etc/3proxy/3proxy.cfg

# ── DNS: your own resolvers and cache so you don’t hammer the system on every request
nserver 1.1.1.1
nserver 9.9.9.9
nscache 65536

# ── logs with daily rotation, flush after each entry
log /var/log/3proxy/3proxy.log D
logformat "- +_L%t.%. %N.%p %E %U %C:%c %R:%r %O %I %h %T"
rotate 30

# ── who to run as after startup (numeric uid/gid of your user)
setgid 65534
setuid 65534

# ── limits
maxconn 200

# ── users: login:type_password:password
users socksuser:CL:StrongPassHere

# ── authentication: strong = required login/password
auth strong

# ── ACL: allow only a known user from a known subnet
allow socksuser 203.0.113.0/24
deny *

# ── and only then start the service
socks -p1080 -i0.0.0.0 -e198.51.100.7

What’s important to understand here:

  • auth strong requires a login and password. There’s also auth none (no check) and auth iponly (by IP only) — and the first one in combination with -i0.0.0.0 gives an open proxy.
  • allow / deny come after auth and before socks. The line socks is the point where the service starts listening; anything you declare after it will not apply to this service.
  • -i — the address on which we listen; -e — the address we go out to. On a server with multiple IPs, -e decides which address you “appear” from.
  • CL in the users line means the password is in plaintext. For production it’s better to use CR (crypt) — then the config holds a hash: users socksuser:CR:$1$....
Note:

Default ports for 3proxy: SOCKS — 1080, HTTP proxy — 3128, FTP — 21, POP3 — 110, SMTP — 25. If you don’t specify -p in the config, it will start on 1080. This same value is the default for Dante and most images — meaning that’s the first thing they’ll scan.

systemd unit:

# /etc/systemd/system/3proxy.service
[Unit]
Description=3proxy tiny proxy server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/3proxy /etc/3proxy/3proxy.cfg
ExecReload=/bin/kill -SIGUSR1 $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/log/3proxy

[Install]
WantedBy=multi-user.target

SIGUSR1 — the standard signal to reload the config, no restart needed. The ProtectSystem/NoNewPrivileges directives are free and noticeably reduce consequences if a future proxy has a hole.


Firewall: not one line of defense, but three

The proxy rule and the firewall rule solve different tasks, and replacing one with the other is a bad idea.

# nftables: allow only the known subnet on 1080
nft add rule inet filter input tcp dport 1080 ip saddr 203.0.113.0/24 accept
nft add rule inet filter input tcp dport 1080 drop
# ufw — the same thing
ufw allow from 203.0.113.0/24 to any port 1080 proto tcp
ufw deny 1080/tcp
Security:

Why an open proxy is a problem for the server owner. Through your SOCKS5, outsiders’ connections go out from your IP. Spam, brute-forcing other people’s dashboards, scanning, visiting what would attract abuse complaints — in the logs of the affected party will be your address. The hoster receives a complaint and, at best, asks for an explanation; at worst — blocks the machine without warning. Plus the channel and bandwidth cap: the 1080 port without a password is found by mass scanners, and a “free” proxy quickly becomes popular.

The minimal set that should be considered mandatory:

  1. Always authentication, even for a quick “couple of hours” test.
  2. IP-based restriction where the source is known — at the proxy level and in the firewall.
  3. Logs with date, IP and user, and at least weekly — take a look at them.
  4. A nonstandard port as cosmetic overlay on top of items 1–3, not a replacement for them.

Check: five commands and one DNS trap

# 1. Is the port actually listening and by whom?
ss -tlnp | grep 1080

# 2. Simple check without authentication
curl -x socks5h://127.0.0.1:1080 https://ifconfig.co

# 3. With login and password
curl -x socks5h://socksuser:StrongPassHere@vps.example.com:1080 https://ifconfig.co

# 4. The same with separate flags
curl --socks5-hostname vps.example.com:1080 -U socksuser:StrongPassHere https://ifconfig.co

# 5. Verify that the port is closed from the outside (run from another machine)
nc -vz vps.example.com 1080

Now about the trap. curl has two very similar flags, and the difference between them is the difference between “the provider sees your domains” and “does not see them.”


Diagram based on the proxy schemes table from everything.curl.dev

Official curl documentation provides a table without ambiguity: for SOCKS 5 the name resolves on the client, for SOCKS 5h — by the proxy. Accordingly:

  • --socks5 and the socks5:// scheme — DNS queries go out from your machine. The connection to the site goes through the proxy, but who you are opening to is known by your resolver and everyone who sees its traffic.
  • --socks5-hostname and the socks5h:// scheme — curl sends the proxy the domain name itself, and the server resolves it.
Important:

If you need a proxy to bypass DNS filtering or simply not reveal the list of visited domains to the local resolver — use only socks5h:// (or --socks5-hostname). The socks5:// scheme in this scenario is useless: you’ll get the same substituted address and will go through the proxy using that.

The same applies to client configurations: in Firefox you need the option Proxy DNS when using SOCKS v5 enabled; in sing-box and Xray the outgoing type socks is responsible for this field, returning a domain instead of an IP.

Checking where DNS actually went is easiest on the server: run tcpdump -i any port 53 on the proxy and make a single request from the client. If you see the request, the server resolves it; if you don’t, the client resolves it, and you likely forgot the letter h.


What to choose in the end

If the access is needed by you alone and the SSH key is already configured — take ssh -D with autossh. This is the only option where a new listening port does not appear on the server, so there is less new surface for attacks.

If you need the proxy as a permanent service for several people and you already manage system users — Dante, it has the clearest rules model and detailed logs.

If you want to separate your users from system users, have flexible ACL by time and subnet, and also an HTTP proxy on the same daemon — 3proxy.

Common to all three: authentication is mandatory, IP-limited access, logs readable. A proxy you set up “to try in the evening” and forget about will live for years — and all that time it will respond with your IP address.

Related forum topics: WARP-cli on Linux server in proxy mode and Gray IP and CGNAT: five routes to a home server.


Sources

Question:

What do you use and why? I’m especially curious about Dante: has anyone got pam.username working instead of simply username — and was it worth the effort with the PAM stack?