A LIGHtweight, Intuitive, Efficient Rust Implementation for secure asynchronous SOCKS5 proxying.
Alighieri speaks SOCKS5 (RFC 1928/1929) with TCP CONNECT and UDP ASSOCIATE,
configured through a small, Dante-inspired rule language. It is aimed at
operators who want explicit, deny-by-default access control — CIDR, port,
command, protocol, and auth-method filters, Argon2id-hashed credentials, DNS
safety policy, and per-client rate limits — in a single binary that runs as a
console process or a native Windows Service.
New to it? Jump to Quick start, or let the configuration wizard write your first config.
- Features
- Quick start
- Configuration wizard
- Configuration
- Machine-readable management CLI
- Linux service (systemd)
- Windows Service
- Architecture
- Rust API and plugin SDK
- Benchmarks
- Comparison with Dante
- Security considerations
- License
- RFC 1928/1929 SOCKS5 with TCP CONNECT and UDP ASSOCIATE
- Dante-inspired line-oriented configuration (
client/socksrule blocks) - Access control with CIDR selectors, port ranges, command/protocol/method filters
- Username/password authentication with Argon2id userlist hashes
- Deny-by-default rule evaluation (first match wins)
- Structured logs with optional file output and size-based rotation
- DNS policy controls for address-family preference, caching, and unsafe ranges
- Prometheus-style metrics on an optional local HTTP endpoint
- Optional TLS listener for clients that can wrap SOCKS5 in TLS, with automatic Let's Encrypt (ACME) certificates via TLS-ALPN-01
- Per-client abuse controls for connection and auth-failure rates, plus a token-bucket bandwidth throttle (TCP shaped, UDP policed)
- Windows Event Log service lifecycle and startup failure events
- Hot reload for policy, DNS, timeout, auth, and userlist changes
- Configuration wizard — generate or edit a config from a short-lived, loopback-only web UI
- Optional plugin SDK — statically link custom control-plane, TCP, and UDP behavior into a private host binary
- Async — built on Tokio for high-performance I/O
- Portable — first-class on Windows and Linux; macOS and *BSD are not yet officially supported (no CI coverage)
- Secure defaults — no auth required? Think again. The default config still lets you build restrictive rules.
Install the stock server from crates.io with a stable Rust toolchain (1.88 or newer):
cargo install alighieri --locked
alighieri --versionPrebuilt Linux and Windows binaries are also attached to each release. To build from a source checkout:
cargo build --release
./target/release/alighieri doc/alighieri.confOr with Cargo directly:
cargo run --release -- doc/alighieri.confThat starts a no-auth SOCKS5 proxy on 127.0.0.1:1080. From another shell,
route a request through it to confirm it works:
curl --socks5-hostname 127.0.0.1:1080 https://example.comValidate a configuration without starting the server:
cargo run -- --check doc/alighieri.conf
cargo run -- --check --json doc/alighieri.conf
cargo run -- config metadata --jsonStarter templates live under doc/templates. They are meant
as safe wizard/UI seeds and should still be reviewed before deployment.
Generate or edit a configuration through a short-lived local web UI — see Configuration wizard for the full workflow:
cargo run -- config wizard --output alighieri.conf # generate a new file
cargo run -- config wizard --import alighieri.conf # edit an existing fileManage hashed userlist entries:
alighieri user add alice --userlist /etc/alighieri/users
alighieri user list --userlist /etc/alighieri/users
alighieri user verify alice --userlist /etc/alighieri/users
alighieri user delete alice --userlist /etc/alighieri/usersAutomation can discover the versioned local management interface with
alighieri capabilities --json; see
Machine-readable management CLI.
On Linux, install and start Alighieri as a hardened systemd service (details under Linux service (systemd)). From a source checkout:
sudo ./scripts/alighieri.sh # install, or open the management menu if already installedFrom the root of an extracted Linux release archive, the helper automatically uses its bundled, version-matched binary:
sudo ./scripts/alighieri.sh installOn Windows, install and start Alighieri as a native Windows Service:
alighieri service install --config "C:\ProgramData\Alighieri\alighieri.conf"
alighieri service start
alighieri service reloadMulti-arch (linux/amd64, linux/arm64) images are published to the GitHub
Container Registry. Mount a config and publish the port:
docker run --rm -p 1080:1080 \
-v "$(pwd)/alighieri.conf:/etc/alighieri/alighieri.conf:ro" \
ghcr.io/wiresock/alighieri:latestIn that config set internal: 0.0.0.0 port = 1080 so the listener is reachable
from outside the container, and logoutput: stdout so logs reach docker logs.
The image is distroless and runs as a non-root user (uid 65532) with no shell,
so it also works under --read-only. Because of the non-root user, the
bind-mounted config must be readable by it — a host-only chmod 600 config will
fail with a permission error, so make it world-readable or owned by uid 65532.
Use :latest or pin a release with ghcr.io/wiresock/alighieri:X.Y.Z.
Alighieri can generate — or edit — its configuration through a short-lived local web UI, so operators don't have to hand-write the Dante-inspired syntax:
alighieri config wizard # generate ./alighieri.conf
alighieri config wizard --output proxy.conf # choose the output path
alighieri config wizard --import proxy.conf # load an existing file to editThe wizard starts a one-shot HTTP server, prints a URL containing a one-time token, and exits as soon as one configuration is saved. It is configuration generation, not remote administration:
- it binds to loopback only (override the port with
--listen 127.0.0.1:PORT, which is still validated to stay on a loopback address); - the URL carries a random per-run token, and requests without it are refused;
- it never exposes runtime control, credential browsing, or service management.
Three built-in templates seed the form:
- Local, no auth — a loopback listener for apps on the same machine.
- LAN, username/password — a
0.0.0.0listener backed by an Argon2id userlist. - Public SOCKS5-over-TLS (ProxiFyre) — an authenticated public endpoint on TCP 443 with automatic ACME certificates and optional fixed-range UDP relay, intended for ProxiFyre or another TLS-capable SOCKS5 client.
Whatever you choose, the result is validated with the real parser before it is
written, saved atomically, and the previous file (if any) is preserved as
<name>.bak.
The public profile fixes the listener at 0.0.0.0:443, offers only the
username authentication method, enables TCP CONNECT, applies an
authentication-failure rate limit, and keeps private, link-local, loopback, and
reserved destinations blocked. Its generated destination ACL is
to: 0.0.0.0/0, so this ready-made deployment flow is deliberately IPv4-only.
Alighieri supports IPv6 in other configurations; this narrower profile keeps
the server, ACL, and client choices aligned. It obtains and renews a standard
publicly trusted certificate through the existing ACME TLS-ALPN-01 listener.
The wizard does not ask for, generate, display, or store a plaintext password.
Before starting the service:
- create a DNS A record pointing the selected domain directly at the VPS public
IPv4 address and wait for
dig +short proxy.example.comto return that address rather than unrelated proxy or CDN addresses; - if Cloudflare manages the zone, set that A record to DNS only (gray cloud), not the standard Proxied mode (orange cloud). Specialized Cloudflare TCP proxy products are outside this walkthrough;
- do not publish an AAAA record for this hostname unless IPv6 TCP 443 and compatible Alighieri rules are separately configured and tested. An incorrect or unreachable AAAA record can interfere with ACME validation;
- allow inbound TCP 443 in the host firewall and cloud security group, allow outbound TCP 443 for ACME, and ensure no other service occupies TCP 443;
- if UDP ASSOCIATE is enabled, allow its selected inbound UDP range (the default
is
40000-40099); and - create the selected userlist before starting or restarting Alighieri.
TLS-ALPN-01 requires public TCP port 443 but does not require opening HTTP
port 80. The reviewed
public-tls-proxifyre.conf template
shows the same defaults, and the ACME test guide covers
issuance and troubleshooting in detail.
Keep the wizard loopback-only and reach it through one SSH session. From the operator's local computer:
ssh -o ExitOnForwardFailure=yes \
-L 8080:127.0.0.1:8080 \
user@vps-addressInside that SSH session, change to the source checkout or extracted release archive. On a fresh VPS, prepare the supported installation before starting the wizard. From a source checkout:
sudo ./scripts/alighieri.sh install --no-startFrom an extracted Linux release archive, run this instead:
sudo ./scripts/alighieri.sh install --no-startEither preparation command creates the alighieri account, service
directories, binary, and systemd unit without enabling or starting the service.
Existing installations can skip it. Then, inside the same SSH session, run:
sudo alighieri config wizard \
--listen 127.0.0.1:8080 \
--output /etc/alighieri/alighieri.confKeep SSH open and open the printed tokenized
http://127.0.0.1:8080/?token=... URL in the local browser. The tunnel maps
local port 8080 to the VPS loopback listener, so do not open TCP 8080 in the
VPS firewall and do not bind the wizard publicly. If 8080 is occupied, choose
another port in both commands. Do not post or share the tokenized URL.
After the wizard writes the service configuration, create the first user and
make the resulting file service-readable before running the normal installer.
That final run derives the port-443 CAP_NET_BIND_SERVICE capability and starts
the service. The same helper command works from either a source checkout or an
extracted release archive:
sudo alighieri user add proxyuser --userlist /etc/alighieri/users
sudo chown root:alighieri -- /etc/alighieri/users
sudo chmod 640 -- /etc/alighieri/users
sudo alighieri --check --config /etc/alighieri/alighieri.conf
sudo ./scripts/alighieri.sh install --config /etc/alighieri/alighieri.conf
systemctl status alighieri
journalctl -u alighieri -fuser add prompts for the password securely and stores an Argon2id hash.
The public profile requires absolute userlist and ACME-cache paths so the
operator shell and service cannot resolve the same text to different files. On
Linux, its userlist must be a direct file in the installer-managed
/etc/alighieri directory (for example /etc/alighieri/users), which remains
reachable inside the hardened systemd sandbox.
On Windows those defaults are under %ProgramData%\Alighieri; the completion
page provides an elevated-PowerShell bootstrap that hardens this directory,
atomically installs the generated configuration at its canonical service path
with a backup, creates the user, and only then installs and starts the service.
Install ProxiFyre 2.5.0 or later
using the current architecture-matched online *-setup.exe, then use the
ProxiFyre GUI as
the recommended editor. ProxiFyre's first-party installer is unsigned and
Windows may show Unknown publisher; before approving elevation, verify the
download against its matching .sha256 sidecar from the official release.
| Setting | Value |
|---|---|
| Proxy type | SOCKS5 |
| Server | the public-profile domain, such as proxy.example.com |
| Port | 443 |
| Transport | TLS |
| Username and password | the username and password created with alighieri user add |
| TLS server name | the same domain (or ProxiFyre's default from the endpoint hostname) |
| Certificate validation | enabled; keep Allow invalid certificate disabled |
| Certificate pin | not required for a normal publicly trusted production ACME certificate |
| Protocols | TCP, plus UDP only when UDP ASSOCIATE was enabled in Alighieri |
| Destination address family | IPv4 |
Open ProxiFyre from the Start Menu, add a routing rule, and select or enter the Windows application that should use it. Enter the values above, select TLS, keep normal certificate and hostname validation enabled, select TCP and only the conditionally enabled UDP option, and select IPv4. Choose Validate, then Apply & Restart, and confirm that the header reports Running. Use the Logs tab for connection or routing-rule diagnosis. The official configuration reference covers advanced fields.
Advanced/manual app-config.json
For automation, managed deployment, or intentional manual editing, save the
following as app-config.json next to ProxiFyre.exe. Replace the application
name and credentials. Remove UDP from supportedProtocols when UDP ASSOCIATE
was not enabled in the Alighieri wizard. Manual configuration must explicitly
select TLS because the format otherwise defaults to plaintext SOCKS5. The
installed directory normally requires elevation; credentials remain plaintext
in this file, so restrict access to it and restart ProxiFyreService after
manual changes.
{
"logLevel": "Error",
"proxies": [
{
"appNames": ["chrome"],
"socks5ProxyEndpoint": "proxy.example.com:443",
"username": "proxyuser",
"password": "REPLACE_WITH_USER_ADD_PASSWORD",
"socks5Transport": "TLS",
"tlsServerName": "proxy.example.com",
"tlsAllowInvalidCertificate": false,
"supportedProtocols": ["TCP", "UDP"],
"supportedAddressFamilies": ["IPv4"]
}
],
"excludes": []
}The explicit IPv4 destination family matches this profile's
to: 0.0.0.0/0 ACL; the upstream ProxiFyre endpoint separately requires the
domain's A record. Keep normal certificate validation enabled: the production
ACME certificate is publicly trusted, so neither a fingerprint pin nor an
invalid-certificate bypass is appropriate.
The endpoint is an application proxy, not a full IP-level VPN. TLS protects the SOCKS5 control connection, authentication, and relayed TCP CONNECT traffic, but UDP relay datagrams use separate sockets and are not encapsulated in the TLS stream. Applications such as QUIC, voice clients, and games may provide their own UDP payload encryption. Selecting ACME staging is useful for issuance testing, but its certificate is not trusted by normal clients.
--import PATH loads an existing file into the form and, unless --output
overrides it, writes back to the same path. Representable public configurations
with an ACME-backed TLS listener, username-only authentication, and compatible
TCP/UDP rules are recognised as the public profile; their modeled domain,
e-mail, cache, staging, userlist, UDP range, and advertised host are pre-filled
and preserved on regeneration. A LAN username configuration is still imported
as the LAN profile rather than being mistaken for public TLS.
Import remains loss-aware: before saving, the wizard lists every setting it
cannot reproduce, both on the console and in a banner above the form.
Certificate-file/key-file TLS, custom TLS or UDP behavior, metrics, unmodeled
rate limits, custom timeouts or DNS policy, logging, and extra or customised ACL
rules continue to produce warnings. The original is kept as <name>.bak, so a
dropped setting can be restored.
The configuration language is inspired by Dante's sockd.conf but is an
independent, simplified implementation. A minimal permissive example:
# Interface to listen on.
internal: 127.0.0.1 port = 1080
# Address used for outbound connections (0.0.0.0 = OS default).
external: 0.0.0.0
# Offer no-auth and username/password. 'username' requires a userlist.
socksmethod: none username
userlist: /etc/alighieri/users
connecttimeout: 30
handshaketimeout: 10
iotimeout: 0 # 0 = no idle timeout
udptimeout: 60
maxconnections: 1024
logoutput: stdout
logformat: text
dns.prefer: system
dns.tryall: false
# dns.cachettl: 60
# metrics.listen: 127.0.0.1:9090
# tls.certfile: /etc/alighieri/tls/server.crt
# tls.keyfile: /etc/alighieri/tls/server.key
# ratelimit.connectionrate: 60/60
# ratelimit.authfailurerate: 5/300
# ratelimit.concurrentconnections: 10
# ratelimit.byterate: 10MiB/60
# Optionally split policy into separate files.
# include: conf.d/*.conf
# Admit connections from localhost and the LAN.
client pass "localhost" {
from: 127.0.0.1 to: 0.0.0.0/0
}
client pass "lan" {
from: 10.0.0.0/8 to: 0.0.0.0/0
}
# Deny SOCKS access to loopback destinations.
socks block "deny-loopback" {
from: 0.0.0.0/0 to: 127.0.0.0/8
}
# Allow everything else.
socks pass "allow-default" {
from: 0.0.0.0/0 to: 0.0.0.0/0
protocol: tcp udp
command: connect udpassociate
}| Setting | Default | Description |
|---|---|---|
include |
— | Include another config file or final-component glob |
internal |
— (required) | Listening address (IP port = N or IP:PORT) |
external |
0.0.0.0 |
Source address for outbound connections |
proxyprotocol |
— | Trusted upstream CIDR(s) allowed to send a PROXY protocol (v1/v2) header; the real client address then drives rules/limits/logs. Unset disables it |
socksmethod |
none |
Offered auth methods (none, username) |
userlist |
— | Path to username:password-or-hash file |
auth.command |
— | External verifier program; runs per credential (username/password on stdin, exit 0 = allow) instead of the userlist |
auth.cachettl |
300 |
Reuse successful credential checks for this many seconds (0 disables) |
connecttimeout |
30 |
Seconds to wait for outbound connects |
handshaketimeout |
10 |
Seconds to wait for SOCKS greeting/auth/request |
iotimeout |
0 (disabled) |
Idle timeout for established TCP relays (seconds) |
udptimeout |
60 |
Idle timeout for UDP associations (seconds) |
udp.portrange |
— | Bind the client-facing UDP relay port (BND.PORT) within a fixed MIN-MAX range for firewalling; unset uses an ephemeral port |
udp.strictreply |
true |
Require UDP replies from the exact remote host:port contacted; set false to relax to host-only for compatibility (see below) |
udp.advertise |
— | Public host (IP or hostname) advertised as the BND.ADDR in the UDP ASSOCIATE reply, for a proxy reached via NAT; the real relay port is kept. A hostname is resolved per association via the async resolver (bounded by dns.timeout), falling back to the bound relay address if it cannot be resolved — so --check validates only its syntax, not reachability |
maxconnections |
1024 |
Maximum concurrent client TCP connections |
shutdown.draintimeout |
10 |
Seconds shutdown waits for in-flight connections before aborting the rest. 0 severs them immediately (the most aggressive setting — it is not an "unlimited" drain; a shutdown then logs a warning) |
logoutput |
stdout |
One or more of stdout, stderr, file |
logfile |
— | File path used when logoutput includes file |
logformat |
text |
Log encoding: text or json |
logrotate.size |
10MiB |
Rotate active log file above this size |
logrotate.keep |
5 |
Number of rotated log files to retain |
dns.prefer |
system |
DNS address ordering: system, ipv4, or ipv6 |
dns.tryall |
false |
Try every resolved address for TCP CONNECT |
dns.deny |
— | Deny resolved IP categories after DNS lookup |
dns.cachettl |
0 |
Cache domain lookup answers for this many seconds |
dns.timeout |
5 |
Deadline (seconds) for resolving one destination name |
metrics.listen |
— | Optional HTTP metrics endpoint address (loopback unless metrics.allowpublic) |
metrics.allowpublic |
false |
Allow a non-loopback metrics.listen; required because the endpoint is unauthenticated |
tls.certfile |
— | PEM certificate chain for TLS-wrapped client traffic |
tls.keyfile |
— | PEM private key for TLS-wrapped client traffic |
tls.acme.domains |
— | Domains for automatic Let's Encrypt certs (TLS-ALPN-01, needs port 443) |
tls.acme.email |
— | Optional ACME account contact e-mail |
tls.acme.cache |
— | Directory persisting the ACME account and certificates |
tls.acme.staging |
off |
Use Let's Encrypt staging (testing; untrusted certs) |
ratelimit.connectionrate |
— | Per-client TCP accepts as COUNT/WINDOW_SECONDS |
ratelimit.authfailurerate |
— | Per-client auth failures as COUNT/WINDOW_SECONDS |
ratelimit.concurrentconnections |
— | Per-client concurrent accepted TCP connections |
ratelimit.byterate |
— | Per-client bandwidth throttle (both directions) as BYTES/WINDOW_SECONDS: TCP is shaped (slowed), UDP policed (excess dropped) |
When logfile is set, file logging is enabled even if file is omitted from
logoutput. Size suffixes accept bytes or K, KB, KiB, M, MB, MiB,
G, GB, and GiB.
When Alighieri runs behind a TCP load balancer (HAProxy, nginx stream, an
AWS/GCP Network Load Balancer), set proxyprotocol to the balancer's address
range so the original client address is recovered from the PROXY protocol header
it prepends:
proxyprotocol: 10.0.0.0/8 # one or more trusted upstream CIDRsBoth v1 (text) and v2 (binary) are accepted. Only connections from the listed CIDRs are trusted (and must send a header); any other source is rejected, so a client cannot forge its address — keep the listener firewalled to the balancer.
include loads additional configuration files before continuing with the
current file. Relative include paths are resolved from the file that declares
them, and simple wildcards are supported in the final path component:
include: conf.d/*.confIncluded files are processed in sorted path order. Include cycles are rejected and configuration errors report file and line context.
dns.deny accepts private, linklocal, loopback, multicast,
unspecified, documentation, and reserved. For IPv4, reserved covers the
IANA special-purpose ranges the other categories do not — 0.0.0.0/8,
100.64.0.0/10 (CGNAT), 192.0.0.0/24, 192.88.99.0/24 (6to4), 198.18.0.0/15
(benchmarking), and 240.0.0.0/4 (including the broadcast address). For IPv6,
reserved matches :: (unspecified), ::1 (loopback), 2001:db8::/32
(documentation), 2002::/16 (6to4), 64:ff9b::/96 (the NAT64 well-known prefix,
which can reach embedded IPv4 such as 127.0.0.1 via a NAT64 gateway), and
2001::/23 (the IETF protocol-assignments block, including Teredo and ORCHIDv2).
IPv4-in-IPv6 forms of the IPv4 reserved ranges are caught too. Private,
link-local, multicast,
and the TEST-NET documentation ranges have their own categories, so combine
reserved with them (e.g. private linklocal loopback reserved) for broader
coverage. For example:
dns.prefer: ipv4
dns.tryall: true
dns.deny: private linklocal loopback reserved
dns.cachettl: 60DNS deny rules apply to both domain names and IP literals before ACL
evaluation. TCP CONNECT can try later DNS answers when dns.tryall is enabled;
UDP ASSOCIATE uses the first allowed answer. dns.cachettl is disabled by
default; set it to a positive number of seconds to cache domain lookups, or to
0/off to keep every lookup live.
For UDP ASSOCIATE, the relay forwards a reply to the client only from a remote
the client has actually sent to, so an off-path host cannot inject unsolicited
datagrams. By default the match is the exact host:port the client contacted,
which blocks a co-located attacker on the same host but a different port (notably
on shared hosts or loopback). Set udp.strictreply: false to relax the match to
host-only (any source port on a contacted host) for servers that legitimately
answer from a different port (e.g. TFTP) — at the cost of that protection.
On a TLS listener, TLS protects the SOCKS5 control connection, authentication, and relayed TCP CONNECT traffic. UDP relay datagrams travel through separate UDP sockets and are not encapsulated in the TLS stream; any payload confidentiality for UDP comes from the application protocol itself (for example QUIC).
When metrics.listen is set, Alighieri serves Prometheus-style text metrics at
/metrics. The endpoint is unauthenticated and exposes operational counters and
rule labels, so it must be bound to loopback:
metrics.listen: 127.0.0.1:9090Binding it to a non-loopback (or unspecified, e.g. 0.0.0.0) address is refused
at startup unless you explicitly opt in — only do so behind your own network
access controls (a firewall, private network, or an authenticating reverse
proxy):
metrics.listen: 0.0.0.0:9090
metrics.allowpublic: trueThe endpoint reports connection counts, auth failures, SOCKS allow/deny counts,
TCP and UDP relay byte counters, UDP association counters, and ACL rule hits by
scope, verdict, and config source line. Named ACL rule hits are also reported
through alighieri_rule_named_hits_total, which adds the optional rule name as
a label. It also reports rate-limit events.
The per-rule hit counters (alighieri_rule_hits_total and
alighieri_rule_named_hits_total) are best-effort: to keep the
authorisation hot path non-blocking, an increment is dropped if it would
contend with another update or an in-progress scrape, so these series can
slightly undercount under heavy load. The aggregate counters (connections,
allow/deny totals, bytes, rate-limit events) are exact.
Optional per-client abuse controls are keyed by source IP. The connection and
auth-failure rates use fixed windows; byterate is a token-bucket bandwidth
throttle:
ratelimit.connectionrate: 60/60 # 60 accepted TCP connections per minute
ratelimit.authfailurerate: 5/300 # 5 failed auth attempts per 5 minutes
ratelimit.concurrentconnections: 10 # 10 active accepted TCP connections
ratelimit.byterate: 10MiB/60 # bandwidth throttle (both directions) — see belowChanges apply on hot reload: the per-client throttle bucket is re-tuned in place (so a client's existing flows pick up a new rate), and connection/auth-failure accounting updates for new admissions.
ratelimit.byterateis a bandwidth throttle, not a hard cap. TheBYTES/WINDOW_SECONDSvalue is a sustained rate (BYTES / WINDOW) with a burst up toBYTES, metering both directions against one per-client budget. TCP relays are shaped — slowed with read backpressure — and UDP datagrams over the rate are policed (dropped, since delaying real-time traffic is worse), so a sustained flow is throttled smoothly instead of stalling or being cut. For per-destination throttling, asocksrule can add a per-sessionbandwidthlimit.
When both tls.certfile and tls.keyfile are set, Alighieri expects clients to
complete a TLS handshake before sending the SOCKS5 greeting. SOCKS5 clients
must explicitly support TLS or connect through a local TLS wrapper:
tls.certfile: /etc/alighieri/tls/server.crt
tls.keyfile: /etc/alighieri/tls/server.keyInstead of certificate files, Alighieri can obtain and renew certificates automatically from Let's Encrypt (ACME) — no certbot, no cron:
tls.acme.domains: proxy.example.com
tls.acme.email: admin@example.com # optional account contact
tls.acme.cache: /var/lib/alighieri/acme # persists the account + certs
# tls.acme.staging: on # Let's Encrypt staging while testingValidation uses the TLS-ALPN-01 challenge, answered on the TLS listener
itself — so it needs no port 80 and no DNS API, but the listener must be
reachable at each domain on port 443 (set internal to :443, directly or
behind a forwarder). The cache directory persists the ACME account and issued
certificates so they survive restarts without re-requesting (which would hit
Let's Encrypt's rate limits), and certificates renew in the background with no
restart. tls.acme.* is mutually exclusive with tls.certfile/tls.keyfile.
--check rejects a tls.acme.domains entry that a public CA clearly cannot issue
for over TLS-ALPN-01: each must be a multi-label DNS name made of ASCII
letter/digit/hyphen labels (no leading or trailing hyphen). Wildcards
(*.example.com, which would need DNS-01), underscores, IP addresses,
single-label/local names (localhost), and the special-use TLDs .local,
.test, .invalid, .localhost, .example, .internal, .arpa, .onion, and
.alt are rejected. Internationalised names must be supplied as punycode
(xn--…); a single trailing dot (an absolute name) is accepted and normalised
away. This is a fail-fast on clearly-invalid shapes and suffixes — whether the TLD
is actually delegated, an xn-- label is valid IDNA, and the name exists in DNS
and is reachable on port 443 are verified by ACME at issuance.
Because the challenge is validated by an inbound connection, ACME interacts with
the admission gates. With proxyprotocol enabled, any validation connection
that reaches the listener without a trusted PROXY header (for example Let's
Encrypt connecting directly) is rejected by the proxy-protocol gate, so
issuance/renewal fails unless every validation connection is proxied through a
trusted PROXY-protocol upstream doing TCP passthrough (the proxy warns when both
are set). Likewise, don't set
ratelimit.connectionrate/ratelimit.concurrentconnections so tight that the
handful of validation connections are rejected.
For a complete end-to-end walkthrough on a fresh public server — DNS, firewall, running it, watching issuance, and proxying a request through the TLS listener — see doc/acme-tls-test.md.
Rules are evaluated top-to-bottom, first match wins. If no rule matches, the request is denied.
client pass/block { from: CIDR [port = N] to: CIDR [port = N] }— evaluated at connection admission.socks pass/block { from: CIDR [port = N] to: CIDR|HOSTNAME [port = N] [command: ...] [protocol: tcp|udp] [method: none|username] [bandwidth: BYTES/WINDOW_SECONDS] }— evaluated per SOCKS request.
Rules can optionally be named by placing a single token between the verdict and the opening brace. Quoted names are accepted for readability:
socks pass "allow-web" {
to: 0.0.0.0/0 port = 80-443
command: connect
}The matching rule name is included in structured logs and the
alighieri_rule_named_hits_total metric.
Omitted selectors match both IPv4 and IPv6. Explicit IPv4 CIDRs such as
0.0.0.0/0 remain IPv4-only; add ::/0 in a separate rule for explicit
dual-stack matching. to: in a client rule refers to the proxy's own
accepting address; in a socks rule it refers to the request destination.
A socks rule to: can match the requested destination hostname instead of
an IP/CIDR. The hostname is matched before DNS resolution, so you allowlist
the name the client asked for rather than whatever it resolves to:
.example.com— the domain and all subdomains (example.com,api.example.com, …).example.com— that exact host only.
# Allow only GitHub over TLS; everything else is denied by default.
socks pass "github" {
to: .github.com port = 443
command: connect
}Hostname patterns are valid only in a socks rule to: — a from: selector
and a client rule to: stay IP/CIDR-only. An earlier block { to: 10.0.0.0/8 }
still rejects a domain that resolves into a denied range, so deny-by-default
and DNS-rebinding protection are preserved.
Hostname patterns use a strict DNS-label grammar: each label is 1–63 characters
of a-z, 0-9, and - (not starting or ending with a hyphen), matched
case-insensitively, with an optional trailing dot. This is intentionally narrower
than what the proxy accepts as a requested destination — a client may ask for a
name containing an underscore or a non-ASCII (IDN) label, which cannot be written
as an exact to: hostname pattern. Such destinations are still governed by
IP/CIDR rules (matched after resolution) and by deny-by-default.
A socks rule may carry a bandwidth: BYTES/WINDOW_SECONDS limit that
throttles each matching CONNECT relay (a per-session token bucket: sustained
BYTES / WINDOW with a burst up to BYTES). It is enforced like
ratelimit.byterate — the flow is shaped (slowed), not torn down — and a
session is bounded by both its per-client byterate and the matched rule's
limit, whichever is tighter. bandwidth is valid only in a socks rule and
applies to CONNECT; UDP keeps the per-client limit.
# Throttle each bulk-download session to ~5 MiB/s, leave everything else alone.
socks pass "downloads" {
to: .cdn.example.com port = 443
command: connect
bandwidth: 5MiB/1
}One entry per line. Argon2id entries generated by alighieri user add are
stored as Alighieri comment directives so they cannot collide with legacy
plaintext passwords:
# /etc/alighieri/users
# alighieri:user:argon2:616c696365:$argon2id$v=19$m=19456,t=2,p=1$...
# alighieri:user:argon2:626f62:$argon2id$v=19$m=19456,t=2,p=1$...
Manage entries with:
alighieri user add alice --userlist /etc/alighieri/users
alighieri user list --userlist /etc/alighieri/users
alighieri user verify alice --userlist /etc/alighieri/users
alighieri user delete alice --userlist /etc/alighieri/usersThe same operations support versioned JSON responses, password input through stdin, and resolution of the effective include-aware userlist from a configuration. See the management CLI protocol for the complete automation contract and security requirements.
Plaintext username:password entries remain supported for compatibility, but
hashed entries are preferred. Restrict the file to administrators and the
account running Alighieri. A manual single-user deployment can use owner-only
mode (0600); the managed Linux service requires root:alighieri ownership
and mode 0640 so the unprivileged service can read but not rewrite credentials.
On Linux, user add --config automatically creates a missing userlist only when
the config is a regular file owned by root:alighieri with mode 0640 and no
extended access ACL. Custom service groups and other ownership or ACL layouts
must pre-create the userlist with the required metadata; Linux access ACLs on
existing userlists are preserved across updates.
user add and user delete update the file under a lock, replace it
atomically, and keep the previous contents beside it as <userlist>.bak when
the file already existed.
Alighieri loads the userlist once when the proxy starts. After adding, updating,
or removing users, send SIGHUP on Unix to reload the running proxy process, or
run alighieri service reload on Windows for the change to take effect.
Because SOCKS clients open a new proxy connection per stream and Argon2id
verification is deliberately expensive, successful credential checks are
cached in memory for auth.cachettl seconds (default 300). The cache stores
a keyed tag derived with a per-process random salt — never the password — only
caches successes (failed attempts always pay the full hashing cost), and is
cleared whenever the configuration or userlist is reloaded. Set
auth.cachettl: 0 to verify every handshake at full cost.
Set auth.command to delegate username/password verification to an external
program instead of the userlist — useful for LDAP, OIDC, PAM, or a corporate
auth service:
socksmethod: username
auth.command: /usr/local/bin/verify-userFor each attempt Alighieri runs the program and writes two newline-terminated
lines to its stdin — the username, then the password (never on the command
line or environment, which can leak). Exit status 0 allows the connection;
anything else, or a timeout, denies it. The script should read with read -r,
and credentials containing a line feed (LF), carriage return (CR), or NUL byte
are rejected to keep the
framing unambiguous. Successful results are cached exactly like the userlist
(auth.cachettl), and with auth.command set the username method no longer
requires a userlist. To bound resource use, at most 64 verifier processes run
concurrently; under a heavier burst the excess waits for a slot and is denied
(treated as a timeout) if it cannot start within the handshake timeout.
The value is split on whitespace into the program path and its arguments, with
no quoting — so a program path that itself contains spaces (for example
C:\Program Files\...) cannot be expressed directly. Point auth.command at a
space-free wrapper script (the usual pattern anyway, since the verifier
typically shells out to ldapsearch, curl, pamtester, and the like) and put
the real path inside it. Windows Service mode additionally requires the first
value to be an explicit filesystem path, such as
C:\Alighieri\verify-user.cmd; it rejects a bare program name resolved through
PATH so service logging can prove that its rotation files do not overlap the
authentication helper.
On Unix, send SIGHUP to reload the configuration without restarting the process:
kill -HUP <alighieri-pid>On Windows, ask the installed service to reload through the Service Control Manager:
alighieri service reloadThe new configuration is validated before it replaces the active runtime policy. New client connections use the reloaded ACLs, DNS policy, authentication settings, userlist, timeout values, and rate-limit settings. Existing connections continue with the configuration they accepted under.
Listener addresses, maxconnections, metrics listener settings, TLS listener
settings, and logging sinks are process-level resources; changes to those
settings are reported in the logs and require a restart.
Tools and local setup UIs can inspect the same distinction with
alighieri config metadata --json.
Alighieri provides a versioned local CLI/stdio contract for administration tools, including a future cross-platform manager operating through an existing authenticated SSH connection. Discover the exact features supported by a binary before using them:
alighieri capabilities --json
alighieri user list --config /etc/alighieri/alighieri.conf --jsonUser add, delete, list, and verify operations can return one JSON envelope with stable protocol-1 error codes. Add and verify accept a single bounded password record on stdin, so the secret never needs to appear in argv, an environment variable, or JSON:
trusted-password-provider | \
sudo -n -- /usr/local/bin/alighieri user add alice \
--config /etc/alighieri/alighieri.conf \
--password-stdin --jsontrusted-password-provider is a placeholder for a protected process that
writes exactly one password record without logging it. Production clients
should write their protected buffer directly to the SSH channel and clear it
promptly; shell variables can leave additional copies in memory.
When delegating this command through sudo, keep the complete transitive
configuration and include search space administrator-controlled, including
directories that can supply wildcard matches, or use a root-owned wrapper fixed
to the intended userlist.
This interface does not open a management port or install a resident agent. SSH authentication and host-key verification, operating-system privileges, service control, and reloads remain the caller's responsibility. The complete management CLI protocol defines compatibility negotiation, command and result schemas, stdin framing, error identifiers, idempotent deletion, SSH/Rust client flows, and the security boundary.
On Linux, scripts/alighieri.sh manages the whole
lifecycle as a hardened systemd service — install, upgrade, uninstall, and
status. Mutating commands require flock; automatic legacy-unit migration also
requires the version-matched alighieri-installer-fs companion and fails before
detaching the live unit when the companion is unavailable or incompatible.
From a Linux release archive: extract the archive and run the bundled, version-matched helper from its root. It finds the archive's binary automatically, so no Rust toolchain is required:
sudo ./scripts/alighieri.sh installThe Linux archives include scripts/alighieri.sh, doc/alighieri.conf, and the
narrowly path-scoped alighieri-installer-fs companion used for atomic systemd
unit transactions. The lifecycle script stages that companion privately from
the same archive; it never executes the service payload selected by --binary
as root for those filesystem operations. The lifecycle script selects the
archive-root alighieri binary, so the installed binary, lifecycle logic,
companion, and initial config all come from the same release. It never clones a
mutable repository branch or downloads a replacement helper. An explicit
--binary PATH remains available for an intentionally selected service
artifact and takes precedence over auto-detection.
To upgrade an existing Linux service, extract the newer archive, change into its root, and run:
sudo ./scripts/alighieri.sh upgrade
sudo ./scripts/alighieri.sh statusThe helper validates the new binary against the live configuration before it
changes anything, then restarts the service. The status output includes the
installed version so the result is easy to verify. When status is invoked with
sudo, its version probe runs as the confined alighieri service identity and
reports output only when --version succeeds.
From a checkout: run it directly — it uses an existing target/release
service build if present, otherwise builds one, or takes a service binary
extracted from a release.
Install and reconfigure operations, plus upgrades that migrate a legacy systemd
unit, still build the version-matched alighieri-installer-fs companion from
that checkout, so they require a Rust toolchain even with --binary. Use a
complete extracted Linux release archive for a toolchain-free installation:
sudo ./scripts/alighieri.sh # install, or manage if already installed
sudo ./scripts/alighieri.sh install --binary ./alighieri # prebuilt service binary
sudo ./scripts/alighieri.sh install --no-start # prepare unit/files without first start
sudo ./scripts/alighieri.sh upgrade # use bundled binary or rebuild, then restart
sudo ./scripts/alighieri.sh status # show version, binary, service, and config state
sudo ./scripts/alighieri.sh uninstall # remove the service and binary
sudo ./scripts/alighieri.sh uninstall --purge-all # also remove config, logs, and the userRun with no command on an already-installed host to get an interactive menu
(status, logs, upgrade, reconfigure, uninstall). upgrade pre-flights the new
binary with --check against the live config, replaces it, and restarts while
preserving the config. Exact, unmodified units generated by Alighieri 0.1.0
through 0.4.0 are migrated transactionally to the current hardened unit during
that upgrade. Mutating helper invocations are serialized, and the migration
journal is recovered automatically after an interrupted installer process. A
successful legacy migration retains the exact old unit at
/etc/systemd/system/alighieri.service.pre-migration; keep it through release
verification, then remove it when rollback is no longer needed. Customized units
and drop-ins are never silently overwritten; if
their effective service sandbox is incompatible, the helper stops with commands
to inspect the customization. install (re-run) intentionally rewrites the
managed unit and re-applies permissions to installer-managed files. A plain
reconfigure preserves the config path already recorded in the unit; pass
install --config /absolute/path to select a different one explicitly. A
custom config must already be owned by root:alighieri with mode 0640, and
every directory in its physical path must be root-owned and not group- or
world-writable. On a fresh host, run the default install --no-start preparation
first so the service account and group exist, harden the custom file, and then
rerun install --config /absolute/path. (The older
scripts/install-linux.sh remains as a thin
compatibility shim that forwards to alighieri.sh.)
From an extracted release, keep the archive and run upgrade or install
directly from its root; the menu and command-line actions automatically reuse
the archive's bundled binary. From a source checkout, those actions reuse an
existing release build or build one with Cargo. Status, logs, and uninstall need
no source artifact.
The installer puts the binary at /usr/local/bin/alighieri, creates a
dedicated unprivileged alighieri system user, installs a default config to
/etc/alighieri/alighieri.conf (kept if it already exists; either way set to
root:alighieri mode 640, readable only by the service user), and writes
/etc/systemd/system/alighieri.service before enabling and starting it. With
install --no-start, it finishes after writing and daemon-reloading the unit;
it neither enables nor starts a fresh service, which lets you create a required
userlist before the first authenticated launch.
Before rewriting or restarting the managed unit, the installer validates the
configuration inside a transient sandbox with the same service account,
working directory, and path-hiding protections (including ProtectHome,
PrivateTmp, and PrivateDevices). When starting an authenticated deployment,
it also requires the effective userlist to be a physical file below a
root-controlled directory chain, owned by root:alighieri with mode 0640, and
fully parses it there. This rejects credentials the service could rewrite,
custom paths the root account can read but the service cannot traverse or see,
and malformed entries; --no-start exempts only a genuinely missing userlist
for first-time credential bootstrap. The final install enforces the complete
policy before starting the service.
The unit runs as the alighieri user with NoNewPrivileges,
ProtectSystem=strict, ProtectHome, PrivateTmp, a capability set restricted
to at most CAP_NET_BIND_SERVICE (see below), and a @system-service syscall
filter. The default config logs to stdout, which systemd captures into the
journal:
systemctl status alighieri # service state
journalctl -u alighieri -f # follow logs
systemctl reload alighieri # SIGHUP hot reload after editing the config
systemctl stop alighieriEditing /etc/alighieri/alighieri.conf and running systemctl reload alighieri
applies policy, DNS, auth, and timeout changes to new connections; existing
connections keep running under the configuration they were accepted with (see
Hot reload). Binding a port below 1024 — including the :443
that ACME's TLS-ALPN-01 challenge needs — requires no hand-editing: when it
generates the unit, the installer grants CAP_NET_BIND_SERVICE if the config
uses a privileged internal: port or tls.acme.* (following include: files
too), and otherwise leaves the capability set empty. It also provisions a
writable StateDirectory= (/var/lib/alighieri) for the ACME certificate
cache, so tls.acme.cache: /var/lib/alighieri/acme works under
ProtectSystem=strict. Because the capability is baked into the unit at install
time, after switching to a privileged port or enabling ACME in an existing
deployment re-run sudo ./scripts/alighieri.sh install from either a source
checkout or the extracted release archive to regenerate the unit — a plain
systemctl reload keeps the old capability set.
Alighieri enlarges its relay sockets' kernel buffers for high-throughput UDP
relaying (e.g. tunnelling a VPN) — it requests 4 MiB each. On Linux the
kernel clamps SO_RCVBUF/SO_SNDBUF to net.core.rmem_max /
net.core.wmem_max (commonly only ~208 KiB by default), so to actually get the
larger buffers (fewer dropped datagrams under bursts) raise those limits. Linux
also stores roughly double the requested value (kernel bookkeeping), so the
4 MiB request needs a limit of ~8 MiB:
sudo sysctl -w net.core.rmem_max=8388608 net.core.wmem_max=8388608
# persist:
echo 'net.core.rmem_max=8388608
net.core.wmem_max=8388608' | sudo tee /etc/sysctl.d/90-alighieri.confThe same alighieri.exe supports interactive console mode and Windows Service
mode. Service support is compiled only on Windows and is isolated under the
platform-specific module tree.
Recommended deployment path:
C:\ProgramData\Alighieri\
├── alighieri.conf
└── logs\
└── alighieri.log
Install, start, inspect, stop, and remove the service from an elevated Administrator shell:
alighieri service install --config "C:\ProgramData\Alighieri\alighieri.conf"
alighieri service start
alighieri service status
alighieri service reload
alighieri service stop
alighieri service uninstallThe installed service uses:
- service name:
Alighieri - display name:
Alighieri SOCKS5 Proxy Server - startup type: automatic
- account:
NT AUTHORITY\LocalService - default log file:
C:\ProgramData\Alighieri\logs\alighieri.log(a configuredlogfileis honored; ordinary relative paths resolve against the active configuration file's directory, and that location must be writable byLocalService) - Event Log source:
Alighieriin the Windows Application log - recovery: restart on crash (after 5s, then 30s, then 60s; failure count resets
after an hour), the Windows equivalent of systemd's
Restart=on-failure - accepts
STOPand systemSHUTDOWN, so an OS restart stops it cleanly: it stops accepting, drains in-flight connections for up toshutdown.draintimeoutseconds (default 10; cutting any that remain), then flushes logs and reports a cleanStoppedstatus
The installer validates the configuration before creating the service and the
start and reload commands validate the installed configuration before asking
the Service Control Manager to act on the service. Credentials stay in the
configured userlist file; the service command line stores only the
configuration file path. Service file logs use the configured logfile when
present and otherwise use the default ProgramData path. An ordinary relative
configured path resolves against the active configuration file's directory,
which must be writable by LocalService. Service logs rotate with the same
logrotate.size, logrotate.keep, and logformat settings as console file
logging.
Service install registers the Alighieri Event Log source. Service mode writes
startup, stop, reload-request, and startup/runtime failure events to the Windows
Application log. File logging remains the detailed operational log.
For an elevated manual smoke test covering install, start, reload, Event Log
inspection, stop, and uninstall, see
doc/windows-service.md and
doc/windows-service-smoke-test.ps1.
src/
├── lib.rs # Module declarations and crate documentation
├── main.rs # CLI entry point, logging setup, signal handling
├── errors.rs # Crate-wide `Error` enum with SOCKS5 reply mapping
├── net.rs # CIDR and port-range primitives
├── config.rs # Dante-inspired parser and `Config` struct
├── acl.rs # Rule evaluation engine (first-match-wins, deny-by-default)
├── auth.rs # Username/password database with constant-time verification
├── socks5.rs # RFC 1928/1929 wire-format helpers
├── connection.rs # Per-client SOCKS5 state machine
├── plugin.rs # Optional, feature-gated plugin SDK
├── platform/ # Platform-specific integrations such as Windows Service
├── runtime.rs # Shared console/service runtime helpers
├── server.rs # Accept loop with semaphore-based connection limit
└── relay.rs # TCP bidirectional relay + UDP associate relay
Version 0.5 supports the configuration model and parser, crate-wide
Error/Result, Server lifecycle, runtime shutdown/reload drivers, and the
feature-gated plugin SDK as its public Rust API. Patch releases in the 0.5.x
line preserve compatibility for those documented interfaces; an intentional
breaking API or SDK change requires 0.6.0. Engine, wire-protocol, relay,
platform, and CLI-support internals are not part of that compatibility
contract. See the complete API documentation.
The plugins feature adds an in-process SDK:
[dependencies]
alighieri = { version = "0.5", features = ["plugins"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "time"] }Plugins are Rust code statically linked into a custom host binary. The stock
alighieri binary does not discover or dynamically load plugin files. A
minimal control-plane plugin can tag admitted flows:
use alighieri::plugin::{async_trait, FlowCtx, FlowDecision, Plugin};
struct Audit;
#[async_trait]
impl Plugin for Audit {
fn name(&self) -> &str {
"audit"
}
async fn on_flow(&self, ctx: &mut FlowCtx<'_>) -> FlowDecision {
ctx.tags.insert("observed");
FlowDecision::Continue
}
}Register the compiled-in set after binding and before running the server:
use std::{path::PathBuf, sync::Arc};
use alighieri::{
config::Config,
errors::Result,
plugin::PluginHost,
runtime::{
reload_signal_channel, run_bound_server_reloading_until_shutdown,
shutdown_signal,
},
server::Server,
};
// `Audit` is the plugin defined in the preceding example.
#[tokio::main]
async fn main() -> Result<()> {
let config_path = PathBuf::from("alighieri.conf");
let config = Config::load(&config_path)?;
let plugins = PluginHost::new(vec![Arc::new(Audit)]);
let server = Server::bind(config).await?.with_plugins(plugins);
run_bound_server_reloading_until_shutdown(
server,
config_path,
shutdown_signal(),
reload_signal_channel(),
)
.await
}The dedicated plugin SDK guide covers TCP interception, UDP datagram verdicts, association takeover, composition, and testing. A custom host is a modified/combined Alighieri build, so review the AGPL and commercial licensing options before distributing or deploying one.
An end-to-end load generator lives in examples/loadgen.rs; it measures relay
throughput, connection-setup rate (with and without authentication), and UDP
associate packet rates against a self-hosted proxy (or, with --proxy, a
separately started proxy on the same host):
cargo run --release --example loadgen -- throughput --connections 8See doc/benchmarks.md for scenario details,
methodology, and recorded baselines.
Dante (sockd, Inferno Nettverk) is the
long-standing C reference SOCKS server: full SOCKS4/5 including the BIND command
and GSSAPI, a client-side "socksify" preload library, and broad Unix
portability, hardened since the late 1990s. Alighieri borrows Dante's
configuration model but trades breadth (SOCKS4, BIND, GSSAPI, the client
library, exotic Unixes) for memory safety, first-class Windows support,
SOCKS-over-TLS, and built-in observability.
Dante capabilities below are drawn from its documented feature set and vary by version; verify against the version you would deploy.
Platforms & architecture
| Alighieri | Dante | |
|---|---|---|
| Linux | first-class (CI + systemd manager) | yes |
| Windows | native Service + Event Log | not supported |
| macOS / *BSD / Solaris / AIX | not officially supported (no CI coverage) | broadly supported |
| Language | Rust (memory-safe) | C |
| Process model | async, single process (Tokio tasks) | multi-process (preforked) / threaded |
Protocol & commands
| Alighieri | Dante | |
|---|---|---|
| SOCKS5 (RFC 1928/1929) | yes | yes |
| SOCKS4 / 4a | no | yes |
| CONNECT | yes | yes |
| UDP ASSOCIATE | yes — dual-stack, IPv4-mapped handling, configurable udp.portrange |
yes |
| BIND (reverse connect, e.g. active FTP) | no — RFC 1928 reply 0x07 (command not supported) |
yes |
| IPv4 / IPv6 | yes / yes (dual-stack listeners) | yes / yes |
| Client socksify library (LD_PRELOAD) | no (server only) | yes (socksify / libsocks) |
Authentication
| Alighieri | Dante | |
|---|---|---|
| None | yes | yes |
| Username/password (RFC 1929) | yes — Argon2id-hashed userlist + verified-credential cache | yes |
| GSSAPI / Kerberos | no | yes |
| PAM / system auth | no | yes |
Access control & configuration
| Alighieri | Dante | |
|---|---|---|
| Model | Dante-inspired client / socks rules, deny-by-default |
the original sockd.conf |
| Selectors | CIDR, port, command, protocol, auth-method, destination hostname | CIDR, port, command, protocol, user, hostname/domain |
| Hostname / domain rules | yes (socks to: patterns) |
yes |
| libwrap / TCP wrappers | no | yes |
Named rules + include |
yes | no (single config file) |
| Destination redirect / rewrite | no | yes |
| DNS policy (family preference, all-address fallback, deny categories, caching) | rich, built-in | basic |
Operations & observability
| Alighieri | Dante | |
|---|---|---|
| Log formats | text or JSON, size-rotation, non-blocking writer | syslog + file |
| Prometheus metrics | built-in endpoint | no |
| Hot reload | SIGHUP (Unix) + Windows SCM | SIGHUP |
| Service tooling | systemd install/upgrade/uninstall script; Windows Service | distro init/systemd packaging |
| Config wizard / validation | loopback wizard, --check, --check --json |
startup config check |
| Bandwidth / abuse limits | connection-rate, auth-failure-rate, concurrency, token-bucket throttle (per-client byterate + per-rule bandwidth) |
session limits (+ bandwidth in some builds) |
Security & project
| Alighieri | Dante | |
|---|---|---|
| Memory safety | Rust | C |
| SOCKS-over-TLS listener | yes (rustls, TLS 1.2/1.3) | no (uses GSSAPI for confidentiality/integrity) |
| Credential storage | Argon2id hashes | system / crypt / PAM |
| License | AGPL-3.0-or-later + commercial | BSD-style (permissive) |
| Maturity | developing (current release v0.5.3) | decades in production |
Which to choose
- Dante if you need SOCKS4, the BIND command (active FTP / callbacks), GSSAPI/Kerberos or PAM auth, the client-side socksify library, run on BSD/Solaris/AIX, or want a permissive license and a decades-proven codebase.
- Alighieri if you want SOCKS5 on Linux and Windows from one memory-safe codebase, SOCKS-over-TLS, Prometheus/JSON observability, Argon2id-hashed credentials, fixed UDP relay port ranges for firewalling, and a modern ops story (wizard, systemd installer, hot reload on both OSes) — and do not need SOCKS4/BIND/GSSAPI.
Closing the remaining gaps (and going further) is tracked in
doc/roadmap.md.
- Deny by default. No
clientorsocksrules means everything is denied. - Argon2id userlist hashes avoid storing plaintext passwords for username/password authentication.
- UDP relay source validation drops datagrams from IPs other than the authenticated client.
- Fragmented UDP datagrams are dropped (
FRAG != 0) to avoid amplification and evasion risks. - No BIND support. Active-mode FTP's BIND command is not implemented; it is a common source of misconfiguration and rarely needed today.
Alighieri is dual-licensed — AGPL-3.0-or-later by default, with a commercial option available by agreement:
- Open source (default): the GNU Affero General Public License v3.0 or
later (
AGPL-3.0-or-later). Unless you have signed a commercial agreement, your use is under the AGPL. Practically, its network-use clause (section 13) means that if you run a modified version of Alighieri as a network service, you must make the corresponding source of your modified version available to that service's users. - Commercial (by agreement): if the AGPL's copyleft and source-disclosure obligations don't fit your use — e.g. embedding Alighieri in a proprietary product, or running a modified version as a service without publishing your changes — a commercial license from WireSock is available, by separate signed agreement, that lifts them.
See LICENSING.md for which license applies to you and how to
obtain a commercial one. Copyright © 2026 WireSock; commercial inquiries:
licensing@wiresock.net.