瀏覽代碼

feat: ansible chained proxy setup with shadowsocks + trojan

Provision two servers as a chained proxy pair:
- Relay (中转机): shadowsocks-rust for fast encrypted transit
- Landing (落地机): trojan-go with Let's Encrypt TLS for geo-sensitive services

Includes base server hardening (SSH, UFW, fail2ban), reference Surge client
config with Sukka's rulesets, and Ansible Vault for secret management.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
kotoyuuko 3 周之前
當前提交
7d72410741
共有 35 個文件被更改,包括 1264 次插入0 次删除
  1. 105 0
      README.md
  2. 10 0
      ansible.cfg
  3. 47 0
      docs/surge-client.conf
  4. 11 0
      group_vars/all.yml
  5. 12 0
      group_vars/landing.yml
  6. 6 0
      group_vars/relay.yml
  7. 7 0
      group_vars/vault.yml.example
  8. 12 0
      inventory/hosts.yml
  9. 2 0
      openspec/changes/archive/2026-04-22-chain-proxy-setup/.openspec.yaml
  10. 124 0
      openspec/changes/archive/2026-04-22-chain-proxy-setup/design.md
  11. 33 0
      openspec/changes/archive/2026-04-22-chain-proxy-setup/proposal.md
  12. 48 0
      openspec/changes/archive/2026-04-22-chain-proxy-setup/specs/proxy-rules/spec.md
  13. 51 0
      openspec/changes/archive/2026-04-22-chain-proxy-setup/specs/server-base/spec.md
  14. 51 0
      openspec/changes/archive/2026-04-22-chain-proxy-setup/specs/shadowsocks-relay/spec.md
  15. 67 0
      openspec/changes/archive/2026-04-22-chain-proxy-setup/specs/trojan-landing/spec.md
  16. 48 0
      openspec/changes/archive/2026-04-22-chain-proxy-setup/tasks.md
  17. 20 0
      openspec/config.yaml
  18. 48 0
      openspec/specs/proxy-rules/spec.md
  19. 51 0
      openspec/specs/server-base/spec.md
  20. 51 0
      openspec/specs/shadowsocks-relay/spec.md
  21. 67 0
      openspec/specs/trojan-landing/spec.md
  22. 10 0
      roles/base/handlers/main.yml
  23. 70 0
      roles/base/tasks/main.yml
  24. 14 0
      roles/base/templates/sshd_config.j2
  25. 7 0
      roles/shadowsocks/defaults/main.yml
  26. 6 0
      roles/shadowsocks/handlers/main.yml
  27. 74 0
      roles/shadowsocks/tasks/main.yml
  28. 15 0
      roles/shadowsocks/templates/shadowsocks.service.j2
  29. 8 0
      roles/shadowsocks/templates/ss-config.json.j2
  30. 7 0
      roles/trojan/defaults/main.yml
  31. 6 0
      roles/trojan/handlers/main.yml
  32. 126 0
      roles/trojan/tasks/main.yml
  33. 18 0
      roles/trojan/templates/trojan-config.json.j2
  34. 17 0
      roles/trojan/templates/trojan.service.j2
  35. 15 0
      site.yml

+ 105 - 0
README.md

