Context
This is a greenfield Ansible project to automate the provisioning of a two-server chained proxy setup. The two servers are:
- Relay server (中转机): Runs shadowsocks-rust as an encrypted proxy. Acts as the transit node — general traffic from clients exits here. Also serves as the first hop for chained traffic to the landing server.
- Landing server (落地机): Runs Trojan as an encrypted proxy disguised as HTTPS traffic. Provides a local IP exit for geo-sensitive services (AI platforms, streaming). Also supports direct client connections.
The chaining is done on the client side using Surge's underlying-proxy feature:
- Client → Relay (Shadowsocks) → Landing (Trojan) → Internet (for chained traffic)
- Client → Landing (Trojan) → Internet (for direct traffic)
- Client → Relay (Shadowsocks) → Internet (for relay-only traffic)
The Ansible playbook provisions the server-side daemons only. Client-side Surge configuration is documented but not deployed by Ansible.
Goals / Non-Goals
Goals:
- Fully automated, idempotent server provisioning via Ansible
- shadowsocks-rust deployed on relay server as a systemd service
- Trojan deployed on landing server as a systemd service with TLS (Let's Encrypt)
- Basic server hardening (firewall, SSH key-only, fail2ban)
- Document client-side Surge configuration with underlying-proxy chain and routing rules
- Landing server supports both chained (via relay) and direct connections
Non-Goals:
- Client-side Surge deployment or configuration management (document only)
- Web UI or dashboard for proxy management
- Automatic failover or high availability
- VPN tunneling (this is a proxy-only setup)
- Traffic logging or analytics
Decisions
1. Protocol pairing: Shadowsocks on relay, Trojan on landing
- Relay (SS): Shadowsocks is fast and lightweight, ideal for the transit hop. shadowsocks-rust provides best performance with AEAD ciphers.
- Landing (Trojan): Trojan disguises traffic as normal HTTPS, beneficial for the endpoint that handles geo-sensitive services. Requires a domain and TLS cert.
Why this pairing over the reverse: The relay is a transit node where speed matters most. The landing server faces service providers that may inspect traffic patterns — Trojan's HTTPS disguise is more valuable here.
2. Ansible project structure: roles-based layout
inventory/
hosts.yml
group_vars/
all.yml
relay.yml
landing.yml
roles/
base/ # Common system setup
shadowsocks/ # shadowsocks-rust installation and config
trojan/ # Trojan installation and config
site.yml # Main playbook
Why over a flat playbook: Roles enable reuse, testing, and clear separation. Each role is independently testable.
3. shadowsocks-rust deployment
- Download pre-built binary from GitHub releases (configurable version)
- Configure via JSON config file generated from Jinja2 template
- Run as systemd service under dedicated
ssserver user
- Use AEAD cipher (e.g.,
aes-256-gcm or chacha20-ietf-poly1305)
4. Trojan deployment with TLS
- Install Trojan (trojan-go or trojan-gfw) from release binary
- TLS certificate via Let's Encrypt (certbot) with auto-renewal
- Requires a domain name pointing to the landing server
- Trojan listens on port 443, with a fallback web server for non-Trojan traffic (camouflage)
- Run as systemd service under dedicated user
5. Client-side Surge configuration (documented, not deployed)
The project includes a reference Surge client config showing:
[Proxy]
Relay-SS = ss, relay_ip, ss_port, encrypt-method=aes-256-gcm, password=xxx
Landing-Trojan = trojan, landing_domain, 443, password=xxx
Landing-Chain = trojan, landing_domain, 443, password=xxx, underlying-proxy=Relay-SS
[Proxy Group]
Chain = select, Landing-Chain
Direct-Landing = select, Landing-Trojan
[Rule]
# Sukka's rulesets (https://github.com/SukkaW/Surge)
# DOMAIN-SET and non_ip rules MUST come before ip rules
# AI services - through chain (relay → landing, exit from landing IP)
DOMAIN-SET,https://ruleset.skk.moe/List/domainset/ai.conf,Chain
RULE-SET,https://ruleset.skk.moe/List/non_ip/ai.conf,Chain
# Streaming - through chain
RULE-SET,https://ruleset.skk.moe/List/non_ip/stream_us.conf,Chain
# IP-based rules last
RULE-SET,https://ruleset.skk.moe/List/ip/stream_us.conf,Chain
# Default - relay only
FINAL,Relay-SS
All domain matching is delegated to Sukka's externally maintained rulesets — no self-maintained domain lists. Rulesets auto-update every 12 hours.
The underlying-proxy on Landing-Chain means Surge first connects to the relay via SS, then through that connection reaches the landing server via Trojan.
6. Server hardening baseline
The base role handles:
- UFW firewall with default deny, allowing only SSH + service-specific ports
- SSH hardened (key-only auth, no root login)
- fail2ban for SSH brute-force protection
- Automatic security updates (unattended-upgrades)
Risks / Trade-offs
- [Single point of failure on relay] → If the relay goes down, chained traffic stops. Mitigation: direct landing connection remains available; add monitoring as future enhancement.
- [TLS certificate for Trojan] → Landing server requires a domain name and Let's Encrypt cert. Mitigation: automate cert provisioning with certbot in the Ansible role.
- [Cert renewal] → Let's Encrypt certs expire every 90 days. Mitigation: certbot auto-renewal via cron/systemd timer, with a handler to reload Trojan.
- [Rule maintenance] → Domain lists change over time. Mitigation: delegated entirely to Sukka's rulesets (https://github.com/SukkaW/Surge), which auto-update every 12 hours — no local maintenance needed.
- [Security of proxy credentials] → Passwords stored in Ansible vars. Mitigation: use Ansible Vault for all secrets; restrict deployed config file permissions.