diff --git a/backend/internal/converter/client.go b/backend/internal/converter/client.go index 20b481d..a4d396f 100644 --- a/backend/internal/converter/client.go +++ b/backend/internal/converter/client.go @@ -186,7 +186,9 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred p["password"] = value } if value, ok := opts["simple-obfs"].(map[string]interface{}); ok && boolValue(value["enable"]) { - p["plugin"] = "simple-obfs" + // The listener config key is `simple-obfs`, but the mihomo SS + // client proxy doc (proxies/ss.md) names the plugin `obfs`. + p["plugin"] = "obfs" p["plugin-opts"] = value } applySSPluginWrappers(p, opts) @@ -199,6 +201,10 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred if value, ok := opts["obfs-opts"]; ok { p["obfs-opts"] = value } + // Translate TLS-like listener wrappers (shadow-tls/res-tls/jls-config) + // into the snell `obfs-opts` format documented in proxies/snell.md. + // Skipped when an explicit `obfs-opts` block is already present. + applySnellObfsOpts(p, opts) result = append(result, p) case "vmess", "vless": if len(credentials) == 0 { @@ -223,7 +229,10 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred copyOption(p, opts, "authenticated-length") // encryption (client) and decryption (server) are a generated pair from // `mihomo generate vless-x25519` / `vless-mlkem768` — never copy decryption→encryption. - copyOption(p, opts, "encryption") + // VLESS has an `encryption` field (xtls-rprx-vision etc.); VMess does not. + if protocol == "vless" { + copyOption(p, opts, "encryption") + } if flow := userFieldFromOpts(opts, cred.UUID, "flow"); flow != nil { p["flow"] = flow } else { @@ -266,7 +275,7 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred for _, key := range []string{ "up", "down", "obfs", "obfs-password", "bbr-profile", "realm-opts", "alpn", "sni", "servername", "skip-cert-verify", "name-cert-verify", - "fingerprint", + "fingerprint", "handshake-timeout", "max-idle-time", } { copyOption(p, opts, key) } @@ -287,7 +296,7 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred if token, ok := opts["token"]; ok { p := makeProxy("") p["token"] = token - for _, key := range []string{"congestion-controller", "bbr-profile", "max-idle-time", "authentication-timeout", "alpn", "max-udp-relay-packet-size"} { + for _, key := range []string{"congestion-controller", "bbr-profile", "max-idle-time", "authentication-timeout", "alpn", "max-udp-relay-packet-size", "sni", "skip-cert-verify"} { copyOption(p, opts, key) } result = append(result, p) @@ -299,7 +308,7 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred p := makeProxy(fmt.Sprintf("%d", i+1)) p["uuid"] = cred.UUID p["password"] = cred.Password - for _, key := range []string{"congestion-controller", "bbr-profile", "max-idle-time", "authentication-timeout", "alpn", "max-udp-relay-packet-size"} { + for _, key := range []string{"congestion-controller", "bbr-profile", "max-idle-time", "authentication-timeout", "alpn", "max-udp-relay-packet-size", "sni", "skip-cert-verify"} { copyOption(p, opts, key) } result = append(result, p) @@ -340,15 +349,13 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred if p["alpn"] == nil { p["alpn"] = []string{"h3"} } - if p["quic-versions"] == nil { - p["quic-versions"] = []string{"v2"} - } - if p["congestion-controller"] == nil { - p["congestion-controller"] = "bbr" - } - if p["zero-rtt"] == nil { - p["zero-rtt"] = true - } + // quic-versions / congestion-controller / zero-rtt are NOT defaulted + // here: the official docs (proxies/shadowquic.md) specify defaults of + // v1 / cubic / false, which mihomo applies automatically when these + // fields are absent. Inventing conflicting defaults (v2/bbr/true) + // would override the listener's intent and break connectivity with + // servers that don't support them. They pass through via copyOption + // above only when present in the listener config. result = append(result, p) } case "anytls": @@ -543,6 +550,89 @@ func ssClientFingerprint(src map[string]interface{}) string { return "chrome" } +// applySnellObfsOpts translates a Snell listener's TLS-like wrapper blocks +// (shadow-tls/res-tls/jls-config) into the snell `obfs-opts` format documented +// in proxies/snell.md. Snell carries these wrappers as obfs-opts (with a +// `mode` field) rather than the `plugin`/`plugin-opts` form used by SS. +// +// Only the first matching enabled wrapper is emitted (they are mutually +// exclusive on a single Snell listener). When the listener config already +// provides an explicit `obfs-opts` block it is left untouched. +func applySnellObfsOpts(p map[string]interface{}, src map[string]interface{}) { + if _, ok := p["obfs-opts"]; ok { + return + } + if stls, ok := src["shadow-tls"].(map[string]interface{}); ok { + if enable, _ := stls["enable"].(bool); enable { + opts := map[string]interface{}{"mode": "shadow-tls"} + if v, ok := stls["version"]; ok { + opts["version"] = v + } + // Password: v2 uses the top-level password; v3 uses users[0].password. + if pwd, ok := stls["password"].(string); ok && pwd != "" { + opts["password"] = pwd + } else if users, ok := stls["users"].([]interface{}); ok && len(users) > 0 { + if u, ok := users[0].(map[string]interface{}); ok { + if pwd, ok := u["password"].(string); ok { + opts["password"] = pwd + } + } + } + if alpn, ok := stls["alpn"]; ok { + opts["alpn"] = alpn + } + // host from handshake.dest (strip :port). + if hs, ok := stls["handshake"].(map[string]interface{}); ok { + if dest, ok := hs["dest"].(string); ok { + opts["host"] = stripHostPort(dest) + } + } + p["obfs-opts"] = opts + return + } + } + if restls, ok := src["res-tls"].(map[string]interface{}); ok { + if enable, _ := restls["enable"].(bool); enable { + opts := map[string]interface{}{ + "mode": "restls", + "version-hint": "tls13", + } + if dest, ok := restls["dest"].(string); ok { + opts["host"] = stripHostPort(dest) + } + if pwd, ok := restls["password"].(string); ok { + opts["password"] = pwd + } + if script, ok := restls["restls-script"].(string); ok && script != "" { + opts["restls-script"] = script + } + p["obfs-opts"] = opts + return + } + } + if jls, ok := src["jls-config"].(map[string]interface{}); ok { + if enable, _ := jls["enable"].(bool); enable { + opts := map[string]interface{}{"mode": "jls"} + if dest, ok := jls["dest"].(string); ok { + opts["host"] = stripHostPort(dest) + } + // JLS users: use first user's username+password (single-user case). + if users, ok := jls["users"].([]interface{}); ok && len(users) > 0 { + if u, ok := users[0].(map[string]interface{}); ok { + if un, ok := u["username"].(string); ok { + opts["username"] = un + } + if pwd, ok := u["password"].(string); ok { + opts["password"] = pwd + } + } + } + p["obfs-opts"] = opts + return + } + } +} + // stripHostPort removes the :port suffix from a host:port string, returning // the bare hostname. If the input has no port, it is returned unchanged. func stripHostPort(addr string) string { @@ -696,8 +786,6 @@ func realityClientOptions(src map[string]interface{}) map[string]interface{} { } if v, ok := cfg["support-x25519mlkem768"]; ok { result["support-x25519mlkem768"] = v - } else { - result["support-x25519mlkem768"] = true } return result } diff --git a/backend/internal/converter/client_reality_test.go b/backend/internal/converter/client_reality_test.go index aefcc2c..c5c1c3b 100644 --- a/backend/internal/converter/client_reality_test.go +++ b/backend/internal/converter/client_reality_test.go @@ -2,6 +2,7 @@ package converter import ( "encoding/base64" + "fmt" "strings" "testing" @@ -85,7 +86,19 @@ func TestShadowQUICClientExport(t *testing.T) { t.Fatal(err) } p := proxies[0] - if p["username"] != "u" || p["password"] != "p" || p["sni"] != "test.com" || p["udp"] != true || p["zero-rtt"] != true { - t.Fatalf("%#v", p) + if p["username"] != "u" || p["password"] != "p" || p["sni"] != "test.com" || p["udp"] != true { + t.Fatalf("base fields mismatch: %#v", p) + } + // quic-versions / congestion-controller must pass through unchanged. + if p["quic-versions"] == nil || fmt.Sprint(p["quic-versions"]) != "[v2]" { + t.Fatalf("quic-versions not passed through: %#v", p["quic-versions"]) + } + if p["congestion-controller"] != "bbr" { + t.Fatalf("congestion-controller not passed through: %#v", p["congestion-controller"]) + } + // zero-rtt is absent from the listener config; the converter must NOT + // invent a default (official docs default is false, applied by mihomo). + if _, ok := p["zero-rtt"]; ok { + t.Fatalf("zero-rtt should not be defaulted when absent from listener config: %#v", p["zero-rtt"]) } } diff --git a/backend/internal/node/uri_builders.go b/backend/internal/node/uri_builders.go index 0c7ef75..e716512 100644 --- a/backend/internal/node/uri_builders.go +++ b/backend/internal/node/uri_builders.go @@ -323,7 +323,7 @@ func tuicURIs(name, host, port string, cfg map[string]interface{}) ([]string, er continue } params := map[string]string{} - for key, out := range map[string]string{"congestion-controller": "congestion_control", "bbr-profile": "bbr_profile", "udp-relay-mode": "udp_relay_mode"} { + for key, out := range map[string]string{"congestion-controller": "congestion-controller", "bbr-profile": "bbr-profile", "udp-relay-mode": "udp-relay-mode"} { if v, ok := cfg[key].(string); ok && v != "" { params[out] = v } @@ -354,7 +354,7 @@ func tuicURIs(name, host, port string, cfg map[string]interface{}) ([]string, er return nil, fmt.Errorf("tuic user %q has empty password", uuid) } params := map[string]string{} - for key, out := range map[string]string{"congestion-controller": "congestion_control", "bbr-profile": "bbr_profile", "udp-relay-mode": "udp_relay_mode", "max-udp-relay-packet-size": "max_udp_relay_packet_size"} { + for key, out := range map[string]string{"congestion-controller": "congestion-controller", "bbr-profile": "bbr-profile", "udp-relay-mode": "udp-relay-mode", "max-udp-relay-packet-size": "max-udp-relay-packet-size"} { if v, ok := cfg[key].(string); ok && v != "" { params[out] = v } diff --git a/backend/internal/router/subscription.go b/backend/internal/router/subscription.go index 258b8a6..69318a1 100644 --- a/backend/internal/router/subscription.go +++ b/backend/internal/router/subscription.go @@ -3,6 +3,7 @@ package router import ( "encoding/base64" "errors" + "log" "net/http" "net/url" "strconv" @@ -79,7 +80,8 @@ func subscriptionHandler(db *gorm.DB, cfg *config.Config) gin.HandlerFunc { if target == "v2ray" || target == "base64" || target == "raw" || target == "uri" { raw, err = converter.GenerateUserBase64Subscription(db, pu, c.Request, node.ClientURIsWithCredentials) if err != nil { - c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) + log.Printf("subscription error: %v", err) + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "subscription generation failed"}) return } page := subpage.LoadPageSettings(db) @@ -104,7 +106,8 @@ func subscriptionHandler(db *gorm.DB, cfg *config.Config) gin.HandlerFunc { if target == "singbox" || target == "sing-box" || target == "sfa" || target == "sfm" { raw, err = converter.GenerateUserSingboxSubscription(db, pu, c.Request) if err != nil { - c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) + log.Printf("subscription error: %v", err) + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "subscription generation failed"}) return } writeSubHeaders(c, db, &pu) @@ -119,7 +122,8 @@ func subscriptionHandler(db *gorm.DB, cfg *config.Config) gin.HandlerFunc { return } if err != nil { - c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) + log.Printf("subscription error: %v", err) + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "subscription generation failed"}) return } @@ -365,7 +369,8 @@ func writeSubHTML(c *gin.Context, db *gorm.DB, cfg *config.Config, pu models.Pro } html, err := subpage.RenderHTML(db, pu, base, links) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + log.Printf("subscription error: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "subscription generation failed"}) return } c.Header("Cache-Control", "no-store") diff --git a/frontend/src/components/ListenerConfigFields.tsx b/frontend/src/components/ListenerConfigFields.tsx index 1aaeeeb..e5a1ea7 100644 --- a/frontend/src/components/ListenerConfigFields.tsx +++ b/frontend/src/components/ListenerConfigFields.tsx @@ -250,8 +250,21 @@ export function configToFormValues(raw: string | undefined | null): Record): Record | undefined { @@ -435,6 +450,10 @@ export function formValuesToConfig( set('cwnd', values.cwnd); set('bbr-profile', values['bbr-profile']); set('quic-versions', values['quic-versions']); + set('max-datagram-frame-size', values['max-datagram-frame-size']); + set('recv-window-conn', values['recv-window-conn']); + set('recv-window', values['recv-window']); + if (values['disable-mtu-discovery'] === true) cfg['disable-mtu-discovery'] = true; break; case 'anytls': set('padding-scheme', values['padding-scheme']); @@ -450,8 +469,18 @@ export function formValuesToConfig( set('padding-min', values['padding-min']); set('padding-max', values['padding-max']); set('table-type', values['table-type']); + set('custom-table', values['custom-table']); + set('custom-tables', values['custom-tables']); + set('fallback', values['fallback']); set('handshake-timeout', values['handshake-timeout']); if (values['enable-pure-downlink'] === true) cfg['enable-pure-downlink'] = true; + if (values.httpmask_enabled) { + cfg['httpmask'] = cleanObj({ + disable: values.httpmask_disable === true ? true : undefined, + mode: values.httpmask_mode, + 'path-root': values.httpmask_path_root, + }) || {}; + } break; case 'trusttunnel': set('network', values.network); @@ -766,8 +795,8 @@ const ListenerConfigFields: React.FC = ({ protocol }) => { { const d = await generateMaterial({ kind: 'password' }); form.setFieldsValue({ psk: d.password }); }}>{t('common.generate') || 'Generate'}} /> - - ({ value: v, label: String(v) }))} /> + + + + + + + + + + + + )} @@ -982,12 +1023,35 @@ const ListenerConfigFields: React.FC = ({ protocol }) => { + + + + + + + + + + + + + + + + + )} diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 755c705..7ad13c1 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -15,7 +15,7 @@ export default { sectionSecurity: 'Security', sectionTransport: 'Transport', sectionTLS: 'TLS certificate', sectionReality: 'Reality', sectionSSOption: 'Trojan SS option', cipher: 'Cipher', password: 'Password', passwordOptional: 'Password (optional)', uuid: 'UUID', privateKey: 'Private key', certificate: 'Certificate', alpn: 'ALPN', sni: 'SNI', clientFingerprint: 'Client fingerprint', allowInsecure: 'Allow insecure', muxOption: 'Mux', realityPrivateKey: 'Reality private key', realityShortID: 'Short ID', realityServerNames: 'Server names', realityDest: 'Dest', transport: 'Transport', path: 'Path', host: 'Host', serviceName: 'Service name', up: 'Up', down: 'Down', ignoreClientBandwidth: 'Ignore client bandwidth', masquerade: 'Masquerade', congestionControl: 'Congestion control', clone: 'Clone', saveTemplate: 'Save as template', versions: 'Versions', cloned: 'Cloned', templateCreated: 'Template created', instantiated: 'Listener created from template', batchDone: 'Batch operation completed', templates: 'Listener Templates', templateName: 'Template name', createdAt: 'Created at', instantiate: 'Instantiate', deleteTemplateConfirm: 'Delete this template?', templateDeleted: 'Template deleted', enableSelected: 'Enable selected', disableSelected: 'Disable selected', newPort: 'New port', version: 'Version', reason: 'Reason', diff: 'Diff', rollback: 'Rollback', rollbackConfirm: 'Rollback to this version?', rollbackDone: 'Rollback completed' , addUser: 'Add user', aeadMethod: 'AEAD method', allowInsecureHint: 'Allow insecure (debug only)', alterId: 'Alter ID', alterIdHint: 'VMess alterId, prefer 0', authenticationTimeout: 'Auth timeout', bandwidthHint: 'Bandwidth limit, e.g. 100 mbps', bbrProfile: 'BBR profile', clientAuthCert: 'Client cert', clientAuthType: 'Client auth type', congestionController: 'Congestion controller', createWizard: 'Create listener wizard', cwnd: 'CWND', decryption: 'Decryption', encryption: 'Encryption', - encryptionHint: 'Client-side VLESS encryption pair from generate output; do not reuse the decryption value', decryptionHint: 'Server-side VLESS decryption from mihomo generate vless-x25519 / vless-mlkem768', echKey: 'ECH Key', enablePureDownlink: 'Pure downlink', flow: 'Flow', flowHint: 'e.g. xtls-rprx-vision', grpcHint: 'gRPC transport', grpcServiceName: 'gRPC service name', handshakeTimeout: 'Handshake timeout', jlsDest: 'JLS dest', jlsHint: 'JLS settings', jlsProxy: 'JLS proxy', jlsSni: 'JLS SNI', jlsSniHint: 'JLS SNI hostname', jlsUpstreamAddr: 'JLS upstream', jlsUsers: 'JLS users', kcpTunConn: 'KCP connections', kcpTunCrypt: 'KCP crypt', kcpTunKey: 'KCP key', kcpTunMode: 'KCP mode', kcpTunNocomp: 'KCP no compress', kcpTunRcvwnd: 'KCP recv window', kcpTunSndwnd: 'KCP send window', maxIdleTime: 'Max idle time', maxUdpRelayPacketSize: 'Max UDP relay packet', mekyaHint: 'Mekya settings', mekyaKcpSection: 'Mekya KCP', mekyaMaxSimultaneous: 'Max simultaneous', mekyaMaxSimultaneousHint: 'Max concurrent connections', mekyaMaxWriteDuration: 'Max write duration', mekyaMaxWriteDurationHint: 'Write timeout', mekyaMaxWriteSize: 'Max write size', mekyaMaxWriteSizeHint: 'Single write limit', mekyaPacketBuffer: 'Packet buffer', mekyaPacketBufferHint: 'Packet buffer size', mkcpCongestion: 'mKCP congestion', mkcpDownlink: 'mKCP downlink', mkcpHeader: 'mKCP header', mkcpHint: 'mKCP transport', mkcpReadBuffer: 'mKCP read buffer', mkcpSeed: 'mKCP seed', mkcpUplink: 'mKCP uplink', mkcpWriteBuffer: 'mKCP write buffer', muxBrutal: 'Mux brutal', muxBrutalDown: 'Brutal down', muxBrutalUp: 'Brutal up', muxMaxConnections: 'Mux max connections', muxMaxStreams: 'Mux max streams', muxMinStreams: 'Mux min streams', muxOnlyTcp: 'Mux TCP only', muxPadding: 'Mux padding', muxProtocol: 'Mux protocol', muxStatistic: 'Mux statistics', network: 'Network', obfs: 'Obfuscation', obfsMaxPacketSize: 'Obfs max packet', obfsMinPacketSize: 'Obfs min packet', obfsOptsHost: 'Obfs host', obfsOptsMode: 'Obfs mode', obfsPassword: 'Obfs password', paddingMax: 'Padding max', paddingMin: 'Padding min', paddingScheme: 'Padding scheme', passwordOptionalHint: 'Optional; empty uses bound user password', passwordOptionalPlaceholder: 'Optional password', psk: 'PSK', quicVersions: 'QUIC versions', rateLimit: 'Rate limit', rateLimitHint: 'e.g. 10m', realityExclusiveHint: 'Reality and TLS cert are mutually exclusive', realityKeyHint: 'Server private-key. Empty=auto on save. Client pbk derived for share URIs.', realityShortId: 'Reality short ID', realmId: 'Realm ID', realmProxy: 'Realm proxy', realmServerUrl: 'Realm server URL', realmSkipCert: 'Skip cert verify', realmStun: 'STUN', realmToken: 'Realm token', resTlsDest: 'ResTLS dest', resTlsHint: 'ResTLS settings', resTlsMinRecordLen: 'Min record length', resTlsPassword: 'ResTLS password', resTlsProxy: 'ResTLS proxy', resTlsScript: 'ResTLS script', sectionJLS: 'JLS', sectionJlsUpstream: 'JLS upstream', sectionKcpTun: 'KCP Tun', sectionMekya: 'Mekya', sectionMkcp: 'mKCP', sectionMux: 'Mux', sectionRealm: 'Realm', sectionResTLS: 'ResTLS', sectionShadowTLS: 'ShadowTLS', sectionSimpleObfs: 'Simple Obfs', sectionXHTTP: 'XHTTP', shadowTlsHandshakeDest: 'Handshake dest', shadowTlsHandshakeProxy: 'Handshake proxy', shadowTlsHint: 'ShadowTLS settings', shadowTlsPassword: 'ShadowTLS password', shadowTlsPasswordHint: 'v2/v3 password', shadowTlsUsers: 'ShadowTLS users', shadowTlsVersion: 'ShadowTLS version', simpleObfsMode: 'Simple Obfs mode', snellVersion: 'Snell version', ssOptionMethod: 'SS method', ssOptionPassword: 'SS password', sudokuKey: 'Sudoku key', tableType: 'Table type', tlsPairHint: 'Certificate and private key must both be set', token: 'Token', tokenHint: 'Auth token', tokenPlaceholder: 'Enter token', trafficPattern: 'Traffic pattern', userHintMandatory: 'At least one user required', wizardBasic: 'Basics', wizardConfig: 'Protocol', wizardConfigHint: 'Fill protocol-specific fields (TLS / Reality / users, etc.)', wizardReview: 'Review', wsPath: 'WebSocket path', wsPathHint: 'e.g. /ws', xhttpHint: 'XHTTP transport', xhttpHost: 'XHTTP host', xhttpMode: 'XHTTP mode', xhttpPath: 'XHTTP path', zeroRtt: '0-RTT'}, + encryptionHint: 'Client-side VLESS encryption pair from generate output; do not reuse the decryption value', decryptionHint: 'Server-side VLESS decryption from mihomo generate vless-x25519 / vless-mlkem768', echKey: 'ECH Key', enablePureDownlink: 'Pure downlink', flow: 'Flow', flowHint: 'e.g. xtls-rprx-vision', grpcHint: 'gRPC transport', grpcServiceName: 'gRPC service name', handshakeTimeout: 'Handshake timeout', jlsDest: 'JLS dest', jlsHint: 'JLS settings', jlsProxy: 'JLS proxy', jlsSni: 'JLS SNI', jlsSniHint: 'JLS SNI hostname', jlsUpstreamAddr: 'JLS upstream', jlsUsers: 'JLS users', kcpTunConn: 'KCP connections', kcpTunCrypt: 'KCP crypt', kcpTunKey: 'KCP key', kcpTunMode: 'KCP mode', kcpTunNocomp: 'KCP no compress', kcpTunRcvwnd: 'KCP recv window', kcpTunSndwnd: 'KCP send window', maxIdleTime: 'Max idle time', maxUdpRelayPacketSize: 'Max UDP relay packet', mekyaHint: 'Mekya settings', mekyaKcpSection: 'Mekya KCP', mekyaMaxSimultaneous: 'Max simultaneous', mekyaMaxSimultaneousHint: 'Max concurrent connections', mekyaMaxWriteDuration: 'Max write duration', mekyaMaxWriteDurationHint: 'Write timeout', mekyaMaxWriteSize: 'Max write size', mekyaMaxWriteSizeHint: 'Single write limit', mekyaPacketBuffer: 'Packet buffer', mekyaPacketBufferHint: 'Packet buffer size', mkcpCongestion: 'mKCP congestion', mkcpDownlink: 'mKCP downlink', mkcpHeader: 'mKCP header', mkcpHint: 'mKCP transport', mkcpReadBuffer: 'mKCP read buffer', mkcpSeed: 'mKCP seed', mkcpUplink: 'mKCP uplink', mkcpWriteBuffer: 'mKCP write buffer', muxBrutal: 'Mux brutal', muxBrutalDown: 'Brutal down', muxBrutalUp: 'Brutal up', muxMaxConnections: 'Mux max connections', muxMaxStreams: 'Mux max streams', muxMinStreams: 'Mux min streams', muxOnlyTcp: 'Mux TCP only', muxPadding: 'Mux padding', muxProtocol: 'Mux protocol', muxStatistic: 'Mux statistics', network: 'Network', obfs: 'Obfuscation', obfsMaxPacketSize: 'Obfs max packet', obfsMinPacketSize: 'Obfs min packet', obfsOptsHost: 'Obfs host', obfsOptsMode: 'Obfs mode', obfsPassword: 'Obfs password', paddingMax: 'Padding max', paddingMin: 'Padding min', paddingScheme: 'Padding scheme', passwordOptionalHint: 'Optional; empty uses bound user password', passwordOptionalPlaceholder: 'Optional password', psk: 'PSK', quicVersions: 'QUIC versions', rateLimit: 'Rate limit', rateLimitHint: 'e.g. 10m', realityExclusiveHint: 'Reality and TLS cert are mutually exclusive', realityKeyHint: 'Server private-key. Empty=auto on save. Client pbk derived for share URIs.', realityShortId: 'Reality short ID', realmId: 'Realm ID', realmProxy: 'Realm proxy', realmServerUrl: 'Realm server URL', realmSkipCert: 'Skip cert verify', realmStun: 'STUN', realmToken: 'Realm token', resTlsDest: 'ResTLS dest', resTlsHint: 'ResTLS settings', resTlsMinRecordLen: 'Min record length', resTlsPassword: 'ResTLS password', resTlsProxy: 'ResTLS proxy', resTlsScript: 'ResTLS script', sectionJLS: 'JLS', sectionJlsUpstream: 'JLS upstream', sectionKcpTun: 'KCP Tun', sectionMekya: 'Mekya', sectionMkcp: 'mKCP', sectionMux: 'Mux', sectionRealm: 'Realm', sectionResTLS: 'ResTLS', sectionShadowTLS: 'ShadowTLS', sectionSimpleObfs: 'Simple Obfs', sectionXHTTP: 'XHTTP', shadowTlsHandshakeDest: 'Handshake dest', shadowTlsHandshakeProxy: 'Handshake proxy', shadowTlsHint: 'ShadowTLS settings', shadowTlsPassword: 'ShadowTLS password', shadowTlsPasswordHint: 'v2/v3 password', shadowTlsUsers: 'ShadowTLS users', shadowTlsVersion: 'ShadowTLS version', simpleObfsMode: 'Simple Obfs mode', snellVersion: 'Snell version', ssOptionMethod: 'SS method', ssOptionPassword: 'SS password', sudokuKey: 'Sudoku key', customTable: 'Custom table', customTables: 'Custom tables', sudokuFallback: 'Fallback', sectionHttpmask: 'HTTPMask', httpmaskDisable: 'Disable HTTP mask', httpmaskMode: 'HTTP mask mode', httpmaskPathRoot: 'HTTP path root', tableType: 'Table type', tlsPairHint: 'Certificate and private key must both be set', token: 'Token', tokenHint: 'Auth token', tokenPlaceholder: 'Enter token', trafficPattern: 'Traffic pattern', userHintMandatory: 'At least one user required', wizardBasic: 'Basics', wizardConfig: 'Protocol', wizardConfigHint: 'Fill protocol-specific fields (TLS / Reality / users, etc.)', wizardReview: 'Review', wsPath: 'WebSocket path', wsPathHint: 'e.g. /ws', xhttpHint: 'XHTTP transport', xhttpHost: 'XHTTP host', xhttpMode: 'XHTTP mode', xhttpPath: 'XHTTP path', zeroRtt: '0-RTT'}, core: { title: 'Core', status: 'Status', running: 'Running', stoppedStatus: 'Stopped', version: 'Version', pid: 'PID', uptime: 'Uptime', start: 'Start', stop: 'Stop', restart: 'Restart', started: 'Started', stopped: 'Stopped', restarted: 'Restarted' , operationFailed: 'Operation failed', unavailable: 'Core unavailable'}, logs: { title: 'Logs', empty: 'No logs', level: 'Level', timestamp: 'Time', payload: 'Payload', autoRefresh: 'Auto refresh', clear: 'Clear' }, config: { title: 'Config Engine', subtitle: 'Visual Mihomo configuration', visual: 'Visual Config', yaml: 'YAML Config', proxies: 'Proxy Configuration', yamlPreview: 'YAML Preview', addProxy: 'Add Proxy', editProxy: 'Edit Proxy', deleteProxy: 'Delete Proxy', deleteConfirm: 'Delete this proxy?', generateSuccess: 'Configuration generated successfully', validateSuccess: 'Configuration is valid', validateFailed: 'Configuration validation failed', proxyName: 'Proxy Name', proxyType: 'Proxy Type', proxyServer: 'Proxy Server', proxyPort: 'Proxy Port', proxyPassword: 'Proxy Password', proxyUUID: 'Proxy UUID' , generate: 'Generate config', validate: 'Validate config'}, diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index c37d292..c95670b 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -15,7 +15,7 @@ export default { sectionSecurity: '安全层', sectionTransport: '传输层', sectionTLS: 'TLS 证书', sectionReality: 'Reality', sectionSSOption: 'Trojan SS 混淆', cipher: '加密方法', password: '密码', passwordOptional: '密码(可选)', uuid: 'UUID', privateKey: '私钥', certificate: '证书', alpn: 'ALPN', sni: 'SNI', clientFingerprint: '客户端指纹', allowInsecure: '允许不安全', muxOption: '多路复用', realityPrivateKey: 'Reality 私钥', realityShortID: 'Short ID', realityServerNames: 'Server Names', realityDest: 'Dest', transport: '传输', path: '路径', host: 'Host', serviceName: 'Service Name', up: '上行', down: '下行', ignoreClientBandwidth: '忽略客户端带宽', masquerade: 'Masquerade', congestionControl: '拥塞控制', clone: '克隆', saveTemplate: '保存为模板', versions: '配置版本', cloned: '克隆成功', templateCreated: '模板创建成功', instantiated: '已从模板创建节点', batchDone: '批量操作完成', templates: '节点模板', templateName: '模板名称', createdAt: '创建时间', instantiate: '实例化', deleteTemplateConfirm: '确定删除此模板?', templateDeleted: '模板已删除', enableSelected: '启用选中', disableSelected: '禁用选中', newPort: '新端口', version: '版本', reason: '原因', diff: '配置差异', rollback: '回滚', rollbackConfirm: '确定回滚到此版本?', rollbackDone: '回滚完成' , addUser: '添加用户', aeadMethod: 'AEAD 加密方式', allowInsecureHint: '允许不安全连接(仅调试)', alterId: 'Alter ID', alterIdHint: 'VMess 额外 ID,建议 0', authenticationTimeout: '认证超时', bandwidthHint: '带宽限制,如 100 mbps', bbrProfile: 'BBR 配置', clientAuthCert: '客户端证书', clientAuthType: '客户端认证类型', congestionController: '拥塞控制', createWizard: '向导创建节点', cwnd: 'CWND', decryption: '解密', encryption: 'Encryption', - encryptionHint: '客户端 VLESS Encryption:与 decryption 成对,取 generate 输出的 encryption 一侧,勿直接复制 decryption', decryptionHint: '服务端 VLESS Encryption:由 mihomo generate vless-x25519 / vless-mlkem768 生成,填 decryption 一侧', echKey: 'ECH Key', enablePureDownlink: '纯下行', flow: '流控', flowHint: '如 xtls-rprx-vision', grpcHint: 'gRPC 传输', grpcServiceName: 'gRPC Service Name', handshakeTimeout: '握手超时', jlsDest: 'JLS 目标', jlsHint: 'Joliz 相关配置', jlsProxy: 'JLS 代理', jlsSni: 'JLS SNI', jlsSniHint: 'JLS SNI 主机名', jlsUpstreamAddr: 'JLS 上游地址', jlsUsers: 'JLS 用户', kcpTunConn: 'KCP 连接数', kcpTunCrypt: 'KCP 加密', kcpTunKey: 'KCP 密钥', kcpTunMode: 'KCP 模式', kcpTunNocomp: 'KCP 禁用压缩', kcpTunRcvwnd: 'KCP 接收窗口', kcpTunSndwnd: 'KCP 发送窗口', maxIdleTime: '最大空闲时间', maxUdpRelayPacketSize: '最大 UDP 中继包', mekyaHint: 'Mekya 相关配置', mekyaKcpSection: 'Mekya KCP', mekyaMaxSimultaneous: '最大并发', mekyaMaxSimultaneousHint: '最大同时连接数', mekyaMaxWriteDuration: '最大写入时长', mekyaMaxWriteDurationHint: '写入超时', mekyaMaxWriteSize: '最大写入大小', mekyaMaxWriteSizeHint: '单次写入上限', mekyaPacketBuffer: '包缓冲', mekyaPacketBufferHint: '数据包缓冲大小', mkcpCongestion: 'mKCP 拥塞控制', mkcpDownlink: 'mKCP 下行', mkcpHeader: 'mKCP 头类型', mkcpHint: 'mKCP 传输', mkcpReadBuffer: 'mKCP 读缓冲', mkcpSeed: 'mKCP Seed', mkcpUplink: 'mKCP 上行', mkcpWriteBuffer: 'mKCP 写缓冲', muxBrutal: 'Mux Brutal', muxBrutalDown: 'Brutal 下行', muxBrutalUp: 'Brutal 上行', muxMaxConnections: 'Mux 最大连接', muxMaxStreams: 'Mux 最大流', muxMinStreams: 'Mux 最小流', muxOnlyTcp: '仅 TCP Mux', muxPadding: 'Mux 填充', muxProtocol: 'Mux 协议', muxStatistic: 'Mux 统计', network: '传输网络', obfs: '混淆', obfsMaxPacketSize: '混淆最大包', obfsMinPacketSize: '混淆最小包', obfsOptsHost: '混淆 Host', obfsOptsMode: '混淆模式', obfsPassword: '混淆密码', paddingMax: '填充最大', paddingMin: '填充最小', paddingScheme: '填充方案', passwordOptionalHint: '可选,留空使用绑定用户密码', passwordOptionalPlaceholder: '可选密码', psk: 'PSK', quicVersions: 'QUIC 版本', rateLimit: '速率限制', rateLimitHint: '如 10m', realityExclusiveHint: 'Reality 与 TLS 证书互斥', realityKeyHint: '服务端私钥。可留空保存时自动生成;客户端pbk由私钥推导写入分享链接。', realityShortId: 'Reality Short ID', realmId: 'Realm ID', realmProxy: 'Realm 代理', realmServerUrl: 'Realm 服务地址', realmSkipCert: '跳过证书校验', realmStun: 'STUN', realmToken: 'Realm Token', resTlsDest: 'ResTLS 目标', resTlsHint: 'ResTLS 配置', resTlsMinRecordLen: '最小记录长度', resTlsPassword: 'ResTLS 密码', resTlsProxy: 'ResTLS 代理', resTlsScript: 'ResTLS 脚本', sectionJLS: 'JLS', sectionJlsUpstream: 'JLS 上游', sectionKcpTun: 'KCP Tun', sectionMekya: 'Mekya', sectionMkcp: 'mKCP', sectionMux: 'Mux', sectionRealm: 'Realm', sectionResTLS: 'ResTLS', sectionShadowTLS: 'ShadowTLS', sectionSimpleObfs: 'Simple Obfs', sectionXHTTP: 'XHTTP', shadowTlsHandshakeDest: '握手目标', shadowTlsHandshakeProxy: '握手代理', shadowTlsHint: 'ShadowTLS 配置', shadowTlsPassword: 'ShadowTLS 密码', shadowTlsPasswordHint: '版本 2/3 密码', shadowTlsUsers: 'ShadowTLS 用户', shadowTlsVersion: 'ShadowTLS 版本', simpleObfsMode: 'Simple Obfs 模式', snellVersion: 'Snell 版本', ssOptionMethod: 'SS 加密方式', ssOptionPassword: 'SS 密码', sudokuKey: 'Sudoku Key', tableType: '表类型', tlsPairHint: '证书与私钥需成对填写', token: 'Token', tokenHint: '认证 Token', tokenPlaceholder: '输入 Token', trafficPattern: '流量模式', userHintMandatory: '至少一个用户', wizardBasic: '基础信息', wizardConfig: '协议配置', wizardConfigHint: '填写协议相关字段(TLS / Reality / 用户等)', wizardReview: '确认创建', wsPath: 'WebSocket 路径', wsPathHint: '如 /ws', xhttpHint: 'XHTTP 传输', xhttpHost: 'XHTTP Host', xhttpMode: 'XHTTP 模式', xhttpPath: 'XHTTP 路径', zeroRtt: '0-RTT'}, + encryptionHint: '客户端 VLESS Encryption:与 decryption 成对,取 generate 输出的 encryption 一侧,勿直接复制 decryption', decryptionHint: '服务端 VLESS Encryption:由 mihomo generate vless-x25519 / vless-mlkem768 生成,填 decryption 一侧', echKey: 'ECH Key', enablePureDownlink: '纯下行', flow: '流控', flowHint: '如 xtls-rprx-vision', grpcHint: 'gRPC 传输', grpcServiceName: 'gRPC Service Name', handshakeTimeout: '握手超时', jlsDest: 'JLS 目标', jlsHint: 'Joliz 相关配置', jlsProxy: 'JLS 代理', jlsSni: 'JLS SNI', jlsSniHint: 'JLS SNI 主机名', jlsUpstreamAddr: 'JLS 上游地址', jlsUsers: 'JLS 用户', kcpTunConn: 'KCP 连接数', kcpTunCrypt: 'KCP 加密', kcpTunKey: 'KCP 密钥', kcpTunMode: 'KCP 模式', kcpTunNocomp: 'KCP 禁用压缩', kcpTunRcvwnd: 'KCP 接收窗口', kcpTunSndwnd: 'KCP 发送窗口', maxIdleTime: '最大空闲时间', maxUdpRelayPacketSize: '最大 UDP 中继包', mekyaHint: 'Mekya 相关配置', mekyaKcpSection: 'Mekya KCP', mekyaMaxSimultaneous: '最大并发', mekyaMaxSimultaneousHint: '最大同时连接数', mekyaMaxWriteDuration: '最大写入时长', mekyaMaxWriteDurationHint: '写入超时', mekyaMaxWriteSize: '最大写入大小', mekyaMaxWriteSizeHint: '单次写入上限', mekyaPacketBuffer: '包缓冲', mekyaPacketBufferHint: '数据包缓冲大小', mkcpCongestion: 'mKCP 拥塞控制', mkcpDownlink: 'mKCP 下行', mkcpHeader: 'mKCP 头类型', mkcpHint: 'mKCP 传输', mkcpReadBuffer: 'mKCP 读缓冲', mkcpSeed: 'mKCP Seed', mkcpUplink: 'mKCP 上行', mkcpWriteBuffer: 'mKCP 写缓冲', muxBrutal: 'Mux Brutal', muxBrutalDown: 'Brutal 下行', muxBrutalUp: 'Brutal 上行', muxMaxConnections: 'Mux 最大连接', muxMaxStreams: 'Mux 最大流', muxMinStreams: 'Mux 最小流', muxOnlyTcp: '仅 TCP Mux', muxPadding: 'Mux 填充', muxProtocol: 'Mux 协议', muxStatistic: 'Mux 统计', network: '传输网络', obfs: '混淆', obfsMaxPacketSize: '混淆最大包', obfsMinPacketSize: '混淆最小包', obfsOptsHost: '混淆 Host', obfsOptsMode: '混淆模式', obfsPassword: '混淆密码', paddingMax: '填充最大', paddingMin: '填充最小', paddingScheme: '填充方案', passwordOptionalHint: '可选,留空使用绑定用户密码', passwordOptionalPlaceholder: '可选密码', psk: 'PSK', quicVersions: 'QUIC 版本', rateLimit: '速率限制', rateLimitHint: '如 10m', realityExclusiveHint: 'Reality 与 TLS 证书互斥', realityKeyHint: '服务端私钥。可留空保存时自动生成;客户端pbk由私钥推导写入分享链接。', realityShortId: 'Reality Short ID', realmId: 'Realm ID', realmProxy: 'Realm 代理', realmServerUrl: 'Realm 服务地址', realmSkipCert: '跳过证书校验', realmStun: 'STUN', realmToken: 'Realm Token', resTlsDest: 'ResTLS 目标', resTlsHint: 'ResTLS 配置', resTlsMinRecordLen: '最小记录长度', resTlsPassword: 'ResTLS 密码', resTlsProxy: 'ResTLS 代理', resTlsScript: 'ResTLS 脚本', sectionJLS: 'JLS', sectionJlsUpstream: 'JLS 上游', sectionKcpTun: 'KCP Tun', sectionMekya: 'Mekya', sectionMkcp: 'mKCP', sectionMux: 'Mux', sectionRealm: 'Realm', sectionResTLS: 'ResTLS', sectionShadowTLS: 'ShadowTLS', sectionSimpleObfs: 'Simple Obfs', sectionXHTTP: 'XHTTP', shadowTlsHandshakeDest: '握手目标', shadowTlsHandshakeProxy: '握手代理', shadowTlsHint: 'ShadowTLS 配置', shadowTlsPassword: 'ShadowTLS 密码', shadowTlsPasswordHint: '版本 2/3 密码', shadowTlsUsers: 'ShadowTLS 用户', shadowTlsVersion: 'ShadowTLS 版本', simpleObfsMode: 'Simple Obfs 模式', snellVersion: 'Snell 版本', ssOptionMethod: 'SS 加密方式', ssOptionPassword: 'SS 密码', sudokuKey: 'Sudoku Key', customTable: '自定义表', customTables: '自定义表列表', sudokuFallback: '回落', sectionHttpmask: 'HTTPMask', httpmaskDisable: '禁用 HTTP 伪装', httpmaskMode: 'HTTP 伪装模式', httpmaskPathRoot: 'HTTP 路径前缀', tableType: '表类型', tlsPairHint: '证书与私钥需成对填写', token: 'Token', tokenHint: '认证 Token', tokenPlaceholder: '输入 Token', trafficPattern: '流量模式', userHintMandatory: '至少一个用户', wizardBasic: '基础信息', wizardConfig: '协议配置', wizardConfigHint: '填写协议相关字段(TLS / Reality / 用户等)', wizardReview: '确认创建', wsPath: 'WebSocket 路径', wsPathHint: '如 /ws', xhttpHint: 'XHTTP 传输', xhttpHost: 'XHTTP Host', xhttpMode: 'XHTTP 模式', xhttpPath: 'XHTTP 路径', zeroRtt: '0-RTT'}, core: { title: '核心管理', status: '核心状态', running: '运行中', stoppedStatus: '已停止', version: '版本', pid: '进程 ID', uptime: '运行时间', start: '启动', stop: '停止', restart: '重启', started: '已启动', stopped: '已停止', restarted: '已重启' , operationFailed: '操作失败', unavailable: '核心不可用'}, logs: { title: '运行日志', empty: '暂无日志', level: '级别', timestamp: '时间', payload: '内容', autoRefresh: '自动刷新', clear: '清空' }, config: { title: '配置引擎', subtitle: '可视化管理 Mihomo 配置', visual: '可视化配置', yaml: 'YAML 配置', proxies: '代理配置', yamlPreview: 'YAML 预览', addProxy: '添加代理', editProxy: '编辑代理', deleteProxy: '删除代理', deleteConfirm: '确定要删除此代理吗?', generateSuccess: '配置生成成功', validateSuccess: '配置验证通过', validateFailed: '配置验证失败', proxyName: '代理名称', proxyType: '代理类型', proxyServer: '代理服务器', proxyPort: '代理端口', proxyPassword: '代理密码', proxyUUID: '代理 UUID' , generate: '生成配置', validate: '校验配置'},