Empty/missing jls-upstream caused whole config reload to fail and left
new listeners stuck as config_not_applied. Autofill defaults and ensure
addr/sni when compiling shadowquic listeners.
yaml:"-" dropped the subscription dial flags on save so UI switches
reset after refresh. Store as client-tfo/client-mptcp and strip them
when merging the visual fragment into the serving Mihomo YAML.
- Remove mode from Config general settings UI
- Client proxies only emit wiki tfo/mptcp booleans; strip inbound-* and
other server-only keys from subscription YAML
- OpenAPI SSL engine notes match current acmez stack
* feat(proxy): expose per-proxy tfo / mptcp (TCP Fast Open / Multipath TCP)
Add Mihomo's proxy-level `tfo` and `mptcp` options (wiki.metacubex.one/config/proxies)
to the panel so clients can opt into TCP Fast Open / Multipath TCP on a per-node basis.
Mapping accuracy:
- These are PROXY-level keys, distinct from the global `inbound-tfo` / `inbound-mptcp`
general switches. They travel through ProxyEntry.Options (the same catch-all that already
feeds both the serving config and the client subscription document), so they surface in
BOTH the server mihomo config and the generated client subscription config — no field is
lost or misrouted.
Frontend (web + mobile share the same Config page / responsive Modal):
- Proxy editor gains TFO (TCP Fast Open) and MPTCP (Multipath TCP) switches.
- An info note states they only take effect on TCP transports.
- Modal body becomes scrollable on phones (maxHeight 78vh) so the added rows stay reachable
on small screens; width/top already adapt via useIsMobile.
i18n: zh-CN / en strings for proxyTfo, proxyMptcp, proxyTcpOnly.
Tests:
- config: tfo/mptcp serialize into proxy YAML and round-trip; absent stays absent, explicit
false is preserved.
- converter: client subscription document preserves tfo/mptcp from visual proxies.
* fix(i18n): add missing commas after proxy TFO/MPTCP keys
---------
Co-authored-by: 3m-ui dev <dev@local>
Co-authored-by: kazeyukiro <kazeyukiro@users.noreply.github.com>
* feat(config): add inbound-tfo / inbound-mptcp global kernel settings
Expose Mihomo's top-level `inbound-tfo` and `inbound-mptcp` general keys
(TCP Fast Open / Multipath TCP applied to every inbound listener socket) in
the panel. These are GLOBAL switches, not per-listener fields, so they are
carried on VisualConfig — the same struct that feeds the serving Mihomo
config fragment — and never copied into client subscription documents.
Backend:
- VisualConfig gains InboundTfo / InboundMPTCP (json inboundTfo/inboundMptcp,
yaml inbound-tfo/inbound-mptcp), matching Mihomo's exact key names.
- MihomoConfig gains the same keys (omitempty) for schema completeness.
- GenerateFinalConfig already merges the visual-config fragment over the
template (only listeners/rules/proxies/groups are stripped), so the keys
surface in the serving config exactly as Mihomo expects.
Frontend:
- Config page gains a 'General' card (mode, log level, allow-lan, ipv6,
inbound-tfo, inbound-mptcp) loaded from /config/visual and saved via
POST /config/visual. The full VisualConfig is preserved on save so existing
proxies/groups/rules/DNS are not clobbered.
- i18n zh-CN / en strings added.
Tests:
- config package tests assert inbound-tfo / inbound-mptcp appear as `true`
in GenerateFinalConfig output when enabled and never become `true` when off.
* fix: gofmt visual.go; type saveVisualConfig payload as VisualConfig
---------
Co-authored-by: 3m-ui dev <dev@local>
Co-authored-by: kazeyukiro <kazeyukiro@users.noreply.github.com>
PR #91 made the creation-time guards (ensureEndpointAvailable,
ensureBatchEndpointsAvailable) transport-aware so a TCP listener and a UDP
listener can share an address:port. But the third copy of the conflict check
inside Mihomo config generation — validateListenerEndpoints in
internal/mihomo/config/generator.go — was left untouched. So a TCP+UDP pair
that creation now permits was still rejected when the panel regenerated the
Mihomo config, surfacing as:
warning: Mihomo config generate after listener change failed: ...
listeners "节点2" and "节点5" have conflicting bind address/port ranges
(0.0.0.0:16201 and 0.0.0.0:16201)
Make validateListenerEndpoints reuse protocol.ListenerTransports and only flag
a conflict when the two listeners share an L4 transport, matching the
creation-time guard. Same-protocol pairs on the same address:port still
conflict.
Tests:
- TestValidateListenerEndpointsAllowsTCPandUDPOnSamePort
- TestValidateListenerEndpointsRejectsTwoTCPOnSamePort
- TestValidateListenerEndpointsRejectsTwoUDPOnSamePort
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
The in-app "Download backup" button exported the SQLite file with a plain
`os.Open` + `io.Copy` while the panel's GORM pool was still open and writing.
That copy races with concurrent writes (torn pages) and, even after the prior
WAL checkpoint, could leave committed frames in -wal that the copy never
replayed — an inconsistent or partially-lost backup.
Replace the checkpoint + file copy with SQLite's online backup primitive
VACUUM INTO, which reads through the WAL and in-flight transactions to produce
a self-contained, fully-replayed snapshot without stopping the panel.
Also stop bundling 3m-ui.db-wal / 3m-ui.db-shm into the zip. The VACUUM INTO
result is already complete (WAL merged in), so the sidecars were dead weight —
and they were never restored on import anyway, which silently dropped
un-checkpointed writes on restore.
Tests:
- TestWriteZipSnapshotIsConsistentAndSelfContained: a WAL-mode db with 200 rows
held open by a live connection; WriteZip produces a zip whose 3m-ui.db opens
standalone with all 200 rows and contains no -wal/-shm entries.
- go vet + go test -tags sqlite_modernc ./... green.
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
The endpoint conflict guard in ensureEndpointAvailable (and its batch
counterpart ensureBatchEndpointsAvailable) compared only port ranges and
bind addresses — it never looked at the L4 protocol. A TCP listener on
443 and a UDP listener on 443 are independent sockets at the OS level and
Mihomo serves both fine, but 3m-ui rejected the second one with
"conflicts on 0.0.0.0:443". So you could not run e.g. VLESS (TCP) and
TUIC/Hysteria2 (UDP) on the same port.
Add protocol.ListenerTransports, which maps a listener's protocol kind to
the L4 transport(s) its inbound binds (UDP for tuic / hysteria /
hysteria2 / shadowquic / mieru; TCP for everything else). The proxy `udp`
toggle is UDP relay over an established connection and is intentionally
ignored — it does not open a UDP listening socket. The conflict check now
also requires the two listeners to share an L4 transport, so a TCP+UDP
pair on the same address:port no longer collides while two TCP or two UDP
listeners still do.
Tests:
- protocol.ListenerTransports unit tests (udp/tcp/case-insensitive).
- listener.listenersShareTransport unit tests.
- End-to-end: a TCP+UDP pair on 443 coexists and the UDP listener stays on
443 through Create (it is not silently reassigned to a free port), while
two TCP or two UDP on 443 still conflict.
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
Replace golang.org/x/crypto/acme/autocert domain HTTP-01 with
domainHTTPIssuer (acmez + scheduled renew). DNS-01 and IP already used
acmez. PEM cache: http-<domain>.crt/.key; auto-renew ~30d before expiry.
No panel-hosted rule-sets. Remote MetaCubeX geosite (cn/apple@cn/…) and
cn_ip via cdn.jsdelivr.net; DNS Ali for CN, Google DoH via proxy otherwise.
Remove embedded .srs and /client/rule-set routes.
Embed geosite-cn.srs / geoip-cn.srs and serve them at
/api/v1/client/rule-set/*.srs on the same host as the subscription.
Subscription stays small for SFI while domain+IP CN coverage is complete.
Remove multi-MB inline domain/IP dumps that broke mobile clients.
Embed dnsmasq-china-list accelerated-domains (~110k suffixes) plus
existing CN IP CIDRs so domestic domains/IPs go direct without remote
rule-set downloads that break client startup.
GitHub raw geosite/geoip downloads fail on many networks and prevent
SFI/SFM from starting. Use private IP + offline CN domain_suffix list
for direct, final still proxy.
sing-box subscriptions now route geosite-cn + geoip-cn (remote rule-sets)
and private IPs to direct, matching Clash CN defaults. HY2 share query
params are centralized: always sni/alpn=h3, insecure+allowInsecure when
skip-cert, optional mport for hop — aligned with official URI scheme and
Loon parsers.
GenerateUserSingboxSubscription only used listenerToProxies and dropped
nodes on export errors; it also ignored remote mirrors and external
links (unlike Clash/v2ray). Add Share ClientYAML recovery (same export
TLS path), merge mirrors/external proxies, unique outbound tags, richer
port parsing, and HY2 auth/password fallback.
- ApplyToConfig no longer overwrites Reality camouflage SNI with
cert-derived IP; Reality always skip-cert
- ResolveClientSNI: IP dial target without PEM uses IP SNI (not AccessSNI
domain like www.bing.com which forced broken verify)
- VLESS/VMess UUID from username when uuid field empty
- Credential rows always populate uuid for panel UUID-in-username users
ClientURIsWithCredentials no longer writes an empty users map over
Config (this dropped HY2/AnyTLS/Mieru passwords and emptied subscriptions).
HY2 URI export accepts password/auth when users is absent. Subscription
may still recover via ExportShare* only when primary fails, and Share
already resolves SNI via export.BuildProfile — not a divergent policy.
v2ray base64 and Clash/Mihomo YAML subscriptions only use
ClientURIsWithCredentials / listenerToProxies (export.BuildProfile).
Remove ExportShare* fallbacks that could emit a different sni/insecure
without the operator noticing.
Introduce internal/export as the single cert/SNI/skip-cert resolver.
ClientURIs, DecodeNodeModel, resolveShareTLS and Clash proxy TLS helpers
all consume BuildProfileFromConfig so formal IP certs always emit sni and
subscriptions no longer diverge from share links on certificate identity.
DecodeNodeModel now sets listener ID and materializes certificate paths
or certstore PEM. resolveShareTLS expands paths and never leaves SNI
empty on IP hosts. HY2 share/URI default alpn=h3 and force sni=host
when unresolved so Loon no longer sees bare host:port links.