English | 简体中文
fucksub is a subscription converter backend rewritten in Go. The goal is to make subconverter-style subscription conversion more maintainable, more testable, and far clearer about where and how things fail.
The original subconverter README defines the project as "a tool to convert between various proxy subscription formats", and ships a large target matrix: Clash, ClashR, Quantumult, Quantumult X, Loon, Mellow, SS, SSD, SSR, Surfboard, Surge, Trojan, V2Ray, Mixed, Auto, and more. SubConverter-Extended later added Mihomo-oriented enhancements, mainly to address the original project's slow adoption of newer protocols such as AnyTLS, Hysteria2, and TUIC.
fucksub starts from a more direct premise: SubConverter-Extended can no longer be relied on for maintenance — issues went unfixed for a long time and the repository was removed — so continuing to patch the old code is no longer worth it. fucksub is a rewrite with a smaller but controllable capability boundary.
- Only three output targets are supported:
clash,quanx,surge. clashtargets Clash/Mihomo-kernel clients, using Mihomo's Go parsing and validation.quanxtargets Quantumult X and emits Quantumult X config fragments.surgetargets Surge and emits Surge config fragments.- Byte-for-byte compatibility with the original subconverter or SubConverter-Extended is a non-goal.
- Unsupported protocols, fields, rules, and group semantics are never dropped silently — they are surfaced via warnings.
| Client / Format | target | Status | Notes |
|---|---|---|---|
| Clash/Mihomo | clash |
Production candidate | Emits Mihomo YAML with proxy-providers, rule-providers, custom proxy groups, and clash_rule_base merging, and validates the config through Mihomo. |
| Quantumult X | quanx |
Gray-release ready | Emits [server_local], [server_remote], [policy], [filter_remote], [filter_local]. Complex rulesets/providers are converted conservatively and warned. |
| Surge | surge |
Gray-release ready | Emits [General], [Proxy], [Proxy Group], [Rule]. With list=true, only [Proxy] is emitted. |
The original subconverter targets clashr, quan, loon, mellow, ss, sssub,
ssd, ssr, surfboard, trojan, v2ray, mixed, auto, etc. are not yet supported.
Shadowrocket has no dedicated target right now. The original subconverter usually suggests
ss, ssr, v2ray, or mixed for Shadowrocket, and those targets are not implemented
in this project yet.
Status meanings:
native: natively supported by the target and emitted directly.mapped: the target uses different syntax, but the current implementation can map it.degraded: only an approximate expression is possible; a warning is produced.unsupported: no reliable equivalent semantics or the implementation is unconfirmed; it is skipped and warned.
| Protocol | Clash/Mihomo | Surge | Quantumult X |
|---|---|---|---|
direct |
native | native | native |
reject |
native | native | native |
http |
native | native | native |
https |
native | native | mapped |
socks5 |
native | native | native |
socks5-tls |
native | native | mapped |
ss |
native | native | native |
ssr |
native | unsupported | mapped |
snell |
native | native | unsupported |
vmess |
native | native | native |
vless |
native | unsupported | mapped |
trojan |
native | native | native |
hysteria |
native | unsupported | unsupported |
hysteria2 |
native | native | unsupported |
tuic |
native | native | unsupported |
anytls |
native | native | native |
wireguard |
native | native | unsupported |
ssh |
native | native | unsupported |
mieru |
native | unsupported | unsupported |
masque |
native | unsupported | unsupported |
trust-tunnel |
native | native | unsupported |
openvpn |
native | unsupported | unsupported |
Notes:
directandrejectare mainly used as policy references, not emitted as remote nodes.- The
clashtarget keeps the original Mihomo proxy map and lets the Mihomo adapter run proxy/config validation. - The
surgeandquanxtargets only emit fields the mapper has confirmed; fields whose equivalent cannot be confirmed produce a warning.
The url parameter accepts one or more subscriptions. Multiple subscriptions are joined
with | and then URL-encoded.
Currently supported fetch schemes:
http://...https://...data:...
Default production security policy:
- Rejects loopback, link-local, private CIDR, multicast, metadata-service, and reserved documentation ranges by default.
- DNS resolution results and HTTP redirect targets are revalidated.
- Error messages redact the URL — basic auth, full path, and query tokens are not returned.
- Private deployments that genuinely need to fetch intranet subscriptions can set
FUCKSUB_ALLOW_PRIVATE_NETWORKS=trueto relax the address policy.
Currently supported parsing:
- Clash/Mihomo YAML:
proxies,proxy-providers - Base64 or plaintext node URI lists:
ss://,vmess://,trojan://,vless://,hysteria2://,hy2://,anytls://,tuic:// - Surge
[Proxy]-style node text lines - Quantumult X server-style node text lines
The project requires Go 1.25.4.
go run ./cmd/serverIt listens on :8080 by default, overridable via environment variable:
PORT=25500 go run ./cmd/serverAPI authentication is disabled by default, so the conversion capability can be opened
directly to callers. Configure it explicitly when you need to protect /sub:
FUCKSUB_API_TOKEN=your-token go run ./cmd/serverOnce enabled, /sub accepts X-Fucksub-Token: your-token,
Authorization: Bearer your-token, or a token=your-token query parameter for
subscription clients that cannot send custom headers. Header tokens take priority.
/stats remains header-only. /health and the built-in web page do not require a
token.
The built-in access log records only method, path, status, latency, and client IP.
It does not log URL query strings, subscription URLs, token, sig, basic-auth
credentials, or inline data: payloads. Runtime request deadlines are enabled by
default:
FUCKSUB_REQUEST_TIMEOUT=60s \
FUCKSUB_PROFILE_UPDATE_INTERVAL=24h \
go run ./cmd/serverFUCKSUB_REQUEST_TIMEOUT accepts Go durations (60s, 2m) or plain seconds.
FUCKSUB_PROFILE_UPDATE_INTERVAL controls the fallback Profile-Update-Interval
response header when the request does not provide an interval parameter.
Optional server-side node insertion is configured at the service boundary:
FUCKSUB_INSERT_URL='https://example.com/ops-sub|data:,ss://...' \
FUCKSUB_PREPEND_INSERT=true \
go run ./cmd/serverWhen callers pass insert=true, configured insert sources are merged with user
subscriptions before filtering, rename, emoji, and dedupe. Insert source failures
return insert_fetch_failed warnings and do not block the user's subscription.
Generated profiles route HTTP(S) remote rulesets for all targets through this
service's /getruleset endpoint. Clash/Mihomo currently receives the original
provider payload, while Surge and Quantumult X receive target-specific text rulesets
converted on demand. This keeps profile output consistent and leaves one place for
future target-specific ruleset behavior. The link prefix is derived from the request
Host by default (which may be an IP); to pin a stable public address (a domain, a
reverse-proxy entry, ...) set FUCKSUB_MANAGED_CONFIG_PREFIX, the equivalent of
subconverter's managed_config_prefix:
FUCKSUB_MANAGED_CONFIG_PREFIX=https://example.com go run ./cmd/serverNote: this prefix is written verbatim into generated profiles (including any
basic-auth credentials in it) and must be reachable by clients. Do not hardcode a
credentialed URL into source or a public repo — inject it through this environment
variable at deploy time. /getruleset does not require the FUCKSUB_API_TOKEN
header, because client applications fetch it after importing the generated profile;
instead, generated managed ruleset URLs carry a sig query parameter when a signing
key is available.
Managed ruleset signing:
FUCKSUB_API_TOKEN=your-token \
FUCKSUB_RULESET_SIGNATURE=enforce \
go run ./cmd/serverFUCKSUB_RULESET_SIGNING_KEY is used first when set. If it is not set but
FUCKSUB_API_TOKEN is configured, fucksub derives a stable ruleset signing key from
the API token, so deployments that protect /sub automatically protect
/getruleset against unsigned arbitrary fetches. FUCKSUB_RULESET_SIGNATURE accepts
enforce, warn, or off; with a key present the default is enforce. With no key
source, signing is forced off and startup logs a warning that /getruleset is open.
/getruleset accepts text rulesets, Clash YAML provider payloads (payload:), and
Clash/Mihomo MRS files. Full Clash profiles (proxies:, rule-providers:, rules:)
are rejected as ruleset sources instead of being guessed. Surge/Quantumult X renderers
drop unsupported external-ruleset rule types with X-Fucksub-Warnings that include
the dropped type and samples.
Optional managed-ruleset runtime knobs:
FUCKSUB_RULESET_CACHE_TTL=5m \
FUCKSUB_RULESET_CACHE_MAX_ENTRIES=1024 \
FUCKSUB_RULESET_CACHE_MAX_BODY=16777216 \
FUCKSUB_RULESET_RATE_PER_MINUTE=600 \
FUCKSUB_RULESET_RATE_BURST=120 \
FUCKSUB_RULESET_CLIENT_RATE_PER_MINUTE=120 \
FUCKSUB_RULESET_CLIENT_RATE_BURST=30 \
FUCKSUB_RULESET_UPSTREAM_MIN_INTERVAL=100ms \
FUCKSUB_RULESET_UPSTREAM_MAX_CONCURRENT=4 \
FUCKSUB_GEOSITE_MAP='openai=https://example.com/openai.list;apple=https://example.com/apple.list' \
FUCKSUB_GEOSITE_MAX_RULES=10000 \
go run ./cmd/serverSubscription/config fetches go through a bounded in-memory cache with
stale-on-error fallback: within FUCKSUB_SUB_CACHE_TTL a cached body is served
directly; after that the next request refreshes upstream, and if the refresh
fails a previously good copy is served for up to FUCKSUB_SUB_CACHE_STALE_TTL
more (a failed upstream is then retried at most every 30s instead of per
request). Concurrent requests for the same URL share one upstream fetch, and
transport errors / gateway 5xx get a single automatic retry.
FUCKSUB_SUB_CACHE_TTL=5m \
FUCKSUB_SUB_CACHE_STALE_TTL=6h \
FUCKSUB_SUB_CACHE_MAX_ENTRIES=256 \
FUCKSUB_SUB_CACHE_MAX_BYTES=134217728 \
go run ./cmd/serverFUCKSUB_SUB_CACHE_TTL<=0 disables the cache entirely;
FUCKSUB_SUB_CACHE_STALE_TTL<=0 disables only the stale-on-error window.
/sub has no rejecting rate limit by default. Deployments that need server-side
backpressure can enable a global bucket, a per-client bucket, or both:
FUCKSUB_SUB_RATE_PER_MINUTE=120 \
FUCKSUB_SUB_RATE_BURST=30 \
FUCKSUB_SUB_CLIENT_RATE_PER_MINUTE=30 \
FUCKSUB_SUB_CLIENT_RATE_BURST=10 \
FUCKSUB_SUB_MAX_CLIENT_BUCKETS=1024 \
go run ./cmd/serverWhen enabled, rejected /sub requests return HTTP 429 and increment
security.subscriptionRateLimited in /stats.
GEOSITE,<tag> is native for Clash/Mihomo. For Surge and Quantumult X it is expanded
only when FUCKSUB_GEOSITE_MAP provides an explicit tag-to-ruleset URL; otherwise the
rule is dropped with unsupported_geosite instead of silently disappearing.
Quantumult X Hysteria2 is intentionally skipped with a warning until its public server
syntax is confirmed; emitting an unknown hysteria2= line can make the whole imported
profile unusable.
Successful /sub responses pass through valid upstream Subscription-Userinfo.
With multiple subscription URLs, upload, download, and total are summed and
expire uses the earliest non-zero value. Add append_info=false to suppress this
header. Successful responses also include Profile-Update-Interval; request
interval is interpreted as seconds and rendered as client-compatible hours.
Runtime counters are exposed as JSON at /stats and grouped by purpose:
request (totals by operation and target/status), subscription.cache (fetch cache
hit/miss/bypass/stale/singleflight/eviction/size), ruleset.fetch,
ruleset.renderCache, ruleset.dropped, warnings, and security (signature
failures plus ruleset/subscription rate-limit rejects).
Health check:
curl http://127.0.0.1:8080/healthConvert a subscription:
curl -G 'http://127.0.0.1:8080/sub' \
--data-urlencode 'target=clash' \
--data-urlencode 'url=https://example.com/subscription'Generate Quantumult X:
curl -G 'http://127.0.0.1:8080/sub' \
--data-urlencode 'target=quanx' \
--data-urlencode 'url=https://example.com/subscription'Generate Surge:
curl -G 'http://127.0.0.1:8080/sub' \
--data-urlencode 'target=surge' \
--data-urlencode 'url=https://example.com/subscription'Open / in a browser to use the built-in web page that builds subscription links.
GET /sub?target=%TARGET%&url=%URL%&config=%CONFIG%
| Parameter | Required | Description |
|---|---|---|
target |
Yes | Output target: clash, quanx, surge. |
url |
Yes | Subscription address; supports http, https, data; multiple subscriptions joined with ` |
config |
No | External config address; supports http, https, data. |
filename |
No | Download filename; the server strips control characters and path separators. |
probe |
No | When set to true, returns only a JSON probe result instead of the config body. |
token |
No | /sub query-token fallback when FUCKSUB_API_TOKEN is enabled. Not accepted by /stats. Because token is a known query key, an unencoded upstream URL containing &token= is split at that point — percent-encode the url value (& as %26) to keep an upstream token parameter intact. |
append_info |
No | Defaults to true. Set append_info=false to suppress Subscription-Userinfo. |
interval |
No | Subscription update interval in seconds; response header is emitted in hours. |
Compatibility toggle parameters:
| Parameter | Description |
|---|---|
list |
Emit node-related content only. Clash drops groups/rules; Surge emits only [Proxy]; QuanX emits only the local node section. |
tfo |
Emit TCP Fast Open related fields for mappable targets. |
udp |
Emit UDP related fields for mappable targets. |
scv / skip-cert-verify |
Apply global certificate verification skipping to nodes that do not already declare it. |
sort |
Sort by node name. |
add_emoji / remove_old_emoji |
Add or strip emoji in node names. |
rename |
Rename nodes, formatted as match@replace. |
include_remarks / exclude_remarks |
Keep or exclude by node-name regex. |
fdn |
Add built-in filters for expired/traffic-info style nodes. |
expand |
Inline remote rulesets into the generated profile when possible; per-ruleset failures fall back to remote references with expand_ruleset_failed. |
insert |
Merge server-configured insert sources when FUCKSUB_INSERT_URL is configured; otherwise returns unsupported_insert. |
new_name |
Kept as a known compatibility key only. new_name=false returns unsupported_new_name; output always uses Mihomo field names. |
curl -G 'http://127.0.0.1:8080/sub' \
--data-urlencode 'target=clash' \
--data-urlencode 'url=https://example.com/subscription' \
--data-urlencode 'probe=true'Sample response:
{
"target": "clash",
"sourceCount": 1,
"nodeCount": 12,
"warnings": []
}config currently supports the common [custom] fields from subconverter-style external
config. When a file has no explicit [custom] section, top-level config lines are treated
as [custom].
| Option | Status | Description |
|---|---|---|
custom_proxy_group |
Supported | Parses proxy groups and maps them to Mihomo/Surge/QuanX per target. |
ruleset |
Supported | Parses rules and remote rulesets; warns when the target cannot express them. |
enable_rule_generator |
Supported | Controls whether rules are generated. |
overwrite_original_rules |
Supported | Controls rule overwrite semantics. |
clash_rule_base |
Supported | Only effective for target=clash, used as the base YAML. |
surge_rule_base |
Supported | Only effective for target=surge, merged by profile section before generated sections are written. Generated [Proxy], [Proxy Group], and [Rule] sections replace same-named base sections, so base [Rule] entries do not participate in overwrite_original_rules. |
quanx_rule_base |
Supported | Only effective for target=quanx, merged by profile module before generated modules are written. Generated modules replace same-named base modules. |
rename |
Supported | Node-name regex replacement. |
emoji |
Supported | Node-name emoji rules. |
add_emoji / remove_old_emoji |
Supported | Controls emoji adding and stripping. |
include_remarks / exclude_remarks |
Supported | Node filtering. |
!!import |
Not yet supported | Returns an unsupported_config_directive warning. |
When the conversion encounters degradation, skips, unsupported constructs, or config risks, fucksub tries to keep producing a usable result and surfaces warnings.
- Successful response: if warnings exist, the response includes the
X-Fucksub-Warningsheader. - Failed response: if warnings were already identified before the failure, the response
still includes the
X-Fucksub-Warningsheader. probe=true: warnings appear in thewarningsfield of the JSON response.
- The conversion capability is open to callers by default and requires no token.
- If your deployment needs to protect
/sub, setFUCKSUB_API_TOKEN. Browser/probe flows should use a request header; copied subscription links may usetoken=query for clients that cannot send headers. - For internet-facing deployments, configure basic rate limiting, an access-log retention policy, and abnormal-request alerting at the reverse-proxy layer.
- Setting
FUCKSUB_ALLOW_PRIVATE_NETWORKS=trueon a public service is not recommended; that switch is only for private deployments that explicitly need to reach intranet subscription sources. - Access logs record only method/path/status/latency/client IP. Conversion logs record target, source/node counts, warning codes, status, and latency. Neither log includes the full subscription URL or query string.
Copy-and-run samples are in examples/.
SubConverter-Extended solved the original project's slow adoption of newer protocols such as AnyTLS, but it still inherited a large body of legacy implementation and a complex compatibility surface. For a project whose maintenance has stalled, whose issues go unfixed, and whose repository can no longer be relied on, piling on patches makes its behavior increasingly unpredictable.
fucksub chose a rewrite:
- Unify the HTTP service, parsing, conversion, validation, and testing in Go.
- Keep only the three client targets that actually need to be delivered right now.
- Use a Mihomo adapter for Clash/Mihomo protocol parsing and config validation.
- Use explicit mappers for Surge/Quantumult X, warning whenever something cannot be expressed safely.
- Pin behavior with a compatibility matrix and a golden corpus to reduce the hidden risk of output that "looks convertible but is actually unusable".
The original subconverter has many targets, but the more targets there are, the easier it
is for protocols, fields, and rule semantics to masquerade as one another. fucksub
currently prioritizes keeping the behavior of clash, quanx, and surge explainable,
testable, and maintainable.
Requires Go 1.25.4. Recommended to run before committing:
zsh -ic 'goup <<< 4; GO111MODULE=on gofmt -l .'
zsh -ic 'goup <<< 4; GO111MODULE=on go test ./...'
zsh -ic 'goup <<< 4; GO111MODULE=on go vet ./...'