Research Date: 2026-02-14 Research Purpose: Provide technical foundation for SSH remote control of Surge
Surge provides two remote control methods:
- CLI tool (
surge-cli) - Direct command-line operations, suitable for interactive scenarios - HTTP API - RESTful interface, suitable for programmatic control and UI development
Both have almost complete functional overlap, CLI is a wrapper around HTTP API. Recommend building TUI based on HTTP API for more flexibility.
/Applications/Surge.app/Contents/Applications/surge-cliSupports remote operations via --remote/-r parameter:
surge-cli --remote password@host:port <command>Limitation: Requires HTTP API enabled in Surge configuration (see below)
| Category | Command | Description |
|---|---|---|
| Configuration Management | reload |
Reload main config file |
switch-profile <name> |
Switch to specified profile | |
| Status Viewing | dump active |
Active connection list |
dump request |
Recent request history | |
dump rule |
Currently effective rules | |
dump policy |
All policies and policy groups | |
dump dns |
DNS cache | |
dump profile [original/effective] |
View config file | |
dump event |
Event logs | |
| Testing Tools | test-network |
Test network latency |
test-policy <name> |
Test single proxy | |
test-all-policies |
Test all proxies | |
test-group <name> |
Retest policy group | |
| Monitoring | watch request |
Real-time track new requests |
| Maintenance | flush dns |
Flush DNS cache |
kill <id> |
Terminate specified connection | |
stop |
Stop Surge | |
| Diagnostics | diagnostics |
Run network diagnostics |
set-log-level <level> |
Temporarily modify log level |
- Default: Human-readable formatted text
--rawparameter: Output JSON format, suitable for programmatic parsing
Example:
# Human readable
$ surge-cli dump active
Active Connections:
#1 TCP 192.168.1.100:54321 → google.com:443 [Proxy]
# JSON format
$ surge-cli --raw dump active
{"connections":[{"id":1,"protocol":"TCP","src":"192.168.1.100:54321",...}]}Add in Surge configuration file [General]:
[General]
http-api = your-secret-key@127.0.0.1:6170
http-api-tls = false # Optional, enable HTTPSSecurity:
- Default listens on
127.0.0.1, local access only (recommended) - Can set
0.0.0.0to allow remote access (requires firewall coordination) http-api-tls = trueenables HTTPS, uses MITM CA-signed certificate- Default port: 6170 (non-standard port, avoids conflicts)
Choose one of two methods:
- HTTP Header:
X-Key: your-secret-key - URL Parameter:
?x-key=your-secret-key
- Only supports GET / POST
- GET: Parameters passed via URL Query
- POST: Parameters passed via JSON Body
- Response: Always returns JSON
| Endpoint | Description | Platform Limitation |
|---|---|---|
/v1/features/mitm |
MITM feature | - |
/v1/features/capture |
Traffic capture | - |
/v1/features/rewrite |
URL rewrite | - |
/v1/features/scripting |
Scripting feature | - |
/v1/features/system_proxy |
System proxy | macOS |
/v1/features/enhanced_mode |
Enhanced mode | macOS |
GET Response:
{"enabled": true}POST Request:
{"enabled": false}GET /v1/outbound- Returns current mode:direct/proxy/rulePOST /v1/outbound- Switch mode, JSON Body:{"mode": "rule"}GET /v1/outbound/global- Get global default policyPOST /v1/outbound/global- Modify global policy, JSON:{"policy": "Proxy"}
| Endpoint | Method | Parameters | Description |
|---|---|---|---|
/v1/policies |
GET | - | List all policies |
/v1/policies/detail |
GET | ?policy_name=X |
Get policy details ( |
/v1/policies/test |
POST | {"policy_names":["X"], "url":"..."} |
Test policies |
/v1/policy_groups |
GET | - | List all policy groups |
/v1/policy_groups/select |
POST | {"group_name":"X", "policy":"Y"} |
Switch policy group selection |
/v1/policy_groups/test |
POST | {"group_name":"X"} |
Immediately test policy group |
GET /v1/policies actual response:
{
"proxies": ["Proxy1", "Proxy2", "..."],
"policy-groups": ["Group1", "Group2", "..."]
}GET /v1/policy_groups actual response (HashMap, not array):
{
"PolicyGroup1": [
{
"isGroup": false,
"name": "Proxy1",
"typeDescription": "Shadowsocks",
"lineHash": "abc123",
"enabled": true
}
],
"PolicyGroup2": [...]
}Note: /v1/policies/detail returns config file fragment (text), not structured JSON. In practice, policy info should be extracted from policy_groups.
GET /v1/requests/recent- Recent request list (default 100 entries)GET /v1/requests/active- Currently active connectionsPOST /v1/requests/kill- Terminate connection, JSON:{"id": 123}
Actual Request Object Structure (field names use camelCase):
{
"id": 123,
"processPath": "/usr/bin/curl",
"rule": "DOMAIN,google.com,Proxy",
"policyName": "Proxy", // ⚠️ Note field name
"remoteHost": "google.com:443",
"URL": "https://google.com",
"method": "CONNECT",
"status": "Completed",
"startDate": 1708000740.5, // Unix timestamp (seconds + decimal)
"inBytes": 2048, // ⚠️ Note field name (download)
"outBytes": 1024, // ⚠️ Note field name (upload)
"completed": true,
"failed": false
}Key Field Name Differences:
policy→policyNameupload→outBytesdownload→inBytesremote_address→remoteHoststart_time→startDate(Unix timestamp)
POST /v1/dns/flush- Flush DNS cacheGET /v1/dns- Get DNS cache contentsPOST /v1/test/dns_delay- Test DNS latency
GET /v1/profiles/current?sensitive=0- Get current config (sensitive=1shows sensitive info)POST /v1/profiles/reload- Reload configurationPOST /v1/profiles/switch- Switch profile (macOS), JSON:{"name": "work"}
POST /v1/scripting/evaluate- Execute script testGET /v1/modules- List module statusPOST /v1/modules- Enable/disable module, JSON:{"module_name": "X", "enabled": true}
GET /v1/mitm/ca- Download MITM CA certificate (DER binary format)POST /v1/stop- Stop Surge engineGET /v1/events- Get event center contentsGET /v1/traffic- Traffic statistics (supported in some versions)
| Dimension | CLI | HTTP API | Recommended Scenario |
|---|---|---|---|
| Call Method | Shell command | HTTP request | API for programmatic |
| Output Format | Text/JSON (--raw) | JSON | API more unified |
| Remote Access | Via -r parameter |
Direct HTTP | API more direct |
| Feature Coverage | ~90% | 100% | API more comprehensive |
| Real-time Monitoring | watch request |
Requires polling | CLI more convenient |
| Debug Friendly | Direct execution | Need to construct requests | CLI faster |
| UI Development | Need to parse text | Native JSON | API more suitable |
Key Differences:
- CLI's
watch requestcan continuously stream output, HTTP API can only poll - HTTP API provides traffic statistics, module management and other features CLI lacks
- CLI depends on HTTP API for operation (calls API under the hood)
| Risk | Severity | Mitigation |
|---|---|---|
| API Key Leakage | Critical | Use strong key (32+ characters), rotate regularly |
| Man-in-the-Middle Attack | High | Enable http-api-tls = true |
| Brute Force | Medium | No built-in rate limiting, rely on firewall |
| Unauthorized Access | Critical | Only listen on 127.0.0.1, access via SSH tunnel |
Option 1: SSH Tunnel (Most Secure)
[General]
http-api = strong-random-key-32-chars@127.0.0.1:6171
http-api-tls = false # SSH already encrypted, no need for duplicate encryptionLocal forwarding:
ssh -L 6171:127.0.0.1:6171 user@mac-host
curl -H "X-Key: strong-random-key-32-chars" http://127.0.0.1:6171/v1/outboundOption 2: Direct Exposure (Requires Firewall)
[General]
http-api = strong-random-key-32-chars@0.0.0.0:6171
http-api-tls = true # Must enable TLSLimit source IP:
# macOS firewall rules
sudo pfctl -e
echo "block drop in on en0 proto tcp from any to any port 6171" | sudo pfctl -f -
echo "pass in on en0 proto tcp from 192.168.1.100 to any port 6171" | sudo pfctl -f -# Check if Surge is running
ps aux | grep Surge
# Test local API (default port 6170)
curl -H "X-Key: your-key" http://127.0.0.1:6170/v1/outbound
# Expected response
{"mode": "rule"}
# Test policy list (verify actual format)
curl -H "X-Key: your-key" http://127.0.0.1:6170/v1/policies
# Expected response format
{"proxies": ["Proxy1", "Proxy2"], "policy-groups": ["Group1"]}# Establish SSH tunnel
ssh -L 6171:127.0.0.1:6171 user@remote-mac -N &
# Test remote API
curl -H "X-Key: your-key" http://127.0.0.1:6171/v1/policies
# Expected: Returns policy list JSON# Execute on local machine
surge-cli --remote your-key@remote-mac:6171 dump active
# Expected: Display remote Mac's active connections- No WebSocket Support - Cannot push events in real-time, can only poll
- No Batch Operations - Each policy test requires separate call
- No Permission Levels - API Key has full permissions (all or nothing)
- No Audit Logs - Cannot track API call history
- Version Differences - Some endpoints only available in newer versions (e.g.,
/v1/traffic)
- macOS Exclusive Features:
system_proxy,enhanced_mode,profiles/switch - iOS Has No CLI - Only accessible via HTTP API
- Minimum Version Requirement: Surge Mac 4.0+ (HTTP API v1)
- Polling Frequency: Recommend no more than once per second to avoid affecting Surge performance
- Data Volume:
dump requestdefault returns 100 entries, can be large with many requests - Timeout Settings: Policy testing (
test-policy) may take 5-30 seconds
For SSH remote control Surge TUI requirements:
| Solution | Advantages | Disadvantages | Recommendation |
|---|---|---|---|
| Pure CLI Wrapper | Rapid development, directly reuse commands | Complex output parsing, poor extensibility | ⭐⭐ |
| HTTP API + TUI | Structured data, easy to extend | Need to handle HTTP requests | ⭐⭐⭐⭐⭐ |
| Hybrid Solution | Combine advantages of both | High maintenance cost | ⭐⭐⭐ |
Recommendation: Build based on HTTP API, using Rust + ratatui + reqwest
Reasons:
- JSON responses don't need parsing, directly map to data structures
- HTTP connection reuse, better performance than executing CLI each time
- Easy to extend new features (CLI features limited by official updates)
- Can run independently of
surge-cli(reduce dependencies)
- Surge CLI Official Documentation
- Surge HTTP API Official Documentation
- Surge Enhanced Mode Explanation
- Surge Official Manual
Research Conclusion: Surge provides comprehensive remote control capabilities, HTTP API is the best choice for building TUI. Next step is to clarify TUI functional scope and interaction design.