@@ -0,0 +1,105 @@
+# Ansible Proxy Chain
+
+Ansible playbook for provisioning a two-server chained proxy setup:
+
+- **Relay server (中转机)**: Shadowsocks-rust — handles general traffic
+- **Landing server (落地机)**: Trojan-Go — handles AI/streaming services requiring local IP
+
+Client-side Surge uses `underlying-proxy` to chain connections:
+```
+Client → Relay (SS) → Landing (Trojan) → Internet   # chained
+Client → Landing (Trojan) → Internet                 # direct
+Client → Relay (SS) → Internet                       # relay only
+```
+
+## Prerequisites
+
+- Two servers running Ubuntu/Debian
+- A domain name pointing to the landing server (for Trojan TLS)
+- Ansible 2.12+ on your local machine
+- `community.general` Ansible collection: `ansible-galaxy collection install community.general`
+
+## Setup
+
+### 1. Configure inventory
+
+Edit `inventory/hosts.yml` with your server IPs:
+
+```yaml
+all:
+  children:
+    relay:
+      hosts:
+        relay-server:
+          ansible_host: "1.2.3.4"
+    landing:
+      hosts:
+        landing-server:
+          ansible_host: "5.6.7.8"
+```
+
+### 2. Configure secrets
+
+```bash
+cp group_vars/vault.yml.example group_vars/vault.yml
+# Edit vault.yml with your passwords
+ansible-vault encrypt group_vars/vault.yml
+```
+
+### 3. Configure variables
+
+Edit `group_vars/relay.yml`:
+- `ss_port`: Shadowsocks listen port (default: 8388)
+- `ss_cipher`: Encryption method (default: aes-256-gcm)
+
+Edit `group_vars/landing.yml`:
+- `trojan_domain`: Your domain name
+- `certbot_email`: Email for Let's Encrypt notifications
+
+### 4. Run the playbook
+
+```bash
+ansible-playbook site.yml --ask-vault-pass
+```
+
+## Client Configuration
+
+See `docs/surge-client.conf` for a reference Surge client configuration with:
+- Proxy definitions (Relay-SS, Landing-Trojan, Landing-Chain)
+- Routing rules using [Sukka's rulesets](https://github.com/SukkaW/Surge)
+- AI and streaming traffic → chained through landing server
+- Default traffic → relay server
+
+## Project Structure
+
+```
+├── ansible.cfg
+├── inventory/
+│   └── hosts.yml
+├── group_vars/
+│   ├── all.yml
+│   ├── relay.yml
+│   ├── landing.yml
+│   └── vault.yml.example
+├── roles/
+│   ├── base/           # SSH hardening, UFW, fail2ban
+│   ├── shadowsocks/    # shadowsocks-rust (relay)
+│   └── trojan/         # trojan-go + certbot (landing)
+├── docs/
+│   └── surge-client.conf
+└── site.yml
+```
+
+## Variables Reference
+
+| Variable | Default | Description |
+|---|---|---|
+| `ss_port` | 8388 | Shadowsocks listen port |
+| `ss_cipher` | aes-256-gcm | Shadowsocks encryption method |
+| `ss_version` | 1.21.2 | shadowsocks-rust release version |
+| `trojan_port` | 443 | Trojan listen port |
+| `trojan_domain` | — | Domain name for TLS certificate |
+| `trojan_fallback_port` | 8080 | Fallback port for non-Trojan traffic |
+| `trojan_version` | 0.10.6 | trojan-go release version |
+| `certbot_email` | — | Email for Let's Encrypt |
+| `ssh_port` | 22 | SSH listen port |

+ 10 - 0
ansible.cfg

@@ -0,0 +1,10 @@
+[defaults]
+inventory = inventory/hosts.yml
+roles_path = roles
+host_key_checking = False
+retry_files_enabled = False
+
+[privilege_escalation]
+become = True
+become_method = sudo
+become_user = root

+ 47 - 0
docs/surge-client.conf

@@ -0,0 +1,47 @@
+# Surge Client Reference Configuration
+# Chained proxy setup: Relay (Shadowsocks) + Landing (Trojan)
+#
+# Replace placeholders:
+#   RELAY_IP        - Relay server IP address
+#   SS_PORT         - Shadowsocks port (default: 8388)
+#   SS_PASSWORD     - Shadowsocks password
+#   LANDING_DOMAIN  - Landing server domain name
+#   TROJAN_PASSWORD - Trojan password
+
+[General]
+loglevel = notify
+
+[Proxy]
+# Relay server - Shadowsocks (中转机)
+Relay-SS = ss, RELAY_IP, SS_PORT, encrypt-method=aes-256-gcm, password=SS_PASSWORD
+
+# Landing server - Trojan direct (落地机直连)
+Landing-Trojan = trojan, LANDING_DOMAIN, 443, password=TROJAN_PASSWORD
+
+# Landing server - chained through relay (落地机经中转)
+Landing-Chain = trojan, LANDING_DOMAIN, 443, password=TROJAN_PASSWORD, underlying-proxy=Relay-SS
+
+[Proxy Group]
+# For services needing landing server's local IP (AI, streaming)
+Chain = select, Landing-Chain
+# For direct access to landing server
+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 → Chain (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 Services → Chain (exit from landing IP) ---
+RULE-SET,https://ruleset.skk.moe/List/non_ip/stream_us.conf,Chain
+
+# --- IP-based rules (must come after domain rules) ---
+RULE-SET,https://ruleset.skk.moe/List/ip/stream_us.conf,Chain
+
+# --- Default: all other traffic through relay ---
+FINAL,Relay-SS

+ 11 - 0
group_vars/all.yml

@@ -0,0 +1,11 @@
+base_packages:
+  - curl
+  - wget
+  - vim
+  - htop
+  - unzip
+  - ufw
+  - fail2ban
+  - unattended-upgrades
+
+ssh_port: 22

+ 12 - 0
group_vars/landing.yml

@@ -0,0 +1,12 @@
+trojan_domain: "YOUR_DOMAIN"
+trojan_password: "{{ vault_trojan_password }}"
+trojan_port: 443
+trojan_fallback_port: 8080
+
+certbot_email: "YOUR_EMAIL"
+
+tls_cert_path: "/etc/letsencrypt/live/{{ trojan_domain }}/fullchain.pem"
+tls_key_path: "/etc/letsencrypt/live/{{ trojan_domain }}/privkey.pem"
+
+allowed_ports:
+  - "{{ trojan_port }}"

+ 6 - 0
group_vars/relay.yml

@@ -0,0 +1,6 @@
+ss_port: 8388
+ss_cipher: "aes-256-gcm"
+ss_password: "{{ vault_ss_password }}"
+
+allowed_ports:
+  - "{{ ss_port }}"

+ 7 - 0
group_vars/vault.yml.example

@@ -0,0 +1,7 @@
+# Copy this file to vault.yml and encrypt with:
+#   ansible-vault encrypt group_vars/vault.yml
+#
+# Then reference these variables in relay.yml and landing.yml
+
+vault_ss_password: "your-shadowsocks-password-here"
+vault_trojan_password: "your-trojan-password-here"

+ 12 - 0
inventory/hosts.yml

@@ -0,0 +1,12 @@
+all:
+  children:
+    relay:
+      hosts:
+        relay-server:
+          ansible_host: "YOUR_RELAY_IP"
+          ansible_user: root
+    landing:
+      hosts:
+        landing-server:
+          ansible_host: "YOUR_LANDING_IP"
+          ansible_user: root

+ 2 - 0
openspec/changes/archive/2026-04-22-chain-proxy-setup/.openspec.yaml

@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-04-22

+ 124 - 0
openspec/changes/archive/2026-04-22-chain-proxy-setup/design.md

@@ -0,0 +1,124 @@
+## 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:
+
+```ini
+[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.

+ 33 - 0
openspec/changes/archive/2026-04-22-chain-proxy-setup/proposal.md

@@ -0,0 +1,33 @@
+## Why
+
+We need to set up two servers as a chained proxy pair using Ansible for automated, repeatable deployment. The "relay server" (中转机) handles general traffic and forwards it onward, while the "landing server" (落地机) provides local IP access for services that require geo-specific endpoints (AI services, streaming). Surge (client-side) uses its underlying-proxy feature to chain connections through both servers. The servers themselves run encrypted proxy daemons — Shadowsocks on the relay for fast transit, Trojan on the landing for HTTPS-disguised access to geo-sensitive services.
+
+## What Changes
+
+- Create Ansible inventory defining two server roles: `relay` (中转机) and `landing` (落地机)
+- Create playbooks and roles to provision both servers with base system configuration (packages, security hardening, SSH)
+- Deploy and configure **shadowsocks-rust** on the relay server as an encrypted forward proxy
+- Deploy and configure **Trojan** (trojan-go or trojan) on the landing server with TLS termination
+- Configure the landing server to also expose a direct proxy endpoint (bypassing the relay chain)
+- Client-side Surge uses `underlying-proxy` to chain: Client → Relay (SS) → Landing (Trojan) → Internet
+- Surge policy rules on the client route AI/streaming traffic through the landing chain, other traffic through the relay directly
+
+## Capabilities
+
+### New Capabilities
+- `server-base`: Base server initialization (packages, security, SSH hardening, firewall)
+- `shadowsocks-relay`: Shadowsocks-rust deployment and configuration for the relay/transit server
+- `trojan-landing`: Trojan deployment and configuration for the landing server with TLS
+- `proxy-rules`: Client-side Surge routing rules for AI services, streaming services, and default traffic
+
+### Modified Capabilities
+
+(none - this is a greenfield project)
+
+## Impact
+
+- New Ansible project structure: inventory, playbooks, roles, and group_vars
+- Relay server will run shadowsocks-rust as a systemd service
+- Landing server will run Trojan as a systemd service, requiring a domain name and TLS certificate
+- Network traffic patterns: chained traffic flows Client → Relay (SS) → Landing (Trojan) → Internet; direct traffic flows Client → Landing (Trojan) → Internet
+- Firewall rules: relay allows SS port, landing allows Trojan port (443)

+ 48 - 0
openspec/changes/archive/2026-04-22-chain-proxy-setup/specs/proxy-rules/spec.md

@@ -0,0 +1,48 @@
+## ADDED Requirements
+
+### Requirement: Reference Surge client config documents the chained proxy setup
+The project SHALL include a reference Surge client configuration file showing how to configure the relay (Shadowsocks) and landing (Trojan) proxies with underlying-proxy chaining.
+
+#### Scenario: Chain proxy definition
+- **WHEN** a user reads the reference Surge config
+- **THEN** it defines a Shadowsocks proxy pointing to the relay server
+- **THEN** it defines a Trojan proxy pointing to the landing server with `underlying-proxy` set to the relay SS proxy
+- **THEN** it defines a direct Trojan proxy to the landing server without underlying-proxy
+
+### Requirement: Surge rules use Sukka's ruleset for AI services
+The reference Surge configuration SHALL use rulesets from `ruleset.skk.moe` (https://github.com/SukkaW/Surge) for AI service routing, not self-maintained domain lists.
+
+#### Scenario: AI traffic routed through chained landing proxy
+- **WHEN** a client request matches Sukka's AI ruleset (e.g., `RULE-SET,https://ruleset.skk.moe/List/non_ip/ai.conf`)
+- **THEN** the Surge rule directs it through the chained landing proxy
+- **THEN** traffic exits from the landing server's local IP
+
+### Requirement: Surge rules use Sukka's ruleset for streaming services
+The reference Surge configuration SHALL use rulesets from `ruleset.skk.moe` for streaming service routing, selecting the appropriate regional ruleset.
+
+#### Scenario: Streaming traffic routed through chained landing proxy
+- **WHEN** a client request matches Sukka's streaming ruleset (e.g., `RULE-SET,https://ruleset.skk.moe/List/non_ip/stream_us.conf`)
+- **THEN** the Surge rule directs it through the chained landing proxy
+- **THEN** traffic exits from the landing server's local IP
+
+### Requirement: Rule ordering follows Sukka's prescribed order
+The reference Surge configuration SHALL place `DOMAIN-SET` and `non_ip` rules before all IP-based rules, as required by Sukka's ruleset documentation.
+
+#### Scenario: Correct rule ordering
+- **WHEN** the Surge config is loaded
+- **THEN** all `DOMAIN-SET` and `non_ip` RULE-SET entries appear before any `ip` RULE-SET entries
+
+### Requirement: Default traffic routes through the relay only
+The reference Surge configuration SHALL route all traffic not matching any specific ruleset through the relay Shadowsocks proxy directly.
+
+#### Scenario: Non-matched domain goes through relay
+- **WHEN** a client request does not match any ruleset
+- **THEN** the Surge `FINAL` rule directs it through the relay SS proxy
+- **THEN** traffic exits from the relay server's IP
+
+### Requirement: No self-maintained domain lists
+The project SHALL NOT maintain its own domain lists for AI or streaming services. All domain/rule matching SHALL be delegated to Sukka's externally maintained rulesets.
+
+#### Scenario: No local domain list files
+- **WHEN** the project repository is inspected
+- **THEN** there are no manually curated domain list files for proxy routing

+ 51 - 0
openspec/changes/archive/2026-04-22-chain-proxy-setup/specs/server-base/spec.md

@@ -0,0 +1,51 @@
+## ADDED Requirements
+
+### Requirement: Ansible inventory defines relay and landing server groups
+The inventory SHALL define two host groups: `relay` and `landing`, each containing the respective server's connection details (IP, SSH user, SSH key).
+
+#### Scenario: Inventory is valid
+- **WHEN** the inventory file is parsed by Ansible
+- **THEN** two groups `relay` and `landing` are available, each with at least one host
+
+### Requirement: Base packages are installed on all servers
+The base role SHALL install essential packages: `curl`, `wget`, `vim`, `htop`, `unzip`, `ufw`, `fail2ban`, `unattended-upgrades`.
+
+#### Scenario: Fresh server provisioning
+- **WHEN** the base role runs on a fresh Ubuntu/Debian server
+- **THEN** all listed packages are installed and available
+
+### Requirement: SSH is hardened
+The base role SHALL configure SSH to disable password authentication, disable root login, and only allow key-based authentication.
+
+#### Scenario: SSH hardening applied
+- **WHEN** the base role completes
+- **THEN** `/etc/ssh/sshd_config` has `PasswordAuthentication no`, `PermitRootLogin no`, and `PubkeyAuthentication yes`
+- **THEN** the sshd service is restarted
+
+### Requirement: UFW firewall is configured with default deny
+The base role SHALL enable UFW with a default deny incoming policy and allow SSH (port 22).
+
+#### Scenario: Firewall base rules
+- **WHEN** the base role completes
+- **THEN** UFW is active with default deny incoming
+- **THEN** SSH port 22 is allowed
+
+#### Scenario: Proxy ports are allowed per server role
+- **WHEN** the base role runs on a relay server
+- **THEN** the relay proxy port is allowed through UFW
+- **WHEN** the base role runs on a landing server
+- **THEN** both the chained and direct proxy ports are allowed through UFW
+
+### Requirement: fail2ban protects SSH
+The base role SHALL configure fail2ban to monitor SSH login attempts and ban IPs after repeated failures.
+
+#### Scenario: fail2ban is active
+- **WHEN** the base role completes
+- **THEN** fail2ban is running with an SSH jail enabled
+
+### Requirement: Automatic security updates are enabled
+The base role SHALL enable unattended-upgrades for security patches.
+
+#### Scenario: Unattended upgrades configured
+- **WHEN** the base role completes
+- **THEN** unattended-upgrades is configured to auto-install security updates

+ 51 - 0
openspec/changes/archive/2026-04-22-chain-proxy-setup/specs/shadowsocks-relay/spec.md

@@ -0,0 +1,51 @@
+## ADDED Requirements
+
+### Requirement: shadowsocks-rust is installed on the relay server
+The shadowsocks role SHALL download and install the shadowsocks-rust `ssserver` binary from GitHub releases to a configurable path, with a configurable version.
+
+#### Scenario: Fresh installation
+- **WHEN** the shadowsocks role runs on a relay server without shadowsocks-rust installed
+- **THEN** the specified version of `ssserver` binary is downloaded and installed
+- **THEN** the binary is executable and owned by a dedicated service user
+
+#### Scenario: Version upgrade
+- **WHEN** the shadowsocks version variable is changed and the playbook is re-run
+- **THEN** the binary is replaced with the new version
+- **THEN** the service is restarted
+
+### Requirement: shadowsocks-rust runs as a systemd service
+The shadowsocks role SHALL create a systemd unit file for `ssserver` and ensure it is enabled and started.
+
+#### Scenario: Service is running
+- **WHEN** the shadowsocks role completes
+- **THEN** the `ssserver` systemd service is enabled and running
+- **THEN** the service runs under a dedicated non-root user
+
+### Requirement: shadowsocks-rust configuration is templated
+The relay server's shadowsocks configuration SHALL be generated from a Jinja2 template with variables for port, password, and cipher method.
+
+#### Scenario: Configuration is generated from template
+- **WHEN** the shadowsocks role runs
+- **THEN** a JSON config file is rendered from template with port, password, and encryption method
+- **THEN** the config file permissions are restricted (readable only by the service user)
+
+#### Scenario: Configuration change triggers restart
+- **WHEN** a variable in `group_vars/relay.yml` is changed and the playbook is re-run
+- **THEN** the configuration file is updated
+- **THEN** the ssserver service is restarted
+
+### Requirement: shadowsocks uses AEAD cipher
+The shadowsocks configuration SHALL use an AEAD cipher (e.g., `aes-256-gcm` or `chacha20-ietf-poly1305`), configurable via Ansible variable.
+
+#### Scenario: AEAD cipher is configured
+- **WHEN** the shadowsocks configuration is generated
+- **THEN** the `method` field uses the configured AEAD cipher
+- **THEN** the default cipher is `aes-256-gcm` if not overridden
+
+### Requirement: shadowsocks credentials are managed via Ansible Vault
+The relay server's shadowsocks password SHALL be defined in `group_vars` and encrypted with Ansible Vault.
+
+#### Scenario: Password is not in plaintext in repository
+- **WHEN** the shadowsocks role generates the configuration
+- **THEN** the password comes from an Ansible Vault-encrypted variable
+- **THEN** no plaintext password exists in version-controlled files

+ 67 - 0
openspec/changes/archive/2026-04-22-chain-proxy-setup/specs/trojan-landing/spec.md

@@ -0,0 +1,67 @@
+## ADDED Requirements
+
+### Requirement: Trojan is installed on the landing server
+The trojan role SHALL download and install the Trojan binary (trojan-go or trojan-gfw) from release artifacts to a configurable path.
+
+#### Scenario: Fresh installation
+- **WHEN** the trojan role runs on a landing server without Trojan installed
+- **THEN** the specified version of the Trojan binary is downloaded and installed
+- **THEN** the binary is executable and owned by a dedicated service user
+
+#### Scenario: Version upgrade
+- **WHEN** the Trojan version variable is changed and the playbook is re-run
+- **THEN** the binary is replaced with the new version
+- **THEN** the service is restarted
+
+### Requirement: Trojan runs as a systemd service
+The trojan role SHALL create a systemd unit file for Trojan and ensure it is enabled and started.
+
+#### Scenario: Service is running
+- **WHEN** the trojan role completes
+- **THEN** the Trojan systemd service is enabled and running
+- **THEN** the service runs under a dedicated non-root user (with `CAP_NET_BIND_SERVICE` for port 443)
+
+### Requirement: TLS certificate is provisioned via Let's Encrypt
+The trojan role SHALL use certbot to obtain a TLS certificate for the landing server's domain, with automatic renewal.
+
+#### Scenario: Certificate provisioning
+- **WHEN** the trojan role runs with a configured domain name
+- **THEN** certbot obtains a TLS certificate for that domain
+- **THEN** the certificate and key are accessible to the Trojan service
+
+#### Scenario: Certificate auto-renewal
+- **WHEN** the certificate is within 30 days of expiry
+- **THEN** certbot renews it automatically via systemd timer or cron
+- **THEN** the Trojan service is reloaded after renewal
+
+### Requirement: Trojan listens on port 443 with TLS
+The Trojan service SHALL listen on port 443 and terminate TLS using the Let's Encrypt certificate, presenting as a normal HTTPS server to non-Trojan clients.
+
+#### Scenario: Trojan accepts connections on 443
+- **WHEN** a Trojan client connects to port 443 with valid credentials
+- **THEN** the connection is accepted and proxied
+
+#### Scenario: Non-Trojan traffic is handled by fallback
+- **WHEN** a non-Trojan HTTPS request arrives on port 443
+- **THEN** Trojan forwards it to a local fallback web server (camouflage)
+
+### Requirement: Trojan configuration is templated
+The landing server's Trojan configuration SHALL be generated from a Jinja2 template with variables for password, domain, TLS paths, and fallback settings.
+
+#### Scenario: Configuration is generated from template
+- **WHEN** the trojan role runs
+- **THEN** a JSON config file is rendered from template
+- **THEN** the config file permissions are restricted (readable only by the service user)
+
+#### Scenario: Configuration change triggers restart
+- **WHEN** a variable in `group_vars/landing.yml` is changed and the playbook is re-run
+- **THEN** the configuration file is updated
+- **THEN** the Trojan service is restarted
+
+### Requirement: Trojan credentials are managed via Ansible Vault
+The landing server's Trojan password SHALL be defined in `group_vars` and encrypted with Ansible Vault.
+
+#### Scenario: Password is not in plaintext in repository
+- **WHEN** the trojan role generates the configuration
+- **THEN** the password comes from an Ansible Vault-encrypted variable
+- **THEN** no plaintext password exists in version-controlled files

+ 48 - 0
openspec/changes/archive/2026-04-22-chain-proxy-setup/tasks.md

@@ -0,0 +1,48 @@
+## 1. Project Structure & Inventory
+
+- [x] 1.1 Create Ansible project directory structure: `inventory/`, `group_vars/`, `roles/`, and `site.yml`
+- [x] 1.2 Create `inventory/hosts.yml` with `relay` and `landing` host groups and placeholder connection details
+- [x] 1.3 Create `group_vars/all.yml` with shared variables (SSH user, common packages)
+- [x] 1.4 Create `group_vars/relay.yml` with relay-specific variables (SS port, cipher, password placeholder)
+- [x] 1.5 Create `group_vars/landing.yml` with landing-specific variables (Trojan password, domain, TLS paths)
+- [x] 1.6 Create `ansible.cfg` with sensible defaults (inventory path, roles path, vault settings)
+
+## 2. Base Server Role
+
+- [x] 2.1 Create `roles/base/tasks/main.yml` with package installation tasks
+- [x] 2.2 Add SSH hardening tasks (template `sshd_config`, restart sshd handler)
+- [x] 2.3 Create `roles/base/templates/sshd_config.j2` with hardened SSH configuration
+- [x] 2.4 Add UFW firewall tasks (default deny, allow SSH, allow role-specific ports via variable)
+- [x] 2.5 Add fail2ban tasks (install, configure SSH jail, enable service)
+- [x] 2.6 Add unattended-upgrades tasks (install, configure security auto-updates)
+- [x] 2.7 Create `roles/base/handlers/main.yml` for service restart handlers
+
+## 3. Shadowsocks Relay Role
+
+- [x] 3.1 Create `roles/shadowsocks/tasks/main.yml` — download shadowsocks-rust binary from GitHub releases, create service user
+- [x] 3.2 Create `roles/shadowsocks/templates/ss-config.json.j2` — JSON config with server port, password, AEAD cipher
+- [x] 3.3 Create `roles/shadowsocks/templates/shadowsocks.service.j2` — systemd unit file running as dedicated user
+- [x] 3.4 Create `roles/shadowsocks/handlers/main.yml` — restart handler triggered on config change
+- [x] 3.5 Create `roles/shadowsocks/defaults/main.yml` — default values (version, cipher `aes-256-gcm`, port)
+
+## 4. Trojan Landing Role
+
+- [x] 4.1 Create `roles/trojan/tasks/main.yml` — download Trojan binary, create service user, grant `CAP_NET_BIND_SERVICE`
+- [x] 4.2 Add certbot tasks — install certbot, obtain Let's Encrypt certificate for configured domain
+- [x] 4.3 Add certbot renewal hook — reload Trojan service on certificate renewal
+- [x] 4.4 Create `roles/trojan/templates/trojan-config.json.j2` — JSON config with password, TLS cert/key paths, fallback address
+- [x] 4.5 Create `roles/trojan/templates/trojan.service.j2` — systemd unit file running as dedicated user
+- [x] 4.6 Create `roles/trojan/handlers/main.yml` — restart handler triggered on config change
+- [x] 4.7 Create `roles/trojan/defaults/main.yml` — default values (version, port 443, fallback port)
+
+## 5. Reference Surge Client Configuration
+
+- [x] 5.1 Create `docs/surge-client.conf` — reference Surge config with proxy definitions (Relay-SS, Landing-Trojan, Landing-Chain with underlying-proxy)
+- [x] 5.2 Add proxy rules using Sukka's rulesets (`ruleset.skk.moe`) — AI ruleset → Chain, streaming ruleset → Chain, with correct ordering (DOMAIN-SET/non_ip before ip rules)
+- [x] 5.3 Add FINAL rule routing default traffic through Relay-SS
+
+## 6. Main Playbook & Integration
+
+- [x] 6.1 Create `site.yml` main playbook applying roles in order: base → shadowsocks (relay group) / trojan (landing group)
+- [x] 6.2 Add Ansible Vault example for encrypting SS password and Trojan password
+- [x] 6.3 Create README.md with setup instructions, variable reference, domain/TLS prerequisites, and usage guide

+ 20 - 0
openspec/config.yaml

@@ -0,0 +1,20 @@
+schema: spec-driven
+
+# Project context (optional)
+# This is shown to AI when creating artifacts.
+# Add your tech stack, conventions, style guides, domain knowledge, etc.
+# Example:
+#   context: |
+#     Tech stack: TypeScript, React, Node.js
+#     We use conventional commits
+#     Domain: e-commerce platform
+
+# Per-artifact rules (optional)
+# Add custom rules for specific artifacts.
+# Example:
+#   rules:
+#     proposal:
+#       - Keep proposals under 500 words
+#       - Always include a "Non-goals" section
+#     tasks:
+#       - Break tasks into chunks of max 2 hours

+ 48 - 0
openspec/specs/proxy-rules/spec.md

@@ -0,0 +1,48 @@
+## ADDED Requirements
+
+### Requirement: Reference Surge client config documents the chained proxy setup
+The project SHALL include a reference Surge client configuration file showing how to configure the relay (Shadowsocks) and landing (Trojan) proxies with underlying-proxy chaining.
+
+#### Scenario: Chain proxy definition
+- **WHEN** a user reads the reference Surge config
+- **THEN** it defines a Shadowsocks proxy pointing to the relay server
+- **THEN** it defines a Trojan proxy pointing to the landing server with `underlying-proxy` set to the relay SS proxy
+- **THEN** it defines a direct Trojan proxy to the landing server without underlying-proxy
+
+### Requirement: Surge rules use Sukka's ruleset for AI services
+The reference Surge configuration SHALL use rulesets from `ruleset.skk.moe` (https://github.com/SukkaW/Surge) for AI service routing, not self-maintained domain lists.
+
+#### Scenario: AI traffic routed through chained landing proxy
+- **WHEN** a client request matches Sukka's AI ruleset (e.g., `RULE-SET,https://ruleset.skk.moe/List/non_ip/ai.conf`)
+- **THEN** the Surge rule directs it through the chained landing proxy
+- **THEN** traffic exits from the landing server's local IP
+
+### Requirement: Surge rules use Sukka's ruleset for streaming services
+The reference Surge configuration SHALL use rulesets from `ruleset.skk.moe` for streaming service routing, selecting the appropriate regional ruleset.
+
+#### Scenario: Streaming traffic routed through chained landing proxy
+- **WHEN** a client request matches Sukka's streaming ruleset (e.g., `RULE-SET,https://ruleset.skk.moe/List/non_ip/stream_us.conf`)
+- **THEN** the Surge rule directs it through the chained landing proxy
+- **THEN** traffic exits from the landing server's local IP
+
+### Requirement: Rule ordering follows Sukka's prescribed order
+The reference Surge configuration SHALL place `DOMAIN-SET` and `non_ip` rules before all IP-based rules, as required by Sukka's ruleset documentation.
+
+#### Scenario: Correct rule ordering
+- **WHEN** the Surge config is loaded
+- **THEN** all `DOMAIN-SET` and `non_ip` RULE-SET entries appear before any `ip` RULE-SET entries
+
+### Requirement: Default traffic routes through the relay only
+The reference Surge configuration SHALL route all traffic not matching any specific ruleset through the relay Shadowsocks proxy directly.
+
+#### Scenario: Non-matched domain goes through relay
+- **WHEN** a client request does not match any ruleset
+- **THEN** the Surge `FINAL` rule directs it through the relay SS proxy
+- **THEN** traffic exits from the relay server's IP
+
+### Requirement: No self-maintained domain lists
+The project SHALL NOT maintain its own domain lists for AI or streaming services. All domain/rule matching SHALL be delegated to Sukka's externally maintained rulesets.
+
+#### Scenario: No local domain list files
+- **WHEN** the project repository is inspected
+- **THEN** there are no manually curated domain list files for proxy routing

+ 51 - 0
openspec/specs/server-base/spec.md

@@ -0,0 +1,51 @@
+## ADDED Requirements
+
+### Requirement: Ansible inventory defines relay and landing server groups
+The inventory SHALL define two host groups: `relay` and `landing`, each containing the respective server's connection details (IP, SSH user, SSH key).
+
+#### Scenario: Inventory is valid
+- **WHEN** the inventory file is parsed by Ansible
+- **THEN** two groups `relay` and `landing` are available, each with at least one host
+
+### Requirement: Base packages are installed on all servers
+The base role SHALL install essential packages: `curl`, `wget`, `vim`, `htop`, `unzip`, `ufw`, `fail2ban`, `unattended-upgrades`.
+
+#### Scenario: Fresh server provisioning
+- **WHEN** the base role runs on a fresh Ubuntu/Debian server
+- **THEN** all listed packages are installed and available
+
+### Requirement: SSH is hardened
+The base role SHALL configure SSH to disable password authentication, disable root login, and only allow key-based authentication.
+
+#### Scenario: SSH hardening applied
+- **WHEN** the base role completes
+- **THEN** `/etc/ssh/sshd_config` has `PasswordAuthentication no`, `PermitRootLogin no`, and `PubkeyAuthentication yes`
+- **THEN** the sshd service is restarted
+
+### Requirement: UFW firewall is configured with default deny
+The base role SHALL enable UFW with a default deny incoming policy and allow SSH (port 22).
+
+#### Scenario: Firewall base rules
+- **WHEN** the base role completes
+- **THEN** UFW is active with default deny incoming
+- **THEN** SSH port 22 is allowed
+
+#### Scenario: Proxy ports are allowed per server role
+- **WHEN** the base role runs on a relay server
+- **THEN** the relay proxy port is allowed through UFW
+- **WHEN** the base role runs on a landing server
+- **THEN** both the chained and direct proxy ports are allowed through UFW
+
+### Requirement: fail2ban protects SSH
+The base role SHALL configure fail2ban to monitor SSH login attempts and ban IPs after repeated failures.
+
+#### Scenario: fail2ban is active
+- **WHEN** the base role completes
+- **THEN** fail2ban is running with an SSH jail enabled
+
+### Requirement: Automatic security updates are enabled
+The base role SHALL enable unattended-upgrades for security patches.
+
+#### Scenario: Unattended upgrades configured
+- **WHEN** the base role completes
+- **THEN** unattended-upgrades is configured to auto-install security updates

+ 51 - 0
openspec/specs/shadowsocks-relay/spec.md

@@ -0,0 +1,51 @@
+## ADDED Requirements
+
+### Requirement: shadowsocks-rust is installed on the relay server
+The shadowsocks role SHALL download and install the shadowsocks-rust `ssserver` binary from GitHub releases to a configurable path, with a configurable version.
+
+#### Scenario: Fresh installation
+- **WHEN** the shadowsocks role runs on a relay server without shadowsocks-rust installed
+- **THEN** the specified version of `ssserver` binary is downloaded and installed
+- **THEN** the binary is executable and owned by a dedicated service user
+
+#### Scenario: Version upgrade
+- **WHEN** the shadowsocks version variable is changed and the playbook is re-run
+- **THEN** the binary is replaced with the new version
+- **THEN** the service is restarted
+
+### Requirement: shadowsocks-rust runs as a systemd service
+The shadowsocks role SHALL create a systemd unit file for `ssserver` and ensure it is enabled and started.
+
+#### Scenario: Service is running
+- **WHEN** the shadowsocks role completes
+- **THEN** the `ssserver` systemd service is enabled and running
+- **THEN** the service runs under a dedicated non-root user
+
+### Requirement: shadowsocks-rust configuration is templated
+The relay server's shadowsocks configuration SHALL be generated from a Jinja2 template with variables for port, password, and cipher method.
+
+#### Scenario: Configuration is generated from template
+- **WHEN** the shadowsocks role runs
+- **THEN** a JSON config file is rendered from template with port, password, and encryption method
+- **THEN** the config file permissions are restricted (readable only by the service user)
+
+#### Scenario: Configuration change triggers restart
+- **WHEN** a variable in `group_vars/relay.yml` is changed and the playbook is re-run
+- **THEN** the configuration file is updated
+- **THEN** the ssserver service is restarted
+
+### Requirement: shadowsocks uses AEAD cipher
+The shadowsocks configuration SHALL use an AEAD cipher (e.g., `aes-256-gcm` or `chacha20-ietf-poly1305`), configurable via Ansible variable.
+
+#### Scenario: AEAD cipher is configured
+- **WHEN** the shadowsocks configuration is generated
+- **THEN** the `method` field uses the configured AEAD cipher
+- **THEN** the default cipher is `aes-256-gcm` if not overridden
+
+### Requirement: shadowsocks credentials are managed via Ansible Vault
+The relay server's shadowsocks password SHALL be defined in `group_vars` and encrypted with Ansible Vault.
+
+#### Scenario: Password is not in plaintext in repository
+- **WHEN** the shadowsocks role generates the configuration
+- **THEN** the password comes from an Ansible Vault-encrypted variable
+- **THEN** no plaintext password exists in version-controlled files

+ 67 - 0
openspec/specs/trojan-landing/spec.md

@@ -0,0 +1,67 @@
+## ADDED Requirements
+
+### Requirement: Trojan is installed on the landing server
+The trojan role SHALL download and install the Trojan binary (trojan-go or trojan-gfw) from release artifacts to a configurable path.
+
+#### Scenario: Fresh installation
+- **WHEN** the trojan role runs on a landing server without Trojan installed
+- **THEN** the specified version of the Trojan binary is downloaded and installed
+- **THEN** the binary is executable and owned by a dedicated service user
+
+#### Scenario: Version upgrade
+- **WHEN** the Trojan version variable is changed and the playbook is re-run
+- **THEN** the binary is replaced with the new version
+- **THEN** the service is restarted
+
+### Requirement: Trojan runs as a systemd service
+The trojan role SHALL create a systemd unit file for Trojan and ensure it is enabled and started.
+
+#### Scenario: Service is running
+- **WHEN** the trojan role completes
+- **THEN** the Trojan systemd service is enabled and running
+- **THEN** the service runs under a dedicated non-root user (with `CAP_NET_BIND_SERVICE` for port 443)
+
+### Requirement: TLS certificate is provisioned via Let's Encrypt
+The trojan role SHALL use certbot to obtain a TLS certificate for the landing server's domain, with automatic renewal.
+
+#### Scenario: Certificate provisioning
+- **WHEN** the trojan role runs with a configured domain name
+- **THEN** certbot obtains a TLS certificate for that domain
+- **THEN** the certificate and key are accessible to the Trojan service
+
+#### Scenario: Certificate auto-renewal
+- **WHEN** the certificate is within 30 days of expiry
+- **THEN** certbot renews it automatically via systemd timer or cron
+- **THEN** the Trojan service is reloaded after renewal
+
+### Requirement: Trojan listens on port 443 with TLS
+The Trojan service SHALL listen on port 443 and terminate TLS using the Let's Encrypt certificate, presenting as a normal HTTPS server to non-Trojan clients.
+
+#### Scenario: Trojan accepts connections on 443
+- **WHEN** a Trojan client connects to port 443 with valid credentials
+- **THEN** the connection is accepted and proxied
+
+#### Scenario: Non-Trojan traffic is handled by fallback
+- **WHEN** a non-Trojan HTTPS request arrives on port 443
+- **THEN** Trojan forwards it to a local fallback web server (camouflage)
+
+### Requirement: Trojan configuration is templated
+The landing server's Trojan configuration SHALL be generated from a Jinja2 template with variables for password, domain, TLS paths, and fallback settings.
+
+#### Scenario: Configuration is generated from template
+- **WHEN** the trojan role runs
+- **THEN** a JSON config file is rendered from template
+- **THEN** the config file permissions are restricted (readable only by the service user)
+
+#### Scenario: Configuration change triggers restart
+- **WHEN** a variable in `group_vars/landing.yml` is changed and the playbook is re-run
+- **THEN** the configuration file is updated
+- **THEN** the Trojan service is restarted
+
+### Requirement: Trojan credentials are managed via Ansible Vault
+The landing server's Trojan password SHALL be defined in `group_vars` and encrypted with Ansible Vault.
+
+#### Scenario: Password is not in plaintext in repository
+- **WHEN** the trojan role generates the configuration
+- **THEN** the password comes from an Ansible Vault-encrypted variable
+- **THEN** no plaintext password exists in version-controlled files

+ 10 - 0
roles/base/handlers/main.yml

@@ -0,0 +1,10 @@
+---
+- name: restart sshd
+  ansible.builtin.systemd:
+    name: sshd
+    state: restarted
+
+- name: restart fail2ban
+  ansible.builtin.systemd:
+    name: fail2ban
+    state: restarted

+ 70 - 0
roles/base/tasks/main.yml

@@ -0,0 +1,70 @@
+---
+- name: Update apt cache
+  ansible.builtin.apt:
+    update_cache: yes
+    cache_valid_time: 3600
+
+- name: Install base packages
+  ansible.builtin.apt:
+    name: "{{ base_packages }}"
+    state: present
+
+- name: Harden SSH configuration
+  ansible.builtin.template:
+    src: sshd_config.j2
+    dest: /etc/ssh/sshd_config
+    owner: root
+    group: root
+    mode: "0644"
+    validate: "sshd -t -f %s"
+  notify: restart sshd
+
+- name: Enable UFW
+  community.general.ufw:
+    state: enabled
+    policy: deny
+    direction: incoming
+
+- name: Allow SSH through UFW
+  community.general.ufw:
+    rule: allow
+    port: "{{ ssh_port }}"
+    proto: tcp
+
+- name: Allow role-specific ports through UFW
+  community.general.ufw:
+    rule: allow
+    port: "{{ item }}"
+    proto: tcp
+  loop: "{{ allowed_ports | default([]) }}"
+
+- name: Configure fail2ban SSH jail
+  ansible.builtin.copy:
+    dest: /etc/fail2ban/jail.local
+    content: |
+      [sshd]
+      enabled = true
+      port = {{ ssh_port }}
+      maxretry = 5
+      bantime = 3600
+      findtime = 600
+    owner: root
+    group: root
+    mode: "0644"
+  notify: restart fail2ban
+
+- name: Enable and start fail2ban
+  ansible.builtin.systemd:
+    name: fail2ban
+    enabled: yes
+    state: started
+
+- name: Configure unattended-upgrades
+  ansible.builtin.copy:
+    dest: /etc/apt/apt.conf.d/20auto-upgrades
+    content: |
+      APT::Periodic::Update-Package-Lists "1";
+      APT::Periodic::Unattended-Upgrade "1";
+    owner: root
+    group: root
+    mode: "0644"

+ 14 - 0
roles/base/templates/sshd_config.j2

@@ -0,0 +1,14 @@
+Port {{ ssh_port }}
+Protocol 2
+
+PermitRootLogin prohibit-password
+PubkeyAuthentication yes
+PasswordAuthentication no
+ChallengeResponseAuthentication no
+UsePAM yes
+
+X11Forwarding no
+PrintMotd no
+AcceptEnv LANG LC_*
+
+Subsystem sftp /usr/lib/openssh/sftp-server

+ 7 - 0
roles/shadowsocks/defaults/main.yml

@@ -0,0 +1,7 @@
+---
+ss_version: "1.21.2"
+ss_port: 8388
+ss_cipher: "aes-256-gcm"
+ss_bin_path: /usr/local/bin/ssserver
+ss_config_path: /etc/shadowsocks/config.json
+ss_user: ssserver

+ 6 - 0
roles/shadowsocks/handlers/main.yml

@@ -0,0 +1,6 @@
+---
+- name: restart shadowsocks
+  ansible.builtin.systemd:
+    name: shadowsocks
+    daemon_reload: yes
+    state: restarted

+ 74 - 0
roles/shadowsocks/tasks/main.yml

@@ -0,0 +1,74 @@
+---
+- name: Create shadowsocks service user
+  ansible.builtin.user:
+    name: "{{ ss_user }}"
+    system: yes
+    shell: /usr/sbin/nologin
+    create_home: no
+
+- name: Create shadowsocks config directory
+  ansible.builtin.file:
+    path: "{{ ss_config_path | dirname }}"
+    state: directory
+    owner: "{{ ss_user }}"
+    group: "{{ ss_user }}"
+    mode: "0750"
+
+- name: Download shadowsocks-rust binary
+  ansible.builtin.get_url:
+    url: "https://github.com/shadowsocks/shadowsocks-rust/releases/download/v{{ ss_version }}/shadowsocks-v{{ ss_version }}.x86_64-unknown-linux-gnu.tar.xz"
+    dest: /tmp/shadowsocks.tar.xz
+    mode: "0644"
+
+- name: Extract shadowsocks-rust binary
+  ansible.builtin.unarchive:
+    src: /tmp/shadowsocks.tar.xz
+    dest: /tmp/
+    remote_src: yes
+
+- name: Install ssserver binary
+  ansible.builtin.copy:
+    src: /tmp/ssserver
+    dest: "{{ ss_bin_path }}"
+    remote_src: yes
+    owner: root
+    group: root
+    mode: "0755"
+  notify: restart shadowsocks
+
+- name: Clean up downloaded archive
+  ansible.builtin.file:
+    path: "{{ item }}"
+    state: absent
+  loop:
+    - /tmp/shadowsocks.tar.xz
+    - /tmp/ssserver
+    - /tmp/sslocal
+    - /tmp/ssurl
+    - /tmp/ssmanager
+    - /tmp/ssservice
+
+- name: Deploy shadowsocks configuration
+  ansible.builtin.template:
+    src: ss-config.json.j2
+    dest: "{{ ss_config_path }}"
+    owner: "{{ ss_user }}"
+    group: "{{ ss_user }}"
+    mode: "0640"
+  notify: restart shadowsocks
+
+- name: Deploy shadowsocks systemd unit
+  ansible.builtin.template:
+    src: shadowsocks.service.j2
+    dest: /etc/systemd/system/shadowsocks.service
+    owner: root
+    group: root
+    mode: "0644"
+  notify: restart shadowsocks
+
+- name: Enable and start shadowsocks service
+  ansible.builtin.systemd:
+    name: shadowsocks
+    daemon_reload: yes
+    enabled: yes
+    state: started

+ 15 - 0
roles/shadowsocks/templates/shadowsocks.service.j2

@@ -0,0 +1,15 @@
+[Unit]
+Description=Shadowsocks-rust Server
+After=network.target
+
+[Service]
+Type=simple
+User={{ ss_user }}
+Group={{ ss_user }}
+ExecStart={{ ss_bin_path }} -c {{ ss_config_path }}
+Restart=on-failure
+RestartSec=5
+LimitNOFILE=65536
+
+[Install]
+WantedBy=multi-user.target

+ 8 - 0
roles/shadowsocks/templates/ss-config.json.j2

@@ -0,0 +1,8 @@
+{
+    "server": "0.0.0.0",
+    "server_port": {{ ss_port }},
+    "password": "{{ ss_password }}",
+    "method": "{{ ss_cipher }}",
+    "mode": "tcp_and_udp",
+    "no_delay": true
+}

+ 7 - 0
roles/trojan/defaults/main.yml

@@ -0,0 +1,7 @@
+---
+trojan_version: "0.10.6"
+trojan_port: 443
+trojan_fallback_port: 8080
+trojan_bin_path: /usr/local/bin/trojan-go
+trojan_config_path: /etc/trojan-go/config.json
+trojan_user: trojan

+ 6 - 0
roles/trojan/handlers/main.yml

@@ -0,0 +1,6 @@
+---
+- name: restart trojan
+  ansible.builtin.systemd:
+    name: trojan-go
+    daemon_reload: yes
+    state: restarted

+ 126 - 0
roles/trojan/tasks/main.yml

@@ -0,0 +1,126 @@
+---
+- name: Create trojan service user
+  ansible.builtin.user:
+    name: "{{ trojan_user }}"
+    system: yes
+    shell: /usr/sbin/nologin
+    create_home: no
+
+- name: Create trojan config directory
+  ansible.builtin.file:
+    path: "{{ trojan_config_path | dirname }}"
+    state: directory
+    owner: "{{ trojan_user }}"
+    group: "{{ trojan_user }}"
+    mode: "0750"
+
+- name: Download trojan-go binary
+  ansible.builtin.get_url:
+    url: "https://github.com/p4gefau1t/trojan-go/releases/download/v{{ trojan_version }}/trojan-go-linux-amd64.zip"
+    dest: /tmp/trojan-go.zip
+    mode: "0644"
+
+- name: Extract trojan-go binary
+  ansible.builtin.unarchive:
+    src: /tmp/trojan-go.zip
+    dest: /tmp/trojan-go-extract/
+    remote_src: yes
+    creates: /tmp/trojan-go-extract/trojan-go
+
+- name: Install trojan-go binary
+  ansible.builtin.copy:
+    src: /tmp/trojan-go-extract/trojan-go
+    dest: "{{ trojan_bin_path }}"
+    remote_src: yes
+    owner: root
+    group: root
+    mode: "0755"
+  notify: restart trojan
+
+- name: Grant CAP_NET_BIND_SERVICE to trojan-go
+  community.general.capabilities:
+    path: "{{ trojan_bin_path }}"
+    capability: cap_net_bind_service=+ep
+    state: present
+
+- name: Clean up downloaded archive
+  ansible.builtin.file:
+    path: "{{ item }}"
+    state: absent
+  loop:
+    - /tmp/trojan-go.zip
+    - /tmp/trojan-go-extract
+
+- name: Install certbot
+  ansible.builtin.apt:
+    name:
+      - certbot
+    state: present
+
+- name: Obtain Let's Encrypt certificate
+  ansible.builtin.command:
+    cmd: >
+      certbot certonly --standalone
+      --non-interactive --agree-tos
+      --email {{ certbot_email }}
+      -d {{ trojan_domain }}
+    creates: "/etc/letsencrypt/live/{{ trojan_domain }}/fullchain.pem"
+
+- name: Grant trojan user read access to TLS certificates
+  ansible.builtin.file:
+    path: /etc/letsencrypt
+    state: directory
+    mode: "0755"
+
+- name: Ensure live directory is accessible
+  ansible.builtin.file:
+    path: "/etc/letsencrypt/live/{{ trojan_domain }}"
+    state: directory
+    mode: "0755"
+
+- name: Ensure archive directory is accessible
+  ansible.builtin.file:
+    path: "/etc/letsencrypt/archive/{{ trojan_domain }}"
+    state: directory
+    mode: "0755"
+
+- name: Deploy certbot renewal hook for trojan
+  ansible.builtin.copy:
+    dest: /etc/letsencrypt/renewal-hooks/post/restart-trojan.sh
+    content: |
+      #!/bin/bash
+      systemctl reload trojan-go
+    owner: root
+    group: root
+    mode: "0755"
+
+- name: Deploy trojan-go configuration
+  ansible.builtin.template:
+    src: trojan-config.json.j2
+    dest: "{{ trojan_config_path }}"
+    owner: "{{ trojan_user }}"
+    group: "{{ trojan_user }}"
+    mode: "0640"
+  notify: restart trojan
+
+- name: Deploy trojan-go systemd unit
+  ansible.builtin.template:
+    src: trojan.service.j2
+    dest: /etc/systemd/system/trojan-go.service
+    owner: root
+    group: root
+    mode: "0644"
+  notify: restart trojan
+
+- name: Enable and start trojan-go service
+  ansible.builtin.systemd:
+    name: trojan-go
+    daemon_reload: yes
+    enabled: yes
+    state: started
+
+- name: Enable certbot auto-renewal timer
+  ansible.builtin.systemd:
+    name: certbot.timer
+    enabled: yes
+    state: started

+ 18 - 0
roles/trojan/templates/trojan-config.json.j2

@@ -0,0 +1,18 @@
+{
+    "run_type": "server",
+    "local_addr": "0.0.0.0",
+    "local_port": {{ trojan_port }},
+    "remote_addr": "127.0.0.1",
+    "remote_port": {{ trojan_fallback_port }},
+    "password": [
+        "{{ trojan_password }}"
+    ],
+    "ssl": {
+        "cert": "{{ tls_cert_path }}",
+        "key": "{{ tls_key_path }}",
+        "sni": "{{ trojan_domain }}"
+    },
+    "router": {
+        "enabled": false
+    }
+}

+ 17 - 0
roles/trojan/templates/trojan.service.j2

@@ -0,0 +1,17 @@
+[Unit]
+Description=Trojan-Go Server
+After=network.target
+
+[Service]
+Type=simple
+User={{ trojan_user }}
+Group={{ trojan_user }}
+ExecStart={{ trojan_bin_path }} -config {{ trojan_config_path }}
+ExecReload=/bin/kill -HUP $MAINPID
+Restart=on-failure
+RestartSec=5
+LimitNOFILE=65536
+AmbientCapabilities=CAP_NET_BIND_SERVICE
+
+[Install]
+WantedBy=multi-user.target

+ 15 - 0
site.yml

@@ -0,0 +1,15 @@
+---
+- name: Base server setup (all hosts)
+  hosts: all
+  roles:
+    - base
+
+- name: Deploy Shadowsocks on relay server
+  hosts: relay
+  roles:
+    - shadowsocks
+
+- name: Deploy Trojan on landing server
+  hosts: landing
+  roles:
+    - trojan