diff --git a/.github/update.log b/.github/update.log index 666e9f0282..bdb01aab23 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1294,3 +1294,4 @@ Update On Fri Mar 6 20:05:10 CET 2026 Update On Sat Mar 7 19:46:34 CET 2026 Update On Sun Mar 8 19:47:17 CET 2026 Update On Mon Mar 9 20:06:07 CET 2026 +Update On Tue Mar 10 20:01:59 CET 2026 diff --git a/clash-meta/adapter/outbound/trojan.go b/clash-meta/adapter/outbound/trojan.go index b691ff0080..497ff05b1f 100644 --- a/clash-meta/adapter/outbound/trojan.go +++ b/clash-meta/adapter/outbound/trojan.go @@ -27,8 +27,7 @@ type Trojan struct { hexPassword [trojan.KeyLength]byte // for gun mux - gunConfig *gun.Config - gunTransport *gun.TransportWrap + gunTransport *gun.Transport realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -178,7 +177,7 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con var c net.Conn // gun transport if t.gunTransport != nil { - c, err = gun.StreamGunWithTransport(t.gunTransport, t.gunConfig) + c, err = t.gunTransport.Dial() } else { c, err = t.dialer.DialContext(ctx, "tcp", t.addr) } @@ -206,7 +205,7 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata) var c net.Conn // grpc transport if t.gunTransport != nil { - c, err = gun.StreamGunWithTransport(t.gunTransport, t.gunConfig) + c, err = t.gunTransport.Dial() } else { c, err = t.dialer.DialContext(ctx, "tcp", t.addr) } @@ -317,13 +316,14 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { Reality: t.realityConfig, } - t.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig) - - t.gunConfig = &gun.Config{ - ServiceName: option.GrpcOpts.GrpcServiceName, - UserAgent: option.GrpcOpts.GrpcUserAgent, - Host: option.SNI, + gunConfig := &gun.Config{ + ServiceName: option.GrpcOpts.GrpcServiceName, + UserAgent: option.GrpcOpts.GrpcUserAgent, + Host: option.SNI, + PingInterval: option.GrpcOpts.PingInterval, } + + t.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) } return t, nil diff --git a/clash-meta/adapter/outbound/vless.go b/clash-meta/adapter/outbound/vless.go index bd060b185d..78ccb6676f 100644 --- a/clash-meta/adapter/outbound/vless.go +++ b/clash-meta/adapter/outbound/vless.go @@ -33,8 +33,7 @@ type Vless struct { encryption *encryption.ClientInstance // for gun mux - gunConfig *gun.Config - gunTransport *gun.TransportWrap + gunTransport *gun.Transport realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -234,7 +233,7 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -260,7 +259,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) ( var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -431,9 +430,10 @@ func NewVless(option VlessOption) (*Vless, error) { } gunConfig := &gun.Config{ - ServiceName: option.GrpcOpts.GrpcServiceName, - UserAgent: option.GrpcOpts.GrpcUserAgent, - Host: option.ServerName, + ServiceName: option.GrpcOpts.GrpcServiceName, + UserAgent: option.GrpcOpts.GrpcUserAgent, + Host: option.ServerName, + PingInterval: option.GrpcOpts.PingInterval, } if option.ServerName == "" { gunConfig.Host = v.addr @@ -457,9 +457,7 @@ func NewVless(option VlessOption) (*Vless, error) { } } - v.gunConfig = gunConfig - - v.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig) + v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) } return v, nil diff --git a/clash-meta/adapter/outbound/vmess.go b/clash-meta/adapter/outbound/vmess.go index 29ed63fde2..0de06b7c17 100644 --- a/clash-meta/adapter/outbound/vmess.go +++ b/clash-meta/adapter/outbound/vmess.go @@ -34,8 +34,7 @@ type Vmess struct { option *VmessOption // for gun mux - gunConfig *gun.Config - gunTransport *gun.TransportWrap + gunTransport *gun.Transport realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -86,6 +85,7 @@ type HTTP2Options struct { type GrpcOptions struct { GrpcServiceName string `proxy:"grpc-service-name,omitempty"` GrpcUserAgent string `proxy:"grpc-user-agent,omitempty"` + PingInterval int `proxy:"ping-interval,omitempty"` } type WSOptions struct { @@ -295,7 +295,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -318,7 +318,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) ( var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -437,9 +437,10 @@ func NewVmess(option VmessOption) (*Vmess, error) { } gunConfig := &gun.Config{ - ServiceName: option.GrpcOpts.GrpcServiceName, - UserAgent: option.GrpcOpts.GrpcUserAgent, - Host: option.ServerName, + ServiceName: option.GrpcOpts.GrpcServiceName, + UserAgent: option.GrpcOpts.GrpcUserAgent, + Host: option.ServerName, + PingInterval: option.GrpcOpts.PingInterval, } if option.ServerName == "" { gunConfig.Host = v.addr @@ -463,9 +464,7 @@ func NewVmess(option VmessOption) (*Vmess, error) { } } - v.gunConfig = gunConfig - - v.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig) + v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) } return v, nil diff --git a/clash-meta/adapter/provider/parser.go b/clash-meta/adapter/provider/parser.go index f6200fdde2..1668ccf99f 100644 --- a/clash-meta/adapter/provider/parser.go +++ b/clash-meta/adapter/provider/parser.go @@ -18,8 +18,8 @@ var ( type healthCheckSchema struct { Enable bool `provider:"enable"` - URL string `provider:"url"` - Interval int `provider:"interval"` + URL string `provider:"url,omitempty"` + Interval int `provider:"interval,omitempty"` TestTimeout int `provider:"timeout,omitempty"` Lazy bool `provider:"lazy,omitempty"` ExpectedStatus string `provider:"expected-status,omitempty"` diff --git a/clash-meta/docs/config.yaml b/clash-meta/docs/config.yaml index 09467a11e4..efaa9ede90 100644 --- a/clash-meta/docs/config.yaml +++ b/clash-meta/docs/config.yaml @@ -669,6 +669,7 @@ proxies: # socks5 grpc-opts: grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" + # ping-interval: 0 # 默认关闭,单位为秒 # ip-version: ipv4 # vless @@ -759,6 +760,7 @@ proxies: # socks5 grpc-opts: grpc-service-name: "grpc" # grpc-user-agent: "grpc-go/1.36.0" + # ping-interval: 0 # 默认关闭,单位为秒 reality-opts: public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE @@ -830,6 +832,7 @@ proxies: # socks5 grpc-opts: grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" + # ping-interval: 0 # 默认关闭,单位为秒 - name: trojan-ws server: server diff --git a/clash-meta/transport/gun/gun.go b/clash-meta/transport/gun/gun.go index 27c0cbbbe2..65313df94d 100644 --- a/clash-meta/transport/gun/gun.go +++ b/clash-meta/transport/gun/gun.go @@ -59,9 +59,10 @@ type Conn struct { } type Config struct { - ServiceName string - UserAgent string - Host string + ServiceName string + UserAgent string + Host string + PingInterval int } func (g *Conn) initReader() { @@ -246,7 +247,7 @@ func (g *Conn) SetDeadline(t time.Time) error { return nil } -func NewHTTP2Client(dialFn DialFn, tlsConfig *vmess.TLSConfig) *TransportWrap { +func NewTransport(dialFn DialFn, tlsConfig *vmess.TLSConfig, gunCfg *Config) *Transport { dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) defer cancel() @@ -288,14 +289,16 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *vmess.TLSConfig) *TransportWrap { DialTLSContext: dialFunc, AllowHTTP: false, DisableCompression: true, + ReadIdleTimeout: time.Duration(gunCfg.PingInterval) * time.Second, // If zero, no health check is performed PingTimeout: 0, } ctx, cancel := context.WithCancel(context.Background()) - wrap := &TransportWrap{ - Http2Transport: transport, - ctx: ctx, - cancel: cancel, + wrap := &Transport{ + transport: transport, + cfg: gunCfg, + ctx: ctx, + cancel: cancel, } return wrap } @@ -307,18 +310,18 @@ func ServiceNameToPath(serviceName string) string { return "/" + serviceName + "/Tun" } -func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, error) { +func (t *Transport) Dial() (net.Conn, error) { serviceName := "GunService" - if cfg.ServiceName != "" { - serviceName = cfg.ServiceName + if t.cfg.ServiceName != "" { + serviceName = t.cfg.ServiceName } path := ServiceNameToPath(serviceName) reader, writer := io.Pipe() header := defaultHeader.Clone() - if cfg.UserAgent != "" { - header.Set("User-Agent", cfg.UserAgent) + if t.cfg.UserAgent != "" { + header.Set("User-Agent", t.cfg.UserAgent) } request := &http.Request{ @@ -326,17 +329,17 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er Body: reader, URL: &url.URL{ Scheme: "https", - Host: cfg.Host, + Host: t.cfg.Host, Path: path, // for unescape path - Opaque: "//" + cfg.Host + path, + Opaque: "//" + t.cfg.Host + path, }, Proto: "HTTP/2", ProtoMajor: 2, ProtoMinor: 0, Header: header, } - request = request.WithContext(transport.ctx) + request = request.WithContext(t.ctx) conn := &Conn{ initFn: func() (io.ReadCloser, NetAddr, error) { @@ -348,7 +351,7 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er }, } request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace)) - response, err := transport.RoundTrip(request) + response, err := t.transport.RoundTrip(request) if err != nil { return nil, nAddr, err } @@ -361,13 +364,13 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er return conn, nil } -func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, cfg *Config) (net.Conn, error) { +func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, gunCfg *Config) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { return conn, nil } - transport := NewHTTP2Client(dialFn, tlsConfig) - c, err := StreamGunWithTransport(transport, cfg) + transport := NewTransport(dialFn, tlsConfig, gunCfg) + c, err := transport.Dial() if err != nil { return nil, err } diff --git a/clash-meta/transport/gun/transport.go b/clash-meta/transport/gun/transport.go index 4b9da971f3..80c0af1537 100644 --- a/clash-meta/transport/gun/transport.go +++ b/clash-meta/transport/gun/transport.go @@ -10,17 +10,18 @@ import ( "github.com/metacubex/http" ) -type TransportWrap struct { - *http.Http2Transport +type Transport struct { + transport *http.Http2Transport + cfg *Config ctx context.Context cancel context.CancelFunc closeOnce sync.Once } -func (tw *TransportWrap) Close() error { - tw.closeOnce.Do(func() { - tw.cancel() - CloseTransport(tw.Http2Transport) +func (t *Transport) Close() error { + t.closeOnce.Do(func() { + t.cancel() + CloseHttp2Transport(t.transport) }) return nil } diff --git a/clash-meta/transport/gun/transport_close.go b/clash-meta/transport/gun/transport_close.go index 44fefd7c12..add4906e46 100644 --- a/clash-meta/transport/gun/transport_close.go +++ b/clash-meta/transport/gun/transport_close.go @@ -44,7 +44,7 @@ func closeClientConn(cc *http.Http2ClientConn) { // like forceCloseConn() in htt _ = cc.Close() } -func CloseTransport(tr *http.Http2Transport) { +func CloseHttp2Transport(tr *http.Http2Transport) { connPool := transportConnPool(tr) p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data) p.mu.Lock() diff --git a/clash-meta/transport/sudoku/crypto/record_conn.go b/clash-meta/transport/sudoku/crypto/record_conn.go index 7a80c7f5f3..7c0357151b 100644 --- a/clash-meta/transport/sudoku/crypto/record_conn.go +++ b/clash-meta/transport/sudoku/crypto/record_conn.go @@ -5,6 +5,7 @@ import ( "crypto/aes" "crypto/cipher" "crypto/hmac" + "crypto/rand" "crypto/sha256" "encoding/binary" "errors" @@ -12,6 +13,7 @@ import ( "io" "net" "sync" + "sync/atomic" "golang.org/x/crypto/chacha20poly1305" ) @@ -55,13 +57,15 @@ type RecordConn struct { recvAEADEpoch uint32 // Send direction state. - sendEpoch uint32 - sendSeq uint64 - sendBytes int64 + sendEpoch uint32 + sendSeq uint64 + sendBytes int64 + sendEpochUpdates uint32 // Receive direction state. - recvEpoch uint32 - recvSeq uint64 + recvEpoch uint32 + recvSeq uint64 + recvInitialized bool readBuf bytes.Buffer @@ -105,6 +109,9 @@ func NewRecordConn(conn net.Conn, method string, baseSend, baseRecv []byte) (*Re } rc := &RecordConn{Conn: conn, method: method} rc.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)} + if err := rc.resetTrafficState(); err != nil { + return nil, err + } return rc, nil } @@ -127,11 +134,9 @@ func (c *RecordConn) Rekey(baseSend, baseRecv []byte) error { defer c.writeMu.Unlock() c.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)} - c.sendEpoch = 0 - c.sendSeq = 0 - c.sendBytes = 0 - c.recvEpoch = 0 - c.recvSeq = 0 + if err := c.resetTrafficState(); err != nil { + return err + } c.readBuf.Reset() c.sendAEAD = nil @@ -141,6 +146,21 @@ func (c *RecordConn) Rekey(baseSend, baseRecv []byte) error { return nil } +func (c *RecordConn) resetTrafficState() error { + sendEpoch, sendSeq, err := randomRecordCounters() + if err != nil { + return fmt.Errorf("initialize record counters: %w", err) + } + c.sendEpoch = sendEpoch + c.sendSeq = sendSeq + c.sendBytes = 0 + c.sendEpochUpdates = 0 + c.recvEpoch = 0 + c.recvSeq = 0 + c.recvInitialized = false + return nil +} + func normalizeAEADMethod(method string) string { switch method { case "", "chacha20-poly1305": @@ -166,6 +186,44 @@ func cloneBytes(b []byte) []byte { return append([]byte(nil), b...) } +func randomRecordCounters() (uint32, uint64, error) { + epoch, err := randomNonZeroUint32() + if err != nil { + return 0, 0, err + } + seq, err := randomNonZeroUint64() + if err != nil { + return 0, 0, err + } + return epoch, seq, nil +} + +func randomNonZeroUint32() (uint32, error) { + var b [4]byte + for { + if _, err := io.ReadFull(rand.Reader, b[:]); err != nil { + return 0, err + } + v := binary.BigEndian.Uint32(b[:]) + if v != 0 && v != ^uint32(0) { + return v, nil + } + } +} + +func randomNonZeroUint64() (uint64, error) { + var b [8]byte + for { + if _, err := io.ReadFull(rand.Reader, b[:]); err != nil { + return 0, err + } + v := binary.BigEndian.Uint64(b[:]) + if v != 0 && v != ^uint64(0) { + return v, nil + } + } +} + func (c *RecordConn) newAEADFor(base []byte, epoch uint32) (cipher.AEAD, error) { if c.method == "none" { return nil, nil @@ -209,17 +267,49 @@ func deriveEpochKey(base []byte, epoch uint32, method string) []byte { return mac.Sum(nil) } -func (c *RecordConn) maybeBumpSendEpochLocked(addedPlain int) { - if KeyUpdateAfterBytes <= 0 || c.method == "none" { - return +func (c *RecordConn) maybeBumpSendEpochLocked(addedPlain int) error { + ku := atomic.LoadInt64(&KeyUpdateAfterBytes) + if ku <= 0 || c.method == "none" { + return nil } c.sendBytes += int64(addedPlain) - threshold := KeyUpdateAfterBytes * int64(c.sendEpoch+1) + threshold := ku * int64(c.sendEpochUpdates+1) if c.sendBytes < threshold { - return + return nil } c.sendEpoch++ - c.sendSeq = 0 + c.sendEpochUpdates++ + nextSeq, err := randomNonZeroUint64() + if err != nil { + return fmt.Errorf("rotate record seq: %w", err) + } + c.sendSeq = nextSeq + return nil +} + +func (c *RecordConn) validateRecvPosition(epoch uint32, seq uint64) error { + if !c.recvInitialized { + return nil + } + if epoch < c.recvEpoch { + return fmt.Errorf("replayed epoch: got %d want >=%d", epoch, c.recvEpoch) + } + if epoch == c.recvEpoch && seq != c.recvSeq { + return fmt.Errorf("out of order: epoch=%d got=%d want=%d", epoch, seq, c.recvSeq) + } + if epoch > c.recvEpoch { + const maxJump = 8 + if epoch-c.recvEpoch > maxJump { + return fmt.Errorf("epoch jump too large: got=%d want<=%d", epoch-c.recvEpoch, maxJump) + } + } + return nil +} + +func (c *RecordConn) markRecvPosition(epoch uint32, seq uint64) { + c.recvEpoch = epoch + c.recvSeq = seq + 1 + c.recvInitialized = true } func (c *RecordConn) Write(p []byte) (int, error) { @@ -282,7 +372,9 @@ func (c *RecordConn) Write(p []byte) (int, error) { } total += n - c.maybeBumpSendEpochLocked(n) + if err := c.maybeBumpSendEpochLocked(n); err != nil { + return total, err + } } return total, nil } @@ -324,31 +416,17 @@ func (c *RecordConn) Read(p []byte) (int, error) { epoch := binary.BigEndian.Uint32(header[:4]) seq := binary.BigEndian.Uint64(header[4:]) - if epoch < c.recvEpoch { - return 0, fmt.Errorf("replayed epoch: got %d want >=%d", epoch, c.recvEpoch) - } - if epoch == c.recvEpoch && seq != c.recvSeq { - return 0, fmt.Errorf("out of order: epoch=%d got=%d want=%d", epoch, seq, c.recvSeq) - } - if epoch > c.recvEpoch { - const maxJump = 8 - if epoch-c.recvEpoch > maxJump { - return 0, fmt.Errorf("epoch jump too large: got=%d want<=%d", epoch-c.recvEpoch, maxJump) - } - c.recvEpoch = epoch - c.recvSeq = 0 - if seq != 0 { - return 0, fmt.Errorf("out of order: epoch advanced to %d but seq=%d", epoch, seq) - } + if err := c.validateRecvPosition(epoch, seq); err != nil { + return 0, err } - if c.recvAEAD == nil || c.recvAEADEpoch != c.recvEpoch { - a, err := c.newAEADFor(c.keys.baseRecv, c.recvEpoch) + if c.recvAEAD == nil || c.recvAEADEpoch != epoch { + a, err := c.newAEADFor(c.keys.baseRecv, epoch) if err != nil { return 0, err } c.recvAEAD = a - c.recvAEADEpoch = c.recvEpoch + c.recvAEADEpoch = epoch } aead := c.recvAEAD @@ -356,7 +434,7 @@ func (c *RecordConn) Read(p []byte) (int, error) { if err != nil { return 0, fmt.Errorf("decryption failed: epoch=%d seq=%d: %w", epoch, seq, err) } - c.recvSeq++ + c.markRecvPosition(epoch, seq) c.readBuf.Write(plaintext) return c.readBuf.Read(p) diff --git a/clash-meta/transport/sudoku/crypto/record_conn_test.go b/clash-meta/transport/sudoku/crypto/record_conn_test.go new file mode 100644 index 0000000000..4ea0b9b88e --- /dev/null +++ b/clash-meta/transport/sudoku/crypto/record_conn_test.go @@ -0,0 +1,86 @@ +package crypto + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "io" + "net" + "testing" + "time" +) + +type captureConn struct { + bytes.Buffer +} + +func (c *captureConn) Read(_ []byte) (int, error) { return 0, io.EOF } +func (c *captureConn) Write(p []byte) (int, error) { return c.Buffer.Write(p) } +func (c *captureConn) Close() error { return nil } +func (c *captureConn) LocalAddr() net.Addr { return nil } +func (c *captureConn) RemoteAddr() net.Addr { return nil } +func (c *captureConn) SetDeadline(time.Time) error { return nil } +func (c *captureConn) SetReadDeadline(time.Time) error { return nil } +func (c *captureConn) SetWriteDeadline(time.Time) error { return nil } + +type replayConn struct { + reader *bytes.Reader +} + +func (c *replayConn) Read(p []byte) (int, error) { return c.reader.Read(p) } +func (c *replayConn) Write(p []byte) (int, error) { return len(p), nil } +func (c *replayConn) Close() error { return nil } +func (c *replayConn) LocalAddr() net.Addr { return nil } +func (c *replayConn) RemoteAddr() net.Addr { return nil } +func (c *replayConn) SetDeadline(time.Time) error { return nil } +func (c *replayConn) SetReadDeadline(time.Time) error { return nil } +func (c *replayConn) SetWriteDeadline(time.Time) error { return nil } + +func TestRecordConn_FirstFrameUsesRandomizedCounters(t *testing.T) { + pskSend := sha256.Sum256([]byte("record-send")) + pskRecv := sha256.Sum256([]byte("record-recv")) + + raw := &captureConn{} + writer, err := NewRecordConn(raw, "chacha20-poly1305", pskSend[:], pskRecv[:]) + if err != nil { + t.Fatalf("new writer: %v", err) + } + + if writer.sendEpoch == 0 || writer.sendSeq == 0 { + t.Fatalf("expected non-zero randomized counters, got epoch=%d seq=%d", writer.sendEpoch, writer.sendSeq) + } + + want := []byte("record prefix camouflage") + if _, err := writer.Write(want); err != nil { + t.Fatalf("write: %v", err) + } + + wire := raw.Bytes() + if len(wire) < 2+recordHeaderSize { + t.Fatalf("short frame: %d", len(wire)) + } + + bodyLen := int(binary.BigEndian.Uint16(wire[:2])) + if bodyLen != len(wire)-2 { + t.Fatalf("body len mismatch: got %d want %d", bodyLen, len(wire)-2) + } + + epoch := binary.BigEndian.Uint32(wire[2:6]) + seq := binary.BigEndian.Uint64(wire[6:14]) + if epoch == 0 || seq == 0 { + t.Fatalf("wire header still starts from zero: epoch=%d seq=%d", epoch, seq) + } + + reader, err := NewRecordConn(&replayConn{reader: bytes.NewReader(wire)}, "chacha20-poly1305", pskRecv[:], pskSend[:]) + if err != nil { + t.Fatalf("new reader: %v", err) + } + + got := make([]byte, len(want)) + if _, err := io.ReadFull(reader, got); err != nil { + t.Fatalf("read: %v", err) + } + if !bytes.Equal(got, want) { + t.Fatalf("plaintext mismatch: got %q want %q", got, want) + } +} diff --git a/clash-meta/transport/sudoku/early_handshake.go b/clash-meta/transport/sudoku/early_handshake.go new file mode 100644 index 0000000000..803a5293a8 --- /dev/null +++ b/clash-meta/transport/sudoku/early_handshake.go @@ -0,0 +1,345 @@ +package sudoku + +import ( + "bytes" + "crypto/ecdh" + "crypto/rand" + "encoding/hex" + "fmt" + "net" + "time" + + "github.com/metacubex/mihomo/transport/sudoku/crypto" + httpmaskobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask" + sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" +) + +const earlyKIPHandshakeTTL = 60 * time.Second + +type EarlyCodecConfig struct { + PSK string + AEAD string + EnablePureDownlink bool + PaddingMin int + PaddingMax int +} + +type EarlyClientState struct { + RequestPayload []byte + + cfg EarlyCodecConfig + table *sudokuobfs.Table + nonce [kipHelloNonceSize]byte + ephemeral *ecdh.PrivateKey + sessionC2S []byte + sessionS2C []byte + responseSet bool +} + +type EarlyServerState struct { + ResponsePayload []byte + UserHash string + + cfg EarlyCodecConfig + table *sudokuobfs.Table + sessionC2S []byte + sessionS2C []byte +} + +type ReplayAllowFunc func(userHash string, nonce [kipHelloNonceSize]byte, now time.Time) bool + +type earlyMemoryConn struct { + reader *bytes.Reader + write bytes.Buffer +} + +func newEarlyMemoryConn(readBuf []byte) *earlyMemoryConn { + return &earlyMemoryConn{reader: bytes.NewReader(readBuf)} +} + +func (c *earlyMemoryConn) Read(p []byte) (int, error) { + if c == nil || c.reader == nil { + return 0, net.ErrClosed + } + return c.reader.Read(p) +} + +func (c *earlyMemoryConn) Write(p []byte) (int, error) { + if c == nil { + return 0, net.ErrClosed + } + return c.write.Write(p) +} + +func (c *earlyMemoryConn) Close() error { return nil } +func (c *earlyMemoryConn) LocalAddr() net.Addr { return earlyDummyAddr("local") } +func (c *earlyMemoryConn) RemoteAddr() net.Addr { return earlyDummyAddr("remote") } +func (c *earlyMemoryConn) SetDeadline(time.Time) error { return nil } +func (c *earlyMemoryConn) SetReadDeadline(time.Time) error { return nil } +func (c *earlyMemoryConn) SetWriteDeadline(time.Time) error { return nil } +func (c *earlyMemoryConn) Written() []byte { return append([]byte(nil), c.write.Bytes()...) } + +type earlyDummyAddr string + +func (a earlyDummyAddr) Network() string { return string(a) } +func (a earlyDummyAddr) String() string { return string(a) } + +func buildEarlyClientObfsConn(raw net.Conn, cfg EarlyCodecConfig, table *sudokuobfs.Table) net.Conn { + base := sudokuobfs.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false) + if cfg.EnablePureDownlink { + return base + } + packed := sudokuobfs.NewPackedConn(raw, table, cfg.PaddingMin, cfg.PaddingMax) + return newDirectionalConn(raw, packed, base) +} + +func buildEarlyServerObfsConn(raw net.Conn, cfg EarlyCodecConfig, table *sudokuobfs.Table) net.Conn { + uplink := sudokuobfs.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false) + if cfg.EnablePureDownlink { + return uplink + } + packed := sudokuobfs.NewPackedConn(raw, table, cfg.PaddingMin, cfg.PaddingMax) + return newDirectionalConn(raw, uplink, packed, packed.Flush) +} + +func NewEarlyClientState(cfg EarlyCodecConfig, table *sudokuobfs.Table, userHash [kipHelloUserHashSize]byte, feats uint32) (*EarlyClientState, error) { + if table == nil { + return nil, fmt.Errorf("nil table") + } + + curve := ecdh.X25519() + ephemeral, err := curve.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("ecdh generate failed: %w", err) + } + + var nonce [kipHelloNonceSize]byte + if _, err := rand.Read(nonce[:]); err != nil { + return nil, fmt.Errorf("nonce generate failed: %w", err) + } + + var clientPub [kipHelloPubSize]byte + copy(clientPub[:], ephemeral.PublicKey().Bytes()) + hello := &KIPClientHello{ + Timestamp: time.Now(), + UserHash: userHash, + Nonce: nonce, + ClientPub: clientPub, + Features: feats, + } + + mem := newEarlyMemoryConn(nil) + obfsConn := buildEarlyClientObfsConn(mem, cfg, table) + pskC2S, pskS2C := derivePSKDirectionalBases(cfg.PSK) + rc, err := crypto.NewRecordConn(obfsConn, cfg.AEAD, pskC2S, pskS2C) + if err != nil { + return nil, fmt.Errorf("client early crypto setup failed: %w", err) + } + if err := WriteKIPMessage(rc, KIPTypeClientHello, hello.EncodePayload()); err != nil { + return nil, fmt.Errorf("write early client hello failed: %w", err) + } + + return &EarlyClientState{ + RequestPayload: mem.Written(), + cfg: cfg, + table: table, + nonce: nonce, + ephemeral: ephemeral, + }, nil +} + +func (s *EarlyClientState) ProcessResponse(payload []byte) error { + if s == nil { + return fmt.Errorf("nil client state") + } + + mem := newEarlyMemoryConn(payload) + obfsConn := buildEarlyClientObfsConn(mem, s.cfg, s.table) + pskC2S, pskS2C := derivePSKDirectionalBases(s.cfg.PSK) + rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, pskC2S, pskS2C) + if err != nil { + return fmt.Errorf("client early crypto setup failed: %w", err) + } + + msg, err := ReadKIPMessage(rc) + if err != nil { + return fmt.Errorf("read early server hello failed: %w", err) + } + if msg.Type != KIPTypeServerHello { + return fmt.Errorf("unexpected early handshake message: %d", msg.Type) + } + sh, err := DecodeKIPServerHelloPayload(msg.Payload) + if err != nil { + return fmt.Errorf("decode early server hello failed: %w", err) + } + if sh.Nonce != s.nonce { + return fmt.Errorf("early handshake nonce mismatch") + } + + shared, err := x25519SharedSecret(s.ephemeral, sh.ServerPub[:]) + if err != nil { + return fmt.Errorf("ecdh failed: %w", err) + } + s.sessionC2S, s.sessionS2C, err = deriveSessionDirectionalBases(s.cfg.PSK, shared, s.nonce) + if err != nil { + return fmt.Errorf("derive session keys failed: %w", err) + } + s.responseSet = true + return nil +} + +func (s *EarlyClientState) WrapConn(raw net.Conn) (net.Conn, error) { + if s == nil { + return nil, fmt.Errorf("nil client state") + } + if !s.responseSet { + return nil, fmt.Errorf("early handshake not completed") + } + + obfsConn := buildEarlyClientObfsConn(raw, s.cfg, s.table) + rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, s.sessionC2S, s.sessionS2C) + if err != nil { + return nil, fmt.Errorf("setup client session crypto failed: %w", err) + } + return rc, nil +} + +func (s *EarlyClientState) Ready() bool { + return s != nil && s.responseSet +} + +func NewHTTPMaskClientEarlyHandshake(cfg EarlyCodecConfig, table *sudokuobfs.Table, userHash [kipHelloUserHashSize]byte, feats uint32) (*httpmaskobfs.ClientEarlyHandshake, error) { + state, err := NewEarlyClientState(cfg, table, userHash, feats) + if err != nil { + return nil, err + } + return &httpmaskobfs.ClientEarlyHandshake{ + RequestPayload: state.RequestPayload, + HandleResponse: state.ProcessResponse, + Ready: state.Ready, + WrapConn: state.WrapConn, + }, nil +} + +func ProcessEarlyClientPayload(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, payload []byte, allowReplay ReplayAllowFunc) (*EarlyServerState, error) { + if len(payload) == 0 { + return nil, fmt.Errorf("empty early payload") + } + if len(tables) == 0 { + return nil, fmt.Errorf("no tables configured") + } + + var firstErr error + for _, table := range tables { + state, err := processEarlyClientPayloadForTable(cfg, table, payload, allowReplay) + if err == nil { + return state, nil + } + if firstErr == nil { + firstErr = err + } + } + if firstErr == nil { + firstErr = fmt.Errorf("early handshake probe failed") + } + return nil, firstErr +} + +func processEarlyClientPayloadForTable(cfg EarlyCodecConfig, table *sudokuobfs.Table, payload []byte, allowReplay ReplayAllowFunc) (*EarlyServerState, error) { + mem := newEarlyMemoryConn(payload) + obfsConn := buildEarlyServerObfsConn(mem, cfg, table) + pskC2S, pskS2C := derivePSKDirectionalBases(cfg.PSK) + rc, err := crypto.NewRecordConn(obfsConn, cfg.AEAD, pskS2C, pskC2S) + if err != nil { + return nil, err + } + + msg, err := ReadKIPMessage(rc) + if err != nil { + return nil, err + } + if msg.Type != KIPTypeClientHello { + return nil, fmt.Errorf("unexpected handshake message: %d", msg.Type) + } + ch, err := DecodeKIPClientHelloPayload(msg.Payload) + if err != nil { + return nil, err + } + if absInt64(time.Now().Unix()-ch.Timestamp.Unix()) > int64(earlyKIPHandshakeTTL.Seconds()) { + return nil, fmt.Errorf("time skew/replay") + } + + userHash := hex.EncodeToString(ch.UserHash[:]) + if allowReplay != nil && !allowReplay(userHash, ch.Nonce, time.Now()) { + return nil, fmt.Errorf("replay detected") + } + + curve := ecdh.X25519() + serverEphemeral, err := curve.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("ecdh generate failed: %w", err) + } + shared, err := x25519SharedSecret(serverEphemeral, ch.ClientPub[:]) + if err != nil { + return nil, fmt.Errorf("ecdh failed: %w", err) + } + sessionC2S, sessionS2C, err := deriveSessionDirectionalBases(cfg.PSK, shared, ch.Nonce) + if err != nil { + return nil, fmt.Errorf("derive session keys failed: %w", err) + } + + var serverPub [kipHelloPubSize]byte + copy(serverPub[:], serverEphemeral.PublicKey().Bytes()) + serverHello := &KIPServerHello{ + Nonce: ch.Nonce, + ServerPub: serverPub, + SelectedFeats: ch.Features & KIPFeatAll, + } + + respMem := newEarlyMemoryConn(nil) + respObfs := buildEarlyServerObfsConn(respMem, cfg, table) + respConn, err := crypto.NewRecordConn(respObfs, cfg.AEAD, pskS2C, pskC2S) + if err != nil { + return nil, fmt.Errorf("server early crypto setup failed: %w", err) + } + if err := WriteKIPMessage(respConn, KIPTypeServerHello, serverHello.EncodePayload()); err != nil { + return nil, fmt.Errorf("write early server hello failed: %w", err) + } + + return &EarlyServerState{ + ResponsePayload: respMem.Written(), + UserHash: userHash, + cfg: cfg, + table: table, + sessionC2S: sessionC2S, + sessionS2C: sessionS2C, + }, nil +} + +func (s *EarlyServerState) WrapConn(raw net.Conn) (net.Conn, error) { + if s == nil { + return nil, fmt.Errorf("nil server state") + } + obfsConn := buildEarlyServerObfsConn(raw, s.cfg, s.table) + rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, s.sessionS2C, s.sessionC2S) + if err != nil { + return nil, fmt.Errorf("setup server session crypto failed: %w", err) + } + return rc, nil +} + +func NewHTTPMaskServerEarlyHandshake(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, allowReplay ReplayAllowFunc) *httpmaskobfs.TunnelServerEarlyHandshake { + return &httpmaskobfs.TunnelServerEarlyHandshake{ + Prepare: func(payload []byte) (*httpmaskobfs.PreparedServerEarlyHandshake, error) { + state, err := ProcessEarlyClientPayload(cfg, tables, payload, allowReplay) + if err != nil { + return nil, err + } + return &httpmaskobfs.PreparedServerEarlyHandshake{ + ResponsePayload: state.ResponsePayload, + WrapConn: state.WrapConn, + UserHash: state.UserHash, + }, nil + }, + } +} diff --git a/clash-meta/transport/sudoku/handshake.go b/clash-meta/transport/sudoku/handshake.go index 971d47fd8b..bac688e8f5 100644 --- a/clash-meta/transport/sudoku/handshake.go +++ b/clash-meta/transport/sudoku/handshake.go @@ -337,6 +337,9 @@ func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, *Handshak if err := cfg.Validate(); err != nil { return nil, nil, fmt.Errorf("invalid config: %w", err) } + if userHash, ok := httpmask.EarlyHandshakeUserHash(rawConn); ok { + return rawConn, &HandshakeMeta{UserHash: userHash}, nil + } handshakeTimeout := time.Duration(cfg.HandshakeTimeoutSeconds) * time.Second if handshakeTimeout <= 0 { diff --git a/clash-meta/transport/sudoku/httpmask_tunnel.go b/clash-meta/transport/sudoku/httpmask_tunnel.go index 1ff2bb3883..c066eb1c65 100644 --- a/clash-meta/transport/sudoku/httpmask_tunnel.go +++ b/clash-meta/transport/sudoku/httpmask_tunnel.go @@ -14,6 +14,30 @@ type HTTPMaskTunnelServer struct { ts *httpmask.TunnelServer } +func newHTTPMaskEarlyCodecConfig(cfg *ProtocolConfig, psk string) EarlyCodecConfig { + return EarlyCodecConfig{ + PSK: psk, + AEAD: cfg.AEADMethod, + EnablePureDownlink: cfg.EnablePureDownlink, + PaddingMin: cfg.PaddingMin, + PaddingMax: cfg.PaddingMax, + } +} + +func newClientHTTPMaskEarlyHandshake(cfg *ProtocolConfig) (*httpmask.ClientEarlyHandshake, error) { + table, err := pickClientTable(cfg) + if err != nil { + return nil, err + } + + return NewHTTPMaskClientEarlyHandshake( + newHTTPMaskEarlyCodecConfig(cfg, ClientAEADSeed(cfg.Key)), + table, + kipUserHashFromKey(cfg.Key), + KIPFeatAll, + ) +} + func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer { return newHTTPMaskTunnelServer(cfg, false) } @@ -35,6 +59,11 @@ func newHTTPMaskTunnelServer(cfg *ProtocolConfig, passThroughOnReject bool) *HTT Mode: cfg.HTTPMaskMode, PathRoot: cfg.HTTPMaskPathRoot, AuthKey: ServerAEADSeed(cfg.Key), + EarlyHandshake: NewHTTPMaskServerEarlyHandshake( + newHTTPMaskEarlyCodecConfig(cfg, ServerAEADSeed(cfg.Key)), + cfg.tableCandidates(), + globalHandshakeReplay.allow, + ), // When upstream fallback is enabled, preserve rejected HTTP requests for the caller. PassThroughOnReject: passThroughOnReject, }) @@ -101,14 +130,25 @@ func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *Protocol default: return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode) } + var ( + earlyHandshake *httpmask.ClientEarlyHandshake + err error + ) + if upgrade != nil { + earlyHandshake, err = newClientHTTPMaskEarlyHandshake(cfg) + if err != nil { + return nil, err + } + } return httpmask.DialTunnel(ctx, serverAddress, httpmask.TunnelDialOptions{ - Mode: cfg.HTTPMaskMode, - TLSEnabled: cfg.HTTPMaskTLSEnabled, - HostOverride: cfg.HTTPMaskHost, - PathRoot: cfg.HTTPMaskPathRoot, - AuthKey: ClientAEADSeed(cfg.Key), - Upgrade: upgrade, - Multiplex: cfg.HTTPMaskMultiplex, - DialContext: dial, + Mode: cfg.HTTPMaskMode, + TLSEnabled: cfg.HTTPMaskTLSEnabled, + HostOverride: cfg.HTTPMaskHost, + PathRoot: cfg.HTTPMaskPathRoot, + AuthKey: ClientAEADSeed(cfg.Key), + EarlyHandshake: earlyHandshake, + Upgrade: upgrade, + Multiplex: cfg.HTTPMaskMultiplex, + DialContext: dial, }) } diff --git a/clash-meta/transport/sudoku/httpmask_tunnel_test.go b/clash-meta/transport/sudoku/httpmask_tunnel_test.go index 8894882ef9..01eb3a5078 100644 --- a/clash-meta/transport/sudoku/httpmask_tunnel_test.go +++ b/clash-meta/transport/sudoku/httpmask_tunnel_test.go @@ -389,6 +389,68 @@ func TestHTTPMaskTunnel_WS_TCPRoundTrip(t *testing.T) { } } +func TestHTTPMaskTunnel_EarlyHandshake_TCPRoundTrip(t *testing.T) { + modes := []string{"stream", "poll", "ws"} + for _, mode := range modes { + t.Run(mode, func(t *testing.T) { + key := "tunnel-early-" + mode + target := "1.1.1.1:80" + + serverCfg := newTunnelTestTable(t, key) + serverCfg.HTTPMaskMode = mode + + addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error { + if s.Type != SessionTypeTCP { + return fmt.Errorf("unexpected session type: %v", s.Type) + } + if s.Target != target { + return fmt.Errorf("target mismatch: %s", s.Target) + } + _, _ = s.Conn.Write([]byte("ok")) + return nil + }) + defer stop() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + clientCfg := *serverCfg + clientCfg.ServerAddress = addr + + handshakeCfg := clientCfg + handshakeCfg.DisableHTTPMask = true + tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, func(raw net.Conn) (net.Conn, error) { + return ClientHandshake(raw, &handshakeCfg) + }) + if err != nil { + t.Fatalf("dial tunnel: %v", err) + } + defer tunnelConn.Close() + + addrBuf, err := EncodeAddress(target) + if err != nil { + t.Fatalf("encode addr: %v", err) + } + if err := WriteKIPMessage(tunnelConn, KIPTypeOpenTCP, addrBuf); err != nil { + t.Fatalf("write addr: %v", err) + } + + buf := make([]byte, 2) + if _, err := io.ReadFull(tunnelConn, buf); err != nil { + t.Fatalf("read: %v", err) + } + if string(buf) != "ok" { + t.Fatalf("unexpected payload: %q", buf) + } + + stop() + for err := range errCh { + t.Fatalf("server error: %v", err) + } + }) + } +} + func TestHTTPMaskTunnel_Validation(t *testing.T) { cfg := DefaultConfig() cfg.Key = "k" diff --git a/clash-meta/transport/sudoku/obfs/httpmask/early_handshake.go b/clash-meta/transport/sudoku/obfs/httpmask/early_handshake.go new file mode 100644 index 0000000000..54158577bc --- /dev/null +++ b/clash-meta/transport/sudoku/obfs/httpmask/early_handshake.go @@ -0,0 +1,174 @@ +package httpmask + +import ( + "encoding/base64" + "errors" + "fmt" + "net" + "net/url" + "strings" +) + +const ( + tunnelEarlyDataQueryKey = "ed" + tunnelEarlyDataHeader = "X-Sudoku-Early" +) + +type ClientEarlyHandshake struct { + RequestPayload []byte + HandleResponse func(payload []byte) error + Ready func() bool + WrapConn func(raw net.Conn) (net.Conn, error) +} + +type TunnelServerEarlyHandshake struct { + Prepare func(payload []byte) (*PreparedServerEarlyHandshake, error) +} + +type PreparedServerEarlyHandshake struct { + ResponsePayload []byte + WrapConn func(raw net.Conn) (net.Conn, error) + UserHash string +} + +type earlyHandshakeMeta interface { + HTTPMaskEarlyHandshakeUserHash() string +} + +type earlyHandshakeConn struct { + net.Conn + userHash string +} + +func (c *earlyHandshakeConn) HTTPMaskEarlyHandshakeUserHash() string { + if c == nil { + return "" + } + return c.userHash +} + +func wrapEarlyHandshakeConn(conn net.Conn, userHash string) net.Conn { + if conn == nil { + return nil + } + return &earlyHandshakeConn{Conn: conn, userHash: userHash} +} + +func EarlyHandshakeUserHash(conn net.Conn) (string, bool) { + if conn == nil { + return "", false + } + v, ok := conn.(earlyHandshakeMeta) + if !ok { + return "", false + } + return v.HTTPMaskEarlyHandshakeUserHash(), true +} + +type authorizeResponse struct { + token string + earlyPayload []byte +} + +func isTunnelTokenByte(c byte) bool { + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '-' || + c == '_' +} + +func parseAuthorizeResponse(body []byte) (*authorizeResponse, error) { + s := strings.TrimSpace(string(body)) + idx := strings.Index(s, "token=") + if idx < 0 { + return nil, errors.New("missing token") + } + s = s[idx+len("token="):] + if s == "" { + return nil, errors.New("empty token") + } + + var b strings.Builder + for i := 0; i < len(s); i++ { + c := s[i] + if isTunnelTokenByte(c) { + b.WriteByte(c) + continue + } + break + } + token := b.String() + if token == "" { + return nil, errors.New("empty token") + } + + out := &authorizeResponse{token: token} + if earlyLine := findAuthorizeField(body, "ed="); earlyLine != "" { + decoded, err := base64.RawURLEncoding.DecodeString(earlyLine) + if err != nil { + return nil, fmt.Errorf("decode early authorize payload failed: %w", err) + } + out.earlyPayload = decoded + } + return out, nil +} + +func findAuthorizeField(body []byte, prefix string) string { + for _, line := range strings.Split(strings.TrimSpace(string(body)), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, prefix) { + return strings.TrimSpace(strings.TrimPrefix(line, prefix)) + } + } + return "" +} + +func setEarlyDataQuery(rawURL string, payload []byte) (string, error) { + if len(payload) == 0 { + return rawURL, nil + } + u, err := url.Parse(rawURL) + if err != nil { + return "", err + } + q := u.Query() + q.Set(tunnelEarlyDataQueryKey, base64.RawURLEncoding.EncodeToString(payload)) + u.RawQuery = q.Encode() + return u.String(), nil +} + +func parseEarlyDataQuery(u *url.URL) ([]byte, error) { + if u == nil { + return nil, nil + } + val := strings.TrimSpace(u.Query().Get(tunnelEarlyDataQueryKey)) + if val == "" { + return nil, nil + } + return base64.RawURLEncoding.DecodeString(val) +} + +func applyEarlyHandshakeOrUpgrade(raw net.Conn, opts TunnelDialOptions) (net.Conn, error) { + out := raw + if opts.EarlyHandshake != nil && opts.EarlyHandshake.WrapConn != nil && (opts.EarlyHandshake.Ready == nil || opts.EarlyHandshake.Ready()) { + wrapped, err := opts.EarlyHandshake.WrapConn(raw) + if err != nil { + return nil, err + } + if wrapped != nil { + out = wrapped + } + return out, nil + } + if opts.Upgrade != nil { + wrapped, err := opts.Upgrade(raw) + if err != nil { + return nil, err + } + if wrapped != nil { + out = wrapped + } + } + return out, nil +} diff --git a/clash-meta/transport/sudoku/obfs/httpmask/tunnel.go b/clash-meta/transport/sudoku/obfs/httpmask/tunnel.go index 20981c3906..a100c6202e 100644 --- a/clash-meta/transport/sudoku/obfs/httpmask/tunnel.go +++ b/clash-meta/transport/sudoku/obfs/httpmask/tunnel.go @@ -72,6 +72,10 @@ type TunnelDialOptions struct { // AuthKey enables short-term HMAC auth for HTTP tunnel requests (anti-probing). // When set (non-empty), each HTTP request carries an Authorization bearer token derived from AuthKey. AuthKey string + // EarlyHandshake folds the protocol handshake into the HTTP/WS setup round trip. + // When the server accepts the early payload, DialTunnel returns a conn that is already post-handshake. + // When the server does not echo early data, DialTunnel falls back to Upgrade. + EarlyHandshake *ClientEarlyHandshake // Upgrade optionally wraps the raw tunnel conn and/or writes a small prelude before DialTunnel returns. // It is called with the raw tunnel conn; if it returns a non-nil conn, that conn is returned by DialTunnel. Upgrade func(raw net.Conn) (net.Conn, error) @@ -225,30 +229,11 @@ func canonicalHeaderHost(urlHost, scheme string) string { } func parseTunnelToken(body []byte) (string, error) { - s := strings.TrimSpace(string(body)) - idx := strings.Index(s, "token=") - if idx < 0 { - return "", errors.New("missing token") + resp, err := parseAuthorizeResponse(body) + if err != nil { + return "", err } - s = s[idx+len("token="):] - if s == "" { - return "", errors.New("empty token") - } - // Token is base64.RawURLEncoding (A-Z a-z 0-9 - _). Strip any trailing bytes (e.g. from CDN compression). - var b strings.Builder - for i := 0; i < len(s); i++ { - c := s[i] - if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' { - b.WriteByte(c) - continue - } - break - } - token := b.String() - if token == "" { - return "", errors.New("empty token") - } - return token, nil + return resp.token, nil } type httpClientTarget struct { @@ -353,6 +338,13 @@ func dialSessionWithClient(ctx context.Context, client *http.Client, target http auth := newTunnelAuth(opts.AuthKey, 0) authorizeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/session")}).String() + if opts.EarlyHandshake != nil && len(opts.EarlyHandshake.RequestPayload) > 0 { + var err error + authorizeURL, err = setEarlyDataQuery(authorizeURL, opts.EarlyHandshake.RequestPayload) + if err != nil { + return nil, err + } + } var bodyBytes []byte for attempt := 0; ; attempt++ { @@ -410,13 +402,19 @@ func dialSessionWithClient(ctx context.Context, client *http.Client, target http break } - token, err := parseTunnelToken(bodyBytes) + authResp, err := parseAuthorizeResponse(bodyBytes) if err != nil { return nil, fmt.Errorf("%s authorize failed: %q", mode, strings.TrimSpace(string(bodyBytes))) } + token := authResp.token if token == "" { return nil, fmt.Errorf("%s authorize empty token", mode) } + if opts.EarlyHandshake != nil && len(authResp.earlyPayload) > 0 && opts.EarlyHandshake.HandleResponse != nil { + if err := opts.EarlyHandshake.HandleResponse(authResp.earlyPayload); err != nil { + return nil, err + } + } pushURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/api/v1/upload"), RawQuery: "token=" + url.QueryEscape(token)}).String() pullURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/stream"), RawQuery: "token=" + url.QueryEscape(token)}).String() @@ -671,16 +669,10 @@ func dialStreamSplitWithClient(ctx context.Context, client *http.Client, target if c == nil { return nil, fmt.Errorf("failed to build stream split conn") } - outConn := net.Conn(c) - if opts.Upgrade != nil { - upgraded, err := opts.Upgrade(c) - if err != nil { - _ = c.Close() - return nil, err - } - if upgraded != nil { - outConn = upgraded - } + outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) + if err != nil { + _ = c.Close() + return nil, err } return outConn, nil } @@ -694,16 +686,10 @@ func dialStreamSplit(ctx context.Context, serverAddress string, opts TunnelDialO if c == nil { return nil, fmt.Errorf("failed to build stream split conn") } - outConn := net.Conn(c) - if opts.Upgrade != nil { - upgraded, err := opts.Upgrade(c) - if err != nil { - _ = c.Close() - return nil, err - } - if upgraded != nil { - outConn = upgraded - } + outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) + if err != nil { + _ = c.Close() + return nil, err } return outConn, nil } @@ -1120,16 +1106,10 @@ func dialPollWithClient(ctx context.Context, client *http.Client, target httpCli if c == nil { return nil, fmt.Errorf("failed to build poll conn") } - outConn := net.Conn(c) - if opts.Upgrade != nil { - upgraded, err := opts.Upgrade(c) - if err != nil { - _ = c.Close() - return nil, err - } - if upgraded != nil { - outConn = upgraded - } + outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) + if err != nil { + _ = c.Close() + return nil, err } return outConn, nil } @@ -1143,16 +1123,10 @@ func dialPoll(ctx context.Context, serverAddress string, opts TunnelDialOptions) if c == nil { return nil, fmt.Errorf("failed to build poll conn") } - outConn := net.Conn(c) - if opts.Upgrade != nil { - upgraded, err := opts.Upgrade(c) - if err != nil { - _ = c.Close() - return nil, err - } - if upgraded != nil { - outConn = upgraded - } + outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) + if err != nil { + _ = c.Close() + return nil, err } return outConn, nil } @@ -1528,6 +1502,8 @@ type TunnelServerOptions struct { PullReadTimeout time.Duration // SessionTTL is a best-effort TTL to prevent leaked sessions. 0 uses a conservative default. SessionTTL time.Duration + // EarlyHandshake optionally folds the protocol handshake into the initial HTTP/WS round trip. + EarlyHandshake *TunnelServerEarlyHandshake } type TunnelServer struct { @@ -1538,6 +1514,7 @@ type TunnelServer struct { pullReadTimeout time.Duration sessionTTL time.Duration + earlyHandshake *TunnelServerEarlyHandshake mu sync.Mutex sessions map[string]*tunnelSession @@ -1570,6 +1547,7 @@ func NewTunnelServer(opts TunnelServerOptions) *TunnelServer { passThroughOnReject: opts.PassThroughOnReject, pullReadTimeout: timeout, sessionTTL: ttl, + earlyHandshake: opts.EarlyHandshake, sessions: make(map[string]*tunnelSession), } } @@ -1925,9 +1903,12 @@ func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, he switch strings.ToUpper(req.method) { case http.MethodGet: - // Stream split-session: GET /session (no token) => token + start tunnel on a server-side pipe. if token == "" && path == "/session" { - return s.sessionAuthorize(rawConn) + earlyPayload, err := parseEarlyDataQuery(u) + if err != nil { + return rejectOrReply(http.StatusBadRequest, "bad request") + } + return s.sessionAuthorize(rawConn, earlyPayload) } // Stream split-session: GET /stream?token=... => downlink poll. if token != "" && path == "/stream" { @@ -2045,10 +2026,18 @@ func writeSimpleHTTPResponse(w io.Writer, code int, body string) error { func writeTokenHTTPResponse(w io.Writer, token string) error { token = strings.TrimRight(token, "\r\n") - // Use application/octet-stream to avoid CDN auto-compression (e.g. brotli) breaking clients that expect a plain token string. + return writeTokenHTTPResponseWithEarlyData(w, token, nil) +} + +func writeTokenHTTPResponseWithEarlyData(w io.Writer, token string, earlyPayload []byte) error { + token = strings.TrimRight(token, "\r\n") + body := "token=" + token + if len(earlyPayload) > 0 { + body += "\ned=" + base64.RawURLEncoding.EncodeToString(earlyPayload) + } _, err := io.WriteString(w, - fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nCache-Control: no-store\r\nPragma: no-cache\r\nContent-Length: %d\r\nConnection: close\r\n\r\ntoken=%s", - len("token=")+len(token), token)) + fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nCache-Control: no-store\r\nPragma: no-cache\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", + len(body), body)) return err } @@ -2088,7 +2077,11 @@ func (s *TunnelServer) handlePoll(rawConn net.Conn, req *httpRequestHeader, head switch strings.ToUpper(req.method) { case http.MethodGet: if token == "" && path == "/session" { - return s.sessionAuthorize(rawConn) + earlyPayload, err := parseEarlyDataQuery(u) + if err != nil { + return rejectOrReply(http.StatusBadRequest, "bad request") + } + return s.sessionAuthorize(rawConn, earlyPayload) } if token != "" && path == "/stream" { if s.passThroughOnReject && !s.sessionHas(token) { @@ -2128,7 +2121,7 @@ func (s *TunnelServer) handlePoll(rawConn net.Conn, req *httpRequestHeader, head } } -func (s *TunnelServer) sessionAuthorize(rawConn net.Conn) (HandleResult, net.Conn, error) { +func (s *TunnelServer) sessionAuthorize(rawConn net.Conn, earlyPayload []byte) (HandleResult, net.Conn, error) { token, err := newSessionToken() if err != nil { _ = writeSimpleHTTPResponse(rawConn, http.StatusInternalServerError, "internal error") @@ -2137,6 +2130,37 @@ func (s *TunnelServer) sessionAuthorize(rawConn net.Conn) (HandleResult, net.Con } c1, c2 := newHalfPipe() + outConn := net.Conn(c1) + var responsePayload []byte + var userHash string + if len(earlyPayload) > 0 && s.earlyHandshake != nil && s.earlyHandshake.Prepare != nil { + prepared, err := s.earlyHandshake.Prepare(earlyPayload) + if err != nil { + _ = c1.Close() + _ = c2.Close() + if s.passThroughOnReject { + return HandlePassThrough, newRejectedPreBufferedConn(rawConn, nil), nil + } + _ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found") + _ = rawConn.Close() + return HandleDone, nil, nil + } + responsePayload = prepared.ResponsePayload + userHash = prepared.UserHash + if prepared.WrapConn != nil { + wrapped, err := prepared.WrapConn(c1) + if err != nil { + _ = c1.Close() + _ = c2.Close() + _ = writeSimpleHTTPResponse(rawConn, http.StatusInternalServerError, "internal error") + _ = rawConn.Close() + return HandleDone, nil, nil + } + if wrapped != nil { + outConn = wrapEarlyHandshakeConn(wrapped, userHash) + } + } + } s.mu.Lock() s.sessions[token] = &tunnelSession{conn: c2, lastActive: time.Now()} @@ -2144,9 +2168,9 @@ func (s *TunnelServer) sessionAuthorize(rawConn net.Conn) (HandleResult, net.Con go s.reapLater(token) - _ = writeTokenHTTPResponse(rawConn, token) + _ = writeTokenHTTPResponseWithEarlyData(rawConn, token, responsePayload) _ = rawConn.Close() - return HandleStartTunnel, c1, nil + return HandleStartTunnel, outConn, nil } func newSessionToken() (string, error) { diff --git a/clash-meta/transport/sudoku/obfs/httpmask/tunnel_ws.go b/clash-meta/transport/sudoku/obfs/httpmask/tunnel_ws.go index e1299e3da1..8ef8d5c3af 100644 --- a/clash-meta/transport/sudoku/obfs/httpmask/tunnel_ws.go +++ b/clash-meta/transport/sudoku/obfs/httpmask/tunnel_ws.go @@ -2,6 +2,7 @@ package httpmask import ( "context" + "encoding/base64" "fmt" "io" mrand "math/rand" @@ -115,6 +116,16 @@ func dialWS(ctx context.Context, serverAddress string, opts TunnelDialOptions) ( Host: urlHost, Path: joinPathRoot(opts.PathRoot, "/ws"), } + if opts.EarlyHandshake != nil && len(opts.EarlyHandshake.RequestPayload) > 0 { + rawURL, err := setEarlyDataQuery(u.String(), opts.EarlyHandshake.RequestPayload) + if err != nil { + return nil, err + } + u, err = url.Parse(rawURL) + if err != nil { + return nil, err + } + } header := make(stdhttp.Header) applyWSHeaders(header, headerHost) @@ -132,6 +143,16 @@ func dialWS(ctx context.Context, serverAddress string, opts TunnelDialOptions) ( d := ws.Dialer{ Host: headerHost, Header: ws.HandshakeHeaderHTTP(header), + OnHeader: func(key, value []byte) error { + if !strings.EqualFold(string(key), tunnelEarlyDataHeader) || opts.EarlyHandshake == nil || opts.EarlyHandshake.HandleResponse == nil { + return nil + } + decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(string(value))) + if err != nil { + return err + } + return opts.EarlyHandshake.HandleResponse(decoded) + }, NetDial: func(dialCtx context.Context, network, addr string) (net.Conn, error) { if addr == urlHost { addr = dialAddr @@ -161,16 +182,10 @@ func dialWS(ctx context.Context, serverAddress string, opts TunnelDialOptions) ( } wsConn := newWSStreamConn(conn, ws.StateClientSide) - if opts.Upgrade == nil { - return wsConn, nil - } - upgraded, err := opts.Upgrade(wsConn) + upgraded, err := applyEarlyHandshakeOrUpgrade(wsConn, opts) if err != nil { _ = wsConn.Close() return nil, err } - if upgraded != nil { - return upgraded, nil - } - return wsConn, nil + return upgraded, nil } diff --git a/clash-meta/transport/sudoku/obfs/httpmask/tunnel_ws_server.go b/clash-meta/transport/sudoku/obfs/httpmask/tunnel_ws_server.go index 3e79e58aff..b17b1ded3d 100644 --- a/clash-meta/transport/sudoku/obfs/httpmask/tunnel_ws_server.go +++ b/clash-meta/transport/sudoku/obfs/httpmask/tunnel_ws_server.go @@ -1,6 +1,7 @@ package httpmask import ( + "encoding/base64" "net" "net/http" "net/url" @@ -63,15 +64,46 @@ func (s *TunnelServer) handleWS(rawConn net.Conn, req *httpRequestHeader, header return rejectOrReply(http.StatusNotFound, "not found") } + earlyPayload, err := parseEarlyDataQuery(u) + if err != nil { + return rejectOrReply(http.StatusBadRequest, "bad request") + } + var prepared *PreparedServerEarlyHandshake + if len(earlyPayload) > 0 && s.earlyHandshake != nil && s.earlyHandshake.Prepare != nil { + prepared, err = s.earlyHandshake.Prepare(earlyPayload) + if err != nil { + return rejectOrReply(http.StatusNotFound, "not found") + } + } + prefix := make([]byte, 0, len(headerBytes)+len(buffered)) prefix = append(prefix, headerBytes...) prefix = append(prefix, buffered...) wsConnRaw := newPreBufferedConn(rawConn, prefix) - if _, err := ws.Upgrade(wsConnRaw); err != nil { + upgrader := ws.Upgrader{} + if prepared != nil && len(prepared.ResponsePayload) > 0 { + upgrader.OnBeforeUpgrade = func() (ws.HandshakeHeader, error) { + h := http.Header{} + h.Set(tunnelEarlyDataHeader, base64.RawURLEncoding.EncodeToString(prepared.ResponsePayload)) + return ws.HandshakeHeaderHTTP(h), nil + } + } + if _, err := upgrader.Upgrade(wsConnRaw); err != nil { _ = rawConn.Close() return HandleDone, nil, nil } - return HandleStartTunnel, newWSStreamConn(wsConnRaw, ws.StateServerSide), nil + outConn := net.Conn(newWSStreamConn(wsConnRaw, ws.StateServerSide)) + if prepared != nil && prepared.WrapConn != nil { + wrapped, err := prepared.WrapConn(outConn) + if err != nil { + _ = outConn.Close() + return HandleDone, nil, nil + } + if wrapped != nil { + outConn = wrapEarlyHandshakeConn(wrapped, prepared.UserHash) + } + } + return HandleStartTunnel, outConn, nil } diff --git a/clash-meta/transport/sudoku/obfs/sudoku/packed.go b/clash-meta/transport/sudoku/obfs/sudoku/packed.go index 346314a32d..0edf4f32b4 100644 --- a/clash-meta/transport/sudoku/obfs/sudoku/packed.go +++ b/clash-meta/transport/sudoku/obfs/sudoku/packed.go @@ -11,35 +11,35 @@ import ( ) const ( - // 每次从 RNG 获取批量随机数的缓存大小,减少 RNG 函数调用开销 RngBatchSize = 128 + + packedProtectedPrefixBytes = 14 ) -// 1. 使用 12字节->16组 的块处理优化 Write (减少循环开销) -// 2. 使用整数阈值随机概率判断 Padding,与纯 Sudoku 保持流量特征一致 -// 3. Read 使用 copy 移动避免底层数组泄漏 +// PackedConn encodes traffic with the packed Sudoku layout while preserving +// the same padding model as the regular connection. type PackedConn struct { net.Conn table *Table reader *bufio.Reader - // 读缓冲 + // Read-side buffers. rawBuf []byte - pendingData []byte // 解码后尚未被 Read 取走的字节 + pendingData []byte - // 写缓冲与状态 + // Write-side state. writeMu sync.Mutex writeBuf []byte - bitBuf uint64 // 暂存的位数据 - bitCount int // 暂存的位数 + bitBuf uint64 + bitCount int - // 读状态 + // Read-side bit accumulator. readBitBuf uint64 readBits int - // 随机数与填充控制 - 使用整数阈值随机,与 Conn 一致 + // Padding selection matches Conn's threshold-based model. rng *rand.Rand - paddingThreshold uint64 // 与 Conn 保持一致的随机概率模型 + paddingThreshold uint64 padMarker byte padPool []byte } @@ -95,7 +95,6 @@ func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn { return pc } -// maybeAddPadding 内联辅助:根据概率阈值插入 padding func (pc *PackedConn) maybeAddPadding(out []byte) []byte { if shouldPad(pc.rng, pc.paddingThreshold) { out = append(out, pc.getPaddingByte()) @@ -103,7 +102,73 @@ func (pc *PackedConn) maybeAddPadding(out []byte) []byte { return out } -// Write 极致优化版 - 批量处理 12 字节 +func (pc *PackedConn) appendGroup(out []byte, group byte) []byte { + out = pc.maybeAddPadding(out) + return append(out, pc.encodeGroup(group)) +} + +func (pc *PackedConn) appendForcedPadding(out []byte) []byte { + return append(out, pc.getPaddingByte()) +} + +func (pc *PackedConn) nextProtectedPrefixGap() int { + return 1 + pc.rng.Intn(2) +} + +func (pc *PackedConn) writeProtectedPrefix(out []byte, p []byte) ([]byte, int) { + if len(p) == 0 { + return out, 0 + } + + limit := len(p) + if limit > packedProtectedPrefixBytes { + limit = packedProtectedPrefixBytes + } + + for padCount := 0; padCount < 1+pc.rng.Intn(2); padCount++ { + out = pc.appendForcedPadding(out) + } + + gap := pc.nextProtectedPrefixGap() + effective := 0 + for i := 0; i < limit; i++ { + pc.bitBuf = (pc.bitBuf << 8) | uint64(p[i]) + pc.bitCount += 8 + for pc.bitCount >= 6 { + pc.bitCount -= 6 + group := byte(pc.bitBuf >> pc.bitCount) + if pc.bitCount == 0 { + pc.bitBuf = 0 + } else { + pc.bitBuf &= (1 << pc.bitCount) - 1 + } + out = pc.appendGroup(out, group&0x3F) + } + + effective++ + if effective >= gap { + out = pc.appendForcedPadding(out) + effective = 0 + gap = pc.nextProtectedPrefixGap() + } + } + + return out, limit +} + +func (pc *PackedConn) drainPendingData(dst []byte) int { + n := copy(dst, pc.pendingData) + if n == len(pc.pendingData) { + pc.pendingData = pc.pendingData[:0] + return n + } + + remaining := len(pc.pendingData) - n + copy(pc.pendingData, pc.pendingData[n:]) + pc.pendingData = pc.pendingData[:remaining] + return n +} + func (pc *PackedConn) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil @@ -112,20 +177,19 @@ func (pc *PackedConn) Write(p []byte) (int, error) { pc.writeMu.Lock() defer pc.writeMu.Unlock() - // 1. 预分配内存,避免 append 导致的多次扩容 - // 预估:原数据 * 1.5 (4/3 + padding 余量) needed := len(p)*3/2 + 32 if cap(pc.writeBuf) < needed { pc.writeBuf = make([]byte, 0, needed) } out := pc.writeBuf[:0] - i := 0 + var prefixN int + out, prefixN = pc.writeProtectedPrefix(out, p) + + i := prefixN n := len(p) - // 2. 头部对齐处理 (Slow Path) for pc.bitCount > 0 && i < n { - out = pc.maybeAddPadding(out) b := p[i] i++ pc.bitBuf = (pc.bitBuf << 8) | uint64(b) @@ -138,14 +202,11 @@ func (pc *PackedConn) Write(p []byte) (int, error) { } else { pc.bitBuf &= (1 << pc.bitCount) - 1 } - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(group&0x3F)) + out = pc.appendGroup(out, group&0x3F) } } - // 3. 极速批量处理 (Fast Path) - 每次处理 12 字节 → 生成 16 个编码组 for i+11 < n { - // 处理 4 组,每组 3 字节 for batch := 0; batch < 4; batch++ { b1, b2, b3 := p[i], p[i+1], p[i+2] i += 3 @@ -155,19 +216,13 @@ func (pc *PackedConn) Write(p []byte) (int, error) { g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03) g4 := b3 & 0x3F - // 每个组之前都有概率插入 padding - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g1)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g2)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g3)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g4)) + out = pc.appendGroup(out, g1) + out = pc.appendGroup(out, g2) + out = pc.appendGroup(out, g3) + out = pc.appendGroup(out, g4) } } - // 4. 处理剩余的 3 字节块 for i+2 < n { b1, b2, b3 := p[i], p[i+1], p[i+2] i += 3 @@ -177,17 +232,12 @@ func (pc *PackedConn) Write(p []byte) (int, error) { g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03) g4 := b3 & 0x3F - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g1)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g2)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g3)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g4)) + out = pc.appendGroup(out, g1) + out = pc.appendGroup(out, g2) + out = pc.appendGroup(out, g3) + out = pc.appendGroup(out, g4) } - // 5. 尾部处理 (Tail Path) - 处理剩余的 1 或 2 个字节 for ; i < n; i++ { b := p[i] pc.bitBuf = (pc.bitBuf << 8) | uint64(b) @@ -200,35 +250,28 @@ func (pc *PackedConn) Write(p []byte) (int, error) { } else { pc.bitBuf &= (1 << pc.bitCount) - 1 } - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(group&0x3F)) + out = pc.appendGroup(out, group&0x3F) } } - // 6. 处理残留位 if pc.bitCount > 0 { - out = pc.maybeAddPadding(out) group := byte(pc.bitBuf << (6 - pc.bitCount)) pc.bitBuf = 0 pc.bitCount = 0 - out = append(out, pc.encodeGroup(group&0x3F)) + out = pc.appendGroup(out, group&0x3F) out = append(out, pc.padMarker) } - // 尾部可能添加 padding out = pc.maybeAddPadding(out) - // 发送数据 if len(out) > 0 { - _, err := pc.Conn.Write(out) pc.writeBuf = out[:0] - return len(p), err + return len(p), writeFull(pc.Conn, out) } pc.writeBuf = out[:0] return len(p), nil } -// Flush 处理最后不足 6 bit 的情况 func (pc *PackedConn) Flush() error { pc.writeMu.Lock() defer pc.writeMu.Unlock() @@ -243,38 +286,34 @@ func (pc *PackedConn) Flush() error { out = append(out, pc.padMarker) } - // 尾部随机添加 padding out = pc.maybeAddPadding(out) if len(out) > 0 { - _, err := pc.Conn.Write(out) pc.writeBuf = out[:0] - return err + return writeFull(pc.Conn, out) } return nil } -// Read 优化版:减少切片操作,避免内存泄漏 -func (pc *PackedConn) Read(p []byte) (int, error) { - // 1. 优先返回待处理区的数据 - if len(pc.pendingData) > 0 { - n := copy(p, pc.pendingData) - if n == len(pc.pendingData) { - pc.pendingData = pc.pendingData[:0] - } else { - // 优化:移动剩余数据到数组头部,避免切片指向中间导致内存泄漏 - remaining := len(pc.pendingData) - n - copy(pc.pendingData, pc.pendingData[n:]) - pc.pendingData = pc.pendingData[:remaining] +func writeFull(w io.Writer, b []byte) error { + for len(b) > 0 { + n, err := w.Write(b) + if err != nil { + return err } - return n, nil + b = b[n:] + } + return nil +} + +func (pc *PackedConn) Read(p []byte) (int, error) { + if len(pc.pendingData) > 0 { + return pc.drainPendingData(p), nil } - // 2. 循环读取直到解出数据或出错 for { nr, rErr := pc.reader.Read(pc.rawBuf) if nr > 0 { - // 缓存频繁访问的变量 rBuf := pc.readBitBuf rBits := pc.readBits padMarker := pc.padMarker @@ -324,24 +363,13 @@ func (pc *PackedConn) Read(p []byte) (int, error) { } } - // 3. 返回解码后的数据 - 优化:避免底层数组泄漏 - n := copy(p, pc.pendingData) - if n == len(pc.pendingData) { - pc.pendingData = pc.pendingData[:0] - } else { - remaining := len(pc.pendingData) - n - copy(pc.pendingData, pc.pendingData[n:]) - pc.pendingData = pc.pendingData[:remaining] - } - return n, nil + return pc.drainPendingData(p), nil } -// getPaddingByte 从 Pool 中随机取 Padding 字节 func (pc *PackedConn) getPaddingByte() byte { return pc.padPool[pc.rng.Intn(len(pc.padPool))] } -// encodeGroup 编码 6-bit 组 func (pc *PackedConn) encodeGroup(group byte) byte { return pc.table.layout.encodeGroup(group) } diff --git a/clash-meta/transport/sudoku/obfs/sudoku/packed_prefix_test.go b/clash-meta/transport/sudoku/obfs/sudoku/packed_prefix_test.go new file mode 100644 index 0000000000..f041c0f561 --- /dev/null +++ b/clash-meta/transport/sudoku/obfs/sudoku/packed_prefix_test.go @@ -0,0 +1,91 @@ +package sudoku + +import ( + "bytes" + "io" + "math/rand" + "net" + "testing" + "time" +) + +type mockConn struct { + readBuf []byte + writeBuf []byte +} + +func (c *mockConn) Read(p []byte) (int, error) { + if len(c.readBuf) == 0 { + return 0, io.EOF + } + n := copy(p, c.readBuf) + c.readBuf = c.readBuf[n:] + return n, nil +} + +func (c *mockConn) Write(p []byte) (int, error) { + c.writeBuf = append(c.writeBuf, p...) + return len(p), nil +} + +func (c *mockConn) Close() error { return nil } +func (c *mockConn) LocalAddr() net.Addr { return nil } +func (c *mockConn) RemoteAddr() net.Addr { return nil } +func (c *mockConn) SetDeadline(time.Time) error { return nil } +func (c *mockConn) SetReadDeadline(time.Time) error { return nil } +func (c *mockConn) SetWriteDeadline(time.Time) error { return nil } + +func TestPackedConn_ProtectedPrefixPadding(t *testing.T) { + table := NewTable("packed-prefix-seed", "prefer_ascii") + mock := &mockConn{} + writer := NewPackedConn(mock, table, 0, 0) + writer.rng = rand.New(rand.NewSource(1)) + + payload := bytes.Repeat([]byte{0}, 32) + if _, err := writer.Write(payload); err != nil { + t.Fatalf("write: %v", err) + } + + wire := append([]byte(nil), mock.writeBuf...) + if len(wire) < 20 { + t.Fatalf("wire too short: %d", len(wire)) + } + + firstHint := -1 + nonHintCount := 0 + maxHintRun := 0 + currentHintRun := 0 + for i, b := range wire[:20] { + if table.layout.isHint(b) { + if firstHint == -1 { + firstHint = i + } + currentHintRun++ + if currentHintRun > maxHintRun { + maxHintRun = currentHintRun + } + continue + } + nonHintCount++ + currentHintRun = 0 + } + + if firstHint < 1 || firstHint > 2 { + t.Fatalf("expected 1-2 leading padding bytes, first hint index=%d", firstHint) + } + if nonHintCount < 6 { + t.Fatalf("expected dense prefix padding, got only %d non-hint bytes in first 20", nonHintCount) + } + if maxHintRun > 3 { + t.Fatalf("prefix still exposes long hint run: %d", maxHintRun) + } + + reader := NewPackedConn(&mockConn{readBuf: wire}, table, 0, 0) + decoded := make([]byte, len(payload)) + if _, err := io.ReadFull(reader, decoded); err != nil { + t.Fatalf("read back: %v", err) + } + if !bytes.Equal(decoded, payload) { + t.Fatalf("roundtrip mismatch") + } +} diff --git a/clash-meta/transport/trusttunnel/force_close.go b/clash-meta/transport/trusttunnel/force_close.go index 3253b6c64c..d34b9376df 100644 --- a/clash-meta/transport/trusttunnel/force_close.go +++ b/clash-meta/transport/trusttunnel/force_close.go @@ -11,7 +11,7 @@ func forceCloseAllConnections(roundTripper RoundTripper) { roundTripper.CloseIdleConnections() switch tr := roundTripper.(type) { case *http.Http2Transport: - gun.CloseTransport(tr) + gun.CloseHttp2Transport(tr) case *http3.Transport: _ = tr.Close() } diff --git a/clash-nyanpasu/.github/workflows/ci.yml b/clash-nyanpasu/.github/workflows/ci.yml index 5c774d3984..859c4c1423 100644 --- a/clash-nyanpasu/.github/workflows/ci.yml +++ b/clash-nyanpasu/.github/workflows/ci.yml @@ -144,6 +144,11 @@ jobs: with: node-version: 24 + - name: Install Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + - uses: Swatinem/rust-cache@v2 name: Cache Rust dependencies with: @@ -231,6 +236,11 @@ jobs: with: node-version: 24 + - name: Install Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + - uses: Swatinem/rust-cache@v2 name: Cache Rust dependencies with: diff --git a/clash-nyanpasu/.vscode/extensions.json b/clash-nyanpasu/.vscode/extensions.json index 22e6d38a5e..7c24fc805f 100644 --- a/clash-nyanpasu/.vscode/extensions.json +++ b/clash-nyanpasu/.vscode/extensions.json @@ -3,7 +3,6 @@ "inlang.vs-code-extension", "editorconfig.editorconfig", "vadimcn.vscode-lldb", - "bungcip.better-toml", "denoland.vscode-deno", "esbenp.prettier-vscode", "yoavbls.pretty-ts-errors", @@ -11,6 +10,7 @@ "syler.sass-indented", "stylelint.vscode-stylelint", "bradlc.vscode-tailwindcss", - "oxc.oxc-vscode" + "oxc.oxc-vscode", + "tamasfe.even-better-toml" ] } diff --git a/clash-nyanpasu/backend/tauri/src/window.rs b/clash-nyanpasu/backend/tauri/src/window.rs index 74fe39cfde..709c1bf979 100644 --- a/clash-nyanpasu/backend/tauri/src/window.rs +++ b/clash-nyanpasu/backend/tauri/src/window.rs @@ -780,14 +780,30 @@ pub trait AppWindow { let state = match current_monitor { Some(_) => { + let maximized = win.is_maximized()?; + let fullscreen = win.is_fullscreen()?; + let is_minimized = win.is_minimized()?; + let size = win.inner_size()?; + + // During system shutdown, Windows sends resize events with 0x0 dimensions. + // Skip saving in this case to preserve the last valid window state. + if size.width == 0 || size.height == 0 { + if !maximized && !fullscreen && !is_minimized { + tracing::debug!( + "skipping window state save: invalid size {}x{} in normal state", + size.width, + size.height + ); + return Ok(()); + } + } + let mut state = WindowState { - maximized: win.is_maximized()?, - fullscreen: win.is_fullscreen()?, + maximized, + fullscreen, ..WindowState::default() }; - let is_minimized = win.is_minimized()?; - let size = win.inner_size()?; if size.width > 0 && size.height > 0 && !state.maximized && !is_minimized { state.width = size.width; state.height = size.height; diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 282f272ed5..4e9e7bb393 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -11,11 +11,13 @@ }, "dependencies": { "@dnd-kit/core": "6.3.1", + "@dnd-kit/helpers": "^0.3.2", + "@dnd-kit/react": "^0.3.2", "@dnd-kit/sortable": "10.0.0", "@dnd-kit/utilities": "3.2.2", "@emotion/styled": "11.14.1", "@hookform/resolvers": "5.2.2", - "@inlang/paraglide-js": "2.13.2", + "@inlang/paraglide-js": "2.14.0", "@juggle/resize-observer": "3.4.0", "@material/material-color-utilities": "0.4.0", "@mui/icons-material": "7.3.9", @@ -25,16 +27,7 @@ "@nyanpasu/interface": "workspace:^", "@nyanpasu/ui": "workspace:^", "@paper-design/shaders-react": "0.0.71", - "@radix-ui/react-context-menu": "2.2.16", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-scroll-area": "1.2.10", - "@radix-ui/react-select": "2.2.6", - "@radix-ui/react-separator": "1.1.8", - "@radix-ui/react-slot": "1.2.4", - "@radix-ui/react-switch": "1.2.6", - "@radix-ui/react-tooltip": "1.2.8", - "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-controllable-state": "^1.2.2", "@tailwindcss/postcss": "4.2.1", "@tanstack/react-table": "8.21.3", "@tanstack/react-virtual": "3.13.21", @@ -50,12 +43,13 @@ "country-emoji": "1.5.6", "dayjs": "1.11.19", "framer-motion": "12.35.2", - "i18next": "25.8.14", + "i18next": "25.8.17", "jotai": "2.18.1", "json-schema": "0.4.0", "material-react-table": "3.2.1", "monaco-editor": "0.55.1", "mui-color-input": "7.0.0", + "radix-ui": "^1.4.3", "react": "19.2.4", "react-dom": "19.2.4", "react-error-boundary": "6.0.0", @@ -75,12 +69,12 @@ "@csstools/normalize.css": "12.1.1", "@emotion/babel-plugin": "11.13.5", "@emotion/react": "11.14.0", - "@iconify/json": "2.2.447", + "@iconify/json": "2.2.448", "@monaco-editor/react": "4.7.0", "@tanstack/react-query": "5.90.21", - "@tanstack/react-router": "1.166.3", - "@tanstack/react-router-devtools": "1.166.3", - "@tanstack/router-plugin": "1.166.3", + "@tanstack/react-router": "1.166.6", + "@tanstack/react-router-devtools": "1.166.6", + "@tanstack/router-plugin": "1.166.6", "@tauri-apps/plugin-clipboard-manager": "2.3.2", "@tauri-apps/plugin-dialog": "2.6.0", "@tauri-apps/plugin-fs": "2.4.5", diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/button.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/button.tsx index 1ee2bbf4fd..b5264cc915 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/button.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/button.tsx @@ -1,9 +1,9 @@ import { cva, type VariantProps } from 'class-variance-authority' import { AnimatePresence, motion } from 'framer-motion' +import { Slot } from 'radix-ui' import { lazy, Suspense, useCallback } from 'react' import { chains } from '@/utils/chain' import { cn } from '@nyanpasu/ui' -import { Slot, Slottable } from '@radix-ui/react-slot' import { CircularProgress } from './progress' import { useRipple } from './ripple' @@ -134,7 +134,7 @@ export const Button = ({ onClick, ...props }: ButtonProps) => { - const Comp = asChild ? Slot : 'button' + const Comp = asChild ? Slot.Root : 'button' const ripple = useRipple() @@ -161,7 +161,7 @@ export const Button = ({ data-loading={String(Boolean(loading))} {...props} > - {children} + {children} {loading && ( diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/card.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/card.tsx index a422bc1460..519a2028f7 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/card.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/card.tsx @@ -1,7 +1,7 @@ import { cva, type VariantProps } from 'class-variance-authority' +import { Slot } from 'radix-ui' import { createContext, HTMLAttributes, useContext } from 'react' import { cn } from '@nyanpasu/ui' -import { Slot } from '@radix-ui/react-slot' export const cardVariants = cva('rounded-3xl text-on-surface overflow-hidden', { variants: { @@ -104,7 +104,7 @@ export const Card = ({ className, ...props }: CardProps) => { - const Comp = asChild ? Slot : 'div' + const Comp = asChild ? Slot.Root : 'div' return ( { - const Comp = asChild ? Slot : 'div' + const Comp = asChild ? Slot.Root : 'div' return } diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/context-menu.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/context-menu.tsx index 3fafa95a85..4b61e0b0a0 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/context-menu.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/context-menu.tsx @@ -1,9 +1,9 @@ import ArrowRight from '~icons/material-symbols/arrow-right-rounded' import Check from '~icons/material-symbols/check-rounded' import { AnimatePresence, motion } from 'framer-motion' +import { ContextMenu as ContextMenuPrimitive } from 'radix-ui' import { ComponentProps, createContext, useContext } from 'react' import { cn } from '@nyanpasu/ui' -import * as ContextMenuPrimitive from '@radix-ui/react-context-menu' import { useControllableState } from '@radix-ui/react-use-controllable-state' const MotionContent = ({ diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/dropdown-menu.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/dropdown-menu.tsx index e74c93187d..f0de663250 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/dropdown-menu.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/dropdown-menu.tsx @@ -3,9 +3,9 @@ import Check from '~icons/material-symbols/check-rounded' import RadioChecked from '~icons/material-symbols/radio-button-checked' import Radio from '~icons/material-symbols/radio-button-unchecked' import { AnimatePresence, motion } from 'framer-motion' +import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui' import { ComponentProps, createContext, useContext } from 'react' import { cn } from '@nyanpasu/ui' -import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' import { useControllableState } from '@radix-ui/react-use-controllable-state' const MotionContent = ({ diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/modal.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/modal.tsx index c44440ea06..ff183ec98d 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/ui/modal.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/ui/modal.tsx @@ -1,8 +1,7 @@ import { AnimatePresence, motion } from 'framer-motion' +import { Dialog as DialogPrimitive, Slot } from 'radix-ui' import { ComponentProps, createContext, useContext, useId } from 'react' import { cn } from '@nyanpasu/ui' -import * as DialogPrimitive from '@radix-ui/react-dialog' -import { Slot, Slottable } from '@radix-ui/react-slot' import { useControllableState } from '@radix-ui/react-use-controllable-state' import { Button, type ButtonProps } from './button' @@ -37,7 +36,7 @@ export function ModalTrigger({ }: ComponentProps) { const { layoutId } = useModalContext() - const Comp = asChild ? Slot : 'button' + const Comp = asChild ? Slot.Root : 'button' return ( - {children} + {children}
) { +}: React.ComponentProps) { return ( (null) + useEffect(() => { if (action === Action.ImportLocalProfile) { // if the current path is the index page, open the modal immediately @@ -85,6 +91,7 @@ export default function LocalProfileButton({ children }: PropsWithChildren) { clearTimeout(timeout) } } + // oxlint-disable-next-line eslint-plugin-react-hooks/exhaustive-deps }, [action]) const form = useForm>({ @@ -103,7 +110,7 @@ export default function LocalProfileButton({ children }: PropsWithChildren) { type: 'local', ...data, }, - fileData: null, + fileData: profileContent || ProfileTemplate.profile, }, }) @@ -206,7 +213,9 @@ export default function LocalProfileButton({ children }: PropsWithChildren) { onChange={(name) => { form.setValue('desc', name) }} - onFileRead={(value) => field.onChange(value)} + onFileRead={(value) => { + setProfileContent(value) + }} disabled={blockTask.isPending} > @@ -232,7 +241,7 @@ export default function LocalProfileButton({ children }: PropsWithChildren) {
{m.profile_import_local_file_size_label({ - size: filesize(form.watch('file')?.length ?? 0), + size: filesize(profileContent?.length ?? 0), })}
diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/profiles-header.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/profiles-header.tsx index e5abf7a6f7..8fed117be7 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/profiles-header.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/profiles-header.tsx @@ -36,7 +36,7 @@ export default function ProfilesHeader() {
diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/profiles-list.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/profiles-list.tsx index 15be67872b..4fa1a563b9 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/profiles-list.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/profiles/$type/_modules/profiles-list.tsx @@ -1,7 +1,8 @@ import DeleteForeverOutlineRounded from '~icons/material-symbols/delete-forever-outline-rounded' import DragClickRounded from '~icons/material-symbols/drag-click-rounded' import { AnimatePresence, motion } from 'framer-motion' -import { ComponentProps } from 'react' +import { isEqual } from 'lodash-es' +import { ComponentProps, useRef } from 'react' import { RegisterContextMenu, RegisterContextMenuContent, @@ -14,6 +15,9 @@ import { ContextMenuItem } from '@/components/ui/context-menu' import { LinearProgress } from '@/components/ui/progress' import TextMarquee from '@/components/ui/text-marquee' import { m } from '@/paraglide/messages' +import { move } from '@dnd-kit/helpers' +import { DragDropProvider } from '@dnd-kit/react' +import { useSortable } from '@dnd-kit/react/sortable' import { hexFromArgb } from '@material/material-color-utilities' import { Profile, useProfile } from '@nyanpasu/interface' import { cn } from '@nyanpasu/ui' @@ -38,7 +42,13 @@ const Chip = ({ children, className, ...props }: ComponentProps<'span'>) => { ) } -const GridViewProfile = ({ profile }: { profile: Profile }) => { +const GridViewProfile = ({ + profile, + index, +}: { + profile: Profile + index: number +}) => { const { type } = IndexRoute.useParams() const activeProfile = useActiveProfile(profile) @@ -50,84 +60,95 @@ const GridViewProfile = ({ profile }: { profile: Profile }) => { const { themePalette } = useExperimentalThemeContext() + const cardRef = useRef(null) + + const { isDragging: _isDragging } = useSortable({ + id: profile.uid, + index, + element: cardRef.current, + }) + return ( - - {isPending && ( - - +
+ + {isPending && ( + + -

- {m.profile_pending_mask_message()} -

-
- )} -
- - {activeProfile.isActive && ( - - hexFromArgb(color), +

+ {m.profile_pending_mask_message()} +

+ )} - distortion={0.5} - swirl={0.1} - grainMixer={0} - grainOverlay={0} - speed={1 / 3} - /> - )} - - - - {profile.name} - + {activeProfile.isActive && ( - {m.profile_is_active_label()} + + hexFromArgb(color), + )} + distortion={0.5} + swirl={0.1} + grainMixer={0} + grainOverlay={0} + speed={1 / 3} + /> )} - - -
- {isRemote ? ( - {m.profile_remote_label()} - ) : ( - {m.profile_local_label()} + + + {profile.name} + + + {activeProfile.isActive && ( + {m.profile_is_active_label()} )} -
-
+ - - - + +
+ {isRemote ? ( + {m.profile_remote_label()} + ) : ( + {m.profile_local_label()} + )} +
+
+ + + + +
@@ -183,6 +204,7 @@ export default function ProfilesList({ const { query: { data: profiles }, + sort, } = useProfile() // Type guard: restrict type to the allowed PROFILE_TYPES keys @@ -226,21 +248,39 @@ export default function ProfilesList({ 'sm:min-h-[calc(100vh-40px-48px)]', )} > -
{ + const currentUids = filteredProfiles.map((profile) => profile.uid) + + const nextUids = move(currentUids, event) + + if (isEqual(currentUids, nextUids)) { + return + } + + sort.mutate(nextUids) + }} > - {filteredProfiles.map((profile) => ( - - ))} -
+
+ {filteredProfiles.map((profile, index) => ( + + ))} +
+
diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/settings/_modules/settings-title.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/settings/_modules/settings-title.tsx index f8500f7436..8ee97d1a26 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/settings/_modules/settings-title.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/(main)/main/settings/_modules/settings-title.tsx @@ -48,7 +48,7 @@ export function SettingsTitle({ const id = useId() - const showTopTitle = offset.top > 60 + const showTopTitle = offset.top > 40 return ( <> diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json index 6468619303..bfd0cb0da7 100644 --- a/clash-nyanpasu/manifest/version.json +++ b/clash-nyanpasu/manifest/version.json @@ -1,8 +1,8 @@ { "manifest_version": 1, "latest": { - "mihomo": "v1.19.20", - "mihomo_alpha": "alpha-4c35436", + "mihomo": "v1.19.21", + "mihomo_alpha": "alpha-e28fe24", "clash_rs": "v0.9.4", "clash_premium": "2023-09-05-gdcc8d87", "clash_rs_alpha": "latest" @@ -69,5 +69,5 @@ "linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf" } }, - "updated_at": "2026-03-08T22:22:02.470Z" + "updated_at": "2026-03-09T22:22:50.198Z" } diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json index c0a25a9767..1199c873f0 100644 --- a/clash-nyanpasu/package.json +++ b/clash-nyanpasu/package.json @@ -69,9 +69,9 @@ "dedent": "1.7.2", "globals": "17.4.0", "knip": "5.86.0", - "lint-staged": "16.3.2", + "lint-staged": "16.3.3", "npm-run-all2": "8.0.4", - "oxlint": "1.51.0", + "oxlint": "1.52.0", "postcss": "8.5.8", "postcss-html": "1.8.1", "postcss-import": "16.1.1", @@ -85,13 +85,13 @@ "stylelint-config-recess-order": "7.6.1", "stylelint-config-standard": "40.0.0", "stylelint-declaration-block-no-ignored-properties": "3.0.0", - "stylelint-order": "7.0.1", + "stylelint-order": "8.0.0", "stylelint-scss": "7.0.0", "tailwindcss": "4.2.1", "tsx": "4.21.0", "typescript": "5.9.3" }, - "packageManager": "pnpm@10.31.0", + "packageManager": "pnpm@10.32.0", "engines": { "node": "24.14.0" }, diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index b7ed621b3b..5d0fe5cd02 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -62,14 +62,14 @@ importers: specifier: 5.86.0 version: 5.86.0(@types/node@24.11.0)(typescript@5.9.3) lint-staged: - specifier: 16.3.2 - version: 16.3.2 + specifier: 16.3.3 + version: 16.3.3 npm-run-all2: specifier: 8.0.4 version: 8.0.4 oxlint: - specifier: 1.51.0 - version: 1.51.0 + specifier: 1.52.0 + version: 1.52.0 postcss: specifier: 8.5.8 version: 8.5.8 @@ -102,7 +102,7 @@ importers: version: 1.1.0(postcss-html@1.8.1)(stylelint@17.4.0(typescript@5.9.3)) stylelint-config-recess-order: specifier: 7.6.1 - version: 7.6.1(stylelint-order@7.0.1(stylelint@17.4.0(typescript@5.9.3)))(stylelint@17.4.0(typescript@5.9.3)) + version: 7.6.1(stylelint-order@8.0.0(stylelint@17.4.0(typescript@5.9.3)))(stylelint@17.4.0(typescript@5.9.3)) stylelint-config-standard: specifier: 40.0.0 version: 40.0.0(stylelint@17.4.0(typescript@5.9.3)) @@ -110,8 +110,8 @@ importers: specifier: 3.0.0 version: 3.0.0(stylelint@17.4.0(typescript@5.9.3)) stylelint-order: - specifier: 7.0.1 - version: 7.0.1(stylelint@17.4.0(typescript@5.9.3)) + specifier: 8.0.0 + version: 8.0.0(stylelint@17.4.0(typescript@5.9.3)) stylelint-scss: specifier: 7.0.0 version: 7.0.0(stylelint@17.4.0(typescript@5.9.3)) @@ -164,6 +164,12 @@ importers: '@dnd-kit/core': specifier: 6.3.1 version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@dnd-kit/helpers': + specifier: ^0.3.2 + version: 0.3.2 + '@dnd-kit/react': + specifier: ^0.3.2 + version: 0.3.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@dnd-kit/sortable': specifier: 10.0.0 version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) @@ -177,8 +183,8 @@ importers: specifier: 5.2.2 version: 5.2.2(react-hook-form@7.71.2(react@19.2.4)) '@inlang/paraglide-js': - specifier: 2.13.2 - version: 2.13.2(babel-plugin-macros@3.1.0) + specifier: 2.14.0 + version: 2.14.0(babel-plugin-macros@3.1.0) '@juggle/resize-observer': specifier: 3.4.0 version: 3.4.0 @@ -206,35 +212,8 @@ importers: '@paper-design/shaders-react': specifier: 0.0.71 version: 0.0.71(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context-menu': - specifier: 2.2.16 - version: 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-dialog': - specifier: 1.1.15 - version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-dropdown-menu': - specifier: 2.1.16 - version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-scroll-area': - specifier: 1.2.10 - version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-select': - specifier: 2.2.6 - version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-separator': - specifier: 1.1.8 - version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': - specifier: 1.2.4 - version: 1.2.4(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-switch': - specifier: 1.2.6 - version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-tooltip': - specifier: 1.2.8 - version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-use-controllable-state': - specifier: 1.2.2 + specifier: ^1.2.2 version: 1.2.2(@types/react@19.2.14)(react@19.2.4) '@tailwindcss/postcss': specifier: 4.2.1 @@ -247,7 +226,7 @@ importers: version: 3.13.21(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/router-zod-adapter': specifier: 1.81.5 - version: 1.81.5(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(zod@4.3.6) + version: 1.81.5(@tanstack/react-router@1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(zod@4.3.6) '@tauri-apps/api': specifier: 2.10.1 version: 2.10.1 @@ -282,8 +261,8 @@ importers: specifier: 12.35.2 version: 12.35.2(@emotion/is-prop-valid@1.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) i18next: - specifier: 25.8.14 - version: 25.8.14(typescript@5.9.3) + specifier: 25.8.17 + version: 25.8.17(typescript@5.9.3) jotai: specifier: 2.18.1 version: 2.18.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4) @@ -299,6 +278,9 @@ importers: mui-color-input: specifier: 7.0.0 version: 7.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: 19.2.4 version: 19.2.4 @@ -319,7 +301,7 @@ importers: version: 8.2.0(33a3b7a41cb869607c091202280d085c) react-i18next: specifier: 15.7.4 - version: 15.7.4(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + version: 15.7.4(i18next@25.8.17(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.4) @@ -352,8 +334,8 @@ importers: specifier: 11.14.0 version: 11.14.0(@types/react@19.2.14)(react@19.2.4) '@iconify/json': - specifier: 2.2.447 - version: 2.2.447 + specifier: 2.2.448 + version: 2.2.448 '@monaco-editor/react': specifier: 4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -361,14 +343,14 @@ importers: specifier: 5.90.21 version: 5.90.21(react@19.2.4) '@tanstack/react-router': - specifier: 1.166.3 - version: 1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 1.166.6 + version: 1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-router-devtools': - specifier: 1.166.3 - version: 1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.166.2)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 1.166.6 + version: 1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.166.6)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/router-plugin': - specifier: 1.166.3 - version: 1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)) + specifier: 1.166.6 + version: 1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)) '@tauri-apps/plugin-clipboard-manager': specifier: 2.3.2 version: 2.3.2 @@ -518,7 +500,7 @@ importers: version: 6.0.0(react@19.2.4) react-i18next: specifier: 15.7.4 - version: 15.7.4(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + version: 15.7.4(i18next@25.8.17(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) react-use: specifier: 17.6.0 version: 17.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -1382,23 +1364,47 @@ packages: resolution: {integrity: sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==} engines: {node: '>=14'} + '@dnd-kit/abstract@0.3.2': + resolution: {integrity: sha512-uvPVK+SZYD6Viddn9M0K0JQdXknuVSxA/EbMlFRanve3P/XTc18oLa5zGftKSGjfQGmuzkZ34E26DSbly1zi3Q==} + '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: react: '>=16.8.0' + '@dnd-kit/collision@0.3.2': + resolution: {integrity: sha512-pNmNSLCI8S9fNQ7QJ3fBCDjiT0sqBhUFcKgmyYaGvGCAU+kq0AP8OWlh0JSisc9k5mFyxmRpmFQcnJpILz/RPA==} + '@dnd-kit/core@6.3.1': resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' + '@dnd-kit/dom@0.3.2': + resolution: {integrity: sha512-cIUAVgt2szQyz6JRy7I+0r+xeyOAGH21Y15hb5bIyHoDEaZBvIDH+OOlD9eoLjCbsxDLN9WloU2CBi3OE6LYDg==} + + '@dnd-kit/geometry@0.3.2': + resolution: {integrity: sha512-3UBPuIS7E3oGiHxOE8h810QA+0pnrnCtGxl4Os1z3yy5YkC/BEYGY+TxWPTQaY1/OMV7GCX7ZNMlama2QN3n3w==} + + '@dnd-kit/helpers@0.3.2': + resolution: {integrity: sha512-pj7pCE6BiysNetpPnzb3BJOrcKiqueUr1LFg6wYoi2fIFYpz66n2Ojd7HTwfwkpv0oyC3QlvA6Dk8cOmi6VavA==} + + '@dnd-kit/react@0.3.2': + resolution: {integrity: sha512-1Opg1xw6I75Z95c+rF2NJa0pdGb8rLAENtuopKtJ1J0PudWlz+P6yL137xy/6DV43uaRmNGtsdbMbR0yRYJ72g==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + '@dnd-kit/sortable@10.0.0': resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} peerDependencies: '@dnd-kit/core': ^6.3.0 react: '>=16.8.0' + '@dnd-kit/state@0.3.2': + resolution: {integrity: sha512-dLUIkoYrIJhGXfF2wGLTfb46vUokEsO/OoE21TSfmahYrx7ysTmnwbePsznFaHlwgZhQEh6AlLvthLCeY21b1A==} + '@dnd-kit/utilities@3.2.2': resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} peerDependencies: @@ -1686,8 +1692,8 @@ packages: prettier-plugin-ember-template-tag: optional: true - '@iconify/json@2.2.447': - resolution: {integrity: sha512-hRPz6U6eN0C959BrYb0rE+I6f7BjDIPfgt8C9Ln5FxZ1Hw3aJQ+su/qbWy/+yJU74lLlXuwaS0ZfE8Kam6NkeA==} + '@iconify/json@2.2.448': + resolution: {integrity: sha512-67zot0eJmV9qbap6pDyE+VBtje09kKoetE0zRwauScpSeG1AnCUkL4YdiT9BA6Nu78Np5CNAgF6rZJ6b7G6dNw==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -1695,8 +1701,8 @@ packages: '@iconify/utils@3.1.0': resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} - '@inlang/paraglide-js@2.13.2': - resolution: {integrity: sha512-ecxw95pmMbasVj7M/B6pu5wqYHomYQBcu3QzDl1svwAkbnRqRmsdrH4IizzFwqeVWd+uluibMIy1VOGywin94A==} + '@inlang/paraglide-js@2.14.0': + resolution: {integrity: sha512-6Tno8RvEhnALdgueWNQACiEm3YM6hAfbxnYB+JWML9p5s1O4t0DOqgU9YD8fwpixOnZbU6cJRkvt4v9acXDioA==} hasBin: true '@inlang/recommend-sherlock@0.2.1': @@ -2420,124 +2426,124 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.51.0': - resolution: {integrity: sha512-jJYIqbx4sX+suIxWstc4P7SzhEwb4ArWA2KVrmEuu9vH2i0qM6QIHz/ehmbGE4/2fZbpuMuBzTl7UkfNoqiSgw==} + '@oxlint/binding-android-arm-eabi@1.52.0': + resolution: {integrity: sha512-fW2pmR1VzFEdcvOYeSiv+R7CqffOjr9Bv5QmZaHuHJ4ZCqouaF6o48N/hJ3H1n9Zd8PCMFgJkeqUvUsVce01mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.51.0': - resolution: {integrity: sha512-GtXyBCcH4ti98YdiMNCrpBNGitx87EjEWxevnyhcBK12k/Vu4EzSB45rzSC4fGFUD6sQgeaxItRCEEWeVwPafw==} + '@oxlint/binding-android-arm64@1.52.0': + resolution: {integrity: sha512-ptuJljIB+klNi8//qxXyGD51NLJXY9lv40Olc7l3/pEyjejWwXGvGMO0GM6f0JsjmbnDL+VkX7RVQNhByaX8WA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.51.0': - resolution: {integrity: sha512-3QJbeYaMHn6Bh2XeBXuITSsbnIctyTjvHf5nRjKYrT9pPeErNIpp5VDEeAXC0CZSwSVTsc8WOSDwgrAI24JolQ==} + '@oxlint/binding-darwin-arm64@1.52.0': + resolution: {integrity: sha512-5d079Uw43BHVZzOwm3uJI2PgSbsZJTpfHDq2jMOR6rRjGiEBlgasaEvAA26VBqpkO1++/59ZCKLBnEpkro3zIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.51.0': - resolution: {integrity: sha512-NzErhMaTEN1cY0E8C5APy74lw5VwsNfJfVPBMWPVQLqAbO0k4FFLjvHURvkUL+Y18Wu+8Vs1kbqPh2hjXYA4pg==} + '@oxlint/binding-darwin-x64@1.52.0': + resolution: {integrity: sha512-vRTjnhPEHAyfUhO9w6GM1VkxeVXFcDs+huyB5YNMw+Py+6PRYDFFrrOEr0rZYcoGtSH25ScozZV8I1UXrzaDjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.51.0': - resolution: {integrity: sha512-msAIh3vPAoKoHlOE/oe6Q5C/n9umypv/k81lED82ibrJotn+3YG2Qp1kiR8o/Dg5iOEU97c6tl0utxcyFenpFw==} + '@oxlint/binding-freebsd-x64@1.52.0': + resolution: {integrity: sha512-vFthhhciRAliAjoKMsvi7UkkQp/EtMNhmCRYBuKsNiTH0k4H3SFfbuWWr80Q7+uTXijfBP91KO/EeF48RggC7A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.51.0': - resolution: {integrity: sha512-CqQPcvqYyMe9ZBot2stjGogEzk1z8gGAngIX7srSzrzexmXixwVxBdFZyxTVM0CjGfDeV+Ru0w25/WNjlMM2Hw==} + '@oxlint/binding-linux-arm-gnueabihf@1.52.0': + resolution: {integrity: sha512-qX3K4mKbju54ojUa8nigVxxZAUDBGu5MGzpoXvWmiw+7hafoQKaLAoTm94EqRlv9v27p864GQBgc4g3qYtMXXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.51.0': - resolution: {integrity: sha512-dstrlYQgZMnyOssxSbolGCge/sDbko12N/35RBNuqLpoPbft2aeBidBAb0dvQlyBd9RJ6u8D4o4Eh8Un6iTgyQ==} + '@oxlint/binding-linux-arm-musleabihf@1.52.0': + resolution: {integrity: sha512-x5D5/EUS9U4kndPncLB6mDfCsv7i8XcRLu0DZyTngXvyqapc96WwmyyOG2j8Dt26aE8Ykgh6AhsHp9bQtoBUAw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.51.0': - resolution: {integrity: sha512-QEjUpXO7d35rP1/raLGGbAsBLLGZIzV3ZbeSjqWlD3oRnxpRIZ6iL4o51XQHkconn3uKssc+1VKdtHJ81BBhDA==} + '@oxlint/binding-linux-arm64-gnu@1.52.0': + resolution: {integrity: sha512-2Ep1tnGLuGG7lUkKG/nilIJ0/T2rebEcATxMJ7afuhD6Z2Sc9dDcpX00IngAMyR9l6hXrvaOw9YA5HUAJVSENg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.51.0': - resolution: {integrity: sha512-YSJua5irtG4DoMAjUapDTPhkQLHhBIY0G9JqlZS6/SZPzqDkPku/1GdWs0D6h/wyx0Iz31lNCfIaWKBQhzP0wQ==} + '@oxlint/binding-linux-arm64-musl@1.52.0': + resolution: {integrity: sha512-54wxvb1Pztz0GMgTLUG9HsH8uhZSL4UbG7n4PDxWIRT9TygTVYKfD6D7iasYdKg6ZpWB5Y86VMxgjSJpR/Y7bQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.51.0': - resolution: {integrity: sha512-7L4Wj2IEUNDETKssB9IDYt16T6WlF+X2jgC/hBq3diGHda9vJLpAgb09+D3quFq7TdkFtI7hwz/jmuQmQFPc1Q==} + '@oxlint/binding-linux-ppc64-gnu@1.52.0': + resolution: {integrity: sha512-A82Zks1lJyLclrj8n2tJPHOw2ieZXCaBctnCarS1BRlPQMC1Y98vWCLqgvg9ssWy5ZAja0IjUHN1cYsp53mrqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.51.0': - resolution: {integrity: sha512-cBUHqtOXy76G41lOB401qpFoKx1xq17qYkhWrLSM7eEjiHM9sOtYqpr6ZdqCnN9s6ZpzudX4EkeHOFH2E9q0vA==} + '@oxlint/binding-linux-riscv64-gnu@1.52.0': + resolution: {integrity: sha512-ci89Ou+u9vnA0r4eQqGm/KPEkpea+QEtZCLKkrOAD/K5ZBwjS8ToID6aMgsDbIOJUNBGufsmX0iCC7EWrNKQFA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.51.0': - resolution: {integrity: sha512-WKbg8CysgZcHfZX0ixQFBRSBvFZUHa3SBnEjHY2FVYt2nbNJEjzTxA3ZR5wMU0NOCNKIAFUFvAh5/XJKPRJuJg==} + '@oxlint/binding-linux-riscv64-musl@1.52.0': + resolution: {integrity: sha512-3/+DVDWajFSu69TaYnKkoUgMEcHR3puO8TcBu3fPCKRhbLjgwDiYIVRdvQX0QaSjkNPJARmpYq7vlPHWNo2cUA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.51.0': - resolution: {integrity: sha512-N1QRUvJTxqXNSu35YOufdjsAVmKVx5bkrggOWAhTWBc3J4qjcBwr1IfyLh/6YCg8sYRSR1GraldS9jUgJL/U4A==} + '@oxlint/binding-linux-s390x-gnu@1.52.0': + resolution: {integrity: sha512-BU7CbceOh00NDmY1IYr72qZoj4sJVHB9DCL2tIq2vyNllNJIpZWTxqlzdqmC4FViXWMy8kZNkOa+SdauH+EcoQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.51.0': - resolution: {integrity: sha512-e0Mz0DizsCoqNIjeOg6OUKe8JKJWZ5zZlwsd05Bmr51Jo3AOL4UJnPvwKumr4BBtBrDZkCmOLhCvDGm95nJM2g==} + '@oxlint/binding-linux-x64-gnu@1.52.0': + resolution: {integrity: sha512-JUVZ6TKYl1yArS3xGsNLQlZxgVpjNKtZFja6VxSTDy2ToN7H58PiDRcxWoN2XoIcWlHSvK7pkIPFNOyzdEJ23A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.51.0': - resolution: {integrity: sha512-wD8HGTWhYBKXvRDvoBVB1y+fEYV01samhWQSy1Zkxq2vpezvMnjaFKRuiP6tBNITLGuffbNDEXOwcAhJ3gI5Ug==} + '@oxlint/binding-linux-x64-musl@1.52.0': + resolution: {integrity: sha512-IatLKG6UUbIbTBjBZ9SIAYp4SIvOpYIXPXn9cMLqWxh9HrHsu0fLNL+VQ67y4vdlIleYLeuIHkAp3M6saIN1RQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.51.0': - resolution: {integrity: sha512-5NSwQ2hDEJ0GPXqikjWtwzgAQCsS7P9aLMNenjjKa+gknN3lTCwwwERsT6lKXSirfU3jLjexA2XQvQALh5h27w==} + '@oxlint/binding-openharmony-arm64@1.52.0': + resolution: {integrity: sha512-CWgJ6FepHryuc/lgQWStFf3lcvEkbFLSa9zqO0D0QLVfrdg43I4XItKpL/bnfm4n7obzwgG8j8sBggdoxJQKfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.51.0': - resolution: {integrity: sha512-JEZyah1M0RHMw8d+jjSSJmSmO8sABA1J1RtrHYujGPeCkYg1NeH0TGuClpe2h5QtioRTaF57y/TZfn/2IFV6fA==} + '@oxlint/binding-win32-arm64-msvc@1.52.0': + resolution: {integrity: sha512-EuNAbPpctu8jYMZnvYh53Xw3YVY2nIi9bQlyMjY0eKiJxDv8ikHrAfcVcwTQW9xa5tp0eiMkmW7iHPP5CYUC9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.51.0': - resolution: {integrity: sha512-q3cEoKH6kwjz/WRyHwSf0nlD2F5Qw536kCXvmlSu+kaShzgrA0ojmh45CA81qL+7udfCaZL2SdKCZlLiGBVFlg==} + '@oxlint/binding-win32-ia32-msvc@1.52.0': + resolution: {integrity: sha512-wu3fquQttzSXwyy8DfdOG3Kyb17yAbRhwPlly7NHSXkrffAEAmZ6+o38tCNgsReGLugbn/wbq4uS4nEQubCq+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.51.0': - resolution: {integrity: sha512-Q14+fOGb9T28nWF/0EUsYqERiRA7cl1oy4TJrGmLaqhm+aO2cV+JttboHI3CbdeMCAyDI1+NoSlrM7Melhp/cw==} + '@oxlint/binding-win32-x64-msvc@1.52.0': + resolution: {integrity: sha512-wikx9I9J9/lPOZlrCCNgm8YjWkia8NZfhWd1TTvZTMguyChbw/oA2VEM6Fzx+kkpA+1qu5Mo7nrLdOXEJavw8g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2638,6 +2644,9 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@preact/signals-core@1.14.0': + resolution: {integrity: sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ==} + '@prettier/plugin-oxc@0.1.3': resolution: {integrity: sha512-aABz3zIRilpWMekbt1FL1JVBQrQLR8L4Td2SRctECrWSsXGTNn/G1BqNSKCdbvQS1LWstAXfqcXzDki7GAAJyg==} engines: {node: '>=14'} @@ -2648,6 +2657,45 @@ packages: '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-arrow@1.1.7': resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: @@ -2661,6 +2709,58 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -2775,6 +2875,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-id@1.1.1': resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} peerDependencies: @@ -2784,6 +2910,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-menu@2.1.16': resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} peerDependencies: @@ -2797,6 +2936,71 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.8': resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} peerDependencies: @@ -2875,6 +3079,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.11': resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} peerDependencies: @@ -2914,8 +3144,21 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-separator@1.1.8': - resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -2958,6 +3201,71 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.2.8': resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} peerDependencies: @@ -3007,6 +3315,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: @@ -3524,20 +3841,20 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.166.3': - resolution: {integrity: sha512-tmQMxCXBlaTjUfG5zlYPsB1bF9gFKULAOF1q6ePvFpsvhWz5bTmEdKPF2XIjd4D7alGM0MVB1DJGghmqigw7oA==} + '@tanstack/react-router-devtools@1.166.6': + resolution: {integrity: sha512-TheVyOgo8ljD8wHHLceFsnKrX7nhTIQv9WokSrPjNTP4H3synUMADxh8yZafVYdr6lS2CBvldd5s7JI8DcwBUg==} engines: {node: '>=20.19'} peerDependencies: - '@tanstack/react-router': ^1.166.3 - '@tanstack/router-core': ^1.166.2 + '@tanstack/react-router': ^1.166.6 + '@tanstack/router-core': ^1.166.6 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/react-router@1.166.3': - resolution: {integrity: sha512-5NOwAnEp+koHYaRkK5+biYiuOxnQe/7q8R7LLAJ5Ryk6hXoIimOv6gWimPxANwhCWg9spfRZCNswi8EQaidYBg==} + '@tanstack/react-router@1.166.6': + resolution: {integrity: sha512-lfymPCfTkLQaNj/KIPElt+6B9REwPw2/Of3KtMwhNINs7h2xFQMSAOYk+ItCv8i93lBczlg89BRHtRS99qmzyA==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -3568,30 +3885,30 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.166.2': - resolution: {integrity: sha512-zn3NhENOAX9ToQiX077UV2OH3aJKOvV2ZMNZZxZ3gDG3i3WqL8NfWfEgetEAfMN37/Mnt90PpotYgf7IyuoKqQ==} + '@tanstack/router-core@1.166.6': + resolution: {integrity: sha512-SwVPMQxjoY4jwiNgD9u5kDFp/iSaf3wgf1t93xRCC6qDHmv/xLaawhvwEPNIJaPepWuSYRpywpJWH9hGlBqVbw==} engines: {node: '>=20.19'} - '@tanstack/router-devtools-core@1.166.2': - resolution: {integrity: sha512-Ke8HquuwMhLYpo/6nxNgrzi9Ns2lsK9uwDba6WKA8I0K7fyYZoAUu+7AD6gdEcVU4NF6LjtMPfUCHmVtYYRTDw==} + '@tanstack/router-devtools-core@1.166.6': + resolution: {integrity: sha512-ndPnCDeSGW3bd33u3EMe3+EJGLiFOHZaIKRJRLdZClOB6J6pvzKMELJgizBtjaR6X56FdCsC/STgjPkOeR9paA==} engines: {node: '>=20.19'} peerDependencies: - '@tanstack/router-core': ^1.166.2 + '@tanstack/router-core': ^1.166.6 csstype: ^3.0.10 peerDependenciesMeta: csstype: optional: true - '@tanstack/router-generator@1.166.2': - resolution: {integrity: sha512-wbvdyP1PKKQKk4aVlGeK9S5uDy8zodTr3tEZ2gRKNavJLusXbEWqtoo42JxHFFNB6dtguehFMt8PyZPAtkgWwQ==} + '@tanstack/router-generator@1.166.6': + resolution: {integrity: sha512-D7Z6oLP2IfflXUzOOxIgeCD8v3/SXU//cgBon0pbF13HkKdf9Zlt97kQqcaOkbnruJJ6i5xtUIsoAQbMmj+EsQ==} engines: {node: '>=20.19'} - '@tanstack/router-plugin@1.166.3': - resolution: {integrity: sha512-yhnJRohpdKB24Fh7fW5mwgffpOcERZlXdk3i8PjXn+OYgAiG/cpuXXOJpZZ6An68vDW+Z5zBuTynXsDi2ZE4JQ==} + '@tanstack/router-plugin@1.166.6': + resolution: {integrity: sha512-07ZwOMNDlKIoaRtrfP5zO3VfqXNg2Zm7qvqZOBaTbbqgMvaKclW0ylqakweXtDwiNs9GPf/+lH/xyc+CgLGUyg==} engines: {node: '>=20.19'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.166.3 + '@tanstack/react-router': ^1.166.6 vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' vite-plugin-solid: ^2.11.10 webpack: '>=5.92.0' @@ -5330,8 +5647,8 @@ packages: hyphenate-style-name@1.1.0: resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} - i18next@25.8.14: - resolution: {integrity: sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA==} + i18next@25.8.17: + resolution: {integrity: sha512-vWtCttyn5bpOK4hWbRAe1ZXkA+Yzcn2OcACT+WJavtfGMcxzkfvXTLMeOU8MUhRmAySKjU4VVuKlo0sSGeBokA==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -5713,8 +6030,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lint-staged@16.3.2: - resolution: {integrity: sha512-xKqhC2AeXLwiAHXguxBjuChoTTWFC6Pees0SHPwOpwlvI3BH7ZADFPddAdN3pgo3aiKgPUx/bxE78JfUnxQnlg==} + lint-staged@16.3.3: + resolution: {integrity: sha512-RLq2koZ5fGWrx7tcqx2tSTMQj4lRkfNJaebO/li/uunhCJbtZqwTuwPHpgIimAHHi/2nZIiGrkCHDCOeR1onxA==} engines: {node: '>=20.17'} hasBin: true @@ -6125,8 +6442,8 @@ packages: oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} - oxlint@1.51.0: - resolution: {integrity: sha512-g6DNPaV9/WI9MoX2XllafxQuxwY1TV++j7hP8fTJByVBuCoVtm3dy9f/2vtH/HU40JztcgWF4G7ua+gkainklQ==} + oxlint@1.52.0: + resolution: {integrity: sha512-InLldD+6+3iHJGIrtU1W37UIpsg+xoGCemkZCuSQhxUO3evMX+L872ONvbECyRza9k7ScMCukJIK3Al/2ZMDnQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -6305,8 +6622,8 @@ packages: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} - postcss-sorting@9.1.0: - resolution: {integrity: sha512-Mn8KJ45HNNG6JBpBizXcyf6LqY/qyqetGcou/nprDnFwBFBLGj0j/sNKV2lj2KMOVOwdXu14aEzqJv8CIV6e8g==} + postcss-sorting@10.0.0: + resolution: {integrity: sha512-TXbU+h6vVRW+86c/+ewhWq9k7pr7ijASTnepVhCQiC87zAOTkvB1v2dHyWP+ggstSTX/PNvjzS+IOqzejndz9w==} peerDependencies: postcss: ^8.4.20 @@ -6418,6 +6735,19 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -7060,8 +7390,8 @@ packages: peerDependencies: stylelint: ^17.0.0 - stylelint-order@7.0.1: - resolution: {integrity: sha512-GWPei1zBVDDjxM+/BmcSCiOcHNd8rSqW6FUZtqQGlTRpD0Z5nSzspzWD8rtKif5KPdzUG68DApKEV/y/I9VbTw==} + stylelint-order@8.0.0: + resolution: {integrity: sha512-1oAwPRz6Ba8u9LPjgvdbeMjZszHhjY6DBBK+xMlS3IC89GdTsvAPpGYvW+dkn6pxc4ZaI1S959g4c8CftZbhIg==} engines: {node: '>=20.19.0'} peerDependencies: stylelint: ^16.18.0 || ^17.0.0 @@ -7697,7 +8027,7 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.3.0 - tinyexec: 1.0.1 + tinyexec: 1.0.2 '@babel/code-frame@7.27.1': dependencies: @@ -8616,7 +8946,7 @@ snapshots: '@commitlint/types': 20.4.3 git-raw-commits: 4.0.0 minimist: 1.2.8 - tinyexec: 1.0.1 + tinyexec: 1.0.2 '@commitlint/resolve-extends@20.4.3': dependencies: @@ -8677,11 +9007,23 @@ snapshots: '@ctrl/tinycolor@4.1.0': {} + '@dnd-kit/abstract@0.3.2': + dependencies: + '@dnd-kit/geometry': 0.3.2 + '@dnd-kit/state': 0.3.2 + tslib: 2.8.1 + '@dnd-kit/accessibility@3.1.1(react@19.2.4)': dependencies: react: 19.2.4 tslib: 2.8.1 + '@dnd-kit/collision@0.3.2': + dependencies: + '@dnd-kit/abstract': 0.3.2 + '@dnd-kit/geometry': 0.3.2 + tslib: 2.8.1 + '@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@dnd-kit/accessibility': 3.1.1(react@19.2.4) @@ -8690,6 +9032,33 @@ snapshots: react-dom: 19.2.4(react@19.2.4) tslib: 2.7.0 + '@dnd-kit/dom@0.3.2': + dependencies: + '@dnd-kit/abstract': 0.3.2 + '@dnd-kit/collision': 0.3.2 + '@dnd-kit/geometry': 0.3.2 + '@dnd-kit/state': 0.3.2 + tslib: 2.8.1 + + '@dnd-kit/geometry@0.3.2': + dependencies: + '@dnd-kit/state': 0.3.2 + tslib: 2.8.1 + + '@dnd-kit/helpers@0.3.2': + dependencies: + '@dnd-kit/abstract': 0.3.2 + tslib: 2.8.1 + + '@dnd-kit/react@0.3.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@dnd-kit/abstract': 0.3.2 + '@dnd-kit/dom': 0.3.2 + '@dnd-kit/state': 0.3.2 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tslib: 2.8.1 + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': dependencies: '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -8697,6 +9066,11 @@ snapshots: react: 19.2.4 tslib: 2.7.0 + '@dnd-kit/state@0.3.2': + dependencies: + '@preact/signals-core': 1.14.0 + tslib: 2.8.1 + '@dnd-kit/utilities@3.2.2(react@19.2.4)': dependencies: react: 19.2.4 @@ -8945,7 +9319,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@iconify/json@2.2.447': + '@iconify/json@2.2.448': dependencies: '@iconify/types': 2.0.0 pathe: 2.0.3 @@ -8958,7 +9332,7 @@ snapshots: '@iconify/types': 2.0.0 mlly: 1.8.0 - '@inlang/paraglide-js@2.13.2(babel-plugin-macros@3.1.0)': + '@inlang/paraglide-js@2.14.0(babel-plugin-macros@3.1.0)': dependencies: '@inlang/recommend-sherlock': 0.2.1 '@inlang/sdk': 2.7.0(babel-plugin-macros@3.1.0) @@ -9689,61 +10063,61 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@oxlint/binding-android-arm-eabi@1.51.0': + '@oxlint/binding-android-arm-eabi@1.52.0': optional: true - '@oxlint/binding-android-arm64@1.51.0': + '@oxlint/binding-android-arm64@1.52.0': optional: true - '@oxlint/binding-darwin-arm64@1.51.0': + '@oxlint/binding-darwin-arm64@1.52.0': optional: true - '@oxlint/binding-darwin-x64@1.51.0': + '@oxlint/binding-darwin-x64@1.52.0': optional: true - '@oxlint/binding-freebsd-x64@1.51.0': + '@oxlint/binding-freebsd-x64@1.52.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.51.0': + '@oxlint/binding-linux-arm-gnueabihf@1.52.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.51.0': + '@oxlint/binding-linux-arm-musleabihf@1.52.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.51.0': + '@oxlint/binding-linux-arm64-gnu@1.52.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.51.0': + '@oxlint/binding-linux-arm64-musl@1.52.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.51.0': + '@oxlint/binding-linux-ppc64-gnu@1.52.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.51.0': + '@oxlint/binding-linux-riscv64-gnu@1.52.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.51.0': + '@oxlint/binding-linux-riscv64-musl@1.52.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.51.0': + '@oxlint/binding-linux-s390x-gnu@1.52.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.51.0': + '@oxlint/binding-linux-x64-gnu@1.52.0': optional: true - '@oxlint/binding-linux-x64-musl@1.51.0': + '@oxlint/binding-linux-x64-musl@1.52.0': optional: true - '@oxlint/binding-openharmony-arm64@1.51.0': + '@oxlint/binding-openharmony-arm64@1.52.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.51.0': + '@oxlint/binding-win32-arm64-msvc@1.52.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.51.0': + '@oxlint/binding-win32-ia32-msvc@1.52.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.51.0': + '@oxlint/binding-win32-x64-msvc@1.52.0': optional: true '@paper-design/shaders-react@0.0.71(@types/react@19.2.14)(react@19.2.4)': @@ -9814,6 +10188,8 @@ snapshots: '@popperjs/core@2.11.8': {} + '@preact/signals-core@1.14.0': {} + '@prettier/plugin-oxc@0.1.3': dependencies: oxc-parser: 0.99.0 @@ -9822,6 +10198,46 @@ snapshots: '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -9831,6 +10247,60 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) @@ -9942,6 +10412,37 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) @@ -9949,6 +10450,15 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -9975,6 +10485,105 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + aria-hidden: 1.2.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/react-dom': 2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -10041,6 +10650,34 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -10104,9 +10741,28 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: @@ -10142,6 +10798,83 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -10190,6 +10923,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: react: 19.2.4 @@ -10600,22 +11340,22 @@ snapshots: '@tanstack/query-core': 5.90.20 react: 19.2.4 - '@tanstack/react-router-devtools@1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.166.2)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-router-devtools@1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.166.6)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/react-router': 1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-devtools-core': 1.166.2(@tanstack/router-core@1.166.2)(csstype@3.2.3) + '@tanstack/react-router': 1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-devtools-core': 1.166.6(@tanstack/router-core@1.166.6)(csstype@3.2.3) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@tanstack/router-core': 1.166.2 + '@tanstack/router-core': 1.166.6 transitivePeerDependencies: - csstype - '@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-router@1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/history': 1.161.4 '@tanstack/react-store': 0.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-core': 1.166.2 + '@tanstack/router-core': 1.166.6 isbot: 5.1.28 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -10647,7 +11387,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@tanstack/router-core@1.166.2': + '@tanstack/router-core@1.166.6': dependencies: '@tanstack/history': 1.161.4 '@tanstack/store': 0.9.1 @@ -10657,18 +11397,18 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.166.2(@tanstack/router-core@1.166.2)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.166.6(@tanstack/router-core@1.166.6)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.166.2 + '@tanstack/router-core': 1.166.6 clsx: 2.1.1 goober: 2.1.16(csstype@3.2.3) tiny-invariant: 1.3.3 optionalDependencies: csstype: 3.2.3 - '@tanstack/router-generator@1.166.2': + '@tanstack/router-generator@1.166.6': dependencies: - '@tanstack/router-core': 1.166.2 + '@tanstack/router-core': 1.166.6 '@tanstack/router-utils': 1.161.4 '@tanstack/virtual-file-routes': 1.161.4 prettier: 3.8.1 @@ -10679,7 +11419,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.166.3(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/router-plugin@1.166.6(@tanstack/react-router@1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) @@ -10687,15 +11427,15 @@ snapshots: '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 - '@tanstack/router-core': 1.166.2 - '@tanstack/router-generator': 1.166.2 + '@tanstack/router-core': 1.166.6 + '@tanstack/router-generator': 1.166.6 '@tanstack/router-utils': 1.161.4 '@tanstack/virtual-file-routes': 1.161.4 chokidar: 3.6.0 unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-router': 1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -10714,9 +11454,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(zod@4.3.6)': + '@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(zod@4.3.6)': dependencies: - '@tanstack/react-router': 1.166.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-router': 1.166.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) zod: 4.3.6 '@tanstack/store@0.9.1': {} @@ -12534,7 +13274,7 @@ snapshots: hyphenate-style-name@1.1.0: {} - i18next@25.8.14(typescript@5.9.3): + i18next@25.8.17(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.6 optionalDependencies: @@ -12827,7 +13567,7 @@ snapshots: lines-and-columns@1.2.4: {} - lint-staged@16.3.2: + lint-staged@16.3.3: dependencies: commander: 14.0.3 listr2: 9.0.5 @@ -13430,27 +14170,27 @@ snapshots: '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 - oxlint@1.51.0: + oxlint@1.52.0: optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.51.0 - '@oxlint/binding-android-arm64': 1.51.0 - '@oxlint/binding-darwin-arm64': 1.51.0 - '@oxlint/binding-darwin-x64': 1.51.0 - '@oxlint/binding-freebsd-x64': 1.51.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.51.0 - '@oxlint/binding-linux-arm-musleabihf': 1.51.0 - '@oxlint/binding-linux-arm64-gnu': 1.51.0 - '@oxlint/binding-linux-arm64-musl': 1.51.0 - '@oxlint/binding-linux-ppc64-gnu': 1.51.0 - '@oxlint/binding-linux-riscv64-gnu': 1.51.0 - '@oxlint/binding-linux-riscv64-musl': 1.51.0 - '@oxlint/binding-linux-s390x-gnu': 1.51.0 - '@oxlint/binding-linux-x64-gnu': 1.51.0 - '@oxlint/binding-linux-x64-musl': 1.51.0 - '@oxlint/binding-openharmony-arm64': 1.51.0 - '@oxlint/binding-win32-arm64-msvc': 1.51.0 - '@oxlint/binding-win32-ia32-msvc': 1.51.0 - '@oxlint/binding-win32-x64-msvc': 1.51.0 + '@oxlint/binding-android-arm-eabi': 1.52.0 + '@oxlint/binding-android-arm64': 1.52.0 + '@oxlint/binding-darwin-arm64': 1.52.0 + '@oxlint/binding-darwin-x64': 1.52.0 + '@oxlint/binding-freebsd-x64': 1.52.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.52.0 + '@oxlint/binding-linux-arm-musleabihf': 1.52.0 + '@oxlint/binding-linux-arm64-gnu': 1.52.0 + '@oxlint/binding-linux-arm64-musl': 1.52.0 + '@oxlint/binding-linux-ppc64-gnu': 1.52.0 + '@oxlint/binding-linux-riscv64-gnu': 1.52.0 + '@oxlint/binding-linux-riscv64-musl': 1.52.0 + '@oxlint/binding-linux-s390x-gnu': 1.52.0 + '@oxlint/binding-linux-x64-gnu': 1.52.0 + '@oxlint/binding-linux-x64-musl': 1.52.0 + '@oxlint/binding-openharmony-arm64': 1.52.0 + '@oxlint/binding-win32-arm64-msvc': 1.52.0 + '@oxlint/binding-win32-ia32-msvc': 1.52.0 + '@oxlint/binding-win32-x64-msvc': 1.52.0 p-retry@7.1.1: dependencies: @@ -13610,7 +14350,7 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-sorting@9.1.0(postcss@8.5.8): + postcss-sorting@10.0.0(postcss@8.5.8): dependencies: postcss: 8.5.8 @@ -13670,6 +14410,69 @@ snapshots: queue-microtask@1.2.3: {} + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -13700,11 +14503,11 @@ snapshots: dependencies: react: 19.2.4 - react-i18next@15.7.4(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + react-i18next@15.7.4(i18next@25.8.17(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.3 html-parse-stringify: 3.0.1 - i18next: 25.8.14(typescript@5.9.3) + i18next: 25.8.17(typescript@5.9.3) react: 19.2.4 optionalDependencies: react-dom: 19.2.4(react@19.2.4) @@ -14264,10 +15067,10 @@ snapshots: postcss-html: 1.8.1 stylelint: 17.4.0(typescript@5.9.3) - stylelint-config-recess-order@7.6.1(stylelint-order@7.0.1(stylelint@17.4.0(typescript@5.9.3)))(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-recess-order@7.6.1(stylelint-order@8.0.0(stylelint@17.4.0(typescript@5.9.3)))(stylelint@17.4.0(typescript@5.9.3)): dependencies: stylelint: 17.4.0(typescript@5.9.3) - stylelint-order: 7.0.1(stylelint@17.4.0(typescript@5.9.3)) + stylelint-order: 8.0.0(stylelint@17.4.0(typescript@5.9.3)) stylelint-config-recommended@18.0.0(stylelint@17.4.0(typescript@5.9.3)): dependencies: @@ -14282,10 +15085,10 @@ snapshots: dependencies: stylelint: 17.4.0(typescript@5.9.3) - stylelint-order@7.0.1(stylelint@17.4.0(typescript@5.9.3)): + stylelint-order@8.0.0(stylelint@17.4.0(typescript@5.9.3)): dependencies: postcss: 8.5.8 - postcss-sorting: 9.1.0(postcss@8.5.8) + postcss-sorting: 10.0.0(postcss@8.5.8) stylelint: 17.4.0(typescript@5.9.3) stylelint-scss@7.0.0(stylelint@17.4.0(typescript@5.9.3)): diff --git a/mihomo/adapter/outbound/trojan.go b/mihomo/adapter/outbound/trojan.go index b691ff0080..497ff05b1f 100644 --- a/mihomo/adapter/outbound/trojan.go +++ b/mihomo/adapter/outbound/trojan.go @@ -27,8 +27,7 @@ type Trojan struct { hexPassword [trojan.KeyLength]byte // for gun mux - gunConfig *gun.Config - gunTransport *gun.TransportWrap + gunTransport *gun.Transport realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -178,7 +177,7 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con var c net.Conn // gun transport if t.gunTransport != nil { - c, err = gun.StreamGunWithTransport(t.gunTransport, t.gunConfig) + c, err = t.gunTransport.Dial() } else { c, err = t.dialer.DialContext(ctx, "tcp", t.addr) } @@ -206,7 +205,7 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata) var c net.Conn // grpc transport if t.gunTransport != nil { - c, err = gun.StreamGunWithTransport(t.gunTransport, t.gunConfig) + c, err = t.gunTransport.Dial() } else { c, err = t.dialer.DialContext(ctx, "tcp", t.addr) } @@ -317,13 +316,14 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { Reality: t.realityConfig, } - t.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig) - - t.gunConfig = &gun.Config{ - ServiceName: option.GrpcOpts.GrpcServiceName, - UserAgent: option.GrpcOpts.GrpcUserAgent, - Host: option.SNI, + gunConfig := &gun.Config{ + ServiceName: option.GrpcOpts.GrpcServiceName, + UserAgent: option.GrpcOpts.GrpcUserAgent, + Host: option.SNI, + PingInterval: option.GrpcOpts.PingInterval, } + + t.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) } return t, nil diff --git a/mihomo/adapter/outbound/vless.go b/mihomo/adapter/outbound/vless.go index bd060b185d..78ccb6676f 100644 --- a/mihomo/adapter/outbound/vless.go +++ b/mihomo/adapter/outbound/vless.go @@ -33,8 +33,7 @@ type Vless struct { encryption *encryption.ClientInstance // for gun mux - gunConfig *gun.Config - gunTransport *gun.TransportWrap + gunTransport *gun.Transport realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -234,7 +233,7 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -260,7 +259,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) ( var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -431,9 +430,10 @@ func NewVless(option VlessOption) (*Vless, error) { } gunConfig := &gun.Config{ - ServiceName: option.GrpcOpts.GrpcServiceName, - UserAgent: option.GrpcOpts.GrpcUserAgent, - Host: option.ServerName, + ServiceName: option.GrpcOpts.GrpcServiceName, + UserAgent: option.GrpcOpts.GrpcUserAgent, + Host: option.ServerName, + PingInterval: option.GrpcOpts.PingInterval, } if option.ServerName == "" { gunConfig.Host = v.addr @@ -457,9 +457,7 @@ func NewVless(option VlessOption) (*Vless, error) { } } - v.gunConfig = gunConfig - - v.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig) + v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) } return v, nil diff --git a/mihomo/adapter/outbound/vmess.go b/mihomo/adapter/outbound/vmess.go index 29ed63fde2..0de06b7c17 100644 --- a/mihomo/adapter/outbound/vmess.go +++ b/mihomo/adapter/outbound/vmess.go @@ -34,8 +34,7 @@ type Vmess struct { option *VmessOption // for gun mux - gunConfig *gun.Config - gunTransport *gun.TransportWrap + gunTransport *gun.Transport realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -86,6 +85,7 @@ type HTTP2Options struct { type GrpcOptions struct { GrpcServiceName string `proxy:"grpc-service-name,omitempty"` GrpcUserAgent string `proxy:"grpc-user-agent,omitempty"` + PingInterval int `proxy:"ping-interval,omitempty"` } type WSOptions struct { @@ -295,7 +295,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -318,7 +318,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) ( var c net.Conn // gun transport if v.gunTransport != nil { - c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig) + c, err = v.gunTransport.Dial() } else { c, err = v.dialer.DialContext(ctx, "tcp", v.addr) } @@ -437,9 +437,10 @@ func NewVmess(option VmessOption) (*Vmess, error) { } gunConfig := &gun.Config{ - ServiceName: option.GrpcOpts.GrpcServiceName, - UserAgent: option.GrpcOpts.GrpcUserAgent, - Host: option.ServerName, + ServiceName: option.GrpcOpts.GrpcServiceName, + UserAgent: option.GrpcOpts.GrpcUserAgent, + Host: option.ServerName, + PingInterval: option.GrpcOpts.PingInterval, } if option.ServerName == "" { gunConfig.Host = v.addr @@ -463,9 +464,7 @@ func NewVmess(option VmessOption) (*Vmess, error) { } } - v.gunConfig = gunConfig - - v.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig) + v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) } return v, nil diff --git a/mihomo/adapter/provider/parser.go b/mihomo/adapter/provider/parser.go index f6200fdde2..1668ccf99f 100644 --- a/mihomo/adapter/provider/parser.go +++ b/mihomo/adapter/provider/parser.go @@ -18,8 +18,8 @@ var ( type healthCheckSchema struct { Enable bool `provider:"enable"` - URL string `provider:"url"` - Interval int `provider:"interval"` + URL string `provider:"url,omitempty"` + Interval int `provider:"interval,omitempty"` TestTimeout int `provider:"timeout,omitempty"` Lazy bool `provider:"lazy,omitempty"` ExpectedStatus string `provider:"expected-status,omitempty"` diff --git a/mihomo/docs/config.yaml b/mihomo/docs/config.yaml index 09467a11e4..efaa9ede90 100644 --- a/mihomo/docs/config.yaml +++ b/mihomo/docs/config.yaml @@ -669,6 +669,7 @@ proxies: # socks5 grpc-opts: grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" + # ping-interval: 0 # 默认关闭,单位为秒 # ip-version: ipv4 # vless @@ -759,6 +760,7 @@ proxies: # socks5 grpc-opts: grpc-service-name: "grpc" # grpc-user-agent: "grpc-go/1.36.0" + # ping-interval: 0 # 默认关闭,单位为秒 reality-opts: public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE @@ -830,6 +832,7 @@ proxies: # socks5 grpc-opts: grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" + # ping-interval: 0 # 默认关闭,单位为秒 - name: trojan-ws server: server diff --git a/mihomo/transport/gun/gun.go b/mihomo/transport/gun/gun.go index 27c0cbbbe2..65313df94d 100644 --- a/mihomo/transport/gun/gun.go +++ b/mihomo/transport/gun/gun.go @@ -59,9 +59,10 @@ type Conn struct { } type Config struct { - ServiceName string - UserAgent string - Host string + ServiceName string + UserAgent string + Host string + PingInterval int } func (g *Conn) initReader() { @@ -246,7 +247,7 @@ func (g *Conn) SetDeadline(t time.Time) error { return nil } -func NewHTTP2Client(dialFn DialFn, tlsConfig *vmess.TLSConfig) *TransportWrap { +func NewTransport(dialFn DialFn, tlsConfig *vmess.TLSConfig, gunCfg *Config) *Transport { dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout) defer cancel() @@ -288,14 +289,16 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *vmess.TLSConfig) *TransportWrap { DialTLSContext: dialFunc, AllowHTTP: false, DisableCompression: true, + ReadIdleTimeout: time.Duration(gunCfg.PingInterval) * time.Second, // If zero, no health check is performed PingTimeout: 0, } ctx, cancel := context.WithCancel(context.Background()) - wrap := &TransportWrap{ - Http2Transport: transport, - ctx: ctx, - cancel: cancel, + wrap := &Transport{ + transport: transport, + cfg: gunCfg, + ctx: ctx, + cancel: cancel, } return wrap } @@ -307,18 +310,18 @@ func ServiceNameToPath(serviceName string) string { return "/" + serviceName + "/Tun" } -func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, error) { +func (t *Transport) Dial() (net.Conn, error) { serviceName := "GunService" - if cfg.ServiceName != "" { - serviceName = cfg.ServiceName + if t.cfg.ServiceName != "" { + serviceName = t.cfg.ServiceName } path := ServiceNameToPath(serviceName) reader, writer := io.Pipe() header := defaultHeader.Clone() - if cfg.UserAgent != "" { - header.Set("User-Agent", cfg.UserAgent) + if t.cfg.UserAgent != "" { + header.Set("User-Agent", t.cfg.UserAgent) } request := &http.Request{ @@ -326,17 +329,17 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er Body: reader, URL: &url.URL{ Scheme: "https", - Host: cfg.Host, + Host: t.cfg.Host, Path: path, // for unescape path - Opaque: "//" + cfg.Host + path, + Opaque: "//" + t.cfg.Host + path, }, Proto: "HTTP/2", ProtoMajor: 2, ProtoMinor: 0, Header: header, } - request = request.WithContext(transport.ctx) + request = request.WithContext(t.ctx) conn := &Conn{ initFn: func() (io.ReadCloser, NetAddr, error) { @@ -348,7 +351,7 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er }, } request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace)) - response, err := transport.RoundTrip(request) + response, err := t.transport.RoundTrip(request) if err != nil { return nil, nAddr, err } @@ -361,13 +364,13 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er return conn, nil } -func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, cfg *Config) (net.Conn, error) { +func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, gunCfg *Config) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { return conn, nil } - transport := NewHTTP2Client(dialFn, tlsConfig) - c, err := StreamGunWithTransport(transport, cfg) + transport := NewTransport(dialFn, tlsConfig, gunCfg) + c, err := transport.Dial() if err != nil { return nil, err } diff --git a/mihomo/transport/gun/transport.go b/mihomo/transport/gun/transport.go index 4b9da971f3..80c0af1537 100644 --- a/mihomo/transport/gun/transport.go +++ b/mihomo/transport/gun/transport.go @@ -10,17 +10,18 @@ import ( "github.com/metacubex/http" ) -type TransportWrap struct { - *http.Http2Transport +type Transport struct { + transport *http.Http2Transport + cfg *Config ctx context.Context cancel context.CancelFunc closeOnce sync.Once } -func (tw *TransportWrap) Close() error { - tw.closeOnce.Do(func() { - tw.cancel() - CloseTransport(tw.Http2Transport) +func (t *Transport) Close() error { + t.closeOnce.Do(func() { + t.cancel() + CloseHttp2Transport(t.transport) }) return nil } diff --git a/mihomo/transport/gun/transport_close.go b/mihomo/transport/gun/transport_close.go index 44fefd7c12..add4906e46 100644 --- a/mihomo/transport/gun/transport_close.go +++ b/mihomo/transport/gun/transport_close.go @@ -44,7 +44,7 @@ func closeClientConn(cc *http.Http2ClientConn) { // like forceCloseConn() in htt _ = cc.Close() } -func CloseTransport(tr *http.Http2Transport) { +func CloseHttp2Transport(tr *http.Http2Transport) { connPool := transportConnPool(tr) p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data) p.mu.Lock() diff --git a/mihomo/transport/sudoku/crypto/record_conn.go b/mihomo/transport/sudoku/crypto/record_conn.go index 7a80c7f5f3..7c0357151b 100644 --- a/mihomo/transport/sudoku/crypto/record_conn.go +++ b/mihomo/transport/sudoku/crypto/record_conn.go @@ -5,6 +5,7 @@ import ( "crypto/aes" "crypto/cipher" "crypto/hmac" + "crypto/rand" "crypto/sha256" "encoding/binary" "errors" @@ -12,6 +13,7 @@ import ( "io" "net" "sync" + "sync/atomic" "golang.org/x/crypto/chacha20poly1305" ) @@ -55,13 +57,15 @@ type RecordConn struct { recvAEADEpoch uint32 // Send direction state. - sendEpoch uint32 - sendSeq uint64 - sendBytes int64 + sendEpoch uint32 + sendSeq uint64 + sendBytes int64 + sendEpochUpdates uint32 // Receive direction state. - recvEpoch uint32 - recvSeq uint64 + recvEpoch uint32 + recvSeq uint64 + recvInitialized bool readBuf bytes.Buffer @@ -105,6 +109,9 @@ func NewRecordConn(conn net.Conn, method string, baseSend, baseRecv []byte) (*Re } rc := &RecordConn{Conn: conn, method: method} rc.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)} + if err := rc.resetTrafficState(); err != nil { + return nil, err + } return rc, nil } @@ -127,11 +134,9 @@ func (c *RecordConn) Rekey(baseSend, baseRecv []byte) error { defer c.writeMu.Unlock() c.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)} - c.sendEpoch = 0 - c.sendSeq = 0 - c.sendBytes = 0 - c.recvEpoch = 0 - c.recvSeq = 0 + if err := c.resetTrafficState(); err != nil { + return err + } c.readBuf.Reset() c.sendAEAD = nil @@ -141,6 +146,21 @@ func (c *RecordConn) Rekey(baseSend, baseRecv []byte) error { return nil } +func (c *RecordConn) resetTrafficState() error { + sendEpoch, sendSeq, err := randomRecordCounters() + if err != nil { + return fmt.Errorf("initialize record counters: %w", err) + } + c.sendEpoch = sendEpoch + c.sendSeq = sendSeq + c.sendBytes = 0 + c.sendEpochUpdates = 0 + c.recvEpoch = 0 + c.recvSeq = 0 + c.recvInitialized = false + return nil +} + func normalizeAEADMethod(method string) string { switch method { case "", "chacha20-poly1305": @@ -166,6 +186,44 @@ func cloneBytes(b []byte) []byte { return append([]byte(nil), b...) } +func randomRecordCounters() (uint32, uint64, error) { + epoch, err := randomNonZeroUint32() + if err != nil { + return 0, 0, err + } + seq, err := randomNonZeroUint64() + if err != nil { + return 0, 0, err + } + return epoch, seq, nil +} + +func randomNonZeroUint32() (uint32, error) { + var b [4]byte + for { + if _, err := io.ReadFull(rand.Reader, b[:]); err != nil { + return 0, err + } + v := binary.BigEndian.Uint32(b[:]) + if v != 0 && v != ^uint32(0) { + return v, nil + } + } +} + +func randomNonZeroUint64() (uint64, error) { + var b [8]byte + for { + if _, err := io.ReadFull(rand.Reader, b[:]); err != nil { + return 0, err + } + v := binary.BigEndian.Uint64(b[:]) + if v != 0 && v != ^uint64(0) { + return v, nil + } + } +} + func (c *RecordConn) newAEADFor(base []byte, epoch uint32) (cipher.AEAD, error) { if c.method == "none" { return nil, nil @@ -209,17 +267,49 @@ func deriveEpochKey(base []byte, epoch uint32, method string) []byte { return mac.Sum(nil) } -func (c *RecordConn) maybeBumpSendEpochLocked(addedPlain int) { - if KeyUpdateAfterBytes <= 0 || c.method == "none" { - return +func (c *RecordConn) maybeBumpSendEpochLocked(addedPlain int) error { + ku := atomic.LoadInt64(&KeyUpdateAfterBytes) + if ku <= 0 || c.method == "none" { + return nil } c.sendBytes += int64(addedPlain) - threshold := KeyUpdateAfterBytes * int64(c.sendEpoch+1) + threshold := ku * int64(c.sendEpochUpdates+1) if c.sendBytes < threshold { - return + return nil } c.sendEpoch++ - c.sendSeq = 0 + c.sendEpochUpdates++ + nextSeq, err := randomNonZeroUint64() + if err != nil { + return fmt.Errorf("rotate record seq: %w", err) + } + c.sendSeq = nextSeq + return nil +} + +func (c *RecordConn) validateRecvPosition(epoch uint32, seq uint64) error { + if !c.recvInitialized { + return nil + } + if epoch < c.recvEpoch { + return fmt.Errorf("replayed epoch: got %d want >=%d", epoch, c.recvEpoch) + } + if epoch == c.recvEpoch && seq != c.recvSeq { + return fmt.Errorf("out of order: epoch=%d got=%d want=%d", epoch, seq, c.recvSeq) + } + if epoch > c.recvEpoch { + const maxJump = 8 + if epoch-c.recvEpoch > maxJump { + return fmt.Errorf("epoch jump too large: got=%d want<=%d", epoch-c.recvEpoch, maxJump) + } + } + return nil +} + +func (c *RecordConn) markRecvPosition(epoch uint32, seq uint64) { + c.recvEpoch = epoch + c.recvSeq = seq + 1 + c.recvInitialized = true } func (c *RecordConn) Write(p []byte) (int, error) { @@ -282,7 +372,9 @@ func (c *RecordConn) Write(p []byte) (int, error) { } total += n - c.maybeBumpSendEpochLocked(n) + if err := c.maybeBumpSendEpochLocked(n); err != nil { + return total, err + } } return total, nil } @@ -324,31 +416,17 @@ func (c *RecordConn) Read(p []byte) (int, error) { epoch := binary.BigEndian.Uint32(header[:4]) seq := binary.BigEndian.Uint64(header[4:]) - if epoch < c.recvEpoch { - return 0, fmt.Errorf("replayed epoch: got %d want >=%d", epoch, c.recvEpoch) - } - if epoch == c.recvEpoch && seq != c.recvSeq { - return 0, fmt.Errorf("out of order: epoch=%d got=%d want=%d", epoch, seq, c.recvSeq) - } - if epoch > c.recvEpoch { - const maxJump = 8 - if epoch-c.recvEpoch > maxJump { - return 0, fmt.Errorf("epoch jump too large: got=%d want<=%d", epoch-c.recvEpoch, maxJump) - } - c.recvEpoch = epoch - c.recvSeq = 0 - if seq != 0 { - return 0, fmt.Errorf("out of order: epoch advanced to %d but seq=%d", epoch, seq) - } + if err := c.validateRecvPosition(epoch, seq); err != nil { + return 0, err } - if c.recvAEAD == nil || c.recvAEADEpoch != c.recvEpoch { - a, err := c.newAEADFor(c.keys.baseRecv, c.recvEpoch) + if c.recvAEAD == nil || c.recvAEADEpoch != epoch { + a, err := c.newAEADFor(c.keys.baseRecv, epoch) if err != nil { return 0, err } c.recvAEAD = a - c.recvAEADEpoch = c.recvEpoch + c.recvAEADEpoch = epoch } aead := c.recvAEAD @@ -356,7 +434,7 @@ func (c *RecordConn) Read(p []byte) (int, error) { if err != nil { return 0, fmt.Errorf("decryption failed: epoch=%d seq=%d: %w", epoch, seq, err) } - c.recvSeq++ + c.markRecvPosition(epoch, seq) c.readBuf.Write(plaintext) return c.readBuf.Read(p) diff --git a/mihomo/transport/sudoku/crypto/record_conn_test.go b/mihomo/transport/sudoku/crypto/record_conn_test.go new file mode 100644 index 0000000000..4ea0b9b88e --- /dev/null +++ b/mihomo/transport/sudoku/crypto/record_conn_test.go @@ -0,0 +1,86 @@ +package crypto + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "io" + "net" + "testing" + "time" +) + +type captureConn struct { + bytes.Buffer +} + +func (c *captureConn) Read(_ []byte) (int, error) { return 0, io.EOF } +func (c *captureConn) Write(p []byte) (int, error) { return c.Buffer.Write(p) } +func (c *captureConn) Close() error { return nil } +func (c *captureConn) LocalAddr() net.Addr { return nil } +func (c *captureConn) RemoteAddr() net.Addr { return nil } +func (c *captureConn) SetDeadline(time.Time) error { return nil } +func (c *captureConn) SetReadDeadline(time.Time) error { return nil } +func (c *captureConn) SetWriteDeadline(time.Time) error { return nil } + +type replayConn struct { + reader *bytes.Reader +} + +func (c *replayConn) Read(p []byte) (int, error) { return c.reader.Read(p) } +func (c *replayConn) Write(p []byte) (int, error) { return len(p), nil } +func (c *replayConn) Close() error { return nil } +func (c *replayConn) LocalAddr() net.Addr { return nil } +func (c *replayConn) RemoteAddr() net.Addr { return nil } +func (c *replayConn) SetDeadline(time.Time) error { return nil } +func (c *replayConn) SetReadDeadline(time.Time) error { return nil } +func (c *replayConn) SetWriteDeadline(time.Time) error { return nil } + +func TestRecordConn_FirstFrameUsesRandomizedCounters(t *testing.T) { + pskSend := sha256.Sum256([]byte("record-send")) + pskRecv := sha256.Sum256([]byte("record-recv")) + + raw := &captureConn{} + writer, err := NewRecordConn(raw, "chacha20-poly1305", pskSend[:], pskRecv[:]) + if err != nil { + t.Fatalf("new writer: %v", err) + } + + if writer.sendEpoch == 0 || writer.sendSeq == 0 { + t.Fatalf("expected non-zero randomized counters, got epoch=%d seq=%d", writer.sendEpoch, writer.sendSeq) + } + + want := []byte("record prefix camouflage") + if _, err := writer.Write(want); err != nil { + t.Fatalf("write: %v", err) + } + + wire := raw.Bytes() + if len(wire) < 2+recordHeaderSize { + t.Fatalf("short frame: %d", len(wire)) + } + + bodyLen := int(binary.BigEndian.Uint16(wire[:2])) + if bodyLen != len(wire)-2 { + t.Fatalf("body len mismatch: got %d want %d", bodyLen, len(wire)-2) + } + + epoch := binary.BigEndian.Uint32(wire[2:6]) + seq := binary.BigEndian.Uint64(wire[6:14]) + if epoch == 0 || seq == 0 { + t.Fatalf("wire header still starts from zero: epoch=%d seq=%d", epoch, seq) + } + + reader, err := NewRecordConn(&replayConn{reader: bytes.NewReader(wire)}, "chacha20-poly1305", pskRecv[:], pskSend[:]) + if err != nil { + t.Fatalf("new reader: %v", err) + } + + got := make([]byte, len(want)) + if _, err := io.ReadFull(reader, got); err != nil { + t.Fatalf("read: %v", err) + } + if !bytes.Equal(got, want) { + t.Fatalf("plaintext mismatch: got %q want %q", got, want) + } +} diff --git a/mihomo/transport/sudoku/early_handshake.go b/mihomo/transport/sudoku/early_handshake.go new file mode 100644 index 0000000000..803a5293a8 --- /dev/null +++ b/mihomo/transport/sudoku/early_handshake.go @@ -0,0 +1,345 @@ +package sudoku + +import ( + "bytes" + "crypto/ecdh" + "crypto/rand" + "encoding/hex" + "fmt" + "net" + "time" + + "github.com/metacubex/mihomo/transport/sudoku/crypto" + httpmaskobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask" + sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku" +) + +const earlyKIPHandshakeTTL = 60 * time.Second + +type EarlyCodecConfig struct { + PSK string + AEAD string + EnablePureDownlink bool + PaddingMin int + PaddingMax int +} + +type EarlyClientState struct { + RequestPayload []byte + + cfg EarlyCodecConfig + table *sudokuobfs.Table + nonce [kipHelloNonceSize]byte + ephemeral *ecdh.PrivateKey + sessionC2S []byte + sessionS2C []byte + responseSet bool +} + +type EarlyServerState struct { + ResponsePayload []byte + UserHash string + + cfg EarlyCodecConfig + table *sudokuobfs.Table + sessionC2S []byte + sessionS2C []byte +} + +type ReplayAllowFunc func(userHash string, nonce [kipHelloNonceSize]byte, now time.Time) bool + +type earlyMemoryConn struct { + reader *bytes.Reader + write bytes.Buffer +} + +func newEarlyMemoryConn(readBuf []byte) *earlyMemoryConn { + return &earlyMemoryConn{reader: bytes.NewReader(readBuf)} +} + +func (c *earlyMemoryConn) Read(p []byte) (int, error) { + if c == nil || c.reader == nil { + return 0, net.ErrClosed + } + return c.reader.Read(p) +} + +func (c *earlyMemoryConn) Write(p []byte) (int, error) { + if c == nil { + return 0, net.ErrClosed + } + return c.write.Write(p) +} + +func (c *earlyMemoryConn) Close() error { return nil } +func (c *earlyMemoryConn) LocalAddr() net.Addr { return earlyDummyAddr("local") } +func (c *earlyMemoryConn) RemoteAddr() net.Addr { return earlyDummyAddr("remote") } +func (c *earlyMemoryConn) SetDeadline(time.Time) error { return nil } +func (c *earlyMemoryConn) SetReadDeadline(time.Time) error { return nil } +func (c *earlyMemoryConn) SetWriteDeadline(time.Time) error { return nil } +func (c *earlyMemoryConn) Written() []byte { return append([]byte(nil), c.write.Bytes()...) } + +type earlyDummyAddr string + +func (a earlyDummyAddr) Network() string { return string(a) } +func (a earlyDummyAddr) String() string { return string(a) } + +func buildEarlyClientObfsConn(raw net.Conn, cfg EarlyCodecConfig, table *sudokuobfs.Table) net.Conn { + base := sudokuobfs.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false) + if cfg.EnablePureDownlink { + return base + } + packed := sudokuobfs.NewPackedConn(raw, table, cfg.PaddingMin, cfg.PaddingMax) + return newDirectionalConn(raw, packed, base) +} + +func buildEarlyServerObfsConn(raw net.Conn, cfg EarlyCodecConfig, table *sudokuobfs.Table) net.Conn { + uplink := sudokuobfs.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false) + if cfg.EnablePureDownlink { + return uplink + } + packed := sudokuobfs.NewPackedConn(raw, table, cfg.PaddingMin, cfg.PaddingMax) + return newDirectionalConn(raw, uplink, packed, packed.Flush) +} + +func NewEarlyClientState(cfg EarlyCodecConfig, table *sudokuobfs.Table, userHash [kipHelloUserHashSize]byte, feats uint32) (*EarlyClientState, error) { + if table == nil { + return nil, fmt.Errorf("nil table") + } + + curve := ecdh.X25519() + ephemeral, err := curve.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("ecdh generate failed: %w", err) + } + + var nonce [kipHelloNonceSize]byte + if _, err := rand.Read(nonce[:]); err != nil { + return nil, fmt.Errorf("nonce generate failed: %w", err) + } + + var clientPub [kipHelloPubSize]byte + copy(clientPub[:], ephemeral.PublicKey().Bytes()) + hello := &KIPClientHello{ + Timestamp: time.Now(), + UserHash: userHash, + Nonce: nonce, + ClientPub: clientPub, + Features: feats, + } + + mem := newEarlyMemoryConn(nil) + obfsConn := buildEarlyClientObfsConn(mem, cfg, table) + pskC2S, pskS2C := derivePSKDirectionalBases(cfg.PSK) + rc, err := crypto.NewRecordConn(obfsConn, cfg.AEAD, pskC2S, pskS2C) + if err != nil { + return nil, fmt.Errorf("client early crypto setup failed: %w", err) + } + if err := WriteKIPMessage(rc, KIPTypeClientHello, hello.EncodePayload()); err != nil { + return nil, fmt.Errorf("write early client hello failed: %w", err) + } + + return &EarlyClientState{ + RequestPayload: mem.Written(), + cfg: cfg, + table: table, + nonce: nonce, + ephemeral: ephemeral, + }, nil +} + +func (s *EarlyClientState) ProcessResponse(payload []byte) error { + if s == nil { + return fmt.Errorf("nil client state") + } + + mem := newEarlyMemoryConn(payload) + obfsConn := buildEarlyClientObfsConn(mem, s.cfg, s.table) + pskC2S, pskS2C := derivePSKDirectionalBases(s.cfg.PSK) + rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, pskC2S, pskS2C) + if err != nil { + return fmt.Errorf("client early crypto setup failed: %w", err) + } + + msg, err := ReadKIPMessage(rc) + if err != nil { + return fmt.Errorf("read early server hello failed: %w", err) + } + if msg.Type != KIPTypeServerHello { + return fmt.Errorf("unexpected early handshake message: %d", msg.Type) + } + sh, err := DecodeKIPServerHelloPayload(msg.Payload) + if err != nil { + return fmt.Errorf("decode early server hello failed: %w", err) + } + if sh.Nonce != s.nonce { + return fmt.Errorf("early handshake nonce mismatch") + } + + shared, err := x25519SharedSecret(s.ephemeral, sh.ServerPub[:]) + if err != nil { + return fmt.Errorf("ecdh failed: %w", err) + } + s.sessionC2S, s.sessionS2C, err = deriveSessionDirectionalBases(s.cfg.PSK, shared, s.nonce) + if err != nil { + return fmt.Errorf("derive session keys failed: %w", err) + } + s.responseSet = true + return nil +} + +func (s *EarlyClientState) WrapConn(raw net.Conn) (net.Conn, error) { + if s == nil { + return nil, fmt.Errorf("nil client state") + } + if !s.responseSet { + return nil, fmt.Errorf("early handshake not completed") + } + + obfsConn := buildEarlyClientObfsConn(raw, s.cfg, s.table) + rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, s.sessionC2S, s.sessionS2C) + if err != nil { + return nil, fmt.Errorf("setup client session crypto failed: %w", err) + } + return rc, nil +} + +func (s *EarlyClientState) Ready() bool { + return s != nil && s.responseSet +} + +func NewHTTPMaskClientEarlyHandshake(cfg EarlyCodecConfig, table *sudokuobfs.Table, userHash [kipHelloUserHashSize]byte, feats uint32) (*httpmaskobfs.ClientEarlyHandshake, error) { + state, err := NewEarlyClientState(cfg, table, userHash, feats) + if err != nil { + return nil, err + } + return &httpmaskobfs.ClientEarlyHandshake{ + RequestPayload: state.RequestPayload, + HandleResponse: state.ProcessResponse, + Ready: state.Ready, + WrapConn: state.WrapConn, + }, nil +} + +func ProcessEarlyClientPayload(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, payload []byte, allowReplay ReplayAllowFunc) (*EarlyServerState, error) { + if len(payload) == 0 { + return nil, fmt.Errorf("empty early payload") + } + if len(tables) == 0 { + return nil, fmt.Errorf("no tables configured") + } + + var firstErr error + for _, table := range tables { + state, err := processEarlyClientPayloadForTable(cfg, table, payload, allowReplay) + if err == nil { + return state, nil + } + if firstErr == nil { + firstErr = err + } + } + if firstErr == nil { + firstErr = fmt.Errorf("early handshake probe failed") + } + return nil, firstErr +} + +func processEarlyClientPayloadForTable(cfg EarlyCodecConfig, table *sudokuobfs.Table, payload []byte, allowReplay ReplayAllowFunc) (*EarlyServerState, error) { + mem := newEarlyMemoryConn(payload) + obfsConn := buildEarlyServerObfsConn(mem, cfg, table) + pskC2S, pskS2C := derivePSKDirectionalBases(cfg.PSK) + rc, err := crypto.NewRecordConn(obfsConn, cfg.AEAD, pskS2C, pskC2S) + if err != nil { + return nil, err + } + + msg, err := ReadKIPMessage(rc) + if err != nil { + return nil, err + } + if msg.Type != KIPTypeClientHello { + return nil, fmt.Errorf("unexpected handshake message: %d", msg.Type) + } + ch, err := DecodeKIPClientHelloPayload(msg.Payload) + if err != nil { + return nil, err + } + if absInt64(time.Now().Unix()-ch.Timestamp.Unix()) > int64(earlyKIPHandshakeTTL.Seconds()) { + return nil, fmt.Errorf("time skew/replay") + } + + userHash := hex.EncodeToString(ch.UserHash[:]) + if allowReplay != nil && !allowReplay(userHash, ch.Nonce, time.Now()) { + return nil, fmt.Errorf("replay detected") + } + + curve := ecdh.X25519() + serverEphemeral, err := curve.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("ecdh generate failed: %w", err) + } + shared, err := x25519SharedSecret(serverEphemeral, ch.ClientPub[:]) + if err != nil { + return nil, fmt.Errorf("ecdh failed: %w", err) + } + sessionC2S, sessionS2C, err := deriveSessionDirectionalBases(cfg.PSK, shared, ch.Nonce) + if err != nil { + return nil, fmt.Errorf("derive session keys failed: %w", err) + } + + var serverPub [kipHelloPubSize]byte + copy(serverPub[:], serverEphemeral.PublicKey().Bytes()) + serverHello := &KIPServerHello{ + Nonce: ch.Nonce, + ServerPub: serverPub, + SelectedFeats: ch.Features & KIPFeatAll, + } + + respMem := newEarlyMemoryConn(nil) + respObfs := buildEarlyServerObfsConn(respMem, cfg, table) + respConn, err := crypto.NewRecordConn(respObfs, cfg.AEAD, pskS2C, pskC2S) + if err != nil { + return nil, fmt.Errorf("server early crypto setup failed: %w", err) + } + if err := WriteKIPMessage(respConn, KIPTypeServerHello, serverHello.EncodePayload()); err != nil { + return nil, fmt.Errorf("write early server hello failed: %w", err) + } + + return &EarlyServerState{ + ResponsePayload: respMem.Written(), + UserHash: userHash, + cfg: cfg, + table: table, + sessionC2S: sessionC2S, + sessionS2C: sessionS2C, + }, nil +} + +func (s *EarlyServerState) WrapConn(raw net.Conn) (net.Conn, error) { + if s == nil { + return nil, fmt.Errorf("nil server state") + } + obfsConn := buildEarlyServerObfsConn(raw, s.cfg, s.table) + rc, err := crypto.NewRecordConn(obfsConn, s.cfg.AEAD, s.sessionS2C, s.sessionC2S) + if err != nil { + return nil, fmt.Errorf("setup server session crypto failed: %w", err) + } + return rc, nil +} + +func NewHTTPMaskServerEarlyHandshake(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, allowReplay ReplayAllowFunc) *httpmaskobfs.TunnelServerEarlyHandshake { + return &httpmaskobfs.TunnelServerEarlyHandshake{ + Prepare: func(payload []byte) (*httpmaskobfs.PreparedServerEarlyHandshake, error) { + state, err := ProcessEarlyClientPayload(cfg, tables, payload, allowReplay) + if err != nil { + return nil, err + } + return &httpmaskobfs.PreparedServerEarlyHandshake{ + ResponsePayload: state.ResponsePayload, + WrapConn: state.WrapConn, + UserHash: state.UserHash, + }, nil + }, + } +} diff --git a/mihomo/transport/sudoku/handshake.go b/mihomo/transport/sudoku/handshake.go index 971d47fd8b..bac688e8f5 100644 --- a/mihomo/transport/sudoku/handshake.go +++ b/mihomo/transport/sudoku/handshake.go @@ -337,6 +337,9 @@ func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, *Handshak if err := cfg.Validate(); err != nil { return nil, nil, fmt.Errorf("invalid config: %w", err) } + if userHash, ok := httpmask.EarlyHandshakeUserHash(rawConn); ok { + return rawConn, &HandshakeMeta{UserHash: userHash}, nil + } handshakeTimeout := time.Duration(cfg.HandshakeTimeoutSeconds) * time.Second if handshakeTimeout <= 0 { diff --git a/mihomo/transport/sudoku/httpmask_tunnel.go b/mihomo/transport/sudoku/httpmask_tunnel.go index 1ff2bb3883..c066eb1c65 100644 --- a/mihomo/transport/sudoku/httpmask_tunnel.go +++ b/mihomo/transport/sudoku/httpmask_tunnel.go @@ -14,6 +14,30 @@ type HTTPMaskTunnelServer struct { ts *httpmask.TunnelServer } +func newHTTPMaskEarlyCodecConfig(cfg *ProtocolConfig, psk string) EarlyCodecConfig { + return EarlyCodecConfig{ + PSK: psk, + AEAD: cfg.AEADMethod, + EnablePureDownlink: cfg.EnablePureDownlink, + PaddingMin: cfg.PaddingMin, + PaddingMax: cfg.PaddingMax, + } +} + +func newClientHTTPMaskEarlyHandshake(cfg *ProtocolConfig) (*httpmask.ClientEarlyHandshake, error) { + table, err := pickClientTable(cfg) + if err != nil { + return nil, err + } + + return NewHTTPMaskClientEarlyHandshake( + newHTTPMaskEarlyCodecConfig(cfg, ClientAEADSeed(cfg.Key)), + table, + kipUserHashFromKey(cfg.Key), + KIPFeatAll, + ) +} + func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer { return newHTTPMaskTunnelServer(cfg, false) } @@ -35,6 +59,11 @@ func newHTTPMaskTunnelServer(cfg *ProtocolConfig, passThroughOnReject bool) *HTT Mode: cfg.HTTPMaskMode, PathRoot: cfg.HTTPMaskPathRoot, AuthKey: ServerAEADSeed(cfg.Key), + EarlyHandshake: NewHTTPMaskServerEarlyHandshake( + newHTTPMaskEarlyCodecConfig(cfg, ServerAEADSeed(cfg.Key)), + cfg.tableCandidates(), + globalHandshakeReplay.allow, + ), // When upstream fallback is enabled, preserve rejected HTTP requests for the caller. PassThroughOnReject: passThroughOnReject, }) @@ -101,14 +130,25 @@ func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *Protocol default: return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode) } + var ( + earlyHandshake *httpmask.ClientEarlyHandshake + err error + ) + if upgrade != nil { + earlyHandshake, err = newClientHTTPMaskEarlyHandshake(cfg) + if err != nil { + return nil, err + } + } return httpmask.DialTunnel(ctx, serverAddress, httpmask.TunnelDialOptions{ - Mode: cfg.HTTPMaskMode, - TLSEnabled: cfg.HTTPMaskTLSEnabled, - HostOverride: cfg.HTTPMaskHost, - PathRoot: cfg.HTTPMaskPathRoot, - AuthKey: ClientAEADSeed(cfg.Key), - Upgrade: upgrade, - Multiplex: cfg.HTTPMaskMultiplex, - DialContext: dial, + Mode: cfg.HTTPMaskMode, + TLSEnabled: cfg.HTTPMaskTLSEnabled, + HostOverride: cfg.HTTPMaskHost, + PathRoot: cfg.HTTPMaskPathRoot, + AuthKey: ClientAEADSeed(cfg.Key), + EarlyHandshake: earlyHandshake, + Upgrade: upgrade, + Multiplex: cfg.HTTPMaskMultiplex, + DialContext: dial, }) } diff --git a/mihomo/transport/sudoku/httpmask_tunnel_test.go b/mihomo/transport/sudoku/httpmask_tunnel_test.go index 8894882ef9..01eb3a5078 100644 --- a/mihomo/transport/sudoku/httpmask_tunnel_test.go +++ b/mihomo/transport/sudoku/httpmask_tunnel_test.go @@ -389,6 +389,68 @@ func TestHTTPMaskTunnel_WS_TCPRoundTrip(t *testing.T) { } } +func TestHTTPMaskTunnel_EarlyHandshake_TCPRoundTrip(t *testing.T) { + modes := []string{"stream", "poll", "ws"} + for _, mode := range modes { + t.Run(mode, func(t *testing.T) { + key := "tunnel-early-" + mode + target := "1.1.1.1:80" + + serverCfg := newTunnelTestTable(t, key) + serverCfg.HTTPMaskMode = mode + + addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error { + if s.Type != SessionTypeTCP { + return fmt.Errorf("unexpected session type: %v", s.Type) + } + if s.Target != target { + return fmt.Errorf("target mismatch: %s", s.Target) + } + _, _ = s.Conn.Write([]byte("ok")) + return nil + }) + defer stop() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + clientCfg := *serverCfg + clientCfg.ServerAddress = addr + + handshakeCfg := clientCfg + handshakeCfg.DisableHTTPMask = true + tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, func(raw net.Conn) (net.Conn, error) { + return ClientHandshake(raw, &handshakeCfg) + }) + if err != nil { + t.Fatalf("dial tunnel: %v", err) + } + defer tunnelConn.Close() + + addrBuf, err := EncodeAddress(target) + if err != nil { + t.Fatalf("encode addr: %v", err) + } + if err := WriteKIPMessage(tunnelConn, KIPTypeOpenTCP, addrBuf); err != nil { + t.Fatalf("write addr: %v", err) + } + + buf := make([]byte, 2) + if _, err := io.ReadFull(tunnelConn, buf); err != nil { + t.Fatalf("read: %v", err) + } + if string(buf) != "ok" { + t.Fatalf("unexpected payload: %q", buf) + } + + stop() + for err := range errCh { + t.Fatalf("server error: %v", err) + } + }) + } +} + func TestHTTPMaskTunnel_Validation(t *testing.T) { cfg := DefaultConfig() cfg.Key = "k" diff --git a/mihomo/transport/sudoku/obfs/httpmask/early_handshake.go b/mihomo/transport/sudoku/obfs/httpmask/early_handshake.go new file mode 100644 index 0000000000..54158577bc --- /dev/null +++ b/mihomo/transport/sudoku/obfs/httpmask/early_handshake.go @@ -0,0 +1,174 @@ +package httpmask + +import ( + "encoding/base64" + "errors" + "fmt" + "net" + "net/url" + "strings" +) + +const ( + tunnelEarlyDataQueryKey = "ed" + tunnelEarlyDataHeader = "X-Sudoku-Early" +) + +type ClientEarlyHandshake struct { + RequestPayload []byte + HandleResponse func(payload []byte) error + Ready func() bool + WrapConn func(raw net.Conn) (net.Conn, error) +} + +type TunnelServerEarlyHandshake struct { + Prepare func(payload []byte) (*PreparedServerEarlyHandshake, error) +} + +type PreparedServerEarlyHandshake struct { + ResponsePayload []byte + WrapConn func(raw net.Conn) (net.Conn, error) + UserHash string +} + +type earlyHandshakeMeta interface { + HTTPMaskEarlyHandshakeUserHash() string +} + +type earlyHandshakeConn struct { + net.Conn + userHash string +} + +func (c *earlyHandshakeConn) HTTPMaskEarlyHandshakeUserHash() string { + if c == nil { + return "" + } + return c.userHash +} + +func wrapEarlyHandshakeConn(conn net.Conn, userHash string) net.Conn { + if conn == nil { + return nil + } + return &earlyHandshakeConn{Conn: conn, userHash: userHash} +} + +func EarlyHandshakeUserHash(conn net.Conn) (string, bool) { + if conn == nil { + return "", false + } + v, ok := conn.(earlyHandshakeMeta) + if !ok { + return "", false + } + return v.HTTPMaskEarlyHandshakeUserHash(), true +} + +type authorizeResponse struct { + token string + earlyPayload []byte +} + +func isTunnelTokenByte(c byte) bool { + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '-' || + c == '_' +} + +func parseAuthorizeResponse(body []byte) (*authorizeResponse, error) { + s := strings.TrimSpace(string(body)) + idx := strings.Index(s, "token=") + if idx < 0 { + return nil, errors.New("missing token") + } + s = s[idx+len("token="):] + if s == "" { + return nil, errors.New("empty token") + } + + var b strings.Builder + for i := 0; i < len(s); i++ { + c := s[i] + if isTunnelTokenByte(c) { + b.WriteByte(c) + continue + } + break + } + token := b.String() + if token == "" { + return nil, errors.New("empty token") + } + + out := &authorizeResponse{token: token} + if earlyLine := findAuthorizeField(body, "ed="); earlyLine != "" { + decoded, err := base64.RawURLEncoding.DecodeString(earlyLine) + if err != nil { + return nil, fmt.Errorf("decode early authorize payload failed: %w", err) + } + out.earlyPayload = decoded + } + return out, nil +} + +func findAuthorizeField(body []byte, prefix string) string { + for _, line := range strings.Split(strings.TrimSpace(string(body)), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, prefix) { + return strings.TrimSpace(strings.TrimPrefix(line, prefix)) + } + } + return "" +} + +func setEarlyDataQuery(rawURL string, payload []byte) (string, error) { + if len(payload) == 0 { + return rawURL, nil + } + u, err := url.Parse(rawURL) + if err != nil { + return "", err + } + q := u.Query() + q.Set(tunnelEarlyDataQueryKey, base64.RawURLEncoding.EncodeToString(payload)) + u.RawQuery = q.Encode() + return u.String(), nil +} + +func parseEarlyDataQuery(u *url.URL) ([]byte, error) { + if u == nil { + return nil, nil + } + val := strings.TrimSpace(u.Query().Get(tunnelEarlyDataQueryKey)) + if val == "" { + return nil, nil + } + return base64.RawURLEncoding.DecodeString(val) +} + +func applyEarlyHandshakeOrUpgrade(raw net.Conn, opts TunnelDialOptions) (net.Conn, error) { + out := raw + if opts.EarlyHandshake != nil && opts.EarlyHandshake.WrapConn != nil && (opts.EarlyHandshake.Ready == nil || opts.EarlyHandshake.Ready()) { + wrapped, err := opts.EarlyHandshake.WrapConn(raw) + if err != nil { + return nil, err + } + if wrapped != nil { + out = wrapped + } + return out, nil + } + if opts.Upgrade != nil { + wrapped, err := opts.Upgrade(raw) + if err != nil { + return nil, err + } + if wrapped != nil { + out = wrapped + } + } + return out, nil +} diff --git a/mihomo/transport/sudoku/obfs/httpmask/tunnel.go b/mihomo/transport/sudoku/obfs/httpmask/tunnel.go index 20981c3906..a100c6202e 100644 --- a/mihomo/transport/sudoku/obfs/httpmask/tunnel.go +++ b/mihomo/transport/sudoku/obfs/httpmask/tunnel.go @@ -72,6 +72,10 @@ type TunnelDialOptions struct { // AuthKey enables short-term HMAC auth for HTTP tunnel requests (anti-probing). // When set (non-empty), each HTTP request carries an Authorization bearer token derived from AuthKey. AuthKey string + // EarlyHandshake folds the protocol handshake into the HTTP/WS setup round trip. + // When the server accepts the early payload, DialTunnel returns a conn that is already post-handshake. + // When the server does not echo early data, DialTunnel falls back to Upgrade. + EarlyHandshake *ClientEarlyHandshake // Upgrade optionally wraps the raw tunnel conn and/or writes a small prelude before DialTunnel returns. // It is called with the raw tunnel conn; if it returns a non-nil conn, that conn is returned by DialTunnel. Upgrade func(raw net.Conn) (net.Conn, error) @@ -225,30 +229,11 @@ func canonicalHeaderHost(urlHost, scheme string) string { } func parseTunnelToken(body []byte) (string, error) { - s := strings.TrimSpace(string(body)) - idx := strings.Index(s, "token=") - if idx < 0 { - return "", errors.New("missing token") + resp, err := parseAuthorizeResponse(body) + if err != nil { + return "", err } - s = s[idx+len("token="):] - if s == "" { - return "", errors.New("empty token") - } - // Token is base64.RawURLEncoding (A-Z a-z 0-9 - _). Strip any trailing bytes (e.g. from CDN compression). - var b strings.Builder - for i := 0; i < len(s); i++ { - c := s[i] - if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' { - b.WriteByte(c) - continue - } - break - } - token := b.String() - if token == "" { - return "", errors.New("empty token") - } - return token, nil + return resp.token, nil } type httpClientTarget struct { @@ -353,6 +338,13 @@ func dialSessionWithClient(ctx context.Context, client *http.Client, target http auth := newTunnelAuth(opts.AuthKey, 0) authorizeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/session")}).String() + if opts.EarlyHandshake != nil && len(opts.EarlyHandshake.RequestPayload) > 0 { + var err error + authorizeURL, err = setEarlyDataQuery(authorizeURL, opts.EarlyHandshake.RequestPayload) + if err != nil { + return nil, err + } + } var bodyBytes []byte for attempt := 0; ; attempt++ { @@ -410,13 +402,19 @@ func dialSessionWithClient(ctx context.Context, client *http.Client, target http break } - token, err := parseTunnelToken(bodyBytes) + authResp, err := parseAuthorizeResponse(bodyBytes) if err != nil { return nil, fmt.Errorf("%s authorize failed: %q", mode, strings.TrimSpace(string(bodyBytes))) } + token := authResp.token if token == "" { return nil, fmt.Errorf("%s authorize empty token", mode) } + if opts.EarlyHandshake != nil && len(authResp.earlyPayload) > 0 && opts.EarlyHandshake.HandleResponse != nil { + if err := opts.EarlyHandshake.HandleResponse(authResp.earlyPayload); err != nil { + return nil, err + } + } pushURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/api/v1/upload"), RawQuery: "token=" + url.QueryEscape(token)}).String() pullURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/stream"), RawQuery: "token=" + url.QueryEscape(token)}).String() @@ -671,16 +669,10 @@ func dialStreamSplitWithClient(ctx context.Context, client *http.Client, target if c == nil { return nil, fmt.Errorf("failed to build stream split conn") } - outConn := net.Conn(c) - if opts.Upgrade != nil { - upgraded, err := opts.Upgrade(c) - if err != nil { - _ = c.Close() - return nil, err - } - if upgraded != nil { - outConn = upgraded - } + outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) + if err != nil { + _ = c.Close() + return nil, err } return outConn, nil } @@ -694,16 +686,10 @@ func dialStreamSplit(ctx context.Context, serverAddress string, opts TunnelDialO if c == nil { return nil, fmt.Errorf("failed to build stream split conn") } - outConn := net.Conn(c) - if opts.Upgrade != nil { - upgraded, err := opts.Upgrade(c) - if err != nil { - _ = c.Close() - return nil, err - } - if upgraded != nil { - outConn = upgraded - } + outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) + if err != nil { + _ = c.Close() + return nil, err } return outConn, nil } @@ -1120,16 +1106,10 @@ func dialPollWithClient(ctx context.Context, client *http.Client, target httpCli if c == nil { return nil, fmt.Errorf("failed to build poll conn") } - outConn := net.Conn(c) - if opts.Upgrade != nil { - upgraded, err := opts.Upgrade(c) - if err != nil { - _ = c.Close() - return nil, err - } - if upgraded != nil { - outConn = upgraded - } + outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) + if err != nil { + _ = c.Close() + return nil, err } return outConn, nil } @@ -1143,16 +1123,10 @@ func dialPoll(ctx context.Context, serverAddress string, opts TunnelDialOptions) if c == nil { return nil, fmt.Errorf("failed to build poll conn") } - outConn := net.Conn(c) - if opts.Upgrade != nil { - upgraded, err := opts.Upgrade(c) - if err != nil { - _ = c.Close() - return nil, err - } - if upgraded != nil { - outConn = upgraded - } + outConn, err := applyEarlyHandshakeOrUpgrade(c, opts) + if err != nil { + _ = c.Close() + return nil, err } return outConn, nil } @@ -1528,6 +1502,8 @@ type TunnelServerOptions struct { PullReadTimeout time.Duration // SessionTTL is a best-effort TTL to prevent leaked sessions. 0 uses a conservative default. SessionTTL time.Duration + // EarlyHandshake optionally folds the protocol handshake into the initial HTTP/WS round trip. + EarlyHandshake *TunnelServerEarlyHandshake } type TunnelServer struct { @@ -1538,6 +1514,7 @@ type TunnelServer struct { pullReadTimeout time.Duration sessionTTL time.Duration + earlyHandshake *TunnelServerEarlyHandshake mu sync.Mutex sessions map[string]*tunnelSession @@ -1570,6 +1547,7 @@ func NewTunnelServer(opts TunnelServerOptions) *TunnelServer { passThroughOnReject: opts.PassThroughOnReject, pullReadTimeout: timeout, sessionTTL: ttl, + earlyHandshake: opts.EarlyHandshake, sessions: make(map[string]*tunnelSession), } } @@ -1925,9 +1903,12 @@ func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, he switch strings.ToUpper(req.method) { case http.MethodGet: - // Stream split-session: GET /session (no token) => token + start tunnel on a server-side pipe. if token == "" && path == "/session" { - return s.sessionAuthorize(rawConn) + earlyPayload, err := parseEarlyDataQuery(u) + if err != nil { + return rejectOrReply(http.StatusBadRequest, "bad request") + } + return s.sessionAuthorize(rawConn, earlyPayload) } // Stream split-session: GET /stream?token=... => downlink poll. if token != "" && path == "/stream" { @@ -2045,10 +2026,18 @@ func writeSimpleHTTPResponse(w io.Writer, code int, body string) error { func writeTokenHTTPResponse(w io.Writer, token string) error { token = strings.TrimRight(token, "\r\n") - // Use application/octet-stream to avoid CDN auto-compression (e.g. brotli) breaking clients that expect a plain token string. + return writeTokenHTTPResponseWithEarlyData(w, token, nil) +} + +func writeTokenHTTPResponseWithEarlyData(w io.Writer, token string, earlyPayload []byte) error { + token = strings.TrimRight(token, "\r\n") + body := "token=" + token + if len(earlyPayload) > 0 { + body += "\ned=" + base64.RawURLEncoding.EncodeToString(earlyPayload) + } _, err := io.WriteString(w, - fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nCache-Control: no-store\r\nPragma: no-cache\r\nContent-Length: %d\r\nConnection: close\r\n\r\ntoken=%s", - len("token=")+len(token), token)) + fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nCache-Control: no-store\r\nPragma: no-cache\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", + len(body), body)) return err } @@ -2088,7 +2077,11 @@ func (s *TunnelServer) handlePoll(rawConn net.Conn, req *httpRequestHeader, head switch strings.ToUpper(req.method) { case http.MethodGet: if token == "" && path == "/session" { - return s.sessionAuthorize(rawConn) + earlyPayload, err := parseEarlyDataQuery(u) + if err != nil { + return rejectOrReply(http.StatusBadRequest, "bad request") + } + return s.sessionAuthorize(rawConn, earlyPayload) } if token != "" && path == "/stream" { if s.passThroughOnReject && !s.sessionHas(token) { @@ -2128,7 +2121,7 @@ func (s *TunnelServer) handlePoll(rawConn net.Conn, req *httpRequestHeader, head } } -func (s *TunnelServer) sessionAuthorize(rawConn net.Conn) (HandleResult, net.Conn, error) { +func (s *TunnelServer) sessionAuthorize(rawConn net.Conn, earlyPayload []byte) (HandleResult, net.Conn, error) { token, err := newSessionToken() if err != nil { _ = writeSimpleHTTPResponse(rawConn, http.StatusInternalServerError, "internal error") @@ -2137,6 +2130,37 @@ func (s *TunnelServer) sessionAuthorize(rawConn net.Conn) (HandleResult, net.Con } c1, c2 := newHalfPipe() + outConn := net.Conn(c1) + var responsePayload []byte + var userHash string + if len(earlyPayload) > 0 && s.earlyHandshake != nil && s.earlyHandshake.Prepare != nil { + prepared, err := s.earlyHandshake.Prepare(earlyPayload) + if err != nil { + _ = c1.Close() + _ = c2.Close() + if s.passThroughOnReject { + return HandlePassThrough, newRejectedPreBufferedConn(rawConn, nil), nil + } + _ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found") + _ = rawConn.Close() + return HandleDone, nil, nil + } + responsePayload = prepared.ResponsePayload + userHash = prepared.UserHash + if prepared.WrapConn != nil { + wrapped, err := prepared.WrapConn(c1) + if err != nil { + _ = c1.Close() + _ = c2.Close() + _ = writeSimpleHTTPResponse(rawConn, http.StatusInternalServerError, "internal error") + _ = rawConn.Close() + return HandleDone, nil, nil + } + if wrapped != nil { + outConn = wrapEarlyHandshakeConn(wrapped, userHash) + } + } + } s.mu.Lock() s.sessions[token] = &tunnelSession{conn: c2, lastActive: time.Now()} @@ -2144,9 +2168,9 @@ func (s *TunnelServer) sessionAuthorize(rawConn net.Conn) (HandleResult, net.Con go s.reapLater(token) - _ = writeTokenHTTPResponse(rawConn, token) + _ = writeTokenHTTPResponseWithEarlyData(rawConn, token, responsePayload) _ = rawConn.Close() - return HandleStartTunnel, c1, nil + return HandleStartTunnel, outConn, nil } func newSessionToken() (string, error) { diff --git a/mihomo/transport/sudoku/obfs/httpmask/tunnel_ws.go b/mihomo/transport/sudoku/obfs/httpmask/tunnel_ws.go index e1299e3da1..8ef8d5c3af 100644 --- a/mihomo/transport/sudoku/obfs/httpmask/tunnel_ws.go +++ b/mihomo/transport/sudoku/obfs/httpmask/tunnel_ws.go @@ -2,6 +2,7 @@ package httpmask import ( "context" + "encoding/base64" "fmt" "io" mrand "math/rand" @@ -115,6 +116,16 @@ func dialWS(ctx context.Context, serverAddress string, opts TunnelDialOptions) ( Host: urlHost, Path: joinPathRoot(opts.PathRoot, "/ws"), } + if opts.EarlyHandshake != nil && len(opts.EarlyHandshake.RequestPayload) > 0 { + rawURL, err := setEarlyDataQuery(u.String(), opts.EarlyHandshake.RequestPayload) + if err != nil { + return nil, err + } + u, err = url.Parse(rawURL) + if err != nil { + return nil, err + } + } header := make(stdhttp.Header) applyWSHeaders(header, headerHost) @@ -132,6 +143,16 @@ func dialWS(ctx context.Context, serverAddress string, opts TunnelDialOptions) ( d := ws.Dialer{ Host: headerHost, Header: ws.HandshakeHeaderHTTP(header), + OnHeader: func(key, value []byte) error { + if !strings.EqualFold(string(key), tunnelEarlyDataHeader) || opts.EarlyHandshake == nil || opts.EarlyHandshake.HandleResponse == nil { + return nil + } + decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(string(value))) + if err != nil { + return err + } + return opts.EarlyHandshake.HandleResponse(decoded) + }, NetDial: func(dialCtx context.Context, network, addr string) (net.Conn, error) { if addr == urlHost { addr = dialAddr @@ -161,16 +182,10 @@ func dialWS(ctx context.Context, serverAddress string, opts TunnelDialOptions) ( } wsConn := newWSStreamConn(conn, ws.StateClientSide) - if opts.Upgrade == nil { - return wsConn, nil - } - upgraded, err := opts.Upgrade(wsConn) + upgraded, err := applyEarlyHandshakeOrUpgrade(wsConn, opts) if err != nil { _ = wsConn.Close() return nil, err } - if upgraded != nil { - return upgraded, nil - } - return wsConn, nil + return upgraded, nil } diff --git a/mihomo/transport/sudoku/obfs/httpmask/tunnel_ws_server.go b/mihomo/transport/sudoku/obfs/httpmask/tunnel_ws_server.go index 3e79e58aff..b17b1ded3d 100644 --- a/mihomo/transport/sudoku/obfs/httpmask/tunnel_ws_server.go +++ b/mihomo/transport/sudoku/obfs/httpmask/tunnel_ws_server.go @@ -1,6 +1,7 @@ package httpmask import ( + "encoding/base64" "net" "net/http" "net/url" @@ -63,15 +64,46 @@ func (s *TunnelServer) handleWS(rawConn net.Conn, req *httpRequestHeader, header return rejectOrReply(http.StatusNotFound, "not found") } + earlyPayload, err := parseEarlyDataQuery(u) + if err != nil { + return rejectOrReply(http.StatusBadRequest, "bad request") + } + var prepared *PreparedServerEarlyHandshake + if len(earlyPayload) > 0 && s.earlyHandshake != nil && s.earlyHandshake.Prepare != nil { + prepared, err = s.earlyHandshake.Prepare(earlyPayload) + if err != nil { + return rejectOrReply(http.StatusNotFound, "not found") + } + } + prefix := make([]byte, 0, len(headerBytes)+len(buffered)) prefix = append(prefix, headerBytes...) prefix = append(prefix, buffered...) wsConnRaw := newPreBufferedConn(rawConn, prefix) - if _, err := ws.Upgrade(wsConnRaw); err != nil { + upgrader := ws.Upgrader{} + if prepared != nil && len(prepared.ResponsePayload) > 0 { + upgrader.OnBeforeUpgrade = func() (ws.HandshakeHeader, error) { + h := http.Header{} + h.Set(tunnelEarlyDataHeader, base64.RawURLEncoding.EncodeToString(prepared.ResponsePayload)) + return ws.HandshakeHeaderHTTP(h), nil + } + } + if _, err := upgrader.Upgrade(wsConnRaw); err != nil { _ = rawConn.Close() return HandleDone, nil, nil } - return HandleStartTunnel, newWSStreamConn(wsConnRaw, ws.StateServerSide), nil + outConn := net.Conn(newWSStreamConn(wsConnRaw, ws.StateServerSide)) + if prepared != nil && prepared.WrapConn != nil { + wrapped, err := prepared.WrapConn(outConn) + if err != nil { + _ = outConn.Close() + return HandleDone, nil, nil + } + if wrapped != nil { + outConn = wrapEarlyHandshakeConn(wrapped, prepared.UserHash) + } + } + return HandleStartTunnel, outConn, nil } diff --git a/mihomo/transport/sudoku/obfs/sudoku/packed.go b/mihomo/transport/sudoku/obfs/sudoku/packed.go index 346314a32d..0edf4f32b4 100644 --- a/mihomo/transport/sudoku/obfs/sudoku/packed.go +++ b/mihomo/transport/sudoku/obfs/sudoku/packed.go @@ -11,35 +11,35 @@ import ( ) const ( - // 每次从 RNG 获取批量随机数的缓存大小,减少 RNG 函数调用开销 RngBatchSize = 128 + + packedProtectedPrefixBytes = 14 ) -// 1. 使用 12字节->16组 的块处理优化 Write (减少循环开销) -// 2. 使用整数阈值随机概率判断 Padding,与纯 Sudoku 保持流量特征一致 -// 3. Read 使用 copy 移动避免底层数组泄漏 +// PackedConn encodes traffic with the packed Sudoku layout while preserving +// the same padding model as the regular connection. type PackedConn struct { net.Conn table *Table reader *bufio.Reader - // 读缓冲 + // Read-side buffers. rawBuf []byte - pendingData []byte // 解码后尚未被 Read 取走的字节 + pendingData []byte - // 写缓冲与状态 + // Write-side state. writeMu sync.Mutex writeBuf []byte - bitBuf uint64 // 暂存的位数据 - bitCount int // 暂存的位数 + bitBuf uint64 + bitCount int - // 读状态 + // Read-side bit accumulator. readBitBuf uint64 readBits int - // 随机数与填充控制 - 使用整数阈值随机,与 Conn 一致 + // Padding selection matches Conn's threshold-based model. rng *rand.Rand - paddingThreshold uint64 // 与 Conn 保持一致的随机概率模型 + paddingThreshold uint64 padMarker byte padPool []byte } @@ -95,7 +95,6 @@ func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn { return pc } -// maybeAddPadding 内联辅助:根据概率阈值插入 padding func (pc *PackedConn) maybeAddPadding(out []byte) []byte { if shouldPad(pc.rng, pc.paddingThreshold) { out = append(out, pc.getPaddingByte()) @@ -103,7 +102,73 @@ func (pc *PackedConn) maybeAddPadding(out []byte) []byte { return out } -// Write 极致优化版 - 批量处理 12 字节 +func (pc *PackedConn) appendGroup(out []byte, group byte) []byte { + out = pc.maybeAddPadding(out) + return append(out, pc.encodeGroup(group)) +} + +func (pc *PackedConn) appendForcedPadding(out []byte) []byte { + return append(out, pc.getPaddingByte()) +} + +func (pc *PackedConn) nextProtectedPrefixGap() int { + return 1 + pc.rng.Intn(2) +} + +func (pc *PackedConn) writeProtectedPrefix(out []byte, p []byte) ([]byte, int) { + if len(p) == 0 { + return out, 0 + } + + limit := len(p) + if limit > packedProtectedPrefixBytes { + limit = packedProtectedPrefixBytes + } + + for padCount := 0; padCount < 1+pc.rng.Intn(2); padCount++ { + out = pc.appendForcedPadding(out) + } + + gap := pc.nextProtectedPrefixGap() + effective := 0 + for i := 0; i < limit; i++ { + pc.bitBuf = (pc.bitBuf << 8) | uint64(p[i]) + pc.bitCount += 8 + for pc.bitCount >= 6 { + pc.bitCount -= 6 + group := byte(pc.bitBuf >> pc.bitCount) + if pc.bitCount == 0 { + pc.bitBuf = 0 + } else { + pc.bitBuf &= (1 << pc.bitCount) - 1 + } + out = pc.appendGroup(out, group&0x3F) + } + + effective++ + if effective >= gap { + out = pc.appendForcedPadding(out) + effective = 0 + gap = pc.nextProtectedPrefixGap() + } + } + + return out, limit +} + +func (pc *PackedConn) drainPendingData(dst []byte) int { + n := copy(dst, pc.pendingData) + if n == len(pc.pendingData) { + pc.pendingData = pc.pendingData[:0] + return n + } + + remaining := len(pc.pendingData) - n + copy(pc.pendingData, pc.pendingData[n:]) + pc.pendingData = pc.pendingData[:remaining] + return n +} + func (pc *PackedConn) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil @@ -112,20 +177,19 @@ func (pc *PackedConn) Write(p []byte) (int, error) { pc.writeMu.Lock() defer pc.writeMu.Unlock() - // 1. 预分配内存,避免 append 导致的多次扩容 - // 预估:原数据 * 1.5 (4/3 + padding 余量) needed := len(p)*3/2 + 32 if cap(pc.writeBuf) < needed { pc.writeBuf = make([]byte, 0, needed) } out := pc.writeBuf[:0] - i := 0 + var prefixN int + out, prefixN = pc.writeProtectedPrefix(out, p) + + i := prefixN n := len(p) - // 2. 头部对齐处理 (Slow Path) for pc.bitCount > 0 && i < n { - out = pc.maybeAddPadding(out) b := p[i] i++ pc.bitBuf = (pc.bitBuf << 8) | uint64(b) @@ -138,14 +202,11 @@ func (pc *PackedConn) Write(p []byte) (int, error) { } else { pc.bitBuf &= (1 << pc.bitCount) - 1 } - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(group&0x3F)) + out = pc.appendGroup(out, group&0x3F) } } - // 3. 极速批量处理 (Fast Path) - 每次处理 12 字节 → 生成 16 个编码组 for i+11 < n { - // 处理 4 组,每组 3 字节 for batch := 0; batch < 4; batch++ { b1, b2, b3 := p[i], p[i+1], p[i+2] i += 3 @@ -155,19 +216,13 @@ func (pc *PackedConn) Write(p []byte) (int, error) { g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03) g4 := b3 & 0x3F - // 每个组之前都有概率插入 padding - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g1)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g2)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g3)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g4)) + out = pc.appendGroup(out, g1) + out = pc.appendGroup(out, g2) + out = pc.appendGroup(out, g3) + out = pc.appendGroup(out, g4) } } - // 4. 处理剩余的 3 字节块 for i+2 < n { b1, b2, b3 := p[i], p[i+1], p[i+2] i += 3 @@ -177,17 +232,12 @@ func (pc *PackedConn) Write(p []byte) (int, error) { g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03) g4 := b3 & 0x3F - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g1)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g2)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g3)) - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(g4)) + out = pc.appendGroup(out, g1) + out = pc.appendGroup(out, g2) + out = pc.appendGroup(out, g3) + out = pc.appendGroup(out, g4) } - // 5. 尾部处理 (Tail Path) - 处理剩余的 1 或 2 个字节 for ; i < n; i++ { b := p[i] pc.bitBuf = (pc.bitBuf << 8) | uint64(b) @@ -200,35 +250,28 @@ func (pc *PackedConn) Write(p []byte) (int, error) { } else { pc.bitBuf &= (1 << pc.bitCount) - 1 } - out = pc.maybeAddPadding(out) - out = append(out, pc.encodeGroup(group&0x3F)) + out = pc.appendGroup(out, group&0x3F) } } - // 6. 处理残留位 if pc.bitCount > 0 { - out = pc.maybeAddPadding(out) group := byte(pc.bitBuf << (6 - pc.bitCount)) pc.bitBuf = 0 pc.bitCount = 0 - out = append(out, pc.encodeGroup(group&0x3F)) + out = pc.appendGroup(out, group&0x3F) out = append(out, pc.padMarker) } - // 尾部可能添加 padding out = pc.maybeAddPadding(out) - // 发送数据 if len(out) > 0 { - _, err := pc.Conn.Write(out) pc.writeBuf = out[:0] - return len(p), err + return len(p), writeFull(pc.Conn, out) } pc.writeBuf = out[:0] return len(p), nil } -// Flush 处理最后不足 6 bit 的情况 func (pc *PackedConn) Flush() error { pc.writeMu.Lock() defer pc.writeMu.Unlock() @@ -243,38 +286,34 @@ func (pc *PackedConn) Flush() error { out = append(out, pc.padMarker) } - // 尾部随机添加 padding out = pc.maybeAddPadding(out) if len(out) > 0 { - _, err := pc.Conn.Write(out) pc.writeBuf = out[:0] - return err + return writeFull(pc.Conn, out) } return nil } -// Read 优化版:减少切片操作,避免内存泄漏 -func (pc *PackedConn) Read(p []byte) (int, error) { - // 1. 优先返回待处理区的数据 - if len(pc.pendingData) > 0 { - n := copy(p, pc.pendingData) - if n == len(pc.pendingData) { - pc.pendingData = pc.pendingData[:0] - } else { - // 优化:移动剩余数据到数组头部,避免切片指向中间导致内存泄漏 - remaining := len(pc.pendingData) - n - copy(pc.pendingData, pc.pendingData[n:]) - pc.pendingData = pc.pendingData[:remaining] +func writeFull(w io.Writer, b []byte) error { + for len(b) > 0 { + n, err := w.Write(b) + if err != nil { + return err } - return n, nil + b = b[n:] + } + return nil +} + +func (pc *PackedConn) Read(p []byte) (int, error) { + if len(pc.pendingData) > 0 { + return pc.drainPendingData(p), nil } - // 2. 循环读取直到解出数据或出错 for { nr, rErr := pc.reader.Read(pc.rawBuf) if nr > 0 { - // 缓存频繁访问的变量 rBuf := pc.readBitBuf rBits := pc.readBits padMarker := pc.padMarker @@ -324,24 +363,13 @@ func (pc *PackedConn) Read(p []byte) (int, error) { } } - // 3. 返回解码后的数据 - 优化:避免底层数组泄漏 - n := copy(p, pc.pendingData) - if n == len(pc.pendingData) { - pc.pendingData = pc.pendingData[:0] - } else { - remaining := len(pc.pendingData) - n - copy(pc.pendingData, pc.pendingData[n:]) - pc.pendingData = pc.pendingData[:remaining] - } - return n, nil + return pc.drainPendingData(p), nil } -// getPaddingByte 从 Pool 中随机取 Padding 字节 func (pc *PackedConn) getPaddingByte() byte { return pc.padPool[pc.rng.Intn(len(pc.padPool))] } -// encodeGroup 编码 6-bit 组 func (pc *PackedConn) encodeGroup(group byte) byte { return pc.table.layout.encodeGroup(group) } diff --git a/mihomo/transport/sudoku/obfs/sudoku/packed_prefix_test.go b/mihomo/transport/sudoku/obfs/sudoku/packed_prefix_test.go new file mode 100644 index 0000000000..f041c0f561 --- /dev/null +++ b/mihomo/transport/sudoku/obfs/sudoku/packed_prefix_test.go @@ -0,0 +1,91 @@ +package sudoku + +import ( + "bytes" + "io" + "math/rand" + "net" + "testing" + "time" +) + +type mockConn struct { + readBuf []byte + writeBuf []byte +} + +func (c *mockConn) Read(p []byte) (int, error) { + if len(c.readBuf) == 0 { + return 0, io.EOF + } + n := copy(p, c.readBuf) + c.readBuf = c.readBuf[n:] + return n, nil +} + +func (c *mockConn) Write(p []byte) (int, error) { + c.writeBuf = append(c.writeBuf, p...) + return len(p), nil +} + +func (c *mockConn) Close() error { return nil } +func (c *mockConn) LocalAddr() net.Addr { return nil } +func (c *mockConn) RemoteAddr() net.Addr { return nil } +func (c *mockConn) SetDeadline(time.Time) error { return nil } +func (c *mockConn) SetReadDeadline(time.Time) error { return nil } +func (c *mockConn) SetWriteDeadline(time.Time) error { return nil } + +func TestPackedConn_ProtectedPrefixPadding(t *testing.T) { + table := NewTable("packed-prefix-seed", "prefer_ascii") + mock := &mockConn{} + writer := NewPackedConn(mock, table, 0, 0) + writer.rng = rand.New(rand.NewSource(1)) + + payload := bytes.Repeat([]byte{0}, 32) + if _, err := writer.Write(payload); err != nil { + t.Fatalf("write: %v", err) + } + + wire := append([]byte(nil), mock.writeBuf...) + if len(wire) < 20 { + t.Fatalf("wire too short: %d", len(wire)) + } + + firstHint := -1 + nonHintCount := 0 + maxHintRun := 0 + currentHintRun := 0 + for i, b := range wire[:20] { + if table.layout.isHint(b) { + if firstHint == -1 { + firstHint = i + } + currentHintRun++ + if currentHintRun > maxHintRun { + maxHintRun = currentHintRun + } + continue + } + nonHintCount++ + currentHintRun = 0 + } + + if firstHint < 1 || firstHint > 2 { + t.Fatalf("expected 1-2 leading padding bytes, first hint index=%d", firstHint) + } + if nonHintCount < 6 { + t.Fatalf("expected dense prefix padding, got only %d non-hint bytes in first 20", nonHintCount) + } + if maxHintRun > 3 { + t.Fatalf("prefix still exposes long hint run: %d", maxHintRun) + } + + reader := NewPackedConn(&mockConn{readBuf: wire}, table, 0, 0) + decoded := make([]byte, len(payload)) + if _, err := io.ReadFull(reader, decoded); err != nil { + t.Fatalf("read back: %v", err) + } + if !bytes.Equal(decoded, payload) { + t.Fatalf("roundtrip mismatch") + } +} diff --git a/mihomo/transport/trusttunnel/force_close.go b/mihomo/transport/trusttunnel/force_close.go index 3253b6c64c..d34b9376df 100644 --- a/mihomo/transport/trusttunnel/force_close.go +++ b/mihomo/transport/trusttunnel/force_close.go @@ -11,7 +11,7 @@ func forceCloseAllConnections(roundTripper RoundTripper) { roundTripper.CloseIdleConnections() switch tr := roundTripper.(type) { case *http.Http2Transport: - gun.CloseTransport(tr) + gun.CloseHttp2Transport(tr) case *http3.Transport: _ = tr.Close() } diff --git a/openwrt-packages/luci-app-partexp/luci-app-partexp/Makefile b/openwrt-packages/luci-app-partexp/luci-app-partexp/Makefile index d112ef67bf..c94e807f7d 100644 --- a/openwrt-packages/luci-app-partexp/luci-app-partexp/Makefile +++ b/openwrt-packages/luci-app-partexp/luci-app-partexp/Makefile @@ -11,7 +11,7 @@ LUCI_TITLE:=LuCI Support for Automatic Partition Mount LUCI_PKGARCH:=all LUCI_DEPENDS:=+fdisk +block-mount +bc +blkid +parted +btrfs-progs +losetup +resize2fs +e2fsprogs +f2fs-tools +kmod-loop PKG_VERSION:=2.0.3 -PKG_RELEASE:=20260228 +PKG_RELEASE:=20260310 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Sirpdboy diff --git a/openwrt-packages/luci-app-partexp/luci-app-partexp/root/usr/bin/partexp b/openwrt-packages/luci-app-partexp/luci-app-partexp/root/usr/bin/partexp index a58d038516..0ded8656d1 100644 --- a/openwrt-packages/luci-app-partexp/luci-app-partexp/root/usr/bin/partexp +++ b/openwrt-packages/luci-app-partexp/luci-app-partexp/root/usr/bin/partexp @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright (C) 2021-2025 sirpdboy https://github.com/sirpdboy/luci-app-partexp +# Copyright (C) 2021-2026 sirpdboy https://github.com/sirpdboy/luci-app-partexp # # This is free software, licensed under the Apache License, Version 2.0 . # @@ -279,8 +279,8 @@ check_part_space() { check_free_space() { DISK=$1 PARTED_OUTPUT=$(parted -s /dev/$DISK unit GB print free 2>/dev/null) - FREE_SPACE=$(echo "$PARTED_OUTPUT" | grep "Free Space" | awk '{print $3}' ) - echo $FREE_SPACE |awk -F '.' '{print $1}' | sed 's/[A-Za-z]//g' + FREE_SPACE=$(echo "$PARTED_OUTPUT" | awk '/Free Space/ {last=$3} END {print last}' | sed 's/GB$//') + echo $FREE_SPACE | cut -d'.' -f1 } show_partition_info() { @@ -418,12 +418,6 @@ rootfs_resize() { fi } -get_config() { - config_get target_function $1 target_function 1 - config_get target_disk $1 target_disk 1 - config_get_bool keep_config $1 keep_config 1 - config_get format_type $1 format_type -} # 修改 fdiskB 函数,使用环境变量参数 fdiskB() { diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua index ac6d637e9f..ddd5d7d590 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua @@ -1521,3 +1521,13 @@ function get_core(field, candidates) end return nil end + +function cleanEmptyTables(t) + if type(t) ~= "table" then return nil end + for k, v in pairs(t) do + if type(v) == "table" then + t[k] = cleanEmptyTables(v) + end + end + return next(t) and t or nil +end diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua index 9bcacff2f0..4585f66cdb 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -241,31 +241,21 @@ function gen_outbound(flag, node, tag, proxy_table) path = node.xhttp_path or "/", host = node.xhttp_host, extra = (function() - local extra_tbl = {} - -- 解析 xhttp_extra 并做简单容错处理 + local extra = {} if node.xhttp_extra then - local success, parsed = pcall(jsonc.parse, api.base64Decode(node.xhttp_extra)) - if success and parsed then - extra_tbl = parsed.extra or parsed - for k, v in pairs(extra_tbl) do - if (type(v) == "table" and next(v) == nil) or v == nil then - extra_tbl[k] = nil - end - end + local ok, parsed = pcall(jsonc.parse, api.base64Decode(node.xhttp_extra)) + if ok and type(parsed) == "table" then + extra = parsed.extra or parsed end end -- 处理 User-Agent if node.user_agent and node.user_agent ~= "" then - extra_tbl.headers = extra_tbl.headers or {} - if not extra_tbl.headers["User-Agent"] and not extra_tbl.headers["user-agent"] then - extra_tbl.headers["User-Agent"] = node.user_agent + extra.headers = extra.headers or {} + if not extra.headers["User-Agent"] and not extra.headers["user-agent"] then + extra.headers["User-Agent"] = node.user_agent end end - -- 清理空的 headers - if extra_tbl.headers and next(extra_tbl.headers) == nil then - extra_tbl.headers = nil - end - return next(extra_tbl) ~= nil and extra_tbl or nil + return api.cleanEmptyTables(extra) end)() } or nil, hysteriaSettings = (node.transport == "hysteria") and { @@ -322,25 +312,10 @@ function gen_outbound(flag, node, tag, proxy_table) if node.finalmask and node.finalmask ~= "" then local ok, fm = pcall(jsonc.parse, api.base64Decode(node.finalmask)) if ok and type(fm) == "table" then - if not finalmask or not next(finalmask) then - finalmask = fm - else - if type(fm.udp) == "table" then - finalmask.udp = finalmask.udp or {} - for i = 1, #fm.udp do - finalmask.udp[#finalmask.udp+1] = fm.udp[i] - end - end - if type(fm.tcp) == "table" then - finalmask.tcp = fm.tcp - end - if type(fm.quicParams) == "table" then - finalmask.quicParams = fm.quicParams - end - end + finalmask = fm end end - return (finalmask and next(finalmask)) and finalmask or nil + return api.cleanEmptyTables(finalmask) end)() } or nil, settings = { @@ -649,25 +624,10 @@ function gen_config_server(node) if node.finalmask and node.finalmask ~= "" then local ok, fm = pcall(jsonc.parse, api.base64Decode(node.finalmask)) if ok and type(fm) == "table" then - if not finalmask or not next(finalmask) then - finalmask = fm - else - if type(fm.udp) == "table" then - finalmask.udp = finalmask.udp or {} - for i = 1, #fm.udp do - finalmask.udp[#finalmask.udp+1] = fm.udp[i] - end - end - if type(fm.tcp) == "table" then - finalmask.tcp = fm.tcp - end - if type(fm.quicParams) == "table" then - finalmask.quicParams = fm.quicParams - end - end + finalmask = fm end end - return (finalmask and next(finalmask)) and finalmask or nil + return api.cleanEmptyTables(finalmask) end)(), sockopt = { tcpFastOpen = (node.tcp_fast_open == "1") and true or nil, diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js index 53b23b056f..8393bf3474 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js @@ -124,6 +124,10 @@ const glossary = { prefmt: 'rule_%s', field: 'rule-providers', }, + inbound: { + prefmt: 'inbound_%s', + field: 'listeners', + }, server: { prefmt: 'server_%s', field: 'listeners', @@ -148,7 +152,8 @@ const inbound_type = [ ['anytls', _('AnyTLS') + ' - ' + _('TCP')], ['tuic', _('TUIC') + ' - ' + _('UDP')], ['hysteria2', _('Hysteria2') + ' - ' + _('UDP')], - //['tunnel', _('Tunnel') + ' - ' + _('TCP/UDP')] + ['trusttunnel', _('TrustTunnel') + ' - ' + _('TCP/UDP')], + ['tunnel', _('Tunnel') + ' - ' + _('TCP/UDP')] ]; const ip_version = [ @@ -183,6 +188,7 @@ const outbound_type = [ ['hysteria2', _('Hysteria2') + ' - ' + _('UDP')], ['tuic', _('TUIC') + ' - ' + _('UDP')], ['masque', _('Masque') + ' - ' + _('UDP')], // https://blog.cloudflare.com/post-quantum-warp/ + ['trusttunnel', _('TrustTunnel') + ' - ' + _('TCP/UDP')], ['wireguard', _('WireGuard') + ' - ' + _('UDP')], ['ssh', _('SSH') + ' - ' + _('TCP')] ]; diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo/listeners.js b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo/listeners.js new file mode 100644 index 0000000000..4f4c436742 --- /dev/null +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo/listeners.js @@ -0,0 +1,1011 @@ +'use strict'; +'require baseclass'; +'require form'; +'require uci'; +'require ui'; + +'require fchomo as hm'; + +const CBIDummyCopyValue = hm.CopyValue.extend({ + __name__: 'CBI.DummyCopyValue', + + renderWidget(/* ... */) { + let node = hm.CopyValue.prototype.renderWidget.apply(this, arguments); + + node.firstChild.style.width = '30em'; + + return node; + }, + + write: function() {} +}); + +class VlessEncryption { + // origin: + // https://github.com/XTLS/Xray-core/pull/5067 + // server: + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L64 + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/server.go#L42 + // client: + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L12 + // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/client.go#L45 +/* +{ + "method": "mlkem768x25519plus", + "xormode": "native", + "ticket": "600s", + "rtt": "0rtt", + "paddings": [ // Optional + "100-111-1111", + "75-0-111", + "50-0-3333", + ... + ], + "keypairs": [ + { + "type": "vless-x25519", + "server": "cP5Oy9MOpTaBKKE17Pfd56mbb1CIfp5EMpyBYqr2EG8", + "client": "khEcQMT8j41xWmGYKpZtQ4vd8_9VWyFVmmCDIhRJ-Uk" + }, + { + "type": "vless-mlkem768", + "server": "UHPx3nf-FVxF95byAw0YG025aQNw9HxKej-MiG5AhTcdW_WFpHlTVYQU5NHmXP6tmljSnB2iPmSQ29fisGxEog", + "client": "h4sdZgCc5-ZefvQ8mZmReOWQdxYb0mwngMdl7pKhYEZZpGWHUPKAmxug87Bgj3GqSHs195QeVpxfrMLNB5Jm0Ge71Fc-A3aLpaS3C3pARbGQoquUDUEVDNwEWjQTvFpGTUV3Nddw_LlRmWN6Wqhguti9cpS6GhEmkBvBayFeHgZosuaQ1FMoAqIeQzSSSoguCZtGLUmdQjEs3zc5rwG1rNanbhtyI3QnooYvr3A0vggIkbmddjtjwYaVQdMAj9Moavc12EAUajOV91QA73RWVuhelbe7pLumsHiW-emIdBgVhEgDDYdGaLq1E8QjB0WbIfufnJp-CJa3Ieu9gmDASTlQBeEREeA9gfoZcTpYD8elhJIJxaPJKXchvUVkFhZarcivlKoqVuaFPzsJM7KQCBC8zfS0t_oiBka-uzg3_Hl153nMTDaCAbZULPZGE-p2EazI2eFBCDktdHtDffJNo7i7ZYSkWkqN9ysr2QZRvYG_PYCzcYSo34Gf5WNvHKuz0Ye3kFkckfuirCmzr3knw2azrSOmpTOX_RSlMlse7HgFYwxHPMJnzPS19ymiwKZPgrAMvCmAUZmsxZGDoKeusNEDGSSFhLcTQys20qGBGYasIgKYGjAKGjK7SCxSOCGBQSU496XBkXQEeOB7k9Sh8jdB0pQGAZw9Ntwvrts2DjIUcsQBv-XEGfnHQXoBmDgzwzYEWxeHd0oNbPIlz7CqvNseoKu6uPZl85xynum6aWd6BDDAtwobbqYkuMUfOUhXf_cH13kWSnuJ6QrOxah94JzAnda3tWRDQ3RajOOjk-OXhbOqi8QMJRFdA_C-xMwQalM_rTSTKOqyCcaNSTkVmMlmyOt90tptk7jKUizDmGhGbsSU8WMY5mhdZ3eUd5O6gQitiMHI1EqnlaRNsXnKFoJ5yHV82Wp1dhFONCG_dlpqunVJD5bFgpxtdFDD-KmXQTymAalFjxeVl_xdc5xd4XYCYmk5dhEiQBE0J_S3Z6x0tmFORpWG9lESK_OBRSul9oKZh9Vet-UZ8FSOVtNFwbeokRwWpFuFL1dL3UpJeININ2cgUfDNWQlwItkokiFf_Kdy12y2O_hqJtoTpNttNxTOiclDzKM1KHNOjYJgTgydcid3mmJl3eA6ezyrDAw1RLCHBucIvYRfwbkmpYMvnfAaA2DIiaTNaSxX8BUl92V49UVKWlQSp8ijfmmTRHrBMmxKjvBIgHqC6dSMhVUEOMzCKXAO3giCS3eZzdrNQGhhqTxpYYnFf6uLoKOIiaGY-ByI1YoIVXxX8aCTOOpesFvHjwOKBEoj4Hoxd3iFMUJQazR7P2drnfmS11kgipM7pSUgB7POKwxEF0NQCedM41wVIuoathAqD6N6qalwQ6iOKlZOBUwwMVAMRDJ3aomG37ZeLYhv6fB0-pUUJSN1q4knjtkLFIJSUrih9FZ0XnOll_aeEgOICqQkb4aOMrovjcJEWvgdjUqGPdyIGgkurfqBRHih3dukUcYxt6Y__4KLQ7acqMx0FOFv0ZxFRTCIRGj_GAlFWUi6fpuPKebXUnEn1PRE0iNXwUV_4jESWb0" + }, + ... + ] +} +*/ + constructor(payload) { + this.input = payload || ''; + try { + let content = JSON.parse(this.input.trim()); + Object.keys(content).forEach(key => this[key] = content[key]); + } catch {} + + this.method ||= hm.vless_encryption.methods[0][0]; + this.xormode ||= hm.vless_encryption.xormodes[0][0]; + this.ticket ||= hm.vless_encryption.tickets[0][0]; + this.rtt ||= hm.vless_encryption.rtts[0][0]; + this.paddings ||= []; + this.keypairs ||= []; + } + + setKey(key, value) { + this[key] = value; + + return this + } + + _toMihomo(payload, side) { + if (!['server', 'client'].includes(side)) + throw new Error('Unknown side: ' + side); // `Unknown side: '${side}'` + + let required = [ + payload.method, + payload.xormode, + side === 'server' ? payload.ticket : side === 'client' ? payload.rtt : null + ].join('.'); + + return required + + (hm.isEmpty(payload.paddings) ? '' : '.' + payload.paddings.join('.')) + // Optional + (hm.isEmpty(payload.keypairs) ? '' : '.' + payload.keypairs.map(e => e[side]).join('.')); // Required + } + + toString(format, side) { + format ||= 'json'; + + let payload = hm.removeBlankAttrs({ + method: this.method, + xormode: this.xormode, + ticket: this.ticket, + rtt: this.rtt, + paddings: this.paddings || [], + keypairs: this.keypairs || [] + }); + + if (format === 'json') + return JSON.stringify(payload); + else if (format === 'mihomo') + return this._toMihomo(payload, side); + else + throw new Error(`Unknown format: '${format}'`); + } +} + +function renderListeners(s, uciconfig, isClient) { + let o; + + s.tab('field_general', _('General fields')); + s.tab('field_vless_encryption', _('Vless Encryption fields')); + s.tab('field_tls', _('TLS fields')); + s.tab('field_transport', _('Transport fields')); + s.tab('field_multiplex', _('Multiplex fields')); + s.tab('field_listen', _('Listen fields')); + + /* General fields */ + o = s.taboption('field_general', form.Value, 'label', _('Label')); + o.load = hm.loadDefaultLabel; + o.validate = hm.validateUniqueValue; + o.modalonly = true; + + o = s.taboption('field_general', form.Flag, 'enabled', _('Enable')); + o.default = o.enabled; + o.editable = true; + + o = s.taboption('field_general', form.Flag, 'auto_firewall', _('Firewall'), + _('Auto configure firewall')); + o.default = o.enabled; + o.editable = true; + + o = s.taboption('field_general', form.ListValue, 'type', _('Type')); + o.default = hm.inbound_type[0][0]; + hm.inbound_type.forEach((res) => { + o.value.apply(o, res); + }) + + o = s.taboption('field_general', form.Value, 'listen', _('Listen address')); + o.datatype = 'ipaddr'; + o.placeholder = '::'; + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'port', _('Listen port') + ' / ' + _('Ports pool')); + o.datatype = 'or(port, portrange)'; + //o.placeholder = '1080,2079-2080,3080'; // @fw4 does not support port lists with commas + o.rmempty = false; + //o.validate = hm.validateCommonPort; // @fw4 does not support port lists with commas + + /* HTTP / SOCKS fields */ + /* hm.validateAuth */ + o = s.taboption('field_general', form.Value, 'username', _('Username')); + o.validate = hm.validateAuthUsername; + o.depends({type: /^(http|socks|mixed|mieru|trojan|anytls|hysteria2|trusttunnel)$/}); + o.modalonly = true; + + o = s.taboption('field_general', hm.GenValue, 'password', _('Password')); + o.password = true; + o.validate = hm.validateAuthPassword; + o.rmempty = false; + o.depends({type: /^(http|socks|mixed|mieru|trojan|anytls|hysteria2|trusttunnel)$/, username: /.+/}); + o.depends({type: /^(tuic)$/, uuid: /.+/}); + o.modalonly = true; + + /* Hysteria2 fields */ + o = s.taboption('field_general', form.Value, 'hysteria_up_mbps', _('Max upload speed'), + _('In Mbps.')); + o.datatype = 'uinteger'; + o.depends('type', 'hysteria2'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'hysteria_down_mbps', _('Max download speed'), + _('In Mbps.')); + o.datatype = 'uinteger'; + o.depends('type', 'hysteria2'); + o.modalonly = true; + + o = s.taboption('field_general', form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'), + _('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.')); + o.default = o.disabled; + o.depends({type: 'hysteria2', hysteria_up_mbps: '', hysteria_down_mbps: ''}); + o.modalonly = true; + + o = s.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type')); + o.value('', _('Disable')); + o.value('salamander', _('Salamander')); + o.depends('type', 'hysteria2'); + o.modalonly = true; + + o = s.taboption('field_general', hm.GenValue, 'hysteria_obfs_password', _('Obfuscate password'), + _('Enabling obfuscation will make the server incompatible with standard QUIC connections, losing the ability to masquerade with HTTP/3.')); + o.password = true; + o.rmempty = false; + o.depends('type', 'hysteria'); + o.depends({type: 'hysteria2', hysteria_obfs_type: /.+/}); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'hysteria_masquerade', _('Masquerade'), + _('HTTP3 server behavior when authentication fails.
A 404 page will be returned if empty.')); + o.placeholder = 'file:///var/www or http://127.0.0.1:8080' + o.depends('type', 'hysteria2'); + o.modalonly = true; + + /* Shadowsocks fields */ + o = s.taboption('field_general', form.ListValue, 'shadowsocks_chipher', _('Chipher')); + o.default = hm.shadowsocks_cipher_methods[1][0]; + hm.shadowsocks_cipher_methods.forEach((res) => { + o.value.apply(o, res); + }) + o.depends('type', 'shadowsocks'); + o.modalonly = true; + + o = s.taboption('field_general', hm.GenValue, 'shadowsocks_password', _('Password')); + o.password = true; + o.validate = function(section_id, value) { + const encmode = this.section.getOption('shadowsocks_chipher').formvalue(section_id); + return hm.validateShadowsocksPassword.call(this, encmode, section_id, value); + } + o.depends({type: 'shadowsocks', shadowsocks_chipher: /.+/}); + o.modalonly = true; + + /* Mieru fields */ + o = s.taboption('field_general', form.ListValue, 'mieru_transport', _('Transport')); + o.default = 'TCP'; + o.value('TCP'); + o.value('UDP'); + o.depends('type', 'mieru'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'mieru_traffic_pattern', _('Traffic pattern'), + _('A base64 string is used to fine-tune network behavior.
Please refer to the document.') + .format('https://github.com/enfein/mieru/blob/main/docs/traffic-pattern.md')); + o.depends('type', 'mieru'); + o.modalonly = true; + + /* Sudoku fields */ + const sudoku_keytypes = [ + ['sudoku-keypair', _('sudoku-keypair')], + ['uuid', _('UUID')] + ] + o = s.taboption('field_general', hm.GenValue, 'sudoku_key', _('Key'), + _('The ED25519 master public key or UUID generated by Sudoku.')); + o.hm_options = { + type: sudoku_keytypes[0][0], + callback: function(result) { + if (result.uuid) + return [ + [this.option, result.uuid], + ['sudoku_client_key', result.uuid] + ] + else + return [ + [this.option, result.public_key], + ['sudoku_client_key', result.private_key] + ] + } + } + o.renderWidget = function(section_id, option_index, cfgvalue) { + let node = form.Value.prototype.renderWidget.call(this, section_id, option_index, cfgvalue); + const cbid = this.cbid(section_id) + '._keytype_select'; + const selected = this.hm_options.type; + + let selectEl = E('select', { + id: cbid, + class: 'cbi-input-select', + style: 'width: 10em', + }); + + sudoku_keytypes.forEach(([k, v]) => { + selectEl.appendChild(E('option', { + 'value': k, + 'selected': (k === selected) ? '' : null + }, [ v ])); + }); + + node.appendChild(E('div', { 'class': 'control-group' }, [ + selectEl, + E('button', { + class: 'cbi-button cbi-button-add', + click: ui.createHandlerFn(this, () => { + this.hm_options.type = document.getElementById(cbid).value; + + return hm.handleGenKey.call(this, this.hm_options); + }) + }, [ _('Generate') ]) + ])); + + return node; + } + o.rmempty = false; + o.depends('type', 'sudoku'); + o.modalonly = true; + + o = s.taboption('field_general', hm.CopyValue, 'sudoku_client_key', _('Client key')); + o.depends('type', 'sudoku'); + o.modalonly = true; + + o = s.taboption('field_general', form.ListValue, 'sudoku_aead_method', _('Chipher')); + o.default = hm.sudoku_cipher_methods[0][0]; + hm.sudoku_cipher_methods.forEach((res) => { + o.value.apply(o, res); + }) + o.validate = function(section_id, value) { + const pure_downlink = this.section.getUIElement(section_id, 'sudoku_enable_pure_downlink')?.node.querySelector('input').checked; + + if (value === 'none' && pure_downlink === false) + return _('Expecting: %s').format(_('Chipher must be enabled if obfuscate downlink is disabled.')); + + return true; + } + o.depends('type', 'sudoku'); + o.modalonly = true; + + o = s.taboption('field_general', form.ListValue, 'sudoku_table_type', _('Obfuscate type')); + o.value('prefer_ascii', _('Obfuscated as ASCII data stream')); + o.value('prefer_entropy', _('Obfuscated as low-entropy data stream')); + o.depends('type', 'sudoku'); + o.modalonly = true; + + o = s.taboption('field_general', form.DynamicList, 'sudoku_custom_tables', _('Custom byte layout')); + o.renderWidget = function(/* ... */) { + let node = form.DynamicList.prototype.renderWidget.apply(this, arguments); + + (node.querySelector('.control-group') || node).appendChild(E('button', { + class: 'cbi-button cbi-button-positive', + title: _('Generate'), + click: ui.createHandlerFn(this, hm.handleGenKey, this.hm_options || this.option) + }, [ _('Generate') ])); + + return node; + } + o.validate = hm.validateSudokuCustomTable; + o.depends('sudoku_table_type', 'prefer_entropy'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'sudoku_padding_min', _('Minimum padding rate')); + o.datatype = 'and(uinteger, range(0, 100))'; + o.default = 1; + o.rmempty = false; + o.depends('type', 'sudoku'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'sudoku_padding_max', _('Maximum padding rate')); + o.datatype = 'and(uinteger, range(0, 100))'; + o.default = 15; + o.rmempty = false; + o.validate = function(section_id, value) { + const padding_min = this.section.getOption('sudoku_padding_min').formvalue(section_id); + + if (value < padding_min) + return _('Expecting: %s').format(_('Maximum padding rate must be greater than or equal to the minimum padding rate.')); + + return true; + } + o.depends('type', 'sudoku'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'sudoku_handshake_timeout', _('Handshake timeout'), + _('In seconds.')); + o.datatype = 'uinteger'; + o.placeholder = 5; + o.depends('type', 'sudoku'); + o.modalonly = true; + + o = s.taboption('field_general', form.Flag, 'sudoku_enable_pure_downlink', _('Enable obfuscate for downlink'), + _('false = bandwidth optimized downlink; true = pure Sudoku downlink.')); + o.default = o.enabled; + o.depends('type', 'sudoku'); + o.modalonly = true; + + o = s.taboption('field_general', form.Flag, 'sudoku_http_mask', _('HTTP mask')); + o.default = o.enabled; + o.depends('type', 'sudoku'); + o.modalonly = true; + + o = s.taboption('field_general', form.ListValue, 'sudoku_http_mask_mode', _('HTTP mask mode')); + o.default = 'legacy'; + o.value('legacy', _('Legacy')); + o.value('stream', _('split-stream') + ' - ' + _('CDN support')); + o.value('poll', _('poll') + ' - ' + _('CDN support')); + o.value('auto', _('Auto') + ' - ' + _('CDN support')); + o.value('ws', _('WebSocket') + ' - ' + _('CDN support')); + o.depends('sudoku_http_mask', '1'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'sudoku_path_root', _('HTTP root path')); + o.depends('sudoku_http_mask', '1'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'sudoku_fallback', _('Fallback')); + o.datatype = 'hostport'; + o.placeholder = '127.0.0.1:80'; + o.depends('sudoku_http_mask', '1'); + o.modalonly = true; + + /* Tuic fields */ + o = s.taboption('field_general', hm.GenValue, 'uuid', _('UUID')); + o.rmempty = false; + o.validate = hm.validateUUID; + o.depends('type', 'tuic'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'tuic_max_udp_relay_packet_size', _('Max UDP relay packet size')); + o.datatype = 'uinteger'; + o.default = '1500'; + o.depends('type', 'tuic'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'tuic_max_idle_time', _('Idle timeout'), + _('In seconds.')); + o.default = '15000'; + o.validate = hm.validateTimeDuration; + o.depends('type', 'tuic'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'tuic_authentication_timeout', _('Auth timeout'), + _('In seconds.')); + o.default = '1000'; + o.validate = hm.validateTimeDuration; + o.depends('type', 'tuic'); + o.modalonly = true; + + /* Trojan fields */ + o = s.taboption('field_general', form.Flag, 'trojan_ss_enabled', _('Shadowsocks encrypt')); + o.default = o.disabled; + o.depends('type', 'trojan'); + o.modalonly = true; + + o = s.taboption('field_general', form.ListValue, 'trojan_ss_chipher', _('Shadowsocks chipher')); + o.default = hm.trojan_cipher_methods[0][0]; + hm.trojan_cipher_methods.forEach((res) => { + o.value.apply(o, res); + }) + o.depends({type: 'trojan', trojan_ss_enabled: '1'}); + o.modalonly = true; + + o = s.taboption('field_general', hm.GenValue, 'trojan_ss_password', _('Shadowsocks password')); + o.password = true; + o.validate = function(section_id, value) { + const encmode = this.section.getOption('trojan_ss_chipher').formvalue(section_id); + return hm.validateShadowsocksPassword.call(this, encmode, section_id, value); + } + o.depends({type: 'trojan', trojan_ss_enabled: '1'}); + o.modalonly = true; + + /* AnyTLS fields */ + o = s.taboption('field_general', form.TextValue, 'anytls_padding_scheme', _('Padding scheme')); + o.depends('type', 'anytls'); + o.modalonly = true; + + /* VMess / VLESS fields */ + o = s.taboption('field_general', hm.GenValue, 'vmess_uuid', _('UUID')); + o.rmempty = false; + o.validate = hm.validateUUID; + o.depends({type: /^(vmess|vless)$/}); + o.modalonly = true; + + o = s.taboption('field_general', form.ListValue, 'vless_flow', _('Flow')); + o.default = hm.vless_flow[0][0]; + hm.vless_flow.forEach((res) => { + o.value.apply(o, res); + }) + o.depends('type', 'vless'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'vmess_alterid', _('Alter ID'), + _('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.')); + o.datatype = 'uinteger'; + o.placeholder = '0'; + o.depends('type', 'vmess'); + o.modalonly = true; + + /* TrustTunnel fields */ + + /* Tunnel fields */ + o = s.taboption('field_general', form.Value, 'tunnel_target', _('Target address')); + o.datatype = 'hostport'; + o.placeholder = 'target.com:53'; + o.depends('type', 'tunnel'); + o.modalonly = true; + + /* Plugin fields */ + o = s.taboption('field_general', form.ListValue, 'plugin', _('Plugin')); + o.value('', _('none')); + o.value('shadow-tls', _('shadow-tls')); + //o.value('kcp-tun', _('kcp-tun')); + o.depends('type', 'shadowsocks'); + o.modalonly = true; + + o = s.taboption('field_general', form.Value, 'plugin_opts_handshake_dest', _('Plugin: ') + _('Handshake target that supports TLS 1.3')); + o.datatype = 'hostport'; + o.placeholder = 'cloud.tencent.com:443'; + o.rmempty = false; + o.depends({plugin: 'shadow-tls'}); + o.modalonly = true; + + o = s.taboption('field_general', hm.GenValue, 'plugin_opts_thetlspassword', _('Plugin: ') + _('Password')); + o.password = true; + o.rmempty = false; + o.depends({plugin: 'shadow-tls'}); + o.modalonly = true; + + o = s.taboption('field_general', form.ListValue, 'plugin_opts_shadowtls_version', _('Plugin: ') + _('Version')); + o.value('1', _('v1')); + o.value('2', _('v2')); + o.value('3', _('v3')); + o.default = '3'; + o.depends({plugin: 'shadow-tls'}); + o.modalonly = true; + + /* Extra fields */ + if (isClient) { + o = s.taboption('field_general', hm.ListValue, 'rule', _('Sub rule'), + _('Name of the Sub rule used for inbound matching.')); + o.value.apply(o, ['', _('null')]); + o.load = L.bind(hm.loadSubRuleGroup, o, [['', _('null')]]); + o.editable = true; + + o = s.taboption('field_general', hm.ListValue, 'proxy', _('Proxy group'), + _('Name of the Proxy group as outbound.')); + o.default = hm.preset_outbound.direct[0][0]; + hm.preset_outbound.direct.forEach((res) => { + o.value.apply(o, res); + }) + o.load = L.bind(hm.loadProxyGroupLabel, o, hm.preset_outbound.direct); + o.editable = true; + } + + o = s.taboption('field_general', form.ListValue, 'congestion_controller', _('Congestion controller')); + o.default = hm.congestion_controller[0][0]; + hm.congestion_controller.forEach((res) => { + o.value.apply(o, res); + }) + o.depends({type: /^(tuic|trusttunnel)$/}); + o.modalonly = true; + + o = s.taboption('field_general', form.MultiValue, 'network', _('Network type')); + o.value('tcp', _('TCP')); + o.value('udp', _('UDP')); + o.rmempty = false; + o.depends({type: /^(trusttunnel|tunnel)$/}); + o.modalonly = true; + + o = s.taboption('field_general', form.Flag, 'udp', _('UDP')); + o.default = o.disabled; + o.depends({type: /^(socks|mixed|shadowsocks)$/}); + o.modalonly = true; + + /* Vless Encryption fields */ + o = s.taboption('field_general', form.Flag, 'vless_decryption', _('decryption')); + o.default = o.disabled; + o.depends('type', 'vless'); + o.modalonly = true; + + const initVlessEncryptionOption = function(o, key) { + o.load = function(section_id) { + return new VlessEncryption(uci.get(uciconfig, section_id, 'vless_encryption_hmpayload'))[key]; + } + o.onchange = function(ev, section_id, value) { + let UIEl = this.section.getUIElement(section_id, 'vless_encryption_hmpayload'); + let newpayload = new VlessEncryption(UIEl.getValue()).setKey(key, value); + + UIEl.setValue(newpayload.toString()); + + [ + ['server', '_vless_encryption_decryption'], + ['client', '_vless_encryption_encryption'] + ].forEach(([side, option]) => { + UIEl = this.section.getUIElement(section_id, option); + UIEl.setValue(newpayload.toString('mihomo', side)); + }); + } + o.write = function() {}; + } + + o = s.taboption('field_vless_encryption', form.Value, 'vless_encryption_hmpayload', _('Payload')); + o.readonly = true; + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', CBIDummyCopyValue, '_vless_encryption_decryption', _('decryption')); + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', CBIDummyCopyValue, '_vless_encryption_encryption', _('encryption')); + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_method', _('Encryption method')); + o.default = hm.vless_encryption.methods[0][0]; + hm.vless_encryption.methods.forEach((res) => { + o.value.apply(o, res); + }) + initVlessEncryptionOption(o, 'method'); + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', form.RichListValue, 'vless_encryption_xormode', _('XOR mode')); + o.default = hm.vless_encryption.xormodes[0][0]; + hm.vless_encryption.xormodes.forEach((res) => { + o.value.apply(o, res); + }) + initVlessEncryptionOption(o, 'xormode'); + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', hm.RichValue, 'vless_encryption_ticket', _('Server') +' '+ _('RTT')); + o.default = hm.vless_encryption.tickets[0][0]; + hm.vless_encryption.tickets.forEach((res) => { + o.value.apply(o, res); + }) + initVlessEncryptionOption(o, 'ticket'); + o.validate = function(section_id, value) { + if (!value) + return true; + + if (!value.match(/^(\d+-)?\d+s$/)) + return _('Expecting: %s').format('^(\\d+-)?\\d+s$'); + + return true; + } + o.rmempty = false; + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_rtt', _('Client') +' '+ _('RTT')); + o.default = hm.vless_encryption.rtts[0][0]; + hm.vless_encryption.rtts.forEach((res) => { + o.value.apply(o, res); + }) + initVlessEncryptionOption(o, 'rtt'); + o.rmempty = false; + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', hm.less_25_12 ? hm.DynamicList : form.DynamicList, 'vless_encryption_paddings', _('Paddings'), // @less_25_12 + _('The server and client can set different padding parameters.') + '
' + + _('In the order of one Padding-Length and one Padding-Interval, infinite concatenation.') + '
' + + _('The first padding must have a probability of 100% and at least 35 bytes.')); + hm.vless_encryption.paddings.forEach((res) => { + o.value.apply(o, res); + }) + initVlessEncryptionOption(o, 'paddings'); + o.validate = function(section_id, value) { + if (!value) + return true; + + if (!value.match(/^\d+(-\d+){2}$/)) + return _('Expecting: %s').format('^\\d+(-\\d+){2}$'); + + return true; + } + o.allowduplicates = true; + o.depends('vless_decryption', '1'); + o.modalonly = true; + + o = s.taboption('field_vless_encryption', hm.GenText, 'vless_encryption_keypairs', _('Keypairs')); + o.placeholder = '[\n {\n "type": "vless-x25519",\n "server": "cP5Oy9MOpTaBKKE17Pfd56mbb1CIfp5EMpyBYqr2EG8",\n "client": "khEcQMT8j41xWmGYKpZtQ4vd8_9VWyFVmmCDIhRJ-Uk"\n },\n {\n "type": "vless-mlkem768",\n "server": "UHPx3nf-FVxF95byAw0YG025aQNw9HxKej-MiG5AhTcdW_WFpHlTVYQU5NHmXP6tmljSnB2iPmSQ29fisGxEog",\n "client": "h4sdZgCc5-ZefvQ8mZmReOWQdxYb0mwngMdl7pKhYEZZpGWHUPKAmxug87Bgj3GqSHs195QeVpxfrMLNB5J..."\n },\n ...\n]'; + o.rows = 10; + o.hm_options = { + type: hm.vless_encryption.keypairs.types[0][0], + params: '', + callback: function(result) { + const section_id = this.section.section; + const key_type = this.hm_options.type; + + let keypair = {"type": key_type, "server": "", "client": ""}; + switch (key_type) { + case 'vless-x25519': + keypair.server = result.private_key; + keypair.client = result.password; + break; + case 'vless-mlkem768': + keypair.server = result.seed; + keypair.client = result.client; + break; + default: + break; + } + + let keypairs = []; + try { + keypairs = JSON.parse(this.formvalue(section_id).trim()); + } catch {} + if (!Array.isArray(keypairs)) + keypairs = []; + + keypairs.push(keypair); + + return [ + [this.option, JSON.stringify(keypairs, null, 2)] + ] + } + } + o.renderWidget = function(section_id, option_index, cfgvalue) { + let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue); + const cbid = this.cbid(section_id) + '._keytype_select'; + const selected = this.hm_options.type; + + let selectEl = E('select', { + id: cbid, + class: 'cbi-input-select', + style: 'width: 10em', + }); + + hm.vless_encryption.keypairs.types.forEach(([k, v]) => { + selectEl.appendChild(E('option', { + 'value': k, + 'selected': (k === selected) ? '' : null + }, [ v ])); + }); + + node.appendChild(E('div', { 'class': 'control-group' }, [ + selectEl, + E('button', { + class: 'cbi-button cbi-button-add', + click: ui.createHandlerFn(this, () => { + this.hm_options.type = document.getElementById(cbid).value; + + return hm.handleGenKey.call(this, this.hm_options); + }) + }, [ _('Generate') ]) + ])); + + return node; + } + o.load = function(section_id) { + return JSON.stringify(new VlessEncryption(uci.get(uciconfig, section_id, 'vless_encryption_hmpayload'))['keypairs'], null, 2); + } + o.validate = function(section_id, value) { + let result = hm.validateJson.call(this, section_id, value); + + if (result === true) { + let keypairs = JSON.parse(value.trim()); + + if (Array.isArray(keypairs) && keypairs.length >= 1) { + let UIEl = this.section.getUIElement(section_id, 'vless_encryption_hmpayload'); + let newpayload = new VlessEncryption(UIEl.getValue()).setKey('keypairs', keypairs); + + UIEl.setValue(newpayload.toString()); + + [ + ['server', '_vless_encryption_decryption'], + ['client', '_vless_encryption_encryption'] + ].forEach(([side, option]) => { + UIEl = this.section.getUIElement(section_id, option); + UIEl.setValue(newpayload.toString('mihomo', side)); + }); + } else + return _('Expecting: %s').format(_('least one keypair required')); + + return true; + } else + return result; + } + o.rmempty = false; + o.depends('vless_decryption', '1'); + o.modalonly = true; + + /* TLS fields */ + o = s.taboption('field_general', form.Flag, 'tls', _('TLS')); + o.default = o.disabled; + o.validate = function(section_id, value) { + const type = this.section.getOption('type').formvalue(section_id); + let tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input'); + let tls_alpn = this.section.getUIElement(section_id, 'tls_alpn'); + let tls_reality = this.section.getUIElement(section_id, 'tls_reality').node.querySelector('input'); + + // Force enabled + if (['trojan', 'anytls', 'tuic', 'hysteria2', 'trusttunnel'].includes(type)) { + tls.checked = true; + tls.disabled = true; + if (['tuic', 'hysteria2'].includes(type) && !`${tls_alpn.getValue()}`) + tls_alpn.setValue('h3'); + } else { + tls.removeAttribute('disabled'); + } + + // Force disabled + if (['trusttunnel'].includes(type)) { + tls_alpn.node.querySelector('input').disabled = true; + tls_alpn.setValue(''); + } else { + tls_alpn.node.querySelector('input').removeAttribute('disabled'); + } + if (!['vmess', 'vless', 'trojan'].includes(type)) { + tls_reality.checked = false; + tls_reality.disabled = true; + } else { + tls_reality.removeAttribute('disabled'); + } + + return true; + } + o.depends({type: /^(http|socks|mixed|vmess|vless|trojan|anytls|tuic|hysteria2|trusttunnel)$/}); + o.modalonly = true; + + o = s.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'), + _('List of supported application level protocols, in order of preference.')); + o.depends('tls', '1'); + o.modalonly = true; + + o = s.taboption('field_tls', form.Value, 'tls_cert_path', _('Certificate path'), + _('The %s public key, in PEM format.').format(_('Server'))); + o.value('/etc/fchomo/certs/server_publickey.pem'); + o.depends({tls: '1', tls_reality: '0'}); + o.rmempty = false; + o.modalonly = true; + + o = s.taboption('field_tls', form.Button, '_upload_cert', _('Upload certificate'), + _('Save your configuration before uploading files!')); + o.inputstyle = 'action'; + o.inputtitle = _('Upload...'); + o.depends({tls: '1', tls_cert_path: '/etc/fchomo/certs/server_publickey.pem'}); + o.onclick = L.bind(hm.uploadCertificate, o, _('certificate'), 'server_publickey'); + o.modalonly = true; + + o = s.taboption('field_tls', form.Value, 'tls_key_path', _('Key path'), + _('The %s private key, in PEM format.').format(_('Server'))); + o.value('/etc/fchomo/certs/server_privatekey.pem'); + o.rmempty = false; + o.depends({tls: '1', tls_cert_path: /.+/}); + o.modalonly = true; + + o = s.taboption('field_tls', form.Button, '_upload_key', _('Upload key'), + _('Save your configuration before uploading files!')); + o.inputstyle = 'action'; + o.inputtitle = _('Upload...'); + o.depends({tls: '1', tls_key_path: '/etc/fchomo/certs/server_privatekey.pem'}); + o.onclick = L.bind(hm.uploadCertificate, o, _('private key'), 'server_privatekey'); + o.modalonly = true; + + o = s.taboption('field_tls', form.ListValue, 'tls_client_auth_type', _('Client Auth type') + _(' (mTLS)')); + o.default = hm.tls_client_auth_types[0][0]; + hm.tls_client_auth_types.forEach((res) => { + o.value.apply(o, res); + }) + o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic|trusttunnel)$/}); + o.modalonly = true; + + o = s.taboption('field_tls', form.Value, 'tls_client_auth_cert_path', _('Client Auth Certificate path') + _(' (mTLS)'), + _('The %s public key, in PEM format.').format(_('Client'))); + o.value('/etc/fchomo/certs/client_publickey.pem'); + o.validate = function(/* ... */) { + return hm.validateMTLSClientAuth.call(this, 'tls_client_auth_type', ...arguments); + } + o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic|trusttunnel)$/}); + o.modalonly = true; + + o = s.taboption('field_tls', form.Button, '_upload_client_auth_cert', _('Upload certificate') + _(' (mTLS)'), + _('Save your configuration before uploading files!')); + o.inputstyle = 'action'; + o.inputtitle = _('Upload...'); + o.depends({tls: '1', tls_client_auth_cert_path: '/etc/fchomo/certs/client_publickey.pem'}); + o.onclick = L.bind(hm.uploadCertificate, o, _('certificate'), 'client_publickey'); + o.modalonly = true; + + o = s.taboption('field_tls', hm.GenText, 'tls_ech_key', _('ECH key')); + o.placeholder = '-----BEGIN ECH KEYS-----\nACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\nmadSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\ndC5jb20AAA==\n-----END ECH KEYS-----'; + o.hm_placeholder = 'outer-sni.any.domain'; + o.cols = 30; + o.rows = 2; + o.hm_options = { + type: 'ech-keypair', + params: '', + callback: function(result) { + return [ + [this.option, result.ech_key], + ['tls_ech_config', result.ech_cfg] + ] + } + } + o.renderWidget = function(section_id, option_index, cfgvalue) { + let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue); + const cbid = this.cbid(section_id) + '._outer_sni'; + + node.appendChild(E('div', { 'class': 'control-group' }, [ + E('input', { + id: cbid, + class: 'cbi-input-text', + style: 'width: 10em', + placeholder: this.hm_placeholder + }), + E('button', { + class: 'cbi-button cbi-button-add', + click: ui.createHandlerFn(this, () => { + this.hm_options.params = document.getElementById(cbid).value; + + return hm.handleGenKey.call(this, this.hm_options); + }) + }, [ _('Generate') ]) + ])); + + return node; + } + o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic|trusttunnel)$/}); + o.modalonly = true; + + o = s.taboption('field_tls', hm.CopyValue, 'tls_ech_config', _('ECH config'), + _('This ECH parameter needs to be added to the HTTPS record of the domain.')); + o.placeholder = 'AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA'; + o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic|trusttunnel)$/}); + o.modalonly = true; + + // uTLS fields + o = s.taboption('field_tls', form.Flag, 'tls_reality', _('REALITY')); + o.default = o.disabled; + o.depends('tls', '1'); + o.modalonly = true; + + o = s.taboption('field_tls', form.Value, 'tls_reality_dest', _('REALITY handshake server')); + o.datatype = 'hostport'; + o.placeholder = 'cloud.tencent.com:443'; + o.rmempty = false; + o.depends('tls_reality', '1'); + o.modalonly = true; + + o = s.taboption('field_tls', hm.GenValue, 'tls_reality_private_key', _('REALITY private key')); + o.hm_options = { + type: 'reality-keypair', + callback: function(result) { + return [ + [this.option, result.private_key], + ['tls_reality_public_key', result.public_key] + ] + } + } + o.password = true; + o.rmempty = false; + o.depends('tls_reality', '1'); + o.modalonly = true; + + o = s.taboption('field_tls', hm.CopyValue, 'tls_reality_public_key', _('REALITY public key')); + o.depends('tls_reality', '1'); + o.modalonly = true; + + o = s.taboption('field_tls', form.DynamicList, 'tls_reality_short_id', _('REALITY short ID')); + //o.value('', '""'); + o.rmempty = false; + o.depends('tls_reality', '1'); + o.modalonly = true; + + o = s.taboption('field_tls', form.DynamicList, 'tls_reality_server_names', _('REALITY certificate issued to')); + o.datatype = 'list(hostname)'; + o.placeholder = 'cloud.tencent.com'; + o.rmempty = false; + o.depends('tls_reality', '1'); + o.modalonly = true; + + /* Transport fields */ + o = s.taboption('field_general', form.Flag, 'transport_enabled', _('Transport')); + o.default = o.disabled; + o.depends({type: /^(vmess|vless|trojan)$/}); + o.modalonly = true; + + o = s.taboption('field_transport', form.ListValue, 'transport_type', _('Transport type')); + o.value('grpc', _('gRPC')); + o.value('ws', _('WebSocket')); + o.validate = function(section_id, value) { + const type = this.section.getOption('type').formvalue(section_id); + + switch (type) { + case 'vmess': + case 'vless': + if (!['http', 'h2', 'grpc', 'ws'].includes(value)) + return _('Expecting: only support %s.').format(_('HTTP') + + ' / ' + _('HTTPUpgrade') + + ' / ' + _('gRPC') + + ' / ' + _('WebSocket')); + break; + case 'trojan': + if (!['grpc', 'ws'].includes(value)) + return _('Expecting: only support %s.').format(_('gRPC') + + ' / ' + _('WebSocket')); + break; + default: + break; + } + + return true; + } + o.depends('transport_enabled', '1'); + o.modalonly = true; + + o = s.taboption('field_transport', form.Value, 'transport_path', _('Request path')); + o.placeholder = '/'; + o.default = '/'; + o.rmempty = false; + o.depends({transport_enabled: '1', transport_type: 'ws'}); + o.modalonly = true; + + o = s.taboption('field_transport', form.Value, 'transport_grpc_servicename', _('gRPC service name')); + o.placeholder = 'GunService'; + o.rmempty = false; + o.depends({transport_enabled: '1', transport_type: 'grpc'}); + o.modalonly = true; +} + +return baseclass.extend({ + /* Method */ + // render + renderListeners, +}); diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js index 2d55c9c3ee..0cf77708fc 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js @@ -795,7 +795,7 @@ function renderRules(s, uciconfig) { UIEl.setValue(rule.toString('json')); } o.write = function() {}; - //o.depends('SUB-RULE', ''); + //o.depends('SUB-RULE', ''); // work on subrules not rules o.editable = true; o = s.option(form.Flag, 'src', _('src'), diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/inbound.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/inbound.js new file mode 100644 index 0000000000..1dd4d770ae --- /dev/null +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/inbound.js @@ -0,0 +1,38 @@ +'use strict'; +'require form'; +'require uci'; +'require ui'; +'require view'; + +'require fchomo as hm'; +'require fchomo.listeners as lsnr' + +return view.extend({ + load() { + return Promise.all([ + uci.load('fchomo') + ]); + }, + + render(data) { + let m, s, o; + + m = new form.Map('fchomo', _('Edit inbound')); + + /* Inbound settings START */ + s = m.section(hm.GridSection, 'inbound', null); + s.addremove = true; + s.rowcolors = true; + s.sortable = true; + s.nodescriptions = true; + s.hm_modaltitle = [ _('Inbound'), _('Add a inbound') ]; + s.hm_prefmt = hm.glossary[s.sectiontype].prefmt; + s.hm_field = hm.glossary[s.sectiontype].field; + s.hm_lowcase_only = false; + + lsnr.renderListeners(s, data[0], true); + /* Inbound settings END */ + + return m.render(); + } +}); diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js index 54b6f262af..f0e1f6cb33 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js @@ -255,13 +255,13 @@ return view.extend({ /* hm.validateAuth */ so = ss.taboption('field_general', form.Value, 'username', _('Username')); so.validate = hm.validateAuthUsername; - so.depends({type: /^(http|socks5|mieru|ssh)$/}); + so.depends({type: /^(http|socks5|mieru|trusttunnel|ssh)$/}); so.modalonly = true; so = ss.taboption('field_general', form.Value, 'password', _('Password')); so.password = true; so.validate = hm.validateAuthPassword; - so.depends({type: /^(http|socks5|mieru|trojan|anytls|hysteria2|tuic|ssh)$/}); + so.depends({type: /^(http|socks5|mieru|trojan|anytls|hysteria2|tuic|trusttunnel|ssh)$/}); so.modalonly = true; so = ss.taboption('field_general', hm.TextValue, 'headers', _('HTTP header')); @@ -375,6 +375,12 @@ return view.extend({ so.depends('type', 'mieru'); so.modalonly = true; + so = ss.taboption('field_general', form.Value, 'mieru_traffic_pattern', _('Traffic pattern'), + _('A base64 string is used to fine-tune network behavior.
Please refer to the document.') + .format('https://github.com/enfein/mieru/blob/main/docs/traffic-pattern.md')); + so.depends('type', 'mieru'); + so.modalonly = true; + /* Sudoku fields */ so = ss.taboption('field_general', form.Value, 'sudoku_key', _('Key'), _('The ED25519 available private key or UUID provided by Sudoku server.')); @@ -448,18 +454,19 @@ return view.extend({ so.value('stream', _('split-stream') + ' - ' + _('CDN support')); so.value('poll', _('poll') + ' - ' + _('CDN support')); so.value('auto', _('Auto') + ' - ' + _('CDN support')); + so.value('ws', _('WebSocket') + ' - ' + _('CDN support')); so.depends('sudoku_http_mask', '1'); so.modalonly = true; so = ss.taboption('field_general', form.Flag, 'sudoku_http_mask_tls', _('HTTP mask: %s').format(_('TLS'))); so.default = so.disabled; - so.depends({sudoku_http_mask_mode: /^(stream|poll|auto)$/}); + so.depends({sudoku_http_mask_mode: /^(stream|poll|auto|ws)$/}); so.modalonly = true; so = ss.taboption('field_general', form.Value, 'sudoku_http_mask_host', _('HTTP mask: %s').format(_('Host/SNI override'))); so.datatype = 'or(hostname, hostport)'; so.placeholder = 'example.com[:443]'; - so.depends({sudoku_http_mask_mode: /^(stream|poll|auto)$/}); + so.depends({sudoku_http_mask_mode: /^(stream|poll|auto|ws)$/}); so.modalonly = true; so = ss.taboption('field_general', form.Value, 'sudoku_path_root', _('HTTP root path')); @@ -475,8 +482,10 @@ return view.extend({ so.validate = function(section_id, value) { const http_mask_mode = this.section.getOption('sudoku_http_mask_mode').formvalue(section_id); + if (http_mask_mode === 'ws' && value !== 'off') + return _('Expecting: %s').format(_('only applies when %s is not %s.').format(_('HTTP mask mode'), _('WebSocket'))); if (value === 'on' && !['stream', 'poll', 'auto'].includes(http_mask_mode)) - return _('Expecting: %s').format(_('only applies when %s is stream/poll/auto.').format(_('HTTP mask mode'))); + return _('Expecting: %s').format(_('only applies when %s is %s.').format(_('HTTP mask mode'), _('stream/poll/auto'))); return true; } @@ -512,15 +521,6 @@ return view.extend({ so.depends('type', 'tuic'); so.modalonly = true; - so = ss.taboption('field_general', form.ListValue, 'tuic_congestion_controller', _('Congestion controller'), - _('QUIC congestion controller.')); - so.default = hm.congestion_controller[0][0]; - hm.congestion_controller.forEach((res) => { - so.value.apply(so, res); - }) - so.depends('type', 'tuic'); - so.modalonly = true; - so = ss.taboption('field_general', form.ListValue, 'tuic_udp_relay_mode', _('UDP relay mode'), _('UDP packet relay mode.')); so.default = 'native'; @@ -714,12 +714,15 @@ return view.extend({ so.depends('masque_remote_dns_resolve', '1'); so.modalonly = true; - so = ss.taboption('field_general', form.ListValue, 'masque_congestion_controller', _('Congestion controller')); - so.default = hm.congestion_controller[0][0]; - hm.congestion_controller.forEach((res) => { - so.value.apply(so, res); - }) - so.depends('type', 'masque'); + /* TrustTunnel fields */ + so = ss.taboption('field_general', form.Flag, 'trusttunnel_health_check', _('Health check')); + so.default = so.enabled; + so.depends('type', 'trusttunnel'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Flag, 'trusttunnel_quic', _('QUIC')); + so.default = so.disabled; + so.depends('type', 'trusttunnel'); so.modalonly = true; /* WireGuard fields */ @@ -848,9 +851,17 @@ return view.extend({ so.modalonly = true; /* Extra fields */ + so = ss.taboption('field_general', form.ListValue, 'congestion_controller', _('Congestion controller')); + so.default = hm.congestion_controller[0][0]; + hm.congestion_controller.forEach((res) => { + so.value.apply(so, res); + }) + so.depends({type: /^(tuic|masque|trusttunnel)$/}); + so.modalonly = true; + so = ss.taboption('field_general', form.Flag, 'udp', _('UDP')); so.default = so.disabled; - so.depends({type: /^(direct|socks5|ss|mieru|vmess|vless|trojan|anytls|masque|wireguard)$/}); + so.depends({type: /^(direct|socks5|ss|mieru|vmess|vless|trojan|anytls|trusttunnel|masque|wireguard)$/}); so.modalonly = true; so = ss.taboption('field_general', form.Flag, 'uot', _('UoT'), @@ -941,7 +952,7 @@ return view.extend({ let tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input'); // Force enabled - if (['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic'].includes(type)) { + if (['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic', 'trusttunnel'].includes(type)) { tls.checked = true; tls.disabled = true; } else { @@ -950,7 +961,7 @@ return view.extend({ return true; } - so.depends({type: /^(http|socks5|vmess|vless|trojan|anytls|hysteria|hysteria2|tuic)$/}); + so.depends({type: /^(http|socks5|vmess|vless|trojan|anytls|hysteria|hysteria2|tuic|trusttunnel)$/}); so.modalonly = true; so = ss.taboption('field_tls', form.Flag, 'tls_disable_sni', _('Disable SNI'), @@ -961,7 +972,7 @@ return view.extend({ so = ss.taboption('field_tls', form.Value, 'tls_sni', _('TLS SNI'), _('Used to verify the hostname on the returned certificates.')); - so.depends({tls: '1', type: /^(http|vmess|vless|trojan|anytls|hysteria|hysteria2)$/}); + so.depends({tls: '1', type: /^(http|vmess|vless|trojan|anytls|hysteria|hysteria2|trusttunnel)$/}); so.depends({tls: '1', tls_disable_sni: '0', type: /^(tuic)$/}); so.modalonly = true; @@ -991,6 +1002,9 @@ return view.extend({ case 'anytls': def_alpn = ['h2', 'http/1.1']; break; + case 'trusttunnel': + def_alpn = ['h3', 'h2']; + break; default: def_alpn = []; } @@ -1000,7 +1014,7 @@ return view.extend({ return true; } - so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls|hysteria|hysteria2|tuic)$/}); + so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls|hysteria|hysteria2|tuic|trusttunnel)$/}); so.depends({type: 'ss', plugin: 'shadow-tls'}); so.modalonly = true; @@ -1022,7 +1036,7 @@ return view.extend({ '
' + _('This is DANGEROUS, your traffic is almost like PLAIN TEXT! Use at your own risk!')); so.default = so.disabled; - so.depends({tls: '1', type: /^(http|socks5|vmess|vless|trojan|anytls|hysteria|hysteria2|tuic)$/}); + so.depends({tls: '1', type: /^(http|socks5|vmess|vless|trojan|anytls|hysteria|hysteria2|tuic|trusttunnel)$/}); so.modalonly = true; so = ss.taboption('field_tls', form.Value, 'tls_cert_path', _('Certificate path') + _(' (mTLS)'), @@ -1076,7 +1090,7 @@ return view.extend({ hm.tls_client_fingerprints.forEach((res) => { so.value.apply(so, res); }) - so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls)$/}); + so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls|trusttunnel)$/}); so.depends({type: 'ss', plugin: /^(shadow-tls|restls)$/}); so.modalonly = true; diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js index e44e37938d..39641ba36c 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js @@ -6,125 +6,17 @@ 'require view'; 'require fchomo as hm'; - -const CBIDummyCopyValue = hm.CopyValue.extend({ - __name__: 'CBI.DummyCopyValue', - - renderWidget(/* ... */) { - let node = hm.CopyValue.prototype.renderWidget.apply(this, arguments); - - node.firstChild.style.width = '30em'; - - return node; - }, - - write: function() {} -}); - -class VlessEncryption { - // origin: - // https://github.com/XTLS/Xray-core/pull/5067 - // server: - // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L64 - // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/server.go#L42 - // client: - // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L12 - // https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/client.go#L45 -/* -{ - "method": "mlkem768x25519plus", - "xormode": "native", - "ticket": "600s", - "rtt": "0rtt", - "paddings": [ // Optional - "100-111-1111", - "75-0-111", - "50-0-3333", - ... - ], - "keypairs": [ - { - "type": "vless-x25519", - "server": "cP5Oy9MOpTaBKKE17Pfd56mbb1CIfp5EMpyBYqr2EG8", - "client": "khEcQMT8j41xWmGYKpZtQ4vd8_9VWyFVmmCDIhRJ-Uk" - }, - { - "type": "vless-mlkem768", - "server": "UHPx3nf-FVxF95byAw0YG025aQNw9HxKej-MiG5AhTcdW_WFpHlTVYQU5NHmXP6tmljSnB2iPmSQ29fisGxEog", - "client": "h4sdZgCc5-ZefvQ8mZmReOWQdxYb0mwngMdl7pKhYEZZpGWHUPKAmxug87Bgj3GqSHs195QeVpxfrMLNB5Jm0Ge71Fc-A3aLpaS3C3pARbGQoquUDUEVDNwEWjQTvFpGTUV3Nddw_LlRmWN6Wqhguti9cpS6GhEmkBvBayFeHgZosuaQ1FMoAqIeQzSSSoguCZtGLUmdQjEs3zc5rwG1rNanbhtyI3QnooYvr3A0vggIkbmddjtjwYaVQdMAj9Moavc12EAUajOV91QA73RWVuhelbe7pLumsHiW-emIdBgVhEgDDYdGaLq1E8QjB0WbIfufnJp-CJa3Ieu9gmDASTlQBeEREeA9gfoZcTpYD8elhJIJxaPJKXchvUVkFhZarcivlKoqVuaFPzsJM7KQCBC8zfS0t_oiBka-uzg3_Hl153nMTDaCAbZULPZGE-p2EazI2eFBCDktdHtDffJNo7i7ZYSkWkqN9ysr2QZRvYG_PYCzcYSo34Gf5WNvHKuz0Ye3kFkckfuirCmzr3knw2azrSOmpTOX_RSlMlse7HgFYwxHPMJnzPS19ymiwKZPgrAMvCmAUZmsxZGDoKeusNEDGSSFhLcTQys20qGBGYasIgKYGjAKGjK7SCxSOCGBQSU496XBkXQEeOB7k9Sh8jdB0pQGAZw9Ntwvrts2DjIUcsQBv-XEGfnHQXoBmDgzwzYEWxeHd0oNbPIlz7CqvNseoKu6uPZl85xynum6aWd6BDDAtwobbqYkuMUfOUhXf_cH13kWSnuJ6QrOxah94JzAnda3tWRDQ3RajOOjk-OXhbOqi8QMJRFdA_C-xMwQalM_rTSTKOqyCcaNSTkVmMlmyOt90tptk7jKUizDmGhGbsSU8WMY5mhdZ3eUd5O6gQitiMHI1EqnlaRNsXnKFoJ5yHV82Wp1dhFONCG_dlpqunVJD5bFgpxtdFDD-KmXQTymAalFjxeVl_xdc5xd4XYCYmk5dhEiQBE0J_S3Z6x0tmFORpWG9lESK_OBRSul9oKZh9Vet-UZ8FSOVtNFwbeokRwWpFuFL1dL3UpJeININ2cgUfDNWQlwItkokiFf_Kdy12y2O_hqJtoTpNttNxTOiclDzKM1KHNOjYJgTgydcid3mmJl3eA6ezyrDAw1RLCHBucIvYRfwbkmpYMvnfAaA2DIiaTNaSxX8BUl92V49UVKWlQSp8ijfmmTRHrBMmxKjvBIgHqC6dSMhVUEOMzCKXAO3giCS3eZzdrNQGhhqTxpYYnFf6uLoKOIiaGY-ByI1YoIVXxX8aCTOOpesFvHjwOKBEoj4Hoxd3iFMUJQazR7P2drnfmS11kgipM7pSUgB7POKwxEF0NQCedM41wVIuoathAqD6N6qalwQ6iOKlZOBUwwMVAMRDJ3aomG37ZeLYhv6fB0-pUUJSN1q4knjtkLFIJSUrih9FZ0XnOll_aeEgOICqQkb4aOMrovjcJEWvgdjUqGPdyIGgkurfqBRHih3dukUcYxt6Y__4KLQ7acqMx0FOFv0ZxFRTCIRGj_GAlFWUi6fpuPKebXUnEn1PRE0iNXwUV_4jESWb0" - }, - ... - ] -} -*/ - constructor(payload) { - this.input = payload || ''; - try { - let content = JSON.parse(this.input.trim()); - Object.keys(content).forEach(key => this[key] = content[key]); - } catch {} - - this.method ||= hm.vless_encryption.methods[0][0]; - this.xormode ||= hm.vless_encryption.xormodes[0][0]; - this.ticket ||= hm.vless_encryption.tickets[0][0]; - this.rtt ||= hm.vless_encryption.rtts[0][0]; - this.paddings ||= []; - this.keypairs ||= []; - } - - setKey(key, value) { - this[key] = value; - - return this - } - - _toMihomo(payload, side) { - if (!['server', 'client'].includes(side)) - throw new Error('Unknown side: ' + side); // `Unknown side: '${side}'` - - let required = [ - payload.method, - payload.xormode, - side === 'server' ? payload.ticket : side === 'client' ? payload.rtt : null - ].join('.'); - - return required + - (hm.isEmpty(payload.paddings) ? '' : '.' + payload.paddings.join('.')) + // Optional - (hm.isEmpty(payload.keypairs) ? '' : '.' + payload.keypairs.map(e => e[side]).join('.')); // Required - } - - toString(format, side) { - format ||= 'json'; - - let payload = hm.removeBlankAttrs({ - method: this.method, - xormode: this.xormode, - ticket: this.ticket, - rtt: this.rtt, - paddings: this.paddings || [], - keypairs: this.keypairs || [] - }); - - if (format === 'json') - return JSON.stringify(payload); - else if (format === 'mihomo') - return this._toMihomo(payload, side); - else - throw new Error(`Unknown format: '${format}'`); - } -} +'require fchomo.listeners as lsnr' return view.extend({ load() { return Promise.all([ - uci.load('fchomo'), - hm.getFeatures() + uci.load('fchomo') ]); }, render(data) { const dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo'); - const features = data[1]; let m, s, o; @@ -168,845 +60,7 @@ return view.extend({ s.hm_field = hm.glossary[s.sectiontype].field; s.hm_lowcase_only = false; - s.tab('field_general', _('General fields')); - s.tab('field_vless_encryption', _('Vless Encryption fields')); - s.tab('field_tls', _('TLS fields')); - s.tab('field_transport', _('Transport fields')); - s.tab('field_multiplex', _('Multiplex fields')); - s.tab('field_listen', _('Listen fields')); - - /* General fields */ - o = s.taboption('field_general', form.Value, 'label', _('Label')); - o.load = hm.loadDefaultLabel; - o.validate = hm.validateUniqueValue; - o.modalonly = true; - - o = s.taboption('field_general', form.Flag, 'enabled', _('Enable')); - o.default = o.enabled; - o.editable = true; - - o = s.taboption('field_general', form.Flag, 'auto_firewall', _('Firewall'), - _('Auto configure firewall')); - o.default = o.enabled; - o.editable = true; - - o = s.taboption('field_general', form.ListValue, 'type', _('Type')); - o.default = hm.inbound_type[0][0]; - hm.inbound_type.forEach((res) => { - o.value.apply(o, res); - }) - - o = s.taboption('field_general', form.Value, 'listen', _('Listen address')); - o.datatype = 'ipaddr'; - o.placeholder = '::'; - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'port', _('Listen port') + ' / ' + _('Ports pool')); - o.datatype = 'or(port, portrange)'; - //o.placeholder = '1080,2079-2080,3080'; // @fw4 does not support port lists with commas - o.rmempty = false; - //o.validate = hm.validateCommonPort; // @fw4 does not support port lists with commas - - // @dev: Features under development - // @rule - // @proxy - - /* HTTP / SOCKS fields */ - /* hm.validateAuth */ - o = s.taboption('field_general', form.Value, 'username', _('Username')); - o.validate = hm.validateAuthUsername; - o.depends({type: /^(http|socks|mixed|mieru|trojan|anytls|hysteria2)$/}); - o.modalonly = true; - - o = s.taboption('field_general', hm.GenValue, 'password', _('Password')); - o.password = true; - o.validate = hm.validateAuthPassword; - o.rmempty = false; - o.depends({type: /^(http|socks|mixed|mieru|trojan|anytls|hysteria2)$/, username: /.+/}); - o.depends({type: /^(tuic)$/, uuid: /.+/}); - o.modalonly = true; - - /* Hysteria2 fields */ - o = s.taboption('field_general', form.Value, 'hysteria_up_mbps', _('Max upload speed'), - _('In Mbps.')); - o.datatype = 'uinteger'; - o.depends('type', 'hysteria2'); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'hysteria_down_mbps', _('Max download speed'), - _('In Mbps.')); - o.datatype = 'uinteger'; - o.depends('type', 'hysteria2'); - o.modalonly = true; - - o = s.taboption('field_general', form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'), - _('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.')); - o.default = o.disabled; - o.depends({type: 'hysteria2', hysteria_up_mbps: '', hysteria_down_mbps: ''}); - o.modalonly = true; - - o = s.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type')); - o.value('', _('Disable')); - o.value('salamander', _('Salamander')); - o.depends('type', 'hysteria2'); - o.modalonly = true; - - o = s.taboption('field_general', hm.GenValue, 'hysteria_obfs_password', _('Obfuscate password'), - _('Enabling obfuscation will make the server incompatible with standard QUIC connections, losing the ability to masquerade with HTTP/3.')); - o.password = true; - o.rmempty = false; - o.depends('type', 'hysteria'); - o.depends({type: 'hysteria2', hysteria_obfs_type: /.+/}); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'hysteria_masquerade', _('Masquerade'), - _('HTTP3 server behavior when authentication fails.
A 404 page will be returned if empty.')); - o.placeholder = 'file:///var/www or http://127.0.0.1:8080' - o.depends('type', 'hysteria2'); - o.modalonly = true; - - /* Shadowsocks fields */ - o = s.taboption('field_general', form.ListValue, 'shadowsocks_chipher', _('Chipher')); - o.default = hm.shadowsocks_cipher_methods[1][0]; - hm.shadowsocks_cipher_methods.forEach((res) => { - o.value.apply(o, res); - }) - o.depends('type', 'shadowsocks'); - o.modalonly = true; - - o = s.taboption('field_general', hm.GenValue, 'shadowsocks_password', _('Password')); - o.password = true; - o.validate = function(section_id, value) { - const encmode = this.section.getOption('shadowsocks_chipher').formvalue(section_id); - return hm.validateShadowsocksPassword.call(this, encmode, section_id, value); - } - o.depends({type: 'shadowsocks', shadowsocks_chipher: /.+/}); - o.modalonly = true; - - /* Mieru fields */ - o = s.taboption('field_general', form.ListValue, 'mieru_transport', _('Transport')); - o.default = 'TCP'; - o.value('TCP'); - o.value('UDP'); - o.depends('type', 'mieru'); - o.modalonly = true; - - /* Sudoku fields */ - const sudoku_keytypes = [ - ['sudoku-keypair', _('sudoku-keypair')], - ['uuid', _('UUID')] - ] - o = s.taboption('field_general', hm.GenValue, 'sudoku_key', _('Key'), - _('The ED25519 master public key or UUID generated by Sudoku.')); - o.hm_options = { - type: sudoku_keytypes[0][0], - callback: function(result) { - if (result.uuid) - return [ - [this.option, result.uuid], - ['sudoku_client_key', result.uuid] - ] - else - return [ - [this.option, result.public_key], - ['sudoku_client_key', result.private_key] - ] - } - } - o.renderWidget = function(section_id, option_index, cfgvalue) { - let node = form.Value.prototype.renderWidget.call(this, section_id, option_index, cfgvalue); - const cbid = this.cbid(section_id) + '._keytype_select'; - const selected = this.hm_options.type; - - let selectEl = E('select', { - id: cbid, - class: 'cbi-input-select', - style: 'width: 10em', - }); - - sudoku_keytypes.forEach(([k, v]) => { - selectEl.appendChild(E('option', { - 'value': k, - 'selected': (k === selected) ? '' : null - }, [ v ])); - }); - - node.appendChild(E('div', { 'class': 'control-group' }, [ - selectEl, - E('button', { - class: 'cbi-button cbi-button-add', - click: ui.createHandlerFn(this, () => { - this.hm_options.type = document.getElementById(cbid).value; - - return hm.handleGenKey.call(this, this.hm_options); - }) - }, [ _('Generate') ]) - ])); - - return node; - } - o.rmempty = false; - o.depends('type', 'sudoku'); - o.modalonly = true; - - o = s.taboption('field_general', hm.CopyValue, 'sudoku_client_key', _('Client key')); - o.depends('type', 'sudoku'); - o.modalonly = true; - - o = s.taboption('field_general', form.ListValue, 'sudoku_aead_method', _('Chipher')); - o.default = hm.sudoku_cipher_methods[0][0]; - hm.sudoku_cipher_methods.forEach((res) => { - o.value.apply(o, res); - }) - o.validate = function(section_id, value) { - const pure_downlink = this.section.getUIElement(section_id, 'sudoku_enable_pure_downlink')?.node.querySelector('input').checked; - - if (value === 'none' && pure_downlink === false) - return _('Expecting: %s').format(_('Chipher must be enabled if obfuscate downlink is disabled.')); - - return true; - } - o.depends('type', 'sudoku'); - o.modalonly = true; - - o = s.taboption('field_general', form.ListValue, 'sudoku_table_type', _('Obfuscate type')); - o.value('prefer_ascii', _('Obfuscated as ASCII data stream')); - o.value('prefer_entropy', _('Obfuscated as low-entropy data stream')); - o.depends('type', 'sudoku'); - o.modalonly = true; - - o = s.taboption('field_general', form.DynamicList, 'sudoku_custom_tables', _('Custom byte layout')); - o.renderWidget = function(/* ... */) { - let node = form.DynamicList.prototype.renderWidget.apply(this, arguments); - - (node.querySelector('.control-group') || node).appendChild(E('button', { - class: 'cbi-button cbi-button-positive', - title: _('Generate'), - click: ui.createHandlerFn(this, hm.handleGenKey, this.hm_options || this.option) - }, [ _('Generate') ])); - - return node; - } - o.validate = hm.validateSudokuCustomTable; - o.depends('sudoku_table_type', 'prefer_entropy'); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'sudoku_padding_min', _('Minimum padding rate')); - o.datatype = 'and(uinteger, range(0, 100))'; - o.default = 1; - o.rmempty = false; - o.depends('type', 'sudoku'); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'sudoku_padding_max', _('Maximum padding rate')); - o.datatype = 'and(uinteger, range(0, 100))'; - o.default = 15; - o.rmempty = false; - o.validate = function(section_id, value) { - const padding_min = this.section.getOption('sudoku_padding_min').formvalue(section_id); - - if (value < padding_min) - return _('Expecting: %s').format(_('Maximum padding rate must be greater than or equal to the minimum padding rate.')); - - return true; - } - o.depends('type', 'sudoku'); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'sudoku_handshake_timeout', _('Handshake timeout'), - _('In seconds.')); - o.datatype = 'uinteger'; - o.placeholder = 5; - o.depends('type', 'sudoku'); - o.modalonly = true; - - o = s.taboption('field_general', form.Flag, 'sudoku_enable_pure_downlink', _('Enable obfuscate for downlink'), - _('false = bandwidth optimized downlink; true = pure Sudoku downlink.')); - o.default = o.enabled; - o.depends('type', 'sudoku'); - o.modalonly = true; - - o = s.taboption('field_general', form.Flag, 'sudoku_http_mask', _('HTTP mask')); - o.default = o.enabled; - o.depends('type', 'sudoku'); - o.modalonly = true; - - o = s.taboption('field_general', form.ListValue, 'sudoku_http_mask_mode', _('HTTP mask mode')); - o.default = 'legacy'; - o.value('legacy', _('Legacy')); - o.value('stream', _('split-stream') + ' - ' + _('CDN support')); - o.value('poll', _('poll') + ' - ' + _('CDN support')); - o.value('auto', _('Auto') + ' - ' + _('CDN support')); - o.depends('sudoku_http_mask', '1'); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'sudoku_path_root', _('HTTP root path')); - o.depends('sudoku_http_mask', '1'); - o.modalonly = true; - - /* Tuic fields */ - o = s.taboption('field_general', hm.GenValue, 'uuid', _('UUID')); - o.rmempty = false; - o.validate = hm.validateUUID; - o.depends('type', 'tuic'); - o.modalonly = true; - - o = s.taboption('field_general', form.ListValue, 'tuic_congestion_controller', _('Congestion controller'), - _('QUIC congestion controller.')); - o.default = hm.congestion_controller[0][0]; - hm.congestion_controller.forEach((res) => { - o.value.apply(o, res); - }) - o.depends('type', 'tuic'); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'tuic_max_udp_relay_packet_size', _('Max UDP relay packet size')); - o.datatype = 'uinteger'; - o.default = '1500'; - o.depends('type', 'tuic'); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'tuic_max_idle_time', _('Idle timeout'), - _('In seconds.')); - o.default = '15000'; - o.validate = hm.validateTimeDuration; - o.depends('type', 'tuic'); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'tuic_authentication_timeout', _('Auth timeout'), - _('In seconds.')); - o.default = '1000'; - o.validate = hm.validateTimeDuration; - o.depends('type', 'tuic'); - o.modalonly = true; - - /* Trojan fields */ - o = s.taboption('field_general', form.Flag, 'trojan_ss_enabled', _('Shadowsocks encrypt')); - o.default = o.disabled; - o.depends('type', 'trojan'); - o.modalonly = true; - - o = s.taboption('field_general', form.ListValue, 'trojan_ss_chipher', _('Shadowsocks chipher')); - o.default = hm.trojan_cipher_methods[0][0]; - hm.trojan_cipher_methods.forEach((res) => { - o.value.apply(o, res); - }) - o.depends({type: 'trojan', trojan_ss_enabled: '1'}); - o.modalonly = true; - - o = s.taboption('field_general', hm.GenValue, 'trojan_ss_password', _('Shadowsocks password')); - o.password = true; - o.validate = function(section_id, value) { - const encmode = this.section.getOption('trojan_ss_chipher').formvalue(section_id); - return hm.validateShadowsocksPassword.call(this, encmode, section_id, value); - } - o.depends({type: 'trojan', trojan_ss_enabled: '1'}); - o.modalonly = true; - - /* AnyTLS fields */ - o = s.taboption('field_general', form.TextValue, 'anytls_padding_scheme', _('Padding scheme')); - o.depends('type', 'anytls'); - o.modalonly = true; - - /* VMess / VLESS fields */ - o = s.taboption('field_general', hm.GenValue, 'vmess_uuid', _('UUID')); - o.rmempty = false; - o.validate = hm.validateUUID; - o.depends({type: /^(vmess|vless)$/}); - o.modalonly = true; - - o = s.taboption('field_general', form.ListValue, 'vless_flow', _('Flow')); - o.default = hm.vless_flow[0][0]; - hm.vless_flow.forEach((res) => { - o.value.apply(o, res); - }) - o.depends('type', 'vless'); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'vmess_alterid', _('Alter ID'), - _('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.')); - o.datatype = 'uinteger'; - o.placeholder = '0'; - o.depends('type', 'vmess'); - o.modalonly = true; - - /* Plugin fields */ - o = s.taboption('field_general', form.ListValue, 'plugin', _('Plugin')); - o.value('', _('none')); - o.value('shadow-tls', _('shadow-tls')); - //o.value('kcp-tun', _('kcp-tun')); - o.depends('type', 'shadowsocks'); - o.modalonly = true; - - o = s.taboption('field_general', form.Value, 'plugin_opts_handshake_dest', _('Plugin: ') + _('Handshake target that supports TLS 1.3')); - o.datatype = 'hostport'; - o.placeholder = 'cloud.tencent.com:443'; - o.rmempty = false; - o.depends({plugin: 'shadow-tls'}); - o.modalonly = true; - - o = s.taboption('field_general', hm.GenValue, 'plugin_opts_thetlspassword', _('Plugin: ') + _('Password')); - o.password = true; - o.rmempty = false; - o.depends({plugin: 'shadow-tls'}); - o.modalonly = true; - - o = s.taboption('field_general', form.ListValue, 'plugin_opts_shadowtls_version', _('Plugin: ') + _('Version')); - o.value('1', _('v1')); - o.value('2', _('v2')); - o.value('3', _('v3')); - o.default = '3'; - o.depends({plugin: 'shadow-tls'}); - o.modalonly = true; - - /* Extra fields */ - o = s.taboption('field_general', form.Flag, 'udp', _('UDP')); - o.default = o.disabled; - o.depends({type: /^(socks|mixed|shadowsocks)$/}); - o.modalonly = true; - - /* Vless Encryption fields */ - o = s.taboption('field_general', form.Flag, 'vless_decryption', _('decryption')); - o.default = o.disabled; - o.depends('type', 'vless'); - o.modalonly = true; - - const initVlessEncryptionOption = function(o, key) { - o.load = function(section_id) { - return new VlessEncryption(uci.get(data[0], section_id, 'vless_encryption_hmpayload'))[key]; - } - o.onchange = function(ev, section_id, value) { - let UIEl = this.section.getUIElement(section_id, 'vless_encryption_hmpayload'); - let newpayload = new VlessEncryption(UIEl.getValue()).setKey(key, value); - - UIEl.setValue(newpayload.toString()); - - [ - ['server', '_vless_encryption_decryption'], - ['client', '_vless_encryption_encryption'] - ].forEach(([side, option]) => { - UIEl = this.section.getUIElement(section_id, option); - UIEl.setValue(newpayload.toString('mihomo', side)); - }); - } - o.write = function() {}; - } - - o = s.taboption('field_vless_encryption', form.Value, 'vless_encryption_hmpayload', _('Payload')); - o.readonly = true; - o.depends('vless_decryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', CBIDummyCopyValue, '_vless_encryption_decryption', _('decryption')); - o.depends('vless_decryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', CBIDummyCopyValue, '_vless_encryption_encryption', _('encryption')); - o.depends('vless_decryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_method', _('Encryption method')); - o.default = hm.vless_encryption.methods[0][0]; - hm.vless_encryption.methods.forEach((res) => { - o.value.apply(o, res); - }) - initVlessEncryptionOption(o, 'method'); - o.depends('vless_decryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', form.RichListValue, 'vless_encryption_xormode', _('XOR mode')); - o.default = hm.vless_encryption.xormodes[0][0]; - hm.vless_encryption.xormodes.forEach((res) => { - o.value.apply(o, res); - }) - initVlessEncryptionOption(o, 'xormode'); - o.depends('vless_decryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', hm.RichValue, 'vless_encryption_ticket', _('Server') +' '+ _('RTT')); - o.default = hm.vless_encryption.tickets[0][0]; - hm.vless_encryption.tickets.forEach((res) => { - o.value.apply(o, res); - }) - initVlessEncryptionOption(o, 'ticket'); - o.validate = function(section_id, value) { - if (!value) - return true; - - if (!value.match(/^(\d+-)?\d+s$/)) - return _('Expecting: %s').format('^(\\d+-)?\\d+s$'); - - return true; - } - o.rmempty = false; - o.depends('vless_decryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_rtt', _('Client') +' '+ _('RTT')); - o.default = hm.vless_encryption.rtts[0][0]; - hm.vless_encryption.rtts.forEach((res) => { - o.value.apply(o, res); - }) - initVlessEncryptionOption(o, 'rtt'); - o.rmempty = false; - o.depends('vless_decryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', hm.less_25_12 ? hm.DynamicList : form.DynamicList, 'vless_encryption_paddings', _('Paddings'), // @less_25_12 - _('The server and client can set different padding parameters.') + '
' + - _('In the order of one Padding-Length and one Padding-Interval, infinite concatenation.') + '
' + - _('The first padding must have a probability of 100% and at least 35 bytes.')); - hm.vless_encryption.paddings.forEach((res) => { - o.value.apply(o, res); - }) - initVlessEncryptionOption(o, 'paddings'); - o.validate = function(section_id, value) { - if (!value) - return true; - - if (!value.match(/^\d+(-\d+){2}$/)) - return _('Expecting: %s').format('^\\d+(-\\d+){2}$'); - - return true; - } - o.allowduplicates = true; - o.depends('vless_decryption', '1'); - o.modalonly = true; - - o = s.taboption('field_vless_encryption', hm.GenText, 'vless_encryption_keypairs', _('Keypairs')); - o.placeholder = '[\n {\n "type": "vless-x25519",\n "server": "cP5Oy9MOpTaBKKE17Pfd56mbb1CIfp5EMpyBYqr2EG8",\n "client": "khEcQMT8j41xWmGYKpZtQ4vd8_9VWyFVmmCDIhRJ-Uk"\n },\n {\n "type": "vless-mlkem768",\n "server": "UHPx3nf-FVxF95byAw0YG025aQNw9HxKej-MiG5AhTcdW_WFpHlTVYQU5NHmXP6tmljSnB2iPmSQ29fisGxEog",\n "client": "h4sdZgCc5-ZefvQ8mZmReOWQdxYb0mwngMdl7pKhYEZZpGWHUPKAmxug87Bgj3GqSHs195QeVpxfrMLNB5J..."\n },\n ...\n]'; - o.rows = 10; - o.hm_options = { - type: hm.vless_encryption.keypairs.types[0][0], - params: '', - callback: function(result) { - const section_id = this.section.section; - const key_type = this.hm_options.type; - - let keypair = {"type": key_type, "server": "", "client": ""}; - switch (key_type) { - case 'vless-x25519': - keypair.server = result.private_key; - keypair.client = result.password; - break; - case 'vless-mlkem768': - keypair.server = result.seed; - keypair.client = result.client; - break; - default: - break; - } - - let keypairs = []; - try { - keypairs = JSON.parse(this.formvalue(section_id).trim()); - } catch {} - if (!Array.isArray(keypairs)) - keypairs = []; - - keypairs.push(keypair); - - return [ - [this.option, JSON.stringify(keypairs, null, 2)] - ] - } - } - o.renderWidget = function(section_id, option_index, cfgvalue) { - let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue); - const cbid = this.cbid(section_id) + '._keytype_select'; - const selected = this.hm_options.type; - - let selectEl = E('select', { - id: cbid, - class: 'cbi-input-select', - style: 'width: 10em', - }); - - hm.vless_encryption.keypairs.types.forEach(([k, v]) => { - selectEl.appendChild(E('option', { - 'value': k, - 'selected': (k === selected) ? '' : null - }, [ v ])); - }); - - node.appendChild(E('div', { 'class': 'control-group' }, [ - selectEl, - E('button', { - class: 'cbi-button cbi-button-add', - click: ui.createHandlerFn(this, () => { - this.hm_options.type = document.getElementById(cbid).value; - - return hm.handleGenKey.call(this, this.hm_options); - }) - }, [ _('Generate') ]) - ])); - - return node; - } - o.load = function(section_id) { - return JSON.stringify(new VlessEncryption(uci.get(data[0], section_id, 'vless_encryption_hmpayload'))['keypairs'], null, 2); - } - o.validate = function(section_id, value) { - let result = hm.validateJson.call(this, section_id, value); - - if (result === true) { - let keypairs = JSON.parse(value.trim()); - - if (Array.isArray(keypairs) && keypairs.length >= 1) { - let UIEl = this.section.getUIElement(section_id, 'vless_encryption_hmpayload'); - let newpayload = new VlessEncryption(UIEl.getValue()).setKey('keypairs', keypairs); - - UIEl.setValue(newpayload.toString()); - - [ - ['server', '_vless_encryption_decryption'], - ['client', '_vless_encryption_encryption'] - ].forEach(([side, option]) => { - UIEl = this.section.getUIElement(section_id, option); - UIEl.setValue(newpayload.toString('mihomo', side)); - }); - } else - return _('Expecting: %s').format(_('least one keypair required')); - - return true; - } else - return result; - } - o.rmempty = false; - o.depends('vless_decryption', '1'); - o.modalonly = true; - - /* TLS fields */ - o = s.taboption('field_general', form.Flag, 'tls', _('TLS')); - o.default = o.disabled; - o.validate = function(section_id, value) { - const type = this.section.getOption('type').formvalue(section_id); - let tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input'); - let tls_alpn = this.section.getUIElement(section_id, 'tls_alpn'); - let tls_reality = this.section.getUIElement(section_id, 'tls_reality').node.querySelector('input'); - - // Force enabled - if (['trojan', 'anytls', 'tuic', 'hysteria2'].includes(type)) { - tls.checked = true; - tls.disabled = true; - if (['tuic', 'hysteria2'].includes(type) && !`${tls_alpn.getValue()}`) - tls_alpn.setValue('h3'); - } else { - tls.removeAttribute('disabled'); - } - - // Force disabled - if (!['vmess', 'vless', 'trojan'].includes(type)) { - tls_reality.checked = false; - tls_reality.disabled = true; - } else { - tls_reality.removeAttribute('disabled'); - } - - return true; - } - o.depends({type: /^(http|socks|mixed|vmess|vless|trojan|anytls|tuic|hysteria2)$/}); - o.modalonly = true; - - o = s.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'), - _('List of supported application level protocols, in order of preference.')); - o.depends('tls', '1'); - o.modalonly = true; - - o = s.taboption('field_tls', form.Value, 'tls_cert_path', _('Certificate path'), - _('The %s public key, in PEM format.').format(_('Server'))); - o.value('/etc/fchomo/certs/server_publickey.pem'); - o.depends({tls: '1', tls_reality: '0'}); - o.rmempty = false; - o.modalonly = true; - - o = s.taboption('field_tls', form.Button, '_upload_cert', _('Upload certificate'), - _('Save your configuration before uploading files!')); - o.inputstyle = 'action'; - o.inputtitle = _('Upload...'); - o.depends({tls: '1', tls_cert_path: '/etc/fchomo/certs/server_publickey.pem'}); - o.onclick = L.bind(hm.uploadCertificate, o, _('certificate'), 'server_publickey'); - o.modalonly = true; - - o = s.taboption('field_tls', form.Value, 'tls_key_path', _('Key path'), - _('The %s private key, in PEM format.').format(_('Server'))); - o.value('/etc/fchomo/certs/server_privatekey.pem'); - o.rmempty = false; - o.depends({tls: '1', tls_cert_path: /.+/}); - o.modalonly = true; - - o = s.taboption('field_tls', form.Button, '_upload_key', _('Upload key'), - _('Save your configuration before uploading files!')); - o.inputstyle = 'action'; - o.inputtitle = _('Upload...'); - o.depends({tls: '1', tls_key_path: '/etc/fchomo/certs/server_privatekey.pem'}); - o.onclick = L.bind(hm.uploadCertificate, o, _('private key'), 'server_privatekey'); - o.modalonly = true; - - o = s.taboption('field_tls', form.ListValue, 'tls_client_auth_type', _('Client Auth type') + _(' (mTLS)')); - o.default = hm.tls_client_auth_types[0][0]; - hm.tls_client_auth_types.forEach((res) => { - o.value.apply(o, res); - }) - o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic)$/}); - o.modalonly = true; - - o = s.taboption('field_tls', form.Value, 'tls_client_auth_cert_path', _('Client Auth Certificate path') + _(' (mTLS)'), - _('The %s public key, in PEM format.').format(_('Client'))); - o.value('/etc/fchomo/certs/client_publickey.pem'); - o.validate = function(/* ... */) { - return hm.validateMTLSClientAuth.call(this, 'tls_client_auth_type', ...arguments); - } - o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic)$/}); - o.modalonly = true; - - o = s.taboption('field_tls', form.Button, '_upload_client_auth_cert', _('Upload certificate') + _(' (mTLS)'), - _('Save your configuration before uploading files!')); - o.inputstyle = 'action'; - o.inputtitle = _('Upload...'); - o.depends({tls: '1', tls_client_auth_cert_path: '/etc/fchomo/certs/client_publickey.pem'}); - o.onclick = L.bind(hm.uploadCertificate, o, _('certificate'), 'client_publickey'); - o.modalonly = true; - - o = s.taboption('field_tls', hm.GenText, 'tls_ech_key', _('ECH key')); - o.placeholder = '-----BEGIN ECH KEYS-----\nACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\nmadSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\ndC5jb20AAA==\n-----END ECH KEYS-----'; - o.hm_placeholder = 'outer-sni.any.domain'; - o.cols = 30; - o.rows = 2; - o.hm_options = { - type: 'ech-keypair', - params: '', - callback: function(result) { - return [ - [this.option, result.ech_key], - ['tls_ech_config', result.ech_cfg] - ] - } - } - o.renderWidget = function(section_id, option_index, cfgvalue) { - let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue); - const cbid = this.cbid(section_id) + '._outer_sni'; - - node.appendChild(E('div', { 'class': 'control-group' }, [ - E('input', { - id: cbid, - class: 'cbi-input-text', - style: 'width: 10em', - placeholder: this.hm_placeholder - }), - E('button', { - class: 'cbi-button cbi-button-add', - click: ui.createHandlerFn(this, () => { - this.hm_options.params = document.getElementById(cbid).value; - - return hm.handleGenKey.call(this, this.hm_options); - }) - }, [ _('Generate') ]) - ])); - - return node; - } - o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic)$/}); - o.modalonly = true; - - o = s.taboption('field_tls', hm.CopyValue, 'tls_ech_config', _('ECH config'), - _('This ECH parameter needs to be added to the HTTPS record of the domain.')); - o.placeholder = 'AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA'; - o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic)$/}); - o.modalonly = true; - - // uTLS fields - o = s.taboption('field_tls', form.Flag, 'tls_reality', _('REALITY')); - o.default = o.disabled; - o.depends('tls', '1'); - o.modalonly = true; - - o = s.taboption('field_tls', form.Value, 'tls_reality_dest', _('REALITY handshake server')); - o.datatype = 'hostport'; - o.placeholder = 'cloud.tencent.com:443'; - o.rmempty = false; - o.depends('tls_reality', '1'); - o.modalonly = true; - - o = s.taboption('field_tls', hm.GenValue, 'tls_reality_private_key', _('REALITY private key')); - o.hm_options = { - type: 'reality-keypair', - callback: function(result) { - return [ - [this.option, result.private_key], - ['tls_reality_public_key', result.public_key] - ] - } - } - o.password = true; - o.rmempty = false; - o.depends('tls_reality', '1'); - o.modalonly = true; - - o = s.taboption('field_tls', hm.CopyValue, 'tls_reality_public_key', _('REALITY public key')); - o.depends('tls_reality', '1'); - o.modalonly = true; - - o = s.taboption('field_tls', form.DynamicList, 'tls_reality_short_id', _('REALITY short ID')); - //o.value('', '""'); - o.rmempty = false; - o.depends('tls_reality', '1'); - o.modalonly = true; - - o = s.taboption('field_tls', form.DynamicList, 'tls_reality_server_names', _('REALITY certificate issued to')); - o.datatype = 'list(hostname)'; - o.placeholder = 'cloud.tencent.com'; - o.rmempty = false; - o.depends('tls_reality', '1'); - o.modalonly = true; - - /* Transport fields */ - o = s.taboption('field_general', form.Flag, 'transport_enabled', _('Transport')); - o.default = o.disabled; - o.depends({type: /^(vmess|vless|trojan)$/}); - o.modalonly = true; - - o = s.taboption('field_transport', form.ListValue, 'transport_type', _('Transport type')); - o.value('grpc', _('gRPC')); - o.value('ws', _('WebSocket')); - o.validate = function(section_id, value) { - const type = this.section.getOption('type').formvalue(section_id); - - switch (type) { - case 'vmess': - case 'vless': - if (!['http', 'h2', 'grpc', 'ws'].includes(value)) - return _('Expecting: only support %s.').format(_('HTTP') + - ' / ' + _('HTTPUpgrade') + - ' / ' + _('gRPC') + - ' / ' + _('WebSocket')); - break; - case 'trojan': - if (!['grpc', 'ws'].includes(value)) - return _('Expecting: only support %s.').format(_('gRPC') + - ' / ' + _('WebSocket')); - break; - default: - break; - } - - return true; - } - o.depends('transport_enabled', '1'); - o.modalonly = true; - - o = s.taboption('field_transport', form.Value, 'transport_path', _('Request path')); - o.placeholder = '/'; - o.default = '/'; - o.rmempty = false; - o.depends({transport_enabled: '1', transport_type: 'ws'}); - o.modalonly = true; - - o = s.taboption('field_transport', form.Value, 'transport_grpc_servicename', _('gRPC service name')); - o.placeholder = 'GunService'; - o.rmempty = false; - o.depends({transport_enabled: '1', transport_type: 'grpc'}); - o.modalonly = true; + lsnr.renderListeners(s, data[0], false); /* Server settings END */ return m.render(); diff --git a/small/luci-app-fchomo/po/templates/fchomo.pot b/small/luci-app-fchomo/po/templates/fchomo.pot index 76959ec5d4..861bc01a2d 100644 --- a/small/luci-app-fchomo/po/templates/fchomo.pot +++ b/small/luci-app-fchomo/po/templates/fchomo.pot @@ -5,30 +5,30 @@ msgstr "Content-Type: text/plain; charset=UTF-8" msgid "%s log" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:223 -#: htdocs/luci-static/resources/fchomo.js:224 -#: htdocs/luci-static/resources/fchomo.js:225 -#: htdocs/luci-static/resources/fchomo.js:226 -#: htdocs/luci-static/resources/fchomo.js:227 -#: htdocs/luci-static/resources/fchomo.js:228 +#: htdocs/luci-static/resources/fchomo.js:229 +#: htdocs/luci-static/resources/fchomo.js:230 +#: htdocs/luci-static/resources/fchomo.js:231 +#: htdocs/luci-static/resources/fchomo.js:232 +#: htdocs/luci-static/resources/fchomo.js:233 +#: htdocs/luci-static/resources/fchomo.js:234 msgid "%s ports" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:588 -#: htdocs/luci-static/resources/fchomo.js:591 +#: htdocs/luci-static/resources/fchomo.js:594 +#: htdocs/luci-static/resources/fchomo.js:597 #: htdocs/luci-static/resources/view/fchomo/client.js:315 msgid "(Imported)" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:840 +#: htdocs/luci-static/resources/fchomo/listeners.js:848 +#: htdocs/luci-static/resources/fchomo/listeners.js:857 #: htdocs/luci-static/resources/view/fchomo/global.js:543 #: htdocs/luci-static/resources/view/fchomo/global.js:549 -#: htdocs/luci-static/resources/view/fchomo/node.js:1028 -#: htdocs/luci-static/resources/view/fchomo/node.js:1034 #: htdocs/luci-static/resources/view/fchomo/node.js:1042 #: htdocs/luci-static/resources/view/fchomo/node.js:1048 -#: htdocs/luci-static/resources/view/fchomo/server.js:846 -#: htdocs/luci-static/resources/view/fchomo/server.js:854 -#: htdocs/luci-static/resources/view/fchomo/server.js:863 +#: htdocs/luci-static/resources/view/fchomo/node.js:1056 +#: htdocs/luci-static/resources/view/fchomo/node.js:1062 msgid "(mTLS)" msgstr "" @@ -39,19 +39,19 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1056 #: htdocs/luci-static/resources/view/fchomo/client.js:1057 #: htdocs/luci-static/resources/view/fchomo/client.js:1278 -#: htdocs/luci-static/resources/view/fchomo/node.js:1734 -#: htdocs/luci-static/resources/view/fchomo/node.js:1740 +#: htdocs/luci-static/resources/view/fchomo/node.js:1748 #: htdocs/luci-static/resources/view/fchomo/node.js:1754 -#: htdocs/luci-static/resources/view/fchomo/node.js:1760 +#: htdocs/luci-static/resources/view/fchomo/node.js:1768 +#: htdocs/luci-static/resources/view/fchomo/node.js:1774 msgid "-- Please choose --" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:375 +#: htdocs/luci-static/resources/fchomo.js:381 msgid "0-RTT reuse." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:372 -#: htdocs/luci-static/resources/fchomo.js:376 +#: htdocs/luci-static/resources/fchomo.js:378 +#: htdocs/luci-static/resources/fchomo.js:382 msgid "1-RTT only." msgstr "" @@ -59,15 +59,15 @@ msgstr "" msgid "163Music" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:322 +#: htdocs/luci-static/resources/fchomo.js:328 msgid "2022-blake3-aes-128-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:323 +#: htdocs/luci-static/resources/fchomo.js:329 msgid "2022-blake3-aes-256-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:324 +#: htdocs/luci-static/resources/fchomo.js:330 msgid "2022-blake3-chacha20-poly1305" msgstr "" @@ -75,14 +75,22 @@ msgstr "" msgid "0 or 1 only." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1035 +#: htdocs/luci-static/resources/fchomo/listeners.js:818 +#: htdocs/luci-static/resources/fchomo/listeners.js:833 +#: htdocs/luci-static/resources/fchomo/listeners.js:858 #: htdocs/luci-static/resources/view/fchomo/node.js:1049 -#: htdocs/luci-static/resources/view/fchomo/server.js:824 -#: htdocs/luci-static/resources/view/fchomo/server.js:839 -#: htdocs/luci-static/resources/view/fchomo/server.js:864 +#: htdocs/luci-static/resources/view/fchomo/node.js:1063 msgid "Save your configuration before uploading files!" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:239 +#: htdocs/luci-static/resources/view/fchomo/node.js:379 +msgid "" +"A base64 string is used to fine-tune network behavior.
Please refer to " +"the document." +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/global.js:599 msgid "API" msgstr "" @@ -152,11 +160,15 @@ msgstr "" msgid "Add a Node" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1325 +#: htdocs/luci-static/resources/view/fchomo/inbound.js:28 +msgid "Add a inbound" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1339 msgid "Add a provider" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1710 +#: htdocs/luci-static/resources/view/fchomo/node.js:1724 msgid "Add a proxy chain" msgstr "" @@ -172,7 +184,7 @@ msgstr "" msgid "Add a rule set" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:166 +#: htdocs/luci-static/resources/view/fchomo/server.js:58 msgid "Add a server" msgstr "" @@ -180,11 +192,11 @@ msgstr "" msgid "Add a sub rule" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1535 +#: htdocs/luci-static/resources/view/fchomo/node.js:1549 msgid "Add prefix" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1539 +#: htdocs/luci-static/resources/view/fchomo/node.js:1553 msgid "Add suffix" msgstr "" @@ -193,7 +205,7 @@ msgstr "" msgid "Address" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:379 +#: htdocs/luci-static/resources/fchomo.js:385 msgid "" "After the 1-RTT client/server hello, padding randomly 111-1111 bytes with " "100% probability." @@ -209,7 +221,7 @@ msgstr "" msgid "All allowed" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:220 +#: htdocs/luci-static/resources/fchomo.js:226 msgid "All ports" msgstr "" @@ -219,7 +231,7 @@ msgid "" "network from a public website, it must be enabled." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:762 +#: htdocs/luci-static/resources/view/fchomo/node.js:765 msgid "Allowed IPs" msgstr "" @@ -231,13 +243,13 @@ msgstr "" msgid "Already in updating." msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:474 #: htdocs/luci-static/resources/view/fchomo/node.js:635 -#: htdocs/luci-static/resources/view/fchomo/server.js:526 msgid "Alter ID" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:148 -#: htdocs/luci-static/resources/fchomo.js:181 +#: htdocs/luci-static/resources/fchomo.js:152 +#: htdocs/luci-static/resources/fchomo.js:186 msgid "AnyTLS" msgstr "" @@ -254,7 +266,7 @@ msgstr "" msgid "As the TOP upstream of dnsmasq." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:476 +#: htdocs/luci-static/resources/fchomo/listeners.js:424 msgid "Auth timeout" msgstr "" @@ -262,14 +274,14 @@ msgstr "" msgid "Authenticated length" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:389 #: htdocs/luci-static/resources/view/fchomo/global.js:402 -#: htdocs/luci-static/resources/view/fchomo/node.js:450 -#: htdocs/luci-static/resources/view/fchomo/node.js:473 -#: htdocs/luci-static/resources/view/fchomo/server.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:456 +#: htdocs/luci-static/resources/view/fchomo/node.js:480 msgid "Auto" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:189 +#: htdocs/luci-static/resources/fchomo/listeners.js:137 msgid "Auto configure firewall" msgstr "" @@ -311,13 +323,13 @@ msgid "Binary mrs" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:734 -#: htdocs/luci-static/resources/view/fchomo/node.js:1295 -#: htdocs/luci-static/resources/view/fchomo/node.js:1608 +#: htdocs/luci-static/resources/view/fchomo/node.js:1309 +#: htdocs/luci-static/resources/view/fchomo/node.js:1622 msgid "Bind interface" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1296 -#: htdocs/luci-static/resources/view/fchomo/node.js:1609 +#: htdocs/luci-static/resources/view/fchomo/node.js:1310 +#: htdocs/luci-static/resources/view/fchomo/node.js:1623 msgid "Bind outbound interface.
" msgstr "" @@ -355,12 +367,14 @@ msgstr "" msgid "Bypass DSCP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:448 -#: htdocs/luci-static/resources/view/fchomo/node.js:449 -#: htdocs/luci-static/resources/view/fchomo/node.js:450 -#: htdocs/luci-static/resources/view/fchomo/server.js:437 -#: htdocs/luci-static/resources/view/fchomo/server.js:438 -#: htdocs/luci-static/resources/view/fchomo/server.js:439 +#: htdocs/luci-static/resources/fchomo/listeners.js:387 +#: htdocs/luci-static/resources/fchomo/listeners.js:388 +#: htdocs/luci-static/resources/fchomo/listeners.js:389 +#: htdocs/luci-static/resources/fchomo/listeners.js:390 +#: htdocs/luci-static/resources/view/fchomo/node.js:454 +#: htdocs/luci-static/resources/view/fchomo/node.js:455 +#: htdocs/luci-static/resources/view/fchomo/node.js:456 +#: htdocs/luci-static/resources/view/fchomo/node.js:457 msgid "CDN support" msgstr "" @@ -376,21 +390,21 @@ msgstr "" msgid "CORS allowed origins, * will be used if empty." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:704 +#: htdocs/luci-static/resources/fchomo.js:710 msgid "Cancel" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1007 +#: htdocs/luci-static/resources/view/fchomo/node.js:1021 msgid "Cert fingerprint" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/node.js:1022 msgid "" "Certificate fingerprint. Used to implement SSL Pinning and prevent MitM." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1028 -#: htdocs/luci-static/resources/view/fchomo/server.js:816 +#: htdocs/luci-static/resources/fchomo/listeners.js:810 +#: htdocs/luci-static/resources/view/fchomo/node.js:1042 msgid "Certificate path" msgstr "" @@ -419,16 +433,16 @@ msgstr "" msgid "China list version" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:213 +#: htdocs/luci-static/resources/fchomo/listeners.js:306 #: htdocs/luci-static/resources/view/fchomo/node.js:332 -#: htdocs/luci-static/resources/view/fchomo/node.js:385 +#: htdocs/luci-static/resources/view/fchomo/node.js:391 #: htdocs/luci-static/resources/view/fchomo/node.js:641 -#: htdocs/luci-static/resources/view/fchomo/server.js:269 -#: htdocs/luci-static/resources/view/fchomo/server.js:356 msgid "Chipher" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:394 -#: htdocs/luci-static/resources/view/fchomo/server.js:365 +#: htdocs/luci-static/resources/fchomo/listeners.js:315 +#: htdocs/luci-static/resources/view/fchomo/node.js:400 msgid "Chipher must be enabled if obfuscate downlink is disabled." msgstr "" @@ -442,29 +456,29 @@ msgid "" "to download the latest initial package." msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:633 +#: htdocs/luci-static/resources/fchomo/listeners.js:849 #: htdocs/luci-static/resources/view/fchomo/global.js:550 -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/node.js:1029 +#: htdocs/luci-static/resources/view/fchomo/node.js:916 #: htdocs/luci-static/resources/view/fchomo/node.js:1043 -#: htdocs/luci-static/resources/view/fchomo/server.js:645 -#: htdocs/luci-static/resources/view/fchomo/server.js:855 +#: htdocs/luci-static/resources/view/fchomo/node.js:1057 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:22 msgid "Client" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:854 +#: htdocs/luci-static/resources/fchomo/listeners.js:848 msgid "Client Auth Certificate path" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:846 +#: htdocs/luci-static/resources/fchomo/listeners.js:840 msgid "Client Auth type" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1074 +#: htdocs/luci-static/resources/view/fchomo/node.js:1088 msgid "Client fingerprint" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:352 +#: htdocs/luci-static/resources/fchomo/listeners.js:302 msgid "Client key" msgstr "" @@ -476,22 +490,21 @@ msgstr "" msgid "Collecting data..." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:221 -#: htdocs/luci-static/resources/fchomo.js:222 +#: htdocs/luci-static/resources/fchomo.js:227 +#: htdocs/luci-static/resources/fchomo.js:228 msgid "Common ports (bypass P2P traffic)" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1303 +#: htdocs/luci-static/resources/fchomo.js:1309 msgid "Complete" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1555 +#: htdocs/luci-static/resources/view/fchomo/node.js:1569 msgid "Configuration Items" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:515 -#: htdocs/luci-static/resources/view/fchomo/node.js:717 -#: htdocs/luci-static/resources/view/fchomo/server.js:454 +#: htdocs/luci-static/resources/fchomo/listeners.js:537 +#: htdocs/luci-static/resources/view/fchomo/node.js:854 msgid "Congestion controller" msgstr "" @@ -499,19 +512,19 @@ msgstr "" msgid "Connection check" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:573 +#: htdocs/luci-static/resources/fchomo.js:579 msgid "Content copied to clipboard!" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:670 -#: htdocs/luci-static/resources/view/fchomo/node.js:1465 -#: htdocs/luci-static/resources/view/fchomo/node.js:1491 +#: htdocs/luci-static/resources/view/fchomo/node.js:1479 +#: htdocs/luci-static/resources/view/fchomo/node.js:1505 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:332 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:358 msgid "Content will not be verified, Please make sure you enter it correctly." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1464 +#: htdocs/luci-static/resources/view/fchomo/node.js:1478 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:331 msgid "Contents" msgstr "" @@ -520,7 +533,7 @@ msgstr "" msgid "Contents have been saved." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:575 +#: htdocs/luci-static/resources/fchomo.js:581 msgid "Copy" msgstr "" @@ -536,7 +549,7 @@ msgstr "" msgid "Custom Direct List" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1526 +#: htdocs/luci-static/resources/view/fchomo/node.js:1540 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:393 msgid "Custom HTTP header." msgstr "" @@ -545,8 +558,8 @@ msgstr "" msgid "Custom Proxy List" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:407 -#: htdocs/luci-static/resources/view/fchomo/server.js:378 +#: htdocs/luci-static/resources/fchomo/listeners.js:328 +#: htdocs/luci-static/resources/view/fchomo/node.js:413 msgid "Custom byte layout" msgstr "" @@ -555,7 +568,7 @@ msgid "" "Custom internal hosts. Support yaml or json format." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:170 +#: htdocs/luci-static/resources/fchomo.js:175 msgid "DIRECT" msgstr "" @@ -572,7 +585,7 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1428 #: htdocs/luci-static/resources/view/fchomo/client.js:1437 #: htdocs/luci-static/resources/view/fchomo/node.js:712 -#: htdocs/luci-static/resources/view/fchomo/node.js:792 +#: htdocs/luci-static/resources/view/fchomo/node.js:795 msgid "DNS server" msgstr "" @@ -600,15 +613,15 @@ msgstr "" msgid "Default DNS server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:763 +#: htdocs/luci-static/resources/view/fchomo/node.js:766 msgid "Destination addresses allowed to be forwarded via Wireguard." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1733 +#: htdocs/luci-static/resources/view/fchomo/node.js:1747 msgid "Destination provider" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1739 +#: htdocs/luci-static/resources/view/fchomo/node.js:1753 msgid "Destination proxy node" msgstr "" @@ -616,8 +629,8 @@ msgstr "" msgid "Dial fields" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1746 -#: htdocs/luci-static/resources/view/fchomo/node.js:1766 +#: htdocs/luci-static/resources/view/fchomo/node.js:1760 +#: htdocs/luci-static/resources/view/fchomo/node.js:1780 msgid "Different chain head/tail" msgstr "" @@ -637,9 +650,9 @@ msgstr "" msgid "Direct MAC-s" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:193 #: htdocs/luci-static/resources/view/fchomo/global.js:403 #: htdocs/luci-static/resources/view/fchomo/node.js:298 -#: htdocs/luci-static/resources/view/fchomo/server.js:249 msgid "Disable" msgstr "" @@ -655,7 +668,7 @@ msgstr "" msgid "Disable ICMP Forwarding" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:956 +#: htdocs/luci-static/resources/view/fchomo/node.js:967 msgid "Disable SNI" msgstr "" @@ -681,46 +694,46 @@ msgstr "" msgid "Domain" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:957 +#: htdocs/luci-static/resources/view/fchomo/node.js:968 msgid "Donot send server name in ClientHello." msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1577 -#: htdocs/luci-static/resources/view/fchomo/node.js:1021 -#: htdocs/luci-static/resources/view/fchomo/node.js:1595 +#: htdocs/luci-static/resources/view/fchomo/node.js:1035 +#: htdocs/luci-static/resources/view/fchomo/node.js:1609 msgid "Donot verifying server certificate." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1274 +#: htdocs/luci-static/resources/view/fchomo/node.js:1288 msgid "Download bandwidth" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1275 +#: htdocs/luci-static/resources/view/fchomo/node.js:1289 msgid "Download bandwidth in Mbps." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1182 +#: htdocs/luci-static/resources/fchomo.js:1188 msgid "Download failed: %s" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1180 +#: htdocs/luci-static/resources/fchomo.js:1186 msgid "Download successful." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:156 +#: htdocs/luci-static/resources/fchomo.js:161 msgid "Dual stack" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1068 +#: htdocs/luci-static/resources/view/fchomo/node.js:1082 msgid "ECH HTTPS record query servername" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1062 -#: htdocs/luci-static/resources/view/fchomo/server.js:912 +#: htdocs/luci-static/resources/fchomo/listeners.js:906 +#: htdocs/luci-static/resources/view/fchomo/node.js:1076 msgid "ECH config" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:871 +#: htdocs/luci-static/resources/fchomo/listeners.js:865 msgid "ECH key" msgstr "" @@ -736,14 +749,18 @@ msgstr "" msgid "ETag support" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1187 +#: htdocs/luci-static/resources/view/fchomo/node.js:1201 msgid "Early Data first packet length limit." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1193 +#: htdocs/luci-static/resources/view/fchomo/node.js:1207 msgid "Early Data header name" msgstr "" +#: htdocs/luci-static/resources/view/fchomo/inbound.js:20 +msgid "Edit inbound" +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/node.js:203 msgid "Edit node" msgstr "" @@ -752,15 +769,16 @@ msgstr "" msgid "Edit ruleset" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1462 +#: htdocs/luci-static/resources/view/fchomo/node.js:1476 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:329 msgid "Editer" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:366 +#: htdocs/luci-static/resources/fchomo.js:372 msgid "Eliminate encryption header characteristics" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:132 #: htdocs/luci-static/resources/view/fchomo/client.js:931 #: htdocs/luci-static/resources/view/fchomo/client.js:1024 #: htdocs/luci-static/resources/view/fchomo/client.js:1262 @@ -771,12 +789,11 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:401 #: htdocs/luci-static/resources/view/fchomo/global.js:680 #: htdocs/luci-static/resources/view/fchomo/node.js:234 -#: htdocs/luci-static/resources/view/fchomo/node.js:1435 -#: htdocs/luci-static/resources/view/fchomo/node.js:1631 -#: htdocs/luci-static/resources/view/fchomo/node.js:1720 +#: htdocs/luci-static/resources/view/fchomo/node.js:1449 +#: htdocs/luci-static/resources/view/fchomo/node.js:1645 +#: htdocs/luci-static/resources/view/fchomo/node.js:1734 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:257 -#: htdocs/luci-static/resources/view/fchomo/server.js:157 -#: htdocs/luci-static/resources/view/fchomo/server.js:184 +#: htdocs/luci-static/resources/view/fchomo/server.js:49 msgid "Enable" msgstr "" @@ -797,49 +814,49 @@ msgid "" "conversion for outbound connections" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1056 +#: htdocs/luci-static/resources/view/fchomo/node.js:1070 msgid "Enable ECH" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1262 +#: htdocs/luci-static/resources/view/fchomo/node.js:1276 msgid "Enable TCP Brutal" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1263 +#: htdocs/luci-static/resources/view/fchomo/node.js:1277 msgid "Enable TCP Brutal congestion control algorithm" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1251 +#: htdocs/luci-static/resources/view/fchomo/node.js:1265 msgid "Enable multiplexing only for TCP." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:434 -#: htdocs/luci-static/resources/view/fchomo/server.js:423 +#: htdocs/luci-static/resources/fchomo/listeners.js:373 +#: htdocs/luci-static/resources/view/fchomo/node.js:440 msgid "Enable obfuscate for downlink" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1245 +#: htdocs/luci-static/resources/view/fchomo/node.js:1259 msgid "Enable padding" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1256 +#: htdocs/luci-static/resources/view/fchomo/node.js:1270 msgid "Enable statistic" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:857 -#: htdocs/luci-static/resources/view/fchomo/node.js:1577 +#: htdocs/luci-static/resources/view/fchomo/node.js:868 +#: htdocs/luci-static/resources/view/fchomo/node.js:1591 msgid "" "Enable the SUoT protocol, requires server support. Conflict with Multiplex." msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:199 #: htdocs/luci-static/resources/view/fchomo/node.js:304 -#: htdocs/luci-static/resources/view/fchomo/server.js:255 msgid "" "Enabling obfuscation will make the server incompatible with standard QUIC " "connections, losing the ability to masquerade with HTTP/3." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:608 +#: htdocs/luci-static/resources/fchomo/listeners.js:596 msgid "Encryption method" msgstr "" @@ -866,7 +883,7 @@ msgid "" "if empty." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1689 +#: htdocs/luci-static/resources/view/fchomo/node.js:1703 msgid "Exclude matched node types." msgstr "" @@ -877,7 +894,7 @@ msgid "" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1161 -#: htdocs/luci-static/resources/view/fchomo/node.js:1682 +#: htdocs/luci-static/resources/view/fchomo/node.js:1696 msgid "Exclude nodes that meet keywords or regexps." msgstr "" @@ -886,64 +903,65 @@ msgid "Expand/Collapse result" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1121 -#: htdocs/luci-static/resources/view/fchomo/node.js:1667 +#: htdocs/luci-static/resources/view/fchomo/node.js:1681 msgid "Expected HTTP code. 204 will be used if empty." msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1123 -#: htdocs/luci-static/resources/view/fchomo/node.js:1669 +#: htdocs/luci-static/resources/view/fchomo/node.js:1683 msgid "Expected status" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:423 -#: htdocs/luci-static/resources/fchomo.js:426 #: htdocs/luci-static/resources/fchomo.js:429 -#: htdocs/luci-static/resources/fchomo.js:1320 -#: htdocs/luci-static/resources/fchomo.js:1328 -#: htdocs/luci-static/resources/fchomo.js:1336 -#: htdocs/luci-static/resources/fchomo.js:1359 -#: htdocs/luci-static/resources/fchomo.js:1362 -#: htdocs/luci-static/resources/fchomo.js:1369 -#: htdocs/luci-static/resources/fchomo.js:1385 -#: htdocs/luci-static/resources/fchomo.js:1394 -#: htdocs/luci-static/resources/fchomo.js:1406 -#: htdocs/luci-static/resources/fchomo.js:1409 -#: htdocs/luci-static/resources/fchomo.js:1419 -#: htdocs/luci-static/resources/fchomo.js:1431 -#: htdocs/luci-static/resources/fchomo.js:1434 -#: htdocs/luci-static/resources/fchomo.js:1444 -#: htdocs/luci-static/resources/fchomo.js:1454 -#: htdocs/luci-static/resources/fchomo.js:1489 -#: htdocs/luci-static/resources/fchomo.js:1491 -#: htdocs/luci-static/resources/fchomo.js:1504 +#: htdocs/luci-static/resources/fchomo.js:432 +#: htdocs/luci-static/resources/fchomo.js:435 +#: htdocs/luci-static/resources/fchomo.js:1326 +#: htdocs/luci-static/resources/fchomo.js:1334 +#: htdocs/luci-static/resources/fchomo.js:1342 +#: htdocs/luci-static/resources/fchomo.js:1365 +#: htdocs/luci-static/resources/fchomo.js:1368 +#: htdocs/luci-static/resources/fchomo.js:1375 +#: htdocs/luci-static/resources/fchomo.js:1391 +#: htdocs/luci-static/resources/fchomo.js:1400 +#: htdocs/luci-static/resources/fchomo.js:1412 +#: htdocs/luci-static/resources/fchomo.js:1415 +#: htdocs/luci-static/resources/fchomo.js:1425 +#: htdocs/luci-static/resources/fchomo.js:1437 +#: htdocs/luci-static/resources/fchomo.js:1440 +#: htdocs/luci-static/resources/fchomo.js:1450 +#: htdocs/luci-static/resources/fchomo.js:1460 +#: htdocs/luci-static/resources/fchomo.js:1495 +#: htdocs/luci-static/resources/fchomo.js:1497 #: htdocs/luci-static/resources/fchomo.js:1510 -#: htdocs/luci-static/resources/fchomo.js:1517 -#: htdocs/luci-static/resources/fchomo.js:1526 +#: htdocs/luci-static/resources/fchomo.js:1516 +#: htdocs/luci-static/resources/fchomo.js:1523 +#: htdocs/luci-static/resources/fchomo.js:1532 +#: htdocs/luci-static/resources/fchomo/listeners.js:315 +#: htdocs/luci-static/resources/fchomo/listeners.js:359 +#: htdocs/luci-static/resources/fchomo/listeners.js:625 +#: htdocs/luci-static/resources/fchomo/listeners.js:656 +#: htdocs/luci-static/resources/fchomo/listeners.js:757 #: htdocs/luci-static/resources/view/fchomo/client.js:68 #: htdocs/luci-static/resources/view/fchomo/client.js:1018 #: htdocs/luci-static/resources/view/fchomo/client.js:1508 #: htdocs/luci-static/resources/view/fchomo/global.js:880 -#: htdocs/luci-static/resources/view/fchomo/node.js:394 -#: htdocs/luci-static/resources/view/fchomo/node.js:427 -#: htdocs/luci-static/resources/view/fchomo/node.js:479 -#: htdocs/luci-static/resources/view/fchomo/node.js:928 -#: htdocs/luci-static/resources/view/fchomo/node.js:1013 -#: htdocs/luci-static/resources/view/fchomo/node.js:1746 -#: htdocs/luci-static/resources/view/fchomo/node.js:1766 +#: htdocs/luci-static/resources/view/fchomo/node.js:400 +#: htdocs/luci-static/resources/view/fchomo/node.js:433 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +#: htdocs/luci-static/resources/view/fchomo/node.js:939 +#: htdocs/luci-static/resources/view/fchomo/node.js:1027 +#: htdocs/luci-static/resources/view/fchomo/node.js:1760 +#: htdocs/luci-static/resources/view/fchomo/node.js:1780 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:284 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:298 -#: htdocs/luci-static/resources/view/fchomo/server.js:365 -#: htdocs/luci-static/resources/view/fchomo/server.js:409 -#: htdocs/luci-static/resources/view/fchomo/server.js:637 -#: htdocs/luci-static/resources/view/fchomo/server.js:668 -#: htdocs/luci-static/resources/view/fchomo/server.js:769 msgid "Expecting: %s" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1123 -#: htdocs/luci-static/resources/view/fchomo/node.js:1130 -#: htdocs/luci-static/resources/view/fchomo/server.js:979 -#: htdocs/luci-static/resources/view/fchomo/server.js:986 +#: htdocs/luci-static/resources/fchomo/listeners.js:973 +#: htdocs/luci-static/resources/fchomo/listeners.js:980 +#: htdocs/luci-static/resources/view/fchomo/node.js:1137 +#: htdocs/luci-static/resources/view/fchomo/node.js:1144 msgid "Expecting: only support %s." msgstr "" @@ -962,23 +980,24 @@ msgstr "" msgid "Factor" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1261 +#: htdocs/luci-static/resources/fchomo.js:1267 msgid "Failed to execute \"/etc/init.d/fchomo %s %s\" reason: %s" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1214 +#: htdocs/luci-static/resources/fchomo.js:1220 msgid "Failed to generate %s, error: %s." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1626 +#: htdocs/luci-static/resources/fchomo.js:1632 msgid "Failed to upload %s, error: %s." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1645 +#: htdocs/luci-static/resources/fchomo.js:1651 msgid "Failed to upload, error: %s." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:213 +#: htdocs/luci-static/resources/fchomo.js:219 +#: htdocs/luci-static/resources/fchomo/listeners.js:398 msgid "Fallback" msgstr "" @@ -992,7 +1011,7 @@ msgid "Fallback filter" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1156 -#: htdocs/luci-static/resources/view/fchomo/node.js:1676 +#: htdocs/luci-static/resources/view/fchomo/node.js:1690 msgid "Filter nodes that meet keywords or regexps." msgstr "" @@ -1017,12 +1036,12 @@ msgstr "" msgid "Final DNS server (For poisoned domains)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:188 +#: htdocs/luci-static/resources/fchomo/listeners.js:136 msgid "Firewall" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:466 #: htdocs/luci-static/resources/view/fchomo/node.js:627 -#: htdocs/luci-static/resources/view/fchomo/server.js:518 msgid "Flow" msgstr "" @@ -1033,15 +1052,15 @@ msgid "" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1122 -#: htdocs/luci-static/resources/view/fchomo/node.js:1545 -#: htdocs/luci-static/resources/view/fchomo/node.js:1668 +#: htdocs/luci-static/resources/view/fchomo/node.js:1559 +#: htdocs/luci-static/resources/view/fchomo/node.js:1682 msgid "" "For format see
%s." msgstr "" #: htdocs/luci-static/resources/view/fchomo/node.js:707 -#: htdocs/luci-static/resources/view/fchomo/node.js:787 +#: htdocs/luci-static/resources/view/fchomo/node.js:790 msgid "Force DNS remote resolution." msgstr "" @@ -1060,7 +1079,7 @@ msgstr "" msgid "FullCombo Shark!" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1149 +#: htdocs/luci-static/resources/view/fchomo/node.js:1163 msgid "GET" msgstr "" @@ -1072,10 +1091,10 @@ msgstr "" msgid "General" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:119 #: htdocs/luci-static/resources/view/fchomo/client.js:1009 #: htdocs/luci-static/resources/view/fchomo/node.js:222 -#: htdocs/luci-static/resources/view/fchomo/node.js:1425 -#: htdocs/luci-static/resources/view/fchomo/server.js:171 +#: htdocs/luci-static/resources/view/fchomo/node.js:1439 msgid "General fields" msgstr "" @@ -1083,16 +1102,16 @@ msgstr "" msgid "General settings" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:524 -#: htdocs/luci-static/resources/fchomo.js:526 -#: htdocs/luci-static/resources/fchomo.js:540 -#: htdocs/luci-static/resources/fchomo.js:542 +#: htdocs/luci-static/resources/fchomo.js:530 +#: htdocs/luci-static/resources/fchomo.js:532 +#: htdocs/luci-static/resources/fchomo.js:546 +#: htdocs/luci-static/resources/fchomo.js:548 +#: htdocs/luci-static/resources/fchomo/listeners.js:293 +#: htdocs/luci-static/resources/fchomo/listeners.js:334 +#: htdocs/luci-static/resources/fchomo/listeners.js:336 +#: htdocs/luci-static/resources/fchomo/listeners.js:729 +#: htdocs/luci-static/resources/fchomo/listeners.js:898 #: htdocs/luci-static/resources/view/fchomo/global.js:587 -#: htdocs/luci-static/resources/view/fchomo/server.js:343 -#: htdocs/luci-static/resources/view/fchomo/server.js:384 -#: htdocs/luci-static/resources/view/fchomo/server.js:386 -#: htdocs/luci-static/resources/view/fchomo/server.js:741 -#: htdocs/luci-static/resources/view/fchomo/server.js:904 msgid "Generate" msgstr "" @@ -1143,7 +1162,7 @@ msgstr "" msgid "Google" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:226 +#: htdocs/luci-static/resources/fchomo.js:232 msgid "Google FCM" msgstr "" @@ -1155,48 +1174,49 @@ msgstr "" msgid "Group" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:139 -#: htdocs/luci-static/resources/fchomo.js:171 -#: htdocs/luci-static/resources/view/fchomo/node.js:810 -#: htdocs/luci-static/resources/view/fchomo/node.js:1112 -#: htdocs/luci-static/resources/view/fchomo/node.js:1123 -#: htdocs/luci-static/resources/view/fchomo/server.js:979 +#: htdocs/luci-static/resources/fchomo.js:143 +#: htdocs/luci-static/resources/fchomo.js:176 +#: htdocs/luci-static/resources/fchomo/listeners.js:973 +#: htdocs/luci-static/resources/view/fchomo/node.js:813 +#: htdocs/luci-static/resources/view/fchomo/node.js:1126 +#: htdocs/luci-static/resources/view/fchomo/node.js:1137 msgid "HTTP" msgstr "" #: htdocs/luci-static/resources/view/fchomo/node.js:267 -#: htdocs/luci-static/resources/view/fchomo/node.js:1171 -#: htdocs/luci-static/resources/view/fchomo/node.js:1525 +#: htdocs/luci-static/resources/view/fchomo/node.js:1185 +#: htdocs/luci-static/resources/view/fchomo/node.js:1539 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:392 msgid "HTTP header" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:440 -#: htdocs/luci-static/resources/view/fchomo/server.js:429 +#: htdocs/luci-static/resources/fchomo/listeners.js:379 +#: htdocs/luci-static/resources/view/fchomo/node.js:446 msgid "HTTP mask" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:445 -#: htdocs/luci-static/resources/view/fchomo/node.js:479 -#: htdocs/luci-static/resources/view/fchomo/server.js:434 +#: htdocs/luci-static/resources/fchomo/listeners.js:384 +#: htdocs/luci-static/resources/view/fchomo/node.js:451 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 msgid "HTTP mask mode" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:469 +#: htdocs/luci-static/resources/view/fchomo/node.js:476 msgid "HTTP mask multiplex" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:454 -#: htdocs/luci-static/resources/view/fchomo/node.js:459 +#: htdocs/luci-static/resources/view/fchomo/node.js:461 +#: htdocs/luci-static/resources/view/fchomo/node.js:466 msgid "HTTP mask: %s" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1148 +#: htdocs/luci-static/resources/view/fchomo/node.js:1162 msgid "HTTP request method" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:465 -#: htdocs/luci-static/resources/view/fchomo/server.js:443 +#: htdocs/luci-static/resources/fchomo/listeners.js:394 +#: htdocs/luci-static/resources/view/fchomo/node.js:472 msgid "HTTP root path" msgstr "" @@ -1204,15 +1224,15 @@ msgstr "" msgid "HTTP/3" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:263 +#: htdocs/luci-static/resources/fchomo/listeners.js:207 msgid "" "HTTP3 server behavior when authentication fails.
A 404 page will be " "returned if empty." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1113 -#: htdocs/luci-static/resources/view/fchomo/node.js:1124 -#: htdocs/luci-static/resources/view/fchomo/server.js:980 +#: htdocs/luci-static/resources/fchomo/listeners.js:974 +#: htdocs/luci-static/resources/view/fchomo/node.js:1127 +#: htdocs/luci-static/resources/view/fchomo/node.js:1138 msgid "HTTPUpgrade" msgstr "" @@ -1224,36 +1244,40 @@ msgstr "" msgid "Handshake mode" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:541 +#: htdocs/luci-static/resources/fchomo/listeners.js:498 msgid "Handshake target that supports TLS 1.3" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:416 +#: htdocs/luci-static/resources/fchomo/listeners.js:366 msgid "Handshake timeout" msgstr "" +#: htdocs/luci-static/resources/view/fchomo/node.js:718 +msgid "Health check" +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/client.js:1091 -#: htdocs/luci-static/resources/view/fchomo/node.js:1636 +#: htdocs/luci-static/resources/view/fchomo/node.js:1650 msgid "Health check URL" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1120 -#: htdocs/luci-static/resources/view/fchomo/node.js:1666 +#: htdocs/luci-static/resources/view/fchomo/node.js:1680 msgid "Health check expected status" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1100 -#: htdocs/luci-static/resources/view/fchomo/node.js:1646 +#: htdocs/luci-static/resources/view/fchomo/node.js:1660 msgid "Health check interval" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1107 -#: htdocs/luci-static/resources/view/fchomo/node.js:1653 +#: htdocs/luci-static/resources/view/fchomo/node.js:1667 msgid "Health check timeout" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1011 -#: htdocs/luci-static/resources/view/fchomo/node.js:1427 +#: htdocs/luci-static/resources/view/fchomo/node.js:1441 msgid "Health fields" msgstr "" @@ -1265,7 +1289,7 @@ msgstr "" msgid "Hidden" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:816 +#: htdocs/luci-static/resources/view/fchomo/node.js:819 msgid "Host that supports TLS 1.3" msgstr "" @@ -1277,7 +1301,7 @@ msgstr "" msgid "Host-key algorithms" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:459 +#: htdocs/luci-static/resources/view/fchomo/node.js:466 msgid "Host/SNI override" msgstr "" @@ -1286,8 +1310,8 @@ msgstr "" msgid "Hosts" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:150 -#: htdocs/luci-static/resources/fchomo.js:183 +#: htdocs/luci-static/resources/fchomo.js:154 +#: htdocs/luci-static/resources/fchomo.js:188 msgid "Hysteria2" msgstr "" @@ -1300,20 +1324,20 @@ msgstr "" msgid "IP CIDR" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:509 +#: htdocs/luci-static/resources/view/fchomo/node.js:518 msgid "IP override" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1307 -#: htdocs/luci-static/resources/view/fchomo/node.js:1622 +#: htdocs/luci-static/resources/view/fchomo/node.js:1321 +#: htdocs/luci-static/resources/view/fchomo/node.js:1636 msgid "IP version" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:157 +#: htdocs/luci-static/resources/fchomo.js:162 msgid "IPv4 only" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:158 +#: htdocs/luci-static/resources/fchomo.js:163 msgid "IPv6 only" msgstr "" @@ -1334,11 +1358,11 @@ msgstr "" msgid "Idle session timeout" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:469 +#: htdocs/luci-static/resources/fchomo/listeners.js:417 msgid "Idle timeout" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1362 +#: htdocs/luci-static/resources/fchomo.js:1368 msgid "If All ports is selected, uncheck others" msgstr "" @@ -1346,11 +1370,11 @@ msgstr "" msgid "If Block is selected, uncheck others" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:242 +#: htdocs/luci-static/resources/fchomo/listeners.js:186 msgid "Ignore client bandwidth" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:709 +#: htdocs/luci-static/resources/fchomo.js:715 msgid "Import" msgstr "" @@ -1366,8 +1390,8 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1713 #: htdocs/luci-static/resources/view/fchomo/client.js:1748 #: htdocs/luci-static/resources/view/fchomo/client.js:1769 -#: htdocs/luci-static/resources/view/fchomo/node.js:1332 -#: htdocs/luci-static/resources/view/fchomo/node.js:1413 +#: htdocs/luci-static/resources/view/fchomo/node.js:1346 +#: htdocs/luci-static/resources/view/fchomo/node.js:1427 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:143 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:234 msgid "Import mihomo config" @@ -1379,16 +1403,16 @@ msgstr "" msgid "Import rule-set links" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:175 +#: htdocs/luci-static/resources/fchomo/listeners.js:181 #: htdocs/luci-static/resources/view/fchomo/node.js:286 #: htdocs/luci-static/resources/view/fchomo/node.js:292 -#: htdocs/luci-static/resources/view/fchomo/node.js:1583 -#: htdocs/luci-static/resources/view/fchomo/node.js:1589 -#: htdocs/luci-static/resources/view/fchomo/server.js:231 -#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/node.js:1597 +#: htdocs/luci-static/resources/view/fchomo/node.js:1603 msgid "In Mbps." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1503 +#: htdocs/luci-static/resources/view/fchomo/node.js:1517 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:370 msgid "In bytes. %s will be used if empty." msgstr "" @@ -1400,15 +1424,15 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1108 #: htdocs/luci-static/resources/view/fchomo/client.js:1137 -#: htdocs/luci-static/resources/view/fchomo/node.js:1654 +#: htdocs/luci-static/resources/view/fchomo/node.js:1668 msgid "In millisecond. %s will be used if empty." msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:367 +#: htdocs/luci-static/resources/fchomo/listeners.js:418 +#: htdocs/luci-static/resources/fchomo/listeners.js:425 #: htdocs/luci-static/resources/view/fchomo/node.js:601 #: htdocs/luci-static/resources/view/fchomo/node.js:608 -#: htdocs/luci-static/resources/view/fchomo/server.js:417 -#: htdocs/luci-static/resources/view/fchomo/server.js:470 -#: htdocs/luci-static/resources/view/fchomo/server.js:477 msgid "In seconds." msgstr "" @@ -1417,20 +1441,22 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:430 #: htdocs/luci-static/resources/view/fchomo/global.js:515 #: htdocs/luci-static/resources/view/fchomo/node.js:280 -#: htdocs/luci-static/resources/view/fchomo/node.js:1509 -#: htdocs/luci-static/resources/view/fchomo/node.js:1647 +#: htdocs/luci-static/resources/view/fchomo/node.js:1523 +#: htdocs/luci-static/resources/view/fchomo/node.js:1661 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:376 msgid "In seconds. %s will be used if empty." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:917 -#: htdocs/luci-static/resources/view/fchomo/server.js:657 +#: htdocs/luci-static/resources/fchomo/listeners.js:645 +#: htdocs/luci-static/resources/view/fchomo/node.js:928 msgid "" "In the order of one Padding-Length and one Padding-" "Interval, infinite concatenation." msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:449 +#: htdocs/luci-static/resources/view/fchomo/inbound.js:28 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 msgid "Inbound" msgstr "" @@ -1462,7 +1488,7 @@ msgstr "" msgid "Info" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1442 +#: htdocs/luci-static/resources/view/fchomo/node.js:1456 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:272 msgid "Inline" msgstr "" @@ -1472,25 +1498,26 @@ msgid "Interface Control" msgstr "" #: htdocs/luci-static/resources/fchomo.js:49 -#: htdocs/luci-static/resources/fchomo.js:155 -#: htdocs/luci-static/resources/fchomo.js:348 +#: htdocs/luci-static/resources/fchomo.js:160 +#: htdocs/luci-static/resources/fchomo.js:354 msgid "Keep default" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:379 -#: htdocs/luci-static/resources/view/fchomo/server.js:299 +#: htdocs/luci-static/resources/fchomo/listeners.js:249 +#: htdocs/luci-static/resources/view/fchomo/node.js:385 msgid "Key" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1042 -#: htdocs/luci-static/resources/view/fchomo/server.js:831 +#: htdocs/luci-static/resources/fchomo/listeners.js:825 +#: htdocs/luci-static/resources/view/fchomo/node.js:1056 msgid "Key path" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:676 +#: htdocs/luci-static/resources/fchomo/listeners.js:664 msgid "Keypairs" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:127 #: htdocs/luci-static/resources/view/fchomo/client.js:1014 #: htdocs/luci-static/resources/view/fchomo/client.js:1257 #: htdocs/luci-static/resources/view/fchomo/client.js:1349 @@ -1498,24 +1525,23 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1719 #: htdocs/luci-static/resources/view/fchomo/client.js:1775 #: htdocs/luci-static/resources/view/fchomo/node.js:229 -#: htdocs/luci-static/resources/view/fchomo/node.js:1430 -#: htdocs/luci-static/resources/view/fchomo/node.js:1715 +#: htdocs/luci-static/resources/view/fchomo/node.js:1444 +#: htdocs/luci-static/resources/view/fchomo/node.js:1729 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:252 -#: htdocs/luci-static/resources/view/fchomo/server.js:179 msgid "Label" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1114 -#: htdocs/luci-static/resources/view/fchomo/node.js:1660 +#: htdocs/luci-static/resources/view/fchomo/node.js:1674 msgid "Lazy" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:447 -#: htdocs/luci-static/resources/view/fchomo/server.js:436 +#: htdocs/luci-static/resources/fchomo/listeners.js:386 +#: htdocs/luci-static/resources/view/fchomo/node.js:453 msgid "Legacy" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:527 +#: htdocs/luci-static/resources/fchomo/listeners.js:475 msgid "" "Legacy protocol support (VMess MD5 Authentication) is provided for " "compatibility purposes only, use of alterId > 1 is not recommended." @@ -1525,16 +1551,16 @@ msgstr "" msgid "Less compatibility and sometimes better performance." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:969 -#: htdocs/luci-static/resources/view/fchomo/server.js:812 +#: htdocs/luci-static/resources/fchomo/listeners.js:806 +#: htdocs/luci-static/resources/view/fchomo/node.js:980 msgid "List of supported application level protocols, in order of preference." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:199 +#: htdocs/luci-static/resources/fchomo/listeners.js:147 msgid "Listen address" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:176 +#: htdocs/luci-static/resources/fchomo/listeners.js:124 msgid "Listen fields" msgstr "" @@ -1542,8 +1568,8 @@ msgstr "" msgid "Listen interfaces" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:152 #: htdocs/luci-static/resources/view/fchomo/client.js:1374 -#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Listen port" msgstr "" @@ -1551,26 +1577,26 @@ msgstr "" msgid "Listen ports" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:215 +#: htdocs/luci-static/resources/fchomo.js:221 msgid "Load balance" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1440 +#: htdocs/luci-static/resources/view/fchomo/node.js:1454 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:270 msgid "Local" msgstr "" #: htdocs/luci-static/resources/view/fchomo/node.js:694 -#: htdocs/luci-static/resources/view/fchomo/node.js:734 +#: htdocs/luci-static/resources/view/fchomo/node.js:737 msgid "Local IPv6 address" msgstr "" #: htdocs/luci-static/resources/view/fchomo/node.js:686 -#: htdocs/luci-static/resources/view/fchomo/node.js:726 +#: htdocs/luci-static/resources/view/fchomo/node.js:729 msgid "Local address" msgstr "" -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:62 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:70 msgid "Log" msgstr "" @@ -1586,21 +1612,21 @@ msgstr "" msgid "Log level" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:423 +#: htdocs/luci-static/resources/fchomo.js:429 msgid "Lowercase only" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:502 #: htdocs/luci-static/resources/view/fchomo/node.js:700 -#: htdocs/luci-static/resources/view/fchomo/node.js:780 +#: htdocs/luci-static/resources/view/fchomo/node.js:783 msgid "MTU" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:185 +#: htdocs/luci-static/resources/fchomo.js:190 msgid "Masque" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:262 +#: htdocs/luci-static/resources/fchomo/listeners.js:206 msgid "Masquerade" msgstr "" @@ -1632,12 +1658,12 @@ msgstr "" msgid "Match rule set." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1186 +#: htdocs/luci-static/resources/view/fchomo/node.js:1200 msgid "Max Early Data" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:411 #: htdocs/luci-static/resources/view/fchomo/node.js:543 -#: htdocs/luci-static/resources/view/fchomo/server.js:463 msgid "Max UDP relay packet size" msgstr "" @@ -1645,8 +1671,8 @@ msgstr "" msgid "Max count of failures" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:180 #: htdocs/luci-static/resources/view/fchomo/node.js:291 -#: htdocs/luci-static/resources/view/fchomo/server.js:236 msgid "Max download speed" msgstr "" @@ -1654,40 +1680,40 @@ msgstr "" msgid "Max open streams" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:174 #: htdocs/luci-static/resources/view/fchomo/node.js:285 -#: htdocs/luci-static/resources/view/fchomo/server.js:230 msgid "Max upload speed" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1223 -#: htdocs/luci-static/resources/view/fchomo/node.js:1239 +#: htdocs/luci-static/resources/view/fchomo/node.js:1237 +#: htdocs/luci-static/resources/view/fchomo/node.js:1253 msgid "Maximum connections" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1237 +#: htdocs/luci-static/resources/view/fchomo/node.js:1251 msgid "" "Maximum multiplexed streams in a connection before opening a new connection." "
Conflict with %s and %s." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:419 -#: htdocs/luci-static/resources/view/fchomo/server.js:401 +#: htdocs/luci-static/resources/fchomo/listeners.js:351 +#: htdocs/luci-static/resources/view/fchomo/node.js:425 msgid "Maximum padding rate" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:427 -#: htdocs/luci-static/resources/view/fchomo/server.js:409 +#: htdocs/luci-static/resources/fchomo/listeners.js:359 +#: htdocs/luci-static/resources/view/fchomo/node.js:433 msgid "" "Maximum padding rate must be greater than or equal to the minimum padding " "rate." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1236 +#: htdocs/luci-static/resources/view/fchomo/node.js:1250 msgid "Maximum streams" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:143 -#: htdocs/luci-static/resources/fchomo.js:175 +#: htdocs/luci-static/resources/fchomo.js:147 +#: htdocs/luci-static/resources/fchomo.js:180 msgid "Mieru" msgstr "" @@ -1699,7 +1725,7 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/log.js:158 #: htdocs/luci-static/resources/view/fchomo/log.js:163 -#: htdocs/luci-static/resources/view/fchomo/server.js:131 +#: htdocs/luci-static/resources/view/fchomo/server.js:23 msgid "Mihomo server" msgstr "" @@ -1707,22 +1733,22 @@ msgstr "" msgid "Min of idle sessions to keep" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1230 +#: htdocs/luci-static/resources/view/fchomo/node.js:1244 msgid "" "Minimum multiplexed streams in a connection before opening a new connection." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:412 -#: htdocs/luci-static/resources/view/fchomo/server.js:394 +#: htdocs/luci-static/resources/fchomo/listeners.js:344 +#: htdocs/luci-static/resources/view/fchomo/node.js:418 msgid "Minimum padding rate" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1229 -#: htdocs/luci-static/resources/view/fchomo/node.js:1239 +#: htdocs/luci-static/resources/view/fchomo/node.js:1243 +#: htdocs/luci-static/resources/view/fchomo/node.js:1253 msgid "Minimum streams" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:141 +#: htdocs/luci-static/resources/fchomo.js:145 #: htdocs/luci-static/resources/view/fchomo/global.js:497 msgid "Mixed" msgstr "" @@ -1735,12 +1761,12 @@ msgstr "" msgid "Mixed port" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1209 +#: htdocs/luci-static/resources/view/fchomo/node.js:1223 msgid "Multiplex" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:123 #: htdocs/luci-static/resources/view/fchomo/node.js:226 -#: htdocs/luci-static/resources/view/fchomo/server.js:175 msgid "Multiplex fields" msgstr "" @@ -1753,7 +1779,11 @@ msgstr "" msgid "NOT" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1515 +#: htdocs/luci-static/resources/fchomo/listeners.js:528 +msgid "Name of the Proxy group as outbound." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1529 msgid "Name of the Proxy group to download provider." msgstr "" @@ -1761,14 +1791,22 @@ msgstr "" msgid "Name of the Proxy group to download rule set." msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:522 +msgid "Name of the Sub rule used for inbound matching." +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/node.js:527 msgid "Native UDP" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:365 +#: htdocs/luci-static/resources/fchomo.js:371 msgid "Native appearance" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:545 +msgid "Network type" +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/global.js:443 msgid "No Authentication IP ranges" msgstr "" @@ -1778,11 +1816,11 @@ msgid "No add'l params" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1115 -#: htdocs/luci-static/resources/view/fchomo/node.js:1661 +#: htdocs/luci-static/resources/view/fchomo/node.js:1675 msgid "No testing is performed when this provider node is not in use." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:670 +#: htdocs/luci-static/resources/fchomo.js:676 msgid "No valid %s found." msgstr "" @@ -1792,22 +1830,22 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1041 #: htdocs/luci-static/resources/view/fchomo/node.js:217 -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:46 msgid "Node" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1160 -#: htdocs/luci-static/resources/view/fchomo/node.js:1681 +#: htdocs/luci-static/resources/view/fchomo/node.js:1695 msgid "Node exclude filter" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1165 -#: htdocs/luci-static/resources/view/fchomo/node.js:1688 +#: htdocs/luci-static/resources/view/fchomo/node.js:1702 msgid "Node exclude type" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1155 -#: htdocs/luci-static/resources/view/fchomo/node.js:1675 +#: htdocs/luci-static/resources/view/fchomo/node.js:1689 msgid "Node filter" msgstr "" @@ -1815,7 +1853,7 @@ msgstr "" msgid "Node switch tolerance" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:392 +#: htdocs/luci-static/resources/fchomo.js:398 msgid "None" msgstr "" @@ -1823,41 +1861,41 @@ msgstr "" msgid "Not Installed" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1140 +#: htdocs/luci-static/resources/fchomo.js:1146 msgid "Not Running" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:472 +#: htdocs/luci-static/resources/view/fchomo/node.js:479 msgid "OFF" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:474 +#: htdocs/luci-static/resources/view/fchomo/node.js:481 msgid "ON" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:809 +#: htdocs/luci-static/resources/view/fchomo/node.js:812 msgid "Obfs Mode" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:198 #: htdocs/luci-static/resources/view/fchomo/node.js:303 -#: htdocs/luci-static/resources/view/fchomo/server.js:254 msgid "Obfuscate password" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:192 +#: htdocs/luci-static/resources/fchomo/listeners.js:322 #: htdocs/luci-static/resources/view/fchomo/node.js:297 -#: htdocs/luci-static/resources/view/fchomo/node.js:401 -#: htdocs/luci-static/resources/view/fchomo/server.js:248 -#: htdocs/luci-static/resources/view/fchomo/server.js:372 +#: htdocs/luci-static/resources/view/fchomo/node.js:407 msgid "Obfuscate type" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:402 -#: htdocs/luci-static/resources/view/fchomo/server.js:373 +#: htdocs/luci-static/resources/fchomo/listeners.js:323 +#: htdocs/luci-static/resources/view/fchomo/node.js:408 msgid "Obfuscated as ASCII data stream" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:403 -#: htdocs/luci-static/resources/view/fchomo/server.js:374 +#: htdocs/luci-static/resources/fchomo/listeners.js:324 +#: htdocs/luci-static/resources/view/fchomo/node.js:409 msgid "Obfuscated as low-entropy data stream" msgstr "" @@ -1869,7 +1907,7 @@ msgstr "" msgid "Only process traffic from specific interfaces. Leave empty for all." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1134 +#: htdocs/luci-static/resources/fchomo.js:1140 msgid "Open Dashboard" msgstr "" @@ -1883,11 +1921,11 @@ msgid "Override destination" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1010 -#: htdocs/luci-static/resources/view/fchomo/node.js:1426 +#: htdocs/luci-static/resources/view/fchomo/node.js:1440 msgid "Override fields" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:510 +#: htdocs/luci-static/resources/view/fchomo/node.js:519 msgid "Override the IP address of the server that DNS response." msgstr "" @@ -1903,7 +1941,7 @@ msgstr "" msgid "Override the existing ECS in original request." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1069 +#: htdocs/luci-static/resources/view/fchomo/node.js:1083 msgid "Overrides the domain name used for HTTPS record queries." msgstr "" @@ -1911,11 +1949,11 @@ msgstr "" msgid "Overview" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1150 +#: htdocs/luci-static/resources/view/fchomo/node.js:1164 msgid "POST" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1151 +#: htdocs/luci-static/resources/view/fchomo/node.js:1165 msgid "PUT" msgstr "" @@ -1923,31 +1961,31 @@ msgstr "" msgid "Packet encoding" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:507 +#: htdocs/luci-static/resources/fchomo/listeners.js:455 msgid "Padding scheme" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:915 -#: htdocs/luci-static/resources/view/fchomo/server.js:655 +#: htdocs/luci-static/resources/fchomo/listeners.js:643 +#: htdocs/luci-static/resources/view/fchomo/node.js:926 msgid "Paddings" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:165 +#: htdocs/luci-static/resources/fchomo/listeners.js:221 +#: htdocs/luci-static/resources/fchomo/listeners.js:505 #: htdocs/luci-static/resources/view/fchomo/node.js:261 #: htdocs/luci-static/resources/view/fchomo/node.js:340 -#: htdocs/luci-static/resources/view/fchomo/node.js:824 -#: htdocs/luci-static/resources/view/fchomo/server.js:221 -#: htdocs/luci-static/resources/view/fchomo/server.js:277 -#: htdocs/luci-static/resources/view/fchomo/server.js:548 +#: htdocs/luci-static/resources/view/fchomo/node.js:827 msgid "Password" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1490 +#: htdocs/luci-static/resources/fchomo/listeners.js:583 +#: htdocs/luci-static/resources/view/fchomo/node.js:1504 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:357 -#: htdocs/luci-static/resources/view/fchomo/server.js:595 msgid "Payload" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:748 +#: htdocs/luci-static/resources/view/fchomo/node.js:751 msgid "Peer pubkic key" msgstr "" @@ -1957,11 +1995,11 @@ msgid "" "it is not needed." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:775 +#: htdocs/luci-static/resources/view/fchomo/node.js:778 msgid "Periodically sends data packets to maintain connection persistence." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:774 +#: htdocs/luci-static/resources/view/fchomo/node.js:777 msgid "Persistent keepalive" msgstr "" @@ -1982,8 +2020,8 @@ msgid "" "standards." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1463 -#: htdocs/luci-static/resources/view/fchomo/node.js:1489 +#: htdocs/luci-static/resources/view/fchomo/node.js:1477 +#: htdocs/luci-static/resources/view/fchomo/node.js:1503 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:330 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:356 msgid "" @@ -1997,25 +2035,25 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1445 #: htdocs/luci-static/resources/view/fchomo/client.js:1697 #: htdocs/luci-static/resources/view/fchomo/client.js:1749 -#: htdocs/luci-static/resources/view/fchomo/node.js:1333 +#: htdocs/luci-static/resources/view/fchomo/node.js:1347 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:144 msgid "Please type %s fields of mihomo config.
" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:798 -#: htdocs/luci-static/resources/view/fchomo/server.js:534 +#: htdocs/luci-static/resources/fchomo/listeners.js:491 +#: htdocs/luci-static/resources/view/fchomo/node.js:801 msgid "Plugin" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:809 -#: htdocs/luci-static/resources/view/fchomo/node.js:816 -#: htdocs/luci-static/resources/view/fchomo/node.js:824 -#: htdocs/luci-static/resources/view/fchomo/node.js:830 -#: htdocs/luci-static/resources/view/fchomo/node.js:838 -#: htdocs/luci-static/resources/view/fchomo/node.js:844 -#: htdocs/luci-static/resources/view/fchomo/server.js:541 -#: htdocs/luci-static/resources/view/fchomo/server.js:548 -#: htdocs/luci-static/resources/view/fchomo/server.js:554 +#: htdocs/luci-static/resources/fchomo/listeners.js:498 +#: htdocs/luci-static/resources/fchomo/listeners.js:505 +#: htdocs/luci-static/resources/fchomo/listeners.js:511 +#: htdocs/luci-static/resources/view/fchomo/node.js:812 +#: htdocs/luci-static/resources/view/fchomo/node.js:819 +#: htdocs/luci-static/resources/view/fchomo/node.js:827 +#: htdocs/luci-static/resources/view/fchomo/node.js:833 +#: htdocs/luci-static/resources/view/fchomo/node.js:841 +#: htdocs/luci-static/resources/view/fchomo/node.js:847 msgid "Plugin:" msgstr "" @@ -2023,7 +2061,7 @@ msgstr "" msgid "Port" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1371 +#: htdocs/luci-static/resources/fchomo.js:1377 msgid "Port %s alrealy exists!" msgstr "" @@ -2039,21 +2077,21 @@ msgstr "" msgid "Ports" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:152 #: htdocs/luci-static/resources/view/fchomo/node.js:274 -#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Ports pool" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:487 -#: htdocs/luci-static/resources/view/fchomo/node.js:755 +#: htdocs/luci-static/resources/view/fchomo/node.js:496 +#: htdocs/luci-static/resources/view/fchomo/node.js:758 msgid "Pre-shared key" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:159 +#: htdocs/luci-static/resources/fchomo.js:164 msgid "Prefer IPv4" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:160 +#: htdocs/luci-static/resources/fchomo.js:165 msgid "Prefer IPv6" msgstr "" @@ -2064,10 +2102,10 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:736 #: htdocs/luci-static/resources/view/fchomo/global.js:753 -#: htdocs/luci-static/resources/view/fchomo/node.js:1297 -#: htdocs/luci-static/resources/view/fchomo/node.js:1303 -#: htdocs/luci-static/resources/view/fchomo/node.js:1610 -#: htdocs/luci-static/resources/view/fchomo/node.js:1617 +#: htdocs/luci-static/resources/view/fchomo/node.js:1311 +#: htdocs/luci-static/resources/view/fchomo/node.js:1317 +#: htdocs/luci-static/resources/view/fchomo/node.js:1624 +#: htdocs/luci-static/resources/view/fchomo/node.js:1631 msgid "Priority: Proxy Node > Global." msgstr "" @@ -2080,7 +2118,7 @@ msgid "Priv-key passphrase" msgstr "" #: htdocs/luci-static/resources/view/fchomo/node.js:671 -#: htdocs/luci-static/resources/view/fchomo/node.js:740 +#: htdocs/luci-static/resources/view/fchomo/node.js:743 msgid "Private key" msgstr "" @@ -2089,7 +2127,7 @@ msgid "Process matching mode" msgstr "" #: htdocs/luci-static/resources/view/fchomo/global.js:684 -#: htdocs/luci-static/resources/view/fchomo/node.js:1215 +#: htdocs/luci-static/resources/view/fchomo/node.js:1229 msgid "Protocol" msgstr "" @@ -2104,13 +2142,13 @@ msgid "" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1055 -#: htdocs/luci-static/resources/view/fchomo/node.js:1316 -#: htdocs/luci-static/resources/view/fchomo/node.js:1325 -#: htdocs/luci-static/resources/view/fchomo/node.js:1726 +#: htdocs/luci-static/resources/view/fchomo/node.js:1330 +#: htdocs/luci-static/resources/view/fchomo/node.js:1339 +#: htdocs/luci-static/resources/view/fchomo/node.js:1740 msgid "Provider" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1496 +#: htdocs/luci-static/resources/view/fchomo/node.js:1510 msgid "Provider URL" msgstr "" @@ -2133,18 +2171,19 @@ msgid "Proxy MAC-s" msgstr "" #: htdocs/luci-static/resources/view/fchomo/node.js:208 -#: htdocs/luci-static/resources/view/fchomo/node.js:1725 +#: htdocs/luci-static/resources/view/fchomo/node.js:1739 msgid "Proxy Node" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1701 -#: htdocs/luci-static/resources/view/fchomo/node.js:1710 +#: htdocs/luci-static/resources/view/fchomo/node.js:1715 +#: htdocs/luci-static/resources/view/fchomo/node.js:1724 msgid "Proxy chain" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:527 #: htdocs/luci-static/resources/view/fchomo/client.js:783 #: htdocs/luci-static/resources/view/fchomo/client.js:1543 -#: htdocs/luci-static/resources/view/fchomo/node.js:1514 +#: htdocs/luci-static/resources/view/fchomo/node.js:1528 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:381 msgid "Proxy group" msgstr "" @@ -2162,57 +2201,53 @@ msgid "Proxy routerself" msgstr "" #: htdocs/luci-static/resources/view/fchomo/node.js:528 +#: htdocs/luci-static/resources/view/fchomo/node.js:723 msgid "QUIC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:516 -#: htdocs/luci-static/resources/view/fchomo/server.js:455 -msgid "QUIC congestion controller." -msgstr "" - #: htdocs/luci-static/resources/view/fchomo/client.js:926 -#: htdocs/luci-static/resources/view/fchomo/server.js:152 +#: htdocs/luci-static/resources/view/fchomo/server.js:44 msgid "Quick Reload" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1083 -#: htdocs/luci-static/resources/view/fchomo/server.js:919 +#: htdocs/luci-static/resources/fchomo/listeners.js:913 +#: htdocs/luci-static/resources/view/fchomo/node.js:1097 msgid "REALITY" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1098 +#: htdocs/luci-static/resources/view/fchomo/node.js:1112 msgid "REALITY X25519MLKEM768 PQC support" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:956 +#: htdocs/luci-static/resources/fchomo/listeners.js:950 msgid "REALITY certificate issued to" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:924 +#: htdocs/luci-static/resources/fchomo/listeners.js:918 msgid "REALITY handshake server" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:931 +#: htdocs/luci-static/resources/fchomo/listeners.js:925 msgid "REALITY private key" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1088 -#: htdocs/luci-static/resources/view/fchomo/server.js:946 +#: htdocs/luci-static/resources/fchomo/listeners.js:940 +#: htdocs/luci-static/resources/view/fchomo/node.js:1102 msgid "REALITY public key" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1093 -#: htdocs/luci-static/resources/view/fchomo/server.js:950 +#: htdocs/luci-static/resources/fchomo/listeners.js:944 +#: htdocs/luci-static/resources/view/fchomo/node.js:1107 msgid "REALITY short ID" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/server.js:626 -#: htdocs/luci-static/resources/view/fchomo/server.js:645 +#: htdocs/luci-static/resources/fchomo/listeners.js:614 +#: htdocs/luci-static/resources/fchomo/listeners.js:633 +#: htdocs/luci-static/resources/view/fchomo/node.js:916 msgid "RTT" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:357 +#: htdocs/luci-static/resources/fchomo.js:363 msgid "Random" msgstr "" @@ -2220,7 +2255,7 @@ msgstr "" msgid "Random will be used if empty." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:367 +#: htdocs/luci-static/resources/fchomo.js:373 msgid "Randomized traffic characteristics" msgstr "" @@ -2244,10 +2279,10 @@ msgstr "" msgid "Refresh every %s seconds." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1127 +#: htdocs/luci-static/resources/fchomo.js:1133 #: htdocs/luci-static/resources/view/fchomo/client.js:927 #: htdocs/luci-static/resources/view/fchomo/global.js:193 -#: htdocs/luci-static/resources/view/fchomo/server.js:153 +#: htdocs/luci-static/resources/view/fchomo/server.js:45 msgid "Reload" msgstr "" @@ -2255,43 +2290,43 @@ msgstr "" msgid "Reload All" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1441 +#: htdocs/luci-static/resources/view/fchomo/node.js:1455 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:271 msgid "Remote" msgstr "" #: htdocs/luci-static/resources/view/fchomo/node.js:706 -#: htdocs/luci-static/resources/view/fchomo/node.js:786 +#: htdocs/luci-static/resources/view/fchomo/node.js:789 msgid "Remote DNS resolve" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1292 +#: htdocs/luci-static/resources/fchomo.js:1298 msgid "Remove" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1297 -#: htdocs/luci-static/resources/view/fchomo/node.js:1417 -#: htdocs/luci-static/resources/view/fchomo/node.js:1419 +#: htdocs/luci-static/resources/fchomo.js:1303 +#: htdocs/luci-static/resources/view/fchomo/node.js:1431 +#: htdocs/luci-static/resources/view/fchomo/node.js:1433 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:244 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:246 msgid "Remove idles" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1543 +#: htdocs/luci-static/resources/view/fchomo/node.js:1557 msgid "Replace name" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1544 +#: htdocs/luci-static/resources/view/fchomo/node.js:1558 msgid "Replace node name." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:341 +#: htdocs/luci-static/resources/fchomo.js:347 msgid "Request" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1157 -#: htdocs/luci-static/resources/view/fchomo/node.js:1164 -#: htdocs/luci-static/resources/view/fchomo/server.js:998 +#: htdocs/luci-static/resources/fchomo/listeners.js:992 +#: htdocs/luci-static/resources/view/fchomo/node.js:1171 +#: htdocs/luci-static/resources/view/fchomo/node.js:1178 msgid "Request path" msgstr "" @@ -2299,20 +2334,20 @@ msgstr "" msgid "Request timeout" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:344 +#: htdocs/luci-static/resources/fchomo.js:350 msgid "Require and verify" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:342 +#: htdocs/luci-static/resources/fchomo.js:348 msgid "Require any" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:375 -#: htdocs/luci-static/resources/view/fchomo/node.js:1099 +#: htdocs/luci-static/resources/fchomo.js:381 +#: htdocs/luci-static/resources/view/fchomo/node.js:1113 msgid "Requires server support." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:769 +#: htdocs/luci-static/resources/view/fchomo/node.js:772 msgid "Reserved field bytes" msgstr "" @@ -2320,7 +2355,7 @@ msgstr "" msgid "Resources management" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:844 +#: htdocs/luci-static/resources/view/fchomo/node.js:847 msgid "Restls script" msgstr "" @@ -2334,12 +2369,12 @@ msgid "" "Returns the string input for icon in the API to display in this proxy group." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:473 +#: htdocs/luci-static/resources/view/fchomo/node.js:480 msgid "Reuse HTTP connections to reduce RTT for each connection establishment." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:470 -#: htdocs/luci-static/resources/view/fchomo/node.js:474 +#: htdocs/luci-static/resources/view/fchomo/node.js:477 +#: htdocs/luci-static/resources/view/fchomo/node.js:481 msgid "Reusing a single tunnel to carry multiple target connections within it." msgstr "" @@ -2356,8 +2391,8 @@ msgstr "" msgid "Routing GFW" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1302 -#: htdocs/luci-static/resources/view/fchomo/node.js:1616 +#: htdocs/luci-static/resources/view/fchomo/node.js:1316 +#: htdocs/luci-static/resources/view/fchomo/node.js:1630 msgid "Routing mark" msgstr "" @@ -2409,7 +2444,7 @@ msgstr "" msgid "Rule set URL" msgstr "" -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:46 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 msgid "Ruleset" msgstr "" @@ -2417,27 +2452,27 @@ msgstr "" msgid "Ruleset-URI-Scheme" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1140 +#: htdocs/luci-static/resources/fchomo.js:1146 msgid "Running" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:223 +#: htdocs/luci-static/resources/fchomo.js:229 msgid "SMTP" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:140 +#: htdocs/luci-static/resources/fchomo.js:144 msgid "SOCKS" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:172 +#: htdocs/luci-static/resources/fchomo.js:177 msgid "SOCKS5" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:187 +#: htdocs/luci-static/resources/fchomo.js:193 msgid "SSH" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:224 +#: htdocs/luci-static/resources/fchomo.js:230 msgid "STUN" msgstr "" @@ -2445,20 +2480,20 @@ msgstr "" msgid "SUB-RULE" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:862 +#: htdocs/luci-static/resources/view/fchomo/node.js:873 msgid "SUoT version" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:194 #: htdocs/luci-static/resources/view/fchomo/node.js:299 -#: htdocs/luci-static/resources/view/fchomo/server.js:250 msgid "Salamander" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:165 +#: htdocs/luci-static/resources/fchomo.js:170 msgid "Same dstaddr requests. Same node" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:166 +#: htdocs/luci-static/resources/fchomo.js:171 msgid "Same srcaddr and dstaddr requests. Same node" msgstr "" @@ -2466,7 +2501,7 @@ msgstr "" msgid "Segment maximum size" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:212 +#: htdocs/luci-static/resources/fchomo.js:218 msgid "Select" msgstr "" @@ -2474,20 +2509,20 @@ msgstr "" msgid "Select Dashboard" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:381 +#: htdocs/luci-static/resources/fchomo.js:387 msgid "Send padding randomly 0-3333 bytes with 50% probability." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:370 -#: htdocs/luci-static/resources/fchomo.js:371 +#: htdocs/luci-static/resources/fchomo.js:376 +#: htdocs/luci-static/resources/fchomo.js:377 msgid "Send random ticket of 300s-600s duration for client 0-RTT reuse." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:166 -#: htdocs/luci-static/resources/view/fchomo/server.js:626 -#: htdocs/luci-static/resources/view/fchomo/server.js:817 -#: htdocs/luci-static/resources/view/fchomo/server.js:832 -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 +#: htdocs/luci-static/resources/fchomo/listeners.js:614 +#: htdocs/luci-static/resources/fchomo/listeners.js:811 +#: htdocs/luci-static/resources/fchomo/listeners.js:826 +#: htdocs/luci-static/resources/view/fchomo/server.js:58 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:62 msgid "Server" msgstr "" @@ -2495,7 +2530,7 @@ msgstr "" msgid "Server address" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1142 +#: htdocs/luci-static/resources/view/fchomo/node.js:1156 msgid "Server hostname" msgstr "" @@ -2507,27 +2542,27 @@ msgstr "" msgid "Service status" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:142 -#: htdocs/luci-static/resources/fchomo.js:173 +#: htdocs/luci-static/resources/fchomo.js:146 +#: htdocs/luci-static/resources/fchomo.js:178 msgid "Shadowsocks" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:437 #: htdocs/luci-static/resources/view/fchomo/node.js:582 -#: htdocs/luci-static/resources/view/fchomo/server.js:489 msgid "Shadowsocks chipher" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:432 #: htdocs/luci-static/resources/view/fchomo/node.js:577 -#: htdocs/luci-static/resources/view/fchomo/server.js:484 msgid "Shadowsocks encrypt" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:445 #: htdocs/luci-static/resources/view/fchomo/node.js:590 -#: htdocs/luci-static/resources/view/fchomo/server.js:497 msgid "Shadowsocks password" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1257 +#: htdocs/luci-static/resources/view/fchomo/node.js:1271 msgid "Show connections in the dashboard for breaking connections easier." msgstr "" @@ -2535,18 +2570,18 @@ msgstr "" msgid "Silent" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:164 +#: htdocs/luci-static/resources/fchomo.js:169 msgid "Simple round-robin all nodes" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1502 +#: htdocs/luci-static/resources/view/fchomo/node.js:1516 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:369 msgid "Size limit" msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1576 -#: htdocs/luci-static/resources/view/fchomo/node.js:1020 -#: htdocs/luci-static/resources/view/fchomo/node.js:1594 +#: htdocs/luci-static/resources/view/fchomo/node.js:1034 +#: htdocs/luci-static/resources/view/fchomo/node.js:1608 msgid "Skip cert verify" msgstr "" @@ -2562,7 +2597,7 @@ msgstr "" msgid "Skiped sniffing src address" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:177 +#: htdocs/luci-static/resources/fchomo.js:182 msgid "Snell" msgstr "" @@ -2578,7 +2613,7 @@ msgstr "" msgid "Sniffer settings" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:413 +#: htdocs/luci-static/resources/fchomo.js:419 msgid "Specify a ID" msgstr "" @@ -2593,11 +2628,11 @@ msgstr "" msgid "Stack" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:227 +#: htdocs/luci-static/resources/fchomo.js:233 msgid "Steam Client" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:228 +#: htdocs/luci-static/resources/fchomo.js:234 msgid "Steam P2P" msgstr "" @@ -2606,6 +2641,7 @@ msgstr "" msgid "Strategy" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:521 #: htdocs/luci-static/resources/view/fchomo/client.js:1303 #: htdocs/luci-static/resources/view/fchomo/client.js:1312 msgid "Sub rule" @@ -2615,7 +2651,7 @@ msgstr "" msgid "Sub rule group" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:673 +#: htdocs/luci-static/resources/fchomo.js:679 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:215 msgid "Successfully imported %s %s of total %s." msgstr "" @@ -2624,12 +2660,12 @@ msgstr "" msgid "Successfully updated." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1642 +#: htdocs/luci-static/resources/fchomo.js:1648 msgid "Successfully uploaded." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:144 -#: htdocs/luci-static/resources/fchomo.js:176 +#: htdocs/luci-static/resources/fchomo.js:148 +#: htdocs/luci-static/resources/fchomo.js:181 msgid "Sudoku" msgstr "" @@ -2647,20 +2683,21 @@ msgstr "" msgid "System DNS" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:139 -#: htdocs/luci-static/resources/fchomo.js:144 -#: htdocs/luci-static/resources/fchomo.js:145 -#: htdocs/luci-static/resources/fchomo.js:146 -#: htdocs/luci-static/resources/fchomo.js:147 +#: htdocs/luci-static/resources/fchomo.js:143 #: htdocs/luci-static/resources/fchomo.js:148 -#: htdocs/luci-static/resources/fchomo.js:171 +#: htdocs/luci-static/resources/fchomo.js:149 +#: htdocs/luci-static/resources/fchomo.js:150 +#: htdocs/luci-static/resources/fchomo.js:151 +#: htdocs/luci-static/resources/fchomo.js:152 #: htdocs/luci-static/resources/fchomo.js:176 -#: htdocs/luci-static/resources/fchomo.js:177 -#: htdocs/luci-static/resources/fchomo.js:178 -#: htdocs/luci-static/resources/fchomo.js:179 -#: htdocs/luci-static/resources/fchomo.js:180 #: htdocs/luci-static/resources/fchomo.js:181 -#: htdocs/luci-static/resources/fchomo.js:187 +#: htdocs/luci-static/resources/fchomo.js:182 +#: htdocs/luci-static/resources/fchomo.js:183 +#: htdocs/luci-static/resources/fchomo.js:184 +#: htdocs/luci-static/resources/fchomo.js:185 +#: htdocs/luci-static/resources/fchomo.js:186 +#: htdocs/luci-static/resources/fchomo.js:193 +#: htdocs/luci-static/resources/fchomo/listeners.js:546 #: htdocs/luci-static/resources/view/fchomo/client.js:589 #: htdocs/luci-static/resources/view/fchomo/client.js:679 msgid "TCP" @@ -2670,7 +2707,7 @@ msgstr "" msgid "TCP concurrency" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1250 +#: htdocs/luci-static/resources/view/fchomo/node.js:1264 msgid "TCP only" msgstr "" @@ -2682,54 +2719,61 @@ msgstr "" msgid "TCP-Keep-Alive interval" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:140 -#: htdocs/luci-static/resources/fchomo.js:141 -#: htdocs/luci-static/resources/fchomo.js:142 -#: htdocs/luci-static/resources/fchomo.js:143 -#: htdocs/luci-static/resources/fchomo.js:170 -#: htdocs/luci-static/resources/fchomo.js:172 -#: htdocs/luci-static/resources/fchomo.js:173 +#: htdocs/luci-static/resources/fchomo.js:144 +#: htdocs/luci-static/resources/fchomo.js:145 +#: htdocs/luci-static/resources/fchomo.js:146 +#: htdocs/luci-static/resources/fchomo.js:147 +#: htdocs/luci-static/resources/fchomo.js:155 +#: htdocs/luci-static/resources/fchomo.js:156 #: htdocs/luci-static/resources/fchomo.js:175 +#: htdocs/luci-static/resources/fchomo.js:177 +#: htdocs/luci-static/resources/fchomo.js:178 +#: htdocs/luci-static/resources/fchomo.js:180 +#: htdocs/luci-static/resources/fchomo.js:191 msgid "TCP/UDP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1281 -#: htdocs/luci-static/resources/view/fchomo/node.js:1561 +#: htdocs/luci-static/resources/view/fchomo/node.js:1295 +#: htdocs/luci-static/resources/view/fchomo/node.js:1575 msgid "TFO" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:768 #: htdocs/luci-static/resources/view/fchomo/global.js:529 -#: htdocs/luci-static/resources/view/fchomo/node.js:454 -#: htdocs/luci-static/resources/view/fchomo/node.js:811 -#: htdocs/luci-static/resources/view/fchomo/node.js:937 -#: htdocs/luci-static/resources/view/fchomo/server.js:780 +#: htdocs/luci-static/resources/view/fchomo/node.js:461 +#: htdocs/luci-static/resources/view/fchomo/node.js:814 +#: htdocs/luci-static/resources/view/fchomo/node.js:948 msgid "TLS" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:968 -#: htdocs/luci-static/resources/view/fchomo/server.js:811 +#: htdocs/luci-static/resources/fchomo/listeners.js:805 +#: htdocs/luci-static/resources/view/fchomo/node.js:979 msgid "TLS ALPN" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:962 +#: htdocs/luci-static/resources/view/fchomo/node.js:973 msgid "TLS SNI" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:121 #: htdocs/luci-static/resources/view/fchomo/node.js:224 -#: htdocs/luci-static/resources/view/fchomo/server.js:173 msgid "TLS fields" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:149 -#: htdocs/luci-static/resources/fchomo.js:184 +#: htdocs/luci-static/resources/fchomo.js:153 +#: htdocs/luci-static/resources/fchomo.js:189 msgid "TUIC" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:225 +#: htdocs/luci-static/resources/fchomo.js:231 msgid "TURN" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:243 +#: htdocs/luci-static/resources/fchomo/listeners.js:484 +msgid "Target address" +msgstr "" + +#: htdocs/luci-static/resources/fchomo/listeners.js:187 msgid "" "Tell the client to use the BBR flow control algorithm instead of Hysteria CC." msgstr "" @@ -2739,34 +2783,34 @@ msgstr "" msgid "The %s address used by local machine in the Cloudflare WARP network." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:727 -#: htdocs/luci-static/resources/view/fchomo/node.js:735 +#: htdocs/luci-static/resources/view/fchomo/node.js:730 +#: htdocs/luci-static/resources/view/fchomo/node.js:738 msgid "The %s address used by local machine in the Wireguard network." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1043 -#: htdocs/luci-static/resources/view/fchomo/server.js:832 +#: htdocs/luci-static/resources/fchomo/listeners.js:826 +#: htdocs/luci-static/resources/view/fchomo/node.js:1057 msgid "The %s private key, in PEM format." msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:811 +#: htdocs/luci-static/resources/fchomo/listeners.js:849 #: htdocs/luci-static/resources/view/fchomo/global.js:550 -#: htdocs/luci-static/resources/view/fchomo/node.js:1029 -#: htdocs/luci-static/resources/view/fchomo/server.js:817 -#: htdocs/luci-static/resources/view/fchomo/server.js:855 +#: htdocs/luci-static/resources/view/fchomo/node.js:1043 msgid "The %s public key, in PEM format." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1063 +#: htdocs/luci-static/resources/view/fchomo/node.js:1077 msgid "" "The ECH parameter of the HTTPS record for the domain. Leave empty to resolve " "via DNS." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:380 +#: htdocs/luci-static/resources/view/fchomo/node.js:386 msgid "The ED25519 available private key or UUID provided by Sudoku server." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:300 +#: htdocs/luci-static/resources/fchomo/listeners.js:250 msgid "The ED25519 master public key or UUID generated by Sudoku." msgstr "" @@ -2774,8 +2818,8 @@ msgstr "" msgid "The default value is 2:00 every day." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:918 -#: htdocs/luci-static/resources/view/fchomo/server.js:658 +#: htdocs/luci-static/resources/fchomo/listeners.js:646 +#: htdocs/luci-static/resources/view/fchomo/node.js:929 msgid "" "The first padding must have a probability of 100% and at least 35 bytes." msgstr "" @@ -2790,19 +2834,19 @@ msgstr "" msgid "The matching %s will be deemed as poisoned." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:916 -#: htdocs/luci-static/resources/view/fchomo/server.js:656 +#: htdocs/luci-static/resources/fchomo/listeners.js:644 +#: htdocs/luci-static/resources/view/fchomo/node.js:927 msgid "The server and client can set different padding parameters." msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:907 #: htdocs/luci-static/resources/view/fchomo/global.js:594 -#: htdocs/luci-static/resources/view/fchomo/server.js:913 msgid "This ECH parameter needs to be added to the HTTPS record of the domain." msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1579 -#: htdocs/luci-static/resources/view/fchomo/node.js:1023 -#: htdocs/luci-static/resources/view/fchomo/node.js:1597 +#: htdocs/luci-static/resources/view/fchomo/node.js:1037 +#: htdocs/luci-static/resources/view/fchomo/node.js:1611 msgid "" "This is DANGEROUS, your traffic is almost like " "PLAIN TEXT! Use at your own risk!" @@ -2837,28 +2881,33 @@ msgstr "" msgid "Tproxy port" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1753 +#: htdocs/luci-static/resources/fchomo/listeners.js:238 +#: htdocs/luci-static/resources/view/fchomo/node.js:378 +msgid "Traffic pattern" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1767 msgid "Transit proxy group" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1759 +#: htdocs/luci-static/resources/view/fchomo/node.js:1773 msgid "Transit proxy node" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:231 +#: htdocs/luci-static/resources/fchomo/listeners.js:958 #: htdocs/luci-static/resources/view/fchomo/node.js:355 -#: htdocs/luci-static/resources/view/fchomo/node.js:1105 -#: htdocs/luci-static/resources/view/fchomo/server.js:287 -#: htdocs/luci-static/resources/view/fchomo/server.js:964 +#: htdocs/luci-static/resources/view/fchomo/node.js:1119 msgid "Transport" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:122 #: htdocs/luci-static/resources/view/fchomo/node.js:225 -#: htdocs/luci-static/resources/view/fchomo/server.js:174 msgid "Transport fields" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1110 -#: htdocs/luci-static/resources/view/fchomo/server.js:969 +#: htdocs/luci-static/resources/fchomo/listeners.js:963 +#: htdocs/luci-static/resources/view/fchomo/node.js:1124 msgid "Transport type" msgstr "" @@ -2866,11 +2915,16 @@ msgstr "" msgid "Treat the destination IP as the source IP." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:147 -#: htdocs/luci-static/resources/fchomo.js:180 +#: htdocs/luci-static/resources/fchomo.js:151 +#: htdocs/luci-static/resources/fchomo.js:185 msgid "Trojan" msgstr "" +#: htdocs/luci-static/resources/fchomo.js:155 +#: htdocs/luci-static/resources/fchomo.js:191 +msgid "TrustTunnel" +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/global.js:764 msgid "Tun Fwmark/fwmask" msgstr "" @@ -2887,30 +2941,35 @@ msgstr "" msgid "Tun stack." msgstr "" +#: htdocs/luci-static/resources/fchomo.js:156 +msgid "Tunnel" +msgstr "" + +#: htdocs/luci-static/resources/fchomo/listeners.js:141 #: htdocs/luci-static/resources/view/fchomo/client.js:530 #: htdocs/luci-static/resources/view/fchomo/client.js:643 #: htdocs/luci-static/resources/view/fchomo/client.js:737 #: htdocs/luci-static/resources/view/fchomo/client.js:842 #: htdocs/luci-static/resources/view/fchomo/client.js:1028 #: htdocs/luci-static/resources/view/fchomo/node.js:238 -#: htdocs/luci-static/resources/view/fchomo/node.js:1439 -#: htdocs/luci-static/resources/view/fchomo/node.js:1724 +#: htdocs/luci-static/resources/view/fchomo/node.js:1453 +#: htdocs/luci-static/resources/view/fchomo/node.js:1738 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:269 -#: htdocs/luci-static/resources/view/fchomo/server.js:193 msgid "Type" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:149 -#: htdocs/luci-static/resources/fchomo.js:150 -#: htdocs/luci-static/resources/fchomo.js:183 -#: htdocs/luci-static/resources/fchomo.js:184 -#: htdocs/luci-static/resources/fchomo.js:185 -#: htdocs/luci-static/resources/fchomo.js:186 +#: htdocs/luci-static/resources/fchomo.js:153 +#: htdocs/luci-static/resources/fchomo.js:154 +#: htdocs/luci-static/resources/fchomo.js:188 +#: htdocs/luci-static/resources/fchomo.js:189 +#: htdocs/luci-static/resources/fchomo.js:190 +#: htdocs/luci-static/resources/fchomo.js:192 +#: htdocs/luci-static/resources/fchomo/listeners.js:547 +#: htdocs/luci-static/resources/fchomo/listeners.js:551 #: htdocs/luci-static/resources/view/fchomo/client.js:588 #: htdocs/luci-static/resources/view/fchomo/client.js:678 -#: htdocs/luci-static/resources/view/fchomo/node.js:851 -#: htdocs/luci-static/resources/view/fchomo/node.js:1571 -#: htdocs/luci-static/resources/view/fchomo/server.js:563 +#: htdocs/luci-static/resources/view/fchomo/node.js:862 +#: htdocs/luci-static/resources/view/fchomo/node.js:1585 msgid "UDP" msgstr "" @@ -2934,19 +2993,19 @@ msgstr "" msgid "UDP relay mode" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:214 +#: htdocs/luci-static/resources/fchomo.js:220 msgid "URL test" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:503 +#: htdocs/luci-static/resources/fchomo/listeners.js:247 +#: htdocs/luci-static/resources/fchomo/listeners.js:405 +#: htdocs/luci-static/resources/fchomo/listeners.js:460 +#: htdocs/luci-static/resources/view/fchomo/node.js:512 #: htdocs/luci-static/resources/view/fchomo/node.js:621 -#: htdocs/luci-static/resources/view/fchomo/server.js:297 -#: htdocs/luci-static/resources/view/fchomo/server.js:448 -#: htdocs/luci-static/resources/view/fchomo/server.js:512 msgid "UUID" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1185 +#: htdocs/luci-static/resources/fchomo.js:1191 msgid "Unable to download unsupported type: %s" msgstr "" @@ -2971,8 +3030,8 @@ msgstr "" msgid "Unknown error: %s" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:856 -#: htdocs/luci-static/resources/view/fchomo/node.js:1576 +#: htdocs/luci-static/resources/view/fchomo/node.js:867 +#: htdocs/luci-static/resources/view/fchomo/node.js:1590 msgid "UoT" msgstr "" @@ -2980,22 +3039,22 @@ msgstr "" msgid "Update failed." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1508 +#: htdocs/luci-static/resources/view/fchomo/node.js:1522 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:375 msgid "Update interval" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1268 +#: htdocs/luci-static/resources/view/fchomo/node.js:1282 msgid "Upload bandwidth" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1269 +#: htdocs/luci-static/resources/view/fchomo/node.js:1283 msgid "Upload bandwidth in Mbps." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1034 -#: htdocs/luci-static/resources/view/fchomo/server.js:823 -#: htdocs/luci-static/resources/view/fchomo/server.js:863 +#: htdocs/luci-static/resources/fchomo/listeners.js:817 +#: htdocs/luci-static/resources/fchomo/listeners.js:857 +#: htdocs/luci-static/resources/view/fchomo/node.js:1048 msgid "Upload certificate" msgstr "" @@ -3003,17 +3062,17 @@ msgstr "" msgid "Upload initial package" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1048 -#: htdocs/luci-static/resources/view/fchomo/server.js:838 +#: htdocs/luci-static/resources/fchomo/listeners.js:832 +#: htdocs/luci-static/resources/view/fchomo/node.js:1062 msgid "Upload key" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:820 +#: htdocs/luci-static/resources/fchomo/listeners.js:835 +#: htdocs/luci-static/resources/fchomo/listeners.js:860 #: htdocs/luci-static/resources/view/fchomo/global.js:306 -#: htdocs/luci-static/resources/view/fchomo/node.js:1037 #: htdocs/luci-static/resources/view/fchomo/node.js:1051 -#: htdocs/luci-static/resources/view/fchomo/server.js:826 -#: htdocs/luci-static/resources/view/fchomo/server.js:841 -#: htdocs/luci-static/resources/view/fchomo/server.js:866 +#: htdocs/luci-static/resources/view/fchomo/node.js:1065 msgid "Upload..." msgstr "" @@ -3037,7 +3096,7 @@ msgstr "" msgid "Used to resolve the domain of the Proxy node." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:963 +#: htdocs/luci-static/resources/view/fchomo/node.js:974 msgid "Used to verify the hostname on the returned certificates." msgstr "" @@ -3045,8 +3104,8 @@ msgstr "" msgid "User Authentication" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:160 #: htdocs/luci-static/resources/view/fchomo/node.js:256 -#: htdocs/luci-static/resources/view/fchomo/server.js:216 msgid "Username" msgstr "" @@ -3054,50 +3113,50 @@ msgstr "" msgid "Users filter mode" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1198 +#: htdocs/luci-static/resources/view/fchomo/node.js:1212 msgid "V2ray HTTPUpgrade" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1203 +#: htdocs/luci-static/resources/view/fchomo/node.js:1217 msgid "V2ray HTTPUpgrade fast open" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:146 -#: htdocs/luci-static/resources/fchomo.js:179 +#: htdocs/luci-static/resources/fchomo.js:150 +#: htdocs/luci-static/resources/fchomo.js:184 msgid "VLESS" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:145 -#: htdocs/luci-static/resources/fchomo.js:178 +#: htdocs/luci-static/resources/fchomo.js:149 +#: htdocs/luci-static/resources/fchomo.js:183 msgid "VMess" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1445 -#: htdocs/luci-static/resources/view/fchomo/node.js:1730 +#: htdocs/luci-static/resources/view/fchomo/node.js:1459 +#: htdocs/luci-static/resources/view/fchomo/node.js:1744 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:312 msgid "Value" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:343 +#: htdocs/luci-static/resources/fchomo.js:349 msgid "Verify if given" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:494 -#: htdocs/luci-static/resources/view/fchomo/node.js:830 -#: htdocs/luci-static/resources/view/fchomo/server.js:554 +#: htdocs/luci-static/resources/fchomo/listeners.js:511 +#: htdocs/luci-static/resources/view/fchomo/node.js:503 +#: htdocs/luci-static/resources/view/fchomo/node.js:833 msgid "Version" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:838 +#: htdocs/luci-static/resources/view/fchomo/node.js:841 msgid "Version hint" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:120 #: htdocs/luci-static/resources/view/fchomo/node.js:223 -#: htdocs/luci-static/resources/view/fchomo/server.js:172 msgid "Vless Encryption fields" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:380 +#: htdocs/luci-static/resources/fchomo.js:386 msgid "Wait a random 0-111 milliseconds with 75% probability." msgstr "" @@ -3105,16 +3164,19 @@ msgstr "" msgid "Warning" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1115 -#: htdocs/luci-static/resources/view/fchomo/node.js:1126 -#: htdocs/luci-static/resources/view/fchomo/node.js:1131 -#: htdocs/luci-static/resources/view/fchomo/server.js:971 -#: htdocs/luci-static/resources/view/fchomo/server.js:982 -#: htdocs/luci-static/resources/view/fchomo/server.js:987 +#: htdocs/luci-static/resources/fchomo/listeners.js:390 +#: htdocs/luci-static/resources/fchomo/listeners.js:965 +#: htdocs/luci-static/resources/fchomo/listeners.js:976 +#: htdocs/luci-static/resources/fchomo/listeners.js:981 +#: htdocs/luci-static/resources/view/fchomo/node.js:457 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +#: htdocs/luci-static/resources/view/fchomo/node.js:1129 +#: htdocs/luci-static/resources/view/fchomo/node.js:1140 +#: htdocs/luci-static/resources/view/fchomo/node.js:1145 msgid "WebSocket" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:132 +#: htdocs/luci-static/resources/view/fchomo/server.js:24 msgid "When used as a server, HomeProxy is a better choice." msgstr "" @@ -3122,23 +3184,23 @@ msgstr "" msgid "White list" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:186 +#: htdocs/luci-static/resources/fchomo.js:192 msgid "WireGuard" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:749 +#: htdocs/luci-static/resources/view/fchomo/node.js:752 msgid "WireGuard peer public key." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:756 +#: htdocs/luci-static/resources/view/fchomo/node.js:759 msgid "WireGuard pre-shared key." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:741 +#: htdocs/luci-static/resources/view/fchomo/node.js:744 msgid "WireGuard requires base64-encoded private keys." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:617 +#: htdocs/luci-static/resources/fchomo/listeners.js:605 msgid "XOR mode" msgstr "" @@ -3154,23 +3216,23 @@ msgstr "" msgid "YouTube" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1624 +#: htdocs/luci-static/resources/fchomo.js:1630 msgid "Your %s was successfully uploaded. Size: %sB." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:316 -#: htdocs/luci-static/resources/fchomo.js:329 -#: htdocs/luci-static/resources/fchomo.js:334 +#: htdocs/luci-static/resources/fchomo.js:322 +#: htdocs/luci-static/resources/fchomo.js:335 +#: htdocs/luci-static/resources/fchomo.js:340 #: htdocs/luci-static/resources/view/fchomo/node.js:646 msgid "aes-128-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:317 +#: htdocs/luci-static/resources/fchomo.js:323 msgid "aes-192-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:318 -#: htdocs/luci-static/resources/fchomo.js:335 +#: htdocs/luci-static/resources/fchomo.js:324 +#: htdocs/luci-static/resources/fchomo.js:341 msgid "aes-256-gcm" msgstr "" @@ -3182,15 +3244,15 @@ msgstr "" msgid "bbr" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1039 -#: htdocs/luci-static/resources/view/fchomo/server.js:828 -#: htdocs/luci-static/resources/view/fchomo/server.js:868 +#: htdocs/luci-static/resources/fchomo/listeners.js:822 +#: htdocs/luci-static/resources/fchomo/listeners.js:862 +#: htdocs/luci-static/resources/view/fchomo/node.js:1053 msgid "certificate" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:319 -#: htdocs/luci-static/resources/fchomo.js:330 +#: htdocs/luci-static/resources/fchomo.js:325 #: htdocs/luci-static/resources/fchomo.js:336 +#: htdocs/luci-static/resources/fchomo.js:342 msgid "chacha20-ietf-poly1305" msgstr "" @@ -3202,8 +3264,8 @@ msgstr "" msgid "cubic" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:569 -#: htdocs/luci-static/resources/view/fchomo/server.js:600 +#: htdocs/luci-static/resources/fchomo/listeners.js:557 +#: htdocs/luci-static/resources/fchomo/listeners.js:588 msgid "decryption" msgstr "" @@ -3211,36 +3273,36 @@ msgstr "" msgid "dnsmasq selects upstream on its own. (may affect CDN accuracy)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1588 +#: htdocs/luci-static/resources/view/fchomo/node.js:1602 msgid "down" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:870 -#: htdocs/luci-static/resources/view/fchomo/node.js:893 -#: htdocs/luci-static/resources/view/fchomo/server.js:604 +#: htdocs/luci-static/resources/fchomo/listeners.js:592 +#: htdocs/luci-static/resources/view/fchomo/node.js:881 +#: htdocs/luci-static/resources/view/fchomo/node.js:904 msgid "encryption" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:435 -#: htdocs/luci-static/resources/view/fchomo/server.js:424 +#: htdocs/luci-static/resources/fchomo/listeners.js:374 +#: htdocs/luci-static/resources/view/fchomo/node.js:441 msgid "false = bandwidth optimized downlink; true = pure Sudoku downlink." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1114 -#: htdocs/luci-static/resources/view/fchomo/node.js:1125 -#: htdocs/luci-static/resources/view/fchomo/node.js:1130 -#: htdocs/luci-static/resources/view/fchomo/server.js:970 -#: htdocs/luci-static/resources/view/fchomo/server.js:981 -#: htdocs/luci-static/resources/view/fchomo/server.js:986 +#: htdocs/luci-static/resources/fchomo/listeners.js:964 +#: htdocs/luci-static/resources/fchomo/listeners.js:975 +#: htdocs/luci-static/resources/fchomo/listeners.js:980 +#: htdocs/luci-static/resources/view/fchomo/node.js:1128 +#: htdocs/luci-static/resources/view/fchomo/node.js:1139 +#: htdocs/luci-static/resources/view/fchomo/node.js:1144 msgid "gRPC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1181 +#: htdocs/luci-static/resources/view/fchomo/node.js:1195 msgid "gRPC User-Agent" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1177 -#: htdocs/luci-static/resources/view/fchomo/server.js:1005 +#: htdocs/luci-static/resources/fchomo/listeners.js:999 +#: htdocs/luci-static/resources/view/fchomo/node.js:1191 msgid "gRPC service name" msgstr "" @@ -3248,11 +3310,11 @@ msgstr "" msgid "gVisor" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1219 +#: htdocs/luci-static/resources/view/fchomo/node.js:1233 msgid "h2mux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:769 +#: htdocs/luci-static/resources/fchomo/listeners.js:757 msgid "least one keypair required" msgstr "" @@ -3266,17 +3328,17 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1480 #: htdocs/luci-static/resources/view/fchomo/client.js:1711 #: htdocs/luci-static/resources/view/fchomo/client.js:1767 -#: htdocs/luci-static/resources/view/fchomo/node.js:1411 +#: htdocs/luci-static/resources/view/fchomo/node.js:1425 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:232 msgid "mihomo config" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:362 +#: htdocs/luci-static/resources/fchomo.js:368 msgid "mlkem768x25519plus" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1285 -#: htdocs/luci-static/resources/view/fchomo/node.js:1566 +#: htdocs/luci-static/resources/view/fchomo/node.js:1299 +#: htdocs/luci-static/resources/view/fchomo/node.js:1580 msgid "mpTCP" msgstr "" @@ -3288,21 +3350,21 @@ msgstr "" msgid "no-resolve" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1359 -#: htdocs/luci-static/resources/fchomo.js:1454 -#: htdocs/luci-static/resources/fchomo.js:1489 -#: htdocs/luci-static/resources/fchomo.js:1517 +#: htdocs/luci-static/resources/fchomo.js:1365 +#: htdocs/luci-static/resources/fchomo.js:1460 +#: htdocs/luci-static/resources/fchomo.js:1495 +#: htdocs/luci-static/resources/fchomo.js:1523 msgid "non-empty value" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:314 -#: htdocs/luci-static/resources/fchomo.js:328 -#: htdocs/luci-static/resources/fchomo.js:340 +#: htdocs/luci-static/resources/fchomo.js:320 +#: htdocs/luci-static/resources/fchomo.js:334 +#: htdocs/luci-static/resources/fchomo.js:346 +#: htdocs/luci-static/resources/fchomo/listeners.js:492 #: htdocs/luci-static/resources/view/fchomo/node.js:644 #: htdocs/luci-static/resources/view/fchomo/node.js:664 -#: htdocs/luci-static/resources/view/fchomo/node.js:799 +#: htdocs/luci-static/resources/view/fchomo/node.js:802 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:308 -#: htdocs/luci-static/resources/view/fchomo/server.js:535 msgid "none" msgstr "" @@ -3314,19 +3376,25 @@ msgstr "" msgid "not included \",\"" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:200 +#: htdocs/luci-static/resources/fchomo.js:206 +#: htdocs/luci-static/resources/fchomo/listeners.js:523 +#: htdocs/luci-static/resources/fchomo/listeners.js:524 msgid "null" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:800 +#: htdocs/luci-static/resources/view/fchomo/node.js:803 msgid "obfs-simple" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:479 -msgid "only applies when %s is stream/poll/auto." +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +msgid "only applies when %s is %s." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1546 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +msgid "only applies when %s is not %s." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1560 msgid "override.proxy-name" msgstr "" @@ -3334,13 +3402,13 @@ msgstr "" msgid "packet addr (v2ray-core v5+)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:449 -#: htdocs/luci-static/resources/view/fchomo/server.js:438 +#: htdocs/luci-static/resources/fchomo/listeners.js:388 +#: htdocs/luci-static/resources/view/fchomo/node.js:455 msgid "poll" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1053 -#: htdocs/luci-static/resources/view/fchomo/server.js:843 +#: htdocs/luci-static/resources/fchomo/listeners.js:837 +#: htdocs/luci-static/resources/view/fchomo/node.js:1067 msgid "private key" msgstr "" @@ -3353,7 +3421,7 @@ msgstr "" msgid "requires front-end adaptation using the API." msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:804 +#: htdocs/luci-static/resources/view/fchomo/node.js:807 msgid "restls" msgstr "" @@ -3361,17 +3429,17 @@ msgstr "" msgid "rule-set" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:803 -#: htdocs/luci-static/resources/view/fchomo/server.js:536 +#: htdocs/luci-static/resources/fchomo/listeners.js:493 +#: htdocs/luci-static/resources/view/fchomo/node.js:806 msgid "shadow-tls" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1217 +#: htdocs/luci-static/resources/view/fchomo/node.js:1231 msgid "smux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:448 -#: htdocs/luci-static/resources/view/fchomo/server.js:437 +#: htdocs/luci-static/resources/fchomo/listeners.js:387 +#: htdocs/luci-static/resources/view/fchomo/node.js:454 msgid "split-stream" msgstr "" @@ -3379,7 +3447,11 @@ msgstr "" msgid "src" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:296 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +msgid "stream/poll/auto" +msgstr "" + +#: htdocs/luci-static/resources/fchomo/listeners.js:246 msgid "sudoku-keypair" msgstr "" @@ -3387,87 +3459,87 @@ msgstr "" msgid "unchecked" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:426 +#: htdocs/luci-static/resources/fchomo.js:432 msgid "unique UCI identifier" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:429 +#: htdocs/luci-static/resources/fchomo.js:435 msgid "unique identifier" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1526 +#: htdocs/luci-static/resources/fchomo.js:1532 msgid "unique value" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1582 +#: htdocs/luci-static/resources/view/fchomo/node.js:1596 msgid "up" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:495 +#: htdocs/luci-static/resources/fchomo/listeners.js:512 +#: htdocs/luci-static/resources/view/fchomo/node.js:504 #: htdocs/luci-static/resources/view/fchomo/node.js:539 -#: htdocs/luci-static/resources/view/fchomo/node.js:831 -#: htdocs/luci-static/resources/view/fchomo/node.js:863 -#: htdocs/luci-static/resources/view/fchomo/server.js:555 +#: htdocs/luci-static/resources/view/fchomo/node.js:834 +#: htdocs/luci-static/resources/view/fchomo/node.js:874 msgid "v1" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:496 -#: htdocs/luci-static/resources/view/fchomo/node.js:832 -#: htdocs/luci-static/resources/view/fchomo/node.js:864 -#: htdocs/luci-static/resources/view/fchomo/server.js:556 +#: htdocs/luci-static/resources/fchomo/listeners.js:513 +#: htdocs/luci-static/resources/view/fchomo/node.js:505 +#: htdocs/luci-static/resources/view/fchomo/node.js:835 +#: htdocs/luci-static/resources/view/fchomo/node.js:875 msgid "v2" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:497 -#: htdocs/luci-static/resources/view/fchomo/node.js:833 -#: htdocs/luci-static/resources/view/fchomo/server.js:557 +#: htdocs/luci-static/resources/fchomo/listeners.js:514 +#: htdocs/luci-static/resources/view/fchomo/node.js:506 +#: htdocs/luci-static/resources/view/fchomo/node.js:836 msgid "v3" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1406 -#: htdocs/luci-static/resources/fchomo.js:1409 +#: htdocs/luci-static/resources/fchomo.js:1412 +#: htdocs/luci-static/resources/fchomo.js:1415 msgid "valid JSON format" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1013 +#: htdocs/luci-static/resources/view/fchomo/node.js:1027 msgid "valid SHA256 string with %d characters" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1431 -#: htdocs/luci-static/resources/fchomo.js:1434 +#: htdocs/luci-static/resources/fchomo.js:1437 +#: htdocs/luci-static/resources/fchomo.js:1440 msgid "valid URL" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1444 +#: htdocs/luci-static/resources/fchomo.js:1450 msgid "valid base64 key with %d characters" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1504 #: htdocs/luci-static/resources/fchomo.js:1510 +#: htdocs/luci-static/resources/fchomo.js:1516 msgid "valid format: 2x, 2p, 4v" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1491 +#: htdocs/luci-static/resources/fchomo.js:1497 msgid "valid key length with %d characters" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1369 +#: htdocs/luci-static/resources/fchomo.js:1375 msgid "valid port value" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1419 +#: htdocs/luci-static/resources/fchomo.js:1425 msgid "valid uuid" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:386 +#: htdocs/luci-static/resources/fchomo.js:392 msgid "vless-mlkem768" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:385 +#: htdocs/luci-static/resources/fchomo.js:391 msgid "vless-x25519" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:320 +#: htdocs/luci-static/resources/fchomo.js:326 msgid "xchacha20-ietf-poly1305" msgstr "" @@ -3475,7 +3547,7 @@ msgstr "" msgid "yacd-meta" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1218 +#: htdocs/luci-static/resources/view/fchomo/node.js:1232 msgid "yamux" msgstr "" @@ -3487,6 +3559,6 @@ msgstr "" msgid "zero" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1187 +#: htdocs/luci-static/resources/fchomo.js:1193 msgid "🡇" msgstr "" diff --git a/small/luci-app-fchomo/po/zh_Hans/fchomo.po b/small/luci-app-fchomo/po/zh_Hans/fchomo.po index 606640fdcd..0ae3f997ab 100644 --- a/small/luci-app-fchomo/po/zh_Hans/fchomo.po +++ b/small/luci-app-fchomo/po/zh_Hans/fchomo.po @@ -12,30 +12,30 @@ msgstr "" msgid "%s log" msgstr "%s 日志" -#: htdocs/luci-static/resources/fchomo.js:223 -#: htdocs/luci-static/resources/fchomo.js:224 -#: htdocs/luci-static/resources/fchomo.js:225 -#: htdocs/luci-static/resources/fchomo.js:226 -#: htdocs/luci-static/resources/fchomo.js:227 -#: htdocs/luci-static/resources/fchomo.js:228 +#: htdocs/luci-static/resources/fchomo.js:229 +#: htdocs/luci-static/resources/fchomo.js:230 +#: htdocs/luci-static/resources/fchomo.js:231 +#: htdocs/luci-static/resources/fchomo.js:232 +#: htdocs/luci-static/resources/fchomo.js:233 +#: htdocs/luci-static/resources/fchomo.js:234 msgid "%s ports" msgstr "%s 端口" -#: htdocs/luci-static/resources/fchomo.js:588 -#: htdocs/luci-static/resources/fchomo.js:591 +#: htdocs/luci-static/resources/fchomo.js:594 +#: htdocs/luci-static/resources/fchomo.js:597 #: htdocs/luci-static/resources/view/fchomo/client.js:315 msgid "(Imported)" msgstr "(已导入)" +#: htdocs/luci-static/resources/fchomo/listeners.js:840 +#: htdocs/luci-static/resources/fchomo/listeners.js:848 +#: htdocs/luci-static/resources/fchomo/listeners.js:857 #: htdocs/luci-static/resources/view/fchomo/global.js:543 #: htdocs/luci-static/resources/view/fchomo/global.js:549 -#: htdocs/luci-static/resources/view/fchomo/node.js:1028 -#: htdocs/luci-static/resources/view/fchomo/node.js:1034 #: htdocs/luci-static/resources/view/fchomo/node.js:1042 #: htdocs/luci-static/resources/view/fchomo/node.js:1048 -#: htdocs/luci-static/resources/view/fchomo/server.js:846 -#: htdocs/luci-static/resources/view/fchomo/server.js:854 -#: htdocs/luci-static/resources/view/fchomo/server.js:863 +#: htdocs/luci-static/resources/view/fchomo/node.js:1056 +#: htdocs/luci-static/resources/view/fchomo/node.js:1062 msgid "(mTLS)" msgstr "" @@ -46,19 +46,19 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1056 #: htdocs/luci-static/resources/view/fchomo/client.js:1057 #: htdocs/luci-static/resources/view/fchomo/client.js:1278 -#: htdocs/luci-static/resources/view/fchomo/node.js:1734 -#: htdocs/luci-static/resources/view/fchomo/node.js:1740 +#: htdocs/luci-static/resources/view/fchomo/node.js:1748 #: htdocs/luci-static/resources/view/fchomo/node.js:1754 -#: htdocs/luci-static/resources/view/fchomo/node.js:1760 +#: htdocs/luci-static/resources/view/fchomo/node.js:1768 +#: htdocs/luci-static/resources/view/fchomo/node.js:1774 msgid "-- Please choose --" msgstr "-- 请选择 --" -#: htdocs/luci-static/resources/fchomo.js:375 +#: htdocs/luci-static/resources/fchomo.js:381 msgid "0-RTT reuse." msgstr "0-RTT 重用。" -#: htdocs/luci-static/resources/fchomo.js:372 -#: htdocs/luci-static/resources/fchomo.js:376 +#: htdocs/luci-static/resources/fchomo.js:378 +#: htdocs/luci-static/resources/fchomo.js:382 msgid "1-RTT only." msgstr "仅限 1-RTT。" @@ -66,15 +66,15 @@ msgstr "仅限 1-RTT。" msgid "163Music" msgstr "网抑云" -#: htdocs/luci-static/resources/fchomo.js:322 +#: htdocs/luci-static/resources/fchomo.js:328 msgid "2022-blake3-aes-128-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:323 +#: htdocs/luci-static/resources/fchomo.js:329 msgid "2022-blake3-aes-256-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:324 +#: htdocs/luci-static/resources/fchomo.js:330 msgid "2022-blake3-chacha20-poly1305" msgstr "" @@ -82,14 +82,24 @@ msgstr "" msgid "0 or 1 only." msgstr "仅限 01。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1035 +#: htdocs/luci-static/resources/fchomo/listeners.js:818 +#: htdocs/luci-static/resources/fchomo/listeners.js:833 +#: htdocs/luci-static/resources/fchomo/listeners.js:858 #: htdocs/luci-static/resources/view/fchomo/node.js:1049 -#: htdocs/luci-static/resources/view/fchomo/server.js:824 -#: htdocs/luci-static/resources/view/fchomo/server.js:839 -#: htdocs/luci-static/resources/view/fchomo/server.js:864 +#: htdocs/luci-static/resources/view/fchomo/node.js:1063 msgid "Save your configuration before uploading files!" msgstr "上传文件前请先保存配置!" +#: htdocs/luci-static/resources/fchomo/listeners.js:239 +#: htdocs/luci-static/resources/view/fchomo/node.js:379 +msgid "" +"A base64 string is used to fine-tune network behavior.
Please refer to " +"the document." +msgstr "" +"一个 base64 字符串用于微调网络行为。
格式请参考
文档。" + #: htdocs/luci-static/resources/view/fchomo/global.js:599 msgid "API" msgstr "API" @@ -159,11 +169,15 @@ msgstr "新增 DNS 服务器" msgid "Add a Node" msgstr "新增 节点" -#: htdocs/luci-static/resources/view/fchomo/node.js:1325 +#: htdocs/luci-static/resources/view/fchomo/inbound.js:28 +msgid "Add a inbound" +msgstr "新增 入站" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1339 msgid "Add a provider" msgstr "新增 供应商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1710 +#: htdocs/luci-static/resources/view/fchomo/node.js:1724 msgid "Add a proxy chain" msgstr "新增 代理链" @@ -179,7 +193,7 @@ msgstr "新增 路由规则" msgid "Add a rule set" msgstr "新增 规则集" -#: htdocs/luci-static/resources/view/fchomo/server.js:166 +#: htdocs/luci-static/resources/view/fchomo/server.js:58 msgid "Add a server" msgstr "新增 服务器" @@ -187,11 +201,11 @@ msgstr "新增 服务器" msgid "Add a sub rule" msgstr "新增 子规则" -#: htdocs/luci-static/resources/view/fchomo/node.js:1535 +#: htdocs/luci-static/resources/view/fchomo/node.js:1549 msgid "Add prefix" msgstr "添加前缀" -#: htdocs/luci-static/resources/view/fchomo/node.js:1539 +#: htdocs/luci-static/resources/view/fchomo/node.js:1553 msgid "Add suffix" msgstr "添加后缀" @@ -200,7 +214,7 @@ msgstr "添加后缀" msgid "Address" msgstr "地址" -#: htdocs/luci-static/resources/fchomo.js:379 +#: htdocs/luci-static/resources/fchomo.js:385 msgid "" "After the 1-RTT client/server hello, padding randomly 111-1111 bytes with " "100% probability." @@ -217,7 +231,7 @@ msgstr "客户端维护的 NAT 映射 的老化时间。
" msgid "All allowed" msgstr "允许所有" -#: htdocs/luci-static/resources/fchomo.js:220 +#: htdocs/luci-static/resources/fchomo.js:226 msgid "All ports" msgstr "所有端口" @@ -228,7 +242,7 @@ msgid "" msgstr "" "允许从私有网络访问。
要从公共网站访问私有网络上的 API,则必须启用。" -#: htdocs/luci-static/resources/view/fchomo/node.js:762 +#: htdocs/luci-static/resources/view/fchomo/node.js:765 msgid "Allowed IPs" msgstr "允许的 IP" @@ -240,13 +254,13 @@ msgstr "已是最新版本。" msgid "Already in updating." msgstr "已在更新中。" +#: htdocs/luci-static/resources/fchomo/listeners.js:474 #: htdocs/luci-static/resources/view/fchomo/node.js:635 -#: htdocs/luci-static/resources/view/fchomo/server.js:526 msgid "Alter ID" msgstr "额外 ID" -#: htdocs/luci-static/resources/fchomo.js:148 -#: htdocs/luci-static/resources/fchomo.js:181 +#: htdocs/luci-static/resources/fchomo.js:152 +#: htdocs/luci-static/resources/fchomo.js:186 msgid "AnyTLS" msgstr "" @@ -263,7 +277,7 @@ msgstr "作为 dnsmasq 的最优先上游" msgid "As the TOP upstream of dnsmasq." msgstr "作为 dnsmasq 的最优先上游。" -#: htdocs/luci-static/resources/view/fchomo/server.js:476 +#: htdocs/luci-static/resources/fchomo/listeners.js:424 msgid "Auth timeout" msgstr "认证超时" @@ -271,14 +285,14 @@ msgstr "认证超时" msgid "Authenticated length" msgstr "认证长度" +#: htdocs/luci-static/resources/fchomo/listeners.js:389 #: htdocs/luci-static/resources/view/fchomo/global.js:402 -#: htdocs/luci-static/resources/view/fchomo/node.js:450 -#: htdocs/luci-static/resources/view/fchomo/node.js:473 -#: htdocs/luci-static/resources/view/fchomo/server.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:456 +#: htdocs/luci-static/resources/view/fchomo/node.js:480 msgid "Auto" msgstr "自动" -#: htdocs/luci-static/resources/view/fchomo/server.js:189 +#: htdocs/luci-static/resources/fchomo/listeners.js:137 msgid "Auto configure firewall" msgstr "自动配置防火墙" @@ -320,13 +334,13 @@ msgid "Binary mrs" msgstr "二进制 mrs" #: htdocs/luci-static/resources/view/fchomo/global.js:734 -#: htdocs/luci-static/resources/view/fchomo/node.js:1295 -#: htdocs/luci-static/resources/view/fchomo/node.js:1608 +#: htdocs/luci-static/resources/view/fchomo/node.js:1309 +#: htdocs/luci-static/resources/view/fchomo/node.js:1622 msgid "Bind interface" msgstr "绑定接口" -#: htdocs/luci-static/resources/view/fchomo/node.js:1296 -#: htdocs/luci-static/resources/view/fchomo/node.js:1609 +#: htdocs/luci-static/resources/view/fchomo/node.js:1310 +#: htdocs/luci-static/resources/view/fchomo/node.js:1623 msgid "Bind outbound interface.
" msgstr "绑定出站接口。
" @@ -364,12 +378,14 @@ msgstr "绕过 CN 流量" msgid "Bypass DSCP" msgstr "绕过 DSCP" -#: htdocs/luci-static/resources/view/fchomo/node.js:448 -#: htdocs/luci-static/resources/view/fchomo/node.js:449 -#: htdocs/luci-static/resources/view/fchomo/node.js:450 -#: htdocs/luci-static/resources/view/fchomo/server.js:437 -#: htdocs/luci-static/resources/view/fchomo/server.js:438 -#: htdocs/luci-static/resources/view/fchomo/server.js:439 +#: htdocs/luci-static/resources/fchomo/listeners.js:387 +#: htdocs/luci-static/resources/fchomo/listeners.js:388 +#: htdocs/luci-static/resources/fchomo/listeners.js:389 +#: htdocs/luci-static/resources/fchomo/listeners.js:390 +#: htdocs/luci-static/resources/view/fchomo/node.js:454 +#: htdocs/luci-static/resources/view/fchomo/node.js:455 +#: htdocs/luci-static/resources/view/fchomo/node.js:456 +#: htdocs/luci-static/resources/view/fchomo/node.js:457 msgid "CDN support" msgstr "CDN 支持" @@ -385,21 +401,21 @@ msgstr "CORS 允许私有网络" msgid "CORS allowed origins, * will be used if empty." msgstr "CORS 允许的来源,留空则使用 *。" -#: htdocs/luci-static/resources/fchomo.js:704 +#: htdocs/luci-static/resources/fchomo.js:710 msgid "Cancel" msgstr "取消" -#: htdocs/luci-static/resources/view/fchomo/node.js:1007 +#: htdocs/luci-static/resources/view/fchomo/node.js:1021 msgid "Cert fingerprint" msgstr "证书指纹" -#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/node.js:1022 msgid "" "Certificate fingerprint. Used to implement SSL Pinning and prevent MitM." msgstr "证书指纹。用于实现 SSL证书固定 并防止 MitM。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1028 -#: htdocs/luci-static/resources/view/fchomo/server.js:816 +#: htdocs/luci-static/resources/fchomo/listeners.js:810 +#: htdocs/luci-static/resources/view/fchomo/node.js:1042 msgid "Certificate path" msgstr "证书路径" @@ -428,16 +444,16 @@ msgstr "大陆 IPv6 库版本" msgid "China list version" msgstr "大陆域名列表版本" +#: htdocs/luci-static/resources/fchomo/listeners.js:213 +#: htdocs/luci-static/resources/fchomo/listeners.js:306 #: htdocs/luci-static/resources/view/fchomo/node.js:332 -#: htdocs/luci-static/resources/view/fchomo/node.js:385 +#: htdocs/luci-static/resources/view/fchomo/node.js:391 #: htdocs/luci-static/resources/view/fchomo/node.js:641 -#: htdocs/luci-static/resources/view/fchomo/server.js:269 -#: htdocs/luci-static/resources/view/fchomo/server.js:356 msgid "Chipher" msgstr "加密方法" -#: htdocs/luci-static/resources/view/fchomo/node.js:394 -#: htdocs/luci-static/resources/view/fchomo/server.js:365 +#: htdocs/luci-static/resources/fchomo/listeners.js:315 +#: htdocs/luci-static/resources/view/fchomo/node.js:400 msgid "Chipher must be enabled if obfuscate downlink is disabled." msgstr "如果下行链路混淆功能被禁用,则必须启用加密。" @@ -453,29 +469,29 @@ msgstr "" "点击此处下载" "最新的初始包。" +#: htdocs/luci-static/resources/fchomo/listeners.js:633 +#: htdocs/luci-static/resources/fchomo/listeners.js:849 #: htdocs/luci-static/resources/view/fchomo/global.js:550 -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/node.js:1029 +#: htdocs/luci-static/resources/view/fchomo/node.js:916 #: htdocs/luci-static/resources/view/fchomo/node.js:1043 -#: htdocs/luci-static/resources/view/fchomo/server.js:645 -#: htdocs/luci-static/resources/view/fchomo/server.js:855 +#: htdocs/luci-static/resources/view/fchomo/node.js:1057 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:22 msgid "Client" msgstr "客户端" -#: htdocs/luci-static/resources/view/fchomo/server.js:854 +#: htdocs/luci-static/resources/fchomo/listeners.js:848 msgid "Client Auth Certificate path" msgstr "客户端认证证书路径" -#: htdocs/luci-static/resources/view/fchomo/server.js:846 +#: htdocs/luci-static/resources/fchomo/listeners.js:840 msgid "Client Auth type" msgstr "客户端认证类型" -#: htdocs/luci-static/resources/view/fchomo/node.js:1074 +#: htdocs/luci-static/resources/view/fchomo/node.js:1088 msgid "Client fingerprint" msgstr "客户端指纹" -#: htdocs/luci-static/resources/view/fchomo/server.js:352 +#: htdocs/luci-static/resources/fchomo/listeners.js:302 msgid "Client key" msgstr "客户端密钥" @@ -487,22 +503,21 @@ msgstr "客户端状态" msgid "Collecting data..." msgstr "收集数据中..." -#: htdocs/luci-static/resources/fchomo.js:221 -#: htdocs/luci-static/resources/fchomo.js:222 +#: htdocs/luci-static/resources/fchomo.js:227 +#: htdocs/luci-static/resources/fchomo.js:228 msgid "Common ports (bypass P2P traffic)" msgstr "常用端口(绕过 P2P 流量)" -#: htdocs/luci-static/resources/fchomo.js:1303 +#: htdocs/luci-static/resources/fchomo.js:1309 msgid "Complete" msgstr "完成" -#: htdocs/luci-static/resources/view/fchomo/node.js:1555 +#: htdocs/luci-static/resources/view/fchomo/node.js:1569 msgid "Configuration Items" msgstr "配置项" -#: htdocs/luci-static/resources/view/fchomo/node.js:515 -#: htdocs/luci-static/resources/view/fchomo/node.js:717 -#: htdocs/luci-static/resources/view/fchomo/server.js:454 +#: htdocs/luci-static/resources/fchomo/listeners.js:537 +#: htdocs/luci-static/resources/view/fchomo/node.js:854 msgid "Congestion controller" msgstr "拥塞控制器" @@ -510,19 +525,19 @@ msgstr "拥塞控制器" msgid "Connection check" msgstr "连接检查" -#: htdocs/luci-static/resources/fchomo.js:573 +#: htdocs/luci-static/resources/fchomo.js:579 msgid "Content copied to clipboard!" msgstr "内容已复制到剪贴板!" #: htdocs/luci-static/resources/view/fchomo/client.js:670 -#: htdocs/luci-static/resources/view/fchomo/node.js:1465 -#: htdocs/luci-static/resources/view/fchomo/node.js:1491 +#: htdocs/luci-static/resources/view/fchomo/node.js:1479 +#: htdocs/luci-static/resources/view/fchomo/node.js:1505 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:332 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:358 msgid "Content will not be verified, Please make sure you enter it correctly." msgstr "内容将不会被验证,请确保输入正确。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1464 +#: htdocs/luci-static/resources/view/fchomo/node.js:1478 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:331 msgid "Contents" msgstr "内容" @@ -531,7 +546,7 @@ msgstr "内容" msgid "Contents have been saved." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:575 +#: htdocs/luci-static/resources/fchomo.js:581 msgid "Copy" msgstr "复制" @@ -547,7 +562,7 @@ msgstr "Cron 表达式" msgid "Custom Direct List" msgstr "自定义直连列表" -#: htdocs/luci-static/resources/view/fchomo/node.js:1526 +#: htdocs/luci-static/resources/view/fchomo/node.js:1540 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:393 msgid "Custom HTTP header." msgstr "自定义 HTTP header。" @@ -556,8 +571,8 @@ msgstr "自定义 HTTP header。" msgid "Custom Proxy List" msgstr "自定义代理列表" -#: htdocs/luci-static/resources/view/fchomo/node.js:407 -#: htdocs/luci-static/resources/view/fchomo/server.js:378 +#: htdocs/luci-static/resources/fchomo/listeners.js:328 +#: htdocs/luci-static/resources/view/fchomo/node.js:413 msgid "Custom byte layout" msgstr "自定义字节布局" @@ -566,7 +581,7 @@ msgid "" "Custom internal hosts. Support yaml or json format." msgstr "自定义内部 hosts。支持 yamljson 格式。" -#: htdocs/luci-static/resources/fchomo.js:170 +#: htdocs/luci-static/resources/fchomo.js:175 msgid "DIRECT" msgstr "" @@ -583,7 +598,7 @@ msgstr " DNS 端口" #: htdocs/luci-static/resources/view/fchomo/client.js:1428 #: htdocs/luci-static/resources/view/fchomo/client.js:1437 #: htdocs/luci-static/resources/view/fchomo/node.js:712 -#: htdocs/luci-static/resources/view/fchomo/node.js:792 +#: htdocs/luci-static/resources/view/fchomo/node.js:795 msgid "DNS server" msgstr "DNS 服务器" @@ -611,15 +626,15 @@ msgstr "默认 DNS(由 WAN 下发)" msgid "Default DNS server" msgstr "默认 DNS 服务器" -#: htdocs/luci-static/resources/view/fchomo/node.js:763 +#: htdocs/luci-static/resources/view/fchomo/node.js:766 msgid "Destination addresses allowed to be forwarded via Wireguard." msgstr "允许通过 WireGuard 转发的目的地址" -#: htdocs/luci-static/resources/view/fchomo/node.js:1733 +#: htdocs/luci-static/resources/view/fchomo/node.js:1747 msgid "Destination provider" msgstr "落地供应商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1739 +#: htdocs/luci-static/resources/view/fchomo/node.js:1753 msgid "Destination proxy node" msgstr "落地代理节点" @@ -627,8 +642,8 @@ msgstr "落地代理节点" msgid "Dial fields" msgstr "拨号字段" -#: htdocs/luci-static/resources/view/fchomo/node.js:1746 -#: htdocs/luci-static/resources/view/fchomo/node.js:1766 +#: htdocs/luci-static/resources/view/fchomo/node.js:1760 +#: htdocs/luci-static/resources/view/fchomo/node.js:1780 msgid "Different chain head/tail" msgstr "不同的链头/链尾" @@ -648,9 +663,9 @@ msgstr "直连 IPv6 地址" msgid "Direct MAC-s" msgstr "直连 MAC 地址" +#: htdocs/luci-static/resources/fchomo/listeners.js:193 #: htdocs/luci-static/resources/view/fchomo/global.js:403 #: htdocs/luci-static/resources/view/fchomo/node.js:298 -#: htdocs/luci-static/resources/view/fchomo/server.js:249 msgid "Disable" msgstr "禁用" @@ -666,7 +681,7 @@ msgstr "禁用 quic-go 的 通用分段卸载(GSO)" msgid "Disable ICMP Forwarding" msgstr "禁用 ICMP 转发" -#: htdocs/luci-static/resources/view/fchomo/node.js:956 +#: htdocs/luci-static/resources/view/fchomo/node.js:967 msgid "Disable SNI" msgstr "禁用 SNI" @@ -694,46 +709,46 @@ msgstr "" msgid "Domain" msgstr "域名" -#: htdocs/luci-static/resources/view/fchomo/node.js:957 +#: htdocs/luci-static/resources/view/fchomo/node.js:968 msgid "Donot send server name in ClientHello." msgstr "不要在 ClientHello 中发送服务器名称。" #: htdocs/luci-static/resources/view/fchomo/client.js:1577 -#: htdocs/luci-static/resources/view/fchomo/node.js:1021 -#: htdocs/luci-static/resources/view/fchomo/node.js:1595 +#: htdocs/luci-static/resources/view/fchomo/node.js:1035 +#: htdocs/luci-static/resources/view/fchomo/node.js:1609 msgid "Donot verifying server certificate." msgstr "不验证服务器证书。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1274 +#: htdocs/luci-static/resources/view/fchomo/node.js:1288 msgid "Download bandwidth" msgstr "下载带宽" -#: htdocs/luci-static/resources/view/fchomo/node.js:1275 +#: htdocs/luci-static/resources/view/fchomo/node.js:1289 msgid "Download bandwidth in Mbps." msgstr "下载带宽(单位:Mbps)。" -#: htdocs/luci-static/resources/fchomo.js:1182 +#: htdocs/luci-static/resources/fchomo.js:1188 msgid "Download failed: %s" msgstr "下载失败: %s" -#: htdocs/luci-static/resources/fchomo.js:1180 +#: htdocs/luci-static/resources/fchomo.js:1186 msgid "Download successful." msgstr "下载成功。" -#: htdocs/luci-static/resources/fchomo.js:156 +#: htdocs/luci-static/resources/fchomo.js:161 msgid "Dual stack" msgstr "双栈" -#: htdocs/luci-static/resources/view/fchomo/node.js:1068 +#: htdocs/luci-static/resources/view/fchomo/node.js:1082 msgid "ECH HTTPS record query servername" msgstr "ECH HTTPS 记录查询域名" -#: htdocs/luci-static/resources/view/fchomo/node.js:1062 -#: htdocs/luci-static/resources/view/fchomo/server.js:912 +#: htdocs/luci-static/resources/fchomo/listeners.js:906 +#: htdocs/luci-static/resources/view/fchomo/node.js:1076 msgid "ECH config" msgstr "ECH 配置" -#: htdocs/luci-static/resources/view/fchomo/server.js:871 +#: htdocs/luci-static/resources/fchomo/listeners.js:865 msgid "ECH key" msgstr "ECH 密钥" @@ -749,14 +764,18 @@ msgstr "" msgid "ETag support" msgstr "ETag 支持" -#: htdocs/luci-static/resources/view/fchomo/node.js:1187 +#: htdocs/luci-static/resources/view/fchomo/node.js:1201 msgid "Early Data first packet length limit." msgstr "前置数据长度阈值" -#: htdocs/luci-static/resources/view/fchomo/node.js:1193 +#: htdocs/luci-static/resources/view/fchomo/node.js:1207 msgid "Early Data header name" msgstr "前置数据标头" +#: htdocs/luci-static/resources/view/fchomo/inbound.js:20 +msgid "Edit inbound" +msgstr "编辑入站" + #: htdocs/luci-static/resources/view/fchomo/node.js:203 msgid "Edit node" msgstr "编辑节点" @@ -765,15 +784,16 @@ msgstr "编辑节点" msgid "Edit ruleset" msgstr "编辑规则集" -#: htdocs/luci-static/resources/view/fchomo/node.js:1462 +#: htdocs/luci-static/resources/view/fchomo/node.js:1476 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:329 msgid "Editer" msgstr "编辑器" -#: htdocs/luci-static/resources/fchomo.js:366 +#: htdocs/luci-static/resources/fchomo.js:372 msgid "Eliminate encryption header characteristics" msgstr "消除加密头特征" +#: htdocs/luci-static/resources/fchomo/listeners.js:132 #: htdocs/luci-static/resources/view/fchomo/client.js:931 #: htdocs/luci-static/resources/view/fchomo/client.js:1024 #: htdocs/luci-static/resources/view/fchomo/client.js:1262 @@ -784,12 +804,11 @@ msgstr "消除加密头特征" #: htdocs/luci-static/resources/view/fchomo/global.js:401 #: htdocs/luci-static/resources/view/fchomo/global.js:680 #: htdocs/luci-static/resources/view/fchomo/node.js:234 -#: htdocs/luci-static/resources/view/fchomo/node.js:1435 -#: htdocs/luci-static/resources/view/fchomo/node.js:1631 -#: htdocs/luci-static/resources/view/fchomo/node.js:1720 +#: htdocs/luci-static/resources/view/fchomo/node.js:1449 +#: htdocs/luci-static/resources/view/fchomo/node.js:1645 +#: htdocs/luci-static/resources/view/fchomo/node.js:1734 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:257 -#: htdocs/luci-static/resources/view/fchomo/server.js:157 -#: htdocs/luci-static/resources/view/fchomo/server.js:184 +#: htdocs/luci-static/resources/view/fchomo/server.js:49 msgid "Enable" msgstr "启用" @@ -814,49 +833,49 @@ msgstr "" "为出站连接启用 IP4P 转换" -#: htdocs/luci-static/resources/view/fchomo/node.js:1056 +#: htdocs/luci-static/resources/view/fchomo/node.js:1070 msgid "Enable ECH" msgstr "启用 ECH" -#: htdocs/luci-static/resources/view/fchomo/node.js:1262 +#: htdocs/luci-static/resources/view/fchomo/node.js:1276 msgid "Enable TCP Brutal" msgstr "启用 TCP Brutal" -#: htdocs/luci-static/resources/view/fchomo/node.js:1263 +#: htdocs/luci-static/resources/view/fchomo/node.js:1277 msgid "Enable TCP Brutal congestion control algorithm" msgstr "启用 TCP Brutal 拥塞控制算法。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1251 +#: htdocs/luci-static/resources/view/fchomo/node.js:1265 msgid "Enable multiplexing only for TCP." msgstr "仅为 TCP 启用多路复用。" -#: htdocs/luci-static/resources/view/fchomo/node.js:434 -#: htdocs/luci-static/resources/view/fchomo/server.js:423 +#: htdocs/luci-static/resources/fchomo/listeners.js:373 +#: htdocs/luci-static/resources/view/fchomo/node.js:440 msgid "Enable obfuscate for downlink" msgstr "启用下行链路混淆" -#: htdocs/luci-static/resources/view/fchomo/node.js:1245 +#: htdocs/luci-static/resources/view/fchomo/node.js:1259 msgid "Enable padding" msgstr "启用填充" -#: htdocs/luci-static/resources/view/fchomo/node.js:1256 +#: htdocs/luci-static/resources/view/fchomo/node.js:1270 msgid "Enable statistic" msgstr "启用统计" -#: htdocs/luci-static/resources/view/fchomo/node.js:857 -#: htdocs/luci-static/resources/view/fchomo/node.js:1577 +#: htdocs/luci-static/resources/view/fchomo/node.js:868 +#: htdocs/luci-static/resources/view/fchomo/node.js:1591 msgid "" "Enable the SUoT protocol, requires server support. Conflict with Multiplex." msgstr "启用 SUoT 协议,需要服务端支持。与多路复用冲突。" +#: htdocs/luci-static/resources/fchomo/listeners.js:199 #: htdocs/luci-static/resources/view/fchomo/node.js:304 -#: htdocs/luci-static/resources/view/fchomo/server.js:255 msgid "" "Enabling obfuscation will make the server incompatible with standard QUIC " "connections, losing the ability to masquerade with HTTP/3." msgstr "启用混淆将使服务器与标准的 QUIC 连接不兼容,失去 HTTP/3 伪装的能力。" -#: htdocs/luci-static/resources/view/fchomo/server.js:608 +#: htdocs/luci-static/resources/fchomo/listeners.js:596 msgid "Encryption method" msgstr "加密方法" @@ -883,7 +902,7 @@ msgid "" "if empty." msgstr "超过此限制将会触发强制健康检查。留空则使用 5。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1689 +#: htdocs/luci-static/resources/view/fchomo/node.js:1703 msgid "Exclude matched node types." msgstr "排除匹配的节点类型。" @@ -896,7 +915,7 @@ msgstr "" "rel=\"noreferrer noopener\">此处。" #: htdocs/luci-static/resources/view/fchomo/client.js:1161 -#: htdocs/luci-static/resources/view/fchomo/node.js:1682 +#: htdocs/luci-static/resources/view/fchomo/node.js:1696 msgid "Exclude nodes that meet keywords or regexps." msgstr "排除匹配关键词或表达式的节点。" @@ -905,64 +924,65 @@ msgid "Expand/Collapse result" msgstr "展开/收起 结果" #: htdocs/luci-static/resources/view/fchomo/client.js:1121 -#: htdocs/luci-static/resources/view/fchomo/node.js:1667 +#: htdocs/luci-static/resources/view/fchomo/node.js:1681 msgid "Expected HTTP code. 204 will be used if empty." msgstr "预期的 HTTP code。留空则使用 204。" #: htdocs/luci-static/resources/view/fchomo/client.js:1123 -#: htdocs/luci-static/resources/view/fchomo/node.js:1669 +#: htdocs/luci-static/resources/view/fchomo/node.js:1683 msgid "Expected status" msgstr "预期状态" -#: htdocs/luci-static/resources/fchomo.js:423 -#: htdocs/luci-static/resources/fchomo.js:426 #: htdocs/luci-static/resources/fchomo.js:429 -#: htdocs/luci-static/resources/fchomo.js:1320 -#: htdocs/luci-static/resources/fchomo.js:1328 -#: htdocs/luci-static/resources/fchomo.js:1336 -#: htdocs/luci-static/resources/fchomo.js:1359 -#: htdocs/luci-static/resources/fchomo.js:1362 -#: htdocs/luci-static/resources/fchomo.js:1369 -#: htdocs/luci-static/resources/fchomo.js:1385 -#: htdocs/luci-static/resources/fchomo.js:1394 -#: htdocs/luci-static/resources/fchomo.js:1406 -#: htdocs/luci-static/resources/fchomo.js:1409 -#: htdocs/luci-static/resources/fchomo.js:1419 -#: htdocs/luci-static/resources/fchomo.js:1431 -#: htdocs/luci-static/resources/fchomo.js:1434 -#: htdocs/luci-static/resources/fchomo.js:1444 -#: htdocs/luci-static/resources/fchomo.js:1454 -#: htdocs/luci-static/resources/fchomo.js:1489 -#: htdocs/luci-static/resources/fchomo.js:1491 -#: htdocs/luci-static/resources/fchomo.js:1504 +#: htdocs/luci-static/resources/fchomo.js:432 +#: htdocs/luci-static/resources/fchomo.js:435 +#: htdocs/luci-static/resources/fchomo.js:1326 +#: htdocs/luci-static/resources/fchomo.js:1334 +#: htdocs/luci-static/resources/fchomo.js:1342 +#: htdocs/luci-static/resources/fchomo.js:1365 +#: htdocs/luci-static/resources/fchomo.js:1368 +#: htdocs/luci-static/resources/fchomo.js:1375 +#: htdocs/luci-static/resources/fchomo.js:1391 +#: htdocs/luci-static/resources/fchomo.js:1400 +#: htdocs/luci-static/resources/fchomo.js:1412 +#: htdocs/luci-static/resources/fchomo.js:1415 +#: htdocs/luci-static/resources/fchomo.js:1425 +#: htdocs/luci-static/resources/fchomo.js:1437 +#: htdocs/luci-static/resources/fchomo.js:1440 +#: htdocs/luci-static/resources/fchomo.js:1450 +#: htdocs/luci-static/resources/fchomo.js:1460 +#: htdocs/luci-static/resources/fchomo.js:1495 +#: htdocs/luci-static/resources/fchomo.js:1497 #: htdocs/luci-static/resources/fchomo.js:1510 -#: htdocs/luci-static/resources/fchomo.js:1517 -#: htdocs/luci-static/resources/fchomo.js:1526 +#: htdocs/luci-static/resources/fchomo.js:1516 +#: htdocs/luci-static/resources/fchomo.js:1523 +#: htdocs/luci-static/resources/fchomo.js:1532 +#: htdocs/luci-static/resources/fchomo/listeners.js:315 +#: htdocs/luci-static/resources/fchomo/listeners.js:359 +#: htdocs/luci-static/resources/fchomo/listeners.js:625 +#: htdocs/luci-static/resources/fchomo/listeners.js:656 +#: htdocs/luci-static/resources/fchomo/listeners.js:757 #: htdocs/luci-static/resources/view/fchomo/client.js:68 #: htdocs/luci-static/resources/view/fchomo/client.js:1018 #: htdocs/luci-static/resources/view/fchomo/client.js:1508 #: htdocs/luci-static/resources/view/fchomo/global.js:880 -#: htdocs/luci-static/resources/view/fchomo/node.js:394 -#: htdocs/luci-static/resources/view/fchomo/node.js:427 -#: htdocs/luci-static/resources/view/fchomo/node.js:479 -#: htdocs/luci-static/resources/view/fchomo/node.js:928 -#: htdocs/luci-static/resources/view/fchomo/node.js:1013 -#: htdocs/luci-static/resources/view/fchomo/node.js:1746 -#: htdocs/luci-static/resources/view/fchomo/node.js:1766 +#: htdocs/luci-static/resources/view/fchomo/node.js:400 +#: htdocs/luci-static/resources/view/fchomo/node.js:433 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +#: htdocs/luci-static/resources/view/fchomo/node.js:939 +#: htdocs/luci-static/resources/view/fchomo/node.js:1027 +#: htdocs/luci-static/resources/view/fchomo/node.js:1760 +#: htdocs/luci-static/resources/view/fchomo/node.js:1780 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:284 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:298 -#: htdocs/luci-static/resources/view/fchomo/server.js:365 -#: htdocs/luci-static/resources/view/fchomo/server.js:409 -#: htdocs/luci-static/resources/view/fchomo/server.js:637 -#: htdocs/luci-static/resources/view/fchomo/server.js:668 -#: htdocs/luci-static/resources/view/fchomo/server.js:769 msgid "Expecting: %s" msgstr "请输入:%s" -#: htdocs/luci-static/resources/view/fchomo/node.js:1123 -#: htdocs/luci-static/resources/view/fchomo/node.js:1130 -#: htdocs/luci-static/resources/view/fchomo/server.js:979 -#: htdocs/luci-static/resources/view/fchomo/server.js:986 +#: htdocs/luci-static/resources/fchomo/listeners.js:973 +#: htdocs/luci-static/resources/fchomo/listeners.js:980 +#: htdocs/luci-static/resources/view/fchomo/node.js:1137 +#: htdocs/luci-static/resources/view/fchomo/node.js:1144 msgid "Expecting: only support %s." msgstr "请输入:仅支援 %s." @@ -981,23 +1001,24 @@ msgstr "实验性" msgid "Factor" msgstr "条件" -#: htdocs/luci-static/resources/fchomo.js:1261 +#: htdocs/luci-static/resources/fchomo.js:1267 msgid "Failed to execute \"/etc/init.d/fchomo %s %s\" reason: %s" msgstr "无法执行 \"/etc/init.d/fchomo %s %s\" 原因: %s" -#: htdocs/luci-static/resources/fchomo.js:1214 +#: htdocs/luci-static/resources/fchomo.js:1220 msgid "Failed to generate %s, error: %s." msgstr "生成 %s 失败,错误:%s。" -#: htdocs/luci-static/resources/fchomo.js:1626 +#: htdocs/luci-static/resources/fchomo.js:1632 msgid "Failed to upload %s, error: %s." msgstr "上传 %s 失败,错误:%s。" -#: htdocs/luci-static/resources/fchomo.js:1645 +#: htdocs/luci-static/resources/fchomo.js:1651 msgid "Failed to upload, error: %s." msgstr "上传失败,错误:%s。" -#: htdocs/luci-static/resources/fchomo.js:213 +#: htdocs/luci-static/resources/fchomo.js:219 +#: htdocs/luci-static/resources/fchomo/listeners.js:398 msgid "Fallback" msgstr "自动回退" @@ -1011,7 +1032,7 @@ msgid "Fallback filter" msgstr "後備过滤器" #: htdocs/luci-static/resources/view/fchomo/client.js:1156 -#: htdocs/luci-static/resources/view/fchomo/node.js:1676 +#: htdocs/luci-static/resources/view/fchomo/node.js:1690 msgid "Filter nodes that meet keywords or regexps." msgstr "过滤匹配关键字或表达式的节点。" @@ -1036,12 +1057,12 @@ msgstr "兜底 DNS 服务器 (用于未被投毒污染的域名)" msgid "Final DNS server (For poisoned domains)" msgstr "兜底 DNS 服务器 (用于已被投毒污染的域名)" -#: htdocs/luci-static/resources/view/fchomo/server.js:188 +#: htdocs/luci-static/resources/fchomo/listeners.js:136 msgid "Firewall" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:466 #: htdocs/luci-static/resources/view/fchomo/node.js:627 -#: htdocs/luci-static/resources/view/fchomo/server.js:518 msgid "Flow" msgstr "流控" @@ -1054,8 +1075,8 @@ msgstr "" "noopener\">%s." #: htdocs/luci-static/resources/view/fchomo/client.js:1122 -#: htdocs/luci-static/resources/view/fchomo/node.js:1545 -#: htdocs/luci-static/resources/view/fchomo/node.js:1668 +#: htdocs/luci-static/resources/view/fchomo/node.js:1559 +#: htdocs/luci-static/resources/view/fchomo/node.js:1682 msgid "" "For format see %s." @@ -1064,7 +1085,7 @@ msgstr "" "a>." #: htdocs/luci-static/resources/view/fchomo/node.js:707 -#: htdocs/luci-static/resources/view/fchomo/node.js:787 +#: htdocs/luci-static/resources/view/fchomo/node.js:790 msgid "Force DNS remote resolution." msgstr "强制 DNS 远程解析。" @@ -1083,7 +1104,7 @@ msgstr "格式" msgid "FullCombo Shark!" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1149 +#: htdocs/luci-static/resources/view/fchomo/node.js:1163 msgid "GET" msgstr "" @@ -1095,10 +1116,10 @@ msgstr "GFW 域名列表版本" msgid "General" msgstr "常规" +#: htdocs/luci-static/resources/fchomo/listeners.js:119 #: htdocs/luci-static/resources/view/fchomo/client.js:1009 #: htdocs/luci-static/resources/view/fchomo/node.js:222 -#: htdocs/luci-static/resources/view/fchomo/node.js:1425 -#: htdocs/luci-static/resources/view/fchomo/server.js:171 +#: htdocs/luci-static/resources/view/fchomo/node.js:1439 msgid "General fields" msgstr "常规字段" @@ -1106,16 +1127,16 @@ msgstr "常规字段" msgid "General settings" msgstr "常规设置" -#: htdocs/luci-static/resources/fchomo.js:524 -#: htdocs/luci-static/resources/fchomo.js:526 -#: htdocs/luci-static/resources/fchomo.js:540 -#: htdocs/luci-static/resources/fchomo.js:542 +#: htdocs/luci-static/resources/fchomo.js:530 +#: htdocs/luci-static/resources/fchomo.js:532 +#: htdocs/luci-static/resources/fchomo.js:546 +#: htdocs/luci-static/resources/fchomo.js:548 +#: htdocs/luci-static/resources/fchomo/listeners.js:293 +#: htdocs/luci-static/resources/fchomo/listeners.js:334 +#: htdocs/luci-static/resources/fchomo/listeners.js:336 +#: htdocs/luci-static/resources/fchomo/listeners.js:729 +#: htdocs/luci-static/resources/fchomo/listeners.js:898 #: htdocs/luci-static/resources/view/fchomo/global.js:587 -#: htdocs/luci-static/resources/view/fchomo/server.js:343 -#: htdocs/luci-static/resources/view/fchomo/server.js:384 -#: htdocs/luci-static/resources/view/fchomo/server.js:386 -#: htdocs/luci-static/resources/view/fchomo/server.js:741 -#: htdocs/luci-static/resources/view/fchomo/server.js:904 msgid "Generate" msgstr "生成" @@ -1166,7 +1187,7 @@ msgstr "全局填充" msgid "Google" msgstr "谷歌" -#: htdocs/luci-static/resources/fchomo.js:226 +#: htdocs/luci-static/resources/fchomo.js:232 msgid "Google FCM" msgstr "谷歌 FCM" @@ -1178,48 +1199,49 @@ msgstr "授予 fchomo 访问 UCI 配置的权限" msgid "Group" msgstr "组" -#: htdocs/luci-static/resources/fchomo.js:139 -#: htdocs/luci-static/resources/fchomo.js:171 -#: htdocs/luci-static/resources/view/fchomo/node.js:810 -#: htdocs/luci-static/resources/view/fchomo/node.js:1112 -#: htdocs/luci-static/resources/view/fchomo/node.js:1123 -#: htdocs/luci-static/resources/view/fchomo/server.js:979 +#: htdocs/luci-static/resources/fchomo.js:143 +#: htdocs/luci-static/resources/fchomo.js:176 +#: htdocs/luci-static/resources/fchomo/listeners.js:973 +#: htdocs/luci-static/resources/view/fchomo/node.js:813 +#: htdocs/luci-static/resources/view/fchomo/node.js:1126 +#: htdocs/luci-static/resources/view/fchomo/node.js:1137 msgid "HTTP" msgstr "" #: htdocs/luci-static/resources/view/fchomo/node.js:267 -#: htdocs/luci-static/resources/view/fchomo/node.js:1171 -#: htdocs/luci-static/resources/view/fchomo/node.js:1525 +#: htdocs/luci-static/resources/view/fchomo/node.js:1185 +#: htdocs/luci-static/resources/view/fchomo/node.js:1539 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:392 msgid "HTTP header" msgstr "HTTP header" -#: htdocs/luci-static/resources/view/fchomo/node.js:440 -#: htdocs/luci-static/resources/view/fchomo/server.js:429 +#: htdocs/luci-static/resources/fchomo/listeners.js:379 +#: htdocs/luci-static/resources/view/fchomo/node.js:446 msgid "HTTP mask" msgstr "HTTP 伪装" -#: htdocs/luci-static/resources/view/fchomo/node.js:445 -#: htdocs/luci-static/resources/view/fchomo/node.js:479 -#: htdocs/luci-static/resources/view/fchomo/server.js:434 +#: htdocs/luci-static/resources/fchomo/listeners.js:384 +#: htdocs/luci-static/resources/view/fchomo/node.js:451 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 msgid "HTTP mask mode" msgstr "HTTP 伪装模式" -#: htdocs/luci-static/resources/view/fchomo/node.js:469 +#: htdocs/luci-static/resources/view/fchomo/node.js:476 msgid "HTTP mask multiplex" msgstr "HTTP 伪装多路复用" -#: htdocs/luci-static/resources/view/fchomo/node.js:454 -#: htdocs/luci-static/resources/view/fchomo/node.js:459 +#: htdocs/luci-static/resources/view/fchomo/node.js:461 +#: htdocs/luci-static/resources/view/fchomo/node.js:466 msgid "HTTP mask: %s" msgstr "HTTP 伪装: %s" -#: htdocs/luci-static/resources/view/fchomo/node.js:1148 +#: htdocs/luci-static/resources/view/fchomo/node.js:1162 msgid "HTTP request method" msgstr "HTTP 请求方法" -#: htdocs/luci-static/resources/view/fchomo/node.js:465 -#: htdocs/luci-static/resources/view/fchomo/server.js:443 +#: htdocs/luci-static/resources/fchomo/listeners.js:394 +#: htdocs/luci-static/resources/view/fchomo/node.js:472 msgid "HTTP root path" msgstr "HTTP 根路径" @@ -1227,15 +1249,15 @@ msgstr "HTTP 根路径" msgid "HTTP/3" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:263 +#: htdocs/luci-static/resources/fchomo/listeners.js:207 msgid "" "HTTP3 server behavior when authentication fails.
A 404 page will be " "returned if empty." msgstr "身份验证失败时的 HTTP3 服务器响应。默认返回 404 页面。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1113 -#: htdocs/luci-static/resources/view/fchomo/node.js:1124 -#: htdocs/luci-static/resources/view/fchomo/server.js:980 +#: htdocs/luci-static/resources/fchomo/listeners.js:974 +#: htdocs/luci-static/resources/view/fchomo/node.js:1127 +#: htdocs/luci-static/resources/view/fchomo/node.js:1138 msgid "HTTPUpgrade" msgstr "" @@ -1247,36 +1269,40 @@ msgstr "处理域名" msgid "Handshake mode" msgstr "握手模式" -#: htdocs/luci-static/resources/view/fchomo/server.js:541 +#: htdocs/luci-static/resources/fchomo/listeners.js:498 msgid "Handshake target that supports TLS 1.3" msgstr "握手目标 (支援 TLS 1.3)" -#: htdocs/luci-static/resources/view/fchomo/server.js:416 +#: htdocs/luci-static/resources/fchomo/listeners.js:366 msgid "Handshake timeout" msgstr "握手超时" +#: htdocs/luci-static/resources/view/fchomo/node.js:718 +msgid "Health check" +msgstr "健康检查" + #: htdocs/luci-static/resources/view/fchomo/client.js:1091 -#: htdocs/luci-static/resources/view/fchomo/node.js:1636 +#: htdocs/luci-static/resources/view/fchomo/node.js:1650 msgid "Health check URL" msgstr "健康检查 URL" #: htdocs/luci-static/resources/view/fchomo/client.js:1120 -#: htdocs/luci-static/resources/view/fchomo/node.js:1666 +#: htdocs/luci-static/resources/view/fchomo/node.js:1680 msgid "Health check expected status" msgstr "健康检查预期状态" #: htdocs/luci-static/resources/view/fchomo/client.js:1100 -#: htdocs/luci-static/resources/view/fchomo/node.js:1646 +#: htdocs/luci-static/resources/view/fchomo/node.js:1660 msgid "Health check interval" msgstr "健康检查间隔" #: htdocs/luci-static/resources/view/fchomo/client.js:1107 -#: htdocs/luci-static/resources/view/fchomo/node.js:1653 +#: htdocs/luci-static/resources/view/fchomo/node.js:1667 msgid "Health check timeout" msgstr "健康检查超时" #: htdocs/luci-static/resources/view/fchomo/client.js:1011 -#: htdocs/luci-static/resources/view/fchomo/node.js:1427 +#: htdocs/luci-static/resources/view/fchomo/node.js:1441 msgid "Health fields" msgstr "健康字段" @@ -1288,7 +1314,7 @@ msgstr "心跳间隔" msgid "Hidden" msgstr "隐藏" -#: htdocs/luci-static/resources/view/fchomo/node.js:816 +#: htdocs/luci-static/resources/view/fchomo/node.js:819 msgid "Host that supports TLS 1.3" msgstr "主机名称 (支援 TLS 1.3)" @@ -1300,7 +1326,7 @@ msgstr "主机密钥" msgid "Host-key algorithms" msgstr "主机密钥算法" -#: htdocs/luci-static/resources/view/fchomo/node.js:459 +#: htdocs/luci-static/resources/view/fchomo/node.js:466 msgid "Host/SNI override" msgstr "主机/SNI 覆盖" @@ -1309,8 +1335,8 @@ msgstr "主机/SNI 覆盖" msgid "Hosts" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:150 -#: htdocs/luci-static/resources/fchomo.js:183 +#: htdocs/luci-static/resources/fchomo.js:154 +#: htdocs/luci-static/resources/fchomo.js:188 msgid "Hysteria2" msgstr "" @@ -1323,20 +1349,20 @@ msgstr "" msgid "IP CIDR" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:509 +#: htdocs/luci-static/resources/view/fchomo/node.js:518 msgid "IP override" msgstr "IP 覆写" -#: htdocs/luci-static/resources/view/fchomo/node.js:1307 -#: htdocs/luci-static/resources/view/fchomo/node.js:1622 +#: htdocs/luci-static/resources/view/fchomo/node.js:1321 +#: htdocs/luci-static/resources/view/fchomo/node.js:1636 msgid "IP version" msgstr "IP 版本" -#: htdocs/luci-static/resources/fchomo.js:157 +#: htdocs/luci-static/resources/fchomo.js:162 msgid "IPv4 only" msgstr "仅 IPv4" -#: htdocs/luci-static/resources/fchomo.js:158 +#: htdocs/luci-static/resources/fchomo.js:163 msgid "IPv6 only" msgstr "仅 IPv6" @@ -1357,11 +1383,11 @@ msgstr "闲置会话检查间隔" msgid "Idle session timeout" msgstr "闲置会话超时" -#: htdocs/luci-static/resources/view/fchomo/server.js:469 +#: htdocs/luci-static/resources/fchomo/listeners.js:417 msgid "Idle timeout" msgstr "闲置超时" -#: htdocs/luci-static/resources/fchomo.js:1362 +#: htdocs/luci-static/resources/fchomo.js:1368 msgid "If All ports is selected, uncheck others" msgstr "如果选择了“所有端口”,则取消选中“其他”" @@ -1369,11 +1395,11 @@ msgstr "如果选择了“所有端口”,则取消选中“其他”" msgid "If Block is selected, uncheck others" msgstr "如果选择了“阻止”,则取消选中“其他”" -#: htdocs/luci-static/resources/view/fchomo/server.js:242 +#: htdocs/luci-static/resources/fchomo/listeners.js:186 msgid "Ignore client bandwidth" msgstr "忽略客户端带宽" -#: htdocs/luci-static/resources/fchomo.js:709 +#: htdocs/luci-static/resources/fchomo.js:715 msgid "Import" msgstr "导入" @@ -1389,8 +1415,8 @@ msgstr "导入" #: htdocs/luci-static/resources/view/fchomo/client.js:1713 #: htdocs/luci-static/resources/view/fchomo/client.js:1748 #: htdocs/luci-static/resources/view/fchomo/client.js:1769 -#: htdocs/luci-static/resources/view/fchomo/node.js:1332 -#: htdocs/luci-static/resources/view/fchomo/node.js:1413 +#: htdocs/luci-static/resources/view/fchomo/node.js:1346 +#: htdocs/luci-static/resources/view/fchomo/node.js:1427 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:143 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:234 msgid "Import mihomo config" @@ -1402,16 +1428,16 @@ msgstr "导入 mihomo 配置" msgid "Import rule-set links" msgstr "导入规则集链接" +#: htdocs/luci-static/resources/fchomo/listeners.js:175 +#: htdocs/luci-static/resources/fchomo/listeners.js:181 #: htdocs/luci-static/resources/view/fchomo/node.js:286 #: htdocs/luci-static/resources/view/fchomo/node.js:292 -#: htdocs/luci-static/resources/view/fchomo/node.js:1583 -#: htdocs/luci-static/resources/view/fchomo/node.js:1589 -#: htdocs/luci-static/resources/view/fchomo/server.js:231 -#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/node.js:1597 +#: htdocs/luci-static/resources/view/fchomo/node.js:1603 msgid "In Mbps." msgstr "单位为 Mbps。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1503 +#: htdocs/luci-static/resources/view/fchomo/node.js:1517 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:370 msgid "In bytes. %s will be used if empty." msgstr "单位为字节。留空则使用 %s。" @@ -1423,15 +1449,15 @@ msgstr "单位为毫秒。" #: htdocs/luci-static/resources/view/fchomo/client.js:1108 #: htdocs/luci-static/resources/view/fchomo/client.js:1137 -#: htdocs/luci-static/resources/view/fchomo/node.js:1654 +#: htdocs/luci-static/resources/view/fchomo/node.js:1668 msgid "In millisecond. %s will be used if empty." msgstr "单位为毫秒。留空则使用 %s。" +#: htdocs/luci-static/resources/fchomo/listeners.js:367 +#: htdocs/luci-static/resources/fchomo/listeners.js:418 +#: htdocs/luci-static/resources/fchomo/listeners.js:425 #: htdocs/luci-static/resources/view/fchomo/node.js:601 #: htdocs/luci-static/resources/view/fchomo/node.js:608 -#: htdocs/luci-static/resources/view/fchomo/server.js:417 -#: htdocs/luci-static/resources/view/fchomo/server.js:470 -#: htdocs/luci-static/resources/view/fchomo/server.js:477 msgid "In seconds." msgstr "单位为秒。" @@ -1440,14 +1466,14 @@ msgstr "单位为秒。" #: htdocs/luci-static/resources/view/fchomo/global.js:430 #: htdocs/luci-static/resources/view/fchomo/global.js:515 #: htdocs/luci-static/resources/view/fchomo/node.js:280 -#: htdocs/luci-static/resources/view/fchomo/node.js:1509 -#: htdocs/luci-static/resources/view/fchomo/node.js:1647 +#: htdocs/luci-static/resources/view/fchomo/node.js:1523 +#: htdocs/luci-static/resources/view/fchomo/node.js:1661 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:376 msgid "In seconds. %s will be used if empty." msgstr "单位为秒。留空则使用 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:917 -#: htdocs/luci-static/resources/view/fchomo/server.js:657 +#: htdocs/luci-static/resources/fchomo/listeners.js:645 +#: htdocs/luci-static/resources/view/fchomo/node.js:928 msgid "" "In the order of one Padding-Length and one Padding-" "Interval, infinite concatenation." @@ -1456,6 +1482,8 @@ msgstr "" "序,无限连接。" #: htdocs/luci-static/resources/view/fchomo/global.js:449 +#: htdocs/luci-static/resources/view/fchomo/inbound.js:28 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 msgid "Inbound" msgstr "入站" @@ -1487,7 +1515,7 @@ msgstr "引入所有代理节点。" msgid "Info" msgstr "信息" -#: htdocs/luci-static/resources/view/fchomo/node.js:1442 +#: htdocs/luci-static/resources/view/fchomo/node.js:1456 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:272 msgid "Inline" msgstr "内嵌" @@ -1497,25 +1525,26 @@ msgid "Interface Control" msgstr "接口控制" #: htdocs/luci-static/resources/fchomo.js:49 -#: htdocs/luci-static/resources/fchomo.js:155 -#: htdocs/luci-static/resources/fchomo.js:348 +#: htdocs/luci-static/resources/fchomo.js:160 +#: htdocs/luci-static/resources/fchomo.js:354 msgid "Keep default" msgstr "保持缺省" -#: htdocs/luci-static/resources/view/fchomo/node.js:379 -#: htdocs/luci-static/resources/view/fchomo/server.js:299 +#: htdocs/luci-static/resources/fchomo/listeners.js:249 +#: htdocs/luci-static/resources/view/fchomo/node.js:385 msgid "Key" msgstr "密钥" -#: htdocs/luci-static/resources/view/fchomo/node.js:1042 -#: htdocs/luci-static/resources/view/fchomo/server.js:831 +#: htdocs/luci-static/resources/fchomo/listeners.js:825 +#: htdocs/luci-static/resources/view/fchomo/node.js:1056 msgid "Key path" msgstr "证书路径" -#: htdocs/luci-static/resources/view/fchomo/server.js:676 +#: htdocs/luci-static/resources/fchomo/listeners.js:664 msgid "Keypairs" msgstr "密钥对" +#: htdocs/luci-static/resources/fchomo/listeners.js:127 #: htdocs/luci-static/resources/view/fchomo/client.js:1014 #: htdocs/luci-static/resources/view/fchomo/client.js:1257 #: htdocs/luci-static/resources/view/fchomo/client.js:1349 @@ -1523,24 +1552,23 @@ msgstr "密钥对" #: htdocs/luci-static/resources/view/fchomo/client.js:1719 #: htdocs/luci-static/resources/view/fchomo/client.js:1775 #: htdocs/luci-static/resources/view/fchomo/node.js:229 -#: htdocs/luci-static/resources/view/fchomo/node.js:1430 -#: htdocs/luci-static/resources/view/fchomo/node.js:1715 +#: htdocs/luci-static/resources/view/fchomo/node.js:1444 +#: htdocs/luci-static/resources/view/fchomo/node.js:1729 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:252 -#: htdocs/luci-static/resources/view/fchomo/server.js:179 msgid "Label" msgstr "标签" #: htdocs/luci-static/resources/view/fchomo/client.js:1114 -#: htdocs/luci-static/resources/view/fchomo/node.js:1660 +#: htdocs/luci-static/resources/view/fchomo/node.js:1674 msgid "Lazy" msgstr "懒惰状态" -#: htdocs/luci-static/resources/view/fchomo/node.js:447 -#: htdocs/luci-static/resources/view/fchomo/server.js:436 +#: htdocs/luci-static/resources/fchomo/listeners.js:386 +#: htdocs/luci-static/resources/view/fchomo/node.js:453 msgid "Legacy" msgstr "传统" -#: htdocs/luci-static/resources/view/fchomo/server.js:527 +#: htdocs/luci-static/resources/fchomo/listeners.js:475 msgid "" "Legacy protocol support (VMess MD5 Authentication) is provided for " "compatibility purposes only, use of alterId > 1 is not recommended." @@ -1552,16 +1580,16 @@ msgstr "" msgid "Less compatibility and sometimes better performance." msgstr "有时性能更好。" -#: htdocs/luci-static/resources/view/fchomo/node.js:969 -#: htdocs/luci-static/resources/view/fchomo/server.js:812 +#: htdocs/luci-static/resources/fchomo/listeners.js:806 +#: htdocs/luci-static/resources/view/fchomo/node.js:980 msgid "List of supported application level protocols, in order of preference." msgstr "支持的应用层协议协商列表,按顺序排列。" -#: htdocs/luci-static/resources/view/fchomo/server.js:199 +#: htdocs/luci-static/resources/fchomo/listeners.js:147 msgid "Listen address" msgstr "监听地址" -#: htdocs/luci-static/resources/view/fchomo/server.js:176 +#: htdocs/luci-static/resources/fchomo/listeners.js:124 msgid "Listen fields" msgstr "监听字段" @@ -1569,8 +1597,8 @@ msgstr "监听字段" msgid "Listen interfaces" msgstr "监听接口" +#: htdocs/luci-static/resources/fchomo/listeners.js:152 #: htdocs/luci-static/resources/view/fchomo/client.js:1374 -#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Listen port" msgstr "监听端口" @@ -1578,26 +1606,26 @@ msgstr "监听端口" msgid "Listen ports" msgstr "监听端口" -#: htdocs/luci-static/resources/fchomo.js:215 +#: htdocs/luci-static/resources/fchomo.js:221 msgid "Load balance" msgstr "负载均衡" -#: htdocs/luci-static/resources/view/fchomo/node.js:1440 +#: htdocs/luci-static/resources/view/fchomo/node.js:1454 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:270 msgid "Local" msgstr "本地" #: htdocs/luci-static/resources/view/fchomo/node.js:694 -#: htdocs/luci-static/resources/view/fchomo/node.js:734 +#: htdocs/luci-static/resources/view/fchomo/node.js:737 msgid "Local IPv6 address" msgstr "本地 IPv6 地址" #: htdocs/luci-static/resources/view/fchomo/node.js:686 -#: htdocs/luci-static/resources/view/fchomo/node.js:726 +#: htdocs/luci-static/resources/view/fchomo/node.js:729 msgid "Local address" msgstr "本地地址" -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:62 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:70 msgid "Log" msgstr "日志" @@ -1613,21 +1641,21 @@ msgstr "日志为空。" msgid "Log level" msgstr "日志等级" -#: htdocs/luci-static/resources/fchomo.js:423 +#: htdocs/luci-static/resources/fchomo.js:429 msgid "Lowercase only" msgstr "仅限小写" #: htdocs/luci-static/resources/view/fchomo/global.js:502 #: htdocs/luci-static/resources/view/fchomo/node.js:700 -#: htdocs/luci-static/resources/view/fchomo/node.js:780 +#: htdocs/luci-static/resources/view/fchomo/node.js:783 msgid "MTU" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:185 +#: htdocs/luci-static/resources/fchomo.js:190 msgid "Masque" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:262 +#: htdocs/luci-static/resources/fchomo/listeners.js:206 msgid "Masquerade" msgstr "伪装" @@ -1659,12 +1687,12 @@ msgstr "匹配响应通过 ipcidr
" msgid "Match rule set." msgstr "匹配规则集。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1186 +#: htdocs/luci-static/resources/view/fchomo/node.js:1200 msgid "Max Early Data" msgstr "前置数据最大值" +#: htdocs/luci-static/resources/fchomo/listeners.js:411 #: htdocs/luci-static/resources/view/fchomo/node.js:543 -#: htdocs/luci-static/resources/view/fchomo/server.js:463 msgid "Max UDP relay packet size" msgstr "UDP 中继数据包最大尺寸" @@ -1672,8 +1700,8 @@ msgstr "UDP 中继数据包最大尺寸" msgid "Max count of failures" msgstr "最大失败次数" +#: htdocs/luci-static/resources/fchomo/listeners.js:180 #: htdocs/luci-static/resources/view/fchomo/node.js:291 -#: htdocs/luci-static/resources/view/fchomo/server.js:236 msgid "Max download speed" msgstr "最大下载速度" @@ -1681,17 +1709,17 @@ msgstr "最大下载速度" msgid "Max open streams" msgstr "限制打开流的数量" +#: htdocs/luci-static/resources/fchomo/listeners.js:174 #: htdocs/luci-static/resources/view/fchomo/node.js:285 -#: htdocs/luci-static/resources/view/fchomo/server.js:230 msgid "Max upload speed" msgstr "最大上传速度" -#: htdocs/luci-static/resources/view/fchomo/node.js:1223 -#: htdocs/luci-static/resources/view/fchomo/node.js:1239 +#: htdocs/luci-static/resources/view/fchomo/node.js:1237 +#: htdocs/luci-static/resources/view/fchomo/node.js:1253 msgid "Maximum connections" msgstr "最大连接数" -#: htdocs/luci-static/resources/view/fchomo/node.js:1237 +#: htdocs/luci-static/resources/view/fchomo/node.js:1251 msgid "" "Maximum multiplexed streams in a connection before opening a new connection." "
Conflict with %s and %s." @@ -1699,24 +1727,24 @@ msgstr "" "在打开新连接之前,连接中的最大多路复用流数量。
%s 和 " "%s 冲突。" -#: htdocs/luci-static/resources/view/fchomo/node.js:419 -#: htdocs/luci-static/resources/view/fchomo/server.js:401 +#: htdocs/luci-static/resources/fchomo/listeners.js:351 +#: htdocs/luci-static/resources/view/fchomo/node.js:425 msgid "Maximum padding rate" msgstr "最大填充率" -#: htdocs/luci-static/resources/view/fchomo/node.js:427 -#: htdocs/luci-static/resources/view/fchomo/server.js:409 +#: htdocs/luci-static/resources/fchomo/listeners.js:359 +#: htdocs/luci-static/resources/view/fchomo/node.js:433 msgid "" "Maximum padding rate must be greater than or equal to the minimum padding " "rate." msgstr "最大填充率必须大于等于最小填充率。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1236 +#: htdocs/luci-static/resources/view/fchomo/node.js:1250 msgid "Maximum streams" msgstr "最大流数量" -#: htdocs/luci-static/resources/fchomo.js:143 -#: htdocs/luci-static/resources/fchomo.js:175 +#: htdocs/luci-static/resources/fchomo.js:147 +#: htdocs/luci-static/resources/fchomo.js:180 msgid "Mieru" msgstr "" @@ -1728,7 +1756,7 @@ msgstr "Mihomo 客户端" #: htdocs/luci-static/resources/view/fchomo/log.js:158 #: htdocs/luci-static/resources/view/fchomo/log.js:163 -#: htdocs/luci-static/resources/view/fchomo/server.js:131 +#: htdocs/luci-static/resources/view/fchomo/server.js:23 msgid "Mihomo server" msgstr "Mihomo 服务端" @@ -1736,22 +1764,22 @@ msgstr "Mihomo 服务端" msgid "Min of idle sessions to keep" msgstr "要保留的最少闲置会话数" -#: htdocs/luci-static/resources/view/fchomo/node.js:1230 +#: htdocs/luci-static/resources/view/fchomo/node.js:1244 msgid "" "Minimum multiplexed streams in a connection before opening a new connection." msgstr "在打开新连接之前,连接中的最小多路复用流数量。" -#: htdocs/luci-static/resources/view/fchomo/node.js:412 -#: htdocs/luci-static/resources/view/fchomo/server.js:394 +#: htdocs/luci-static/resources/fchomo/listeners.js:344 +#: htdocs/luci-static/resources/view/fchomo/node.js:418 msgid "Minimum padding rate" msgstr "最小填充率" -#: htdocs/luci-static/resources/view/fchomo/node.js:1229 -#: htdocs/luci-static/resources/view/fchomo/node.js:1239 +#: htdocs/luci-static/resources/view/fchomo/node.js:1243 +#: htdocs/luci-static/resources/view/fchomo/node.js:1253 msgid "Minimum streams" msgstr "最小流数量" -#: htdocs/luci-static/resources/fchomo.js:141 +#: htdocs/luci-static/resources/fchomo.js:145 #: htdocs/luci-static/resources/view/fchomo/global.js:497 msgid "Mixed" msgstr "混合" @@ -1764,12 +1792,12 @@ msgstr "混合 系统 TCP 栈和 gVisor UDP 栈。" msgid "Mixed port" msgstr "混合端口" -#: htdocs/luci-static/resources/view/fchomo/node.js:1209 +#: htdocs/luci-static/resources/view/fchomo/node.js:1223 msgid "Multiplex" msgstr "多路复用" +#: htdocs/luci-static/resources/fchomo/listeners.js:123 #: htdocs/luci-static/resources/view/fchomo/node.js:226 -#: htdocs/luci-static/resources/view/fchomo/server.js:175 msgid "Multiplex fields" msgstr "多路复用字段" @@ -1782,7 +1810,11 @@ msgstr "多路复用" msgid "NOT" msgstr "NOT" -#: htdocs/luci-static/resources/view/fchomo/node.js:1515 +#: htdocs/luci-static/resources/fchomo/listeners.js:528 +msgid "Name of the Proxy group as outbound." +msgstr "出站代理组的名称。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1529 msgid "Name of the Proxy group to download provider." msgstr "用于下载供应商订阅的代理组名称。" @@ -1790,14 +1822,22 @@ msgstr "用于下载供应商订阅的代理组名称。" msgid "Name of the Proxy group to download rule set." msgstr "用于下载规则集订阅的代理组名称。" +#: htdocs/luci-static/resources/fchomo/listeners.js:522 +msgid "Name of the Sub rule used for inbound matching." +msgstr "用于入站匹配的子规则名称。" + #: htdocs/luci-static/resources/view/fchomo/node.js:527 msgid "Native UDP" msgstr "原生 UDP" -#: htdocs/luci-static/resources/fchomo.js:365 +#: htdocs/luci-static/resources/fchomo.js:371 msgid "Native appearance" msgstr "原生外观" +#: htdocs/luci-static/resources/fchomo/listeners.js:545 +msgid "Network type" +msgstr "网络类型" + #: htdocs/luci-static/resources/view/fchomo/global.js:443 msgid "No Authentication IP ranges" msgstr "无需认证的 IP 范围" @@ -1807,11 +1847,11 @@ msgid "No add'l params" msgstr "无附加参数" #: htdocs/luci-static/resources/view/fchomo/client.js:1115 -#: htdocs/luci-static/resources/view/fchomo/node.js:1661 +#: htdocs/luci-static/resources/view/fchomo/node.js:1675 msgid "No testing is performed when this provider node is not in use." msgstr "当此供应商的节点未使用时,不执行任何测试。" -#: htdocs/luci-static/resources/fchomo.js:670 +#: htdocs/luci-static/resources/fchomo.js:676 msgid "No valid %s found." msgstr "未找到有效的%s。" @@ -1821,22 +1861,22 @@ msgstr "未找到有效的规则集链接。" #: htdocs/luci-static/resources/view/fchomo/client.js:1041 #: htdocs/luci-static/resources/view/fchomo/node.js:217 -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:46 msgid "Node" msgstr "节点" #: htdocs/luci-static/resources/view/fchomo/client.js:1160 -#: htdocs/luci-static/resources/view/fchomo/node.js:1681 +#: htdocs/luci-static/resources/view/fchomo/node.js:1695 msgid "Node exclude filter" msgstr "排除节点" #: htdocs/luci-static/resources/view/fchomo/client.js:1165 -#: htdocs/luci-static/resources/view/fchomo/node.js:1688 +#: htdocs/luci-static/resources/view/fchomo/node.js:1702 msgid "Node exclude type" msgstr "排除节点类型" #: htdocs/luci-static/resources/view/fchomo/client.js:1155 -#: htdocs/luci-static/resources/view/fchomo/node.js:1675 +#: htdocs/luci-static/resources/view/fchomo/node.js:1689 msgid "Node filter" msgstr "过滤节点" @@ -1844,7 +1884,7 @@ msgstr "过滤节点" msgid "Node switch tolerance" msgstr "节点切换容差" -#: htdocs/luci-static/resources/fchomo.js:392 +#: htdocs/luci-static/resources/fchomo.js:398 msgid "None" msgstr "无" @@ -1852,41 +1892,41 @@ msgstr "无" msgid "Not Installed" msgstr "未安装" -#: htdocs/luci-static/resources/fchomo.js:1140 +#: htdocs/luci-static/resources/fchomo.js:1146 msgid "Not Running" msgstr "未在运行" -#: htdocs/luci-static/resources/view/fchomo/node.js:472 +#: htdocs/luci-static/resources/view/fchomo/node.js:479 msgid "OFF" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:474 +#: htdocs/luci-static/resources/view/fchomo/node.js:481 msgid "ON" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:809 +#: htdocs/luci-static/resources/view/fchomo/node.js:812 msgid "Obfs Mode" msgstr "Obfs 模式" +#: htdocs/luci-static/resources/fchomo/listeners.js:198 #: htdocs/luci-static/resources/view/fchomo/node.js:303 -#: htdocs/luci-static/resources/view/fchomo/server.js:254 msgid "Obfuscate password" msgstr "混淆密码" +#: htdocs/luci-static/resources/fchomo/listeners.js:192 +#: htdocs/luci-static/resources/fchomo/listeners.js:322 #: htdocs/luci-static/resources/view/fchomo/node.js:297 -#: htdocs/luci-static/resources/view/fchomo/node.js:401 -#: htdocs/luci-static/resources/view/fchomo/server.js:248 -#: htdocs/luci-static/resources/view/fchomo/server.js:372 +#: htdocs/luci-static/resources/view/fchomo/node.js:407 msgid "Obfuscate type" msgstr "混淆类型" -#: htdocs/luci-static/resources/view/fchomo/node.js:402 -#: htdocs/luci-static/resources/view/fchomo/server.js:373 +#: htdocs/luci-static/resources/fchomo/listeners.js:323 +#: htdocs/luci-static/resources/view/fchomo/node.js:408 msgid "Obfuscated as ASCII data stream" msgstr "混淆为 ASCII 数据流" -#: htdocs/luci-static/resources/view/fchomo/node.js:403 -#: htdocs/luci-static/resources/view/fchomo/server.js:374 +#: htdocs/luci-static/resources/fchomo/listeners.js:324 +#: htdocs/luci-static/resources/view/fchomo/node.js:409 msgid "Obfuscated as low-entropy data stream" msgstr "混淆为低熵数据流" @@ -1898,7 +1938,7 @@ msgstr "0-63 范围内的一个或多个数字,以逗号分隔" msgid "Only process traffic from specific interfaces. Leave empty for all." msgstr "只处理来自指定接口的流量。留空表示全部。" -#: htdocs/luci-static/resources/fchomo.js:1134 +#: htdocs/luci-static/resources/fchomo.js:1140 msgid "Open Dashboard" msgstr "打开面板" @@ -1912,11 +1952,11 @@ msgid "Override destination" msgstr "覆盖目标地址" #: htdocs/luci-static/resources/view/fchomo/client.js:1010 -#: htdocs/luci-static/resources/view/fchomo/node.js:1426 +#: htdocs/luci-static/resources/view/fchomo/node.js:1440 msgid "Override fields" msgstr "覆盖字段" -#: htdocs/luci-static/resources/view/fchomo/node.js:510 +#: htdocs/luci-static/resources/view/fchomo/node.js:519 msgid "Override the IP address of the server that DNS response." msgstr "覆盖 DNS 回应的服务器的 IP 地址。" @@ -1932,7 +1972,7 @@ msgstr "使用嗅探到的域名覆盖连接目标。" msgid "Override the existing ECS in original request." msgstr "覆盖原始请求中已有的 ECS。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1069 +#: htdocs/luci-static/resources/view/fchomo/node.js:1083 msgid "Overrides the domain name used for HTTPS record queries." msgstr "覆盖用于 HTTPS 记录查询的域名。" @@ -1940,11 +1980,11 @@ msgstr "覆盖用于 HTTPS 记录查询的域名。" msgid "Overview" msgstr "概览" -#: htdocs/luci-static/resources/view/fchomo/node.js:1150 +#: htdocs/luci-static/resources/view/fchomo/node.js:1164 msgid "POST" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1151 +#: htdocs/luci-static/resources/view/fchomo/node.js:1165 msgid "PUT" msgstr "" @@ -1952,31 +1992,31 @@ msgstr "" msgid "Packet encoding" msgstr "数据包编码" -#: htdocs/luci-static/resources/view/fchomo/server.js:507 +#: htdocs/luci-static/resources/fchomo/listeners.js:455 msgid "Padding scheme" msgstr "填充方案" -#: htdocs/luci-static/resources/view/fchomo/node.js:915 -#: htdocs/luci-static/resources/view/fchomo/server.js:655 +#: htdocs/luci-static/resources/fchomo/listeners.js:643 +#: htdocs/luci-static/resources/view/fchomo/node.js:926 msgid "Paddings" msgstr "填充 (Paddings)" +#: htdocs/luci-static/resources/fchomo/listeners.js:165 +#: htdocs/luci-static/resources/fchomo/listeners.js:221 +#: htdocs/luci-static/resources/fchomo/listeners.js:505 #: htdocs/luci-static/resources/view/fchomo/node.js:261 #: htdocs/luci-static/resources/view/fchomo/node.js:340 -#: htdocs/luci-static/resources/view/fchomo/node.js:824 -#: htdocs/luci-static/resources/view/fchomo/server.js:221 -#: htdocs/luci-static/resources/view/fchomo/server.js:277 -#: htdocs/luci-static/resources/view/fchomo/server.js:548 +#: htdocs/luci-static/resources/view/fchomo/node.js:827 msgid "Password" msgstr "密码" -#: htdocs/luci-static/resources/view/fchomo/node.js:1490 +#: htdocs/luci-static/resources/fchomo/listeners.js:583 +#: htdocs/luci-static/resources/view/fchomo/node.js:1504 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:357 -#: htdocs/luci-static/resources/view/fchomo/server.js:595 msgid "Payload" msgstr "Payload" -#: htdocs/luci-static/resources/view/fchomo/node.js:748 +#: htdocs/luci-static/resources/view/fchomo/node.js:751 msgid "Peer pubkic key" msgstr "对端公钥" @@ -1986,11 +2026,11 @@ msgid "" "it is not needed." msgstr "性能可能会略有下降,建议仅在需要时开启。" -#: htdocs/luci-static/resources/view/fchomo/node.js:775 +#: htdocs/luci-static/resources/view/fchomo/node.js:778 msgid "Periodically sends data packets to maintain connection persistence." msgstr "定期发送数据包以维持连接持久性。" -#: htdocs/luci-static/resources/view/fchomo/node.js:774 +#: htdocs/luci-static/resources/view/fchomo/node.js:777 msgid "Persistent keepalive" msgstr "持久连接" @@ -2013,8 +2053,8 @@ msgid "" "standards." msgstr "链接格式标准请参考 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1463 -#: htdocs/luci-static/resources/view/fchomo/node.js:1489 +#: htdocs/luci-static/resources/view/fchomo/node.js:1477 +#: htdocs/luci-static/resources/view/fchomo/node.js:1503 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:330 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:356 msgid "" @@ -2029,25 +2069,25 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1445 #: htdocs/luci-static/resources/view/fchomo/client.js:1697 #: htdocs/luci-static/resources/view/fchomo/client.js:1749 -#: htdocs/luci-static/resources/view/fchomo/node.js:1333 +#: htdocs/luci-static/resources/view/fchomo/node.js:1347 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:144 msgid "Please type %s fields of mihomo config.
" msgstr "请输入 mihomo 配置的 %s 字段。
" -#: htdocs/luci-static/resources/view/fchomo/node.js:798 -#: htdocs/luci-static/resources/view/fchomo/server.js:534 +#: htdocs/luci-static/resources/fchomo/listeners.js:491 +#: htdocs/luci-static/resources/view/fchomo/node.js:801 msgid "Plugin" msgstr "插件" -#: htdocs/luci-static/resources/view/fchomo/node.js:809 -#: htdocs/luci-static/resources/view/fchomo/node.js:816 -#: htdocs/luci-static/resources/view/fchomo/node.js:824 -#: htdocs/luci-static/resources/view/fchomo/node.js:830 -#: htdocs/luci-static/resources/view/fchomo/node.js:838 -#: htdocs/luci-static/resources/view/fchomo/node.js:844 -#: htdocs/luci-static/resources/view/fchomo/server.js:541 -#: htdocs/luci-static/resources/view/fchomo/server.js:548 -#: htdocs/luci-static/resources/view/fchomo/server.js:554 +#: htdocs/luci-static/resources/fchomo/listeners.js:498 +#: htdocs/luci-static/resources/fchomo/listeners.js:505 +#: htdocs/luci-static/resources/fchomo/listeners.js:511 +#: htdocs/luci-static/resources/view/fchomo/node.js:812 +#: htdocs/luci-static/resources/view/fchomo/node.js:819 +#: htdocs/luci-static/resources/view/fchomo/node.js:827 +#: htdocs/luci-static/resources/view/fchomo/node.js:833 +#: htdocs/luci-static/resources/view/fchomo/node.js:841 +#: htdocs/luci-static/resources/view/fchomo/node.js:847 msgid "Plugin:" msgstr "插件:" @@ -2055,7 +2095,7 @@ msgstr "插件:" msgid "Port" msgstr "端口" -#: htdocs/luci-static/resources/fchomo.js:1371 +#: htdocs/luci-static/resources/fchomo.js:1377 msgid "Port %s alrealy exists!" msgstr "端口 %s 已存在!" @@ -2071,21 +2111,21 @@ msgstr "端口范围" msgid "Ports" msgstr "端口" +#: htdocs/luci-static/resources/fchomo/listeners.js:152 #: htdocs/luci-static/resources/view/fchomo/node.js:274 -#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Ports pool" msgstr "端口池" -#: htdocs/luci-static/resources/view/fchomo/node.js:487 -#: htdocs/luci-static/resources/view/fchomo/node.js:755 +#: htdocs/luci-static/resources/view/fchomo/node.js:496 +#: htdocs/luci-static/resources/view/fchomo/node.js:758 msgid "Pre-shared key" msgstr "预共享密钥" -#: htdocs/luci-static/resources/fchomo.js:159 +#: htdocs/luci-static/resources/fchomo.js:164 msgid "Prefer IPv4" msgstr "优先 IPv4" -#: htdocs/luci-static/resources/fchomo.js:160 +#: htdocs/luci-static/resources/fchomo.js:165 msgid "Prefer IPv6" msgstr "优先 IPv6" @@ -2096,10 +2136,10 @@ msgstr "防止某些情况下的 ICMP 环回问题。Ping 不会显示实际延 #: htdocs/luci-static/resources/view/fchomo/global.js:736 #: htdocs/luci-static/resources/view/fchomo/global.js:753 -#: htdocs/luci-static/resources/view/fchomo/node.js:1297 -#: htdocs/luci-static/resources/view/fchomo/node.js:1303 -#: htdocs/luci-static/resources/view/fchomo/node.js:1610 -#: htdocs/luci-static/resources/view/fchomo/node.js:1617 +#: htdocs/luci-static/resources/view/fchomo/node.js:1311 +#: htdocs/luci-static/resources/view/fchomo/node.js:1317 +#: htdocs/luci-static/resources/view/fchomo/node.js:1624 +#: htdocs/luci-static/resources/view/fchomo/node.js:1631 msgid "Priority: Proxy Node > Global." msgstr "优先级: 代理节点 > 全局。" @@ -2112,7 +2152,7 @@ msgid "Priv-key passphrase" msgstr "密钥密码" #: htdocs/luci-static/resources/view/fchomo/node.js:671 -#: htdocs/luci-static/resources/view/fchomo/node.js:740 +#: htdocs/luci-static/resources/view/fchomo/node.js:743 msgid "Private key" msgstr "私钥" @@ -2121,7 +2161,7 @@ msgid "Process matching mode" msgstr "进程匹配模式" #: htdocs/luci-static/resources/view/fchomo/global.js:684 -#: htdocs/luci-static/resources/view/fchomo/node.js:1215 +#: htdocs/luci-static/resources/view/fchomo/node.js:1229 msgid "Protocol" msgstr "协议" @@ -2136,13 +2176,13 @@ msgid "" msgstr "协议参数。 如启用会随机浪费流量(在 v2ray 中默认启用并且无法禁用)。" #: htdocs/luci-static/resources/view/fchomo/client.js:1055 -#: htdocs/luci-static/resources/view/fchomo/node.js:1316 -#: htdocs/luci-static/resources/view/fchomo/node.js:1325 -#: htdocs/luci-static/resources/view/fchomo/node.js:1726 +#: htdocs/luci-static/resources/view/fchomo/node.js:1330 +#: htdocs/luci-static/resources/view/fchomo/node.js:1339 +#: htdocs/luci-static/resources/view/fchomo/node.js:1740 msgid "Provider" msgstr "供应商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1496 +#: htdocs/luci-static/resources/view/fchomo/node.js:1510 msgid "Provider URL" msgstr "供应商订阅 URL" @@ -2165,18 +2205,19 @@ msgid "Proxy MAC-s" msgstr "代理 MAC 地址" #: htdocs/luci-static/resources/view/fchomo/node.js:208 -#: htdocs/luci-static/resources/view/fchomo/node.js:1725 +#: htdocs/luci-static/resources/view/fchomo/node.js:1739 msgid "Proxy Node" msgstr "代理节点" -#: htdocs/luci-static/resources/view/fchomo/node.js:1701 -#: htdocs/luci-static/resources/view/fchomo/node.js:1710 +#: htdocs/luci-static/resources/view/fchomo/node.js:1715 +#: htdocs/luci-static/resources/view/fchomo/node.js:1724 msgid "Proxy chain" msgstr "代理链" +#: htdocs/luci-static/resources/fchomo/listeners.js:527 #: htdocs/luci-static/resources/view/fchomo/client.js:783 #: htdocs/luci-static/resources/view/fchomo/client.js:1543 -#: htdocs/luci-static/resources/view/fchomo/node.js:1514 +#: htdocs/luci-static/resources/view/fchomo/node.js:1528 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:381 msgid "Proxy group" msgstr "代理组" @@ -2194,57 +2235,53 @@ msgid "Proxy routerself" msgstr "代理路由器自身" #: htdocs/luci-static/resources/view/fchomo/node.js:528 +#: htdocs/luci-static/resources/view/fchomo/node.js:723 msgid "QUIC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:516 -#: htdocs/luci-static/resources/view/fchomo/server.js:455 -msgid "QUIC congestion controller." -msgstr "QUIC 拥塞控制器。" - #: htdocs/luci-static/resources/view/fchomo/client.js:926 -#: htdocs/luci-static/resources/view/fchomo/server.js:152 +#: htdocs/luci-static/resources/view/fchomo/server.js:44 msgid "Quick Reload" msgstr "快速重载" -#: htdocs/luci-static/resources/view/fchomo/node.js:1083 -#: htdocs/luci-static/resources/view/fchomo/server.js:919 +#: htdocs/luci-static/resources/fchomo/listeners.js:913 +#: htdocs/luci-static/resources/view/fchomo/node.js:1097 msgid "REALITY" msgstr "REALITY" -#: htdocs/luci-static/resources/view/fchomo/node.js:1098 +#: htdocs/luci-static/resources/view/fchomo/node.js:1112 msgid "REALITY X25519MLKEM768 PQC support" msgstr "REALITY X25519MLKEM768 后量子加密支持" -#: htdocs/luci-static/resources/view/fchomo/server.js:956 +#: htdocs/luci-static/resources/fchomo/listeners.js:950 msgid "REALITY certificate issued to" msgstr "REALITY 证书颁发给" -#: htdocs/luci-static/resources/view/fchomo/server.js:924 +#: htdocs/luci-static/resources/fchomo/listeners.js:918 msgid "REALITY handshake server" msgstr "REALITY 握手服务器" -#: htdocs/luci-static/resources/view/fchomo/server.js:931 +#: htdocs/luci-static/resources/fchomo/listeners.js:925 msgid "REALITY private key" msgstr "REALITY 私钥" -#: htdocs/luci-static/resources/view/fchomo/node.js:1088 -#: htdocs/luci-static/resources/view/fchomo/server.js:946 +#: htdocs/luci-static/resources/fchomo/listeners.js:940 +#: htdocs/luci-static/resources/view/fchomo/node.js:1102 msgid "REALITY public key" msgstr "REALITY 公钥" -#: htdocs/luci-static/resources/view/fchomo/node.js:1093 -#: htdocs/luci-static/resources/view/fchomo/server.js:950 +#: htdocs/luci-static/resources/fchomo/listeners.js:944 +#: htdocs/luci-static/resources/view/fchomo/node.js:1107 msgid "REALITY short ID" msgstr "REALITY 标识符" -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/server.js:626 -#: htdocs/luci-static/resources/view/fchomo/server.js:645 +#: htdocs/luci-static/resources/fchomo/listeners.js:614 +#: htdocs/luci-static/resources/fchomo/listeners.js:633 +#: htdocs/luci-static/resources/view/fchomo/node.js:916 msgid "RTT" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:357 +#: htdocs/luci-static/resources/fchomo.js:363 msgid "Random" msgstr "随机" @@ -2252,7 +2289,7 @@ msgstr "随机" msgid "Random will be used if empty." msgstr "留空将使用随机令牌。" -#: htdocs/luci-static/resources/fchomo.js:367 +#: htdocs/luci-static/resources/fchomo.js:373 msgid "Randomized traffic characteristics" msgstr "随机化流量特征" @@ -2276,10 +2313,10 @@ msgstr "Redirect TCP + Tun UDP" msgid "Refresh every %s seconds." msgstr "每 %s 秒刷新。" -#: htdocs/luci-static/resources/fchomo.js:1127 +#: htdocs/luci-static/resources/fchomo.js:1133 #: htdocs/luci-static/resources/view/fchomo/client.js:927 #: htdocs/luci-static/resources/view/fchomo/global.js:193 -#: htdocs/luci-static/resources/view/fchomo/server.js:153 +#: htdocs/luci-static/resources/view/fchomo/server.js:45 msgid "Reload" msgstr "重载" @@ -2287,43 +2324,43 @@ msgstr "重载" msgid "Reload All" msgstr "重载所有" -#: htdocs/luci-static/resources/view/fchomo/node.js:1441 +#: htdocs/luci-static/resources/view/fchomo/node.js:1455 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:271 msgid "Remote" msgstr "远程" #: htdocs/luci-static/resources/view/fchomo/node.js:706 -#: htdocs/luci-static/resources/view/fchomo/node.js:786 +#: htdocs/luci-static/resources/view/fchomo/node.js:789 msgid "Remote DNS resolve" msgstr "远程 DNS 解析" -#: htdocs/luci-static/resources/fchomo.js:1292 +#: htdocs/luci-static/resources/fchomo.js:1298 msgid "Remove" msgstr "移除" -#: htdocs/luci-static/resources/fchomo.js:1297 -#: htdocs/luci-static/resources/view/fchomo/node.js:1417 -#: htdocs/luci-static/resources/view/fchomo/node.js:1419 +#: htdocs/luci-static/resources/fchomo.js:1303 +#: htdocs/luci-static/resources/view/fchomo/node.js:1431 +#: htdocs/luci-static/resources/view/fchomo/node.js:1433 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:244 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:246 msgid "Remove idles" msgstr "移除闲置" -#: htdocs/luci-static/resources/view/fchomo/node.js:1543 +#: htdocs/luci-static/resources/view/fchomo/node.js:1557 msgid "Replace name" msgstr "名称替换" -#: htdocs/luci-static/resources/view/fchomo/node.js:1544 +#: htdocs/luci-static/resources/view/fchomo/node.js:1558 msgid "Replace node name." msgstr "替换节点名称" -#: htdocs/luci-static/resources/fchomo.js:341 +#: htdocs/luci-static/resources/fchomo.js:347 msgid "Request" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1157 -#: htdocs/luci-static/resources/view/fchomo/node.js:1164 -#: htdocs/luci-static/resources/view/fchomo/server.js:998 +#: htdocs/luci-static/resources/fchomo/listeners.js:992 +#: htdocs/luci-static/resources/view/fchomo/node.js:1171 +#: htdocs/luci-static/resources/view/fchomo/node.js:1178 msgid "Request path" msgstr "请求路径" @@ -2331,20 +2368,20 @@ msgstr "请求路径" msgid "Request timeout" msgstr "请求超时" -#: htdocs/luci-static/resources/fchomo.js:344 +#: htdocs/luci-static/resources/fchomo.js:350 msgid "Require and verify" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:342 +#: htdocs/luci-static/resources/fchomo.js:348 msgid "Require any" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:375 -#: htdocs/luci-static/resources/view/fchomo/node.js:1099 +#: htdocs/luci-static/resources/fchomo.js:381 +#: htdocs/luci-static/resources/view/fchomo/node.js:1113 msgid "Requires server support." msgstr "需要服务器支持。" -#: htdocs/luci-static/resources/view/fchomo/node.js:769 +#: htdocs/luci-static/resources/view/fchomo/node.js:772 msgid "Reserved field bytes" msgstr "保留字段字节" @@ -2352,7 +2389,7 @@ msgstr "保留字段字节" msgid "Resources management" msgstr "资源管理" -#: htdocs/luci-static/resources/view/fchomo/node.js:844 +#: htdocs/luci-static/resources/view/fchomo/node.js:847 msgid "Restls script" msgstr "Restls 剧本" @@ -2366,12 +2403,12 @@ msgid "" "Returns the string input for icon in the API to display in this proxy group." msgstr "在 API 返回 icon 所输入的字符串,以在该代理组显示。" -#: htdocs/luci-static/resources/view/fchomo/node.js:473 +#: htdocs/luci-static/resources/view/fchomo/node.js:480 msgid "Reuse HTTP connections to reduce RTT for each connection establishment." msgstr "重用 HTTP 连接以减少每次建立连接的 RTT。" -#: htdocs/luci-static/resources/view/fchomo/node.js:470 -#: htdocs/luci-static/resources/view/fchomo/node.js:474 +#: htdocs/luci-static/resources/view/fchomo/node.js:477 +#: htdocs/luci-static/resources/view/fchomo/node.js:481 msgid "Reusing a single tunnel to carry multiple target connections within it." msgstr "复用单条隧道使其内部承载多条目标连线。" @@ -2388,8 +2425,8 @@ msgstr "路由 DSCP" msgid "Routing GFW" msgstr "路由 GFW 流量" -#: htdocs/luci-static/resources/view/fchomo/node.js:1302 -#: htdocs/luci-static/resources/view/fchomo/node.js:1616 +#: htdocs/luci-static/resources/view/fchomo/node.js:1316 +#: htdocs/luci-static/resources/view/fchomo/node.js:1630 msgid "Routing mark" msgstr "路由标记" @@ -2441,7 +2478,7 @@ msgstr "规则集" msgid "Rule set URL" msgstr "规则集订阅 URL" -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:46 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 msgid "Ruleset" msgstr "规则集" @@ -2449,27 +2486,27 @@ msgstr "规则集" msgid "Ruleset-URI-Scheme" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1140 +#: htdocs/luci-static/resources/fchomo.js:1146 msgid "Running" msgstr "正在运行" -#: htdocs/luci-static/resources/fchomo.js:223 +#: htdocs/luci-static/resources/fchomo.js:229 msgid "SMTP" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:140 +#: htdocs/luci-static/resources/fchomo.js:144 msgid "SOCKS" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:172 +#: htdocs/luci-static/resources/fchomo.js:177 msgid "SOCKS5" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:187 +#: htdocs/luci-static/resources/fchomo.js:193 msgid "SSH" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:224 +#: htdocs/luci-static/resources/fchomo.js:230 msgid "STUN" msgstr "" @@ -2477,20 +2514,20 @@ msgstr "" msgid "SUB-RULE" msgstr "SUB-RULE" -#: htdocs/luci-static/resources/view/fchomo/node.js:862 +#: htdocs/luci-static/resources/view/fchomo/node.js:873 msgid "SUoT version" msgstr "SUoT 版本" +#: htdocs/luci-static/resources/fchomo/listeners.js:194 #: htdocs/luci-static/resources/view/fchomo/node.js:299 -#: htdocs/luci-static/resources/view/fchomo/server.js:250 msgid "Salamander" msgstr "Salamander" -#: htdocs/luci-static/resources/fchomo.js:165 +#: htdocs/luci-static/resources/fchomo.js:170 msgid "Same dstaddr requests. Same node" msgstr "相同 目标地址 请求。相同节点" -#: htdocs/luci-static/resources/fchomo.js:166 +#: htdocs/luci-static/resources/fchomo.js:171 msgid "Same srcaddr and dstaddr requests. Same node" msgstr "相同 来源地址 和 目标地址 请求。相同节点" @@ -2498,7 +2535,7 @@ msgstr "相同 来源地址 和 目标地址 请求。相同节点" msgid "Segment maximum size" msgstr "分段最大尺寸" -#: htdocs/luci-static/resources/fchomo.js:212 +#: htdocs/luci-static/resources/fchomo.js:218 msgid "Select" msgstr "手动选择" @@ -2506,20 +2543,20 @@ msgstr "手动选择" msgid "Select Dashboard" msgstr "选择面板" -#: htdocs/luci-static/resources/fchomo.js:381 +#: htdocs/luci-static/resources/fchomo.js:387 msgid "Send padding randomly 0-3333 bytes with 50% probability." msgstr "以 50% 的概率发送随机 0-3333 字节的填充。" -#: htdocs/luci-static/resources/fchomo.js:370 -#: htdocs/luci-static/resources/fchomo.js:371 +#: htdocs/luci-static/resources/fchomo.js:376 +#: htdocs/luci-static/resources/fchomo.js:377 msgid "Send random ticket of 300s-600s duration for client 0-RTT reuse." msgstr "发送 300-600 秒的随机票证,以供客户端 0-RTT 重用。" -#: htdocs/luci-static/resources/view/fchomo/server.js:166 -#: htdocs/luci-static/resources/view/fchomo/server.js:626 -#: htdocs/luci-static/resources/view/fchomo/server.js:817 -#: htdocs/luci-static/resources/view/fchomo/server.js:832 -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 +#: htdocs/luci-static/resources/fchomo/listeners.js:614 +#: htdocs/luci-static/resources/fchomo/listeners.js:811 +#: htdocs/luci-static/resources/fchomo/listeners.js:826 +#: htdocs/luci-static/resources/view/fchomo/server.js:58 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:62 msgid "Server" msgstr "服务端" @@ -2527,7 +2564,7 @@ msgstr "服务端" msgid "Server address" msgstr "服务器地址" -#: htdocs/luci-static/resources/view/fchomo/node.js:1142 +#: htdocs/luci-static/resources/view/fchomo/node.js:1156 msgid "Server hostname" msgstr "服务器主机名称" @@ -2539,27 +2576,27 @@ msgstr "服务端状态" msgid "Service status" msgstr "服务状态" -#: htdocs/luci-static/resources/fchomo.js:142 -#: htdocs/luci-static/resources/fchomo.js:173 +#: htdocs/luci-static/resources/fchomo.js:146 +#: htdocs/luci-static/resources/fchomo.js:178 msgid "Shadowsocks" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:437 #: htdocs/luci-static/resources/view/fchomo/node.js:582 -#: htdocs/luci-static/resources/view/fchomo/server.js:489 msgid "Shadowsocks chipher" msgstr "Shadowsocks 加密方法" +#: htdocs/luci-static/resources/fchomo/listeners.js:432 #: htdocs/luci-static/resources/view/fchomo/node.js:577 -#: htdocs/luci-static/resources/view/fchomo/server.js:484 msgid "Shadowsocks encrypt" msgstr "Shadowsocks 加密" +#: htdocs/luci-static/resources/fchomo/listeners.js:445 #: htdocs/luci-static/resources/view/fchomo/node.js:590 -#: htdocs/luci-static/resources/view/fchomo/server.js:497 msgid "Shadowsocks password" msgstr "Shadowsocks 密码" -#: htdocs/luci-static/resources/view/fchomo/node.js:1257 +#: htdocs/luci-static/resources/view/fchomo/node.js:1271 msgid "Show connections in the dashboard for breaking connections easier." msgstr "在面板中显示连接以便于打断连接。" @@ -2567,18 +2604,18 @@ msgstr "在面板中显示连接以便于打断连接。" msgid "Silent" msgstr "静音" -#: htdocs/luci-static/resources/fchomo.js:164 +#: htdocs/luci-static/resources/fchomo.js:169 msgid "Simple round-robin all nodes" msgstr "简单轮替所有节点" -#: htdocs/luci-static/resources/view/fchomo/node.js:1502 +#: htdocs/luci-static/resources/view/fchomo/node.js:1516 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:369 msgid "Size limit" msgstr "大小限制" #: htdocs/luci-static/resources/view/fchomo/client.js:1576 -#: htdocs/luci-static/resources/view/fchomo/node.js:1020 -#: htdocs/luci-static/resources/view/fchomo/node.js:1594 +#: htdocs/luci-static/resources/view/fchomo/node.js:1034 +#: htdocs/luci-static/resources/view/fchomo/node.js:1608 msgid "Skip cert verify" msgstr "跳过证书验证" @@ -2594,7 +2631,7 @@ msgstr "跳过嗅探目标地址" msgid "Skiped sniffing src address" msgstr "跳过嗅探来源地址" -#: htdocs/luci-static/resources/fchomo.js:177 +#: htdocs/luci-static/resources/fchomo.js:182 msgid "Snell" msgstr "" @@ -2610,7 +2647,7 @@ msgstr "嗅探器" msgid "Sniffer settings" msgstr "嗅探器设置" -#: htdocs/luci-static/resources/fchomo.js:413 +#: htdocs/luci-static/resources/fchomo.js:419 msgid "Specify a ID" msgstr "指定一个ID" @@ -2625,11 +2662,11 @@ msgstr "指定需要被代理的目标端口。多个端口必须用逗号隔开 msgid "Stack" msgstr "堆栈" -#: htdocs/luci-static/resources/fchomo.js:227 +#: htdocs/luci-static/resources/fchomo.js:233 msgid "Steam Client" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:228 +#: htdocs/luci-static/resources/fchomo.js:234 msgid "Steam P2P" msgstr "" @@ -2638,6 +2675,7 @@ msgstr "" msgid "Strategy" msgstr "策略" +#: htdocs/luci-static/resources/fchomo/listeners.js:521 #: htdocs/luci-static/resources/view/fchomo/client.js:1303 #: htdocs/luci-static/resources/view/fchomo/client.js:1312 msgid "Sub rule" @@ -2647,7 +2685,7 @@ msgstr "子规则" msgid "Sub rule group" msgstr "子规则组" -#: htdocs/luci-static/resources/fchomo.js:673 +#: htdocs/luci-static/resources/fchomo.js:679 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:215 msgid "Successfully imported %s %s of total %s." msgstr "已成功导入 %s 个%s (共 %s 个)。" @@ -2656,12 +2694,12 @@ msgstr "已成功导入 %s 个%s (共 %s 个)。" msgid "Successfully updated." msgstr "更新成功。" -#: htdocs/luci-static/resources/fchomo.js:1642 +#: htdocs/luci-static/resources/fchomo.js:1648 msgid "Successfully uploaded." msgstr "已成功上传。" -#: htdocs/luci-static/resources/fchomo.js:144 -#: htdocs/luci-static/resources/fchomo.js:176 +#: htdocs/luci-static/resources/fchomo.js:148 +#: htdocs/luci-static/resources/fchomo.js:181 msgid "Sudoku" msgstr "" @@ -2681,20 +2719,21 @@ msgstr "系统" msgid "System DNS" msgstr "系统 DNS" -#: htdocs/luci-static/resources/fchomo.js:139 -#: htdocs/luci-static/resources/fchomo.js:144 -#: htdocs/luci-static/resources/fchomo.js:145 -#: htdocs/luci-static/resources/fchomo.js:146 -#: htdocs/luci-static/resources/fchomo.js:147 +#: htdocs/luci-static/resources/fchomo.js:143 #: htdocs/luci-static/resources/fchomo.js:148 -#: htdocs/luci-static/resources/fchomo.js:171 +#: htdocs/luci-static/resources/fchomo.js:149 +#: htdocs/luci-static/resources/fchomo.js:150 +#: htdocs/luci-static/resources/fchomo.js:151 +#: htdocs/luci-static/resources/fchomo.js:152 #: htdocs/luci-static/resources/fchomo.js:176 -#: htdocs/luci-static/resources/fchomo.js:177 -#: htdocs/luci-static/resources/fchomo.js:178 -#: htdocs/luci-static/resources/fchomo.js:179 -#: htdocs/luci-static/resources/fchomo.js:180 #: htdocs/luci-static/resources/fchomo.js:181 -#: htdocs/luci-static/resources/fchomo.js:187 +#: htdocs/luci-static/resources/fchomo.js:182 +#: htdocs/luci-static/resources/fchomo.js:183 +#: htdocs/luci-static/resources/fchomo.js:184 +#: htdocs/luci-static/resources/fchomo.js:185 +#: htdocs/luci-static/resources/fchomo.js:186 +#: htdocs/luci-static/resources/fchomo.js:193 +#: htdocs/luci-static/resources/fchomo/listeners.js:546 #: htdocs/luci-static/resources/view/fchomo/client.js:589 #: htdocs/luci-static/resources/view/fchomo/client.js:679 msgid "TCP" @@ -2704,7 +2743,7 @@ msgstr "TCP" msgid "TCP concurrency" msgstr "TCP 并发" -#: htdocs/luci-static/resources/view/fchomo/node.js:1250 +#: htdocs/luci-static/resources/view/fchomo/node.js:1264 msgid "TCP only" msgstr "仅 TCP" @@ -2716,54 +2755,61 @@ msgstr "TCP-Keep-Alive 闲置超时" msgid "TCP-Keep-Alive interval" msgstr "TCP-Keep-Alive 间隔" -#: htdocs/luci-static/resources/fchomo.js:140 -#: htdocs/luci-static/resources/fchomo.js:141 -#: htdocs/luci-static/resources/fchomo.js:142 -#: htdocs/luci-static/resources/fchomo.js:143 -#: htdocs/luci-static/resources/fchomo.js:170 -#: htdocs/luci-static/resources/fchomo.js:172 -#: htdocs/luci-static/resources/fchomo.js:173 +#: htdocs/luci-static/resources/fchomo.js:144 +#: htdocs/luci-static/resources/fchomo.js:145 +#: htdocs/luci-static/resources/fchomo.js:146 +#: htdocs/luci-static/resources/fchomo.js:147 +#: htdocs/luci-static/resources/fchomo.js:155 +#: htdocs/luci-static/resources/fchomo.js:156 #: htdocs/luci-static/resources/fchomo.js:175 +#: htdocs/luci-static/resources/fchomo.js:177 +#: htdocs/luci-static/resources/fchomo.js:178 +#: htdocs/luci-static/resources/fchomo.js:180 +#: htdocs/luci-static/resources/fchomo.js:191 msgid "TCP/UDP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1281 -#: htdocs/luci-static/resources/view/fchomo/node.js:1561 +#: htdocs/luci-static/resources/view/fchomo/node.js:1295 +#: htdocs/luci-static/resources/view/fchomo/node.js:1575 msgid "TFO" msgstr "TCP 快速打开 (TFO)" +#: htdocs/luci-static/resources/fchomo/listeners.js:768 #: htdocs/luci-static/resources/view/fchomo/global.js:529 -#: htdocs/luci-static/resources/view/fchomo/node.js:454 -#: htdocs/luci-static/resources/view/fchomo/node.js:811 -#: htdocs/luci-static/resources/view/fchomo/node.js:937 -#: htdocs/luci-static/resources/view/fchomo/server.js:780 +#: htdocs/luci-static/resources/view/fchomo/node.js:461 +#: htdocs/luci-static/resources/view/fchomo/node.js:814 +#: htdocs/luci-static/resources/view/fchomo/node.js:948 msgid "TLS" msgstr "TLS" -#: htdocs/luci-static/resources/view/fchomo/node.js:968 -#: htdocs/luci-static/resources/view/fchomo/server.js:811 +#: htdocs/luci-static/resources/fchomo/listeners.js:805 +#: htdocs/luci-static/resources/view/fchomo/node.js:979 msgid "TLS ALPN" msgstr "TLS ALPN" -#: htdocs/luci-static/resources/view/fchomo/node.js:962 +#: htdocs/luci-static/resources/view/fchomo/node.js:973 msgid "TLS SNI" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:121 #: htdocs/luci-static/resources/view/fchomo/node.js:224 -#: htdocs/luci-static/resources/view/fchomo/server.js:173 msgid "TLS fields" msgstr "TLS字段" -#: htdocs/luci-static/resources/fchomo.js:149 -#: htdocs/luci-static/resources/fchomo.js:184 +#: htdocs/luci-static/resources/fchomo.js:153 +#: htdocs/luci-static/resources/fchomo.js:189 msgid "TUIC" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:225 +#: htdocs/luci-static/resources/fchomo.js:231 msgid "TURN" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:243 +#: htdocs/luci-static/resources/fchomo/listeners.js:484 +msgid "Target address" +msgstr "目标地址" + +#: htdocs/luci-static/resources/fchomo/listeners.js:187 msgid "" "Tell the client to use the BBR flow control algorithm instead of Hysteria CC." msgstr "让客户端使用 BBR 流控算法。" @@ -2773,34 +2819,34 @@ msgstr "让客户端使用 BBR 流控算法。" msgid "The %s address used by local machine in the Cloudflare WARP network." msgstr "Cloudflare WARP 网络中使用的本机 %s 地址。" -#: htdocs/luci-static/resources/view/fchomo/node.js:727 -#: htdocs/luci-static/resources/view/fchomo/node.js:735 +#: htdocs/luci-static/resources/view/fchomo/node.js:730 +#: htdocs/luci-static/resources/view/fchomo/node.js:738 msgid "The %s address used by local machine in the Wireguard network." msgstr "WireGuard 网络中使用的本机 %s 地址。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1043 -#: htdocs/luci-static/resources/view/fchomo/server.js:832 +#: htdocs/luci-static/resources/fchomo/listeners.js:826 +#: htdocs/luci-static/resources/view/fchomo/node.js:1057 msgid "The %s private key, in PEM format." msgstr "%s私钥,需要 PEM 格式。" +#: htdocs/luci-static/resources/fchomo/listeners.js:811 +#: htdocs/luci-static/resources/fchomo/listeners.js:849 #: htdocs/luci-static/resources/view/fchomo/global.js:550 -#: htdocs/luci-static/resources/view/fchomo/node.js:1029 -#: htdocs/luci-static/resources/view/fchomo/server.js:817 -#: htdocs/luci-static/resources/view/fchomo/server.js:855 +#: htdocs/luci-static/resources/view/fchomo/node.js:1043 msgid "The %s public key, in PEM format." msgstr "%s公钥,需要 PEM 格式。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1063 +#: htdocs/luci-static/resources/view/fchomo/node.js:1077 msgid "" "The ECH parameter of the HTTPS record for the domain. Leave empty to resolve " "via DNS." msgstr "域名的 HTTPS 记录的 ECH 参数。留空则通过 DNS 解析。" -#: htdocs/luci-static/resources/view/fchomo/node.js:380 +#: htdocs/luci-static/resources/view/fchomo/node.js:386 msgid "The ED25519 available private key or UUID provided by Sudoku server." msgstr "Sudoku 服务器提供的 ED25519 可用私钥 或 UUID。" -#: htdocs/luci-static/resources/view/fchomo/server.js:300 +#: htdocs/luci-static/resources/fchomo/listeners.js:250 msgid "The ED25519 master public key or UUID generated by Sudoku." msgstr "Sudoku 生成的 ED25519 主公钥 或 UUID。" @@ -2808,8 +2854,8 @@ msgstr "Sudoku 生成的 ED25519 主公钥 或 UUID。" msgid "The default value is 2:00 every day." msgstr "默认值为每天 2:00。" -#: htdocs/luci-static/resources/view/fchomo/node.js:918 -#: htdocs/luci-static/resources/view/fchomo/server.js:658 +#: htdocs/luci-static/resources/fchomo/listeners.js:646 +#: htdocs/luci-static/resources/view/fchomo/node.js:929 msgid "" "The first padding must have a probability of 100% and at least 35 bytes." msgstr "首个填充必须为 100% 的概率并且至少 35 字节。" @@ -2824,19 +2870,19 @@ msgstr "匹配 %s 的将被视为未被投毒污染。" msgid "The matching %s will be deemed as poisoned." msgstr "匹配 %s 的将被视为已被投毒污染。" -#: htdocs/luci-static/resources/view/fchomo/node.js:916 -#: htdocs/luci-static/resources/view/fchomo/server.js:656 +#: htdocs/luci-static/resources/fchomo/listeners.js:644 +#: htdocs/luci-static/resources/view/fchomo/node.js:927 msgid "The server and client can set different padding parameters." msgstr "服务器和客户端可以设置不同的填充参数。" +#: htdocs/luci-static/resources/fchomo/listeners.js:907 #: htdocs/luci-static/resources/view/fchomo/global.js:594 -#: htdocs/luci-static/resources/view/fchomo/server.js:913 msgid "This ECH parameter needs to be added to the HTTPS record of the domain." msgstr "此 ECH 参数需要添加到域名的 HTTPS 记录中。" #: htdocs/luci-static/resources/view/fchomo/client.js:1579 -#: htdocs/luci-static/resources/view/fchomo/node.js:1023 -#: htdocs/luci-static/resources/view/fchomo/node.js:1597 +#: htdocs/luci-static/resources/view/fchomo/node.js:1037 +#: htdocs/luci-static/resources/view/fchomo/node.js:1611 msgid "" "This is DANGEROUS, your traffic is almost like " "PLAIN TEXT! Use at your own risk!" @@ -2875,28 +2921,33 @@ msgstr "Tproxy Fwmark/fwmask" msgid "Tproxy port" msgstr "Tproxy 端口" -#: htdocs/luci-static/resources/view/fchomo/node.js:1753 +#: htdocs/luci-static/resources/fchomo/listeners.js:238 +#: htdocs/luci-static/resources/view/fchomo/node.js:378 +msgid "Traffic pattern" +msgstr "流量模式" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1767 msgid "Transit proxy group" msgstr "中转代理组" -#: htdocs/luci-static/resources/view/fchomo/node.js:1759 +#: htdocs/luci-static/resources/view/fchomo/node.js:1773 msgid "Transit proxy node" msgstr "中转代理节点" +#: htdocs/luci-static/resources/fchomo/listeners.js:231 +#: htdocs/luci-static/resources/fchomo/listeners.js:958 #: htdocs/luci-static/resources/view/fchomo/node.js:355 -#: htdocs/luci-static/resources/view/fchomo/node.js:1105 -#: htdocs/luci-static/resources/view/fchomo/server.js:287 -#: htdocs/luci-static/resources/view/fchomo/server.js:964 +#: htdocs/luci-static/resources/view/fchomo/node.js:1119 msgid "Transport" msgstr "传输层" +#: htdocs/luci-static/resources/fchomo/listeners.js:122 #: htdocs/luci-static/resources/view/fchomo/node.js:225 -#: htdocs/luci-static/resources/view/fchomo/server.js:174 msgid "Transport fields" msgstr "传输层字段" -#: htdocs/luci-static/resources/view/fchomo/node.js:1110 -#: htdocs/luci-static/resources/view/fchomo/server.js:969 +#: htdocs/luci-static/resources/fchomo/listeners.js:963 +#: htdocs/luci-static/resources/view/fchomo/node.js:1124 msgid "Transport type" msgstr "传输层类型" @@ -2904,11 +2955,16 @@ msgstr "传输层类型" msgid "Treat the destination IP as the source IP." msgstr "将 目标 IP 视为 来源 IP。" -#: htdocs/luci-static/resources/fchomo.js:147 -#: htdocs/luci-static/resources/fchomo.js:180 +#: htdocs/luci-static/resources/fchomo.js:151 +#: htdocs/luci-static/resources/fchomo.js:185 msgid "Trojan" msgstr "" +#: htdocs/luci-static/resources/fchomo.js:155 +#: htdocs/luci-static/resources/fchomo.js:191 +msgid "TrustTunnel" +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/global.js:764 msgid "Tun Fwmark/fwmask" msgstr "Tun Fwmark/fwmask" @@ -2925,30 +2981,35 @@ msgstr "Tun 设置" msgid "Tun stack." msgstr "Tun 堆栈" +#: htdocs/luci-static/resources/fchomo.js:156 +msgid "Tunnel" +msgstr "" + +#: htdocs/luci-static/resources/fchomo/listeners.js:141 #: htdocs/luci-static/resources/view/fchomo/client.js:530 #: htdocs/luci-static/resources/view/fchomo/client.js:643 #: htdocs/luci-static/resources/view/fchomo/client.js:737 #: htdocs/luci-static/resources/view/fchomo/client.js:842 #: htdocs/luci-static/resources/view/fchomo/client.js:1028 #: htdocs/luci-static/resources/view/fchomo/node.js:238 -#: htdocs/luci-static/resources/view/fchomo/node.js:1439 -#: htdocs/luci-static/resources/view/fchomo/node.js:1724 +#: htdocs/luci-static/resources/view/fchomo/node.js:1453 +#: htdocs/luci-static/resources/view/fchomo/node.js:1738 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:269 -#: htdocs/luci-static/resources/view/fchomo/server.js:193 msgid "Type" msgstr "类型" -#: htdocs/luci-static/resources/fchomo.js:149 -#: htdocs/luci-static/resources/fchomo.js:150 -#: htdocs/luci-static/resources/fchomo.js:183 -#: htdocs/luci-static/resources/fchomo.js:184 -#: htdocs/luci-static/resources/fchomo.js:185 -#: htdocs/luci-static/resources/fchomo.js:186 +#: htdocs/luci-static/resources/fchomo.js:153 +#: htdocs/luci-static/resources/fchomo.js:154 +#: htdocs/luci-static/resources/fchomo.js:188 +#: htdocs/luci-static/resources/fchomo.js:189 +#: htdocs/luci-static/resources/fchomo.js:190 +#: htdocs/luci-static/resources/fchomo.js:192 +#: htdocs/luci-static/resources/fchomo/listeners.js:547 +#: htdocs/luci-static/resources/fchomo/listeners.js:551 #: htdocs/luci-static/resources/view/fchomo/client.js:588 #: htdocs/luci-static/resources/view/fchomo/client.js:678 -#: htdocs/luci-static/resources/view/fchomo/node.js:851 -#: htdocs/luci-static/resources/view/fchomo/node.js:1571 -#: htdocs/luci-static/resources/view/fchomo/server.js:563 +#: htdocs/luci-static/resources/view/fchomo/node.js:862 +#: htdocs/luci-static/resources/view/fchomo/node.js:1585 msgid "UDP" msgstr "UDP" @@ -2972,19 +3033,19 @@ msgstr "UDP 包中继模式。" msgid "UDP relay mode" msgstr "UDP 中继模式" -#: htdocs/luci-static/resources/fchomo.js:214 +#: htdocs/luci-static/resources/fchomo.js:220 msgid "URL test" msgstr "自动选择" -#: htdocs/luci-static/resources/view/fchomo/node.js:503 +#: htdocs/luci-static/resources/fchomo/listeners.js:247 +#: htdocs/luci-static/resources/fchomo/listeners.js:405 +#: htdocs/luci-static/resources/fchomo/listeners.js:460 +#: htdocs/luci-static/resources/view/fchomo/node.js:512 #: htdocs/luci-static/resources/view/fchomo/node.js:621 -#: htdocs/luci-static/resources/view/fchomo/server.js:297 -#: htdocs/luci-static/resources/view/fchomo/server.js:448 -#: htdocs/luci-static/resources/view/fchomo/server.js:512 msgid "UUID" msgstr "UUID" -#: htdocs/luci-static/resources/fchomo.js:1185 +#: htdocs/luci-static/resources/fchomo.js:1191 msgid "Unable to download unsupported type: %s" msgstr "无法下载不支持的类型: %s" @@ -3009,8 +3070,8 @@ msgstr "未知错误。" msgid "Unknown error: %s" msgstr "未知错误:%s" -#: htdocs/luci-static/resources/view/fchomo/node.js:856 -#: htdocs/luci-static/resources/view/fchomo/node.js:1576 +#: htdocs/luci-static/resources/view/fchomo/node.js:867 +#: htdocs/luci-static/resources/view/fchomo/node.js:1590 msgid "UoT" msgstr "UDP over TCP (UoT)" @@ -3018,22 +3079,22 @@ msgstr "UDP over TCP (UoT)" msgid "Update failed." msgstr "更新失败。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1508 +#: htdocs/luci-static/resources/view/fchomo/node.js:1522 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:375 msgid "Update interval" msgstr "更新间隔" -#: htdocs/luci-static/resources/view/fchomo/node.js:1268 +#: htdocs/luci-static/resources/view/fchomo/node.js:1282 msgid "Upload bandwidth" msgstr "上传带宽" -#: htdocs/luci-static/resources/view/fchomo/node.js:1269 +#: htdocs/luci-static/resources/view/fchomo/node.js:1283 msgid "Upload bandwidth in Mbps." msgstr "上传带宽(单位:Mbps)。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1034 -#: htdocs/luci-static/resources/view/fchomo/server.js:823 -#: htdocs/luci-static/resources/view/fchomo/server.js:863 +#: htdocs/luci-static/resources/fchomo/listeners.js:817 +#: htdocs/luci-static/resources/fchomo/listeners.js:857 +#: htdocs/luci-static/resources/view/fchomo/node.js:1048 msgid "Upload certificate" msgstr "上传证书" @@ -3041,17 +3102,17 @@ msgstr "上传证书" msgid "Upload initial package" msgstr "上传初始资源包" -#: htdocs/luci-static/resources/view/fchomo/node.js:1048 -#: htdocs/luci-static/resources/view/fchomo/server.js:838 +#: htdocs/luci-static/resources/fchomo/listeners.js:832 +#: htdocs/luci-static/resources/view/fchomo/node.js:1062 msgid "Upload key" msgstr "上传密钥" +#: htdocs/luci-static/resources/fchomo/listeners.js:820 +#: htdocs/luci-static/resources/fchomo/listeners.js:835 +#: htdocs/luci-static/resources/fchomo/listeners.js:860 #: htdocs/luci-static/resources/view/fchomo/global.js:306 -#: htdocs/luci-static/resources/view/fchomo/node.js:1037 #: htdocs/luci-static/resources/view/fchomo/node.js:1051 -#: htdocs/luci-static/resources/view/fchomo/server.js:826 -#: htdocs/luci-static/resources/view/fchomo/server.js:841 -#: htdocs/luci-static/resources/view/fchomo/server.js:866 +#: htdocs/luci-static/resources/view/fchomo/node.js:1065 msgid "Upload..." msgstr "上传..." @@ -3075,7 +3136,7 @@ msgstr "用于解析 DNS 服务器的域名。必须是 IP。" msgid "Used to resolve the domain of the Proxy node." msgstr "用于解析代理节点的域名。" -#: htdocs/luci-static/resources/view/fchomo/node.js:963 +#: htdocs/luci-static/resources/view/fchomo/node.js:974 msgid "Used to verify the hostname on the returned certificates." msgstr "用于验证返回的证书上的主机名。" @@ -3083,8 +3144,8 @@ msgstr "用于验证返回的证书上的主机名。" msgid "User Authentication" msgstr "用户认证" +#: htdocs/luci-static/resources/fchomo/listeners.js:160 #: htdocs/luci-static/resources/view/fchomo/node.js:256 -#: htdocs/luci-static/resources/view/fchomo/server.js:216 msgid "Username" msgstr "用户名" @@ -3092,50 +3153,50 @@ msgstr "用户名" msgid "Users filter mode" msgstr "使用者过滤模式" -#: htdocs/luci-static/resources/view/fchomo/node.js:1198 +#: htdocs/luci-static/resources/view/fchomo/node.js:1212 msgid "V2ray HTTPUpgrade" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1203 +#: htdocs/luci-static/resources/view/fchomo/node.js:1217 msgid "V2ray HTTPUpgrade fast open" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:146 -#: htdocs/luci-static/resources/fchomo.js:179 +#: htdocs/luci-static/resources/fchomo.js:150 +#: htdocs/luci-static/resources/fchomo.js:184 msgid "VLESS" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:145 -#: htdocs/luci-static/resources/fchomo.js:178 +#: htdocs/luci-static/resources/fchomo.js:149 +#: htdocs/luci-static/resources/fchomo.js:183 msgid "VMess" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1445 -#: htdocs/luci-static/resources/view/fchomo/node.js:1730 +#: htdocs/luci-static/resources/view/fchomo/node.js:1459 +#: htdocs/luci-static/resources/view/fchomo/node.js:1744 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:312 msgid "Value" msgstr "可视化值" -#: htdocs/luci-static/resources/fchomo.js:343 +#: htdocs/luci-static/resources/fchomo.js:349 msgid "Verify if given" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:494 -#: htdocs/luci-static/resources/view/fchomo/node.js:830 -#: htdocs/luci-static/resources/view/fchomo/server.js:554 +#: htdocs/luci-static/resources/fchomo/listeners.js:511 +#: htdocs/luci-static/resources/view/fchomo/node.js:503 +#: htdocs/luci-static/resources/view/fchomo/node.js:833 msgid "Version" msgstr "版本" -#: htdocs/luci-static/resources/view/fchomo/node.js:838 +#: htdocs/luci-static/resources/view/fchomo/node.js:841 msgid "Version hint" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:120 #: htdocs/luci-static/resources/view/fchomo/node.js:223 -#: htdocs/luci-static/resources/view/fchomo/server.js:172 msgid "Vless Encryption fields" msgstr "Vless Encryption 字段" -#: htdocs/luci-static/resources/fchomo.js:380 +#: htdocs/luci-static/resources/fchomo.js:386 msgid "Wait a random 0-111 milliseconds with 75% probability." msgstr "以 75% 的概率等待随机 0-111 毫秒。" @@ -3143,16 +3204,19 @@ msgstr "以 75% 的概率等待随机 0-111 毫秒。" msgid "Warning" msgstr "警告" -#: htdocs/luci-static/resources/view/fchomo/node.js:1115 -#: htdocs/luci-static/resources/view/fchomo/node.js:1126 -#: htdocs/luci-static/resources/view/fchomo/node.js:1131 -#: htdocs/luci-static/resources/view/fchomo/server.js:971 -#: htdocs/luci-static/resources/view/fchomo/server.js:982 -#: htdocs/luci-static/resources/view/fchomo/server.js:987 +#: htdocs/luci-static/resources/fchomo/listeners.js:390 +#: htdocs/luci-static/resources/fchomo/listeners.js:965 +#: htdocs/luci-static/resources/fchomo/listeners.js:976 +#: htdocs/luci-static/resources/fchomo/listeners.js:981 +#: htdocs/luci-static/resources/view/fchomo/node.js:457 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +#: htdocs/luci-static/resources/view/fchomo/node.js:1129 +#: htdocs/luci-static/resources/view/fchomo/node.js:1140 +#: htdocs/luci-static/resources/view/fchomo/node.js:1145 msgid "WebSocket" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:132 +#: htdocs/luci-static/resources/view/fchomo/server.js:24 msgid "When used as a server, HomeProxy is a better choice." msgstr "用作服务端时,HomeProxy 是更好的选择。" @@ -3160,23 +3224,23 @@ msgstr "用作服务端时,HomeProxy 是更好的选择。" msgid "White list" msgstr "白名单" -#: htdocs/luci-static/resources/fchomo.js:186 +#: htdocs/luci-static/resources/fchomo.js:192 msgid "WireGuard" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:749 +#: htdocs/luci-static/resources/view/fchomo/node.js:752 msgid "WireGuard peer public key." msgstr "WireGuard 对端公钥。" -#: htdocs/luci-static/resources/view/fchomo/node.js:756 +#: htdocs/luci-static/resources/view/fchomo/node.js:759 msgid "WireGuard pre-shared key." msgstr "WireGuard 预共享密钥。" -#: htdocs/luci-static/resources/view/fchomo/node.js:741 +#: htdocs/luci-static/resources/view/fchomo/node.js:744 msgid "WireGuard requires base64-encoded private keys." msgstr "WireGuard 要求 base64 编码的私钥。" -#: htdocs/luci-static/resources/view/fchomo/server.js:617 +#: htdocs/luci-static/resources/fchomo/listeners.js:605 msgid "XOR mode" msgstr "XOR 模式" @@ -3192,23 +3256,23 @@ msgstr "Yaml 格式文本" msgid "YouTube" msgstr "油管" -#: htdocs/luci-static/resources/fchomo.js:1624 +#: htdocs/luci-static/resources/fchomo.js:1630 msgid "Your %s was successfully uploaded. Size: %sB." msgstr "您的 %s 已成功上传。大小:%sB。" -#: htdocs/luci-static/resources/fchomo.js:316 -#: htdocs/luci-static/resources/fchomo.js:329 -#: htdocs/luci-static/resources/fchomo.js:334 +#: htdocs/luci-static/resources/fchomo.js:322 +#: htdocs/luci-static/resources/fchomo.js:335 +#: htdocs/luci-static/resources/fchomo.js:340 #: htdocs/luci-static/resources/view/fchomo/node.js:646 msgid "aes-128-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:317 +#: htdocs/luci-static/resources/fchomo.js:323 msgid "aes-192-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:318 -#: htdocs/luci-static/resources/fchomo.js:335 +#: htdocs/luci-static/resources/fchomo.js:324 +#: htdocs/luci-static/resources/fchomo.js:341 msgid "aes-256-gcm" msgstr "" @@ -3220,15 +3284,15 @@ msgstr "自动" msgid "bbr" msgstr "bbr" -#: htdocs/luci-static/resources/view/fchomo/node.js:1039 -#: htdocs/luci-static/resources/view/fchomo/server.js:828 -#: htdocs/luci-static/resources/view/fchomo/server.js:868 +#: htdocs/luci-static/resources/fchomo/listeners.js:822 +#: htdocs/luci-static/resources/fchomo/listeners.js:862 +#: htdocs/luci-static/resources/view/fchomo/node.js:1053 msgid "certificate" msgstr "证书" -#: htdocs/luci-static/resources/fchomo.js:319 -#: htdocs/luci-static/resources/fchomo.js:330 +#: htdocs/luci-static/resources/fchomo.js:325 #: htdocs/luci-static/resources/fchomo.js:336 +#: htdocs/luci-static/resources/fchomo.js:342 msgid "chacha20-ietf-poly1305" msgstr "" @@ -3240,8 +3304,8 @@ msgstr "" msgid "cubic" msgstr "cubic" -#: htdocs/luci-static/resources/view/fchomo/server.js:569 -#: htdocs/luci-static/resources/view/fchomo/server.js:600 +#: htdocs/luci-static/resources/fchomo/listeners.js:557 +#: htdocs/luci-static/resources/fchomo/listeners.js:588 msgid "decryption" msgstr "decryption" @@ -3249,36 +3313,36 @@ msgstr "decryption" msgid "dnsmasq selects upstream on its own. (may affect CDN accuracy)" msgstr "dnsmasq 自行选择上游服务器。 (可能影响 CDN 准确性)" -#: htdocs/luci-static/resources/view/fchomo/node.js:1588 +#: htdocs/luci-static/resources/view/fchomo/node.js:1602 msgid "down" msgstr "Hysteria 下载速率" -#: htdocs/luci-static/resources/view/fchomo/node.js:870 -#: htdocs/luci-static/resources/view/fchomo/node.js:893 -#: htdocs/luci-static/resources/view/fchomo/server.js:604 +#: htdocs/luci-static/resources/fchomo/listeners.js:592 +#: htdocs/luci-static/resources/view/fchomo/node.js:881 +#: htdocs/luci-static/resources/view/fchomo/node.js:904 msgid "encryption" msgstr "encryption" -#: htdocs/luci-static/resources/view/fchomo/node.js:435 -#: htdocs/luci-static/resources/view/fchomo/server.js:424 +#: htdocs/luci-static/resources/fchomo/listeners.js:374 +#: htdocs/luci-static/resources/view/fchomo/node.js:441 msgid "false = bandwidth optimized downlink; true = pure Sudoku downlink." msgstr "false = 带宽优化下行 true = 纯 Sudoku 下行。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1114 -#: htdocs/luci-static/resources/view/fchomo/node.js:1125 -#: htdocs/luci-static/resources/view/fchomo/node.js:1130 -#: htdocs/luci-static/resources/view/fchomo/server.js:970 -#: htdocs/luci-static/resources/view/fchomo/server.js:981 -#: htdocs/luci-static/resources/view/fchomo/server.js:986 +#: htdocs/luci-static/resources/fchomo/listeners.js:964 +#: htdocs/luci-static/resources/fchomo/listeners.js:975 +#: htdocs/luci-static/resources/fchomo/listeners.js:980 +#: htdocs/luci-static/resources/view/fchomo/node.js:1128 +#: htdocs/luci-static/resources/view/fchomo/node.js:1139 +#: htdocs/luci-static/resources/view/fchomo/node.js:1144 msgid "gRPC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1181 +#: htdocs/luci-static/resources/view/fchomo/node.js:1195 msgid "gRPC User-Agent" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1177 -#: htdocs/luci-static/resources/view/fchomo/server.js:1005 +#: htdocs/luci-static/resources/fchomo/listeners.js:999 +#: htdocs/luci-static/resources/view/fchomo/node.js:1191 msgid "gRPC service name" msgstr "gRPC 服务名称" @@ -3286,11 +3350,11 @@ msgstr "gRPC 服务名称" msgid "gVisor" msgstr "gVisor" -#: htdocs/luci-static/resources/view/fchomo/node.js:1219 +#: htdocs/luci-static/resources/view/fchomo/node.js:1233 msgid "h2mux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:769 +#: htdocs/luci-static/resources/fchomo/listeners.js:757 msgid "least one keypair required" msgstr "至少需要一对密钥" @@ -3304,17 +3368,17 @@ msgstr "metacubexd" #: htdocs/luci-static/resources/view/fchomo/client.js:1480 #: htdocs/luci-static/resources/view/fchomo/client.js:1711 #: htdocs/luci-static/resources/view/fchomo/client.js:1767 -#: htdocs/luci-static/resources/view/fchomo/node.js:1411 +#: htdocs/luci-static/resources/view/fchomo/node.js:1425 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:232 msgid "mihomo config" msgstr "mihomo 配置" -#: htdocs/luci-static/resources/fchomo.js:362 +#: htdocs/luci-static/resources/fchomo.js:368 msgid "mlkem768x25519plus" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1285 -#: htdocs/luci-static/resources/view/fchomo/node.js:1566 +#: htdocs/luci-static/resources/view/fchomo/node.js:1299 +#: htdocs/luci-static/resources/view/fchomo/node.js:1580 msgid "mpTCP" msgstr "多路径 TCP (mpTCP)" @@ -3326,21 +3390,21 @@ msgstr "new_reno" msgid "no-resolve" msgstr "no-resolve" -#: htdocs/luci-static/resources/fchomo.js:1359 -#: htdocs/luci-static/resources/fchomo.js:1454 -#: htdocs/luci-static/resources/fchomo.js:1489 -#: htdocs/luci-static/resources/fchomo.js:1517 +#: htdocs/luci-static/resources/fchomo.js:1365 +#: htdocs/luci-static/resources/fchomo.js:1460 +#: htdocs/luci-static/resources/fchomo.js:1495 +#: htdocs/luci-static/resources/fchomo.js:1523 msgid "non-empty value" msgstr "非空值" -#: htdocs/luci-static/resources/fchomo.js:314 -#: htdocs/luci-static/resources/fchomo.js:328 -#: htdocs/luci-static/resources/fchomo.js:340 +#: htdocs/luci-static/resources/fchomo.js:320 +#: htdocs/luci-static/resources/fchomo.js:334 +#: htdocs/luci-static/resources/fchomo.js:346 +#: htdocs/luci-static/resources/fchomo/listeners.js:492 #: htdocs/luci-static/resources/view/fchomo/node.js:644 #: htdocs/luci-static/resources/view/fchomo/node.js:664 -#: htdocs/luci-static/resources/view/fchomo/node.js:799 +#: htdocs/luci-static/resources/view/fchomo/node.js:802 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:308 -#: htdocs/luci-static/resources/view/fchomo/server.js:535 msgid "none" msgstr "无" @@ -3352,19 +3416,25 @@ msgstr "未找到" msgid "not included \",\"" msgstr "不包含 \",\"" -#: htdocs/luci-static/resources/fchomo.js:200 +#: htdocs/luci-static/resources/fchomo.js:206 +#: htdocs/luci-static/resources/fchomo/listeners.js:523 +#: htdocs/luci-static/resources/fchomo/listeners.js:524 msgid "null" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:800 +#: htdocs/luci-static/resources/view/fchomo/node.js:803 msgid "obfs-simple" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:479 -msgid "only applies when %s is stream/poll/auto." -msgstr "仅当 %s 为 stream/poll/auto 时适用。" +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +msgid "only applies when %s is %s." +msgstr "仅当 %s 为 %s 时适用。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1546 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +msgid "only applies when %s is not %s." +msgstr "仅当 %s 不为 %s 时适用。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1560 msgid "override.proxy-name" msgstr "" @@ -3372,13 +3442,13 @@ msgstr "" msgid "packet addr (v2ray-core v5+)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:449 -#: htdocs/luci-static/resources/view/fchomo/server.js:438 +#: htdocs/luci-static/resources/fchomo/listeners.js:388 +#: htdocs/luci-static/resources/view/fchomo/node.js:455 msgid "poll" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1053 -#: htdocs/luci-static/resources/view/fchomo/server.js:843 +#: htdocs/luci-static/resources/fchomo/listeners.js:837 +#: htdocs/luci-static/resources/view/fchomo/node.js:1067 msgid "private key" msgstr "私钥" @@ -3391,7 +3461,7 @@ msgstr "razord-meta" msgid "requires front-end adaptation using the API." msgstr "需要使用 API 的前端适配。" -#: htdocs/luci-static/resources/view/fchomo/node.js:804 +#: htdocs/luci-static/resources/view/fchomo/node.js:807 msgid "restls" msgstr "" @@ -3399,17 +3469,17 @@ msgstr "" msgid "rule-set" msgstr "规则集" -#: htdocs/luci-static/resources/view/fchomo/node.js:803 -#: htdocs/luci-static/resources/view/fchomo/server.js:536 +#: htdocs/luci-static/resources/fchomo/listeners.js:493 +#: htdocs/luci-static/resources/view/fchomo/node.js:806 msgid "shadow-tls" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1217 +#: htdocs/luci-static/resources/view/fchomo/node.js:1231 msgid "smux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:448 -#: htdocs/luci-static/resources/view/fchomo/server.js:437 +#: htdocs/luci-static/resources/fchomo/listeners.js:387 +#: htdocs/luci-static/resources/view/fchomo/node.js:454 msgid "split-stream" msgstr "" @@ -3417,7 +3487,11 @@ msgstr "" msgid "src" msgstr "src" -#: htdocs/luci-static/resources/view/fchomo/server.js:296 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +msgid "stream/poll/auto" +msgstr "" + +#: htdocs/luci-static/resources/fchomo/listeners.js:246 msgid "sudoku-keypair" msgstr "" @@ -3425,87 +3499,87 @@ msgstr "" msgid "unchecked" msgstr "未检查" -#: htdocs/luci-static/resources/fchomo.js:426 +#: htdocs/luci-static/resources/fchomo.js:432 msgid "unique UCI identifier" msgstr "独立 UCI 标识" -#: htdocs/luci-static/resources/fchomo.js:429 +#: htdocs/luci-static/resources/fchomo.js:435 msgid "unique identifier" msgstr "独立标识" -#: htdocs/luci-static/resources/fchomo.js:1526 +#: htdocs/luci-static/resources/fchomo.js:1532 msgid "unique value" msgstr "独立值" -#: htdocs/luci-static/resources/view/fchomo/node.js:1582 +#: htdocs/luci-static/resources/view/fchomo/node.js:1596 msgid "up" msgstr "Hysteria 上传速率" -#: htdocs/luci-static/resources/view/fchomo/node.js:495 +#: htdocs/luci-static/resources/fchomo/listeners.js:512 +#: htdocs/luci-static/resources/view/fchomo/node.js:504 #: htdocs/luci-static/resources/view/fchomo/node.js:539 -#: htdocs/luci-static/resources/view/fchomo/node.js:831 -#: htdocs/luci-static/resources/view/fchomo/node.js:863 -#: htdocs/luci-static/resources/view/fchomo/server.js:555 +#: htdocs/luci-static/resources/view/fchomo/node.js:834 +#: htdocs/luci-static/resources/view/fchomo/node.js:874 msgid "v1" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:496 -#: htdocs/luci-static/resources/view/fchomo/node.js:832 -#: htdocs/luci-static/resources/view/fchomo/node.js:864 -#: htdocs/luci-static/resources/view/fchomo/server.js:556 +#: htdocs/luci-static/resources/fchomo/listeners.js:513 +#: htdocs/luci-static/resources/view/fchomo/node.js:505 +#: htdocs/luci-static/resources/view/fchomo/node.js:835 +#: htdocs/luci-static/resources/view/fchomo/node.js:875 msgid "v2" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:497 -#: htdocs/luci-static/resources/view/fchomo/node.js:833 -#: htdocs/luci-static/resources/view/fchomo/server.js:557 +#: htdocs/luci-static/resources/fchomo/listeners.js:514 +#: htdocs/luci-static/resources/view/fchomo/node.js:506 +#: htdocs/luci-static/resources/view/fchomo/node.js:836 msgid "v3" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1406 -#: htdocs/luci-static/resources/fchomo.js:1409 +#: htdocs/luci-static/resources/fchomo.js:1412 +#: htdocs/luci-static/resources/fchomo.js:1415 msgid "valid JSON format" msgstr "有效的 JSON 格式" -#: htdocs/luci-static/resources/view/fchomo/node.js:1013 +#: htdocs/luci-static/resources/view/fchomo/node.js:1027 msgid "valid SHA256 string with %d characters" msgstr "包含 %d 个字符的有效 SHA256 字符串" -#: htdocs/luci-static/resources/fchomo.js:1431 -#: htdocs/luci-static/resources/fchomo.js:1434 +#: htdocs/luci-static/resources/fchomo.js:1437 +#: htdocs/luci-static/resources/fchomo.js:1440 msgid "valid URL" msgstr "有效网址" -#: htdocs/luci-static/resources/fchomo.js:1444 +#: htdocs/luci-static/resources/fchomo.js:1450 msgid "valid base64 key with %d characters" msgstr "包含 %d 个字符的有效 base64 密钥" -#: htdocs/luci-static/resources/fchomo.js:1504 #: htdocs/luci-static/resources/fchomo.js:1510 +#: htdocs/luci-static/resources/fchomo.js:1516 msgid "valid format: 2x, 2p, 4v" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1491 +#: htdocs/luci-static/resources/fchomo.js:1497 msgid "valid key length with %d characters" msgstr "包含 %d 个字符的有效密钥" -#: htdocs/luci-static/resources/fchomo.js:1369 +#: htdocs/luci-static/resources/fchomo.js:1375 msgid "valid port value" msgstr "有效端口值" -#: htdocs/luci-static/resources/fchomo.js:1419 +#: htdocs/luci-static/resources/fchomo.js:1425 msgid "valid uuid" msgstr "有效 uuid" -#: htdocs/luci-static/resources/fchomo.js:386 +#: htdocs/luci-static/resources/fchomo.js:392 msgid "vless-mlkem768" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:385 +#: htdocs/luci-static/resources/fchomo.js:391 msgid "vless-x25519" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:320 +#: htdocs/luci-static/resources/fchomo.js:326 msgid "xchacha20-ietf-poly1305" msgstr "" @@ -3513,7 +3587,7 @@ msgstr "" msgid "yacd-meta" msgstr "yacd-meta" -#: htdocs/luci-static/resources/view/fchomo/node.js:1218 +#: htdocs/luci-static/resources/view/fchomo/node.js:1232 msgid "yamux" msgstr "" @@ -3525,10 +3599,13 @@ msgstr "" msgid "zero" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1187 +#: htdocs/luci-static/resources/fchomo.js:1193 msgid "🡇" msgstr "" +#~ msgid "QUIC congestion controller." +#~ msgstr "QUIC 拥塞控制器。" + #~ msgid "" #~ "Uplink keeps the Sudoku protocol, and downlink characteristics are " #~ "consistent with uplink characteristics." diff --git a/small/luci-app-fchomo/po/zh_Hant/fchomo.po b/small/luci-app-fchomo/po/zh_Hant/fchomo.po index f5b5a1e570..2e87b96a64 100644 --- a/small/luci-app-fchomo/po/zh_Hant/fchomo.po +++ b/small/luci-app-fchomo/po/zh_Hant/fchomo.po @@ -12,30 +12,30 @@ msgstr "" msgid "%s log" msgstr "%s 日誌" -#: htdocs/luci-static/resources/fchomo.js:223 -#: htdocs/luci-static/resources/fchomo.js:224 -#: htdocs/luci-static/resources/fchomo.js:225 -#: htdocs/luci-static/resources/fchomo.js:226 -#: htdocs/luci-static/resources/fchomo.js:227 -#: htdocs/luci-static/resources/fchomo.js:228 +#: htdocs/luci-static/resources/fchomo.js:229 +#: htdocs/luci-static/resources/fchomo.js:230 +#: htdocs/luci-static/resources/fchomo.js:231 +#: htdocs/luci-static/resources/fchomo.js:232 +#: htdocs/luci-static/resources/fchomo.js:233 +#: htdocs/luci-static/resources/fchomo.js:234 msgid "%s ports" msgstr "%s 連接埠" -#: htdocs/luci-static/resources/fchomo.js:588 -#: htdocs/luci-static/resources/fchomo.js:591 +#: htdocs/luci-static/resources/fchomo.js:594 +#: htdocs/luci-static/resources/fchomo.js:597 #: htdocs/luci-static/resources/view/fchomo/client.js:315 msgid "(Imported)" msgstr "(已導入)" +#: htdocs/luci-static/resources/fchomo/listeners.js:840 +#: htdocs/luci-static/resources/fchomo/listeners.js:848 +#: htdocs/luci-static/resources/fchomo/listeners.js:857 #: htdocs/luci-static/resources/view/fchomo/global.js:543 #: htdocs/luci-static/resources/view/fchomo/global.js:549 -#: htdocs/luci-static/resources/view/fchomo/node.js:1028 -#: htdocs/luci-static/resources/view/fchomo/node.js:1034 #: htdocs/luci-static/resources/view/fchomo/node.js:1042 #: htdocs/luci-static/resources/view/fchomo/node.js:1048 -#: htdocs/luci-static/resources/view/fchomo/server.js:846 -#: htdocs/luci-static/resources/view/fchomo/server.js:854 -#: htdocs/luci-static/resources/view/fchomo/server.js:863 +#: htdocs/luci-static/resources/view/fchomo/node.js:1056 +#: htdocs/luci-static/resources/view/fchomo/node.js:1062 msgid "(mTLS)" msgstr "" @@ -46,19 +46,19 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1056 #: htdocs/luci-static/resources/view/fchomo/client.js:1057 #: htdocs/luci-static/resources/view/fchomo/client.js:1278 -#: htdocs/luci-static/resources/view/fchomo/node.js:1734 -#: htdocs/luci-static/resources/view/fchomo/node.js:1740 +#: htdocs/luci-static/resources/view/fchomo/node.js:1748 #: htdocs/luci-static/resources/view/fchomo/node.js:1754 -#: htdocs/luci-static/resources/view/fchomo/node.js:1760 +#: htdocs/luci-static/resources/view/fchomo/node.js:1768 +#: htdocs/luci-static/resources/view/fchomo/node.js:1774 msgid "-- Please choose --" msgstr "-- 請選擇 --" -#: htdocs/luci-static/resources/fchomo.js:375 +#: htdocs/luci-static/resources/fchomo.js:381 msgid "0-RTT reuse." msgstr "0-RTT 重用。" -#: htdocs/luci-static/resources/fchomo.js:372 -#: htdocs/luci-static/resources/fchomo.js:376 +#: htdocs/luci-static/resources/fchomo.js:378 +#: htdocs/luci-static/resources/fchomo.js:382 msgid "1-RTT only." msgstr "僅限 1-RTT。" @@ -66,15 +66,15 @@ msgstr "僅限 1-RTT。" msgid "163Music" msgstr "網易雲音樂" -#: htdocs/luci-static/resources/fchomo.js:322 +#: htdocs/luci-static/resources/fchomo.js:328 msgid "2022-blake3-aes-128-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:323 +#: htdocs/luci-static/resources/fchomo.js:329 msgid "2022-blake3-aes-256-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:324 +#: htdocs/luci-static/resources/fchomo.js:330 msgid "2022-blake3-chacha20-poly1305" msgstr "" @@ -82,14 +82,24 @@ msgstr "" msgid "0 or 1 only." msgstr "僅限 01。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1035 +#: htdocs/luci-static/resources/fchomo/listeners.js:818 +#: htdocs/luci-static/resources/fchomo/listeners.js:833 +#: htdocs/luci-static/resources/fchomo/listeners.js:858 #: htdocs/luci-static/resources/view/fchomo/node.js:1049 -#: htdocs/luci-static/resources/view/fchomo/server.js:824 -#: htdocs/luci-static/resources/view/fchomo/server.js:839 -#: htdocs/luci-static/resources/view/fchomo/server.js:864 +#: htdocs/luci-static/resources/view/fchomo/node.js:1063 msgid "Save your configuration before uploading files!" msgstr "上傳文件前請先保存配置!" +#: htdocs/luci-static/resources/fchomo/listeners.js:239 +#: htdocs/luci-static/resources/view/fchomo/node.js:379 +msgid "" +"A base64 string is used to fine-tune network behavior.
Please refer to " +"the document." +msgstr "" +"一個 base64 字串用於微調網路行為。
格式請參考
文檔。" + #: htdocs/luci-static/resources/view/fchomo/global.js:599 msgid "API" msgstr "API" @@ -159,11 +169,15 @@ msgstr "新增 DNS 伺服器" msgid "Add a Node" msgstr "新增 節點" -#: htdocs/luci-static/resources/view/fchomo/node.js:1325 +#: htdocs/luci-static/resources/view/fchomo/inbound.js:28 +msgid "Add a inbound" +msgstr "新增 入站" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1339 msgid "Add a provider" msgstr "新增 供應商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1710 +#: htdocs/luci-static/resources/view/fchomo/node.js:1724 msgid "Add a proxy chain" msgstr "新增 代理鏈" @@ -179,7 +193,7 @@ msgstr "新增 路由規則" msgid "Add a rule set" msgstr "新增 規則集" -#: htdocs/luci-static/resources/view/fchomo/server.js:166 +#: htdocs/luci-static/resources/view/fchomo/server.js:58 msgid "Add a server" msgstr "新增 伺服器" @@ -187,11 +201,11 @@ msgstr "新增 伺服器" msgid "Add a sub rule" msgstr "新增 子規則" -#: htdocs/luci-static/resources/view/fchomo/node.js:1535 +#: htdocs/luci-static/resources/view/fchomo/node.js:1549 msgid "Add prefix" msgstr "添加前綴" -#: htdocs/luci-static/resources/view/fchomo/node.js:1539 +#: htdocs/luci-static/resources/view/fchomo/node.js:1553 msgid "Add suffix" msgstr "添加後綴" @@ -200,7 +214,7 @@ msgstr "添加後綴" msgid "Address" msgstr "位址" -#: htdocs/luci-static/resources/fchomo.js:379 +#: htdocs/luci-static/resources/fchomo.js:385 msgid "" "After the 1-RTT client/server hello, padding randomly 111-1111 bytes with " "100% probability." @@ -217,7 +231,7 @@ msgstr "客戶端維護的 NAT 映射 的老化時間。
" msgid "All allowed" msgstr "允許所有" -#: htdocs/luci-static/resources/fchomo.js:220 +#: htdocs/luci-static/resources/fchomo.js:226 msgid "All ports" msgstr "所有連接埠" @@ -228,7 +242,7 @@ msgid "" msgstr "" "允許從私有網路訪問。
要從公共網站訪問私有網路上的 API,則必須啟用。" -#: htdocs/luci-static/resources/view/fchomo/node.js:762 +#: htdocs/luci-static/resources/view/fchomo/node.js:765 msgid "Allowed IPs" msgstr "允許的 IP" @@ -240,13 +254,13 @@ msgstr "已是最新版本。" msgid "Already in updating." msgstr "已在更新中。" +#: htdocs/luci-static/resources/fchomo/listeners.js:474 #: htdocs/luci-static/resources/view/fchomo/node.js:635 -#: htdocs/luci-static/resources/view/fchomo/server.js:526 msgid "Alter ID" msgstr "額外 ID" -#: htdocs/luci-static/resources/fchomo.js:148 -#: htdocs/luci-static/resources/fchomo.js:181 +#: htdocs/luci-static/resources/fchomo.js:152 +#: htdocs/luci-static/resources/fchomo.js:186 msgid "AnyTLS" msgstr "" @@ -263,7 +277,7 @@ msgstr "作為 dnsmasq 的最優先上游" msgid "As the TOP upstream of dnsmasq." msgstr "作為 dnsmasq 的最優先上游。" -#: htdocs/luci-static/resources/view/fchomo/server.js:476 +#: htdocs/luci-static/resources/fchomo/listeners.js:424 msgid "Auth timeout" msgstr "認證超時" @@ -271,14 +285,14 @@ msgstr "認證超時" msgid "Authenticated length" msgstr "認證長度" +#: htdocs/luci-static/resources/fchomo/listeners.js:389 #: htdocs/luci-static/resources/view/fchomo/global.js:402 -#: htdocs/luci-static/resources/view/fchomo/node.js:450 -#: htdocs/luci-static/resources/view/fchomo/node.js:473 -#: htdocs/luci-static/resources/view/fchomo/server.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:456 +#: htdocs/luci-static/resources/view/fchomo/node.js:480 msgid "Auto" msgstr "自動" -#: htdocs/luci-static/resources/view/fchomo/server.js:189 +#: htdocs/luci-static/resources/fchomo/listeners.js:137 msgid "Auto configure firewall" msgstr "自動配置防火牆" @@ -320,13 +334,13 @@ msgid "Binary mrs" msgstr "二進位 mrs" #: htdocs/luci-static/resources/view/fchomo/global.js:734 -#: htdocs/luci-static/resources/view/fchomo/node.js:1295 -#: htdocs/luci-static/resources/view/fchomo/node.js:1608 +#: htdocs/luci-static/resources/view/fchomo/node.js:1309 +#: htdocs/luci-static/resources/view/fchomo/node.js:1622 msgid "Bind interface" msgstr "綁定介面" -#: htdocs/luci-static/resources/view/fchomo/node.js:1296 -#: htdocs/luci-static/resources/view/fchomo/node.js:1609 +#: htdocs/luci-static/resources/view/fchomo/node.js:1310 +#: htdocs/luci-static/resources/view/fchomo/node.js:1623 msgid "Bind outbound interface.
" msgstr "綁定出站介面。
" @@ -364,12 +378,14 @@ msgstr "繞過 CN 流量" msgid "Bypass DSCP" msgstr "繞過 DSCP" -#: htdocs/luci-static/resources/view/fchomo/node.js:448 -#: htdocs/luci-static/resources/view/fchomo/node.js:449 -#: htdocs/luci-static/resources/view/fchomo/node.js:450 -#: htdocs/luci-static/resources/view/fchomo/server.js:437 -#: htdocs/luci-static/resources/view/fchomo/server.js:438 -#: htdocs/luci-static/resources/view/fchomo/server.js:439 +#: htdocs/luci-static/resources/fchomo/listeners.js:387 +#: htdocs/luci-static/resources/fchomo/listeners.js:388 +#: htdocs/luci-static/resources/fchomo/listeners.js:389 +#: htdocs/luci-static/resources/fchomo/listeners.js:390 +#: htdocs/luci-static/resources/view/fchomo/node.js:454 +#: htdocs/luci-static/resources/view/fchomo/node.js:455 +#: htdocs/luci-static/resources/view/fchomo/node.js:456 +#: htdocs/luci-static/resources/view/fchomo/node.js:457 msgid "CDN support" msgstr "CDN 支援" @@ -385,21 +401,21 @@ msgstr "CORS 允許私有網路" msgid "CORS allowed origins, * will be used if empty." msgstr "CORS 允許的來源,留空則使用 *。" -#: htdocs/luci-static/resources/fchomo.js:704 +#: htdocs/luci-static/resources/fchomo.js:710 msgid "Cancel" msgstr "取消" -#: htdocs/luci-static/resources/view/fchomo/node.js:1007 +#: htdocs/luci-static/resources/view/fchomo/node.js:1021 msgid "Cert fingerprint" msgstr "憑證指紋" -#: htdocs/luci-static/resources/view/fchomo/node.js:1008 +#: htdocs/luci-static/resources/view/fchomo/node.js:1022 msgid "" "Certificate fingerprint. Used to implement SSL Pinning and prevent MitM." msgstr "憑證指紋。用於實現 SSL憑證固定 並防止 MitM。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1028 -#: htdocs/luci-static/resources/view/fchomo/server.js:816 +#: htdocs/luci-static/resources/fchomo/listeners.js:810 +#: htdocs/luci-static/resources/view/fchomo/node.js:1042 msgid "Certificate path" msgstr "憑證路徑" @@ -428,16 +444,16 @@ msgstr "大陸 IPv6 庫版本" msgid "China list version" msgstr "大陸網域清單版本" +#: htdocs/luci-static/resources/fchomo/listeners.js:213 +#: htdocs/luci-static/resources/fchomo/listeners.js:306 #: htdocs/luci-static/resources/view/fchomo/node.js:332 -#: htdocs/luci-static/resources/view/fchomo/node.js:385 +#: htdocs/luci-static/resources/view/fchomo/node.js:391 #: htdocs/luci-static/resources/view/fchomo/node.js:641 -#: htdocs/luci-static/resources/view/fchomo/server.js:269 -#: htdocs/luci-static/resources/view/fchomo/server.js:356 msgid "Chipher" msgstr "加密方法" -#: htdocs/luci-static/resources/view/fchomo/node.js:394 -#: htdocs/luci-static/resources/view/fchomo/server.js:365 +#: htdocs/luci-static/resources/fchomo/listeners.js:315 +#: htdocs/luci-static/resources/view/fchomo/node.js:400 msgid "Chipher must be enabled if obfuscate downlink is disabled." msgstr "如果下行鏈路混淆功能已停用,則必須啟用加密。" @@ -453,29 +469,29 @@ msgstr "" "點擊此處下載" "最新的初始包。" +#: htdocs/luci-static/resources/fchomo/listeners.js:633 +#: htdocs/luci-static/resources/fchomo/listeners.js:849 #: htdocs/luci-static/resources/view/fchomo/global.js:550 -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/node.js:1029 +#: htdocs/luci-static/resources/view/fchomo/node.js:916 #: htdocs/luci-static/resources/view/fchomo/node.js:1043 -#: htdocs/luci-static/resources/view/fchomo/server.js:645 -#: htdocs/luci-static/resources/view/fchomo/server.js:855 +#: htdocs/luci-static/resources/view/fchomo/node.js:1057 #: root/usr/share/luci/menu.d/luci-app-fchomo.json:22 msgid "Client" msgstr "客戶端" -#: htdocs/luci-static/resources/view/fchomo/server.js:854 +#: htdocs/luci-static/resources/fchomo/listeners.js:848 msgid "Client Auth Certificate path" msgstr "客戶端認證憑證路徑" -#: htdocs/luci-static/resources/view/fchomo/server.js:846 +#: htdocs/luci-static/resources/fchomo/listeners.js:840 msgid "Client Auth type" msgstr "客戶端認證類型" -#: htdocs/luci-static/resources/view/fchomo/node.js:1074 +#: htdocs/luci-static/resources/view/fchomo/node.js:1088 msgid "Client fingerprint" msgstr "客戶端指紋" -#: htdocs/luci-static/resources/view/fchomo/server.js:352 +#: htdocs/luci-static/resources/fchomo/listeners.js:302 msgid "Client key" msgstr "客戶端密鑰" @@ -487,22 +503,21 @@ msgstr "客戶端狀態" msgid "Collecting data..." msgstr "收集資料中..." -#: htdocs/luci-static/resources/fchomo.js:221 -#: htdocs/luci-static/resources/fchomo.js:222 +#: htdocs/luci-static/resources/fchomo.js:227 +#: htdocs/luci-static/resources/fchomo.js:228 msgid "Common ports (bypass P2P traffic)" msgstr "常用連接埠(繞過 P2P 流量)" -#: htdocs/luci-static/resources/fchomo.js:1303 +#: htdocs/luci-static/resources/fchomo.js:1309 msgid "Complete" msgstr "完成" -#: htdocs/luci-static/resources/view/fchomo/node.js:1555 +#: htdocs/luci-static/resources/view/fchomo/node.js:1569 msgid "Configuration Items" msgstr "配置項" -#: htdocs/luci-static/resources/view/fchomo/node.js:515 -#: htdocs/luci-static/resources/view/fchomo/node.js:717 -#: htdocs/luci-static/resources/view/fchomo/server.js:454 +#: htdocs/luci-static/resources/fchomo/listeners.js:537 +#: htdocs/luci-static/resources/view/fchomo/node.js:854 msgid "Congestion controller" msgstr "擁塞控制器" @@ -510,19 +525,19 @@ msgstr "擁塞控制器" msgid "Connection check" msgstr "連接檢查" -#: htdocs/luci-static/resources/fchomo.js:573 +#: htdocs/luci-static/resources/fchomo.js:579 msgid "Content copied to clipboard!" msgstr "內容已複製到剪貼簿!" #: htdocs/luci-static/resources/view/fchomo/client.js:670 -#: htdocs/luci-static/resources/view/fchomo/node.js:1465 -#: htdocs/luci-static/resources/view/fchomo/node.js:1491 +#: htdocs/luci-static/resources/view/fchomo/node.js:1479 +#: htdocs/luci-static/resources/view/fchomo/node.js:1505 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:332 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:358 msgid "Content will not be verified, Please make sure you enter it correctly." msgstr "內容將不會被驗證,請確保輸入正確。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1464 +#: htdocs/luci-static/resources/view/fchomo/node.js:1478 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:331 msgid "Contents" msgstr "內容" @@ -531,7 +546,7 @@ msgstr "內容" msgid "Contents have been saved." msgstr "" -#: htdocs/luci-static/resources/fchomo.js:575 +#: htdocs/luci-static/resources/fchomo.js:581 msgid "Copy" msgstr "複製" @@ -547,7 +562,7 @@ msgstr "Cron 表達式" msgid "Custom Direct List" msgstr "自訂直連清單" -#: htdocs/luci-static/resources/view/fchomo/node.js:1526 +#: htdocs/luci-static/resources/view/fchomo/node.js:1540 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:393 msgid "Custom HTTP header." msgstr "自訂 HTTP header。" @@ -556,8 +571,8 @@ msgstr "自訂 HTTP header。" msgid "Custom Proxy List" msgstr "自訂代理清單" -#: htdocs/luci-static/resources/view/fchomo/node.js:407 -#: htdocs/luci-static/resources/view/fchomo/server.js:378 +#: htdocs/luci-static/resources/fchomo/listeners.js:328 +#: htdocs/luci-static/resources/view/fchomo/node.js:413 msgid "Custom byte layout" msgstr "自訂位元組佈局" @@ -566,7 +581,7 @@ msgid "" "Custom internal hosts. Support yaml or json format." msgstr "自訂內部 hosts。支援 yamljson 格式。" -#: htdocs/luci-static/resources/fchomo.js:170 +#: htdocs/luci-static/resources/fchomo.js:175 msgid "DIRECT" msgstr "" @@ -583,7 +598,7 @@ msgstr " DNS 連接埠" #: htdocs/luci-static/resources/view/fchomo/client.js:1428 #: htdocs/luci-static/resources/view/fchomo/client.js:1437 #: htdocs/luci-static/resources/view/fchomo/node.js:712 -#: htdocs/luci-static/resources/view/fchomo/node.js:792 +#: htdocs/luci-static/resources/view/fchomo/node.js:795 msgid "DNS server" msgstr "DNS 伺服器" @@ -611,15 +626,15 @@ msgstr "預設 DNS(由 WAN 下發)" msgid "Default DNS server" msgstr "預設 DNS 伺服器" -#: htdocs/luci-static/resources/view/fchomo/node.js:763 +#: htdocs/luci-static/resources/view/fchomo/node.js:766 msgid "Destination addresses allowed to be forwarded via Wireguard." msgstr "允許通過 WireGuard 轉發的目的位址" -#: htdocs/luci-static/resources/view/fchomo/node.js:1733 +#: htdocs/luci-static/resources/view/fchomo/node.js:1747 msgid "Destination provider" msgstr "落地供應商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1739 +#: htdocs/luci-static/resources/view/fchomo/node.js:1753 msgid "Destination proxy node" msgstr "落地代理節點" @@ -627,8 +642,8 @@ msgstr "落地代理節點" msgid "Dial fields" msgstr "撥號欄位" -#: htdocs/luci-static/resources/view/fchomo/node.js:1746 -#: htdocs/luci-static/resources/view/fchomo/node.js:1766 +#: htdocs/luci-static/resources/view/fchomo/node.js:1760 +#: htdocs/luci-static/resources/view/fchomo/node.js:1780 msgid "Different chain head/tail" msgstr "不同的鏈頭/鏈尾" @@ -648,9 +663,9 @@ msgstr "直連 IPv6 位址" msgid "Direct MAC-s" msgstr "直連 MAC 位址" +#: htdocs/luci-static/resources/fchomo/listeners.js:193 #: htdocs/luci-static/resources/view/fchomo/global.js:403 #: htdocs/luci-static/resources/view/fchomo/node.js:298 -#: htdocs/luci-static/resources/view/fchomo/server.js:249 msgid "Disable" msgstr "停用" @@ -666,7 +681,7 @@ msgstr "停用 quic-go 的 通用分段卸載(GSO)" msgid "Disable ICMP Forwarding" msgstr "禁用 ICMP 轉發" -#: htdocs/luci-static/resources/view/fchomo/node.js:956 +#: htdocs/luci-static/resources/view/fchomo/node.js:967 msgid "Disable SNI" msgstr "停用 SNI" @@ -694,46 +709,46 @@ msgstr "" msgid "Domain" msgstr "網域" -#: htdocs/luci-static/resources/view/fchomo/node.js:957 +#: htdocs/luci-static/resources/view/fchomo/node.js:968 msgid "Donot send server name in ClientHello." msgstr "不要在 ClientHello 中傳送伺服器名稱。" #: htdocs/luci-static/resources/view/fchomo/client.js:1577 -#: htdocs/luci-static/resources/view/fchomo/node.js:1021 -#: htdocs/luci-static/resources/view/fchomo/node.js:1595 +#: htdocs/luci-static/resources/view/fchomo/node.js:1035 +#: htdocs/luci-static/resources/view/fchomo/node.js:1609 msgid "Donot verifying server certificate." msgstr "不驗證伺服器憑證。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1274 +#: htdocs/luci-static/resources/view/fchomo/node.js:1288 msgid "Download bandwidth" msgstr "下載頻寬" -#: htdocs/luci-static/resources/view/fchomo/node.js:1275 +#: htdocs/luci-static/resources/view/fchomo/node.js:1289 msgid "Download bandwidth in Mbps." msgstr "下載頻寬(單位:Mbps)。" -#: htdocs/luci-static/resources/fchomo.js:1182 +#: htdocs/luci-static/resources/fchomo.js:1188 msgid "Download failed: %s" msgstr "下載失敗: %s" -#: htdocs/luci-static/resources/fchomo.js:1180 +#: htdocs/luci-static/resources/fchomo.js:1186 msgid "Download successful." msgstr "下載成功。" -#: htdocs/luci-static/resources/fchomo.js:156 +#: htdocs/luci-static/resources/fchomo.js:161 msgid "Dual stack" msgstr "雙棧" -#: htdocs/luci-static/resources/view/fchomo/node.js:1068 +#: htdocs/luci-static/resources/view/fchomo/node.js:1082 msgid "ECH HTTPS record query servername" msgstr "ECH HTTPS 記錄查詢網域" -#: htdocs/luci-static/resources/view/fchomo/node.js:1062 -#: htdocs/luci-static/resources/view/fchomo/server.js:912 +#: htdocs/luci-static/resources/fchomo/listeners.js:906 +#: htdocs/luci-static/resources/view/fchomo/node.js:1076 msgid "ECH config" msgstr "ECH 配置" -#: htdocs/luci-static/resources/view/fchomo/server.js:871 +#: htdocs/luci-static/resources/fchomo/listeners.js:865 msgid "ECH key" msgstr "ECH 密鑰" @@ -749,14 +764,18 @@ msgstr "" msgid "ETag support" msgstr "ETag 支援" -#: htdocs/luci-static/resources/view/fchomo/node.js:1187 +#: htdocs/luci-static/resources/view/fchomo/node.js:1201 msgid "Early Data first packet length limit." msgstr "前置數據長度閾值" -#: htdocs/luci-static/resources/view/fchomo/node.js:1193 +#: htdocs/luci-static/resources/view/fchomo/node.js:1207 msgid "Early Data header name" msgstr "前置數據標頭" +#: htdocs/luci-static/resources/view/fchomo/inbound.js:20 +msgid "Edit inbound" +msgstr "編輯入站" + #: htdocs/luci-static/resources/view/fchomo/node.js:203 msgid "Edit node" msgstr "編輯節點" @@ -765,15 +784,16 @@ msgstr "編輯節點" msgid "Edit ruleset" msgstr "編輯規則集" -#: htdocs/luci-static/resources/view/fchomo/node.js:1462 +#: htdocs/luci-static/resources/view/fchomo/node.js:1476 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:329 msgid "Editer" msgstr "編輯器" -#: htdocs/luci-static/resources/fchomo.js:366 +#: htdocs/luci-static/resources/fchomo.js:372 msgid "Eliminate encryption header characteristics" msgstr "消除加密頭特徵" +#: htdocs/luci-static/resources/fchomo/listeners.js:132 #: htdocs/luci-static/resources/view/fchomo/client.js:931 #: htdocs/luci-static/resources/view/fchomo/client.js:1024 #: htdocs/luci-static/resources/view/fchomo/client.js:1262 @@ -784,12 +804,11 @@ msgstr "消除加密頭特徵" #: htdocs/luci-static/resources/view/fchomo/global.js:401 #: htdocs/luci-static/resources/view/fchomo/global.js:680 #: htdocs/luci-static/resources/view/fchomo/node.js:234 -#: htdocs/luci-static/resources/view/fchomo/node.js:1435 -#: htdocs/luci-static/resources/view/fchomo/node.js:1631 -#: htdocs/luci-static/resources/view/fchomo/node.js:1720 +#: htdocs/luci-static/resources/view/fchomo/node.js:1449 +#: htdocs/luci-static/resources/view/fchomo/node.js:1645 +#: htdocs/luci-static/resources/view/fchomo/node.js:1734 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:257 -#: htdocs/luci-static/resources/view/fchomo/server.js:157 -#: htdocs/luci-static/resources/view/fchomo/server.js:184 +#: htdocs/luci-static/resources/view/fchomo/server.js:49 msgid "Enable" msgstr "啟用" @@ -814,49 +833,49 @@ msgstr "" "為出站連線啟用 IP4P 轉換" -#: htdocs/luci-static/resources/view/fchomo/node.js:1056 +#: htdocs/luci-static/resources/view/fchomo/node.js:1070 msgid "Enable ECH" msgstr "啟用 ECH" -#: htdocs/luci-static/resources/view/fchomo/node.js:1262 +#: htdocs/luci-static/resources/view/fchomo/node.js:1276 msgid "Enable TCP Brutal" msgstr "啟用 TCP Brutal" -#: htdocs/luci-static/resources/view/fchomo/node.js:1263 +#: htdocs/luci-static/resources/view/fchomo/node.js:1277 msgid "Enable TCP Brutal congestion control algorithm" msgstr "啟用 TCP Brutal 擁塞控制演算法。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1251 +#: htdocs/luci-static/resources/view/fchomo/node.js:1265 msgid "Enable multiplexing only for TCP." msgstr "僅為 TCP 啟用多路復用。" -#: htdocs/luci-static/resources/view/fchomo/node.js:434 -#: htdocs/luci-static/resources/view/fchomo/server.js:423 +#: htdocs/luci-static/resources/fchomo/listeners.js:373 +#: htdocs/luci-static/resources/view/fchomo/node.js:440 msgid "Enable obfuscate for downlink" msgstr "啟用下行鏈路混淆" -#: htdocs/luci-static/resources/view/fchomo/node.js:1245 +#: htdocs/luci-static/resources/view/fchomo/node.js:1259 msgid "Enable padding" msgstr "啟用填充" -#: htdocs/luci-static/resources/view/fchomo/node.js:1256 +#: htdocs/luci-static/resources/view/fchomo/node.js:1270 msgid "Enable statistic" msgstr "啟用統計" -#: htdocs/luci-static/resources/view/fchomo/node.js:857 -#: htdocs/luci-static/resources/view/fchomo/node.js:1577 +#: htdocs/luci-static/resources/view/fchomo/node.js:868 +#: htdocs/luci-static/resources/view/fchomo/node.js:1591 msgid "" "Enable the SUoT protocol, requires server support. Conflict with Multiplex." msgstr "啟用 SUoT 協議,需要服務端支援。與多路復用衝突。" +#: htdocs/luci-static/resources/fchomo/listeners.js:199 #: htdocs/luci-static/resources/view/fchomo/node.js:304 -#: htdocs/luci-static/resources/view/fchomo/server.js:255 msgid "" "Enabling obfuscation will make the server incompatible with standard QUIC " "connections, losing the ability to masquerade with HTTP/3." msgstr "啟用混淆將使伺服器與標準的 QUIC 連線不相容,失去 HTTP/3 偽裝的能力。" -#: htdocs/luci-static/resources/view/fchomo/server.js:608 +#: htdocs/luci-static/resources/fchomo/listeners.js:596 msgid "Encryption method" msgstr "加密方法" @@ -883,7 +902,7 @@ msgid "" "if empty." msgstr "超過此限制將會觸發強制健康檢查。留空則使用 5。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1689 +#: htdocs/luci-static/resources/view/fchomo/node.js:1703 msgid "Exclude matched node types." msgstr "排除匹配的節點類型。" @@ -896,7 +915,7 @@ msgstr "" "rel=\"noreferrer noopener\">此處。" #: htdocs/luci-static/resources/view/fchomo/client.js:1161 -#: htdocs/luci-static/resources/view/fchomo/node.js:1682 +#: htdocs/luci-static/resources/view/fchomo/node.js:1696 msgid "Exclude nodes that meet keywords or regexps." msgstr "排除匹配關鍵字或表達式的節點。" @@ -905,64 +924,65 @@ msgid "Expand/Collapse result" msgstr "展開/收起 結果" #: htdocs/luci-static/resources/view/fchomo/client.js:1121 -#: htdocs/luci-static/resources/view/fchomo/node.js:1667 +#: htdocs/luci-static/resources/view/fchomo/node.js:1681 msgid "Expected HTTP code. 204 will be used if empty." msgstr "預期的 HTTP code。留空則使用 204。" #: htdocs/luci-static/resources/view/fchomo/client.js:1123 -#: htdocs/luci-static/resources/view/fchomo/node.js:1669 +#: htdocs/luci-static/resources/view/fchomo/node.js:1683 msgid "Expected status" msgstr "預期狀態" -#: htdocs/luci-static/resources/fchomo.js:423 -#: htdocs/luci-static/resources/fchomo.js:426 #: htdocs/luci-static/resources/fchomo.js:429 -#: htdocs/luci-static/resources/fchomo.js:1320 -#: htdocs/luci-static/resources/fchomo.js:1328 -#: htdocs/luci-static/resources/fchomo.js:1336 -#: htdocs/luci-static/resources/fchomo.js:1359 -#: htdocs/luci-static/resources/fchomo.js:1362 -#: htdocs/luci-static/resources/fchomo.js:1369 -#: htdocs/luci-static/resources/fchomo.js:1385 -#: htdocs/luci-static/resources/fchomo.js:1394 -#: htdocs/luci-static/resources/fchomo.js:1406 -#: htdocs/luci-static/resources/fchomo.js:1409 -#: htdocs/luci-static/resources/fchomo.js:1419 -#: htdocs/luci-static/resources/fchomo.js:1431 -#: htdocs/luci-static/resources/fchomo.js:1434 -#: htdocs/luci-static/resources/fchomo.js:1444 -#: htdocs/luci-static/resources/fchomo.js:1454 -#: htdocs/luci-static/resources/fchomo.js:1489 -#: htdocs/luci-static/resources/fchomo.js:1491 -#: htdocs/luci-static/resources/fchomo.js:1504 +#: htdocs/luci-static/resources/fchomo.js:432 +#: htdocs/luci-static/resources/fchomo.js:435 +#: htdocs/luci-static/resources/fchomo.js:1326 +#: htdocs/luci-static/resources/fchomo.js:1334 +#: htdocs/luci-static/resources/fchomo.js:1342 +#: htdocs/luci-static/resources/fchomo.js:1365 +#: htdocs/luci-static/resources/fchomo.js:1368 +#: htdocs/luci-static/resources/fchomo.js:1375 +#: htdocs/luci-static/resources/fchomo.js:1391 +#: htdocs/luci-static/resources/fchomo.js:1400 +#: htdocs/luci-static/resources/fchomo.js:1412 +#: htdocs/luci-static/resources/fchomo.js:1415 +#: htdocs/luci-static/resources/fchomo.js:1425 +#: htdocs/luci-static/resources/fchomo.js:1437 +#: htdocs/luci-static/resources/fchomo.js:1440 +#: htdocs/luci-static/resources/fchomo.js:1450 +#: htdocs/luci-static/resources/fchomo.js:1460 +#: htdocs/luci-static/resources/fchomo.js:1495 +#: htdocs/luci-static/resources/fchomo.js:1497 #: htdocs/luci-static/resources/fchomo.js:1510 -#: htdocs/luci-static/resources/fchomo.js:1517 -#: htdocs/luci-static/resources/fchomo.js:1526 +#: htdocs/luci-static/resources/fchomo.js:1516 +#: htdocs/luci-static/resources/fchomo.js:1523 +#: htdocs/luci-static/resources/fchomo.js:1532 +#: htdocs/luci-static/resources/fchomo/listeners.js:315 +#: htdocs/luci-static/resources/fchomo/listeners.js:359 +#: htdocs/luci-static/resources/fchomo/listeners.js:625 +#: htdocs/luci-static/resources/fchomo/listeners.js:656 +#: htdocs/luci-static/resources/fchomo/listeners.js:757 #: htdocs/luci-static/resources/view/fchomo/client.js:68 #: htdocs/luci-static/resources/view/fchomo/client.js:1018 #: htdocs/luci-static/resources/view/fchomo/client.js:1508 #: htdocs/luci-static/resources/view/fchomo/global.js:880 -#: htdocs/luci-static/resources/view/fchomo/node.js:394 -#: htdocs/luci-static/resources/view/fchomo/node.js:427 -#: htdocs/luci-static/resources/view/fchomo/node.js:479 -#: htdocs/luci-static/resources/view/fchomo/node.js:928 -#: htdocs/luci-static/resources/view/fchomo/node.js:1013 -#: htdocs/luci-static/resources/view/fchomo/node.js:1746 -#: htdocs/luci-static/resources/view/fchomo/node.js:1766 +#: htdocs/luci-static/resources/view/fchomo/node.js:400 +#: htdocs/luci-static/resources/view/fchomo/node.js:433 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +#: htdocs/luci-static/resources/view/fchomo/node.js:939 +#: htdocs/luci-static/resources/view/fchomo/node.js:1027 +#: htdocs/luci-static/resources/view/fchomo/node.js:1760 +#: htdocs/luci-static/resources/view/fchomo/node.js:1780 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:284 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:298 -#: htdocs/luci-static/resources/view/fchomo/server.js:365 -#: htdocs/luci-static/resources/view/fchomo/server.js:409 -#: htdocs/luci-static/resources/view/fchomo/server.js:637 -#: htdocs/luci-static/resources/view/fchomo/server.js:668 -#: htdocs/luci-static/resources/view/fchomo/server.js:769 msgid "Expecting: %s" msgstr "請輸入:%s" -#: htdocs/luci-static/resources/view/fchomo/node.js:1123 -#: htdocs/luci-static/resources/view/fchomo/node.js:1130 -#: htdocs/luci-static/resources/view/fchomo/server.js:979 -#: htdocs/luci-static/resources/view/fchomo/server.js:986 +#: htdocs/luci-static/resources/fchomo/listeners.js:973 +#: htdocs/luci-static/resources/fchomo/listeners.js:980 +#: htdocs/luci-static/resources/view/fchomo/node.js:1137 +#: htdocs/luci-static/resources/view/fchomo/node.js:1144 msgid "Expecting: only support %s." msgstr "請輸入:僅支援 %s." @@ -981,23 +1001,24 @@ msgstr "實驗性" msgid "Factor" msgstr "條件" -#: htdocs/luci-static/resources/fchomo.js:1261 +#: htdocs/luci-static/resources/fchomo.js:1267 msgid "Failed to execute \"/etc/init.d/fchomo %s %s\" reason: %s" msgstr "無法執行 \"/etc/init.d/fchomo %s %s\" 原因: %s" -#: htdocs/luci-static/resources/fchomo.js:1214 +#: htdocs/luci-static/resources/fchomo.js:1220 msgid "Failed to generate %s, error: %s." msgstr "生成 %s 失敗,錯誤:%s。" -#: htdocs/luci-static/resources/fchomo.js:1626 +#: htdocs/luci-static/resources/fchomo.js:1632 msgid "Failed to upload %s, error: %s." msgstr "上傳 %s 失敗,錯誤:%s。" -#: htdocs/luci-static/resources/fchomo.js:1645 +#: htdocs/luci-static/resources/fchomo.js:1651 msgid "Failed to upload, error: %s." msgstr "上傳失敗,錯誤:%s。" -#: htdocs/luci-static/resources/fchomo.js:213 +#: htdocs/luci-static/resources/fchomo.js:219 +#: htdocs/luci-static/resources/fchomo/listeners.js:398 msgid "Fallback" msgstr "自動回退" @@ -1011,7 +1032,7 @@ msgid "Fallback filter" msgstr "後備過濾器" #: htdocs/luci-static/resources/view/fchomo/client.js:1156 -#: htdocs/luci-static/resources/view/fchomo/node.js:1676 +#: htdocs/luci-static/resources/view/fchomo/node.js:1690 msgid "Filter nodes that meet keywords or regexps." msgstr "過濾匹配關鍵字或表達式的節點。" @@ -1036,12 +1057,12 @@ msgstr "兜底 DNS 伺服器 (用於未被投毒汙染的網域)" msgid "Final DNS server (For poisoned domains)" msgstr "兜底 DNS 伺服器 (用於已被投毒汙染的網域)" -#: htdocs/luci-static/resources/view/fchomo/server.js:188 +#: htdocs/luci-static/resources/fchomo/listeners.js:136 msgid "Firewall" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:466 #: htdocs/luci-static/resources/view/fchomo/node.js:627 -#: htdocs/luci-static/resources/view/fchomo/server.js:518 msgid "Flow" msgstr "流控" @@ -1054,8 +1075,8 @@ msgstr "" "noopener\">%s." #: htdocs/luci-static/resources/view/fchomo/client.js:1122 -#: htdocs/luci-static/resources/view/fchomo/node.js:1545 -#: htdocs/luci-static/resources/view/fchomo/node.js:1668 +#: htdocs/luci-static/resources/view/fchomo/node.js:1559 +#: htdocs/luci-static/resources/view/fchomo/node.js:1682 msgid "" "For format see %s." @@ -1064,7 +1085,7 @@ msgstr "" "a>." #: htdocs/luci-static/resources/view/fchomo/node.js:707 -#: htdocs/luci-static/resources/view/fchomo/node.js:787 +#: htdocs/luci-static/resources/view/fchomo/node.js:790 msgid "Force DNS remote resolution." msgstr "強制 DNS 遠端解析。" @@ -1083,7 +1104,7 @@ msgstr "格式" msgid "FullCombo Shark!" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1149 +#: htdocs/luci-static/resources/view/fchomo/node.js:1163 msgid "GET" msgstr "" @@ -1095,10 +1116,10 @@ msgstr "GFW 網域清單版本" msgid "General" msgstr "常規" +#: htdocs/luci-static/resources/fchomo/listeners.js:119 #: htdocs/luci-static/resources/view/fchomo/client.js:1009 #: htdocs/luci-static/resources/view/fchomo/node.js:222 -#: htdocs/luci-static/resources/view/fchomo/node.js:1425 -#: htdocs/luci-static/resources/view/fchomo/server.js:171 +#: htdocs/luci-static/resources/view/fchomo/node.js:1439 msgid "General fields" msgstr "常規欄位" @@ -1106,16 +1127,16 @@ msgstr "常規欄位" msgid "General settings" msgstr "常規設定" -#: htdocs/luci-static/resources/fchomo.js:524 -#: htdocs/luci-static/resources/fchomo.js:526 -#: htdocs/luci-static/resources/fchomo.js:540 -#: htdocs/luci-static/resources/fchomo.js:542 +#: htdocs/luci-static/resources/fchomo.js:530 +#: htdocs/luci-static/resources/fchomo.js:532 +#: htdocs/luci-static/resources/fchomo.js:546 +#: htdocs/luci-static/resources/fchomo.js:548 +#: htdocs/luci-static/resources/fchomo/listeners.js:293 +#: htdocs/luci-static/resources/fchomo/listeners.js:334 +#: htdocs/luci-static/resources/fchomo/listeners.js:336 +#: htdocs/luci-static/resources/fchomo/listeners.js:729 +#: htdocs/luci-static/resources/fchomo/listeners.js:898 #: htdocs/luci-static/resources/view/fchomo/global.js:587 -#: htdocs/luci-static/resources/view/fchomo/server.js:343 -#: htdocs/luci-static/resources/view/fchomo/server.js:384 -#: htdocs/luci-static/resources/view/fchomo/server.js:386 -#: htdocs/luci-static/resources/view/fchomo/server.js:741 -#: htdocs/luci-static/resources/view/fchomo/server.js:904 msgid "Generate" msgstr "生成" @@ -1166,7 +1187,7 @@ msgstr "全域填充" msgid "Google" msgstr "Google" -#: htdocs/luci-static/resources/fchomo.js:226 +#: htdocs/luci-static/resources/fchomo.js:232 msgid "Google FCM" msgstr "" @@ -1178,48 +1199,49 @@ msgstr "授予 fchomo 存取 UCI 配置的權限" msgid "Group" msgstr "組" -#: htdocs/luci-static/resources/fchomo.js:139 -#: htdocs/luci-static/resources/fchomo.js:171 -#: htdocs/luci-static/resources/view/fchomo/node.js:810 -#: htdocs/luci-static/resources/view/fchomo/node.js:1112 -#: htdocs/luci-static/resources/view/fchomo/node.js:1123 -#: htdocs/luci-static/resources/view/fchomo/server.js:979 +#: htdocs/luci-static/resources/fchomo.js:143 +#: htdocs/luci-static/resources/fchomo.js:176 +#: htdocs/luci-static/resources/fchomo/listeners.js:973 +#: htdocs/luci-static/resources/view/fchomo/node.js:813 +#: htdocs/luci-static/resources/view/fchomo/node.js:1126 +#: htdocs/luci-static/resources/view/fchomo/node.js:1137 msgid "HTTP" msgstr "" #: htdocs/luci-static/resources/view/fchomo/node.js:267 -#: htdocs/luci-static/resources/view/fchomo/node.js:1171 -#: htdocs/luci-static/resources/view/fchomo/node.js:1525 +#: htdocs/luci-static/resources/view/fchomo/node.js:1185 +#: htdocs/luci-static/resources/view/fchomo/node.js:1539 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:392 msgid "HTTP header" msgstr "HTTP header" -#: htdocs/luci-static/resources/view/fchomo/node.js:440 -#: htdocs/luci-static/resources/view/fchomo/server.js:429 +#: htdocs/luci-static/resources/fchomo/listeners.js:379 +#: htdocs/luci-static/resources/view/fchomo/node.js:446 msgid "HTTP mask" msgstr "HTTP 偽裝" -#: htdocs/luci-static/resources/view/fchomo/node.js:445 -#: htdocs/luci-static/resources/view/fchomo/node.js:479 -#: htdocs/luci-static/resources/view/fchomo/server.js:434 +#: htdocs/luci-static/resources/fchomo/listeners.js:384 +#: htdocs/luci-static/resources/view/fchomo/node.js:451 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 msgid "HTTP mask mode" msgstr "HTTP 偽裝模式" -#: htdocs/luci-static/resources/view/fchomo/node.js:469 +#: htdocs/luci-static/resources/view/fchomo/node.js:476 msgid "HTTP mask multiplex" msgstr "HTTP 偽裝多路復用" -#: htdocs/luci-static/resources/view/fchomo/node.js:454 -#: htdocs/luci-static/resources/view/fchomo/node.js:459 +#: htdocs/luci-static/resources/view/fchomo/node.js:461 +#: htdocs/luci-static/resources/view/fchomo/node.js:466 msgid "HTTP mask: %s" msgstr "HTTP 偽裝: %s" -#: htdocs/luci-static/resources/view/fchomo/node.js:1148 +#: htdocs/luci-static/resources/view/fchomo/node.js:1162 msgid "HTTP request method" msgstr "HTTP 請求方法" -#: htdocs/luci-static/resources/view/fchomo/node.js:465 -#: htdocs/luci-static/resources/view/fchomo/server.js:443 +#: htdocs/luci-static/resources/fchomo/listeners.js:394 +#: htdocs/luci-static/resources/view/fchomo/node.js:472 msgid "HTTP root path" msgstr "HTTP 根路徑" @@ -1227,15 +1249,15 @@ msgstr "HTTP 根路徑" msgid "HTTP/3" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:263 +#: htdocs/luci-static/resources/fchomo/listeners.js:207 msgid "" "HTTP3 server behavior when authentication fails.
A 404 page will be " "returned if empty." msgstr "身份驗證失敗時的 HTTP3 伺服器回應。預設回傳 404 頁面。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1113 -#: htdocs/luci-static/resources/view/fchomo/node.js:1124 -#: htdocs/luci-static/resources/view/fchomo/server.js:980 +#: htdocs/luci-static/resources/fchomo/listeners.js:974 +#: htdocs/luci-static/resources/view/fchomo/node.js:1127 +#: htdocs/luci-static/resources/view/fchomo/node.js:1138 msgid "HTTPUpgrade" msgstr "" @@ -1247,36 +1269,40 @@ msgstr "處理網域" msgid "Handshake mode" msgstr "握手模式" -#: htdocs/luci-static/resources/view/fchomo/server.js:541 +#: htdocs/luci-static/resources/fchomo/listeners.js:498 msgid "Handshake target that supports TLS 1.3" msgstr "握手目標 (支援 TLS 1.3)" -#: htdocs/luci-static/resources/view/fchomo/server.js:416 +#: htdocs/luci-static/resources/fchomo/listeners.js:366 msgid "Handshake timeout" msgstr "握手超時" +#: htdocs/luci-static/resources/view/fchomo/node.js:718 +msgid "Health check" +msgstr "健康檢查" + #: htdocs/luci-static/resources/view/fchomo/client.js:1091 -#: htdocs/luci-static/resources/view/fchomo/node.js:1636 +#: htdocs/luci-static/resources/view/fchomo/node.js:1650 msgid "Health check URL" msgstr "健康檢查 URL" #: htdocs/luci-static/resources/view/fchomo/client.js:1120 -#: htdocs/luci-static/resources/view/fchomo/node.js:1666 +#: htdocs/luci-static/resources/view/fchomo/node.js:1680 msgid "Health check expected status" msgstr "健康檢查预期状态" #: htdocs/luci-static/resources/view/fchomo/client.js:1100 -#: htdocs/luci-static/resources/view/fchomo/node.js:1646 +#: htdocs/luci-static/resources/view/fchomo/node.js:1660 msgid "Health check interval" msgstr "健康檢查间隔" #: htdocs/luci-static/resources/view/fchomo/client.js:1107 -#: htdocs/luci-static/resources/view/fchomo/node.js:1653 +#: htdocs/luci-static/resources/view/fchomo/node.js:1667 msgid "Health check timeout" msgstr "健康檢查超时" #: htdocs/luci-static/resources/view/fchomo/client.js:1011 -#: htdocs/luci-static/resources/view/fchomo/node.js:1427 +#: htdocs/luci-static/resources/view/fchomo/node.js:1441 msgid "Health fields" msgstr "健康欄位" @@ -1288,7 +1314,7 @@ msgstr "心跳間隔" msgid "Hidden" msgstr "隱藏" -#: htdocs/luci-static/resources/view/fchomo/node.js:816 +#: htdocs/luci-static/resources/view/fchomo/node.js:819 msgid "Host that supports TLS 1.3" msgstr "主機名稱 (支援 TLS 1.3)" @@ -1300,7 +1326,7 @@ msgstr "主機金鑰" msgid "Host-key algorithms" msgstr "主機金鑰演算法" -#: htdocs/luci-static/resources/view/fchomo/node.js:459 +#: htdocs/luci-static/resources/view/fchomo/node.js:466 msgid "Host/SNI override" msgstr "主機/SNI 覆蓋" @@ -1309,8 +1335,8 @@ msgstr "主機/SNI 覆蓋" msgid "Hosts" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:150 -#: htdocs/luci-static/resources/fchomo.js:183 +#: htdocs/luci-static/resources/fchomo.js:154 +#: htdocs/luci-static/resources/fchomo.js:188 msgid "Hysteria2" msgstr "" @@ -1323,20 +1349,20 @@ msgstr "" msgid "IP CIDR" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:509 +#: htdocs/luci-static/resources/view/fchomo/node.js:518 msgid "IP override" msgstr "IP 覆寫" -#: htdocs/luci-static/resources/view/fchomo/node.js:1307 -#: htdocs/luci-static/resources/view/fchomo/node.js:1622 +#: htdocs/luci-static/resources/view/fchomo/node.js:1321 +#: htdocs/luci-static/resources/view/fchomo/node.js:1636 msgid "IP version" msgstr "IP 版本" -#: htdocs/luci-static/resources/fchomo.js:157 +#: htdocs/luci-static/resources/fchomo.js:162 msgid "IPv4 only" msgstr "僅 IPv4" -#: htdocs/luci-static/resources/fchomo.js:158 +#: htdocs/luci-static/resources/fchomo.js:163 msgid "IPv6 only" msgstr "僅 IPv6" @@ -1357,11 +1383,11 @@ msgstr "閒置會話檢查間隔" msgid "Idle session timeout" msgstr "閒置會話逾時" -#: htdocs/luci-static/resources/view/fchomo/server.js:469 +#: htdocs/luci-static/resources/fchomo/listeners.js:417 msgid "Idle timeout" msgstr "閒置逾時" -#: htdocs/luci-static/resources/fchomo.js:1362 +#: htdocs/luci-static/resources/fchomo.js:1368 msgid "If All ports is selected, uncheck others" msgstr "如果選擇了“所有連接埠”,則取消選取“其他”" @@ -1369,11 +1395,11 @@ msgstr "如果選擇了“所有連接埠”,則取消選取“其他”" msgid "If Block is selected, uncheck others" msgstr "如果選擇了“封鎖”,則取消選取“其他”" -#: htdocs/luci-static/resources/view/fchomo/server.js:242 +#: htdocs/luci-static/resources/fchomo/listeners.js:186 msgid "Ignore client bandwidth" msgstr "忽略客戶端頻寬" -#: htdocs/luci-static/resources/fchomo.js:709 +#: htdocs/luci-static/resources/fchomo.js:715 msgid "Import" msgstr "導入" @@ -1389,8 +1415,8 @@ msgstr "導入" #: htdocs/luci-static/resources/view/fchomo/client.js:1713 #: htdocs/luci-static/resources/view/fchomo/client.js:1748 #: htdocs/luci-static/resources/view/fchomo/client.js:1769 -#: htdocs/luci-static/resources/view/fchomo/node.js:1332 -#: htdocs/luci-static/resources/view/fchomo/node.js:1413 +#: htdocs/luci-static/resources/view/fchomo/node.js:1346 +#: htdocs/luci-static/resources/view/fchomo/node.js:1427 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:143 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:234 msgid "Import mihomo config" @@ -1402,16 +1428,16 @@ msgstr "導入 mihomo 配置" msgid "Import rule-set links" msgstr "導入規則集連結" +#: htdocs/luci-static/resources/fchomo/listeners.js:175 +#: htdocs/luci-static/resources/fchomo/listeners.js:181 #: htdocs/luci-static/resources/view/fchomo/node.js:286 #: htdocs/luci-static/resources/view/fchomo/node.js:292 -#: htdocs/luci-static/resources/view/fchomo/node.js:1583 -#: htdocs/luci-static/resources/view/fchomo/node.js:1589 -#: htdocs/luci-static/resources/view/fchomo/server.js:231 -#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/node.js:1597 +#: htdocs/luci-static/resources/view/fchomo/node.js:1603 msgid "In Mbps." msgstr "單位為 Mbps。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1503 +#: htdocs/luci-static/resources/view/fchomo/node.js:1517 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:370 msgid "In bytes. %s will be used if empty." msgstr "單位為位元組。留空則使用 %s。" @@ -1423,15 +1449,15 @@ msgstr "單位為毫秒。" #: htdocs/luci-static/resources/view/fchomo/client.js:1108 #: htdocs/luci-static/resources/view/fchomo/client.js:1137 -#: htdocs/luci-static/resources/view/fchomo/node.js:1654 +#: htdocs/luci-static/resources/view/fchomo/node.js:1668 msgid "In millisecond. %s will be used if empty." msgstr "單位為毫秒。留空則使用 %s。" +#: htdocs/luci-static/resources/fchomo/listeners.js:367 +#: htdocs/luci-static/resources/fchomo/listeners.js:418 +#: htdocs/luci-static/resources/fchomo/listeners.js:425 #: htdocs/luci-static/resources/view/fchomo/node.js:601 #: htdocs/luci-static/resources/view/fchomo/node.js:608 -#: htdocs/luci-static/resources/view/fchomo/server.js:417 -#: htdocs/luci-static/resources/view/fchomo/server.js:470 -#: htdocs/luci-static/resources/view/fchomo/server.js:477 msgid "In seconds." msgstr "單位為秒。" @@ -1440,14 +1466,14 @@ msgstr "單位為秒。" #: htdocs/luci-static/resources/view/fchomo/global.js:430 #: htdocs/luci-static/resources/view/fchomo/global.js:515 #: htdocs/luci-static/resources/view/fchomo/node.js:280 -#: htdocs/luci-static/resources/view/fchomo/node.js:1509 -#: htdocs/luci-static/resources/view/fchomo/node.js:1647 +#: htdocs/luci-static/resources/view/fchomo/node.js:1523 +#: htdocs/luci-static/resources/view/fchomo/node.js:1661 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:376 msgid "In seconds. %s will be used if empty." msgstr "單位為秒。留空則使用 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:917 -#: htdocs/luci-static/resources/view/fchomo/server.js:657 +#: htdocs/luci-static/resources/fchomo/listeners.js:645 +#: htdocs/luci-static/resources/view/fchomo/node.js:928 msgid "" "In the order of one Padding-Length and one Padding-" "Interval, infinite concatenation." @@ -1456,6 +1482,8 @@ msgstr "" "序,無限連接。" #: htdocs/luci-static/resources/view/fchomo/global.js:449 +#: htdocs/luci-static/resources/view/fchomo/inbound.js:28 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 msgid "Inbound" msgstr "入站" @@ -1487,7 +1515,7 @@ msgstr "引入所有代理節點。" msgid "Info" msgstr "訊息" -#: htdocs/luci-static/resources/view/fchomo/node.js:1442 +#: htdocs/luci-static/resources/view/fchomo/node.js:1456 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:272 msgid "Inline" msgstr "內嵌" @@ -1497,25 +1525,26 @@ msgid "Interface Control" msgstr "介面控制" #: htdocs/luci-static/resources/fchomo.js:49 -#: htdocs/luci-static/resources/fchomo.js:155 -#: htdocs/luci-static/resources/fchomo.js:348 +#: htdocs/luci-static/resources/fchomo.js:160 +#: htdocs/luci-static/resources/fchomo.js:354 msgid "Keep default" msgstr "保持預設" -#: htdocs/luci-static/resources/view/fchomo/node.js:379 -#: htdocs/luci-static/resources/view/fchomo/server.js:299 +#: htdocs/luci-static/resources/fchomo/listeners.js:249 +#: htdocs/luci-static/resources/view/fchomo/node.js:385 msgid "Key" msgstr "密鑰" -#: htdocs/luci-static/resources/view/fchomo/node.js:1042 -#: htdocs/luci-static/resources/view/fchomo/server.js:831 +#: htdocs/luci-static/resources/fchomo/listeners.js:825 +#: htdocs/luci-static/resources/view/fchomo/node.js:1056 msgid "Key path" msgstr "憑證路徑" -#: htdocs/luci-static/resources/view/fchomo/server.js:676 +#: htdocs/luci-static/resources/fchomo/listeners.js:664 msgid "Keypairs" msgstr "密鑰對" +#: htdocs/luci-static/resources/fchomo/listeners.js:127 #: htdocs/luci-static/resources/view/fchomo/client.js:1014 #: htdocs/luci-static/resources/view/fchomo/client.js:1257 #: htdocs/luci-static/resources/view/fchomo/client.js:1349 @@ -1523,24 +1552,23 @@ msgstr "密鑰對" #: htdocs/luci-static/resources/view/fchomo/client.js:1719 #: htdocs/luci-static/resources/view/fchomo/client.js:1775 #: htdocs/luci-static/resources/view/fchomo/node.js:229 -#: htdocs/luci-static/resources/view/fchomo/node.js:1430 -#: htdocs/luci-static/resources/view/fchomo/node.js:1715 +#: htdocs/luci-static/resources/view/fchomo/node.js:1444 +#: htdocs/luci-static/resources/view/fchomo/node.js:1729 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:252 -#: htdocs/luci-static/resources/view/fchomo/server.js:179 msgid "Label" msgstr "標籤" #: htdocs/luci-static/resources/view/fchomo/client.js:1114 -#: htdocs/luci-static/resources/view/fchomo/node.js:1660 +#: htdocs/luci-static/resources/view/fchomo/node.js:1674 msgid "Lazy" msgstr "懶惰狀態" -#: htdocs/luci-static/resources/view/fchomo/node.js:447 -#: htdocs/luci-static/resources/view/fchomo/server.js:436 +#: htdocs/luci-static/resources/fchomo/listeners.js:386 +#: htdocs/luci-static/resources/view/fchomo/node.js:453 msgid "Legacy" msgstr "傳統" -#: htdocs/luci-static/resources/view/fchomo/server.js:527 +#: htdocs/luci-static/resources/fchomo/listeners.js:475 msgid "" "Legacy protocol support (VMess MD5 Authentication) is provided for " "compatibility purposes only, use of alterId > 1 is not recommended." @@ -1552,16 +1580,16 @@ msgstr "" msgid "Less compatibility and sometimes better performance." msgstr "有時效能較好。" -#: htdocs/luci-static/resources/view/fchomo/node.js:969 -#: htdocs/luci-static/resources/view/fchomo/server.js:812 +#: htdocs/luci-static/resources/fchomo/listeners.js:806 +#: htdocs/luci-static/resources/view/fchomo/node.js:980 msgid "List of supported application level protocols, in order of preference." msgstr "支援的應用層協議協商清單,依序排列。" -#: htdocs/luci-static/resources/view/fchomo/server.js:199 +#: htdocs/luci-static/resources/fchomo/listeners.js:147 msgid "Listen address" msgstr "監聽位址" -#: htdocs/luci-static/resources/view/fchomo/server.js:176 +#: htdocs/luci-static/resources/fchomo/listeners.js:124 msgid "Listen fields" msgstr "監聽欄位" @@ -1569,8 +1597,8 @@ msgstr "監聽欄位" msgid "Listen interfaces" msgstr "監聽介面" +#: htdocs/luci-static/resources/fchomo/listeners.js:152 #: htdocs/luci-static/resources/view/fchomo/client.js:1374 -#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Listen port" msgstr "監聽埠" @@ -1578,26 +1606,26 @@ msgstr "監聽埠" msgid "Listen ports" msgstr "監聽埠" -#: htdocs/luci-static/resources/fchomo.js:215 +#: htdocs/luci-static/resources/fchomo.js:221 msgid "Load balance" msgstr "負載均衡" -#: htdocs/luci-static/resources/view/fchomo/node.js:1440 +#: htdocs/luci-static/resources/view/fchomo/node.js:1454 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:270 msgid "Local" msgstr "本地" #: htdocs/luci-static/resources/view/fchomo/node.js:694 -#: htdocs/luci-static/resources/view/fchomo/node.js:734 +#: htdocs/luci-static/resources/view/fchomo/node.js:737 msgid "Local IPv6 address" msgstr "本地 IPv6 位址" #: htdocs/luci-static/resources/view/fchomo/node.js:686 -#: htdocs/luci-static/resources/view/fchomo/node.js:726 +#: htdocs/luci-static/resources/view/fchomo/node.js:729 msgid "Local address" msgstr "本地位址" -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:62 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:70 msgid "Log" msgstr "日誌" @@ -1613,21 +1641,21 @@ msgstr "日誌為空。" msgid "Log level" msgstr "日誌等級" -#: htdocs/luci-static/resources/fchomo.js:423 +#: htdocs/luci-static/resources/fchomo.js:429 msgid "Lowercase only" msgstr "僅限小寫" #: htdocs/luci-static/resources/view/fchomo/global.js:502 #: htdocs/luci-static/resources/view/fchomo/node.js:700 -#: htdocs/luci-static/resources/view/fchomo/node.js:780 +#: htdocs/luci-static/resources/view/fchomo/node.js:783 msgid "MTU" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:185 +#: htdocs/luci-static/resources/fchomo.js:190 msgid "Masque" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:262 +#: htdocs/luci-static/resources/fchomo/listeners.js:206 msgid "Masquerade" msgstr "偽裝" @@ -1659,12 +1687,12 @@ msgstr "匹配回應通過 ipcidr
" msgid "Match rule set." msgstr "匹配規則集。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1186 +#: htdocs/luci-static/resources/view/fchomo/node.js:1200 msgid "Max Early Data" msgstr "前置數據最大值" +#: htdocs/luci-static/resources/fchomo/listeners.js:411 #: htdocs/luci-static/resources/view/fchomo/node.js:543 -#: htdocs/luci-static/resources/view/fchomo/server.js:463 msgid "Max UDP relay packet size" msgstr "UDP 中繼數據包最大尺寸" @@ -1672,8 +1700,8 @@ msgstr "UDP 中繼數據包最大尺寸" msgid "Max count of failures" msgstr "最大失敗次數" +#: htdocs/luci-static/resources/fchomo/listeners.js:180 #: htdocs/luci-static/resources/view/fchomo/node.js:291 -#: htdocs/luci-static/resources/view/fchomo/server.js:236 msgid "Max download speed" msgstr "最大下載速度" @@ -1681,17 +1709,17 @@ msgstr "最大下載速度" msgid "Max open streams" msgstr "限制打開流的數量" +#: htdocs/luci-static/resources/fchomo/listeners.js:174 #: htdocs/luci-static/resources/view/fchomo/node.js:285 -#: htdocs/luci-static/resources/view/fchomo/server.js:230 msgid "Max upload speed" msgstr "最大上傳速度" -#: htdocs/luci-static/resources/view/fchomo/node.js:1223 -#: htdocs/luci-static/resources/view/fchomo/node.js:1239 +#: htdocs/luci-static/resources/view/fchomo/node.js:1237 +#: htdocs/luci-static/resources/view/fchomo/node.js:1253 msgid "Maximum connections" msgstr "最大連線數" -#: htdocs/luci-static/resources/view/fchomo/node.js:1237 +#: htdocs/luci-static/resources/view/fchomo/node.js:1251 msgid "" "Maximum multiplexed streams in a connection before opening a new connection." "
Conflict with %s and %s." @@ -1699,24 +1727,24 @@ msgstr "" "在開啟新連線之前,連線中的最大多路復用流數量。
%s 和 " "%s 衝突。" -#: htdocs/luci-static/resources/view/fchomo/node.js:419 -#: htdocs/luci-static/resources/view/fchomo/server.js:401 +#: htdocs/luci-static/resources/fchomo/listeners.js:351 +#: htdocs/luci-static/resources/view/fchomo/node.js:425 msgid "Maximum padding rate" msgstr "最大填充率" -#: htdocs/luci-static/resources/view/fchomo/node.js:427 -#: htdocs/luci-static/resources/view/fchomo/server.js:409 +#: htdocs/luci-static/resources/fchomo/listeners.js:359 +#: htdocs/luci-static/resources/view/fchomo/node.js:433 msgid "" "Maximum padding rate must be greater than or equal to the minimum padding " "rate." msgstr "最大填充率必須大於等于最小填充率。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1236 +#: htdocs/luci-static/resources/view/fchomo/node.js:1250 msgid "Maximum streams" msgstr "最大流數量" -#: htdocs/luci-static/resources/fchomo.js:143 -#: htdocs/luci-static/resources/fchomo.js:175 +#: htdocs/luci-static/resources/fchomo.js:147 +#: htdocs/luci-static/resources/fchomo.js:180 msgid "Mieru" msgstr "" @@ -1728,7 +1756,7 @@ msgstr "Mihomo 客戶端" #: htdocs/luci-static/resources/view/fchomo/log.js:158 #: htdocs/luci-static/resources/view/fchomo/log.js:163 -#: htdocs/luci-static/resources/view/fchomo/server.js:131 +#: htdocs/luci-static/resources/view/fchomo/server.js:23 msgid "Mihomo server" msgstr "Mihomo 服務端" @@ -1736,22 +1764,22 @@ msgstr "Mihomo 服務端" msgid "Min of idle sessions to keep" msgstr "要保留的最少閒置會話數" -#: htdocs/luci-static/resources/view/fchomo/node.js:1230 +#: htdocs/luci-static/resources/view/fchomo/node.js:1244 msgid "" "Minimum multiplexed streams in a connection before opening a new connection." msgstr "在開啟新連線之前,連線中的最小多路復用流數量。" -#: htdocs/luci-static/resources/view/fchomo/node.js:412 -#: htdocs/luci-static/resources/view/fchomo/server.js:394 +#: htdocs/luci-static/resources/fchomo/listeners.js:344 +#: htdocs/luci-static/resources/view/fchomo/node.js:418 msgid "Minimum padding rate" msgstr "最小填充率" -#: htdocs/luci-static/resources/view/fchomo/node.js:1229 -#: htdocs/luci-static/resources/view/fchomo/node.js:1239 +#: htdocs/luci-static/resources/view/fchomo/node.js:1243 +#: htdocs/luci-static/resources/view/fchomo/node.js:1253 msgid "Minimum streams" msgstr "最小流數量" -#: htdocs/luci-static/resources/fchomo.js:141 +#: htdocs/luci-static/resources/fchomo.js:145 #: htdocs/luci-static/resources/view/fchomo/global.js:497 msgid "Mixed" msgstr "混合" @@ -1764,12 +1792,12 @@ msgstr "混合 系統 TCP 堆栈和 gVisor UDP 堆栈 msgid "Mixed port" msgstr "混合連接埠" -#: htdocs/luci-static/resources/view/fchomo/node.js:1209 +#: htdocs/luci-static/resources/view/fchomo/node.js:1223 msgid "Multiplex" msgstr "多路復用" +#: htdocs/luci-static/resources/fchomo/listeners.js:123 #: htdocs/luci-static/resources/view/fchomo/node.js:226 -#: htdocs/luci-static/resources/view/fchomo/server.js:175 msgid "Multiplex fields" msgstr "多路復用欄位" @@ -1782,7 +1810,11 @@ msgstr "多路復用" msgid "NOT" msgstr "NOT" -#: htdocs/luci-static/resources/view/fchomo/node.js:1515 +#: htdocs/luci-static/resources/fchomo/listeners.js:528 +msgid "Name of the Proxy group as outbound." +msgstr "出站代理組的名稱。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1529 msgid "Name of the Proxy group to download provider." msgstr "用於下載供應商訂閱的代理組名稱。" @@ -1790,14 +1822,22 @@ msgstr "用於下載供應商訂閱的代理組名稱。" msgid "Name of the Proxy group to download rule set." msgstr "用於下載規則集訂閱的代理組名稱。" +#: htdocs/luci-static/resources/fchomo/listeners.js:522 +msgid "Name of the Sub rule used for inbound matching." +msgstr "用於入站匹配的子規則名稱。" + #: htdocs/luci-static/resources/view/fchomo/node.js:527 msgid "Native UDP" msgstr "原生 UDP" -#: htdocs/luci-static/resources/fchomo.js:365 +#: htdocs/luci-static/resources/fchomo.js:371 msgid "Native appearance" msgstr "原生外觀" +#: htdocs/luci-static/resources/fchomo/listeners.js:545 +msgid "Network type" +msgstr "網路類型" + #: htdocs/luci-static/resources/view/fchomo/global.js:443 msgid "No Authentication IP ranges" msgstr "無需認證的 IP 範圍" @@ -1807,11 +1847,11 @@ msgid "No add'l params" msgstr "無附加參數" #: htdocs/luci-static/resources/view/fchomo/client.js:1115 -#: htdocs/luci-static/resources/view/fchomo/node.js:1661 +#: htdocs/luci-static/resources/view/fchomo/node.js:1675 msgid "No testing is performed when this provider node is not in use." msgstr "當此供應商的節點未使用時,不執行任何測試。" -#: htdocs/luci-static/resources/fchomo.js:670 +#: htdocs/luci-static/resources/fchomo.js:676 msgid "No valid %s found." msgstr "未找到有效的%s。" @@ -1821,22 +1861,22 @@ msgstr "未找到有效的規則集連結。" #: htdocs/luci-static/resources/view/fchomo/client.js:1041 #: htdocs/luci-static/resources/view/fchomo/node.js:217 -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:46 msgid "Node" msgstr "節點" #: htdocs/luci-static/resources/view/fchomo/client.js:1160 -#: htdocs/luci-static/resources/view/fchomo/node.js:1681 +#: htdocs/luci-static/resources/view/fchomo/node.js:1695 msgid "Node exclude filter" msgstr "排除節點" #: htdocs/luci-static/resources/view/fchomo/client.js:1165 -#: htdocs/luci-static/resources/view/fchomo/node.js:1688 +#: htdocs/luci-static/resources/view/fchomo/node.js:1702 msgid "Node exclude type" msgstr "排除節點類型" #: htdocs/luci-static/resources/view/fchomo/client.js:1155 -#: htdocs/luci-static/resources/view/fchomo/node.js:1675 +#: htdocs/luci-static/resources/view/fchomo/node.js:1689 msgid "Node filter" msgstr "過濾節點" @@ -1844,7 +1884,7 @@ msgstr "過濾節點" msgid "Node switch tolerance" msgstr "節點切換容差" -#: htdocs/luci-static/resources/fchomo.js:392 +#: htdocs/luci-static/resources/fchomo.js:398 msgid "None" msgstr "無" @@ -1852,41 +1892,41 @@ msgstr "無" msgid "Not Installed" msgstr "未安裝" -#: htdocs/luci-static/resources/fchomo.js:1140 +#: htdocs/luci-static/resources/fchomo.js:1146 msgid "Not Running" msgstr "未在運作" -#: htdocs/luci-static/resources/view/fchomo/node.js:472 +#: htdocs/luci-static/resources/view/fchomo/node.js:479 msgid "OFF" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:474 +#: htdocs/luci-static/resources/view/fchomo/node.js:481 msgid "ON" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:809 +#: htdocs/luci-static/resources/view/fchomo/node.js:812 msgid "Obfs Mode" msgstr "Obfs 模式" +#: htdocs/luci-static/resources/fchomo/listeners.js:198 #: htdocs/luci-static/resources/view/fchomo/node.js:303 -#: htdocs/luci-static/resources/view/fchomo/server.js:254 msgid "Obfuscate password" msgstr "混淆密碼" +#: htdocs/luci-static/resources/fchomo/listeners.js:192 +#: htdocs/luci-static/resources/fchomo/listeners.js:322 #: htdocs/luci-static/resources/view/fchomo/node.js:297 -#: htdocs/luci-static/resources/view/fchomo/node.js:401 -#: htdocs/luci-static/resources/view/fchomo/server.js:248 -#: htdocs/luci-static/resources/view/fchomo/server.js:372 +#: htdocs/luci-static/resources/view/fchomo/node.js:407 msgid "Obfuscate type" msgstr "混淆類型" -#: htdocs/luci-static/resources/view/fchomo/node.js:402 -#: htdocs/luci-static/resources/view/fchomo/server.js:373 +#: htdocs/luci-static/resources/fchomo/listeners.js:323 +#: htdocs/luci-static/resources/view/fchomo/node.js:408 msgid "Obfuscated as ASCII data stream" msgstr "混淆為 ASCII 資料流" -#: htdocs/luci-static/resources/view/fchomo/node.js:403 -#: htdocs/luci-static/resources/view/fchomo/server.js:374 +#: htdocs/luci-static/resources/fchomo/listeners.js:324 +#: htdocs/luci-static/resources/view/fchomo/node.js:409 msgid "Obfuscated as low-entropy data stream" msgstr "混淆為低熵資料流" @@ -1898,7 +1938,7 @@ msgstr "0-63 範圍內的一個或多個數字,以逗號分隔" msgid "Only process traffic from specific interfaces. Leave empty for all." msgstr "只處理來自指定介面的流量。留空表示全部。" -#: htdocs/luci-static/resources/fchomo.js:1134 +#: htdocs/luci-static/resources/fchomo.js:1140 msgid "Open Dashboard" msgstr "打開面板" @@ -1912,11 +1952,11 @@ msgid "Override destination" msgstr "覆蓋目標位址" #: htdocs/luci-static/resources/view/fchomo/client.js:1010 -#: htdocs/luci-static/resources/view/fchomo/node.js:1426 +#: htdocs/luci-static/resources/view/fchomo/node.js:1440 msgid "Override fields" msgstr "覆蓋欄位" -#: htdocs/luci-static/resources/view/fchomo/node.js:510 +#: htdocs/luci-static/resources/view/fchomo/node.js:519 msgid "Override the IP address of the server that DNS response." msgstr "覆蓋 DNS 回應的伺服器的 IP 位址。" @@ -1932,7 +1972,7 @@ msgstr "使用嗅探到的網域覆寫連線目標。" msgid "Override the existing ECS in original request." msgstr "覆蓋原始請求中已有的 ECS。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1069 +#: htdocs/luci-static/resources/view/fchomo/node.js:1083 msgid "Overrides the domain name used for HTTPS record queries." msgstr "覆蓋用於 HTTPS 記錄查詢的網域。" @@ -1940,11 +1980,11 @@ msgstr "覆蓋用於 HTTPS 記錄查詢的網域。" msgid "Overview" msgstr "概覽" -#: htdocs/luci-static/resources/view/fchomo/node.js:1150 +#: htdocs/luci-static/resources/view/fchomo/node.js:1164 msgid "POST" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1151 +#: htdocs/luci-static/resources/view/fchomo/node.js:1165 msgid "PUT" msgstr "" @@ -1952,31 +1992,31 @@ msgstr "" msgid "Packet encoding" msgstr "數據包編碼" -#: htdocs/luci-static/resources/view/fchomo/server.js:507 +#: htdocs/luci-static/resources/fchomo/listeners.js:455 msgid "Padding scheme" msgstr "填充方案" -#: htdocs/luci-static/resources/view/fchomo/node.js:915 -#: htdocs/luci-static/resources/view/fchomo/server.js:655 +#: htdocs/luci-static/resources/fchomo/listeners.js:643 +#: htdocs/luci-static/resources/view/fchomo/node.js:926 msgid "Paddings" msgstr "填充 (Paddings)" +#: htdocs/luci-static/resources/fchomo/listeners.js:165 +#: htdocs/luci-static/resources/fchomo/listeners.js:221 +#: htdocs/luci-static/resources/fchomo/listeners.js:505 #: htdocs/luci-static/resources/view/fchomo/node.js:261 #: htdocs/luci-static/resources/view/fchomo/node.js:340 -#: htdocs/luci-static/resources/view/fchomo/node.js:824 -#: htdocs/luci-static/resources/view/fchomo/server.js:221 -#: htdocs/luci-static/resources/view/fchomo/server.js:277 -#: htdocs/luci-static/resources/view/fchomo/server.js:548 +#: htdocs/luci-static/resources/view/fchomo/node.js:827 msgid "Password" msgstr "密碼" -#: htdocs/luci-static/resources/view/fchomo/node.js:1490 +#: htdocs/luci-static/resources/fchomo/listeners.js:583 +#: htdocs/luci-static/resources/view/fchomo/node.js:1504 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:357 -#: htdocs/luci-static/resources/view/fchomo/server.js:595 msgid "Payload" msgstr "Payload" -#: htdocs/luci-static/resources/view/fchomo/node.js:748 +#: htdocs/luci-static/resources/view/fchomo/node.js:751 msgid "Peer pubkic key" msgstr "對端公鑰" @@ -1986,11 +2026,11 @@ msgid "" "it is not needed." msgstr "效能可能會略有下降,建議僅在需要時開啟。" -#: htdocs/luci-static/resources/view/fchomo/node.js:775 +#: htdocs/luci-static/resources/view/fchomo/node.js:778 msgid "Periodically sends data packets to maintain connection persistence." msgstr "定期發送資料包以維持連線持久性。" -#: htdocs/luci-static/resources/view/fchomo/node.js:774 +#: htdocs/luci-static/resources/view/fchomo/node.js:777 msgid "Persistent keepalive" msgstr "持久連接" @@ -2013,8 +2053,8 @@ msgid "" "standards." msgstr "連結格式標準請參考 %s。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1463 -#: htdocs/luci-static/resources/view/fchomo/node.js:1489 +#: htdocs/luci-static/resources/view/fchomo/node.js:1477 +#: htdocs/luci-static/resources/view/fchomo/node.js:1503 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:330 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:356 msgid "" @@ -2029,25 +2069,25 @@ msgstr "" #: htdocs/luci-static/resources/view/fchomo/client.js:1445 #: htdocs/luci-static/resources/view/fchomo/client.js:1697 #: htdocs/luci-static/resources/view/fchomo/client.js:1749 -#: htdocs/luci-static/resources/view/fchomo/node.js:1333 +#: htdocs/luci-static/resources/view/fchomo/node.js:1347 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:144 msgid "Please type %s fields of mihomo config.
" msgstr "請輸入 mihomo 配置的 %s 欄位。
" -#: htdocs/luci-static/resources/view/fchomo/node.js:798 -#: htdocs/luci-static/resources/view/fchomo/server.js:534 +#: htdocs/luci-static/resources/fchomo/listeners.js:491 +#: htdocs/luci-static/resources/view/fchomo/node.js:801 msgid "Plugin" msgstr "插件" -#: htdocs/luci-static/resources/view/fchomo/node.js:809 -#: htdocs/luci-static/resources/view/fchomo/node.js:816 -#: htdocs/luci-static/resources/view/fchomo/node.js:824 -#: htdocs/luci-static/resources/view/fchomo/node.js:830 -#: htdocs/luci-static/resources/view/fchomo/node.js:838 -#: htdocs/luci-static/resources/view/fchomo/node.js:844 -#: htdocs/luci-static/resources/view/fchomo/server.js:541 -#: htdocs/luci-static/resources/view/fchomo/server.js:548 -#: htdocs/luci-static/resources/view/fchomo/server.js:554 +#: htdocs/luci-static/resources/fchomo/listeners.js:498 +#: htdocs/luci-static/resources/fchomo/listeners.js:505 +#: htdocs/luci-static/resources/fchomo/listeners.js:511 +#: htdocs/luci-static/resources/view/fchomo/node.js:812 +#: htdocs/luci-static/resources/view/fchomo/node.js:819 +#: htdocs/luci-static/resources/view/fchomo/node.js:827 +#: htdocs/luci-static/resources/view/fchomo/node.js:833 +#: htdocs/luci-static/resources/view/fchomo/node.js:841 +#: htdocs/luci-static/resources/view/fchomo/node.js:847 msgid "Plugin:" msgstr "插件:" @@ -2055,7 +2095,7 @@ msgstr "插件:" msgid "Port" msgstr "連接埠" -#: htdocs/luci-static/resources/fchomo.js:1371 +#: htdocs/luci-static/resources/fchomo.js:1377 msgid "Port %s alrealy exists!" msgstr "連接埠 %s 已存在!" @@ -2071,21 +2111,21 @@ msgstr "連接埠範圍" msgid "Ports" msgstr "連接埠" +#: htdocs/luci-static/resources/fchomo/listeners.js:152 #: htdocs/luci-static/resources/view/fchomo/node.js:274 -#: htdocs/luci-static/resources/view/fchomo/server.js:204 msgid "Ports pool" msgstr "連接埠池" -#: htdocs/luci-static/resources/view/fchomo/node.js:487 -#: htdocs/luci-static/resources/view/fchomo/node.js:755 +#: htdocs/luci-static/resources/view/fchomo/node.js:496 +#: htdocs/luci-static/resources/view/fchomo/node.js:758 msgid "Pre-shared key" msgstr "預先共用金鑰" -#: htdocs/luci-static/resources/fchomo.js:159 +#: htdocs/luci-static/resources/fchomo.js:164 msgid "Prefer IPv4" msgstr "優先 IPv4" -#: htdocs/luci-static/resources/fchomo.js:160 +#: htdocs/luci-static/resources/fchomo.js:165 msgid "Prefer IPv6" msgstr "優先 IPv6" @@ -2096,10 +2136,10 @@ msgstr "防止某些情況下的 ICMP 環回問題。 Ping 不會顯示實際延 #: htdocs/luci-static/resources/view/fchomo/global.js:736 #: htdocs/luci-static/resources/view/fchomo/global.js:753 -#: htdocs/luci-static/resources/view/fchomo/node.js:1297 -#: htdocs/luci-static/resources/view/fchomo/node.js:1303 -#: htdocs/luci-static/resources/view/fchomo/node.js:1610 -#: htdocs/luci-static/resources/view/fchomo/node.js:1617 +#: htdocs/luci-static/resources/view/fchomo/node.js:1311 +#: htdocs/luci-static/resources/view/fchomo/node.js:1317 +#: htdocs/luci-static/resources/view/fchomo/node.js:1624 +#: htdocs/luci-static/resources/view/fchomo/node.js:1631 msgid "Priority: Proxy Node > Global." msgstr "優先權: 代理節點 > 全域。" @@ -2112,7 +2152,7 @@ msgid "Priv-key passphrase" msgstr "金鑰密碼" #: htdocs/luci-static/resources/view/fchomo/node.js:671 -#: htdocs/luci-static/resources/view/fchomo/node.js:740 +#: htdocs/luci-static/resources/view/fchomo/node.js:743 msgid "Private key" msgstr "私鑰" @@ -2121,7 +2161,7 @@ msgid "Process matching mode" msgstr "進程匹配模式" #: htdocs/luci-static/resources/view/fchomo/global.js:684 -#: htdocs/luci-static/resources/view/fchomo/node.js:1215 +#: htdocs/luci-static/resources/view/fchomo/node.js:1229 msgid "Protocol" msgstr "協議" @@ -2136,13 +2176,13 @@ msgid "" msgstr "協議參數。 如啟用會隨機浪費流量(在 v2ray 中預設為啟用且無法停用)。" #: htdocs/luci-static/resources/view/fchomo/client.js:1055 -#: htdocs/luci-static/resources/view/fchomo/node.js:1316 -#: htdocs/luci-static/resources/view/fchomo/node.js:1325 -#: htdocs/luci-static/resources/view/fchomo/node.js:1726 +#: htdocs/luci-static/resources/view/fchomo/node.js:1330 +#: htdocs/luci-static/resources/view/fchomo/node.js:1339 +#: htdocs/luci-static/resources/view/fchomo/node.js:1740 msgid "Provider" msgstr "供應商" -#: htdocs/luci-static/resources/view/fchomo/node.js:1496 +#: htdocs/luci-static/resources/view/fchomo/node.js:1510 msgid "Provider URL" msgstr "供應商訂閱 URL" @@ -2165,18 +2205,19 @@ msgid "Proxy MAC-s" msgstr "代理 MAC 位址" #: htdocs/luci-static/resources/view/fchomo/node.js:208 -#: htdocs/luci-static/resources/view/fchomo/node.js:1725 +#: htdocs/luci-static/resources/view/fchomo/node.js:1739 msgid "Proxy Node" msgstr "代理節點" -#: htdocs/luci-static/resources/view/fchomo/node.js:1701 -#: htdocs/luci-static/resources/view/fchomo/node.js:1710 +#: htdocs/luci-static/resources/view/fchomo/node.js:1715 +#: htdocs/luci-static/resources/view/fchomo/node.js:1724 msgid "Proxy chain" msgstr "代理鏈" +#: htdocs/luci-static/resources/fchomo/listeners.js:527 #: htdocs/luci-static/resources/view/fchomo/client.js:783 #: htdocs/luci-static/resources/view/fchomo/client.js:1543 -#: htdocs/luci-static/resources/view/fchomo/node.js:1514 +#: htdocs/luci-static/resources/view/fchomo/node.js:1528 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:381 msgid "Proxy group" msgstr "代理組" @@ -2194,57 +2235,53 @@ msgid "Proxy routerself" msgstr "代理路由器自身" #: htdocs/luci-static/resources/view/fchomo/node.js:528 +#: htdocs/luci-static/resources/view/fchomo/node.js:723 msgid "QUIC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:516 -#: htdocs/luci-static/resources/view/fchomo/server.js:455 -msgid "QUIC congestion controller." -msgstr "QUIC 壅塞控制器。" - #: htdocs/luci-static/resources/view/fchomo/client.js:926 -#: htdocs/luci-static/resources/view/fchomo/server.js:152 +#: htdocs/luci-static/resources/view/fchomo/server.js:44 msgid "Quick Reload" msgstr "快速重載" -#: htdocs/luci-static/resources/view/fchomo/node.js:1083 -#: htdocs/luci-static/resources/view/fchomo/server.js:919 +#: htdocs/luci-static/resources/fchomo/listeners.js:913 +#: htdocs/luci-static/resources/view/fchomo/node.js:1097 msgid "REALITY" msgstr "REALITY" -#: htdocs/luci-static/resources/view/fchomo/node.js:1098 +#: htdocs/luci-static/resources/view/fchomo/node.js:1112 msgid "REALITY X25519MLKEM768 PQC support" msgstr "REALITY X25519MLKEM768 後量子加密支援" -#: htdocs/luci-static/resources/view/fchomo/server.js:956 +#: htdocs/luci-static/resources/fchomo/listeners.js:950 msgid "REALITY certificate issued to" msgstr "REALITY 證書頒發給" -#: htdocs/luci-static/resources/view/fchomo/server.js:924 +#: htdocs/luci-static/resources/fchomo/listeners.js:918 msgid "REALITY handshake server" msgstr "REALITY 握手伺服器" -#: htdocs/luci-static/resources/view/fchomo/server.js:931 +#: htdocs/luci-static/resources/fchomo/listeners.js:925 msgid "REALITY private key" msgstr "REALITY 私鑰" -#: htdocs/luci-static/resources/view/fchomo/node.js:1088 -#: htdocs/luci-static/resources/view/fchomo/server.js:946 +#: htdocs/luci-static/resources/fchomo/listeners.js:940 +#: htdocs/luci-static/resources/view/fchomo/node.js:1102 msgid "REALITY public key" msgstr "REALITY 公鑰" -#: htdocs/luci-static/resources/view/fchomo/node.js:1093 -#: htdocs/luci-static/resources/view/fchomo/server.js:950 +#: htdocs/luci-static/resources/fchomo/listeners.js:944 +#: htdocs/luci-static/resources/view/fchomo/node.js:1107 msgid "REALITY short ID" msgstr "REALITY 標識符" -#: htdocs/luci-static/resources/view/fchomo/node.js:905 -#: htdocs/luci-static/resources/view/fchomo/server.js:626 -#: htdocs/luci-static/resources/view/fchomo/server.js:645 +#: htdocs/luci-static/resources/fchomo/listeners.js:614 +#: htdocs/luci-static/resources/fchomo/listeners.js:633 +#: htdocs/luci-static/resources/view/fchomo/node.js:916 msgid "RTT" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:357 +#: htdocs/luci-static/resources/fchomo.js:363 msgid "Random" msgstr "隨機" @@ -2252,7 +2289,7 @@ msgstr "隨機" msgid "Random will be used if empty." msgstr "留空將使用隨機令牌。" -#: htdocs/luci-static/resources/fchomo.js:367 +#: htdocs/luci-static/resources/fchomo.js:373 msgid "Randomized traffic characteristics" msgstr "隨機化流量特徵" @@ -2276,10 +2313,10 @@ msgstr "Redirect TCP + Tun UDP" msgid "Refresh every %s seconds." msgstr "每 %s 秒刷新。" -#: htdocs/luci-static/resources/fchomo.js:1127 +#: htdocs/luci-static/resources/fchomo.js:1133 #: htdocs/luci-static/resources/view/fchomo/client.js:927 #: htdocs/luci-static/resources/view/fchomo/global.js:193 -#: htdocs/luci-static/resources/view/fchomo/server.js:153 +#: htdocs/luci-static/resources/view/fchomo/server.js:45 msgid "Reload" msgstr "重載" @@ -2287,43 +2324,43 @@ msgstr "重載" msgid "Reload All" msgstr "重載所有" -#: htdocs/luci-static/resources/view/fchomo/node.js:1441 +#: htdocs/luci-static/resources/view/fchomo/node.js:1455 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:271 msgid "Remote" msgstr "遠端" #: htdocs/luci-static/resources/view/fchomo/node.js:706 -#: htdocs/luci-static/resources/view/fchomo/node.js:786 +#: htdocs/luci-static/resources/view/fchomo/node.js:789 msgid "Remote DNS resolve" msgstr "遠端 DNS 解析" -#: htdocs/luci-static/resources/fchomo.js:1292 +#: htdocs/luci-static/resources/fchomo.js:1298 msgid "Remove" msgstr "移除" -#: htdocs/luci-static/resources/fchomo.js:1297 -#: htdocs/luci-static/resources/view/fchomo/node.js:1417 -#: htdocs/luci-static/resources/view/fchomo/node.js:1419 +#: htdocs/luci-static/resources/fchomo.js:1303 +#: htdocs/luci-static/resources/view/fchomo/node.js:1431 +#: htdocs/luci-static/resources/view/fchomo/node.js:1433 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:244 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:246 msgid "Remove idles" msgstr "移除閒置" -#: htdocs/luci-static/resources/view/fchomo/node.js:1543 +#: htdocs/luci-static/resources/view/fchomo/node.js:1557 msgid "Replace name" msgstr "名稱替換" -#: htdocs/luci-static/resources/view/fchomo/node.js:1544 +#: htdocs/luci-static/resources/view/fchomo/node.js:1558 msgid "Replace node name." msgstr "替換節點名稱" -#: htdocs/luci-static/resources/fchomo.js:341 +#: htdocs/luci-static/resources/fchomo.js:347 msgid "Request" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1157 -#: htdocs/luci-static/resources/view/fchomo/node.js:1164 -#: htdocs/luci-static/resources/view/fchomo/server.js:998 +#: htdocs/luci-static/resources/fchomo/listeners.js:992 +#: htdocs/luci-static/resources/view/fchomo/node.js:1171 +#: htdocs/luci-static/resources/view/fchomo/node.js:1178 msgid "Request path" msgstr "請求路徑" @@ -2331,20 +2368,20 @@ msgstr "請求路徑" msgid "Request timeout" msgstr "請求逾時" -#: htdocs/luci-static/resources/fchomo.js:344 +#: htdocs/luci-static/resources/fchomo.js:350 msgid "Require and verify" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:342 +#: htdocs/luci-static/resources/fchomo.js:348 msgid "Require any" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:375 -#: htdocs/luci-static/resources/view/fchomo/node.js:1099 +#: htdocs/luci-static/resources/fchomo.js:381 +#: htdocs/luci-static/resources/view/fchomo/node.js:1113 msgid "Requires server support." msgstr "需要伺服器支援。" -#: htdocs/luci-static/resources/view/fchomo/node.js:769 +#: htdocs/luci-static/resources/view/fchomo/node.js:772 msgid "Reserved field bytes" msgstr "保留字段位元組" @@ -2352,7 +2389,7 @@ msgstr "保留字段位元組" msgid "Resources management" msgstr "資源管理" -#: htdocs/luci-static/resources/view/fchomo/node.js:844 +#: htdocs/luci-static/resources/view/fchomo/node.js:847 msgid "Restls script" msgstr "Restls 劇本" @@ -2366,12 +2403,12 @@ msgid "" "Returns the string input for icon in the API to display in this proxy group." msgstr "在 API 傳回 icon 所輸入的字串,以在該代理組顯示。" -#: htdocs/luci-static/resources/view/fchomo/node.js:473 +#: htdocs/luci-static/resources/view/fchomo/node.js:480 msgid "Reuse HTTP connections to reduce RTT for each connection establishment." msgstr "重用 HTTP 連接以減少每次建立連接的 RTT。" -#: htdocs/luci-static/resources/view/fchomo/node.js:470 -#: htdocs/luci-static/resources/view/fchomo/node.js:474 +#: htdocs/luci-static/resources/view/fchomo/node.js:477 +#: htdocs/luci-static/resources/view/fchomo/node.js:481 msgid "Reusing a single tunnel to carry multiple target connections within it." msgstr "複用單條隧道使其內部承載多條目標連線。" @@ -2388,8 +2425,8 @@ msgstr "路由 DSCP" msgid "Routing GFW" msgstr "路由 GFW 流量" -#: htdocs/luci-static/resources/view/fchomo/node.js:1302 -#: htdocs/luci-static/resources/view/fchomo/node.js:1616 +#: htdocs/luci-static/resources/view/fchomo/node.js:1316 +#: htdocs/luci-static/resources/view/fchomo/node.js:1630 msgid "Routing mark" msgstr "路由標記" @@ -2441,7 +2478,7 @@ msgstr "規則集" msgid "Rule set URL" msgstr "規則集訂閱 URL" -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:46 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 msgid "Ruleset" msgstr "規則集" @@ -2449,27 +2486,27 @@ msgstr "規則集" msgid "Ruleset-URI-Scheme" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1140 +#: htdocs/luci-static/resources/fchomo.js:1146 msgid "Running" msgstr "正在運作" -#: htdocs/luci-static/resources/fchomo.js:223 +#: htdocs/luci-static/resources/fchomo.js:229 msgid "SMTP" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:140 +#: htdocs/luci-static/resources/fchomo.js:144 msgid "SOCKS" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:172 +#: htdocs/luci-static/resources/fchomo.js:177 msgid "SOCKS5" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:187 +#: htdocs/luci-static/resources/fchomo.js:193 msgid "SSH" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:224 +#: htdocs/luci-static/resources/fchomo.js:230 msgid "STUN" msgstr "" @@ -2477,20 +2514,20 @@ msgstr "" msgid "SUB-RULE" msgstr "SUB-RULE" -#: htdocs/luci-static/resources/view/fchomo/node.js:862 +#: htdocs/luci-static/resources/view/fchomo/node.js:873 msgid "SUoT version" msgstr "SUoT 版本" +#: htdocs/luci-static/resources/fchomo/listeners.js:194 #: htdocs/luci-static/resources/view/fchomo/node.js:299 -#: htdocs/luci-static/resources/view/fchomo/server.js:250 msgid "Salamander" msgstr "Salamander" -#: htdocs/luci-static/resources/fchomo.js:165 +#: htdocs/luci-static/resources/fchomo.js:170 msgid "Same dstaddr requests. Same node" msgstr "相同 目標位址 請求。相同節點" -#: htdocs/luci-static/resources/fchomo.js:166 +#: htdocs/luci-static/resources/fchomo.js:171 msgid "Same srcaddr and dstaddr requests. Same node" msgstr "相同 來源位址 和 目標位址 請求。相同節點" @@ -2498,7 +2535,7 @@ msgstr "相同 來源位址 和 目標位址 請求。相同節點" msgid "Segment maximum size" msgstr "分段最大尺寸" -#: htdocs/luci-static/resources/fchomo.js:212 +#: htdocs/luci-static/resources/fchomo.js:218 msgid "Select" msgstr "手動選擇" @@ -2506,20 +2543,20 @@ msgstr "手動選擇" msgid "Select Dashboard" msgstr "選擇面板" -#: htdocs/luci-static/resources/fchomo.js:381 +#: htdocs/luci-static/resources/fchomo.js:387 msgid "Send padding randomly 0-3333 bytes with 50% probability." msgstr "以 50% 的機率發送隨機 0 到 3333 位元組的填充。" -#: htdocs/luci-static/resources/fchomo.js:370 -#: htdocs/luci-static/resources/fchomo.js:371 +#: htdocs/luci-static/resources/fchomo.js:376 +#: htdocs/luci-static/resources/fchomo.js:377 msgid "Send random ticket of 300s-600s duration for client 0-RTT reuse." msgstr "發送 300-600 秒的隨機票證,以供客戶端 0-RTT 重用。" -#: htdocs/luci-static/resources/view/fchomo/server.js:166 -#: htdocs/luci-static/resources/view/fchomo/server.js:626 -#: htdocs/luci-static/resources/view/fchomo/server.js:817 -#: htdocs/luci-static/resources/view/fchomo/server.js:832 -#: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 +#: htdocs/luci-static/resources/fchomo/listeners.js:614 +#: htdocs/luci-static/resources/fchomo/listeners.js:811 +#: htdocs/luci-static/resources/fchomo/listeners.js:826 +#: htdocs/luci-static/resources/view/fchomo/server.js:58 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:62 msgid "Server" msgstr "服務端" @@ -2527,7 +2564,7 @@ msgstr "服務端" msgid "Server address" msgstr "伺服器位址" -#: htdocs/luci-static/resources/view/fchomo/node.js:1142 +#: htdocs/luci-static/resources/view/fchomo/node.js:1156 msgid "Server hostname" msgstr "伺服器主機名稱" @@ -2539,27 +2576,27 @@ msgstr "服務端狀態" msgid "Service status" msgstr "服務狀態" -#: htdocs/luci-static/resources/fchomo.js:142 -#: htdocs/luci-static/resources/fchomo.js:173 +#: htdocs/luci-static/resources/fchomo.js:146 +#: htdocs/luci-static/resources/fchomo.js:178 msgid "Shadowsocks" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:437 #: htdocs/luci-static/resources/view/fchomo/node.js:582 -#: htdocs/luci-static/resources/view/fchomo/server.js:489 msgid "Shadowsocks chipher" msgstr "Shadowsocks 加密方法" +#: htdocs/luci-static/resources/fchomo/listeners.js:432 #: htdocs/luci-static/resources/view/fchomo/node.js:577 -#: htdocs/luci-static/resources/view/fchomo/server.js:484 msgid "Shadowsocks encrypt" msgstr "Shadowsocks 加密" +#: htdocs/luci-static/resources/fchomo/listeners.js:445 #: htdocs/luci-static/resources/view/fchomo/node.js:590 -#: htdocs/luci-static/resources/view/fchomo/server.js:497 msgid "Shadowsocks password" msgstr "Shadowsocks 密碼" -#: htdocs/luci-static/resources/view/fchomo/node.js:1257 +#: htdocs/luci-static/resources/view/fchomo/node.js:1271 msgid "Show connections in the dashboard for breaking connections easier." msgstr "在面板中顯示連線以便於打斷連線。" @@ -2567,18 +2604,18 @@ msgstr "在面板中顯示連線以便於打斷連線。" msgid "Silent" msgstr "靜音" -#: htdocs/luci-static/resources/fchomo.js:164 +#: htdocs/luci-static/resources/fchomo.js:169 msgid "Simple round-robin all nodes" msgstr "簡單輪替所有節點" -#: htdocs/luci-static/resources/view/fchomo/node.js:1502 +#: htdocs/luci-static/resources/view/fchomo/node.js:1516 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:369 msgid "Size limit" msgstr "大小限制" #: htdocs/luci-static/resources/view/fchomo/client.js:1576 -#: htdocs/luci-static/resources/view/fchomo/node.js:1020 -#: htdocs/luci-static/resources/view/fchomo/node.js:1594 +#: htdocs/luci-static/resources/view/fchomo/node.js:1034 +#: htdocs/luci-static/resources/view/fchomo/node.js:1608 msgid "Skip cert verify" msgstr "跳過憑證驗證" @@ -2594,7 +2631,7 @@ msgstr "跳過嗅探目標位址" msgid "Skiped sniffing src address" msgstr "跳過嗅探來源位址" -#: htdocs/luci-static/resources/fchomo.js:177 +#: htdocs/luci-static/resources/fchomo.js:182 msgid "Snell" msgstr "" @@ -2610,7 +2647,7 @@ msgstr "嗅探器" msgid "Sniffer settings" msgstr "嗅探器設定" -#: htdocs/luci-static/resources/fchomo.js:413 +#: htdocs/luci-static/resources/fchomo.js:419 msgid "Specify a ID" msgstr "指定一個ID" @@ -2625,11 +2662,11 @@ msgstr "指定需要被代理的目標連接埠。多個連接埠必須以逗號 msgid "Stack" msgstr "堆栈" -#: htdocs/luci-static/resources/fchomo.js:227 +#: htdocs/luci-static/resources/fchomo.js:233 msgid "Steam Client" msgstr "Steam 客戶端" -#: htdocs/luci-static/resources/fchomo.js:228 +#: htdocs/luci-static/resources/fchomo.js:234 msgid "Steam P2P" msgstr "" @@ -2638,6 +2675,7 @@ msgstr "" msgid "Strategy" msgstr "策略" +#: htdocs/luci-static/resources/fchomo/listeners.js:521 #: htdocs/luci-static/resources/view/fchomo/client.js:1303 #: htdocs/luci-static/resources/view/fchomo/client.js:1312 msgid "Sub rule" @@ -2647,7 +2685,7 @@ msgstr "子規則" msgid "Sub rule group" msgstr "子規則組" -#: htdocs/luci-static/resources/fchomo.js:673 +#: htdocs/luci-static/resources/fchomo.js:679 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:215 msgid "Successfully imported %s %s of total %s." msgstr "已成功匯入 %s 個%s (共 %s 個)。" @@ -2656,12 +2694,12 @@ msgstr "已成功匯入 %s 個%s (共 %s 個)。" msgid "Successfully updated." msgstr "更新成功。" -#: htdocs/luci-static/resources/fchomo.js:1642 +#: htdocs/luci-static/resources/fchomo.js:1648 msgid "Successfully uploaded." msgstr "已成功上傳。" -#: htdocs/luci-static/resources/fchomo.js:144 -#: htdocs/luci-static/resources/fchomo.js:176 +#: htdocs/luci-static/resources/fchomo.js:148 +#: htdocs/luci-static/resources/fchomo.js:181 msgid "Sudoku" msgstr "" @@ -2681,20 +2719,21 @@ msgstr "系統" msgid "System DNS" msgstr "系統 DNS" -#: htdocs/luci-static/resources/fchomo.js:139 -#: htdocs/luci-static/resources/fchomo.js:144 -#: htdocs/luci-static/resources/fchomo.js:145 -#: htdocs/luci-static/resources/fchomo.js:146 -#: htdocs/luci-static/resources/fchomo.js:147 +#: htdocs/luci-static/resources/fchomo.js:143 #: htdocs/luci-static/resources/fchomo.js:148 -#: htdocs/luci-static/resources/fchomo.js:171 +#: htdocs/luci-static/resources/fchomo.js:149 +#: htdocs/luci-static/resources/fchomo.js:150 +#: htdocs/luci-static/resources/fchomo.js:151 +#: htdocs/luci-static/resources/fchomo.js:152 #: htdocs/luci-static/resources/fchomo.js:176 -#: htdocs/luci-static/resources/fchomo.js:177 -#: htdocs/luci-static/resources/fchomo.js:178 -#: htdocs/luci-static/resources/fchomo.js:179 -#: htdocs/luci-static/resources/fchomo.js:180 #: htdocs/luci-static/resources/fchomo.js:181 -#: htdocs/luci-static/resources/fchomo.js:187 +#: htdocs/luci-static/resources/fchomo.js:182 +#: htdocs/luci-static/resources/fchomo.js:183 +#: htdocs/luci-static/resources/fchomo.js:184 +#: htdocs/luci-static/resources/fchomo.js:185 +#: htdocs/luci-static/resources/fchomo.js:186 +#: htdocs/luci-static/resources/fchomo.js:193 +#: htdocs/luci-static/resources/fchomo/listeners.js:546 #: htdocs/luci-static/resources/view/fchomo/client.js:589 #: htdocs/luci-static/resources/view/fchomo/client.js:679 msgid "TCP" @@ -2704,7 +2743,7 @@ msgstr "TCP" msgid "TCP concurrency" msgstr "TCP 併發" -#: htdocs/luci-static/resources/view/fchomo/node.js:1250 +#: htdocs/luci-static/resources/view/fchomo/node.js:1264 msgid "TCP only" msgstr "僅 TCP" @@ -2716,54 +2755,61 @@ msgstr "TCP-Keep-Alive 閒置逾時" msgid "TCP-Keep-Alive interval" msgstr "TCP-Keep-Alive 間隔" -#: htdocs/luci-static/resources/fchomo.js:140 -#: htdocs/luci-static/resources/fchomo.js:141 -#: htdocs/luci-static/resources/fchomo.js:142 -#: htdocs/luci-static/resources/fchomo.js:143 -#: htdocs/luci-static/resources/fchomo.js:170 -#: htdocs/luci-static/resources/fchomo.js:172 -#: htdocs/luci-static/resources/fchomo.js:173 +#: htdocs/luci-static/resources/fchomo.js:144 +#: htdocs/luci-static/resources/fchomo.js:145 +#: htdocs/luci-static/resources/fchomo.js:146 +#: htdocs/luci-static/resources/fchomo.js:147 +#: htdocs/luci-static/resources/fchomo.js:155 +#: htdocs/luci-static/resources/fchomo.js:156 #: htdocs/luci-static/resources/fchomo.js:175 +#: htdocs/luci-static/resources/fchomo.js:177 +#: htdocs/luci-static/resources/fchomo.js:178 +#: htdocs/luci-static/resources/fchomo.js:180 +#: htdocs/luci-static/resources/fchomo.js:191 msgid "TCP/UDP" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1281 -#: htdocs/luci-static/resources/view/fchomo/node.js:1561 +#: htdocs/luci-static/resources/view/fchomo/node.js:1295 +#: htdocs/luci-static/resources/view/fchomo/node.js:1575 msgid "TFO" msgstr "TCP 快速開啟 (TFO)" +#: htdocs/luci-static/resources/fchomo/listeners.js:768 #: htdocs/luci-static/resources/view/fchomo/global.js:529 -#: htdocs/luci-static/resources/view/fchomo/node.js:454 -#: htdocs/luci-static/resources/view/fchomo/node.js:811 -#: htdocs/luci-static/resources/view/fchomo/node.js:937 -#: htdocs/luci-static/resources/view/fchomo/server.js:780 +#: htdocs/luci-static/resources/view/fchomo/node.js:461 +#: htdocs/luci-static/resources/view/fchomo/node.js:814 +#: htdocs/luci-static/resources/view/fchomo/node.js:948 msgid "TLS" msgstr "TLS" -#: htdocs/luci-static/resources/view/fchomo/node.js:968 -#: htdocs/luci-static/resources/view/fchomo/server.js:811 +#: htdocs/luci-static/resources/fchomo/listeners.js:805 +#: htdocs/luci-static/resources/view/fchomo/node.js:979 msgid "TLS ALPN" msgstr "TLS ALPN" -#: htdocs/luci-static/resources/view/fchomo/node.js:962 +#: htdocs/luci-static/resources/view/fchomo/node.js:973 msgid "TLS SNI" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:121 #: htdocs/luci-static/resources/view/fchomo/node.js:224 -#: htdocs/luci-static/resources/view/fchomo/server.js:173 msgid "TLS fields" msgstr "TLS欄位" -#: htdocs/luci-static/resources/fchomo.js:149 -#: htdocs/luci-static/resources/fchomo.js:184 +#: htdocs/luci-static/resources/fchomo.js:153 +#: htdocs/luci-static/resources/fchomo.js:189 msgid "TUIC" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:225 +#: htdocs/luci-static/resources/fchomo.js:231 msgid "TURN" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:243 +#: htdocs/luci-static/resources/fchomo/listeners.js:484 +msgid "Target address" +msgstr "目標位址" + +#: htdocs/luci-static/resources/fchomo/listeners.js:187 msgid "" "Tell the client to use the BBR flow control algorithm instead of Hysteria CC." msgstr "讓客戶端使用 BBR 流控演算法。" @@ -2773,34 +2819,34 @@ msgstr "讓客戶端使用 BBR 流控演算法。" msgid "The %s address used by local machine in the Cloudflare WARP network." msgstr "Cloudflare WARP 網路中使用的本機 %s 位址。" -#: htdocs/luci-static/resources/view/fchomo/node.js:727 -#: htdocs/luci-static/resources/view/fchomo/node.js:735 +#: htdocs/luci-static/resources/view/fchomo/node.js:730 +#: htdocs/luci-static/resources/view/fchomo/node.js:738 msgid "The %s address used by local machine in the Wireguard network." msgstr "WireGuard 網路中使用的本機 %s 位址。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1043 -#: htdocs/luci-static/resources/view/fchomo/server.js:832 +#: htdocs/luci-static/resources/fchomo/listeners.js:826 +#: htdocs/luci-static/resources/view/fchomo/node.js:1057 msgid "The %s private key, in PEM format." msgstr "%s私鑰,需要 PEM 格式。" +#: htdocs/luci-static/resources/fchomo/listeners.js:811 +#: htdocs/luci-static/resources/fchomo/listeners.js:849 #: htdocs/luci-static/resources/view/fchomo/global.js:550 -#: htdocs/luci-static/resources/view/fchomo/node.js:1029 -#: htdocs/luci-static/resources/view/fchomo/server.js:817 -#: htdocs/luci-static/resources/view/fchomo/server.js:855 +#: htdocs/luci-static/resources/view/fchomo/node.js:1043 msgid "The %s public key, in PEM format." msgstr "%s公鑰,需要 PEM 格式。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1063 +#: htdocs/luci-static/resources/view/fchomo/node.js:1077 msgid "" "The ECH parameter of the HTTPS record for the domain. Leave empty to resolve " "via DNS." msgstr "網域的 HTTPS 記錄的 ECH 參數。留空則透過 DNS 解析。" -#: htdocs/luci-static/resources/view/fchomo/node.js:380 +#: htdocs/luci-static/resources/view/fchomo/node.js:386 msgid "The ED25519 available private key or UUID provided by Sudoku server." msgstr "Sudoku 伺服器提供的 ED25519 可用私鑰 或 UUID。" -#: htdocs/luci-static/resources/view/fchomo/server.js:300 +#: htdocs/luci-static/resources/fchomo/listeners.js:250 msgid "The ED25519 master public key or UUID generated by Sudoku." msgstr "Sudoku 產生的 ED25519 主公鑰 或 UUID。" @@ -2808,8 +2854,8 @@ msgstr "Sudoku 產生的 ED25519 主公鑰 或 UUID。" msgid "The default value is 2:00 every day." msgstr "預設值為每天 2:00。" -#: htdocs/luci-static/resources/view/fchomo/node.js:918 -#: htdocs/luci-static/resources/view/fchomo/server.js:658 +#: htdocs/luci-static/resources/fchomo/listeners.js:646 +#: htdocs/luci-static/resources/view/fchomo/node.js:929 msgid "" "The first padding must have a probability of 100% and at least 35 bytes." msgstr "首個填充必須為 100% 的機率並且至少 35 位元組。" @@ -2824,19 +2870,19 @@ msgstr "匹配 %s 的將被視為未被投毒汙染。" msgid "The matching %s will be deemed as poisoned." msgstr "匹配 %s 的將被視為已被投毒汙染。" -#: htdocs/luci-static/resources/view/fchomo/node.js:916 -#: htdocs/luci-static/resources/view/fchomo/server.js:656 +#: htdocs/luci-static/resources/fchomo/listeners.js:644 +#: htdocs/luci-static/resources/view/fchomo/node.js:927 msgid "The server and client can set different padding parameters." msgstr "伺服器和客戶端可以設定不同的填充參數。" +#: htdocs/luci-static/resources/fchomo/listeners.js:907 #: htdocs/luci-static/resources/view/fchomo/global.js:594 -#: htdocs/luci-static/resources/view/fchomo/server.js:913 msgid "This ECH parameter needs to be added to the HTTPS record of the domain." msgstr "此 ECH 參數需要加入到網域的 HTTPS 記錄中。" #: htdocs/luci-static/resources/view/fchomo/client.js:1579 -#: htdocs/luci-static/resources/view/fchomo/node.js:1023 -#: htdocs/luci-static/resources/view/fchomo/node.js:1597 +#: htdocs/luci-static/resources/view/fchomo/node.js:1037 +#: htdocs/luci-static/resources/view/fchomo/node.js:1611 msgid "" "This is DANGEROUS, your traffic is almost like " "PLAIN TEXT! Use at your own risk!" @@ -2875,28 +2921,33 @@ msgstr "Tproxy Fwmark/fwmask" msgid "Tproxy port" msgstr "Tproxy 連接埠" -#: htdocs/luci-static/resources/view/fchomo/node.js:1753 +#: htdocs/luci-static/resources/fchomo/listeners.js:238 +#: htdocs/luci-static/resources/view/fchomo/node.js:378 +msgid "Traffic pattern" +msgstr "流量模式" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1767 msgid "Transit proxy group" msgstr "中轉代理組" -#: htdocs/luci-static/resources/view/fchomo/node.js:1759 +#: htdocs/luci-static/resources/view/fchomo/node.js:1773 msgid "Transit proxy node" msgstr "中轉代理節點" +#: htdocs/luci-static/resources/fchomo/listeners.js:231 +#: htdocs/luci-static/resources/fchomo/listeners.js:958 #: htdocs/luci-static/resources/view/fchomo/node.js:355 -#: htdocs/luci-static/resources/view/fchomo/node.js:1105 -#: htdocs/luci-static/resources/view/fchomo/server.js:287 -#: htdocs/luci-static/resources/view/fchomo/server.js:964 +#: htdocs/luci-static/resources/view/fchomo/node.js:1119 msgid "Transport" msgstr "傳輸層" +#: htdocs/luci-static/resources/fchomo/listeners.js:122 #: htdocs/luci-static/resources/view/fchomo/node.js:225 -#: htdocs/luci-static/resources/view/fchomo/server.js:174 msgid "Transport fields" msgstr "傳輸層欄位" -#: htdocs/luci-static/resources/view/fchomo/node.js:1110 -#: htdocs/luci-static/resources/view/fchomo/server.js:969 +#: htdocs/luci-static/resources/fchomo/listeners.js:963 +#: htdocs/luci-static/resources/view/fchomo/node.js:1124 msgid "Transport type" msgstr "傳輸層類型" @@ -2904,11 +2955,16 @@ msgstr "傳輸層類型" msgid "Treat the destination IP as the source IP." msgstr "將 目標 IP 視為 來源 IP。" -#: htdocs/luci-static/resources/fchomo.js:147 -#: htdocs/luci-static/resources/fchomo.js:180 +#: htdocs/luci-static/resources/fchomo.js:151 +#: htdocs/luci-static/resources/fchomo.js:185 msgid "Trojan" msgstr "" +#: htdocs/luci-static/resources/fchomo.js:155 +#: htdocs/luci-static/resources/fchomo.js:191 +msgid "TrustTunnel" +msgstr "" + #: htdocs/luci-static/resources/view/fchomo/global.js:764 msgid "Tun Fwmark/fwmask" msgstr "Tun Fwmark/fwmask" @@ -2925,30 +2981,35 @@ msgstr "Tun 設定" msgid "Tun stack." msgstr "Tun 堆栈" +#: htdocs/luci-static/resources/fchomo.js:156 +msgid "Tunnel" +msgstr "" + +#: htdocs/luci-static/resources/fchomo/listeners.js:141 #: htdocs/luci-static/resources/view/fchomo/client.js:530 #: htdocs/luci-static/resources/view/fchomo/client.js:643 #: htdocs/luci-static/resources/view/fchomo/client.js:737 #: htdocs/luci-static/resources/view/fchomo/client.js:842 #: htdocs/luci-static/resources/view/fchomo/client.js:1028 #: htdocs/luci-static/resources/view/fchomo/node.js:238 -#: htdocs/luci-static/resources/view/fchomo/node.js:1439 -#: htdocs/luci-static/resources/view/fchomo/node.js:1724 +#: htdocs/luci-static/resources/view/fchomo/node.js:1453 +#: htdocs/luci-static/resources/view/fchomo/node.js:1738 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:269 -#: htdocs/luci-static/resources/view/fchomo/server.js:193 msgid "Type" msgstr "類型" -#: htdocs/luci-static/resources/fchomo.js:149 -#: htdocs/luci-static/resources/fchomo.js:150 -#: htdocs/luci-static/resources/fchomo.js:183 -#: htdocs/luci-static/resources/fchomo.js:184 -#: htdocs/luci-static/resources/fchomo.js:185 -#: htdocs/luci-static/resources/fchomo.js:186 +#: htdocs/luci-static/resources/fchomo.js:153 +#: htdocs/luci-static/resources/fchomo.js:154 +#: htdocs/luci-static/resources/fchomo.js:188 +#: htdocs/luci-static/resources/fchomo.js:189 +#: htdocs/luci-static/resources/fchomo.js:190 +#: htdocs/luci-static/resources/fchomo.js:192 +#: htdocs/luci-static/resources/fchomo/listeners.js:547 +#: htdocs/luci-static/resources/fchomo/listeners.js:551 #: htdocs/luci-static/resources/view/fchomo/client.js:588 #: htdocs/luci-static/resources/view/fchomo/client.js:678 -#: htdocs/luci-static/resources/view/fchomo/node.js:851 -#: htdocs/luci-static/resources/view/fchomo/node.js:1571 -#: htdocs/luci-static/resources/view/fchomo/server.js:563 +#: htdocs/luci-static/resources/view/fchomo/node.js:862 +#: htdocs/luci-static/resources/view/fchomo/node.js:1585 msgid "UDP" msgstr "UDP" @@ -2972,19 +3033,19 @@ msgstr "UDP 包中繼模式。" msgid "UDP relay mode" msgstr "UDP 中繼模式" -#: htdocs/luci-static/resources/fchomo.js:214 +#: htdocs/luci-static/resources/fchomo.js:220 msgid "URL test" msgstr "自動選擇" -#: htdocs/luci-static/resources/view/fchomo/node.js:503 +#: htdocs/luci-static/resources/fchomo/listeners.js:247 +#: htdocs/luci-static/resources/fchomo/listeners.js:405 +#: htdocs/luci-static/resources/fchomo/listeners.js:460 +#: htdocs/luci-static/resources/view/fchomo/node.js:512 #: htdocs/luci-static/resources/view/fchomo/node.js:621 -#: htdocs/luci-static/resources/view/fchomo/server.js:297 -#: htdocs/luci-static/resources/view/fchomo/server.js:448 -#: htdocs/luci-static/resources/view/fchomo/server.js:512 msgid "UUID" msgstr "UUID" -#: htdocs/luci-static/resources/fchomo.js:1185 +#: htdocs/luci-static/resources/fchomo.js:1191 msgid "Unable to download unsupported type: %s" msgstr "無法下載不支援的類型: %s" @@ -3009,8 +3070,8 @@ msgstr "未知錯誤。" msgid "Unknown error: %s" msgstr "未知錯誤:%s" -#: htdocs/luci-static/resources/view/fchomo/node.js:856 -#: htdocs/luci-static/resources/view/fchomo/node.js:1576 +#: htdocs/luci-static/resources/view/fchomo/node.js:867 +#: htdocs/luci-static/resources/view/fchomo/node.js:1590 msgid "UoT" msgstr "UDP over TCP (UoT)" @@ -3018,22 +3079,22 @@ msgstr "UDP over TCP (UoT)" msgid "Update failed." msgstr "更新失敗。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1508 +#: htdocs/luci-static/resources/view/fchomo/node.js:1522 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:375 msgid "Update interval" msgstr "更新間隔" -#: htdocs/luci-static/resources/view/fchomo/node.js:1268 +#: htdocs/luci-static/resources/view/fchomo/node.js:1282 msgid "Upload bandwidth" msgstr "上傳頻寬" -#: htdocs/luci-static/resources/view/fchomo/node.js:1269 +#: htdocs/luci-static/resources/view/fchomo/node.js:1283 msgid "Upload bandwidth in Mbps." msgstr "上傳頻寬(單位:Mbps)。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1034 -#: htdocs/luci-static/resources/view/fchomo/server.js:823 -#: htdocs/luci-static/resources/view/fchomo/server.js:863 +#: htdocs/luci-static/resources/fchomo/listeners.js:817 +#: htdocs/luci-static/resources/fchomo/listeners.js:857 +#: htdocs/luci-static/resources/view/fchomo/node.js:1048 msgid "Upload certificate" msgstr "上傳憑證" @@ -3041,17 +3102,17 @@ msgstr "上傳憑證" msgid "Upload initial package" msgstr "上傳初始資源包" -#: htdocs/luci-static/resources/view/fchomo/node.js:1048 -#: htdocs/luci-static/resources/view/fchomo/server.js:838 +#: htdocs/luci-static/resources/fchomo/listeners.js:832 +#: htdocs/luci-static/resources/view/fchomo/node.js:1062 msgid "Upload key" msgstr "上傳金鑰" +#: htdocs/luci-static/resources/fchomo/listeners.js:820 +#: htdocs/luci-static/resources/fchomo/listeners.js:835 +#: htdocs/luci-static/resources/fchomo/listeners.js:860 #: htdocs/luci-static/resources/view/fchomo/global.js:306 -#: htdocs/luci-static/resources/view/fchomo/node.js:1037 #: htdocs/luci-static/resources/view/fchomo/node.js:1051 -#: htdocs/luci-static/resources/view/fchomo/server.js:826 -#: htdocs/luci-static/resources/view/fchomo/server.js:841 -#: htdocs/luci-static/resources/view/fchomo/server.js:866 +#: htdocs/luci-static/resources/view/fchomo/node.js:1065 msgid "Upload..." msgstr "上傳..." @@ -3075,7 +3136,7 @@ msgstr "用於解析 DNS 伺服器的網域。必須是 IP。" msgid "Used to resolve the domain of the Proxy node." msgstr "用於解析代理節點的網域。" -#: htdocs/luci-static/resources/view/fchomo/node.js:963 +#: htdocs/luci-static/resources/view/fchomo/node.js:974 msgid "Used to verify the hostname on the returned certificates." msgstr "用於驗證傳回的憑證上的主機名稱。" @@ -3083,8 +3144,8 @@ msgstr "用於驗證傳回的憑證上的主機名稱。" msgid "User Authentication" msgstr "使用者認證" +#: htdocs/luci-static/resources/fchomo/listeners.js:160 #: htdocs/luci-static/resources/view/fchomo/node.js:256 -#: htdocs/luci-static/resources/view/fchomo/server.js:216 msgid "Username" msgstr "使用者名稱" @@ -3092,50 +3153,50 @@ msgstr "使用者名稱" msgid "Users filter mode" msgstr "使用者過濾模式" -#: htdocs/luci-static/resources/view/fchomo/node.js:1198 +#: htdocs/luci-static/resources/view/fchomo/node.js:1212 msgid "V2ray HTTPUpgrade" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1203 +#: htdocs/luci-static/resources/view/fchomo/node.js:1217 msgid "V2ray HTTPUpgrade fast open" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:146 -#: htdocs/luci-static/resources/fchomo.js:179 +#: htdocs/luci-static/resources/fchomo.js:150 +#: htdocs/luci-static/resources/fchomo.js:184 msgid "VLESS" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:145 -#: htdocs/luci-static/resources/fchomo.js:178 +#: htdocs/luci-static/resources/fchomo.js:149 +#: htdocs/luci-static/resources/fchomo.js:183 msgid "VMess" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1445 -#: htdocs/luci-static/resources/view/fchomo/node.js:1730 +#: htdocs/luci-static/resources/view/fchomo/node.js:1459 +#: htdocs/luci-static/resources/view/fchomo/node.js:1744 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:312 msgid "Value" msgstr "可視化值" -#: htdocs/luci-static/resources/fchomo.js:343 +#: htdocs/luci-static/resources/fchomo.js:349 msgid "Verify if given" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:494 -#: htdocs/luci-static/resources/view/fchomo/node.js:830 -#: htdocs/luci-static/resources/view/fchomo/server.js:554 +#: htdocs/luci-static/resources/fchomo/listeners.js:511 +#: htdocs/luci-static/resources/view/fchomo/node.js:503 +#: htdocs/luci-static/resources/view/fchomo/node.js:833 msgid "Version" msgstr "版本" -#: htdocs/luci-static/resources/view/fchomo/node.js:838 +#: htdocs/luci-static/resources/view/fchomo/node.js:841 msgid "Version hint" msgstr "" +#: htdocs/luci-static/resources/fchomo/listeners.js:120 #: htdocs/luci-static/resources/view/fchomo/node.js:223 -#: htdocs/luci-static/resources/view/fchomo/server.js:172 msgid "Vless Encryption fields" msgstr "Vless Encryption 欄位" -#: htdocs/luci-static/resources/fchomo.js:380 +#: htdocs/luci-static/resources/fchomo.js:386 msgid "Wait a random 0-111 milliseconds with 75% probability." msgstr "以 75% 的機率等待隨機 0-111 毫秒。" @@ -3143,16 +3204,19 @@ msgstr "以 75% 的機率等待隨機 0-111 毫秒。" msgid "Warning" msgstr "警告" -#: htdocs/luci-static/resources/view/fchomo/node.js:1115 -#: htdocs/luci-static/resources/view/fchomo/node.js:1126 -#: htdocs/luci-static/resources/view/fchomo/node.js:1131 -#: htdocs/luci-static/resources/view/fchomo/server.js:971 -#: htdocs/luci-static/resources/view/fchomo/server.js:982 -#: htdocs/luci-static/resources/view/fchomo/server.js:987 +#: htdocs/luci-static/resources/fchomo/listeners.js:390 +#: htdocs/luci-static/resources/fchomo/listeners.js:965 +#: htdocs/luci-static/resources/fchomo/listeners.js:976 +#: htdocs/luci-static/resources/fchomo/listeners.js:981 +#: htdocs/luci-static/resources/view/fchomo/node.js:457 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +#: htdocs/luci-static/resources/view/fchomo/node.js:1129 +#: htdocs/luci-static/resources/view/fchomo/node.js:1140 +#: htdocs/luci-static/resources/view/fchomo/node.js:1145 msgid "WebSocket" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:132 +#: htdocs/luci-static/resources/view/fchomo/server.js:24 msgid "When used as a server, HomeProxy is a better choice." msgstr "用作服務端時,HomeProxy 是更好的選擇。" @@ -3160,23 +3224,23 @@ msgstr "用作服務端時,HomeProxy 是更好的選擇。" msgid "White list" msgstr "白名單" -#: htdocs/luci-static/resources/fchomo.js:186 +#: htdocs/luci-static/resources/fchomo.js:192 msgid "WireGuard" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:749 +#: htdocs/luci-static/resources/view/fchomo/node.js:752 msgid "WireGuard peer public key." msgstr "WireGuard 對端公鑰。" -#: htdocs/luci-static/resources/view/fchomo/node.js:756 +#: htdocs/luci-static/resources/view/fchomo/node.js:759 msgid "WireGuard pre-shared key." msgstr "WireGuard 預先共用金鑰。" -#: htdocs/luci-static/resources/view/fchomo/node.js:741 +#: htdocs/luci-static/resources/view/fchomo/node.js:744 msgid "WireGuard requires base64-encoded private keys." msgstr "WireGuard 要求 base64 編碼的私鑰。" -#: htdocs/luci-static/resources/view/fchomo/server.js:617 +#: htdocs/luci-static/resources/fchomo/listeners.js:605 msgid "XOR mode" msgstr "XOR 模式" @@ -3192,23 +3256,23 @@ msgstr "Yaml 格式文本" msgid "YouTube" msgstr "YouTube" -#: htdocs/luci-static/resources/fchomo.js:1624 +#: htdocs/luci-static/resources/fchomo.js:1630 msgid "Your %s was successfully uploaded. Size: %sB." msgstr "您的 %s 已成功上傳。大小:%sB。" -#: htdocs/luci-static/resources/fchomo.js:316 -#: htdocs/luci-static/resources/fchomo.js:329 -#: htdocs/luci-static/resources/fchomo.js:334 +#: htdocs/luci-static/resources/fchomo.js:322 +#: htdocs/luci-static/resources/fchomo.js:335 +#: htdocs/luci-static/resources/fchomo.js:340 #: htdocs/luci-static/resources/view/fchomo/node.js:646 msgid "aes-128-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:317 +#: htdocs/luci-static/resources/fchomo.js:323 msgid "aes-192-gcm" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:318 -#: htdocs/luci-static/resources/fchomo.js:335 +#: htdocs/luci-static/resources/fchomo.js:324 +#: htdocs/luci-static/resources/fchomo.js:341 msgid "aes-256-gcm" msgstr "" @@ -3220,15 +3284,15 @@ msgstr "自動" msgid "bbr" msgstr "bbr" -#: htdocs/luci-static/resources/view/fchomo/node.js:1039 -#: htdocs/luci-static/resources/view/fchomo/server.js:828 -#: htdocs/luci-static/resources/view/fchomo/server.js:868 +#: htdocs/luci-static/resources/fchomo/listeners.js:822 +#: htdocs/luci-static/resources/fchomo/listeners.js:862 +#: htdocs/luci-static/resources/view/fchomo/node.js:1053 msgid "certificate" msgstr "憑證" -#: htdocs/luci-static/resources/fchomo.js:319 -#: htdocs/luci-static/resources/fchomo.js:330 +#: htdocs/luci-static/resources/fchomo.js:325 #: htdocs/luci-static/resources/fchomo.js:336 +#: htdocs/luci-static/resources/fchomo.js:342 msgid "chacha20-ietf-poly1305" msgstr "" @@ -3240,8 +3304,8 @@ msgstr "" msgid "cubic" msgstr "cubic" -#: htdocs/luci-static/resources/view/fchomo/server.js:569 -#: htdocs/luci-static/resources/view/fchomo/server.js:600 +#: htdocs/luci-static/resources/fchomo/listeners.js:557 +#: htdocs/luci-static/resources/fchomo/listeners.js:588 msgid "decryption" msgstr "decryption" @@ -3249,36 +3313,36 @@ msgstr "decryption" msgid "dnsmasq selects upstream on its own. (may affect CDN accuracy)" msgstr "dnsmasq 自行選擇上游服務器。 (可能影響 CDN 準確性)" -#: htdocs/luci-static/resources/view/fchomo/node.js:1588 +#: htdocs/luci-static/resources/view/fchomo/node.js:1602 msgid "down" msgstr "Hysteria 下載速率" -#: htdocs/luci-static/resources/view/fchomo/node.js:870 -#: htdocs/luci-static/resources/view/fchomo/node.js:893 -#: htdocs/luci-static/resources/view/fchomo/server.js:604 +#: htdocs/luci-static/resources/fchomo/listeners.js:592 +#: htdocs/luci-static/resources/view/fchomo/node.js:881 +#: htdocs/luci-static/resources/view/fchomo/node.js:904 msgid "encryption" msgstr "encryption" -#: htdocs/luci-static/resources/view/fchomo/node.js:435 -#: htdocs/luci-static/resources/view/fchomo/server.js:424 +#: htdocs/luci-static/resources/fchomo/listeners.js:374 +#: htdocs/luci-static/resources/view/fchomo/node.js:441 msgid "false = bandwidth optimized downlink; true = pure Sudoku downlink." msgstr "false = 頻寬最佳化下行 true = 純 Sudoku 下行。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1114 -#: htdocs/luci-static/resources/view/fchomo/node.js:1125 -#: htdocs/luci-static/resources/view/fchomo/node.js:1130 -#: htdocs/luci-static/resources/view/fchomo/server.js:970 -#: htdocs/luci-static/resources/view/fchomo/server.js:981 -#: htdocs/luci-static/resources/view/fchomo/server.js:986 +#: htdocs/luci-static/resources/fchomo/listeners.js:964 +#: htdocs/luci-static/resources/fchomo/listeners.js:975 +#: htdocs/luci-static/resources/fchomo/listeners.js:980 +#: htdocs/luci-static/resources/view/fchomo/node.js:1128 +#: htdocs/luci-static/resources/view/fchomo/node.js:1139 +#: htdocs/luci-static/resources/view/fchomo/node.js:1144 msgid "gRPC" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1181 +#: htdocs/luci-static/resources/view/fchomo/node.js:1195 msgid "gRPC User-Agent" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1177 -#: htdocs/luci-static/resources/view/fchomo/server.js:1005 +#: htdocs/luci-static/resources/fchomo/listeners.js:999 +#: htdocs/luci-static/resources/view/fchomo/node.js:1191 msgid "gRPC service name" msgstr "gRPC 服務名稱" @@ -3286,11 +3350,11 @@ msgstr "gRPC 服務名稱" msgid "gVisor" msgstr "gVisor" -#: htdocs/luci-static/resources/view/fchomo/node.js:1219 +#: htdocs/luci-static/resources/view/fchomo/node.js:1233 msgid "h2mux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/server.js:769 +#: htdocs/luci-static/resources/fchomo/listeners.js:757 msgid "least one keypair required" msgstr "至少需要一對密鑰" @@ -3304,17 +3368,17 @@ msgstr "metacubexd" #: htdocs/luci-static/resources/view/fchomo/client.js:1480 #: htdocs/luci-static/resources/view/fchomo/client.js:1711 #: htdocs/luci-static/resources/view/fchomo/client.js:1767 -#: htdocs/luci-static/resources/view/fchomo/node.js:1411 +#: htdocs/luci-static/resources/view/fchomo/node.js:1425 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:232 msgid "mihomo config" msgstr "mihomo 配置" -#: htdocs/luci-static/resources/fchomo.js:362 +#: htdocs/luci-static/resources/fchomo.js:368 msgid "mlkem768x25519plus" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1285 -#: htdocs/luci-static/resources/view/fchomo/node.js:1566 +#: htdocs/luci-static/resources/view/fchomo/node.js:1299 +#: htdocs/luci-static/resources/view/fchomo/node.js:1580 msgid "mpTCP" msgstr "多路徑 TCP (mpTCP)" @@ -3326,21 +3390,21 @@ msgstr "new_reno" msgid "no-resolve" msgstr "no-resolve" -#: htdocs/luci-static/resources/fchomo.js:1359 -#: htdocs/luci-static/resources/fchomo.js:1454 -#: htdocs/luci-static/resources/fchomo.js:1489 -#: htdocs/luci-static/resources/fchomo.js:1517 +#: htdocs/luci-static/resources/fchomo.js:1365 +#: htdocs/luci-static/resources/fchomo.js:1460 +#: htdocs/luci-static/resources/fchomo.js:1495 +#: htdocs/luci-static/resources/fchomo.js:1523 msgid "non-empty value" msgstr "非空值" -#: htdocs/luci-static/resources/fchomo.js:314 -#: htdocs/luci-static/resources/fchomo.js:328 -#: htdocs/luci-static/resources/fchomo.js:340 +#: htdocs/luci-static/resources/fchomo.js:320 +#: htdocs/luci-static/resources/fchomo.js:334 +#: htdocs/luci-static/resources/fchomo.js:346 +#: htdocs/luci-static/resources/fchomo/listeners.js:492 #: htdocs/luci-static/resources/view/fchomo/node.js:644 #: htdocs/luci-static/resources/view/fchomo/node.js:664 -#: htdocs/luci-static/resources/view/fchomo/node.js:799 +#: htdocs/luci-static/resources/view/fchomo/node.js:802 #: htdocs/luci-static/resources/view/fchomo/ruleset.js:308 -#: htdocs/luci-static/resources/view/fchomo/server.js:535 msgid "none" msgstr "無" @@ -3352,19 +3416,25 @@ msgstr "未找到" msgid "not included \",\"" msgstr "不包含 \",\"" -#: htdocs/luci-static/resources/fchomo.js:200 +#: htdocs/luci-static/resources/fchomo.js:206 +#: htdocs/luci-static/resources/fchomo/listeners.js:523 +#: htdocs/luci-static/resources/fchomo/listeners.js:524 msgid "null" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:800 +#: htdocs/luci-static/resources/view/fchomo/node.js:803 msgid "obfs-simple" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:479 -msgid "only applies when %s is stream/poll/auto." -msgstr "僅當 %s 為 stream/poll/auto 時適用。" +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +msgid "only applies when %s is %s." +msgstr "僅當 %s 為 %s 時適用。" -#: htdocs/luci-static/resources/view/fchomo/node.js:1546 +#: htdocs/luci-static/resources/view/fchomo/node.js:486 +msgid "only applies when %s is not %s." +msgstr "僅當 %s 不為 %s 時適用。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1560 msgid "override.proxy-name" msgstr "" @@ -3372,13 +3442,13 @@ msgstr "" msgid "packet addr (v2ray-core v5+)" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:449 -#: htdocs/luci-static/resources/view/fchomo/server.js:438 +#: htdocs/luci-static/resources/fchomo/listeners.js:388 +#: htdocs/luci-static/resources/view/fchomo/node.js:455 msgid "poll" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1053 -#: htdocs/luci-static/resources/view/fchomo/server.js:843 +#: htdocs/luci-static/resources/fchomo/listeners.js:837 +#: htdocs/luci-static/resources/view/fchomo/node.js:1067 msgid "private key" msgstr "私鑰" @@ -3391,7 +3461,7 @@ msgstr "razord-meta" msgid "requires front-end adaptation using the API." msgstr "需要使用 API 的前端適配。" -#: htdocs/luci-static/resources/view/fchomo/node.js:804 +#: htdocs/luci-static/resources/view/fchomo/node.js:807 msgid "restls" msgstr "" @@ -3399,17 +3469,17 @@ msgstr "" msgid "rule-set" msgstr "規則集" -#: htdocs/luci-static/resources/view/fchomo/node.js:803 -#: htdocs/luci-static/resources/view/fchomo/server.js:536 +#: htdocs/luci-static/resources/fchomo/listeners.js:493 +#: htdocs/luci-static/resources/view/fchomo/node.js:806 msgid "shadow-tls" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:1217 +#: htdocs/luci-static/resources/view/fchomo/node.js:1231 msgid "smux" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:448 -#: htdocs/luci-static/resources/view/fchomo/server.js:437 +#: htdocs/luci-static/resources/fchomo/listeners.js:387 +#: htdocs/luci-static/resources/view/fchomo/node.js:454 msgid "split-stream" msgstr "" @@ -3417,7 +3487,11 @@ msgstr "" msgid "src" msgstr "src" -#: htdocs/luci-static/resources/view/fchomo/server.js:296 +#: htdocs/luci-static/resources/view/fchomo/node.js:488 +msgid "stream/poll/auto" +msgstr "" + +#: htdocs/luci-static/resources/fchomo/listeners.js:246 msgid "sudoku-keypair" msgstr "" @@ -3425,87 +3499,87 @@ msgstr "" msgid "unchecked" msgstr "未檢查" -#: htdocs/luci-static/resources/fchomo.js:426 +#: htdocs/luci-static/resources/fchomo.js:432 msgid "unique UCI identifier" msgstr "獨立 UCI 識別" -#: htdocs/luci-static/resources/fchomo.js:429 +#: htdocs/luci-static/resources/fchomo.js:435 msgid "unique identifier" msgstr "獨立標識" -#: htdocs/luci-static/resources/fchomo.js:1526 +#: htdocs/luci-static/resources/fchomo.js:1532 msgid "unique value" msgstr "獨立值" -#: htdocs/luci-static/resources/view/fchomo/node.js:1582 +#: htdocs/luci-static/resources/view/fchomo/node.js:1596 msgid "up" msgstr "Hysteria 上傳速率" -#: htdocs/luci-static/resources/view/fchomo/node.js:495 +#: htdocs/luci-static/resources/fchomo/listeners.js:512 +#: htdocs/luci-static/resources/view/fchomo/node.js:504 #: htdocs/luci-static/resources/view/fchomo/node.js:539 -#: htdocs/luci-static/resources/view/fchomo/node.js:831 -#: htdocs/luci-static/resources/view/fchomo/node.js:863 -#: htdocs/luci-static/resources/view/fchomo/server.js:555 +#: htdocs/luci-static/resources/view/fchomo/node.js:834 +#: htdocs/luci-static/resources/view/fchomo/node.js:874 msgid "v1" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:496 -#: htdocs/luci-static/resources/view/fchomo/node.js:832 -#: htdocs/luci-static/resources/view/fchomo/node.js:864 -#: htdocs/luci-static/resources/view/fchomo/server.js:556 +#: htdocs/luci-static/resources/fchomo/listeners.js:513 +#: htdocs/luci-static/resources/view/fchomo/node.js:505 +#: htdocs/luci-static/resources/view/fchomo/node.js:835 +#: htdocs/luci-static/resources/view/fchomo/node.js:875 msgid "v2" msgstr "" -#: htdocs/luci-static/resources/view/fchomo/node.js:497 -#: htdocs/luci-static/resources/view/fchomo/node.js:833 -#: htdocs/luci-static/resources/view/fchomo/server.js:557 +#: htdocs/luci-static/resources/fchomo/listeners.js:514 +#: htdocs/luci-static/resources/view/fchomo/node.js:506 +#: htdocs/luci-static/resources/view/fchomo/node.js:836 msgid "v3" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1406 -#: htdocs/luci-static/resources/fchomo.js:1409 +#: htdocs/luci-static/resources/fchomo.js:1412 +#: htdocs/luci-static/resources/fchomo.js:1415 msgid "valid JSON format" msgstr "有效的 JSON 格式" -#: htdocs/luci-static/resources/view/fchomo/node.js:1013 +#: htdocs/luci-static/resources/view/fchomo/node.js:1027 msgid "valid SHA256 string with %d characters" msgstr "包含 %d 個字元的有效 SHA256 字串" -#: htdocs/luci-static/resources/fchomo.js:1431 -#: htdocs/luci-static/resources/fchomo.js:1434 +#: htdocs/luci-static/resources/fchomo.js:1437 +#: htdocs/luci-static/resources/fchomo.js:1440 msgid "valid URL" msgstr "有效網址" -#: htdocs/luci-static/resources/fchomo.js:1444 +#: htdocs/luci-static/resources/fchomo.js:1450 msgid "valid base64 key with %d characters" msgstr "包含 %d 個字元的有效 base64 金鑰" -#: htdocs/luci-static/resources/fchomo.js:1504 #: htdocs/luci-static/resources/fchomo.js:1510 +#: htdocs/luci-static/resources/fchomo.js:1516 msgid "valid format: 2x, 2p, 4v" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1491 +#: htdocs/luci-static/resources/fchomo.js:1497 msgid "valid key length with %d characters" msgstr "包含 %d 個字元的有效金鑰" -#: htdocs/luci-static/resources/fchomo.js:1369 +#: htdocs/luci-static/resources/fchomo.js:1375 msgid "valid port value" msgstr "有效連接埠值" -#: htdocs/luci-static/resources/fchomo.js:1419 +#: htdocs/luci-static/resources/fchomo.js:1425 msgid "valid uuid" msgstr "有效 uuid" -#: htdocs/luci-static/resources/fchomo.js:386 +#: htdocs/luci-static/resources/fchomo.js:392 msgid "vless-mlkem768" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:385 +#: htdocs/luci-static/resources/fchomo.js:391 msgid "vless-x25519" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:320 +#: htdocs/luci-static/resources/fchomo.js:326 msgid "xchacha20-ietf-poly1305" msgstr "" @@ -3513,7 +3587,7 @@ msgstr "" msgid "yacd-meta" msgstr "yacd-meta" -#: htdocs/luci-static/resources/view/fchomo/node.js:1218 +#: htdocs/luci-static/resources/view/fchomo/node.js:1232 msgid "yamux" msgstr "" @@ -3525,10 +3599,13 @@ msgstr "" msgid "zero" msgstr "" -#: htdocs/luci-static/resources/fchomo.js:1187 +#: htdocs/luci-static/resources/fchomo.js:1193 msgid "🡇" msgstr "" +#~ msgid "QUIC congestion controller." +#~ msgstr "QUIC 壅塞控制器。" + #~ msgid "" #~ "Uplink keeps the Sudoku protocol, and downlink characteristics are " #~ "consistent with uplink characteristics." diff --git a/small/luci-app-fchomo/root/etc/init.d/fchomo b/small/luci-app-fchomo/root/etc/init.d/fchomo index ddbb35d903..5c8a3e347e 100755 --- a/small/luci-app-fchomo/root/etc/init.d/fchomo +++ b/small/luci-app-fchomo/root/etc/init.d/fchomo @@ -45,6 +45,31 @@ opmc() { # @less_25_12 $OPM $action "$@" } +# add_firewall +add_firewall() { + local enabled auto_firewall listen port + config_get_bool enabled "$1" "enabled" "1" + config_get_bool auto_firewall "$1" "auto_firewall" "1" + config_get listen "$1" "listen" "::" + config_get port "$1" "port" + + [ "$enabled" = "0" ] && return 0 + [ "$auto_firewall" = "0" ] && return 0 + + json_add_object '' + json_add_string type rule + json_add_string target ACCEPT + json_add_string name "$1" + #json_add_string family '' # '' = IPv4 and IPv6 + json_add_string proto 'tcp udp' + json_add_string direction in + json_add_string src "*" + #json_add_string dest '' # '' = input + json_add_string dest_ip "$(echo "$listen" | grep -vE '^(0\.\d+\.\d+\.\d+|::)$')" + json_add_string dest_port "$port" + json_close_object +} + config_load "$CONF" # define global var: DEF_WAN DEF_WAN6 NIC_* NIC6_* @@ -333,6 +358,15 @@ start_service() { procd_set_param stderr 1 procd_set_param respawn + # add_firewall + procd_open_data + # configure firewall + json_add_array firewall + # meta l4proto %s th dport %s counter accept comment "!%s: accept server instance [%s]" + config_foreach add_firewall "inbound" + json_close_array + procd_close_data + procd_close_instance fi fi @@ -380,30 +414,6 @@ start_service() { procd_set_param respawn # add_firewall - add_firewall() { - local enabled auto_firewall listen port - config_get_bool enabled "$1" "enabled" "1" - config_get_bool auto_firewall "$1" "auto_firewall" "1" - config_get listen "$1" "listen" "::" - config_get port "$1" "port" - - [ "$enabled" = "0" ] && return 0 - [ "$auto_firewall" = "0" ] && return 0 - - json_add_object '' - json_add_string type rule - json_add_string target ACCEPT - json_add_string name "$1" - #json_add_string family '' # '' = IPv4 and IPv6 - json_add_string proto 'tcp udp' - json_add_string direction in - json_add_string src "*" - #json_add_string dest '' # '' = input - json_add_string dest_ip "$(echo "$listen" | grep -vE '^(0\.\d+\.\d+\.\d+|::)$')" - json_add_string dest_port "$port" - json_close_object - } - # procd_open_data # configure firewall json_add_array firewall diff --git a/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_node b/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_node index 691f6c9f5d..2cb34b6ca8 100755 --- a/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_node +++ b/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_node @@ -34,6 +34,22 @@ migrate() { uci_set "$CONF" "$1" vless_encryption_encryption "$vless_encryption" fi fi + + # tuic_congestion_controller -> congestion_controller + if isDefined "$1" tuic_congestion_controller; then + local tuic_congestion_controller + config_get tuic_congestion_controller "$1" tuic_congestion_controller "" + uci_remove "$CONF" "$1" tuic_congestion_controller + uci_set "$CONF" "$1" congestion_controller "$tuic_congestion_controller" + fi + + # masque_congestion_controller -> congestion_controller + if isDefined "$1" masque_congestion_controller; then + local masque_congestion_controller + config_get masque_congestion_controller "$1" masque_congestion_controller "" + uci_remove "$CONF" "$1" masque_congestion_controller + uci_set "$CONF" "$1" congestion_controller "$masque_congestion_controller" + fi } config_foreach migrate node diff --git a/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_server b/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_server new file mode 100755 index 0000000000..320d75e5c7 --- /dev/null +++ b/small/luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_server @@ -0,0 +1,33 @@ +#!/bin/sh +# Migration script for fchomo server +# Used to migrate LuCI application server option. + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +CONF=fchomo + +config_load "$CONF" + +# isDefined