Update On Wed Apr 1 21:14:59 CEST 2026

This commit is contained in:
github-action[bot]
2026-04-01 21:14:59 +02:00
parent 2133616eef
commit e333ea1668
96 changed files with 2666 additions and 1237 deletions
+1
View File
@@ -1316,3 +1316,4 @@ Update On Sat Mar 28 19:56:41 CET 2026
Update On Sun Mar 29 20:57:11 CEST 2026
Update On Mon Mar 30 21:11:19 CEST 2026
Update On Tue Mar 31 21:15:50 CEST 2026
Update On Wed Apr 1 21:14:50 CEST 2026
+2 -2
View File
@@ -58,8 +58,8 @@ subprojects {
minSdk = 21
targetSdk = 35
versionName = "2.11.24"
versionCode = 211024
versionName = "2.11.25"
versionCode = 211025
resValue("string", "release_name", "v$versionName")
resValue("integer", "release_code", "$versionCode")
@@ -189,29 +189,16 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
}
}
func (ss *ShadowSocks) dialContext(ctx context.Context) (c net.Conn, err error) {
if ss.kcptunClient != nil {
return ss.kcptunClient.OpenStream(ctx, ss.listenPacketContext)
}
return ss.dialer.DialContext(ctx, "tcp", ss.addr)
}
// DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
var c net.Conn
if ss.kcptunClient != nil {
c, err = ss.kcptunClient.OpenStream(ctx, func(ctx context.Context) (net.PacketConn, net.Addr, error) {
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, nil, err
}
addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
if err != nil {
return nil, nil, err
}
pc, err := ss.dialer.ListenPacket(ctx, "udp", "", addr.AddrPort())
if err != nil {
return nil, nil, err
}
return pc, addr, nil
})
} else {
c, err = ss.dialer.DialContext(ctx, "tcp", ss.addr)
}
c, err := ss.dialContext(ctx)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
@@ -224,24 +211,41 @@ func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_
return NewConn(c, ss), err
}
func (ss *ShadowSocks) listenPacketContext(ctx context.Context) (net.PacketConn, net.Addr, error) {
addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
if err != nil {
return nil, nil, err
}
pc, err := ss.dialer.ListenPacket(ctx, "udp", "", addr.AddrPort())
if err != nil {
return nil, nil, err
}
return pc, addr, nil
}
// ListenPacketContext implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
if ss.option.UDPOverTCP {
tcpConn, err := ss.DialContext(ctx, metadata)
c, err := ss.DialContext(ctx, metadata)
if err != nil {
return nil, err
}
return ss.ListenPacketOnStreamConn(ctx, tcpConn, metadata)
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil
} else {
return newPacketConn(N.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), ss), nil
}
}
if err := ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
if err != nil {
return nil, err
}
pc, err := ss.dialer.ListenPacket(ctx, "udp", "", addr.AddrPort())
pc, addr, err := ss.listenPacketContext(ctx)
if err != nil {
return nil, err
}
@@ -256,22 +260,6 @@ func (ss *ShadowSocks) ProxyInfo() C.ProxyInfo {
return info
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.option.UDPOverTCP {
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil
} else {
return newPacketConn(N.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), ss), nil
}
}
return nil, C.ErrNotSupport
}
// SupportUOT implements C.ProxyAdapter
func (ss *ShadowSocks) SupportUOT() bool {
return ss.option.UDPOverTCP
@@ -36,7 +36,7 @@ type SudokuOption struct {
AEADMethod string `proxy:"aead-method,omitempty"`
PaddingMin *int `proxy:"padding-min,omitempty"`
PaddingMax *int `proxy:"padding-max,omitempty"`
TableType string `proxy:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
TableType string `proxy:"table-type,omitempty"` // "prefer_ascii", "prefer_entropy", or directional "up_ascii_down_entropy"/"up_entropy_down_ascii"
EnablePureDownlink *bool `proxy:"enable-pure-downlink,omitempty"`
HTTPMask *bool `proxy:"http-mask,omitempty"`
HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto", "ws"
@@ -136,7 +136,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
})
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
return nil, err
}
return t.streamConnContext(ctx, c, metadata)
@@ -172,15 +172,18 @@ func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C
return err
}
func (t *Trojan) dialContext(ctx context.Context) (c net.Conn, err error) {
switch t.option.Network {
case "grpc": // gun transport
return t.gunTransport.Dial()
default:
}
return t.dialer.DialContext(ctx, "tcp", t.addr)
}
// DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
var c net.Conn
// gun transport
if t.gunTransport != nil {
c, err = t.gunTransport.Dial()
} else {
c, err = t.dialer.DialContext(ctx, "tcp", t.addr)
}
c, err := t.dialContext(ctx)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
@@ -190,7 +193,7 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
c, err = t.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, err
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
return NewConn(c, t), err
@@ -202,13 +205,7 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
return nil, err
}
var c net.Conn
// grpc transport
if t.gunTransport != nil {
c, err = t.gunTransport.Dial()
} else {
c, err = t.dialer.DialContext(ctx, "tcp", t.addr)
}
c, err := t.dialContext(ctx)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
@@ -218,7 +215,7 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
c, err = t.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, err
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
pc := trojan.NewPacketConn(c)
@@ -2,6 +2,7 @@ package outbound
import (
"context"
"errors"
"fmt"
"net"
"strconv"
@@ -24,6 +25,7 @@ import (
"github.com/metacubex/sing-vmess/packetaddr"
M "github.com/metacubex/sing/common/metadata"
"github.com/metacubex/tls"
"github.com/samber/lo"
)
type Vless struct {
@@ -35,6 +37,8 @@ type Vless struct {
// for gun mux
gunTransport *gun.Transport
// for xhttp
xhttpClient *xhttp.Client
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
@@ -72,12 +76,35 @@ type VlessOption struct {
}
type XHTTPOptions struct {
Path string `proxy:"path,omitempty"`
Host string `proxy:"host,omitempty"`
Mode string `proxy:"mode,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes string `proxy:"x-padding-bytes,omitempty"`
Path string `proxy:"path,omitempty"`
Host string `proxy:"host,omitempty"`
Mode string `proxy:"mode,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes string `proxy:"x-padding-bytes,omitempty"`
DownloadSettings *XHTTPDownloadSettings `proxy:"download-settings,omitempty"`
}
type XHTTPDownloadSettings struct {
// xhttp part
Path *string `proxy:"path,omitempty"`
Host *string `proxy:"host,omitempty"`
Headers *map[string]string `proxy:"headers,omitempty"`
NoGRPCHeader *bool `proxy:"no-grpc-header,omitempty"`
XPaddingBytes *string `proxy:"x-padding-bytes,omitempty"`
// proxy part
Server *string `proxy:"server,omitempty"`
Port *int `proxy:"port,omitempty"`
TLS *bool `proxy:"tls,omitempty"`
ALPN *[]string `proxy:"alpn,omitempty"`
ECHOpts *ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts *RealityOptions `proxy:"reality-opts,omitempty"`
SkipCertVerify *bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint *string `proxy:"fingerprint,omitempty"`
Certificate *string `proxy:"certificate,omitempty"`
PrivateKey *string `proxy:"private-key,omitempty"`
ServerName *string `proxy:"servername,omitempty"`
ClientFingerprint *string `proxy:"client-fingerprint,omitempty"`
}
func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
@@ -162,7 +189,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
case "grpc":
break // already handle in gun transport
case "xhttp":
break // already handle in dialXHTTPConn
break // already handle in xhttp client
default:
// default tcp network
// handle TLS
@@ -241,81 +268,20 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
return conn, nil
}
func (v *Vless) dialXHTTPConn(ctx context.Context) (net.Conn, error) {
requestHost := v.option.XHTTPOpts.Host
if requestHost == "" {
if v.option.ServerName != "" {
requestHost = v.option.ServerName
} else {
requestHost = v.option.Server
}
}
cfg := &xhttp.Config{
Host: requestHost,
Path: v.option.XHTTPOpts.Path,
Mode: v.option.XHTTPOpts.Mode,
Headers: v.option.XHTTPOpts.Headers,
NoGRPCHeader: v.option.XHTTPOpts.NoGRPCHeader,
XPaddingBytes: v.option.XHTTPOpts.XPaddingBytes,
}
mode := cfg.EffectiveMode(v.realityConfig != nil)
switch mode {
case "stream-one":
return xhttp.DialStreamOne(
ctx,
cfg,
func(ctx context.Context) (net.Conn, error) {
return v.dialer.DialContext(ctx, "tcp", v.addr)
},
func(ctx context.Context, raw net.Conn, isH2 bool) (net.Conn, error) {
return v.streamTLSConn(ctx, raw, isH2)
},
)
case "packet-up":
return xhttp.DialPacketUp(
ctx,
cfg,
func(ctx context.Context) (net.Conn, error) {
return v.dialer.DialContext(ctx, "tcp", v.addr)
},
func(ctx context.Context, raw net.Conn, isH2 bool) (net.Conn, error) {
return v.streamTLSConn(ctx, raw, isH2)
},
)
func (v *Vless) dialContext(ctx context.Context) (c net.Conn, err error) {
switch v.option.Network {
case "grpc": // gun transport
return v.gunTransport.Dial()
case "xhttp":
return v.xhttpClient.Dial()
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", mode)
}
return v.dialer.DialContext(ctx, "tcp", v.addr)
}
// DialContext implements C.ProxyAdapter
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
if v.option.Network == "xhttp" {
c, err := v.dialXHTTPConn(ctx)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
c, err = v.streamConnContext(ctx, c, metadata)
if err != nil {
safeConnClose(c, err)
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
return NewConn(c, v), nil
}
var c net.Conn
switch v.option.Network {
case "xhttp":
c, err = v.dialXHTTPConn(ctx)
case "grpc": // gun transport
c, err = v.gunTransport.Dial()
default:
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
}
c, err := v.dialContext(ctx)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
@@ -336,15 +302,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (
return nil, err
}
var c net.Conn
switch v.option.Network {
case "xhttp":
c, err = v.dialXHTTPConn(ctx)
case "grpc": // gun transport
c, err = v.gunTransport.Dial()
default:
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
}
c, err := v.dialContext(ctx)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
@@ -354,16 +312,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (
c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err)
}
return v.ListenPacketOnStreamConn(ctx, c, metadata)
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
if v.option.XUDP {
@@ -399,10 +348,18 @@ func (v *Vless) ProxyInfo() C.ProxyInfo {
// Close implements C.ProxyAdapter
func (v *Vless) Close() error {
var errs []error
if v.gunTransport != nil {
return v.gunTransport.Close()
if err := v.gunTransport.Close(); err != nil {
errs = append(errs, err)
}
}
return nil
if v.xhttpClient != nil {
if err := v.xhttpClient.Close(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
@@ -540,6 +497,129 @@ func NewVless(option VlessOption) (*Vless, error) {
}
v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig)
case "xhttp":
requestHost := v.option.XHTTPOpts.Host
if requestHost == "" {
if v.option.ServerName != "" {
requestHost = v.option.ServerName
} else {
requestHost = v.option.Server
}
}
cfg := &xhttp.Config{
Host: requestHost,
Path: v.option.XHTTPOpts.Path,
Mode: v.option.XHTTPOpts.Mode,
Headers: v.option.XHTTPOpts.Headers,
NoGRPCHeader: v.option.XHTTPOpts.NoGRPCHeader,
XPaddingBytes: v.option.XHTTPOpts.XPaddingBytes,
}
makeTransport := func() http.RoundTripper {
return xhttp.NewTransport(
func(ctx context.Context) (net.Conn, error) {
return v.dialer.DialContext(ctx, "tcp", v.addr)
},
func(ctx context.Context, raw net.Conn, isH2 bool) (net.Conn, error) {
return v.streamTLSConn(ctx, raw, isH2)
},
)
}
var makeDownloadTransport func() http.RoundTripper
if ds := v.option.XHTTPOpts.DownloadSettings; ds != nil {
if cfg.Mode == "stream-one" {
return nil, fmt.Errorf(`xhttp mode "stream-one" cannot be used with download-settings`)
}
downloadServer := lo.FromPtrOr(ds.Server, v.option.Server)
downloadPort := lo.FromPtrOr(ds.Port, v.option.Port)
downloadTLS := lo.FromPtrOr(ds.TLS, v.option.TLS)
downloadALPN := lo.FromPtrOr(ds.ALPN, v.option.ALPN)
downloadEchConfig := v.echConfig
if ds.ECHOpts != nil {
downloadEchConfig, err = ds.ECHOpts.Parse()
if err != nil {
return nil, err
}
}
downloadRealityCfg := v.realityConfig
if ds.RealityOpts != nil {
downloadRealityCfg, err = ds.RealityOpts.Parse()
if err != nil {
return nil, err
}
}
downloadSkipCertVerify := lo.FromPtrOr(ds.SkipCertVerify, v.option.SkipCertVerify)
downloadFingerprint := lo.FromPtrOr(ds.Fingerprint, v.option.Fingerprint)
downloadCertificate := lo.FromPtrOr(ds.Certificate, v.option.Certificate)
downloadPrivateKey := lo.FromPtrOr(ds.PrivateKey, v.option.PrivateKey)
downloadServerName := lo.FromPtrOr(ds.ServerName, v.option.ServerName)
downloadClientFingerprint := lo.FromPtrOr(ds.ClientFingerprint, v.option.ClientFingerprint)
downloadAddr := net.JoinHostPort(downloadServer, strconv.Itoa(downloadPort))
downloadHost := lo.FromPtrOr(ds.Host, v.option.XHTTPOpts.Host)
if downloadHost == "" {
if downloadServerName != "" {
downloadHost = downloadServerName
} else {
downloadHost = downloadServer
}
}
cfg.DownloadConfig = &xhttp.Config{
Host: downloadHost,
Path: lo.FromPtrOr(ds.Path, v.option.XHTTPOpts.Path),
Mode: v.option.XHTTPOpts.Mode,
Headers: lo.FromPtrOr(ds.Headers, v.option.XHTTPOpts.Headers),
NoGRPCHeader: lo.FromPtrOr(ds.NoGRPCHeader, v.option.XHTTPOpts.NoGRPCHeader),
XPaddingBytes: lo.FromPtrOr(ds.XPaddingBytes, v.option.XHTTPOpts.XPaddingBytes),
}
makeDownloadTransport = func() http.RoundTripper {
return xhttp.NewTransport(
func(ctx context.Context) (net.Conn, error) {
return v.dialer.DialContext(ctx, "tcp", downloadAddr)
},
func(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) {
if downloadTLS {
host, _, _ := net.SplitHostPort(downloadAddr)
tlsOpts := vmess.TLSConfig{
Host: host,
SkipCertVerify: downloadSkipCertVerify,
FingerPrint: downloadFingerprint,
Certificate: downloadCertificate,
PrivateKey: downloadPrivateKey,
ClientFingerprint: downloadClientFingerprint,
ECH: downloadEchConfig,
Reality: downloadRealityCfg,
NextProtos: downloadALPN,
}
if isH2 {
tlsOpts.NextProtos = []string{"h2"}
}
if downloadServerName != "" {
tlsOpts.Host = downloadServerName
}
return vmess.StreamTLSConn(ctx, conn, &tlsOpts)
}
return conn, nil
},
)
}
}
v.xhttpClient, err = xhttp.NewClient(cfg, makeTransport, makeDownloadTransport, v.realityConfig != nil)
if err != nil {
return nil, err
}
}
return v, nil
@@ -290,15 +290,18 @@ func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.M
return
}
func (v *Vmess) dialContext(ctx context.Context) (c net.Conn, err error) {
switch v.option.Network {
case "grpc": // gun transport
return v.gunTransport.Dial()
default:
}
return v.dialer.DialContext(ctx, "tcp", v.addr)
}
// DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
var c net.Conn
// gun transport
if v.gunTransport != nil {
c, err = v.gunTransport.Dial()
} else {
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
}
c, err := v.dialContext(ctx)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
@@ -307,6 +310,9 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn
}(c)
c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
return NewConn(c, v), err
}
@@ -315,13 +321,8 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
var c net.Conn
// gun transport
if v.gunTransport != nil {
c, err = v.gunTransport.Dial()
} else {
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
}
c, err := v.dialContext(ctx)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
@@ -331,9 +332,13 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (
c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err)
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
return v.ListenPacketOnStreamConn(ctx, c, metadata)
if pc, ok := c.(net.PacketConn); ok {
return newPacketConn(N.NewThreadSafePacketConn(pc), v), nil
}
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
// ProxyInfo implements C.ProxyAdapter
@@ -351,18 +356,6 @@ func (v *Vmess) Close() error {
return nil
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if pc, ok := c.(net.PacketConn); ok {
return newPacketConn(N.NewThreadSafePacketConn(pc), v), nil
}
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
// SupportUOT implements C.ProxyAdapter
func (v *Vmess) SupportUOT() bool {
return true
@@ -618,6 +618,75 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
anytls["udp"] = true
proxies = append(proxies, anytls)
case "mierus":
urlMieru, err := url.Parse(line)
if err != nil {
continue
}
query := urlMieru.Query()
server := urlMieru.Hostname()
if server == "" {
continue
}
username := urlMieru.User.Username()
password, _ := urlMieru.User.Password()
baseName := urlMieru.Fragment
if baseName == "" {
baseName = query.Get("profile")
}
if baseName == "" {
baseName = server
}
multiplexing := query.Get("multiplexing")
handshakeMode := query.Get("handshake-mode")
trafficPattern := query.Get("traffic-pattern")
portList := query["port"]
protocolList := query["protocol"]
if len(portList) == 0 || len(portList) != len(protocolList) {
continue
}
for i, port := range portList {
protocol := protocolList[i]
name := uniqueName(names, fmt.Sprintf("%s:%s/%s", baseName, port, protocol))
mieru := make(map[string]any, 15)
mieru["name"] = name
mieru["type"] = "mieru"
mieru["server"] = server
mieru["transport"] = protocol
mieru["udp"] = true
mieru["username"] = username
mieru["password"] = password
if strings.Contains(port, "-") {
mieru["port-range"] = port
} else {
portNum, err := strconv.Atoi(port)
if err != nil {
continue
}
mieru["port"] = portNum
}
if multiplexing != "" {
mieru["multiplexing"] = multiplexing
}
if handshakeMode != "" {
mieru["handshake-mode"] = handshakeMode
}
if trafficPattern != "" {
mieru["traffic-pattern"] = trafficPattern
}
proxies = append(proxies, mieru)
}
}
}
@@ -33,3 +33,99 @@ func TestConvertsV2Ray_normal(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, expected, proxies)
}
func TestConvertsV2RayMieru(t *testing.T) {
mierusTest := "mierus://user:pass@1.2.3.4?handshake-mode=HANDSHAKE_NO_WAIT&mtu=1400&multiplexing=MULTIPLEXING_HIGH&port=6666&port=9998-9999&port=6489&port=4896&profile=default&protocol=TCP&protocol=TCP&protocol=UDP&protocol=UDP&traffic-pattern=CCoQAQ"
expected := []map[string]any{
{
"name": "default:6666/TCP",
"type": "mieru",
"server": "1.2.3.4",
"port": 6666,
"transport": "TCP",
"udp": true,
"username": "user",
"password": "pass",
"multiplexing": "MULTIPLEXING_HIGH",
"handshake-mode": "HANDSHAKE_NO_WAIT",
"traffic-pattern": "CCoQAQ",
},
{
"name": "default:9998-9999/TCP",
"type": "mieru",
"server": "1.2.3.4",
"port-range": "9998-9999",
"transport": "TCP",
"udp": true,
"username": "user",
"password": "pass",
"multiplexing": "MULTIPLEXING_HIGH",
"handshake-mode": "HANDSHAKE_NO_WAIT",
"traffic-pattern": "CCoQAQ",
},
{
"name": "default:6489/UDP",
"type": "mieru",
"server": "1.2.3.4",
"port": 6489,
"transport": "UDP",
"udp": true,
"username": "user",
"password": "pass",
"multiplexing": "MULTIPLEXING_HIGH",
"handshake-mode": "HANDSHAKE_NO_WAIT",
"traffic-pattern": "CCoQAQ",
},
{
"name": "default:4896/UDP",
"type": "mieru",
"server": "1.2.3.4",
"port": 4896,
"transport": "UDP",
"udp": true,
"username": "user",
"password": "pass",
"multiplexing": "MULTIPLEXING_HIGH",
"handshake-mode": "HANDSHAKE_NO_WAIT",
"traffic-pattern": "CCoQAQ",
},
}
proxies, err := ConvertsV2Ray([]byte(mierusTest))
assert.Nil(t, err)
assert.Equal(t, expected, proxies)
}
func TestConvertsV2RayMieruMinimal(t *testing.T) {
mierusTest := "mierus://user:pass@example.com?port=443&protocol=TCP&profile=simple"
expected := []map[string]any{
{
"name": "simple:443/TCP",
"type": "mieru",
"server": "example.com",
"port": 443,
"transport": "TCP",
"udp": true,
"username": "user",
"password": "pass",
},
}
proxies, err := ConvertsV2Ray([]byte(mierusTest))
assert.Nil(t, err)
assert.Equal(t, expected, proxies)
}
func TestConvertsV2RayMieruFragment(t *testing.T) {
mierusTest := "mierus://user:pass@example.com?port=443&protocol=TCP&profile=default#myproxy"
proxies, err := ConvertsV2Ray([]byte(mierusTest))
assert.Nil(t, err)
assert.Len(t, proxies, 1)
assert.Equal(t, "myproxy:443/TCP", proxies[0]["name"])
}
@@ -789,6 +789,57 @@ proxies: # socks5
# v2ray-http-upgrade: false
# v2ray-http-upgrade-fast-open: false
- name: "vless-xhttp"
type: vless
server: server
port: 443
uuid: uuid
udp: true
tls: true
network: xhttp
alpn:
- h2
# ech-opts: ...
# reality-opts: ...
# skip-cert-verify: false
# fingerprint: ...
# certificate: ...
# private-key: ...
servername: xxx.com
client-fingerprint: chrome
encryption: ""
xhttp-opts:
path: "/"
host: xxx.com
# mode: "stream-one" # Available: "stream-one", "stream-up" or "packet-up"
# headers:
# X-Forwarded-For: ""
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# download-settings:
# ## xhttp part
# path: "/"
# host: xxx.com
# headers:
# X-Forwarded-For: ""
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# ## proxy part
# server: server
# port: 443
# tls: true
# alpn:
# - h2
# ech-opts: ...
# reality-opts: ...
# skip-cert-verify: false
# fingerprint: ...
# certificate: ...
# private-key: ...
# servername: xxx.com
# client-fingerprint: chrome
# Trojan
- name: "trojan"
type: trojan
@@ -1092,25 +1143,17 @@ proxies: # socks5
aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;且 enable-pure-downlink=false 时不可用)
padding-min: 2 # 最小填充率(0-100
padding-max: 7 # 最大填充率(0-100,必须 >= padding-min
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy、up_ascii_down_entropy、up_entropy_down_ascii
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合;只对 entropy 方向生效
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),非空时覆盖 custom-table
# 推荐:使用 httpmask 对象统一管理 HTTPMask 相关字段:
httpmask:
disable: false # true 禁用所有 HTTP 伪装/隧道
mode: legacy # 可选:legacy(默认)、streamsplit-stream)、poll、auto(先 stream 再 poll)、wsWebSocket 隧道)
# tls: true # 可选:仅在 mode 为 stream/poll/auto/ws 时生效;true 强制 https/wssfalse 强制 http/ws(不会根据端口自动推断)
# tls: true # 可选:按需开启 HTTPS/WSS
# host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 mode 为 stream/poll/auto/ws 时生效
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload、/aabbcc/ws
# multiplex: off # 可选:off(默认)、auto(复用底层 HTTP 连接,减少建链 RTT)、on(Sudoku mux 单隧道多目标;仅在 mode=stream/poll/auto 生效;ws 强制 off
#
# 向后兼容旧写法:
# http-mask: true # 是否启用 http 掩码
# http-mask-mode: legacy # 可选:legacy(默认)、streamsplit-stream)、poll、auto(先 stream 再 poll)、wsWebSocket 隧道)
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto/ws 时生效;true 强制 https/wssfalse 强制 http/ws
# http-mask-host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 http-mask-mode 为 stream/poll/auto/ws 时生效
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致)
# http-mask-multiplex: off # 可选:off(默认)、auto(复用底层 HTTP 连接)、onSudoku mux 单隧道多目标;ws 强制 off)
# multiplex: "off" # 可选字符串:off(默认)、auto(复用底层 HTTP 连接,减少建链 RTT)、on(Sudoku mux 单隧道多目标;仅在 mode=stream/poll/auto 生效;ws 强制 off
enable-pure-downlink: false # 可选:false=带宽优化下行(更快,要求 aead-method != none);true=纯 Sudoku 下行
# anytls
@@ -1671,9 +1714,9 @@ listeners:
aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;且 enable-pure-downlink=false 时不可用)
padding-min: 1 # 最小填充率(0-100
padding-max: 15 # 最大填充率(0-100,必须 >= padding-min
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy、up_ascii_down_entropy、up_entropy_down_ascii
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合;只对 entropy 方向生效
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于多表轮换;非空时覆盖 custom-table
handshake-timeout: 5 # 可选(秒)
enable-pure-downlink: false # 可选:false=带宽优化下行(更快,要求 aead-method != none);true=纯 Sudoku 下行
# 推荐:使用 httpmask 对象统一管理 HTTPMask 相关字段:
@@ -1682,13 +1725,7 @@ listeners:
mode: legacy # 可选:legacy(默认)、streamsplit-stream)、poll、auto(先 stream 再 poll)、wsWebSocket 隧道)
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload、/aabbcc/ws
#
# 可选:当启用 HTTPMask 且识别到“像 HTTP 但不符合 tunnel/auth”的请求时,将原始字节透传给 fallback(常用于与其他服务共端口):
# fallback: "127.0.0.1:80"
#
# 向后兼容旧写法:
# disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false
# http-mask-mode: legacy # 可选:legacy(默认)、streamsplit-stream)、poll、auto(先 stream 再 poll)、wsWebSocket 隧道)
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致)
# fallback: "127.0.0.1:80" # 可选:用于可连接请求的回落转发,可与其他服务共端口
@@ -6,7 +6,7 @@ require (
github.com/bahlo/generic-list-go v0.2.0
github.com/coreos/go-iptables v0.8.0
github.com/dlclark/regexp2 v1.11.5
github.com/enfein/mieru/v3 v3.29.0
github.com/enfein/mieru/v3 v3.30.0
github.com/gobwas/ws v1.4.0
github.com/gofrs/uuid/v5 v5.4.0
github.com/golang/snappy v1.0.0
@@ -20,8 +20,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/enfein/mieru/v3 v3.29.0 h1:i5Hwl5spEWg4ydvYW86zWSYVJ2uGTf5sLYQmFXHdulQ=
github.com/enfein/mieru/v3 v3.29.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/enfein/mieru/v3 v3.30.0 h1:g7v0TuK7y0ZMn6TOdjOs8WEUQk8bvs6WYPBJ16SKdBU=
github.com/enfein/mieru/v3 v3.30.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -17,7 +17,7 @@ type SudokuOption struct {
AEADMethod string `inbound:"aead-method,omitempty"`
PaddingMin *int `inbound:"padding-min,omitempty"`
PaddingMax *int `inbound:"padding-max,omitempty"`
TableType string `inbound:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
TableType string `inbound:"table-type,omitempty"` // "prefer_ascii", "prefer_entropy", or directional "up_ascii_down_entropy"/"up_entropy_down_ascii"
HandshakeTimeoutSecond *int `inbound:"handshake-timeout,omitempty"`
EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"`
CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
@@ -391,3 +391,56 @@ func TestInboundVless_Reality_XHTTP(t *testing.T) {
}
testInboundVless(t, inboundOptions, outboundOptions)
}
func TestInboundVless_XHTTP_DownloadSettings(t *testing.T) {
for _, mode := range []string{"stream-up", "packet-up"} {
t.Run(mode, func(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
XHTTPConfig: inbound.XHTTPConfig{
Path: "/vless-xhttp",
Host: "example.com",
Mode: mode,
},
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
ServerName: "example.org",
ClientFingerprint: "chrome",
Network: "xhttp",
XHTTPOpts: outbound.XHTTPOptions{
Path: "/vless-xhttp",
Host: "example.com",
Mode: mode,
DownloadSettings: &outbound.XHTTPDownloadSettings{},
},
}
testInboundVlessTLS(t, inboundOptions, outboundOptions, false)
})
}
}
func TestInboundVless_XHTTP_StreamUp(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
XHTTPConfig: inbound.XHTTPConfig{
Path: "/vless-xhttp",
Host: "example.com",
Mode: "stream-up",
},
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "xhttp",
XHTTPOpts: outbound.XHTTPOptions{
Path: "/vless-xhttp",
Host: "example.com",
Mode: "stream-up",
},
}
testInboundVlessTLS(t, inboundOptions, outboundOptions, false)
}
@@ -155,9 +155,12 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis
}
forwarderBindInterface := false
if options.FileDescriptor > 0 {
if tunnelName, err := getTunnelName(int32(options.FileDescriptor)); err != nil {
if tunnelName, err := getTunnelName(int32(options.FileDescriptor)); err == nil {
tunName = tunnelName // sing-tun must have the truth tun interface name even it from a fd
forwarderBindInterface = true
log.Debugln("[TUN] use tun name %s for fd %d", tunnelName, options.FileDescriptor)
} else {
log.Warnln("[TUN] get tun name failed for fd %d, fallback to use tun interface name %s", options.FileDescriptor, tunName)
}
}
routeAddress := options.RouteAddress
@@ -148,7 +148,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
}
if config.XHTTPConfig.Mode != "" {
switch config.XHTTPConfig.Mode {
case "auto":
case "auto", "stream-up", "stream-one", "packet-up":
default:
return nil, errors.New("unsupported xhttp mode")
}
@@ -195,14 +195,11 @@ func ResolvePadding(min, max *int, defMin, defMax int) (int, int) {
}
func NormalizeTableType(tableType string) (string, error) {
switch t := strings.ToLower(strings.TrimSpace(tableType)); t {
case "", "prefer_ascii":
return "prefer_ascii", nil
case "prefer_entropy":
return "prefer_entropy", nil
default:
return "", fmt.Errorf("table-type must be prefer_ascii or prefer_entropy")
normalized, err := sudoku.NormalizeASCIIMode(tableType)
if err != nil {
return "", fmt.Errorf("table-type must be prefer_ascii, prefer_entropy, up_ascii_down_entropy, or up_entropy_down_ascii")
}
return normalized, nil
}
func (c *ProtocolConfig) tableCandidates() []*sudoku.Table {
@@ -0,0 +1,119 @@
package sudoku
import (
"bytes"
"io"
"net"
"testing"
"github.com/metacubex/mihomo/transport/sudoku/crypto"
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
func TestDirectionalCustomTableRotationHintRoundTrip(t *testing.T) {
key := "directional-rotate-key"
target := "8.8.8.8:53"
serverTables, err := NewServerTablesWithCustomPatterns(ClientAEADSeed(key), "up_ascii_down_entropy", "", []string{"xpxvvpvv", "vxpvxvvp"})
if err != nil {
t.Fatalf("server tables: %v", err)
}
if len(serverTables) != 2 {
t.Fatalf("expected 2 server tables, got %d", len(serverTables))
}
clientTable, err := sudokuobfs.NewTableWithCustom(ClientAEADSeed(key), "up_ascii_down_entropy", "vxpvxvvp")
if err != nil {
t.Fatalf("client table: %v", err)
}
serverCfg := DefaultConfig()
serverCfg.Key = key
serverCfg.AEADMethod = "chacha20-poly1305"
serverCfg.Tables = serverTables
serverCfg.PaddingMin = 0
serverCfg.PaddingMax = 0
serverCfg.EnablePureDownlink = true
serverCfg.HandshakeTimeoutSeconds = 5
serverCfg.DisableHTTPMask = true
clientCfg := DefaultConfig()
*clientCfg = *serverCfg
clientCfg.ServerAddress = "example.com:443"
serverConn, clientConn := net.Pipe()
defer clientConn.Close()
serverErr := make(chan error, 1)
go func() {
defer close(serverErr)
defer serverConn.Close()
c, meta, err := ServerHandshake(serverConn, serverCfg)
if err != nil {
serverErr <- err
return
}
defer c.Close()
session, err := ReadServerSession(c, meta)
if err != nil {
serverErr <- err
return
}
if session.Type != SessionTypeTCP || session.Target != target {
serverErr <- io.ErrUnexpectedEOF
return
}
payload := make([]byte, len("client-payload"))
if _, err := io.ReadFull(session.Conn, payload); err != nil {
serverErr <- err
return
}
if !bytes.Equal(payload, []byte("client-payload")) {
serverErr <- io.ErrUnexpectedEOF
return
}
if _, err := session.Conn.Write([]byte("server-reply")); err != nil {
serverErr <- err
}
}()
seed := ClientAEADSeed(clientCfg.Key)
obfsConn := buildClientObfsConn(clientConn, clientCfg, clientTable)
pskC2S, pskS2C := derivePSKDirectionalBases(seed)
cConn, err := crypto.NewRecordConn(obfsConn, clientCfg.AEADMethod, pskC2S, pskS2C)
if err != nil {
t.Fatalf("setup crypto: %v", err)
}
defer cConn.Close()
if _, err := kipHandshakeClient(cConn, seed, kipUserHashFromKey(clientCfg.Key), KIPFeatAll, clientTable.Hint(), true); err != nil {
t.Fatalf("client handshake: %v", err)
}
addrBuf, err := EncodeAddress(target)
if err != nil {
t.Fatalf("encode target: %v", err)
}
if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil {
t.Fatalf("write target: %v", err)
}
if _, err := cConn.Write([]byte("client-payload")); err != nil {
t.Fatalf("write payload: %v", err)
}
reply := make([]byte, len("server-reply"))
if _, err := io.ReadFull(cConn, reply); err != nil {
t.Fatalf("read reply: %v", err)
}
if !bytes.Equal(reply, []byte("server-reply")) {
t.Fatalf("unexpected reply: %q", reply)
}
if err := <-serverErr; err != nil {
t.Fatalf("server: %v", err)
}
}
@@ -86,23 +86,23 @@ 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 {
downlinkReader := newClientDownlinkReader(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink)
if downlinkReader == nil {
return base
}
packed := sudokuobfs.NewPackedConn(raw, table, cfg.PaddingMin, cfg.PaddingMax)
return newDirectionalConn(raw, packed, base)
return newDirectionalConn(raw, downlinkReader, 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 {
downlinkWriter, closers := newServerDownlinkWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink)
if downlinkWriter == nil {
return uplink
}
packed := sudokuobfs.NewPackedConn(raw, table, cfg.PaddingMin, cfg.PaddingMax)
return newDirectionalConn(raw, uplink, packed, packed.Flush)
return newDirectionalConn(raw, uplink, downlinkWriter, closers...)
}
func NewEarlyClientState(cfg EarlyCodecConfig, table *sudokuobfs.Table, userHash [kipHelloUserHashSize]byte, feats uint32) (*EarlyClientState, error) {
func NewEarlyClientState(cfg EarlyCodecConfig, table *sudokuobfs.Table, tableHint uint32, hasTableHint bool, userHash [kipHelloUserHashSize]byte, feats uint32) (*EarlyClientState, error) {
if table == nil {
return nil, fmt.Errorf("nil table")
}
@@ -120,13 +120,7 @@ func NewEarlyClientState(cfg EarlyCodecConfig, table *sudokuobfs.Table, userHash
var clientPub [kipHelloPubSize]byte
copy(clientPub[:], ephemeral.PublicKey().Bytes())
hello := &KIPClientHello{
Timestamp: time.Now(),
UserHash: userHash,
Nonce: nonce,
ClientPub: clientPub,
Features: feats,
}
hello := newKIPClientHello(userHash, nonce, clientPub, feats, tableHint, hasTableHint)
mem := newEarlyMemoryConn(nil)
obfsConn := buildEarlyClientObfsConn(mem, cfg, table)
@@ -208,8 +202,8 @@ 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)
func NewHTTPMaskClientEarlyHandshake(cfg EarlyCodecConfig, table *sudokuobfs.Table, tableHint uint32, hasTableHint bool, userHash [kipHelloUserHashSize]byte, feats uint32) (*httpmaskobfs.ClientEarlyHandshake, error) {
state, err := NewEarlyClientState(cfg, table, tableHint, hasTableHint, userHash, feats)
if err != nil {
return nil, err
}
@@ -231,7 +225,7 @@ func ProcessEarlyClientPayload(cfg EarlyCodecConfig, tables []*sudokuobfs.Table,
var firstErr error
for _, table := range tables {
state, err := processEarlyClientPayloadForTable(cfg, table, payload, allowReplay)
state, err := processEarlyClientPayloadForTable(cfg, tables, table, payload, allowReplay)
if err == nil {
return state, nil
}
@@ -245,7 +239,7 @@ func ProcessEarlyClientPayload(cfg EarlyCodecConfig, tables []*sudokuobfs.Table,
return nil, firstErr
}
func processEarlyClientPayloadForTable(cfg EarlyCodecConfig, table *sudokuobfs.Table, payload []byte, allowReplay ReplayAllowFunc) (*EarlyServerState, error) {
func processEarlyClientPayloadForTable(cfg EarlyCodecConfig, tables []*sudokuobfs.Table, table *sudokuobfs.Table, payload []byte, allowReplay ReplayAllowFunc) (*EarlyServerState, error) {
mem := newEarlyMemoryConn(payload)
obfsConn := buildEarlyServerObfsConn(mem, cfg, table)
pskC2S, pskS2C := derivePSKDirectionalBases(cfg.PSK)
@@ -273,6 +267,10 @@ func processEarlyClientPayloadForTable(cfg EarlyCodecConfig, table *sudokuobfs.T
if allowReplay != nil && !allowReplay(userHash, ch.Nonce, time.Now()) {
return nil, fmt.Errorf("replay detected")
}
resolvedTable, err := ResolveClientHelloTable(table, tables, ch)
if err != nil {
return nil, fmt.Errorf("resolve table hint failed: %w", err)
}
curve := ecdh.X25519()
serverEphemeral, err := curve.GenerateKey(rand.Reader)
@@ -297,7 +295,7 @@ func processEarlyClientPayloadForTable(cfg EarlyCodecConfig, table *sudokuobfs.T
}
respMem := newEarlyMemoryConn(nil)
respObfs := buildEarlyServerObfsConn(respMem, cfg, table)
respObfs := buildEarlyServerObfsConn(respMem, cfg, resolvedTable)
respConn, err := crypto.NewRecordConn(respObfs, cfg.AEAD, pskS2C, pskC2S)
if err != nil {
return nil, fmt.Errorf("server early crypto setup failed: %w", err)
@@ -310,7 +308,7 @@ func processEarlyClientPayloadForTable(cfg EarlyCodecConfig, table *sudokuobfs.T
ResponsePayload: respMem.Written(),
UserHash: userHash,
cfg: cfg,
table: table,
table: resolvedTable,
sessionC2S: sessionC2S,
sessionS2C: sessionS2C,
}, nil
@@ -92,3 +92,238 @@ func TestCustomTablesRotation_ProbedByServer(t *testing.T) {
}
}
}
func TestDirectionalTrafficRoundTrip(t *testing.T) {
tests := []struct {
name string
mode string
pure bool
}{
{name: "UpASCII_DownEntropy_Pure", mode: "up_ascii_down_entropy", pure: true},
{name: "UpASCII_DownEntropy_Packed", mode: "up_ascii_down_entropy", pure: false},
{name: "UpEntropy_DownASCII_Pure", mode: "up_entropy_down_ascii", pure: true},
{name: "UpEntropy_DownASCII_Packed", mode: "up_entropy_down_ascii", pure: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := "directional-test-key-" + tt.name
target := "8.8.8.8:53"
table, err := sudokuobfs.NewTableWithCustom(ClientAEADSeed(key), tt.mode, "xpxvvpvv")
if err != nil {
t.Fatalf("table: %v", err)
}
serverCfg := DefaultConfig()
serverCfg.Key = key
serverCfg.AEADMethod = "chacha20-poly1305"
serverCfg.Table = table
serverCfg.PaddingMin = 0
serverCfg.PaddingMax = 0
serverCfg.EnablePureDownlink = tt.pure
serverCfg.HandshakeTimeoutSeconds = 5
serverCfg.DisableHTTPMask = true
clientCfg := DefaultConfig()
*clientCfg = *serverCfg
clientCfg.ServerAddress = "example.com:443"
serverConn, clientConn := net.Pipe()
defer clientConn.Close()
serverErr := make(chan error, 1)
go func() {
defer close(serverErr)
defer serverConn.Close()
c, meta, err := ServerHandshake(serverConn, serverCfg)
if err != nil {
serverErr <- err
return
}
defer c.Close()
session, err := ReadServerSession(c, meta)
if err != nil {
serverErr <- err
return
}
if session.Type != SessionTypeTCP {
serverErr <- io.ErrUnexpectedEOF
return
}
if session.Target != target {
serverErr <- io.ErrClosedPipe
return
}
want := []byte("client-payload")
got := make([]byte, len(want))
if _, err := io.ReadFull(session.Conn, got); err != nil {
serverErr <- err
return
}
if !bytes.Equal(got, want) {
serverErr <- io.ErrUnexpectedEOF
return
}
if _, err := session.Conn.Write([]byte("server-reply")); err != nil {
serverErr <- err
return
}
}()
cConn, err := ClientHandshake(clientConn, clientCfg)
if err != nil {
t.Fatalf("client handshake: %v", err)
}
defer cConn.Close()
addrBuf, err := EncodeAddress(target)
if err != nil {
t.Fatalf("encode target: %v", err)
}
if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil {
t.Fatalf("write target: %v", err)
}
if _, err := cConn.Write([]byte("client-payload")); err != nil {
t.Fatalf("write payload: %v", err)
}
reply := make([]byte, len("server-reply"))
if _, err := io.ReadFull(cConn, reply); err != nil {
t.Fatalf("read reply: %v", err)
}
if !bytes.Equal(reply, []byte("server-reply")) {
t.Fatalf("unexpected reply: %q", reply)
}
if err := <-serverErr; err != nil {
t.Fatalf("server: %v", err)
}
})
}
}
func TestDirectionalTrafficRoundTripTCP(t *testing.T) {
tests := []struct {
name string
mode string
pure bool
}{
{name: "UpASCII_DownEntropy_Pure_TCP", mode: "up_ascii_down_entropy", pure: true},
{name: "UpEntropy_DownASCII_Packed_TCP", mode: "up_entropy_down_ascii", pure: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := "directional-tcp-test-key-" + tt.name
target := "127.0.0.1:18080"
table, err := sudokuobfs.NewTableWithCustom(ClientAEADSeed(key), tt.mode, "xpxvvpvv")
if err != nil {
t.Fatalf("table: %v", err)
}
serverCfg := DefaultConfig()
serverCfg.Key = key
serverCfg.AEADMethod = "chacha20-poly1305"
serverCfg.Table = table
serverCfg.PaddingMin = 0
serverCfg.PaddingMax = 0
serverCfg.EnablePureDownlink = tt.pure
serverCfg.HandshakeTimeoutSeconds = 5
serverCfg.DisableHTTPMask = true
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
serverErr := make(chan error, 1)
go func() {
defer close(serverErr)
raw, err := ln.Accept()
if err != nil {
serverErr <- err
return
}
defer raw.Close()
c, meta, err := ServerHandshake(raw, serverCfg)
if err != nil {
serverErr <- err
return
}
defer c.Close()
session, err := ReadServerSession(c, meta)
if err != nil {
serverErr <- err
return
}
if session.Type != SessionTypeTCP || session.Target != target {
serverErr <- io.ErrUnexpectedEOF
return
}
want := []byte("client-payload")
got := make([]byte, len(want))
if _, err := io.ReadFull(session.Conn, got); err != nil {
serverErr <- err
return
}
if !bytes.Equal(got, want) {
serverErr <- io.ErrUnexpectedEOF
return
}
if _, err := session.Conn.Write([]byte("server-reply")); err != nil {
serverErr <- err
return
}
}()
clientCfg := DefaultConfig()
*clientCfg = *serverCfg
clientCfg.ServerAddress = ln.Addr().String()
raw, err := net.Dial("tcp", clientCfg.ServerAddress)
if err != nil {
t.Fatalf("dial: %v", err)
}
defer raw.Close()
cConn, err := ClientHandshake(raw, clientCfg)
if err != nil {
t.Fatalf("client handshake: %v", err)
}
defer cConn.Close()
addrBuf, err := EncodeAddress(target)
if err != nil {
t.Fatalf("encode target: %v", err)
}
if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil {
t.Fatalf("write target: %v", err)
}
if _, err := cConn.Write([]byte("client-payload")); err != nil {
t.Fatalf("write payload: %v", err)
}
reply := make([]byte, len("server-reply"))
if _, err := io.ReadFull(cConn, reply); err != nil {
t.Fatalf("read reply: %v", err)
}
if !bytes.Equal(reply, []byte("server-reply")) {
t.Fatalf("unexpected reply: %q", reply)
}
if err := <-serverErr; err != nil {
t.Fatalf("server: %v", err)
}
})
}
}
@@ -184,6 +184,14 @@ func (c *directionalConn) Write(p []byte) (int, error) {
return c.writer.Write(p)
}
func (c *directionalConn) ReplaceWriter(writer io.Writer, closers ...func() error) {
if c == nil {
return
}
c.writer = writer
c.closers = closers
}
func (c *directionalConn) Close() error {
var firstErr error
for _, fn := range c.closers {
@@ -227,22 +235,55 @@ func absInt64(v int64) int64 {
return v
}
func oppositeDirectionTable(table *sudoku.Table) *sudoku.Table {
if table == nil {
return nil
}
if other := table.OppositeDirection(); other != nil {
return other
}
return table
}
func newClientDownlinkReader(raw net.Conn, table *sudoku.Table, paddingMin, paddingMax int, pureDownlink bool) io.Reader {
downlinkTable := oppositeDirectionTable(table)
if pureDownlink {
if downlinkTable == table {
return nil
}
return sudoku.NewConn(raw, downlinkTable, paddingMin, paddingMax, false)
}
return sudoku.NewPackedConn(raw, downlinkTable, paddingMin, paddingMax)
}
func newServerDownlinkWriter(raw net.Conn, table *sudoku.Table, paddingMin, paddingMax int, pureDownlink bool) (io.Writer, []func() error) {
downlinkTable := oppositeDirectionTable(table)
if pureDownlink {
if downlinkTable == table {
return nil, nil
}
return sudoku.NewConn(raw, downlinkTable, paddingMin, paddingMax, false), nil
}
packed := sudoku.NewPackedConn(raw, downlinkTable, paddingMin, paddingMax)
return packed, []func() error{packed.Flush}
}
func buildClientObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table) net.Conn {
baseSudoku := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
if cfg.EnablePureDownlink {
downlinkReader := newClientDownlinkReader(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink)
if downlinkReader == nil {
return baseSudoku
}
packed := sudoku.NewPackedConn(raw, table, cfg.PaddingMin, cfg.PaddingMax)
return newDirectionalConn(raw, packed, baseSudoku)
return newDirectionalConn(raw, downlinkReader, baseSudoku)
}
func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table, record bool) (*sudoku.Conn, net.Conn) {
uplinkSudoku := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, record)
if cfg.EnablePureDownlink {
downlinkWriter, closers := newServerDownlinkWriter(raw, table, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink)
if downlinkWriter == nil {
return uplinkSudoku, uplinkSudoku
}
packed := sudoku.NewPackedConn(raw, table, cfg.PaddingMin, cfg.PaddingMax)
return uplinkSudoku, newDirectionalConn(raw, uplinkSudoku, packed, packed.Flush)
return uplinkSudoku, newDirectionalConn(raw, uplinkSudoku, downlinkWriter, closers...)
}
func isLegacyHTTPMaskMode(mode string) bool {
@@ -269,20 +310,20 @@ func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) {
}
}
table, err := pickClientTable(cfg)
choice, err := pickClientTable(cfg)
if err != nil {
return nil, err
}
seed := ClientAEADSeed(cfg.Key)
obfsConn := buildClientObfsConn(rawConn, cfg, table)
obfsConn := buildClientObfsConn(rawConn, cfg, choice.Table)
pskC2S, pskS2C := derivePSKDirectionalBases(seed)
rc, err := crypto.NewRecordConn(obfsConn, cfg.AEADMethod, pskC2S, pskS2C)
if err != nil {
return nil, fmt.Errorf("setup crypto failed: %w", err)
}
if _, err := kipHandshakeClient(rc, seed, kipUserHashFromKey(cfg.Key), KIPFeatAll); err != nil {
if _, err := kipHandshakeClient(rc, seed, kipUserHashFromKey(cfg.Key), KIPFeatAll, choice.Hint, choice.HasHint); err != nil {
_ = rc.Close()
return nil, err
}
@@ -393,6 +434,18 @@ func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, *Handshak
if !globalHandshakeReplay.allow(userHashHex, ch.Nonce, time.Now()) {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("replay"), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
resolvedTable, err := ResolveClientHelloTable(selectedTable, cfg.tableCandidates(), ch)
if err != nil {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("resolve table hint failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
if resolvedTable != selectedTable {
downlinkWriter, closers := newServerDownlinkWriter(baseConn, resolvedTable, cfg.PaddingMin, cfg.PaddingMax, cfg.EnablePureDownlink)
switchable, ok := obfsConn.(*directionalConn)
if !ok {
return nil, nil, &SuspiciousError{Err: fmt.Errorf("switch downlink writer failed"), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
}
switchable.ReplaceWriter(downlinkWriter, closers...)
}
curve := ecdh.X25519()
serverEphemeral, err := curve.GenerateKey(rand.Reader)
@@ -12,7 +12,7 @@ import (
const kipHandshakeSkew = 60 * time.Second
func kipHandshakeClient(rc *crypto.RecordConn, seed string, userHash [kipHelloUserHashSize]byte, feats uint32) (uint32, error) {
func kipHandshakeClient(rc *crypto.RecordConn, seed string, userHash [kipHelloUserHashSize]byte, feats uint32, tableHint uint32, hasTableHint bool) (uint32, error) {
if rc == nil {
return 0, fmt.Errorf("nil conn")
}
@@ -31,13 +31,7 @@ func kipHandshakeClient(rc *crypto.RecordConn, seed string, userHash [kipHelloUs
var clientPub [kipHelloPubSize]byte
copy(clientPub[:], ephemeral.PublicKey().Bytes())
ch := &KIPClientHello{
Timestamp: time.Now(),
UserHash: userHash,
Nonce: nonce,
ClientPub: clientPub,
Features: feats,
}
ch := newKIPClientHello(userHash, nonce, clientPub, feats, tableHint, hasTableHint)
if err := WriteKIPMessage(rc, KIPTypeClientHello, ch.EncodePayload()); err != nil {
return 0, fmt.Errorf("write client hello failed: %w", err)
}
@@ -25,14 +25,16 @@ func newHTTPMaskEarlyCodecConfig(cfg *ProtocolConfig, psk string) EarlyCodecConf
}
func newClientHTTPMaskEarlyHandshake(cfg *ProtocolConfig) (*httpmask.ClientEarlyHandshake, error) {
table, err := pickClientTable(cfg)
choice, err := pickClientTable(cfg)
if err != nil {
return nil, err
}
return NewHTTPMaskClientEarlyHandshake(
newHTTPMaskEarlyCodecConfig(cfg, ClientAEADSeed(cfg.Key)),
table,
choice.Table,
choice.Hint,
choice.HasHint,
kipUserHashFromKey(cfg.Key),
KIPFeatAll,
)
@@ -451,6 +451,64 @@ func TestHTTPMaskTunnel_EarlyHandshake_TCPRoundTrip(t *testing.T) {
}
}
func TestHTTPMaskTunnel_EarlyHandshake_AutoPathRoot_TCPRoundTrip(t *testing.T) {
key := "tunnel-early-auto-pathroot"
target := "1.1.1.1:80"
serverCfg := newTunnelTestTable(t, key)
serverCfg.HTTPMaskMode = "auto"
serverCfg.HTTPMaskPathRoot = "httpmaskpath"
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"
@@ -39,16 +39,20 @@ func ClientAEADSeed(key string) string {
}
// Client-side key material can be:
// - public key: 32 bytes hex compressed point
// - split private key: 64 bytes hex (r||k)
// - master private scalar: 32 bytes hex (x)
// - PSK string: non-hex
//
// We intentionally do NOT treat a 32-byte hex as a public key here; the client is expected
// to carry private material. Server-side should use ServerAEADSeed for public keys.
switch len(b) {
case 64:
case 32:
default:
// 32-byte hex is ambiguous: it can be either a compressed public key or a
// master private scalar. Official Sudoku runtime accepts public keys directly,
// so when the bytes already decode as a point, preserve that point verbatim.
if len(b) == 32 {
if p, err := new(edwards25519.Point).SetBytes(b); err == nil {
return hex.EncodeToString(p.Bytes())
}
}
if len(b) != 64 && len(b) != 32 {
return key
}
if recovered, err := crypto.RecoverPublicKey(key); err == nil {
@@ -15,23 +15,36 @@ func TestClientAEADSeed_IsStableForPrivAndPub(t *testing.T) {
require.NoError(t, err)
require.Equal(t, pub, ClientAEADSeed(priv))
require.Equal(t, pub, ClientAEADSeed(pub))
require.Equal(t, pub, ServerAEADSeed(pub))
require.Equal(t, pub, ServerAEADSeed(priv))
}
}
func TestClientAEADSeed_Supports32ByteMasterScalar(t *testing.T) {
var seed [64]byte
_, err := rand.Read(seed[:])
require.NoError(t, err)
for i := 0; i < 256; i++ {
var seed [64]byte
_, err := rand.Read(seed[:])
require.NoError(t, err)
s, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
require.NoError(t, err)
s, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
require.NoError(t, err)
keyHex := hex.EncodeToString(s.Bytes())
require.Len(t, keyHex, 64)
require.NotEqual(t, keyHex, ClientAEADSeed(keyHex))
require.Equal(t, ClientAEADSeed(keyHex), ServerAEADSeed(ClientAEADSeed(keyHex)))
keyHex := hex.EncodeToString(s.Bytes())
require.Len(t, keyHex, 64)
// 32-byte hex is ambiguous: it can be either a master scalar or an
// already-compressed public key. Public-key encoding wins when both parse.
if _, err := new(edwards25519.Point).SetBytes(s.Bytes()); err == nil {
continue
}
require.NotEqual(t, keyHex, ClientAEADSeed(keyHex))
require.Equal(t, ClientAEADSeed(keyHex), ServerAEADSeed(ClientAEADSeed(keyHex)))
return
}
t.Fatal("failed to generate an unambiguous 32-byte master scalar")
}
func TestServerAEADSeed_LeavesPublicKeyAsIs(t *testing.T) {
@@ -10,6 +10,8 @@ import (
"io"
"strings"
"time"
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
const (
@@ -42,6 +44,8 @@ const (
kipMaxPayload = 64 * 1024
)
const kipClientHelloTableHintSize = 4
var errKIP = errors.New("kip protocol error")
type KIPMessage struct {
@@ -98,11 +102,13 @@ func ReadKIPMessage(r io.Reader) (*KIPMessage, error) {
}
type KIPClientHello struct {
Timestamp time.Time
UserHash [kipHelloUserHashSize]byte
Nonce [kipHelloNonceSize]byte
ClientPub [kipHelloPubSize]byte
Features uint32
Timestamp time.Time
UserHash [kipHelloUserHashSize]byte
Nonce [kipHelloNonceSize]byte
ClientPub [kipHelloPubSize]byte
Features uint32
TableHint uint32
HasTableHint bool
}
type KIPServerHello struct {
@@ -111,6 +117,18 @@ type KIPServerHello struct {
SelectedFeats uint32
}
func newKIPClientHello(userHash [kipHelloUserHashSize]byte, nonce [kipHelloNonceSize]byte, clientPub [kipHelloPubSize]byte, feats uint32, tableHint uint32, hasTableHint bool) *KIPClientHello {
return &KIPClientHello{
Timestamp: time.Now(),
UserHash: userHash,
Nonce: nonce,
ClientPub: clientPub,
Features: feats,
TableHint: tableHint,
HasTableHint: hasTableHint,
}
}
func kipUserHashFromKey(psk string) [kipHelloUserHashSize]byte {
var out [kipHelloUserHashSize]byte
psk = strings.TrimSpace(psk)
@@ -147,6 +165,11 @@ func (m *KIPClientHello) EncodePayload() []byte {
var f [4]byte
binary.BigEndian.PutUint32(f[:], m.Features)
b.Write(f[:])
if m.HasTableHint {
var hint [kipClientHelloTableHintSize]byte
binary.BigEndian.PutUint32(hint[:], m.TableHint)
b.Write(hint[:])
}
return b.Bytes()
}
@@ -166,9 +189,45 @@ func DecodeKIPClientHelloPayload(payload []byte) (*KIPClientHello, error) {
copy(h.ClientPub[:], payload[off:off+kipHelloPubSize])
off += kipHelloPubSize
h.Features = binary.BigEndian.Uint32(payload[off : off+4])
off += 4
if len(payload) >= off+kipClientHelloTableHintSize {
h.TableHint = binary.BigEndian.Uint32(payload[off : off+kipClientHelloTableHintSize])
h.HasTableHint = true
}
return &h, nil
}
func ResolveClientHelloTable(selected *sudokuobfs.Table, candidates []*sudokuobfs.Table, hello *KIPClientHello) (*sudokuobfs.Table, error) {
if selected == nil {
return nil, fmt.Errorf("nil selected table")
}
if hello == nil || !hello.HasTableHint {
return selected, nil
}
if selected.Hint() == hello.TableHint {
return selected, nil
}
if len(candidates) == 0 {
return nil, fmt.Errorf("no table candidates")
}
var hinted *sudokuobfs.Table
for _, candidate := range candidates {
if candidate == nil || candidate.Hint() != hello.TableHint {
continue
}
hinted = candidate
break
}
if hinted == nil {
return nil, fmt.Errorf("unknown table hint: %d", hello.TableHint)
}
if hinted != selected && (!hinted.IsASCII || !selected.IsASCII) {
return nil, fmt.Errorf("table hint %d mismatches probed uplink table", hello.TableHint)
}
return hinted, nil
}
func (m *KIPServerHello) EncodePayload() []byte {
var b bytes.Buffer
b.Write(m.Nonce[:])
@@ -0,0 +1,64 @@
package sudoku
import (
"testing"
sudokuobfs "github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
func TestKIPClientHelloTableHintRoundTrip(t *testing.T) {
hello := &KIPClientHello{
Features: KIPFeatAll,
TableHint: 0x12345678,
HasTableHint: true,
}
decoded, err := DecodeKIPClientHelloPayload(hello.EncodePayload())
if err != nil {
t.Fatalf("decode client hello: %v", err)
}
if !decoded.HasTableHint {
t.Fatalf("expected decoded hello to carry table hint")
}
if decoded.TableHint != hello.TableHint {
t.Fatalf("decoded table hint = %08x, want %08x", decoded.TableHint, hello.TableHint)
}
}
func TestResolveClientHelloTableAllowsDirectionalASCIIRotation(t *testing.T) {
tables, err := NewClientTablesWithCustomPatterns("seed", "up_ascii_down_entropy", "", []string{"xpxvvpvv", "vxpvxvvp"})
if err != nil {
t.Fatalf("build tables: %v", err)
}
if len(tables) != 2 {
t.Fatalf("expected 2 tables, got %d", len(tables))
}
selected, err := ResolveClientHelloTable(tables[0], tables, &KIPClientHello{
TableHint: tables[1].Hint(),
HasTableHint: true,
})
if err != nil {
t.Fatalf("resolve client hello table: %v", err)
}
if selected != tables[1] {
t.Fatalf("resolved table mismatch")
}
}
func TestResolveClientHelloTableRejectsEntropyMismatch(t *testing.T) {
a, err := sudokuobfs.NewTableWithCustom("seed", "prefer_entropy", "xpxvvpvv")
if err != nil {
t.Fatalf("table a: %v", err)
}
b, err := sudokuobfs.NewTableWithCustom("seed", "prefer_entropy", "vxpvxvvp")
if err != nil {
t.Fatalf("table b: %v", err)
}
if _, err := ResolveClientHelloTable(a, []*sudokuobfs.Table{a, b}, &KIPClientHello{
TableHint: b.Hint(),
HasTableHint: true,
}); err == nil {
t.Fatalf("expected entropy-table mismatch to fail")
}
}
@@ -0,0 +1,93 @@
package sudoku
import (
"fmt"
"strings"
)
const (
asciiModeTokenASCII = "ascii"
asciiModeTokenEntropy = "entropy"
)
// ASCIIMode describes the preferred wire layout for each traffic direction.
// Uplink is client->server, Downlink is server->client.
type ASCIIMode struct {
Uplink string
Downlink string
}
// ParseASCIIMode accepts legacy symmetric values ("ascii"/"entropy"/"prefer_*")
// and directional values like "up_ascii_down_entropy".
func ParseASCIIMode(mode string) (ASCIIMode, error) {
raw := strings.ToLower(strings.TrimSpace(mode))
switch raw {
case "", "entropy", "prefer_entropy":
return ASCIIMode{Uplink: asciiModeTokenEntropy, Downlink: asciiModeTokenEntropy}, nil
case "ascii", "prefer_ascii":
return ASCIIMode{Uplink: asciiModeTokenASCII, Downlink: asciiModeTokenASCII}, nil
}
if !strings.HasPrefix(raw, "up_") {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
parts := strings.SplitN(strings.TrimPrefix(raw, "up_"), "_down_", 2)
if len(parts) != 2 {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
up, ok := normalizeASCIIModeToken(parts[0])
if !ok {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
down, ok := normalizeASCIIModeToken(parts[1])
if !ok {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
return ASCIIMode{Uplink: up, Downlink: down}, nil
}
// NormalizeASCIIMode returns the canonical config string for a supported mode.
func NormalizeASCIIMode(mode string) (string, error) {
parsed, err := ParseASCIIMode(mode)
if err != nil {
return "", err
}
return parsed.Canonical(), nil
}
func (m ASCIIMode) Canonical() string {
if m.Uplink == asciiModeTokenASCII && m.Downlink == asciiModeTokenASCII {
return "prefer_ascii"
}
if m.Uplink == asciiModeTokenEntropy && m.Downlink == asciiModeTokenEntropy {
return "prefer_entropy"
}
return "up_" + m.Uplink + "_down_" + m.Downlink
}
func (m ASCIIMode) uplinkPreference() string {
return singleDirectionPreference(m.Uplink)
}
func (m ASCIIMode) downlinkPreference() string {
return singleDirectionPreference(m.Downlink)
}
func normalizeASCIIModeToken(token string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(token)) {
case "ascii", "prefer_ascii":
return asciiModeTokenASCII, true
case "entropy", "prefer_entropy", "":
return asciiModeTokenEntropy, true
default:
return "", false
}
}
func singleDirectionPreference(token string) string {
if token == asciiModeTokenASCII {
return "prefer_ascii"
}
return "prefer_entropy"
}
@@ -0,0 +1,56 @@
package sudoku
import "testing"
func TestNormalizeASCIIMode(t *testing.T) {
tests := []struct {
in string
want string
}{
{"", "prefer_entropy"},
{"entropy", "prefer_entropy"},
{"prefer_ascii", "prefer_ascii"},
{"up_ascii_down_entropy", "up_ascii_down_entropy"},
{"up_entropy_down_ascii", "up_entropy_down_ascii"},
{"up_prefer_ascii_down_prefer_entropy", "up_ascii_down_entropy"},
}
for _, tt := range tests {
got, err := NormalizeASCIIMode(tt.in)
if err != nil {
t.Fatalf("NormalizeASCIIMode(%q): %v", tt.in, err)
}
if got != tt.want {
t.Fatalf("NormalizeASCIIMode(%q) = %q, want %q", tt.in, got, tt.want)
}
}
if _, err := NormalizeASCIIMode("up_ascii_down_binary"); err == nil {
t.Fatalf("expected invalid directional mode to fail")
}
}
func TestNewTableWithCustomDirectionalOpposite(t *testing.T) {
table, err := NewTableWithCustom("seed", "up_ascii_down_entropy", "xpxvvpvv")
if err != nil {
t.Fatalf("NewTableWithCustom: %v", err)
}
if !table.IsASCII {
t.Fatalf("uplink table should be ascii")
}
opposite := table.OppositeDirection()
if opposite == nil || opposite == table {
t.Fatalf("expected distinct opposite table")
}
if opposite.IsASCII {
t.Fatalf("downlink table should be entropy/custom")
}
symmetric, err := NewTableWithCustom("seed", "prefer_ascii", "xpxvvpvv")
if err != nil {
t.Fatalf("NewTableWithCustom symmetric: %v", err)
}
if symmetric.OppositeDirection() != symmetric {
t.Fatalf("symmetric table should point to itself")
}
}
@@ -163,8 +163,7 @@ func (sc *Conn) Write(p []byte) (n int, err error) {
out = append(out, pads[sc.rng.Intn(padLen)])
}
_, err = sc.Conn.Write(out)
return len(p), err
return len(p), writeFull(sc.Conn, out)
}
func (sc *Conn) Read(p []byte) (n int, err error) {
@@ -27,7 +27,7 @@ func (l *byteLayout) isHint(b byte) bool {
return l.name == "ascii" && b == '\n'
}
// resolveLayout picks the byte layout based on ASCII preference and optional custom pattern.
// resolveLayout picks the byte layout for a single traffic direction.
// ASCII always wins if requested. Custom patterns are ignored when ASCII is preferred.
func resolveLayout(mode string, customPattern string) (*byteLayout, error) {
switch strings.ToLower(mode) {
@@ -5,6 +5,7 @@ import (
"encoding/binary"
"errors"
"math/rand"
"strings"
)
var (
@@ -17,6 +18,8 @@ type Table struct {
PaddingPool []byte
IsASCII bool // 标记当前模式
layout *byteLayout
opposite *Table
hint uint32
}
// NewTable initializes the obfuscation tables with built-in layouts.
@@ -29,10 +32,41 @@ func NewTable(key string, mode string) *Table {
return t
}
// NewTableWithCustom initializes obfuscation tables using either predefined or custom layouts.
// mode: "prefer_ascii" or "prefer_entropy". If a custom pattern is provided, ASCII mode still takes precedence.
// NewTableWithCustom initializes the uplink/probe Sudoku table using either predefined
// or directional layouts. Directional modes such as "up_ascii_down_entropy" return the
// client->server table and internally attach the opposite direction table for runtime use.
// The customPattern must contain 8 characters with exactly 2 x, 2 p, and 4 v (case-insensitive).
func NewTableWithCustom(key string, mode string, customPattern string) (*Table, error) {
asciiMode, err := ParseASCIIMode(mode)
if err != nil {
return nil, err
}
uplinkPattern := customPatternForToken(asciiMode.Uplink, customPattern)
downlinkPattern := customPatternForToken(asciiMode.Downlink, customPattern)
hint := tableHintFingerprint(key, asciiMode.Canonical(), uplinkPattern, downlinkPattern)
uplink, err := newSingleDirectionTable(key, asciiMode.uplinkPreference(), uplinkPattern)
if err != nil {
return nil, err
}
uplink.hint = hint
if asciiMode.Uplink == asciiMode.Downlink {
uplink.opposite = uplink
return uplink, nil
}
downlink, err := newSingleDirectionTable(key, asciiMode.downlinkPreference(), downlinkPattern)
if err != nil {
return nil, err
}
downlink.hint = hint
uplink.opposite = downlink
downlink.opposite = uplink
return uplink, nil
}
func newSingleDirectionTable(key string, mode string, customPattern string) (*Table, error) {
layout, err := resolveLayout(mode, customPattern)
if err != nil {
return nil, err
@@ -125,6 +159,38 @@ func NewTableWithCustom(key string, mode string, customPattern string) (*Table,
return t, nil
}
func customPatternForToken(token string, customPattern string) string {
if token == asciiModeTokenEntropy {
return customPattern
}
return ""
}
func (t *Table) OppositeDirection() *Table {
if t == nil || t.opposite == nil {
return t
}
return t.opposite
}
func (t *Table) Hint() uint32 {
if t == nil {
return 0
}
return t.hint
}
func tableHintFingerprint(key string, mode string, uplinkPattern string, downlinkPattern string) uint32 {
sum := sha256.Sum256([]byte(strings.Join([]string{
"sudoku-table-hint",
key,
mode,
strings.ToLower(strings.TrimSpace(uplinkPattern)),
strings.ToLower(strings.TrimSpace(downlinkPattern)),
}, "\x00")))
return binary.BigEndian.Uint32(sum[:4])
}
func packHintsToKey(hints [4]byte) uint32 {
// Sorting network for 4 elements (Bubble sort unrolled)
// Swap if a > b
@@ -14,20 +14,26 @@ import (
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
func pickClientTable(cfg *ProtocolConfig) (*sudoku.Table, error) {
type clientTableChoice struct {
Table *sudoku.Table
Hint uint32
HasHint bool
}
func pickClientTable(cfg *ProtocolConfig) (clientTableChoice, error) {
candidates := cfg.tableCandidates()
if len(candidates) == 0 {
return nil, fmt.Errorf("no table configured")
return clientTableChoice{}, fmt.Errorf("no table configured")
}
if len(candidates) == 1 {
return candidates[0], nil
return clientTableChoice{Table: candidates[0], Hint: candidates[0].Hint()}, nil
}
var b [1]byte
if _, err := crand.Read(b[:]); err != nil {
return nil, fmt.Errorf("random table pick failed: %w", err)
return clientTableChoice{}, fmt.Errorf("random table pick failed: %w", err)
}
idx := int(b[0]) % len(candidates)
return candidates[idx], nil
return clientTableChoice{Table: candidates[idx], Hint: candidates[idx].Hint(), HasHint: true}, nil
}
type readOnlyConn struct {
@@ -17,12 +17,23 @@ func normalizeCustomPatterns(customTable string, customTables []string) []string
return patterns
}
func normalizeTablePatterns(tableType string, customTable string, customTables []string) ([]string, error) {
patterns := normalizeCustomPatterns(customTable, customTables)
if _, err := sudoku.ParseASCIIMode(tableType); err != nil {
return nil, err
}
return patterns, nil
}
// NewTablesWithCustomPatterns builds one or more obfuscation tables from x/v/p custom patterns.
// When customTables is non-empty it overrides customTable (matching upstream Sudoku behavior).
//
// Deprecated-ish: prefer NewClientTablesWithCustomPatterns / NewServerTablesWithCustomPatterns.
func NewTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) {
patterns := normalizeCustomPatterns(customTable, customTables)
patterns, err := normalizeTablePatterns(tableType, customTable, customTables)
if err != nil {
return nil, err
}
tables := make([]*sudoku.Table, 0, len(patterns))
for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
@@ -39,11 +50,18 @@ func NewClientTablesWithCustomPatterns(key string, tableType string, customTable
return NewTablesWithCustomPatterns(key, tableType, customTable, customTables)
}
// NewServerTablesWithCustomPatterns matches upstream server behavior: when custom table rotation is enabled,
// also accept the default table to avoid forcing clients to update in lockstep.
// NewServerTablesWithCustomPatterns matches upstream server behavior: when probeable custom table
// rotation is enabled, also accept the default table to avoid forcing clients to update in lockstep.
func NewServerTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) {
patterns := normalizeCustomPatterns(customTable, customTables)
if len(patterns) > 0 && strings.TrimSpace(patterns[0]) != "" {
patterns, err := normalizeTablePatterns(tableType, customTable, customTables)
if err != nil {
return nil, err
}
asciiMode, err := sudoku.ParseASCIIMode(tableType)
if err != nil {
return nil, err
}
if asciiMode.Uplink == "entropy" && len(patterns) > 0 && strings.TrimSpace(patterns[0]) != "" {
patterns = append([]string{""}, patterns...)
}
return NewTablesWithCustomPatterns(key, tableType, "", patterns)
@@ -0,0 +1,45 @@
package sudoku
import "testing"
func TestDirectionalCustomTableRotationCollapse(t *testing.T) {
patterns := []string{"xpxvvpvv", "vxpvxvvp"}
clientTables, err := NewClientTablesWithCustomPatterns("seed", "up_ascii_down_entropy", "", patterns)
if err != nil {
t.Fatalf("client tables: %v", err)
}
if len(clientTables) != 2 {
t.Fatalf("expected ascii-uplink directional rotation to keep 2 tables, got %d", len(clientTables))
}
if clientTables[0].Hint() == clientTables[1].Hint() {
t.Fatalf("expected directional custom tables to carry distinct hints")
}
if got, want := clientTables[0].EncodeTable[0][0], clientTables[1].EncodeTable[0][0]; got != want {
t.Fatalf("expected directional ascii uplink tables to share the same probe layout, got %x want %x", got, want)
}
if got, want := clientTables[0].OppositeDirection().EncodeTable[0][0], clientTables[1].OppositeDirection().EncodeTable[0][0]; got == want {
t.Fatalf("expected directional downlink custom layouts to differ, both got %x", got)
}
clientTables, err = NewClientTablesWithCustomPatterns("seed", "up_entropy_down_ascii", "", patterns)
if err != nil {
t.Fatalf("client tables entropy uplink: %v", err)
}
if len(clientTables) != 2 {
t.Fatalf("expected entropy-uplink rotation to keep 2 tables, got %d", len(clientTables))
}
serverTables, err := NewServerTablesWithCustomPatterns("seed", "up_ascii_down_entropy", "", patterns)
if err != nil {
t.Fatalf("server tables: %v", err)
}
if len(serverTables) != 2 {
t.Fatalf("expected ascii-uplink server directional table set to keep 2 tables, got %d", len(serverTables))
}
if clientTables, err = NewClientTablesWithCustomPatterns("seed", "up_ascii_down_entropy", patterns[0], nil); err != nil {
t.Fatalf("client table with single custom pattern: %v", err)
} else if got, want := serverTables[0].OppositeDirection().EncodeTable[0][0], clientTables[0].OppositeDirection().EncodeTable[0][0]; got != want {
t.Fatalf("expected server directional downlink table to preserve custom pattern, got %x want %x", got, want)
}
}
@@ -11,7 +11,6 @@ import (
"strconv"
"sync"
"github.com/metacubex/mihomo/common/contextutils"
"github.com/metacubex/mihomo/common/httputils"
"github.com/metacubex/http"
@@ -21,6 +20,8 @@ import (
type DialRawFunc func(ctx context.Context) (net.Conn, error)
type WrapTLSFunc func(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error)
type TransportMaker func() http.RoundTripper
type PacketUpWriter struct {
ctx context.Context
cfg *Config
@@ -72,19 +73,8 @@ func (c *PacketUpWriter) Close() error {
return nil
}
func DialStreamOne(
ctx context.Context,
cfg *Config,
dialRaw DialRawFunc,
wrapTLS WrapTLSFunc,
) (net.Conn, error) {
requestURL := url.URL{
Scheme: "https",
Host: cfg.Host,
Path: cfg.NormalizedPath(),
}
transport := &http.Http2Transport{
func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc) http.RoundTripper {
return &http.Http2Transport{
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
raw, err := dialRaw(ctx)
if err != nil {
@@ -98,22 +88,74 @@ func DialStreamOne(
return wrapped, nil
},
}
}
type Client struct {
mode string
cfg *Config
makeTransport TransportMaker
makeDownloadTransport TransportMaker
ctx context.Context
cancel context.CancelFunc
}
func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport TransportMaker, hasReality bool) (*Client, error) {
mode := cfg.EffectiveMode(hasReality)
switch mode {
case "stream-one", "stream-up", "packet-up":
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", mode)
}
ctx, cancel := context.WithCancel(context.Background())
return &Client{
mode: mode,
cfg: cfg,
makeTransport: makeTransport,
makeDownloadTransport: makeDownloadTransport,
ctx: ctx,
cancel: cancel,
}, nil
}
func (c *Client) Dial() (net.Conn, error) {
switch c.mode {
case "stream-one":
return c.DialStreamOne()
case "stream-up":
return c.DialStreamUp()
case "packet-up":
return c.DialPacketUp()
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", c.mode)
}
}
func (c *Client) Close() error {
c.cancel()
return nil
}
func (c *Client) DialStreamOne() (net.Conn, error) {
transport := c.makeTransport()
requestURL := url.URL{
Scheme: "https",
Host: c.cfg.Host,
Path: c.cfg.NormalizedPath(),
}
pr, pw := io.Pipe()
conn := &Conn{
writer: pw,
}
conn := &Conn{writer: pw}
req, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, contextutils.WithoutCancel(ctx)), http.MethodPost, requestURL.String(), pr)
req, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, c.ctx), http.MethodPost, requestURL.String(), pr)
if err != nil {
_ = pr.Close()
_ = pw.Close()
return nil, err
}
req.Host = cfg.Host
req.Host = c.cfg.Host
if err := cfg.FillStreamRequest(req); err != nil {
if err := c.cfg.FillStreamRequest(req, ""); err != nil {
_ = pr.Close()
_ = pw.Close()
return nil, err
@@ -135,7 +177,6 @@ func DialStreamOne(
}
conn.reader = resp.Body
conn.onClose = func() {
_ = resp.Body.Close()
_ = pr.Close()
httputils.CloseTransport(transport)
}
@@ -143,64 +184,185 @@ func DialStreamOne(
return conn, nil
}
func DialPacketUp(
ctx context.Context,
cfg *Config,
dialRaw DialRawFunc,
wrapTLS WrapTLSFunc,
) (net.Conn, error) {
transport := &http.Http2Transport{
DialTLSContext: func(ctx context.Context, network string, addr string, _ *tls.Config) (net.Conn, error) {
raw, err := dialRaw(ctx)
if err != nil {
return nil, err
}
wrapped, err := wrapTLS(ctx, raw, true)
if err != nil {
_ = raw.Close()
return nil, err
}
return wrapped, nil
},
func (c *Client) DialStreamUp() (net.Conn, error) {
uploadTransport := c.makeTransport()
downloadTransport := uploadTransport
if c.makeDownloadTransport != nil {
downloadTransport = c.makeDownloadTransport()
}
downloadCfg := c.cfg
if ds := c.cfg.DownloadConfig; ds != nil {
downloadCfg = ds
}
streamURL := url.URL{
Scheme: "https",
Host: c.cfg.Host,
Path: c.cfg.NormalizedPath(),
}
downloadURL := url.URL{
Scheme: "https",
Host: downloadCfg.Host,
Path: downloadCfg.NormalizedPath(),
}
pr, pw := io.Pipe()
conn := &Conn{writer: pw}
sessionID := newSessionID()
downloadReq, err := http.NewRequestWithContext(
httputils.NewAddrContext(&conn.NetAddr, c.ctx),
http.MethodGet,
downloadURL.String(),
nil,
)
if err != nil {
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
return nil, err
}
if err := downloadCfg.FillDownloadRequest(downloadReq, sessionID); err != nil {
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
return nil, err
}
downloadReq.Host = downloadCfg.Host
downloadResp, err := downloadTransport.RoundTrip(downloadReq)
if err != nil {
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
return nil, err
}
if downloadResp.StatusCode != http.StatusOK {
_ = downloadResp.Body.Close()
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
return nil, fmt.Errorf("xhttp stream-up download bad status: %s", downloadResp.Status)
}
uploadReq, err := http.NewRequestWithContext(
c.ctx,
http.MethodPost,
streamURL.String(),
pr,
)
if err != nil {
_ = downloadResp.Body.Close()
_ = pr.Close()
_ = pw.Close()
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
return nil, err
}
if err := c.cfg.FillStreamRequest(uploadReq, sessionID); err != nil {
_ = downloadResp.Body.Close()
_ = pr.Close()
_ = pw.Close()
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
return nil, err
}
uploadReq.Host = c.cfg.Host
go func() {
resp, err := uploadTransport.RoundTrip(uploadReq)
if err != nil {
_ = pw.CloseWithError(err)
return
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
_ = pw.CloseWithError(fmt.Errorf("xhttp stream-up upload bad status: %s", resp.Status))
}
}()
conn.reader = downloadResp.Body
conn.onClose = func() {
_ = pr.Close()
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
}
return conn, nil
}
func (c *Client) DialPacketUp() (net.Conn, error) {
uploadTransport := c.makeTransport()
downloadTransport := uploadTransport
if c.makeDownloadTransport != nil {
downloadTransport = c.makeDownloadTransport()
}
downloadCfg := c.cfg
if ds := c.cfg.DownloadConfig; ds != nil {
downloadCfg = ds
}
sessionID := newSessionID()
downloadURL := url.URL{
Scheme: "https",
Host: cfg.Host,
Path: cfg.NormalizedPath(),
Host: downloadCfg.Host,
Path: downloadCfg.NormalizedPath(),
}
ctx = contextutils.WithoutCancel(ctx)
writer := &PacketUpWriter{
ctx: ctx,
cfg: cfg,
ctx: c.ctx,
cfg: c.cfg,
sessionID: sessionID,
transport: transport,
transport: uploadTransport,
seq: 0,
}
conn := &Conn{writer: writer}
req, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, ctx), http.MethodGet, downloadURL.String(), nil)
downloadReq, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, c.ctx), http.MethodGet, downloadURL.String(), nil)
if err != nil {
return nil, err
}
if err := cfg.FillDownloadRequest(req, sessionID); err != nil {
if err := downloadCfg.FillDownloadRequest(downloadReq, sessionID); err != nil {
return nil, err
}
req.Host = cfg.Host
downloadReq.Host = downloadCfg.Host
resp, err := transport.RoundTrip(req)
resp, err := downloadTransport.RoundTrip(downloadReq)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
_ = resp.Body.Close()
httputils.CloseTransport(transport)
httputils.CloseTransport(uploadTransport)
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
return nil, fmt.Errorf("xhttp packet-up download bad status: %s", resp.Status)
}
conn.reader = resp.Body
conn.onClose = func() {
if downloadTransport != uploadTransport {
httputils.CloseTransport(downloadTransport)
}
}
return conn, nil
}
@@ -12,12 +12,22 @@ import (
)
type Config struct {
Host string
Path string
Mode string
Headers map[string]string
NoGRPCHeader bool
XPaddingBytes string
Host string
Path string
Mode string
Headers map[string]string
NoGRPCHeader bool
XPaddingBytes string
DownloadConfig *Config
}
type DownloadConfig struct {
Host string
Path string
Mode string
ServerName string
ClientFingerprint string
SkipCertVerify bool
}
func (c *Config) NormalizedMode() string {
@@ -33,6 +43,9 @@ func (c *Config) EffectiveMode(hasReality bool) string {
return mode
}
if hasReality {
if c.DownloadConfig != nil {
return "stream-up"
}
return "stream-one"
}
return "packet-up"
@@ -126,7 +139,7 @@ func parseRange(s string) (int, int, error) {
return minVal, maxVal, nil
}
func (c *Config) FillStreamRequest(req *http.Request) error {
func (c *Config) FillStreamRequest(req *http.Request, sessionID string) error {
req.Header = c.RequestHeader()
paddingValue, err := c.RandomPadding()
@@ -143,6 +156,8 @@ func (c *Config) FillStreamRequest(req *http.Request) error {
req.Header.Set("Referer", rawURL+sep+"x_padding="+paddingValue)
}
c.ApplyMetaToRequest(req, sessionID, "")
if req.Body != nil && !c.NoGRPCHeader {
req.Header.Set("Content-Type", "application/grpc")
}
@@ -1,6 +1,7 @@
package xhttp
import (
"errors"
"io"
"time"
@@ -26,19 +27,12 @@ func (c *Conn) Read(b []byte) (int, error) {
}
func (c *Conn) Close() error {
err := c.writer.Close()
err2 := c.reader.Close()
if c.onClose != nil {
c.onClose()
}
err := c.writer.Close()
err2 := c.reader.Close()
if err != nil {
return err
}
if err2 != nil {
return err2
}
return nil
return errors.Join(err, err2)
}
func (c *Conn) SetReadDeadline(t time.Time) error { return c.SetDeadline(t) }
@@ -242,6 +242,45 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// stream-up upload: POST /path/{session}
if r.Method == http.MethodPost && len(parts) == 1 {
sessionID := parts[0]
session := h.getSession(sessionID)
if session == nil {
http.Error(w, "unknown xhttp session", http.StatusBadRequest)
return
}
buf := make([]byte, 32*1024)
var seq uint64
for {
n, err := r.Body.Read(buf)
if n > 0 {
if pushErr := session.uploadQueue.Push(Packet{
Seq: seq,
Payload: buf[:n],
}); pushErr != nil {
http.Error(w, pushErr.Error(), http.StatusInternalServerError)
return
}
seq++
}
if err == io.EOF {
break
}
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
return
}
// packet-up upload: POST /path/{session}/{seq}
if r.Method == http.MethodPost && len(parts) == 2 {
sessionID := parts[0]
@@ -13,7 +13,7 @@ require (
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dunglas/httpsfv v1.0.2 // indirect
github.com/enfein/mieru/v3 v3.29.0 // indirect
github.com/enfein/mieru/v3 v3.30.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
@@ -18,8 +18,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/enfein/mieru/v3 v3.29.0 h1:i5Hwl5spEWg4ydvYW86zWSYVJ2uGTf5sLYQmFXHdulQ=
github.com/enfein/mieru/v3 v3.29.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/enfein/mieru/v3 v3.30.0 h1:g7v0TuK7y0ZMn6TOdjOs8WEUQk8bvs6WYPBJ16SKdBU=
github.com/enfein/mieru/v3 v3.30.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -18,7 +18,7 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dunglas/httpsfv v1.0.2 // indirect
github.com/enfein/mieru/v3 v3.29.0 // indirect
github.com/enfein/mieru/v3 v3.30.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
@@ -18,8 +18,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/enfein/mieru/v3 v3.29.0 h1:i5Hwl5spEWg4ydvYW86zWSYVJ2uGTf5sLYQmFXHdulQ=
github.com/enfein/mieru/v3 v3.29.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/enfein/mieru/v3 v3.30.0 h1:g7v0TuK7y0ZMn6TOdjOs8WEUQk8bvs6WYPBJ16SKdBU=
github.com/enfein/mieru/v3 v3.30.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
+17 -33
View File
@@ -2,6 +2,7 @@ package outbound
import (
"context"
"errors"
"fmt"
"net"
"strconv"
@@ -37,7 +38,7 @@ type Vless struct {
// for gun mux
gunTransport *gun.Transport
// for xhttp
dialXHTTPConn func() (net.Conn, error)
xhttpClient *xhttp.Client
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
@@ -132,7 +133,6 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
wsOpts.TLS = true
wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
@@ -188,7 +188,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
case "grpc":
break // already handle in gun transport
case "xhttp":
break // already handle in dialXHTTPConn
break // already handle in xhttp client
default:
// default tcp network
// handle TLS
@@ -272,7 +272,7 @@ func (v *Vless) dialContext(ctx context.Context) (c net.Conn, err error) {
case "grpc": // gun transport
return v.gunTransport.Dial()
case "xhttp":
return v.dialXHTTPConn()
return v.xhttpClient.Dial()
default:
}
return v.dialer.DialContext(ctx, "tcp", v.addr)
@@ -347,10 +347,18 @@ func (v *Vless) ProxyInfo() C.ProxyInfo {
// Close implements C.ProxyAdapter
func (v *Vless) Close() error {
var errs []error
if v.gunTransport != nil {
return v.gunTransport.Close()
if err := v.gunTransport.Close(); err != nil {
errs = append(errs, err)
}
}
return nil
if v.xhttpClient != nil {
if err := v.xhttpClient.Close(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
@@ -607,33 +615,9 @@ func NewVless(option VlessOption) (*Vless, error) {
}
}
mode := cfg.EffectiveMode(v.realityConfig != nil)
switch mode {
case "stream-one":
v.dialXHTTPConn = func() (net.Conn, error) {
transport := makeTransport()
return xhttp.DialStreamOne(cfg, transport)
}
case "stream-up":
v.dialXHTTPConn = func() (net.Conn, error) {
transport := makeTransport()
downloadTransport := transport
if makeDownloadTransport != nil {
downloadTransport = makeDownloadTransport()
}
return xhttp.DialStreamUp(cfg, transport, downloadTransport)
}
case "packet-up":
v.dialXHTTPConn = func() (net.Conn, error) {
transport := makeTransport()
downloadTransport := transport
if makeDownloadTransport != nil {
downloadTransport = makeDownloadTransport()
}
return xhttp.DialPacketUp(cfg, transport, downloadTransport)
}
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", mode)
v.xhttpClient, err = xhttp.NewClient(cfg, makeTransport, makeDownloadTransport, v.realityConfig != nil)
if err != nil {
return nil, err
}
}
+36 -55
View File
@@ -145,24 +145,9 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = mihomoVMess.StreamWebsocketConn(ctx, c, wsOpts)
case "http":
// readability first, so just copy default TLS logic
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts)
if err != nil {
return nil, err
}
c, err = v.streamTLSConn(ctx, c, false)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(v.addr)
@@ -175,23 +160,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c = mihomoVMess.StreamHTTPConn(c, httpOpts)
case "h2":
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
NextProtos: []string{"h2"},
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = mihomoVMess.StreamTLSConn(ctx, c, &tlsOpts)
c, err = v.streamTLSConn(ctx, c, true)
if err != nil {
return nil, err
}
@@ -205,27 +174,9 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
case "grpc":
break // already handle in gun transport
default:
// default tcp network
// handle TLS
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts)
}
c, err = v.streamTLSConn(ctx, c, false)
}
if err != nil {
@@ -290,6 +241,36 @@ func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.M
return
}
func (v *Vmess) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) {
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if isH2 {
tlsOpts.NextProtos = []string{"h2"}
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
return mihomoVMess.StreamTLSConn(ctx, conn, &tlsOpts)
}
return conn, nil
}
func (v *Vmess) dialContext(ctx context.Context) (c net.Conn, err error) {
switch v.option.Network {
case "grpc": // gun transport
+4
View File
@@ -1627,6 +1627,10 @@ listeners:
flow: xtls-rprx-vision
# ws-path: "/" # 如果不为空则开启 websocket 传输层
# grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层
# xhttp-config: # 如果不为空则开启 xhttp 传输层
# path: "/"
# host: ""
# mode: auto # Available: "stream-one", "stream-up" or "packet-up"
# -------------------------
# vless encryption服务端配置:
# (原生外观 / 只 XOR 公钥 / 全随机数。1-RTT 每次下发随机 300 到 600 秒的 ticket 以便 0-RTT 复用 / 只允许 1-RTT
+3
View File
@@ -158,6 +158,9 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis
if tunnelName, err := getTunnelName(int32(options.FileDescriptor)); err == nil {
tunName = tunnelName // sing-tun must have the truth tun interface name even it from a fd
forwarderBindInterface = true
log.Debugln("[TUN] use tun name %s for fd %d", tunnelName, options.FileDescriptor)
} else {
log.Warnln("[TUN] get tun name failed for fd %d, fallback to use tun interface name %s", options.FileDescriptor, tunName)
}
}
routeAddress := options.RouteAddress
+82 -24
View File
@@ -20,6 +20,8 @@ import (
type DialRawFunc func(ctx context.Context) (net.Conn, error)
type WrapTLSFunc func(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error)
type TransportMaker func() http.RoundTripper
type PacketUpWriter struct {
ctx context.Context
cfg *Config
@@ -88,26 +90,72 @@ func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc) http.RoundTripper {
}
}
func DialStreamOne(cfg *Config, transport http.RoundTripper) (net.Conn, error) {
type Client struct {
mode string
cfg *Config
makeTransport TransportMaker
makeDownloadTransport TransportMaker
ctx context.Context
cancel context.CancelFunc
}
func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport TransportMaker, hasReality bool) (*Client, error) {
mode := cfg.EffectiveMode(hasReality)
switch mode {
case "stream-one", "stream-up", "packet-up":
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", mode)
}
ctx, cancel := context.WithCancel(context.Background())
return &Client{
mode: mode,
cfg: cfg,
makeTransport: makeTransport,
makeDownloadTransport: makeDownloadTransport,
ctx: ctx,
cancel: cancel,
}, nil
}
func (c *Client) Dial() (net.Conn, error) {
switch c.mode {
case "stream-one":
return c.DialStreamOne()
case "stream-up":
return c.DialStreamUp()
case "packet-up":
return c.DialPacketUp()
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", c.mode)
}
}
func (c *Client) Close() error {
c.cancel()
return nil
}
func (c *Client) DialStreamOne() (net.Conn, error) {
transport := c.makeTransport()
requestURL := url.URL{
Scheme: "https",
Host: cfg.Host,
Path: cfg.NormalizedPath(),
Host: c.cfg.Host,
Path: c.cfg.NormalizedPath(),
}
pr, pw := io.Pipe()
ctx := context.Background()
conn := &Conn{writer: pw}
req, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, ctx), http.MethodPost, requestURL.String(), pr)
req, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, c.ctx), http.MethodPost, requestURL.String(), pr)
if err != nil {
_ = pr.Close()
_ = pw.Close()
return nil, err
}
req.Host = cfg.Host
req.Host = c.cfg.Host
if err := cfg.FillStreamRequest(req, ""); err != nil {
if err := c.cfg.FillStreamRequest(req, ""); err != nil {
_ = pr.Close()
_ = pw.Close()
return nil, err
@@ -136,16 +184,22 @@ func DialStreamOne(cfg *Config, transport http.RoundTripper) (net.Conn, error) {
return conn, nil
}
func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransport http.RoundTripper) (net.Conn, error) {
downloadCfg := cfg
if ds := cfg.DownloadConfig; ds != nil {
func (c *Client) DialStreamUp() (net.Conn, error) {
uploadTransport := c.makeTransport()
downloadTransport := uploadTransport
if c.makeDownloadTransport != nil {
downloadTransport = c.makeDownloadTransport()
}
downloadCfg := c.cfg
if ds := c.cfg.DownloadConfig; ds != nil {
downloadCfg = ds
}
streamURL := url.URL{
Scheme: "https",
Host: cfg.Host,
Path: cfg.NormalizedPath(),
Host: c.cfg.Host,
Path: c.cfg.NormalizedPath(),
}
downloadURL := url.URL{
@@ -155,13 +209,12 @@ func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
}
pr, pw := io.Pipe()
ctx := context.Background()
conn := &Conn{writer: pw}
sessionID := newSessionID()
downloadReq, err := http.NewRequestWithContext(
httputils.NewAddrContext(&conn.NetAddr, ctx),
httputils.NewAddrContext(&conn.NetAddr, c.ctx),
http.MethodGet,
downloadURL.String(),
nil,
@@ -201,7 +254,7 @@ func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
}
uploadReq, err := http.NewRequestWithContext(
ctx,
c.ctx,
http.MethodPost,
streamURL.String(),
pr,
@@ -217,7 +270,7 @@ func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
return nil, err
}
if err := cfg.FillStreamRequest(uploadReq, sessionID); err != nil {
if err := c.cfg.FillStreamRequest(uploadReq, sessionID); err != nil {
_ = downloadResp.Body.Close()
_ = pr.Close()
_ = pw.Close()
@@ -227,7 +280,7 @@ func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
}
return nil, err
}
uploadReq.Host = cfg.Host
uploadReq.Host = c.cfg.Host
go func() {
resp, err := uploadTransport.RoundTrip(uploadReq)
@@ -255,9 +308,15 @@ func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
return conn, nil
}
func DialPacketUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransport http.RoundTripper) (net.Conn, error) {
downloadCfg := cfg
if ds := cfg.DownloadConfig; ds != nil {
func (c *Client) DialPacketUp() (net.Conn, error) {
uploadTransport := c.makeTransport()
downloadTransport := uploadTransport
if c.makeDownloadTransport != nil {
downloadTransport = c.makeDownloadTransport()
}
downloadCfg := c.cfg
if ds := c.cfg.DownloadConfig; ds != nil {
downloadCfg = ds
}
sessionID := newSessionID()
@@ -268,17 +327,16 @@ func DialPacketUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
Path: downloadCfg.NormalizedPath(),
}
ctx := context.Background()
writer := &PacketUpWriter{
ctx: ctx,
cfg: cfg,
ctx: c.ctx,
cfg: c.cfg,
sessionID: sessionID,
transport: uploadTransport,
seq: 0,
}
conn := &Conn{writer: writer}
downloadReq, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, ctx), http.MethodGet, downloadURL.String(), nil)
downloadReq, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, c.ctx), http.MethodGet, downloadURL.String(), nil)
if err != nil {
return nil, err
}
+27 -27
View File
@@ -5651,7 +5651,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro-crate 3.3.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -6367,9 +6367,9 @@ dependencies = [
[[package]]
name = "oxc_allocator"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff805b88789451a080b3c4d49fa0ebcd02dc6c0e370ed7a37ef954fbaf79915f"
checksum = "5e6fc6ce99f6a28fd477c6df500bbc9bf1c39db166952e15bea218459cc0db0c"
dependencies = [
"allocator-api2",
"hashbrown 0.16.1",
@@ -6379,9 +6379,9 @@ dependencies = [
[[package]]
name = "oxc_ast"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addc03b644cd9f26996bb32883f5cf4f4e46a51d20f5fbdbf675c14b29d38e95"
checksum = "49fa0813bf9fcff5a4e48fc186ee15a0d276b30b0b575389a34a530864567819"
dependencies = [
"bitflags 2.10.0",
"oxc_allocator",
@@ -6396,9 +6396,9 @@ dependencies = [
[[package]]
name = "oxc_ast_macros"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5950f9746248c26af04811e6db0523d354080637995be1dcc1c6bd3fca893bb2"
checksum = "3a2b2a2e09ff0dd4790a5ceb4a93349e0ea769d4d98d778946de48decb763b18"
dependencies = [
"phf 0.13.1",
"proc-macro2",
@@ -6408,9 +6408,9 @@ dependencies = [
[[package]]
name = "oxc_ast_visit"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31da485219d7ca6810872ce84fbcc7d11d8492145012603ead79beaf1476dc92"
checksum = "ef6d2304cb25dbbd028440591bf289ef16e3df98517930e79dcc304be64b3045"
dependencies = [
"oxc_allocator",
"oxc_ast",
@@ -6420,15 +6420,15 @@ dependencies = [
[[package]]
name = "oxc_data_structures"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "623bffc9732a0d39f248a2e7655d6d1704201790e5a8777aa188a678f1746fe8"
checksum = "c8e8f59bed9522098da177d894dc8635fb3eae218ff97d9c695900cb11fd10a2"
[[package]]
name = "oxc_diagnostics"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c612203fb402e998169c3e152a9fc8e736faafea0f13287c92144d4b8bc7b55"
checksum = "e0476859d4319f2b063f7c4a3120ee5b7e3e48032865ca501f8545ff44badcff"
dependencies = [
"cow-utils",
"oxc-miette",
@@ -6437,9 +6437,9 @@ dependencies = [
[[package]]
name = "oxc_ecmascript"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04c62e45b93f4257f5ca6d00f441e669ad52d98d36332394abe9f5527cf461d6"
checksum = "1bcf46e5b1a6f8ea3797e887a9db4c79ed15894ca8685eb628da462d4c4e913f"
dependencies = [
"cow-utils",
"num-bigint",
@@ -6453,9 +6453,9 @@ dependencies = [
[[package]]
name = "oxc_estree"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8794e3fbcd834e8ae4246dbd3121f9ee82c6ae60bc92615a276d42b6b62a2341"
checksum = "2251e6b61eab7b96f0e9d140b68b0f0d8a851c7d260725433e18b1babdcb9430"
[[package]]
name = "oxc_index"
@@ -6469,9 +6469,9 @@ dependencies = [
[[package]]
name = "oxc_parser"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "041125897019b72d23e6549d95985fe379354cf004e69cb811803109375fa91b"
checksum = "439d2580047b77faf6e60d358b48e5292e0e026b9cfc158d46ddd0175244bb26"
dependencies = [
"bitflags 2.10.0",
"cow-utils",
@@ -6492,9 +6492,9 @@ dependencies = [
[[package]]
name = "oxc_regular_expression"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "405e9515c3ae4c7227b3596219ec256dd883cb403db3a0d1c10146f82a894c93"
checksum = "0fb5669d3298a92d440afec516943745794cb4cf977911728cd73e3438db87b9"
dependencies = [
"bitflags 2.10.0",
"oxc_allocator",
@@ -6508,9 +6508,9 @@ dependencies = [
[[package]]
name = "oxc_span"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "894327633e5dcaef8baf34815d68100297f9776e20371502458ea3c42b8a710b"
checksum = "b1d452f6a664627bdd0f1f1586f9258f81cd7edc5c83e9ef50019f701ef1722d"
dependencies = [
"compact_str",
"oxc-miette",
@@ -6522,9 +6522,9 @@ dependencies = [
[[package]]
name = "oxc_str"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50e0b900b4f66db7d5b46a454532464861f675d03e16994040484d2c04151490"
checksum = "5c7a27c4371f69387f3d6f8fa56f70e4c6fa6aedc399285de6ec02bb9fd148d7"
dependencies = [
"compact_str",
"hashbrown 0.16.1",
@@ -6534,9 +6534,9 @@ dependencies = [
[[package]]
name = "oxc_syntax"
version = "0.122.0"
version = "0.123.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5edd0173b4667e5a1775b5d37e06a78c796fab18ee095739186831f2c54400"
checksum = "0d60d91023aafc256ab99c3dbf6181473e495695029c0152d2093e87df18ffe2"
dependencies = [
"bitflags 2.10.0",
"cow-utils",
+6 -6
View File
@@ -169,12 +169,12 @@ display-info = "0.5.0" # should be removed after upgrading to tauri v2
# OXC (The Oxidation Compiler)
# We use it to parse and transpile the old script profile to esm based script profile
oxc_parser = "0.122"
oxc_allocator = "0.122"
oxc_span = "0.122"
oxc_ast = "0.122"
oxc_syntax = "0.122"
oxc_ast_visit = "0.122"
oxc_parser = "0.123"
oxc_allocator = "0.123"
oxc_span = "0.123"
oxc_ast = "0.123"
oxc_syntax = "0.123"
oxc_ast_visit = "0.123"
# Lua Integration
mlua = { version = "0.11", features = [
@@ -73,9 +73,9 @@
"@iconify/json": "2.2.458",
"@monaco-editor/react": "4.7.0",
"@tanstack/react-query": "5.95.2",
"@tanstack/react-router": "1.168.8",
"@tanstack/react-router": "1.168.10",
"@tanstack/react-router-devtools": "1.166.11",
"@tanstack/router-plugin": "1.167.9",
"@tanstack/router-plugin": "1.167.12",
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
"@tauri-apps/plugin-dialog": "2.6.0",
"@tauri-apps/plugin-fs": "2.4.5",
@@ -97,7 +97,7 @@
"clsx": "2.1.1",
"core-js": "3.49.0",
"filesize": "11.0.15",
"meta-json-schema": "1.19.21",
"meta-json-schema": "1.19.22",
"monaco-yaml": "5.4.1",
"nanoid": "5.1.7",
"sass-embedded": "1.98.0",
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.21",
"mihomo_alpha": "alpha-132800e",
"mihomo_alpha": "alpha-73465fe",
"clash_rs": "v0.9.6",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.6-alpha+sha.77e5fd1"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-rs-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2026-03-30T22:23:53.481Z"
"updated_at": "2026-03-31T22:23:44.887Z"
}
+2 -2
View File
@@ -68,10 +68,10 @@
"cross-env": "10.1.0",
"dedent": "1.7.2",
"globals": "17.4.0",
"knip": "6.1.0",
"knip": "6.1.1",
"lint-staged": "16.4.0",
"npm-run-all2": "8.0.4",
"oxlint": "1.57.0",
"oxlint": "1.58.0",
"postcss": "8.5.8",
"postcss-html": "1.8.1",
"postcss-import": "16.1.1",
+129 -129
View File
@@ -59,8 +59,8 @@ importers:
specifier: 17.4.0
version: 17.4.0
knip:
specifier: 6.1.0
version: 6.1.0
specifier: 6.1.1
version: 6.1.1
lint-staged:
specifier: 16.4.0
version: 16.4.0
@@ -68,8 +68,8 @@ importers:
specifier: 8.0.4
version: 8.0.4
oxlint:
specifier: 1.57.0
version: 1.57.0
specifier: 1.58.0
version: 1.58.0
postcss:
specifier: 8.5.8
version: 8.5.8
@@ -226,7 +226,7 @@ importers:
version: 3.13.23(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.168.8(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.168.10(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
@@ -346,14 +346,14 @@ importers:
specifier: 5.95.2
version: 5.95.2(react@19.2.4)
'@tanstack/react-router':
specifier: 1.168.8
version: 1.168.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
specifier: 1.168.10
version: 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/react-router-devtools':
specifier: 1.166.11
version: 1.166.11(@tanstack/react-router@1.168.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.7)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/router-plugin':
specifier: 1.167.9
version: 1.167.9(@tanstack/react-router@1.168.8(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.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))
specifier: 1.167.12
version: 1.167.12(@tanstack/react-router@1.168.10(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.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(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
@@ -418,8 +418,8 @@ importers:
specifier: 11.0.15
version: 11.0.15
meta-json-schema:
specifier: 1.19.21
version: 1.19.21
specifier: 1.19.22
version: 1.19.22
monaco-yaml:
specifier: 5.4.1
version: 5.4.1(monaco-editor@0.55.1)
@@ -588,8 +588,8 @@ importers:
specifier: 17.0.35
version: 17.0.35
adm-zip:
specifier: 0.5.16
version: 0.5.16
specifier: 0.5.17
version: 0.5.17
colorize-template:
specifier: 1.0.0
version: 1.0.0
@@ -2585,124 +2585,124 @@ packages:
cpu: [x64]
os: [win32]
'@oxlint/binding-android-arm-eabi@1.57.0':
resolution: {integrity: sha512-C7EiyfAJG4B70496eV543nKiq5cH0o/xIh/ufbjQz3SIvHhlDDsyn+mRFh+aW8KskTyUpyH2LGWL8p2oN6bl1A==}
'@oxlint/binding-android-arm-eabi@1.58.0':
resolution: {integrity: sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@oxlint/binding-android-arm64@1.57.0':
resolution: {integrity: sha512-9i80AresjZ/FZf5xK8tKFbhQnijD4s1eOZw6/FHUwD59HEZbVLRc2C88ADYJfLZrF5XofWDiRX/Ja9KefCLy7w==}
'@oxlint/binding-android-arm64@1.58.0':
resolution: {integrity: sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@oxlint/binding-darwin-arm64@1.57.0':
resolution: {integrity: sha512-0eUfhRz5L2yKa9I8k3qpyl37XK3oBS5BvrgdVIx599WZK63P8sMbg+0s4IuxmIiZuBK68Ek+Z+gcKgeYf0otsg==}
'@oxlint/binding-darwin-arm64@1.58.0':
resolution: {integrity: sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@oxlint/binding-darwin-x64@1.57.0':
resolution: {integrity: sha512-UvrSuzBaYOue+QMAcuDITe0k/Vhj6KZGjfnI6x+NkxBTke/VoM7ZisaxgNY0LWuBkTnd1OmeQfEQdQ48fRjkQg==}
'@oxlint/binding-darwin-x64@1.58.0':
resolution: {integrity: sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@oxlint/binding-freebsd-x64@1.57.0':
resolution: {integrity: sha512-wtQq0dCoiw4bUwlsNVDJJ3pxJA218fOezpgtLKrbQqUtQJcM9yP8z+I9fu14aHg0uyAxIY+99toL6uBa2r7nxA==}
'@oxlint/binding-freebsd-x64@1.58.0':
resolution: {integrity: sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@oxlint/binding-linux-arm-gnueabihf@1.57.0':
resolution: {integrity: sha512-qxFWl2BBBFcT4djKa+OtMdnLgoHEJXpqjyGwz8OhW35ImoCwR5qtAGqApNYce5260FQqoAHW8S8eZTjiX67Tsg==}
'@oxlint/binding-linux-arm-gnueabihf@1.58.0':
resolution: {integrity: sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxlint/binding-linux-arm-musleabihf@1.57.0':
resolution: {integrity: sha512-SQoIsBU7J0bDW15/f0/RvxHfY3Y0+eB/caKBQtNFbuerTiA6JCYx9P1MrrFTwY2dTm/lMgTSgskvCEYk2AtG/Q==}
'@oxlint/binding-linux-arm-musleabihf@1.58.0':
resolution: {integrity: sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxlint/binding-linux-arm64-gnu@1.57.0':
resolution: {integrity: sha512-jqxYd1W6WMeozsCmqe9Rzbu3SRrGTyGDAipRlRggetyYbUksJqJKvUNTQtZR/KFoJPb+grnSm5SHhdWrywv3RQ==}
'@oxlint/binding-linux-arm64-gnu@1.58.0':
resolution: {integrity: sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.57.0':
resolution: {integrity: sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg==}
'@oxlint/binding-linux-arm64-musl@1.58.0':
resolution: {integrity: sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.57.0':
resolution: {integrity: sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA==}
'@oxlint/binding-linux-ppc64-gnu@1.58.0':
resolution: {integrity: sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.57.0':
resolution: {integrity: sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g==}
'@oxlint/binding-linux-riscv64-gnu@1.58.0':
resolution: {integrity: sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.57.0':
resolution: {integrity: sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ==}
'@oxlint/binding-linux-riscv64-musl@1.58.0':
resolution: {integrity: sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.57.0':
resolution: {integrity: sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw==}
'@oxlint/binding-linux-s390x-gnu@1.58.0':
resolution: {integrity: sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.57.0':
resolution: {integrity: sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg==}
'@oxlint/binding-linux-x64-gnu@1.58.0':
resolution: {integrity: sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.57.0':
resolution: {integrity: sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg==}
'@oxlint/binding-linux-x64-musl@1.58.0':
resolution: {integrity: sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.57.0':
resolution: {integrity: sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ==}
'@oxlint/binding-openharmony-arm64@1.58.0':
resolution: {integrity: sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@oxlint/binding-win32-arm64-msvc@1.57.0':
resolution: {integrity: sha512-Z4D8Pd0AyHBKeazhdIXeUUy5sIS3Mo0veOlzlDECg6PhRRKgEsBJCCV1n+keUZtQ04OP+i7+itS3kOykUyNhDg==}
'@oxlint/binding-win32-arm64-msvc@1.58.0':
resolution: {integrity: sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@oxlint/binding-win32-ia32-msvc@1.57.0':
resolution: {integrity: sha512-StOZ9nFMVKvevicbQfql6Pouu9pgbeQnu60Fvhz2S6yfMaii+wnueLnqQ5I1JPgNF0Syew4voBlAaHD13wH6tw==}
'@oxlint/binding-win32-ia32-msvc@1.58.0':
resolution: {integrity: sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@oxlint/binding-win32-x64-msvc@1.57.0':
resolution: {integrity: sha512-6PuxhYgth8TuW0+ABPOIkGdBYw+qYGxgIdXPHSVpiCDm+hqTTWCmC739St1Xni0DJBt8HnSHTG67i1y6gr8qrA==}
'@oxlint/binding-win32-x64-msvc@1.58.0':
resolution: {integrity: sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
@@ -4016,8 +4016,8 @@ packages:
'@tanstack/router-core':
optional: true
'@tanstack/react-router@1.168.8':
resolution: {integrity: sha512-t0S0QueXubBKmI9eLPcN/A1sLQgTu8/yHerjrvvsGeD12zMdw0uJPKwEKpStQF2OThQtw64cs34uUSYXBUTSNw==}
'@tanstack/react-router@1.168.10':
resolution: {integrity: sha512-/RmDlOwDkCug609KdPB3U+U1zmrtadJpvsmRg2zEn8TRCKRNri7dYZIjQZbNg8PgUiRL4T6njrZBV1ChzblNaA==}
engines: {node: '>=20.19'}
peerDependencies:
react: '>=18.0.0 || >=19.0.0'
@@ -4048,8 +4048,8 @@ 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.168.7':
resolution: {integrity: sha512-z4UEdlzMrFaKBsG4OIxlZEm+wsYBtEp//fnX6kW18jhQpETNcM6u2SXNdX+bcIYp6AaR7ERS3SBENzjC/xxwQQ==}
'@tanstack/router-core@1.168.9':
resolution: {integrity: sha512-18oeEwEDyXOIuO1VBP9ACaK7tYHZUjynGDCoUh/5c/BNhia9vCJCp9O0LfhZXOorDc/PmLSgvmweFhVmIxF10g==}
engines: {node: '>=20.19'}
hasBin: true
@@ -4063,17 +4063,17 @@ packages:
csstype:
optional: true
'@tanstack/router-generator@1.166.22':
resolution: {integrity: sha512-wQ7H8/Q2rmSPuaxWnurJ3DATNnqWV2tajxri9TSiW4QHsG7cWPD34+goeIinKG+GajJyEdfVpz6w/gRJXfbAPw==}
'@tanstack/router-generator@1.166.24':
resolution: {integrity: sha512-vdaGKwuH+r+DPe6R1mjk+TDDmDH6NTG7QqwxHqGEvOH4aGf9sPjhmRKNJZqQr8cPIbfp6u5lXyZ1TeDcSNMVEA==}
engines: {node: '>=20.19'}
'@tanstack/router-plugin@1.167.9':
resolution: {integrity: sha512-h/VV05FEHd4PVyc5Zy8B3trWLcdLt/Pmp+mfifmBKGRw+MUtvdQKbBHhmy4ouOf67s5zDJMc+n8R3xgU7bDwFA==}
'@tanstack/router-plugin@1.167.12':
resolution: {integrity: sha512-StEHcctCuFI5taSjO+lhR/yQ+EK63BdyYa+ne6FoNQPB3MMrOUrz2ZVnbqILRLkh2b+p2EfBKt65sgAKdKygPQ==}
engines: {node: '>=20.19'}
hasBin: true
peerDependencies:
'@rsbuild/core': '>=1.0.2'
'@tanstack/react-router': ^1.168.8
'@tanstack/react-router': ^1.168.10
vite: '>=5.0.0 || >=6.0.0 || >=7.0.0'
vite-plugin-solid: ^2.11.10
webpack: '>=5.92.0'
@@ -4666,8 +4666,8 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
adm-zip@0.5.16:
resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==}
adm-zip@0.5.17:
resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==}
engines: {node: '>=12.0'}
ahooks@3.9.7:
@@ -6092,8 +6092,8 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
knip@6.1.0:
resolution: {integrity: sha512-n5eVbJP7HXmwTsiJcELWJe2O1ESxyCTNxJzRTIECDYDTM465qnqk7fL2dv6ae3NUFvFWorZvGlh9mcwxwJ5Xgw==}
knip@6.1.1:
resolution: {integrity: sha512-BC/kbdxwCgv+p/3YkGbtlLxbOXhQDuR+CeKKFEpJyKb3BFwG1gZa+CMWSqAnPi+kUexz74m327d3zWxyn2fMew==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -6334,8 +6334,8 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
meta-json-schema@1.19.21:
resolution: {integrity: sha512-PkEdW1H+C0HNt+Bw5qAfBHkXgN0ZXB1g5YBhzCRzUNdLnWWe59lMgXRri85IizRRRVe8bVLffDMNbPb+4wrU3Q==}
meta-json-schema@1.19.22:
resolution: {integrity: sha512-j/s7HbG90iZdiL7YBIqmbr/DY0BwGDViCsQxLLKjsIqGryt/SjoV1TgZ1dRaRRa77/m3XQkRqJEWgIAs/yk8Ig==}
engines: {node: '>=18', pnpm: '>=9'}
micromark-core-commonmark@2.0.1:
@@ -6608,12 +6608,12 @@ packages:
oxc-resolver@11.19.1:
resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==}
oxlint@1.57.0:
resolution: {integrity: sha512-DGFsuBX5MFZX9yiDdtKjTrYPq45CZ8Fft6qCltJITYZxfwYjVdGf/6wycGYTACloauwIPxUnYhBVeZbHvleGhw==}
oxlint@1.58.0:
resolution: {integrity: sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
oxlint-tsgolint: '>=0.15.0'
oxlint-tsgolint: '>=0.18.0'
peerDependenciesMeta:
oxlint-tsgolint:
optional: true
@@ -10312,61 +10312,61 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@11.19.1':
optional: true
'@oxlint/binding-android-arm-eabi@1.57.0':
'@oxlint/binding-android-arm-eabi@1.58.0':
optional: true
'@oxlint/binding-android-arm64@1.57.0':
'@oxlint/binding-android-arm64@1.58.0':
optional: true
'@oxlint/binding-darwin-arm64@1.57.0':
'@oxlint/binding-darwin-arm64@1.58.0':
optional: true
'@oxlint/binding-darwin-x64@1.57.0':
'@oxlint/binding-darwin-x64@1.58.0':
optional: true
'@oxlint/binding-freebsd-x64@1.57.0':
'@oxlint/binding-freebsd-x64@1.58.0':
optional: true
'@oxlint/binding-linux-arm-gnueabihf@1.57.0':
'@oxlint/binding-linux-arm-gnueabihf@1.58.0':
optional: true
'@oxlint/binding-linux-arm-musleabihf@1.57.0':
'@oxlint/binding-linux-arm-musleabihf@1.58.0':
optional: true
'@oxlint/binding-linux-arm64-gnu@1.57.0':
'@oxlint/binding-linux-arm64-gnu@1.58.0':
optional: true
'@oxlint/binding-linux-arm64-musl@1.57.0':
'@oxlint/binding-linux-arm64-musl@1.58.0':
optional: true
'@oxlint/binding-linux-ppc64-gnu@1.57.0':
'@oxlint/binding-linux-ppc64-gnu@1.58.0':
optional: true
'@oxlint/binding-linux-riscv64-gnu@1.57.0':
'@oxlint/binding-linux-riscv64-gnu@1.58.0':
optional: true
'@oxlint/binding-linux-riscv64-musl@1.57.0':
'@oxlint/binding-linux-riscv64-musl@1.58.0':
optional: true
'@oxlint/binding-linux-s390x-gnu@1.57.0':
'@oxlint/binding-linux-s390x-gnu@1.58.0':
optional: true
'@oxlint/binding-linux-x64-gnu@1.57.0':
'@oxlint/binding-linux-x64-gnu@1.58.0':
optional: true
'@oxlint/binding-linux-x64-musl@1.57.0':
'@oxlint/binding-linux-x64-musl@1.58.0':
optional: true
'@oxlint/binding-openharmony-arm64@1.57.0':
'@oxlint/binding-openharmony-arm64@1.58.0':
optional: true
'@oxlint/binding-win32-arm64-msvc@1.57.0':
'@oxlint/binding-win32-arm64-msvc@1.58.0':
optional: true
'@oxlint/binding-win32-ia32-msvc@1.57.0':
'@oxlint/binding-win32-ia32-msvc@1.58.0':
optional: true
'@oxlint/binding-win32-x64-msvc@1.57.0':
'@oxlint/binding-win32-x64-msvc@1.58.0':
optional: true
'@paper-design/shaders-react@0.0.72(@types/react@19.2.14)(react@19.2.4)':
@@ -11593,22 +11593,22 @@ snapshots:
'@tanstack/query-core': 5.95.2
react: 19.2.4
'@tanstack/react-router-devtools@1.166.11(@tanstack/react-router@1.168.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.7)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@tanstack/react-router-devtools@1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@tanstack/react-router': 1.168.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/router-devtools-core': 1.167.1(@tanstack/router-core@1.168.7)(csstype@3.2.3)
'@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/router-devtools-core': 1.167.1(@tanstack/router-core@1.168.9)(csstype@3.2.3)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@tanstack/router-core': 1.168.7
'@tanstack/router-core': 1.168.9
transitivePeerDependencies:
- csstype
'@tanstack/react-router@1.168.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@tanstack/react-router@1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@tanstack/history': 1.161.6
'@tanstack/react-store': 0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/router-core': 1.168.7
'@tanstack/router-core': 1.168.9
isbot: 5.1.28
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
@@ -11638,24 +11638,24 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@tanstack/router-core@1.168.7':
'@tanstack/router-core@1.168.9':
dependencies:
'@tanstack/history': 1.161.6
cookie-es: 2.0.0
seroval: 1.4.2
seroval-plugins: 1.4.2(seroval@1.4.2)
'@tanstack/router-devtools-core@1.167.1(@tanstack/router-core@1.168.7)(csstype@3.2.3)':
'@tanstack/router-devtools-core@1.167.1(@tanstack/router-core@1.168.9)(csstype@3.2.3)':
dependencies:
'@tanstack/router-core': 1.168.7
'@tanstack/router-core': 1.168.9
clsx: 2.1.1
goober: 2.1.16(csstype@3.2.3)
optionalDependencies:
csstype: 3.2.3
'@tanstack/router-generator@1.166.22':
'@tanstack/router-generator@1.166.24':
dependencies:
'@tanstack/router-core': 1.168.7
'@tanstack/router-core': 1.168.9
'@tanstack/router-utils': 1.161.6
'@tanstack/virtual-file-routes': 1.161.7
prettier: 3.8.1
@@ -11666,7 +11666,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tanstack/router-plugin@1.167.9(@tanstack/react-router@1.168.8(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.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))':
'@tanstack/router-plugin@1.167.12(@tanstack/react-router@1.168.10(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.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(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)
@@ -11674,15 +11674,15 @@ snapshots:
'@babel/template': 7.28.6
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
'@tanstack/router-core': 1.168.7
'@tanstack/router-generator': 1.166.22
'@tanstack/router-core': 1.168.9
'@tanstack/router-generator': 1.166.24
'@tanstack/router-utils': 1.161.6
'@tanstack/virtual-file-routes': 1.161.7
chokidar: 3.6.0
unplugin: 2.3.11
zod: 3.25.76
optionalDependencies:
'@tanstack/react-router': 1.168.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/react-router': 1.168.10(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.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
@@ -11701,9 +11701,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.168.8(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.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(zod@4.3.6)':
dependencies:
'@tanstack/react-router': 1.168.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/react-router': 1.168.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
zod: 4.3.6
'@tanstack/store@0.9.3': {}
@@ -12354,7 +12354,7 @@ snapshots:
acorn@8.15.0: {}
adm-zip@0.5.16: {}
adm-zip@0.5.17: {}
ahooks@3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
@@ -13727,7 +13727,7 @@ snapshots:
kind-of@6.0.3: {}
knip@6.1.0:
knip@6.1.1:
dependencies:
'@nodelib/fs.walk': 1.2.8
fast-glob: 3.3.3
@@ -14019,7 +14019,7 @@ snapshots:
merge2@1.4.1: {}
meta-json-schema@1.19.21: {}
meta-json-schema@1.19.22: {}
micromark-core-commonmark@2.0.1:
dependencies:
@@ -14446,27 +14446,27 @@ snapshots:
'@oxc-resolver/binding-win32-ia32-msvc': 11.19.1
'@oxc-resolver/binding-win32-x64-msvc': 11.19.1
oxlint@1.57.0:
oxlint@1.58.0:
optionalDependencies:
'@oxlint/binding-android-arm-eabi': 1.57.0
'@oxlint/binding-android-arm64': 1.57.0
'@oxlint/binding-darwin-arm64': 1.57.0
'@oxlint/binding-darwin-x64': 1.57.0
'@oxlint/binding-freebsd-x64': 1.57.0
'@oxlint/binding-linux-arm-gnueabihf': 1.57.0
'@oxlint/binding-linux-arm-musleabihf': 1.57.0
'@oxlint/binding-linux-arm64-gnu': 1.57.0
'@oxlint/binding-linux-arm64-musl': 1.57.0
'@oxlint/binding-linux-ppc64-gnu': 1.57.0
'@oxlint/binding-linux-riscv64-gnu': 1.57.0
'@oxlint/binding-linux-riscv64-musl': 1.57.0
'@oxlint/binding-linux-s390x-gnu': 1.57.0
'@oxlint/binding-linux-x64-gnu': 1.57.0
'@oxlint/binding-linux-x64-musl': 1.57.0
'@oxlint/binding-openharmony-arm64': 1.57.0
'@oxlint/binding-win32-arm64-msvc': 1.57.0
'@oxlint/binding-win32-ia32-msvc': 1.57.0
'@oxlint/binding-win32-x64-msvc': 1.57.0
'@oxlint/binding-android-arm-eabi': 1.58.0
'@oxlint/binding-android-arm64': 1.58.0
'@oxlint/binding-darwin-arm64': 1.58.0
'@oxlint/binding-darwin-x64': 1.58.0
'@oxlint/binding-freebsd-x64': 1.58.0
'@oxlint/binding-linux-arm-gnueabihf': 1.58.0
'@oxlint/binding-linux-arm-musleabihf': 1.58.0
'@oxlint/binding-linux-arm64-gnu': 1.58.0
'@oxlint/binding-linux-arm64-musl': 1.58.0
'@oxlint/binding-linux-ppc64-gnu': 1.58.0
'@oxlint/binding-linux-riscv64-gnu': 1.58.0
'@oxlint/binding-linux-riscv64-musl': 1.58.0
'@oxlint/binding-linux-s390x-gnu': 1.58.0
'@oxlint/binding-linux-x64-gnu': 1.58.0
'@oxlint/binding-linux-x64-musl': 1.58.0
'@oxlint/binding-openharmony-arm64': 1.58.0
'@oxlint/binding-win32-arm64-msvc': 1.58.0
'@oxlint/binding-win32-ia32-msvc': 1.58.0
'@oxlint/binding-win32-x64-msvc': 1.58.0
p-retry@7.1.1:
dependencies:
+1 -1
View File
@@ -16,7 +16,7 @@
"@octokit/types": "16.0.0",
"@types/adm-zip": "0.5.7",
"@types/yargs": "17.0.35",
"adm-zip": "0.5.16",
"adm-zip": "0.5.17",
"colorize-template": "1.0.0",
"consola": "3.4.2",
"fs-extra": "11.3.4",
+2 -2
View File
@@ -1,2 +1,2 @@
LINUX_VERSION-6.12 = .77
LINUX_KERNEL_HASH-6.12.77 = 358836ebe5caef41e7ae9492e7fbcdf5be6e53ee43c99752aebda81e1b2cff67
LINUX_VERSION-6.12 = .79
LINUX_KERNEL_HASH-6.12.79 = 4bfa751f33de2a5d7ecb4ff964743a027fc726a2225a76a18f92f0582aa0790b
@@ -143,7 +143,7 @@ $(eval $(call KernelPackage,mii))
define KernelPackage/mdio-devres
SUBMENU:=$(NETWORK_DEVICES_MENU)
TITLE:=Supports MDIO device registration
DEPENDS:=+kmod-libphy +(TARGET_armvirt||TARGET_bcm27xx_bcm2708||TARGET_loongarch64||TARGET_malta||TARGET_tegra):kmod-of-mdio
DEPENDS:=@!LINUX_5_4 +kmod-libphy +(TARGET_armvirt||TARGET_bcm27xx_bcm2708||TARGET_loongarch64||TARGET_malta||TARGET_tegra):kmod-of-mdio
KCONFIG:=CONFIG_MDIO_DEVRES=y
HIDDEN:=1
FILES:=$(LINUX_DIR)/drivers/net/phy/mdio_devres.ko
@@ -0,0 +1,63 @@
From: Shuah Khan <skhan@linuxfoundation.org>
To: peterz@infradead.org, mingo@redhat.com, will@kernel.org,
kvalo@codeaurora.org, davem@davemloft.net, kuba@kernel.org
Cc: Shuah Khan <skhan@linuxfoundation.org>,
ath10k@lists.infradead.org, linux-wireless@vger.kernel.org,
netdev@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH 1/2] lockdep: add lockdep_assert_not_held()
Date: Fri, 12 Feb 2021 16:28:42 -0700 [thread overview]
Message-ID: <37a29c383bff2fb1605241ee6c7c9be3784fb3c6.1613171185.git.skhan@linuxfoundation.org> (raw)
In-Reply-To: <cover.1613171185.git.skhan@linuxfoundation.org>
Some kernel functions must not be called holding a specific lock. Doing
so could lead to locking problems. Currently these routines call
lock_is_held() to check for lock hold followed by WARN_ON.
Adding a common lockdep interface will help reduce the duplication of this
logic in the rest of the kernel.
Add lockdep_assert_not_held() to be used in these functions to detect
incorrect calls while holding a lock.
lockdep_assert_not_held() provides the opposite functionality of
lockdep_assert_held() which is used to assert calls that require
holding a specific lock.
The need for lockdep_assert_not_held() came up in a discussion on
ath10k patch. ath10k_drain_tx() and i915_vma_pin_ww() are examples
of functions that can use lockdep_assert_not_held().
Link: https://lore.kernel.org/linux-wireless/871rdmu9z9.fsf@codeaurora.org/
Signed-off-by: Shuah Khan <skhan@linuxfoundation.org>
---
include/linux/lockdep.h | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/include/linux/lockdep.h b/include/linux/lockdep.h
index b9e9adec73e8..567e3a1a27ce 100644
--- a/include/linux/lockdep.h
+++ b/include/linux/lockdep.h
@@ -294,6 +294,10 @@ extern void lock_unpin_lock(struct lockdep_map *lock, struct pin_cookie);
#define lockdep_depth(tsk) (debug_locks ? (tsk)->lockdep_depth : 0)
+#define lockdep_assert_not_held(l) do { \
+ WARN_ON(debug_locks && lockdep_is_held(l)); \
+ } while (0)
+
#define lockdep_assert_held(l) do { \
WARN_ON(debug_locks && !lockdep_is_held(l)); \
} while (0)
@@ -383,8 +387,9 @@ extern int lock_is_held(const void *);
extern int lockdep_is_held(const void *);
#define lockdep_is_held_type(l, r) (1)
+#define lockdep_assert_not_held(l) do { (void)(l); } while (0)
#define lockdep_assert_held(l) do { (void)(l); } while (0)
-#define lockdep_assert_held_write(l) do { (void)(l); } while (0)
+#define lockdep_assert_held_write(l) do { (void)(l); } while (0)
#define lockdep_assert_held_read(l) do { (void)(l); } while (0)
#define lockdep_assert_held_once(l) do { (void)(l); } while (0)
--
2.27.0
@@ -1,45 +0,0 @@
From: =?utf-8?q?=C3=81lvaro_Fern=C3=A1ndez_Rojas?= <noltari@gmail.com>
Cc: =?utf-8?q?=C3=81lvaro_Fern=C3=A1ndez_Rojas?= <noltari@gmail.com>
Subject: [PATCH net v3] net: sfp: improve Huawei MA5671a fixup
Date: Fri, 6 Mar 2026 13:29:55 +0100
With the current sfp_fixup_ignore_tx_fault() fixup we ignore the TX_FAULT
signal, but we also need to apply sfp_fixup_ignore_los() in order to be
able to communicate with the module even if the fiber isn't connected for
configuration purposes.
This is needed for all the MA5671a firmwares, excluding the FS modded
firmware.
Fixes: 2069624dac19 ("net: sfp: Add tx-fault workaround for Huawei MA5671A SFP ONT")
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
---
v3: avoid using a vendor name in the function
v2: rebase on top of net/main instead of linux/master
drivers/net/phy/sfp.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
--- a/drivers/net/phy/sfp.c
+++ b/drivers/net/phy/sfp.c
@@ -360,6 +360,12 @@ static void sfp_fixup_ignore_tx_fault(st
sfp->state_ignore_mask |= SFP_F_TX_FAULT;
}
+static void sfp_fixup_ignore_tx_fault_and_los(struct sfp *sfp)
+{
+ sfp_fixup_ignore_tx_fault(sfp);
+ sfp_fixup_ignore_los(sfp);
+}
+
static void sfp_fixup_ignore_hw(struct sfp *sfp, unsigned int mask)
{
sfp->state_hw_mask &= ~mask;
@@ -523,7 +529,7 @@ static const struct sfp_quirk sfp_quirks
// Huawei MA5671A can operate at 2500base-X, but report 1.2GBd NRZ in
// their EEPROM
SFP_QUIRK("HUAWEI", "MA5671A", sfp_quirk_2500basex,
- sfp_fixup_ignore_tx_fault),
+ sfp_fixup_ignore_tx_fault_and_los),
// Lantech 8330-262D-E and 8330-265D can operate at 2500base-X, but
// incorrectly report 2500MBd NRZ in their EEPROM.
@@ -1,116 +0,0 @@
From 5225349f1e750dfd107a4c5dc97d91fa212dc1ed Mon Sep 17 00:00:00 2001
From: Andrew Lunn <andrew@lunn.ch>
Date: Sat, 21 Feb 2026 14:51:54 -0600
Subject: [PATCH] net: phy: register phy led_triggers during probe to avoid
AB-BA deadlock
There is an AB-BA deadlock when both LEDS_TRIGGER_NETDEV and
LED_TRIGGER_PHY are enabled:
[ 1362.049207] [<8054e4b8>] led_trigger_register+0x5c/0x1fc <-- Trying to get lock "triggers_list_lock" via down_write(&triggers_list_lock);
[ 1362.054536] [<80662830>] phy_led_triggers_register+0xd0/0x234
[ 1362.060329] [<8065e200>] phy_attach_direct+0x33c/0x40c
[ 1362.065489] [<80651fc4>] phylink_fwnode_phy_connect+0x15c/0x23c
[ 1362.071480] [<8066ee18>] mtk_open+0x7c/0xba0
[ 1362.075849] [<806d714c>] __dev_open+0x280/0x2b0
[ 1362.080384] [<806d7668>] __dev_change_flags+0x244/0x24c
[ 1362.085598] [<806d7698>] dev_change_flags+0x28/0x78
[ 1362.090528] [<807150e4>] dev_ioctl+0x4c0/0x654 <-- Hold lock "rtnl_mutex" by calling rtnl_lock();
[ 1362.094985] [<80694360>] sock_ioctl+0x2f4/0x4e0
[ 1362.099567] [<802e9c4c>] sys_ioctl+0x32c/0xd8c
[ 1362.104022] [<80014504>] syscall_common+0x34/0x58
Here LED_TRIGGER_PHY is registering LED triggers during phy_attach
while holding RTNL and then taking triggers_list_lock.
[ 1362.191101] [<806c2640>] register_netdevice_notifier+0x60/0x168 <-- Trying to get lock "rtnl_mutex" via rtnl_lock();
[ 1362.197073] [<805504ac>] netdev_trig_activate+0x194/0x1e4
[ 1362.202490] [<8054e28c>] led_trigger_set+0x1d4/0x360 <-- Hold lock "triggers_list_lock" by down_read(&triggers_list_lock);
[ 1362.207511] [<8054eb38>] led_trigger_write+0xd8/0x14c
[ 1362.212566] [<80381d98>] sysfs_kf_bin_write+0x80/0xbc
[ 1362.217688] [<8037fcd8>] kernfs_fop_write_iter+0x17c/0x28c
[ 1362.223174] [<802cbd70>] vfs_write+0x21c/0x3c4
[ 1362.227712] [<802cc0c4>] ksys_write+0x78/0x12c
[ 1362.232164] [<80014504>] syscall_common+0x34/0x58
Here LEDS_TRIGGER_NETDEV is being enabled on an LED. It first takes
triggers_list_lock and then RTNL. A classical AB-BA deadlock.
phy_led_triggers_registers() does not require the RTNL, it does not
make any calls into the network stack which require protection. There
is also no requirement the PHY has been attached to a MAC, the
triggers only make use of phydev state. This allows the call to
phy_led_triggers_registers() to be placed elsewhere. PHY probe() and
release() don't hold RTNL, so solving the AB-BA deadlock.
Reported-by: Shiji Yang <yangshiji66@outlook.com>
Closes: https://lore.kernel.org/all/OS7PR01MB13602B128BA1AD3FA38B6D1FFBC69A@OS7PR01MB13602.jpnprd01.prod.outlook.com/
Fixes: 06f502f57d0d ("leds: trigger: Introduce a NETDEV trigger")
Signed-off-by: Andrew Lunn <andrew@lunn.ch>
---
drivers/net/phy/phy_device.c | 25 +++++++++++++++++--------
1 file changed, 17 insertions(+), 8 deletions(-)
--- a/drivers/net/phy/phy_device.c
+++ b/drivers/net/phy/phy_device.c
@@ -1684,8 +1684,6 @@ int phy_attach_direct(struct net_device
goto error;
phy_resume(phydev);
- if (!phydev->is_on_sfp_module)
- phy_led_triggers_register(phydev);
/**
* If the external phy used by current mac interface is managed by
@@ -2058,9 +2056,6 @@ void phy_detach(struct phy_device *phyde
}
phydev->phylink = NULL;
- if (!phydev->is_on_sfp_module)
- phy_led_triggers_unregister(phydev);
-
if (phydev->mdio.dev.driver)
module_put(phydev->mdio.dev.driver->owner);
@@ -3691,17 +3686,28 @@ static int phy_probe(struct device *dev)
/* Set the state to READY by default */
phydev->state = PHY_READY;
+ /* Register the PHY LED triggers */
+ if (!phydev->is_on_sfp_module)
+ phy_led_triggers_register(phydev);
+
/* Get the LEDs from the device tree, and instantiate standard
* LEDs for them.
*/
if (IS_ENABLED(CONFIG_PHYLIB_LEDS) && !phy_driver_is_genphy(phydev) &&
- !phy_driver_is_genphy_10g(phydev))
+ !phy_driver_is_genphy_10g(phydev)) {
err = of_phy_leds(phydev);
+ if (err)
+ goto out;
+ }
+
+ return 0;
out:
+ if (!phydev->is_on_sfp_module)
+ phy_led_triggers_unregister(phydev);
+
/* Re-assert the reset signal on error */
- if (err)
- phy_device_reset(phydev, 1);
+ phy_device_reset(phydev, 1);
return err;
}
@@ -3716,6 +3722,9 @@ static int phy_remove(struct device *dev
!phy_driver_is_genphy_10g(phydev))
phy_leds_unregister(phydev);
+ if (!phydev->is_on_sfp_module)
+ phy_led_triggers_unregister(phydev);
+
phydev->state = PHY_DOWN;
sfp_bus_del_upstream(phydev->sfp_bus);
@@ -18,9 +18,9 @@ Signed-off-by: Shawn Lin <shawn.lin@rock-chips.com>
--- a/drivers/mmc/host/dw_mmc-rockchip.c
+++ b/drivers/mmc/host/dw_mmc-rockchip.c
@@ -35,6 +35,8 @@ struct dw_mci_rockchip_priv_data {
int default_sample_phase;
int num_phases;
bool internal_phase;
int sample_phase;
int drv_phase;
+ int last_degree;
+ bool use_v2_tuning;
};
+17 -33
View File
@@ -2,6 +2,7 @@ package outbound
import (
"context"
"errors"
"fmt"
"net"
"strconv"
@@ -37,7 +38,7 @@ type Vless struct {
// for gun mux
gunTransport *gun.Transport
// for xhttp
dialXHTTPConn func() (net.Conn, error)
xhttpClient *xhttp.Client
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
@@ -132,7 +133,6 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
wsOpts.TLS = true
wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
@@ -188,7 +188,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
case "grpc":
break // already handle in gun transport
case "xhttp":
break // already handle in dialXHTTPConn
break // already handle in xhttp client
default:
// default tcp network
// handle TLS
@@ -272,7 +272,7 @@ func (v *Vless) dialContext(ctx context.Context) (c net.Conn, err error) {
case "grpc": // gun transport
return v.gunTransport.Dial()
case "xhttp":
return v.dialXHTTPConn()
return v.xhttpClient.Dial()
default:
}
return v.dialer.DialContext(ctx, "tcp", v.addr)
@@ -347,10 +347,18 @@ func (v *Vless) ProxyInfo() C.ProxyInfo {
// Close implements C.ProxyAdapter
func (v *Vless) Close() error {
var errs []error
if v.gunTransport != nil {
return v.gunTransport.Close()
if err := v.gunTransport.Close(); err != nil {
errs = append(errs, err)
}
}
return nil
if v.xhttpClient != nil {
if err := v.xhttpClient.Close(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
@@ -607,33 +615,9 @@ func NewVless(option VlessOption) (*Vless, error) {
}
}
mode := cfg.EffectiveMode(v.realityConfig != nil)
switch mode {
case "stream-one":
v.dialXHTTPConn = func() (net.Conn, error) {
transport := makeTransport()
return xhttp.DialStreamOne(cfg, transport)
}
case "stream-up":
v.dialXHTTPConn = func() (net.Conn, error) {
transport := makeTransport()
downloadTransport := transport
if makeDownloadTransport != nil {
downloadTransport = makeDownloadTransport()
}
return xhttp.DialStreamUp(cfg, transport, downloadTransport)
}
case "packet-up":
v.dialXHTTPConn = func() (net.Conn, error) {
transport := makeTransport()
downloadTransport := transport
if makeDownloadTransport != nil {
downloadTransport = makeDownloadTransport()
}
return xhttp.DialPacketUp(cfg, transport, downloadTransport)
}
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", mode)
v.xhttpClient, err = xhttp.NewClient(cfg, makeTransport, makeDownloadTransport, v.realityConfig != nil)
if err != nil {
return nil, err
}
}
+36 -55
View File
@@ -145,24 +145,9 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = mihomoVMess.StreamWebsocketConn(ctx, c, wsOpts)
case "http":
// readability first, so just copy default TLS logic
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts)
if err != nil {
return nil, err
}
c, err = v.streamTLSConn(ctx, c, false)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(v.addr)
@@ -175,23 +160,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c = mihomoVMess.StreamHTTPConn(c, httpOpts)
case "h2":
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
NextProtos: []string{"h2"},
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = mihomoVMess.StreamTLSConn(ctx, c, &tlsOpts)
c, err = v.streamTLSConn(ctx, c, true)
if err != nil {
return nil, err
}
@@ -205,27 +174,9 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
case "grpc":
break // already handle in gun transport
default:
// default tcp network
// handle TLS
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts)
}
c, err = v.streamTLSConn(ctx, c, false)
}
if err != nil {
@@ -290,6 +241,36 @@ func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.M
return
}
func (v *Vmess) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) {
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := mihomoVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
Certificate: v.option.Certificate,
PrivateKey: v.option.PrivateKey,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if isH2 {
tlsOpts.NextProtos = []string{"h2"}
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
return mihomoVMess.StreamTLSConn(ctx, conn, &tlsOpts)
}
return conn, nil
}
func (v *Vmess) dialContext(ctx context.Context) (c net.Conn, err error) {
switch v.option.Network {
case "grpc": // gun transport
+4
View File
@@ -1627,6 +1627,10 @@ listeners:
flow: xtls-rprx-vision
# ws-path: "/" # 如果不为空则开启 websocket 传输层
# grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层
# xhttp-config: # 如果不为空则开启 xhttp 传输层
# path: "/"
# host: ""
# mode: auto # Available: "stream-one", "stream-up" or "packet-up"
# -------------------------
# vless encryption服务端配置:
# (原生外观 / 只 XOR 公钥 / 全随机数。1-RTT 每次下发随机 300 到 600 秒的 ticket 以便 0-RTT 复用 / 只允许 1-RTT
+3
View File
@@ -158,6 +158,9 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis
if tunnelName, err := getTunnelName(int32(options.FileDescriptor)); err == nil {
tunName = tunnelName // sing-tun must have the truth tun interface name even it from a fd
forwarderBindInterface = true
log.Debugln("[TUN] use tun name %s for fd %d", tunnelName, options.FileDescriptor)
} else {
log.Warnln("[TUN] get tun name failed for fd %d, fallback to use tun interface name %s", options.FileDescriptor, tunName)
}
}
routeAddress := options.RouteAddress
+82 -24
View File
@@ -20,6 +20,8 @@ import (
type DialRawFunc func(ctx context.Context) (net.Conn, error)
type WrapTLSFunc func(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error)
type TransportMaker func() http.RoundTripper
type PacketUpWriter struct {
ctx context.Context
cfg *Config
@@ -88,26 +90,72 @@ func NewTransport(dialRaw DialRawFunc, wrapTLS WrapTLSFunc) http.RoundTripper {
}
}
func DialStreamOne(cfg *Config, transport http.RoundTripper) (net.Conn, error) {
type Client struct {
mode string
cfg *Config
makeTransport TransportMaker
makeDownloadTransport TransportMaker
ctx context.Context
cancel context.CancelFunc
}
func NewClient(cfg *Config, makeTransport TransportMaker, makeDownloadTransport TransportMaker, hasReality bool) (*Client, error) {
mode := cfg.EffectiveMode(hasReality)
switch mode {
case "stream-one", "stream-up", "packet-up":
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", mode)
}
ctx, cancel := context.WithCancel(context.Background())
return &Client{
mode: mode,
cfg: cfg,
makeTransport: makeTransport,
makeDownloadTransport: makeDownloadTransport,
ctx: ctx,
cancel: cancel,
}, nil
}
func (c *Client) Dial() (net.Conn, error) {
switch c.mode {
case "stream-one":
return c.DialStreamOne()
case "stream-up":
return c.DialStreamUp()
case "packet-up":
return c.DialPacketUp()
default:
return nil, fmt.Errorf("xhttp mode %s is not implemented yet", c.mode)
}
}
func (c *Client) Close() error {
c.cancel()
return nil
}
func (c *Client) DialStreamOne() (net.Conn, error) {
transport := c.makeTransport()
requestURL := url.URL{
Scheme: "https",
Host: cfg.Host,
Path: cfg.NormalizedPath(),
Host: c.cfg.Host,
Path: c.cfg.NormalizedPath(),
}
pr, pw := io.Pipe()
ctx := context.Background()
conn := &Conn{writer: pw}
req, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, ctx), http.MethodPost, requestURL.String(), pr)
req, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, c.ctx), http.MethodPost, requestURL.String(), pr)
if err != nil {
_ = pr.Close()
_ = pw.Close()
return nil, err
}
req.Host = cfg.Host
req.Host = c.cfg.Host
if err := cfg.FillStreamRequest(req, ""); err != nil {
if err := c.cfg.FillStreamRequest(req, ""); err != nil {
_ = pr.Close()
_ = pw.Close()
return nil, err
@@ -136,16 +184,22 @@ func DialStreamOne(cfg *Config, transport http.RoundTripper) (net.Conn, error) {
return conn, nil
}
func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransport http.RoundTripper) (net.Conn, error) {
downloadCfg := cfg
if ds := cfg.DownloadConfig; ds != nil {
func (c *Client) DialStreamUp() (net.Conn, error) {
uploadTransport := c.makeTransport()
downloadTransport := uploadTransport
if c.makeDownloadTransport != nil {
downloadTransport = c.makeDownloadTransport()
}
downloadCfg := c.cfg
if ds := c.cfg.DownloadConfig; ds != nil {
downloadCfg = ds
}
streamURL := url.URL{
Scheme: "https",
Host: cfg.Host,
Path: cfg.NormalizedPath(),
Host: c.cfg.Host,
Path: c.cfg.NormalizedPath(),
}
downloadURL := url.URL{
@@ -155,13 +209,12 @@ func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
}
pr, pw := io.Pipe()
ctx := context.Background()
conn := &Conn{writer: pw}
sessionID := newSessionID()
downloadReq, err := http.NewRequestWithContext(
httputils.NewAddrContext(&conn.NetAddr, ctx),
httputils.NewAddrContext(&conn.NetAddr, c.ctx),
http.MethodGet,
downloadURL.String(),
nil,
@@ -201,7 +254,7 @@ func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
}
uploadReq, err := http.NewRequestWithContext(
ctx,
c.ctx,
http.MethodPost,
streamURL.String(),
pr,
@@ -217,7 +270,7 @@ func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
return nil, err
}
if err := cfg.FillStreamRequest(uploadReq, sessionID); err != nil {
if err := c.cfg.FillStreamRequest(uploadReq, sessionID); err != nil {
_ = downloadResp.Body.Close()
_ = pr.Close()
_ = pw.Close()
@@ -227,7 +280,7 @@ func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
}
return nil, err
}
uploadReq.Host = cfg.Host
uploadReq.Host = c.cfg.Host
go func() {
resp, err := uploadTransport.RoundTrip(uploadReq)
@@ -255,9 +308,15 @@ func DialStreamUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
return conn, nil
}
func DialPacketUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransport http.RoundTripper) (net.Conn, error) {
downloadCfg := cfg
if ds := cfg.DownloadConfig; ds != nil {
func (c *Client) DialPacketUp() (net.Conn, error) {
uploadTransport := c.makeTransport()
downloadTransport := uploadTransport
if c.makeDownloadTransport != nil {
downloadTransport = c.makeDownloadTransport()
}
downloadCfg := c.cfg
if ds := c.cfg.DownloadConfig; ds != nil {
downloadCfg = ds
}
sessionID := newSessionID()
@@ -268,17 +327,16 @@ func DialPacketUp(cfg *Config, uploadTransport http.RoundTripper, downloadTransp
Path: downloadCfg.NormalizedPath(),
}
ctx := context.Background()
writer := &PacketUpWriter{
ctx: ctx,
cfg: cfg,
ctx: c.ctx,
cfg: c.cfg,
sessionID: sessionID,
transport: uploadTransport,
seq: 0,
}
conn := &Conn{writer: writer}
downloadReq, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, ctx), http.MethodGet, downloadURL.String(), nil)
downloadReq, err := http.NewRequestWithContext(httputils.NewAddrContext(&conn.NetAddr, c.ctx), http.MethodGet, downloadURL.String(), nil)
if err != nil {
return nil, err
}
@@ -746,6 +746,7 @@ function gen_config(var)
local fakedns = nil
local routing = nil
local observatory = nil
local burstObservatory = nil
local strategy = nil
local inbounds = {}
local outbounds = {}
@@ -1003,19 +1004,31 @@ function gen_config(var)
fallbackTag = fallback_node_tag,
strategy = strategy
})
if not observatory and (_node.balancingStrategy == "leastPing" or _node.balancingStrategy == "leastLoad" or fallback_node_tag) then
if _node.balancingStrategy == "leastPing" or _node.balancingStrategy == "leastLoad" or fallback_node_tag then
local t = api.format_go_time(_node.probeInterval)
if t == "0s" then
t = "60s"
elseif not t:find("[hm]") and tonumber(t:match("%d+")) < 10 then
t = "10s"
end
observatory = {
subjectSelector = { "blc-" },
probeUrl = _node.useCustomProbeUrl and _node.probeUrl or "https://www.google.com/generate_204",
probeInterval = t,
enableConcurrency = true
}
if _node.balancingStrategy == "leastLoad" then
burstObservatory = burstObservatory or {
subjectSelector = { "blc-" },
pingConfig = {
destination = _node.useCustomProbeUrl and _node.probeUrl or nil,
interval = t,
sampling = 3,
timeout = "5s"
}
}
else
observatory = observatory or {
subjectSelector = { "blc-" },
probeUrl = _node.useCustomProbeUrl and _node.probeUrl or nil,
probeInterval = t,
enableConcurrency = true
}
end
end
local loopback_outbound = gen_loopback(loopback_tag, loopback_dst)
local inbound_tag = loopback_outbound.settings.inboundTag
@@ -1670,7 +1683,8 @@ function gen_config(var)
-- 传出连接
outbounds = outbounds,
-- 连接观测
observatory = observatory,
observatory = (not burstObservatory) and observatory or nil,
burstObservatory = burstObservatory,
-- 路由
routing = routing,
-- 本地策略
+15 -22
View File
@@ -140,7 +140,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -151,7 +151,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -883,7 +883,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -996,7 +996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -1499,9 +1499,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
[[package]]
name = "hyper"
version = "1.8.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [
"atomic-waker",
"bytes",
@@ -1514,7 +1514,6 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
"want",
@@ -1554,7 +1553,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.3",
"socket2 0.5.10",
"system-configuration",
"tokio",
"tower-service",
@@ -2246,7 +2245,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -2431,12 +2430,6 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.4"
@@ -2587,7 +2580,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.3",
"socket2 0.5.10",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -2625,7 +2618,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.3",
"socket2 0.5.10",
"tracing",
"windows-sys 0.60.2",
]
@@ -2904,7 +2897,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -2963,7 +2956,7 @@ dependencies = [
"security-framework 3.5.1",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -3463,7 +3456,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -3609,7 +3602,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -4310,7 +4303,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]
+1 -1
View File
@@ -37,7 +37,7 @@ require (
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.3
github.com/sagernet/sing v0.8.4
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212
github.com/sagernet/sing-shadowsocks v0.2.8
+2 -2
View File
@@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.3 h1:zGMy9M1deBPEew9pCYIUHKeE+/lDQ5A2CBqjBjjzqkA=
github.com/sagernet/sing v0.8.3/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.8.4 h1:Fj+jlY3F8vhcRfz/G/P3Dwcs5wqnmyNPT7u1RVVmjFI=
github.com/sagernet/sing v0.8.4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 h1:7mFOUqy+DyOj7qKGd1X54UMXbnbJiiMileK/tn17xYc=
+1 -1
View File
@@ -7,7 +7,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.3.6
PKG_VERSION:=26.4.1
PKG_RELEASE:=1
PKG_PO_VERSION:=$(PKG_VERSION)
@@ -133,9 +133,11 @@ o.validate = function(self, value)
return value:gsub("%s+", ""):gsub("%z", "")
end
o = s:option(Flag, "allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o = s:option(Flag, "allowInsecure", translate("allowInsecure"))
o.default = "0"
o.rmempty = false
o.description = translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.") .. "<br>" ..
translate("Used when the node link does not include this parameter.")
o = s:option(ListValue, "filter_keyword_mode", translate("Filter keyword Mode"))
o.default = "5"
@@ -746,6 +746,7 @@ function gen_config(var)
local fakedns = nil
local routing = nil
local observatory = nil
local burstObservatory = nil
local strategy = nil
local inbounds = {}
local outbounds = {}
@@ -1003,19 +1004,31 @@ function gen_config(var)
fallbackTag = fallback_node_tag,
strategy = strategy
})
if not observatory and (_node.balancingStrategy == "leastPing" or _node.balancingStrategy == "leastLoad" or fallback_node_tag) then
if _node.balancingStrategy == "leastPing" or _node.balancingStrategy == "leastLoad" or fallback_node_tag then
local t = api.format_go_time(_node.probeInterval)
if t == "0s" then
t = "60s"
elseif not t:find("[hm]") and tonumber(t:match("%d+")) < 10 then
t = "10s"
end
observatory = {
subjectSelector = { "blc-" },
probeUrl = _node.useCustomProbeUrl and _node.probeUrl or "https://www.google.com/generate_204",
probeInterval = t,
enableConcurrency = true
}
if _node.balancingStrategy == "leastLoad" then
burstObservatory = burstObservatory or {
subjectSelector = { "blc-" },
pingConfig = {
destination = _node.useCustomProbeUrl and _node.probeUrl or nil,
interval = t,
sampling = 3,
timeout = "5s"
}
}
else
observatory = observatory or {
subjectSelector = { "blc-" },
probeUrl = _node.useCustomProbeUrl and _node.probeUrl or nil,
probeInterval = t,
enableConcurrency = true
}
end
end
local loopback_outbound = gen_loopback(loopback_tag, loopback_dst)
local inbound_tag = loopback_outbound.settings.inboundTag
@@ -1670,7 +1683,8 @@ function gen_config(var)
-- 传出连接
outbounds = outbounds,
-- 连接观测
observatory = observatory,
observatory = (not burstObservatory) and observatory or nil,
burstObservatory = burstObservatory,
-- 路由
routing = routing,
-- 本地策略
@@ -1063,10 +1063,10 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'reality', false);
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || '');
opt.set(dom_prefix + 'tls_allowInsecure', true);
if ((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0') {
opt.set(dom_prefix + 'tls_allowInsecure', false);
}
opt.set(
dom_prefix + 'tls_allowInsecure',
!((queryParam.allowinsecure ?? '0') === '0' && (queryParam.allowInsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0')
);
if (queryParam.fp && queryParam.fp.trim() != "") {
opt.set(dom_prefix + 'utls', true);
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
@@ -1237,7 +1237,7 @@ local current_node = map:get(section)
var params;
for (var i = 0; i < queryArray.length; i++) {
params = queryArray[i].split('=');
queryParam[decodeURIComponent(params[0]).toLowerCase()] = decodeURIComponent(params[1] || '');
queryParam[decodeURIComponent(params[0])] = decodeURIComponent(params[1] || '');
}
}
opt.set(dom_prefix + 'address', unbracketIP(m.hostname));
@@ -1317,11 +1317,10 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'tls_CertSha', queryParam.pcs || '');
opt.set(dom_prefix + 'tls_CertByName', queryParam.vcn || '');
}
opt.set(dom_prefix + 'tls_allowInsecure', true);
if ((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0') {
opt.set(dom_prefix + 'tls_allowInsecure', false);
}
opt.set(
dom_prefix + 'tls_allowInsecure',
!((queryParam.allowinsecure ?? '0') === '0' && (queryParam.allowInsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0')
);
opt.set(dom_prefix + 'tcp_fast_open', queryParam.tfo);
opt.set(dom_prefix + 'use_finalmask', !!queryParam.fm);
opt.set(dom_prefix + 'finalmask', queryParam.fm || "");
@@ -1353,11 +1352,11 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'uuid', ssm.id);
opt.set(dom_prefix + 'tls', ssm.tls === "tls");
if (ssm.tls === "tls") {
var tls_serverName = ssm.host;
if (ssm.sni) {
tls_serverName = ssm.sni
}
opt.set(dom_prefix + 'tls_serverName', tls_serverName);
opt.set(dom_prefix + 'tls_serverName', ssm.sni || ssm.host);
opt.set(
dom_prefix + 'tls_allowInsecure',
!((ssm.allowinsecure ?? '0') === '0' && (ssm.allowInsecure ?? '0') === '0' && (ssm.insecure ?? '0') === '0')
);
}
ssm.net = ssm.net.toLowerCase();
if (ssm.net === "kcp" || ssm.net === "mkcp")
@@ -1469,10 +1468,10 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'reality', false);
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || '');
opt.set(dom_prefix + 'tls_allowInsecure', true);
if ((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0') {
opt.set(dom_prefix + 'tls_allowInsecure', false);
}
opt.set(
dom_prefix + 'tls_allowInsecure',
!((queryParam.allowinsecure ?? '0') === '0' && (queryParam.allowInsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0')
);
if (queryParam.fp && queryParam.fp.trim() != "") {
opt.set(dom_prefix + 'utls', true);
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
@@ -1630,9 +1629,10 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'port', m.port || "443");
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || "");
if (queryParam.insecure && queryParam.insecure == "1") {
opt.set(dom_prefix + 'tls_allowInsecure', true);
}
opt.set(
dom_prefix + 'tls_allowInsecure',
!((queryParam.allowinsecure ?? '0') === '0' && (queryParam.allowInsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0')
);
opt.set(dom_prefix + 'tls_CertSha', queryParam.pcs || '');
opt.set(dom_prefix + 'tls_CertByName', queryParam.vcn || '');
if (m.hash) {
@@ -1672,7 +1672,7 @@ local current_node = map:get(section)
var params;
for (i = 0; i < queryArray.length; i++) {
params = queryArray[i].split('=');
queryParam[decodeURIComponent(params[0])] = decodeURIComponent(params[1] || '');
queryParam[decodeURIComponent(params[0]).toLowerCase()] = decodeURIComponent(params[1] || '');
}
}
opt.set(dom_prefix + 'tuic_congestion_control', queryParam.congestion_control || 'cubic');
@@ -1680,10 +1680,10 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'tuic_alpn', queryParam.alpn || 'default');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || '');
opt.set(dom_prefix + 'tls_disable_sni', queryParam.disable_sni === "1");
opt.set(dom_prefix + 'tls_allowInsecure', true);
if ((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0') {
opt.set(dom_prefix + 'tls_allowInsecure', false);
}
opt.set(
dom_prefix + 'tls_allowInsecure',
!((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0' && (queryParam.allow_insecure ?? '0') === '0')
);
if (hash) {
opt.set('remarks', decodeURIComponent(hash.substr(1)));
}
@@ -1711,7 +1711,7 @@ local current_node = map:get(section)
var params;
for (i = 0; i < queryArray.length; i++) {
params = queryArray[i].split('=');
queryParam[decodeURIComponent(params[0])] = decodeURIComponent(params[1] || '');
queryParam[decodeURIComponent(params[0]).toLowerCase()] = decodeURIComponent(params[1] || '');
}
}
if ((!queryParam.security || queryParam.security == "") && queryParam.sni && queryParam.sni != "") {
@@ -1723,10 +1723,7 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'reality', false);
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || '');
opt.set(dom_prefix + 'tls_allowInsecure', true);
if ((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0') {
opt.set(dom_prefix + 'tls_allowInsecure', false);
}
opt.set(dom_prefix + 'tls_allowInsecure', !((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0'));
if (queryParam.fp && queryParam.fp.trim() != "") {
opt.set(dom_prefix + 'utls', true);
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
@@ -1228,6 +1228,9 @@ msgstr "使用全局配置"
msgid "User-Agent"
msgstr "用户代理(User-Agent)"
msgid "Used when the node link does not include this parameter."
msgstr "当节点链接未包含该参数时,将使用此设置。"
msgid "Add"
msgstr "添加"
@@ -602,14 +602,10 @@ local function processData(szType, content, add_mode, group)
if info.tls == "tls" or info.tls == "1" then
result.tls = "1"
result.tls_serverName = (info.sni and info.sni ~= "") and info.sni or info.host
info.allowinsecure = info.allowinsecure or info.insecure
if info.allowinsecure and (info.allowinsecure == "1" or info.allowinsecure == "0") then
result.tls_allowInsecure = info.allowinsecure
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
result.tls_CertSha = info.pcs
result.tls_CertByName = info.vcn
local insecure = info.allowinsecure or info.allowInsecure or info.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
else
result.tls = "0"
end
@@ -894,12 +890,8 @@ local function processData(szType, content, add_mode, group)
result.reality_mldsa65Verify = params.pqv or nil
end
end
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure and (params.allowinsecure == "1" or params.allowinsecure == "0") then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.uot = params.udp
elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then
result.error_msg = "请更换 Xray 或 Sing-Box 来支持 SS 更多的传输方式。"
@@ -1003,18 +995,8 @@ local function processData(szType, content, add_mode, group)
result.tls_serverName = params.peer or params.sni or ""
result.tls_CertSha = params.pcs
result.tls_CertByName = params.vcn
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure then
if params.allowinsecure == "1" or params.allowinsecure == "0" then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = string.lower(params.allowinsecure) == "true" and "1" or "0"
end
--log(result.remarks .. ' 使用节点AllowInsecure设定: '.. result.tls_allowInsecure)
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
if not params.type then params.type = "tcp" end
params.type = string.lower(params.type)
@@ -1082,8 +1064,7 @@ local function processData(szType, content, add_mode, group)
result.quic_security = params.quicSecurity or "none"
end
if params.type == 'grpc' then
if params.path then result.grpc_serviceName = params.path end
if params.serviceName then result.grpc_serviceName = params.serviceName end
result.grpc_serviceName = params.serviceName or params.path
result.grpc_mode = params.mode or "gun"
end
if params.type == 'xhttp' then
@@ -1280,17 +1261,11 @@ local function processData(szType, content, add_mode, group)
result.use_mldsa65Verify = (params.pqv and params.pqv ~= "") and "1" or nil
result.reality_mldsa65Verify = params.pqv or nil
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
end
result.port = port
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure and (params.allowinsecure == "1" or params.allowinsecure == "0") then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
result.tcp_fast_open = params.tfo
result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil
result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil
@@ -1343,13 +1318,8 @@ local function processData(szType, content, add_mode, group)
result.hysteria_auth_type = "string"
result.hysteria_auth_password = params.auth
result.tls_serverName = params.peer or params.sni or ""
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure and (params.allowinsecure == "1" or params.allowinsecure == "0") then
result.tls_allowInsecure = params.allowinsecure
--log(result.remarks ..' 使用节点AllowInsecure设定: '.. result.tls_allowInsecure)
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.alpn = params.alpn
result.hysteria_up_mbps = params.upmbps
result.hysteria_down_mbps = params.downmbps
@@ -1394,13 +1364,8 @@ local function processData(szType, content, add_mode, group)
result.tls_serverName = params.sni
result.tls_CertSha = params.pcs
result.tls_CertByName = params.vcn
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure and (params.allowinsecure == "1" or params.allowinsecure == "0") then
result.tls_allowInsecure = params.allowinsecure
--log(result.remarks ..' 使用节点AllowInsecure设定: '.. result.tls_allowInsecure)
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
local insecure = params.allowinsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.hysteria2_tls_pinSHA256 = params.pinSHA256
result.hysteria2_hop = params.mport
@@ -1479,17 +1444,8 @@ local function processData(szType, content, add_mode, group)
result.tuic_alpn = params.alpn or "default"
result.tuic_congestion_control = params.congestion_control or "cubic"
result.tuic_udp_relay_mode = params.udp_relay_mode or "native"
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure then
if params.allowinsecure == "1" or params.allowinsecure == "0" then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = string.lower(params.allowinsecure) == "true" and "1" or "0"
end
--log(result.remarks .. ' 使用节点AllowInsecure设定: '.. result.tls_allowInsecure)
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
local insecure = params.allowinsecure or params.insecure or params.allow_insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
elseif szType == "anytls" then
if has_singbox then
result.type = 'sing-box'
@@ -1517,7 +1473,7 @@ local function processData(szType, content, add_mode, group)
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1)] = UrlDecode(v:sub(s + 1))
params[v:sub(1, s - 1):lower()] = UrlDecode(v:sub(s + 1))
end
end
-- [2001:4860:4860::8888]:443
@@ -1552,18 +1508,8 @@ local function processData(szType, content, add_mode, group)
end
end
result.port = port
params.allowinsecure = params.allowinsecure or params.insecure
if params.allowinsecure and (params.allowinsecure == "1" or params.allowinsecure == "0") then
result.tls_allowInsecure = params.allowinsecure
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
local singbox_version = api.get_app_version("sing-box")
local version_ge_1_12 = api.compare_versions(singbox_version:match("[^v]+"), ">=", "1.12.0")
if not has_singbox or not version_ge_1_12 then
log("跳过节点:" .. result.remarks ..",因 " .. szType .. " 类型的节点需要 Sing-Box 1.12 以上版本支持。")
return nil
end
local insecure = params.allowinsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
end
elseif szType == 'naive+https' or szType == 'naive+quic' then
if has_singbox then
+1 -1
View File
@@ -161,7 +161,7 @@
}
-%}
table inet nikki {
table inet momo {
set dns_hijack_nfproto {
type nf_proto
flags interval
+4 -4
View File
@@ -1,15 +1,15 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=nikki
PKG_VERSION:=2026.03.10
PKG_RELEASE:=3
PKG_VERSION:=2026.04.01
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
PKG_SOURCE_VERSION:=v1.19.21
PKG_MIRROR_HASH:=f69a065d90dd5e565cb1e4b81d5c2074d6f97afca9a793fb5c09c2a0c2838369
PKG_SOURCE_VERSION:=v1.19.22
PKG_MIRROR_HASH:=c340a8b5c40fd59dfdb50cf48a5deed47a40a20cc5dc2fa306662a0dc3045b53
PKG_LICENSE:=GPL3.0+
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com>
+113 -113
View File
@@ -142,7 +142,7 @@
<value>Не удалось получить конфигурацию по умолчанию</value>
</data>
<data name="FailedImportedCustomServer" xml:space="preserve">
<value>Не удалось импортировать сервер пользовательской конфигурации</value>
<value>Не удалось импортировать пользовательскую конфигурацию</value>
</data>
<data name="FailedReadConfiguration" xml:space="preserve">
<value>Не удалось прочитать файл конфигурации</value>
@@ -211,13 +211,13 @@
<value>Не удалось импортировать подписку</value>
</data>
<data name="MsgGetSubscriptionSuccessfully" xml:space="preserve">
<value>Содержимое подписки успешно импортировано</value>
<value>Содержимое подписки успешно получено</value>
</data>
<data name="MsgNoValidSubscription" xml:space="preserve">
<value>Нет установленных подписок</value>
</data>
<data name="MsgParsingSuccessfully" xml:space="preserve">
<value>Парсинг {0} прошел успешно</value>
<value>Успешно обработано: {0}</value>
</data>
<data name="MsgStartGettingSubscriptions" xml:space="preserve">
<value>Начинаю получать подписки</value>
@@ -244,7 +244,7 @@
<value>Успешное обновление ядра! Перезапуск службы...</value>
</data>
<data name="NonvmessOrssProtocol" xml:space="preserve">
<value>Не является протоколом VMess или Shadowsocks</value>
<value>Протокол не VMess и не Shadowsocks</value>
</data>
<data name="NotFoundCore" xml:space="preserve">
<value>Файл ядра ({1}) не найден в папке {0}. Скачайте по адресу {2} и поместите его туда</value>
@@ -253,7 +253,7 @@
<value>Сканирование завершено, не найден корректный QR код</value>
</data>
<data name="OperationFailed" xml:space="preserve">
<value>Операция безуспешна, проверьте и попробуйте ещё раз</value>
<value>Операция не удалась, проверьте и повторите попытку</value>
</data>
<data name="PleaseFillRemarks" xml:space="preserve">
<value>Введите примечания</value>
@@ -268,7 +268,7 @@
<value>Сначала выберите сервер</value>
</data>
<data name="RemoveDuplicateServerResult" xml:space="preserve">
<value>Удаление дублей завершено. Старая: {0}, Новая: {1}</value>
<value>Дедупликация конфигураций завершена. Было: {0}, Стало: {1}.</value>
</data>
<data name="RemoveServer" xml:space="preserve">
<value>Вы уверены, что хотите удалить сервер?</value>
@@ -283,16 +283,16 @@
<value>Конфигурация выполнена успешно {0}</value>
</data>
<data name="SuccessfullyImportedCustomServer" xml:space="preserve">
<value>Пользовательская конфигурация сервера успешно импортирована</value>
<value>Пользовательская конфигурация успешно импортирована</value>
</data>
<data name="SuccessfullyImportedServerViaClipboard" xml:space="preserve">
<value>{0} серверов импортировано из буфера обмена</value>
<value>Из буфера обмена импортировано конфигураций: {0}</value>
</data>
<data name="SuccessfullyImportedServerViaScan" xml:space="preserve">
<value>Сканирование URL-адреса импорта прошло успешно</value>
<value>Ссылка общего доступа успешно отсканирована и импортирована</value>
</data>
<data name="TestMeOutput" xml:space="preserve">
<value>Задержка текущего сервера: {0} мс, {1}</value>
<value>Задержка: {0} мс, {1}</value>
</data>
<data name="OperationSuccess" xml:space="preserve">
<value>Операция успешна</value>
@@ -334,7 +334,7 @@
<value>Введите корректный пользовательский DNS</value>
</data>
<data name="TransportPathTip1" xml:space="preserve">
<value>*WebSocket-путь</value>
<value>*Путь ws/http upgrade/xhttp</value>
</data>
<data name="TransportPathTip2" xml:space="preserve">
<value>*HTTP2-путь</value>
@@ -349,7 +349,7 @@
<value>*http-хосты, разделённые запятыми (,)</value>
</data>
<data name="TransportRequestHostTip2" xml:space="preserve">
<value>*WebSocket-хост</value>
<value>*Хост ws/http upgrade/xhttp</value>
</data>
<data name="TransportRequestHostTip3" xml:space="preserve">
<value>*HTTP2-хосты, разделённые запятыми (,)</value>
@@ -382,7 +382,7 @@
<value>Глобальная горячая клавиша {0} зарегистрирована успешно</value>
</data>
<data name="AllGroupServers" xml:space="preserve">
<value>Все серверы</value>
<value>Все</value>
</data>
<data name="FillServerAddressCustom" xml:space="preserve">
<value>Выберите файл конфигурации сервера для импорта</value>
@@ -397,7 +397,7 @@
<value>Локальный</value>
</data>
<data name="MsgServerTitle" xml:space="preserve">
<value>Фильтр серверов</value>
<value>Фильтр, нажмите Enter для выполнения</value>
</data>
<data name="menuCheckUpdate" xml:space="preserve">
<value>Проверить обновления</value>
@@ -409,19 +409,19 @@
<value>Выход</value>
</data>
<data name="menuGlobalHotkeySetting" xml:space="preserve">
<value>Глобальная настройка горячих клавиш</value>
<value>Настройка глобальных горячих клавиш</value>
</data>
<data name="menuHelp" xml:space="preserve">
<value>Помощь</value>
</data>
<data name="menuOptionSetting" xml:space="preserve">
<value>Настройка параметров</value>
<value>Настройки</value>
</data>
<data name="menuPromotion" xml:space="preserve">
<value>Содействие</value>
<value>Продвижение</value>
</data>
<data name="menuReload" xml:space="preserve">
<value>Перезагрузка</value>
<value>Перезагрузить</value>
</data>
<data name="menuRoutingSetting" xml:space="preserve">
<value>Настройки маршрутизации</value>
@@ -436,13 +436,13 @@
<value>Обновить текущую подписку без прокси</value>
</data>
<data name="menuSubGroupUpdateViaProxy" xml:space="preserve">
<value>Обновить подписку через прокси</value>
<value>Обновить текущую подписку через прокси</value>
</data>
<data name="menuSubscription" xml:space="preserve">
<value>Группа подписки</value>
</data>
<data name="menuSubSetting" xml:space="preserve">
<value>Настройки группы подписки</value>
<value>Настройки групп подписки</value>
</data>
<data name="menuSubUpdate" xml:space="preserve">
<value>Обновить подписку без прокси</value>
@@ -472,46 +472,46 @@
<value>Язык (требуется перезапуск)</value>
</data>
<data name="menuAddServerViaClipboard" xml:space="preserve">
<value>Импорт массива URL из буфера обмена</value>
<value>Импорт ссылок общего доступа из буфера обмена</value>
</data>
<data name="menuAddServerViaScan" xml:space="preserve">
<value>Сканировать QR-код с экрана</value>
<value>Сканировать QR-код на экране</value>
</data>
<data name="menuCopyServer" xml:space="preserve">
<value>Клонировать выбранный сервер</value>
<value>Клонировать выбранное</value>
</data>
<data name="menuRemoveDuplicateServer" xml:space="preserve">
<value>Удалить дубликаты серверов</value>
<value>Удалить дубликаты</value>
</data>
<data name="menuRemoveServer" xml:space="preserve">
<value>Удалить выбранные серверы</value>
<value>Удалить выбранное</value>
</data>
<data name="menuSetDefaultServer" xml:space="preserve">
<value>Установить как активный сервер</value>
<value>Сделать активным</value>
</data>
<data name="menuClearServerStatistics" xml:space="preserve">
<value>Очистить всю статистику</value>
<value>Очистить статистику всех сервисов</value>
</data>
<data name="menuRealPingServer" xml:space="preserve">
<value>Тест на реальную задержку сервера</value>
<value>Тест реальной задержки</value>
</data>
<data name="menuSortServerResult" xml:space="preserve">
<value>Сортировать по результату теста</value>
<value>Сортировка по результату теста</value>
</data>
<data name="menuSpeedServer" xml:space="preserve">
<value>Тест на скорость загрузки сервера</value>
<value>Тест скорости загрузки</value>
</data>
<data name="menuTcpingServer" xml:space="preserve">
<value>Тест задержки с tcping</value>
<value>Тест tcping</value>
</data>
<data name="menuExport2ClientConfig" xml:space="preserve">
<value>Экспортировать выбранный сервер для клиента</value>
<value>Экспортировать выбранное как полную конфигурацию</value>
</data>
<data name="menuExport2ShareUrl" xml:space="preserve">
<value>Экспорт URL-адресов общего доступа в буфер обмена</value>
<value>Копировать ссылку общего доступа в буфер обмена</value>
</data>
<data name="menuAddCustomServer" xml:space="preserve">
<value>Добавить сервер пользовательской конфигурации</value>
<value>Добавить пользовательскую конфигурацию</value>
</data>
<data name="menuAddShadowsocksServer" xml:space="preserve">
<value>Добавить сервер [Shadowsocks]</value>
@@ -556,13 +556,13 @@
<value>Поделиться</value>
</data>
<data name="LvEnabled" xml:space="preserve">
<value>Включены обновления</value>
<value>Включить обновление</value>
</data>
<data name="LvSort" xml:space="preserve">
<value>Сортировка</value>
</data>
<data name="LvUserAgent" xml:space="preserve">
<value>Заголовок User-Agent</value>
<value>User-Agent</value>
</data>
<data name="TbCancel" xml:space="preserve">
<value>Отмена</value>
@@ -595,7 +595,7 @@
<value>UUID (id)</value>
</data>
<data name="TbNetwork" xml:space="preserve">
<value>Транспортный протокол сети</value>
<value>Транспортный протокол (network)</value>
</data>
<data name="TbPath" xml:space="preserve">
<value>Путь</value>
@@ -604,13 +604,13 @@
<value>Порт</value>
</data>
<data name="TbRemarks" xml:space="preserve">
<value>Примечание</value>
<value>Псевдоним (remarks)</value>
</data>
<data name="TbRequestHost" xml:space="preserve">
<value>Маскирующий домен (хост)</value>
<value>Камуфляжный домен (host)</value>
</data>
<data name="TbSecurity" xml:space="preserve">
<value>Метод шифрования</value>
<value>Метод шифрования (security)</value>
</data>
<data name="TbSNI" xml:space="preserve">
<value>SNI</value>
@@ -619,13 +619,13 @@
<value>TLS</value>
</data>
<data name="TipNetwork" xml:space="preserve">
<value>*По-умолчанию TCP</value>
<value>*По умолчанию TCP</value>
</data>
<data name="TbCoreType" xml:space="preserve">
<value>Ядро</value>
</data>
<data name="TbFlow5" xml:space="preserve">
<value>Поток</value>
<value>Управление потоком (Flow)</value>
</data>
<data name="TbGUID" xml:space="preserve">
<value>Генерировать</value>
@@ -634,7 +634,7 @@
<value>Пароль</value>
</data>
<data name="TbId4" xml:space="preserve">
<value>Пароль(Необязательно)</value>
<value>Парольеобязательно)</value>
</data>
<data name="TbId5" xml:space="preserve">
<value>UUID(id)</value>
@@ -643,10 +643,10 @@
<value>Шифрование</value>
</data>
<data name="TbSecurity4" xml:space="preserve">
<value>Пользователь(Необязательно)</value>
<value>Пользовательеобязательно)</value>
</data>
<data name="TbSecurity5" xml:space="preserve">
<value>Шифрования</value>
<value>Шифрование</value>
</data>
<data name="TbPreSocksPort" xml:space="preserve">
<value>Порт SOCKS</value>
@@ -655,7 +655,7 @@
<value>* После установки этого значения служба SOCKS будет запущена с использованием Xray/sing-box(TUN) для обеспечения таких функций, как отображение скорости</value>
</data>
<data name="TbBrowse" xml:space="preserve">
<value>Просмотр</value>
<value>Обзор</value>
</data>
<data name="TbEdit" xml:space="preserve">
<value>Редактировать</value>
@@ -667,7 +667,7 @@
<value>Разрешить подключения из локальной сети</value>
</data>
<data name="TbSettingsAutoHideStartup" xml:space="preserve">
<value>Автоскрытие при автозапуске</value>
<value>Автоматически скрывать при запуске</value>
</data>
<data name="TbSettingsAutoUpdateInterval" xml:space="preserve">
<value>Интервал автоматического обновления Geo в часах</value>
@@ -676,7 +676,7 @@
<value>Ядро: базовые настройки</value>
</data>
<data name="TbCustomDnsRay" xml:space="preserve">
<value>V2ray Custom DNS</value>
<value>Пользовательский DNS для V2ray</value>
</data>
<data name="TbSettingsCoreKcp" xml:space="preserve">
<value>Ядро: настройки KCP</value>
@@ -700,10 +700,10 @@
<value>Исключение</value>
</data>
<data name="TbSettingsExceptionTip" xml:space="preserve">
<value>Исключение. Не используйте прокси-сервер для адресов, начинающихся с (,), используйте точку с запятой (;)</value>
<value>Исключения: не использовать прокси для адресов, начинающихся с указанных. Разделяйте точкой с запятой (;)</value>
</data>
<data name="TbSettingsDisplayRealTimeSpeed" xml:space="preserve">
<value>Показывать скорость в реальном времени</value>
<value>Показывать скорость в реальном времени (требуется перезапуск)</value>
</data>
<data name="TbSettingsKeepOlderDedupl" xml:space="preserve">
<value>Сохранить старые при удалении дублей</value>
@@ -721,7 +721,7 @@
<value>Настройки v2rayN</value>
</data>
<data name="TbSettingsPass" xml:space="preserve">
<value>Пароль аутентификации</value>
<value>Пароль авторизации</value>
</data>
<data name="TbSettingsRemoteDNS" xml:space="preserve">
<value>Пользовательский DNS (если несколько, то делите запятыми (,))</value>
@@ -736,10 +736,10 @@
<value>Смешанный порт</value>
</data>
<data name="TbSettingsStartBoot" xml:space="preserve">
<value>Автозапуск</value>
<value>Запускать при старте системы</value>
</data>
<data name="TbSettingsStatistics" xml:space="preserve">
<value>Включить статистику (требуется перезагрузка)</value>
<value>Включить статистику трафика (требуется перезапуск)</value>
</data>
<data name="TbSettingsSubConvert" xml:space="preserve">
<value>URL конвертации подписок</value>
@@ -748,31 +748,31 @@
<value>Настройки системного прокси</value>
</data>
<data name="TbSettingsTrayMenuServersLimit" xml:space="preserve">
<value>Лимит серверов в меню трея</value>
<value>Лимит отображения серверов в контекстном меню трея</value>
</data>
<data name="TbSettingsUdpEnabled" xml:space="preserve">
<value>Включить UDP</value>
</data>
<data name="TbSettingsUser" xml:space="preserve">
<value>Имя пользователя (логин)</value>
<value>Пользователь авторизации</value>
</data>
<data name="TbClearSystemProxy" xml:space="preserve">
<value>Очистить системный прокси</value>
</data>
<data name="TbDisplayGUI" xml:space="preserve">
<value>Показать GUI</value>
<value>Показать интерфейс</value>
</data>
<data name="TbGlobalHotkeySetting" xml:space="preserve">
<value>Настройка горячих клавиш</value>
<value>Настройка глобальных горячих клавиш</value>
</data>
<data name="TbGlobalHotkeySettingTip" xml:space="preserve">
<value>Установите непосредственно, нажав на клавиатуру, вступит в силу после перезапуска</value>
<value>Задайте, нажав нужную комбинацию клавиш; вступит в силу после перезапуска</value>
</data>
<data name="TbNotChangeSystemProxy" xml:space="preserve">
<value>Не изменять системный прокси</value>
<value>Не менять системный прокси</value>
</data>
<data name="TbReset" xml:space="preserve">
<value>Обнулить</value>
<value>Сбросить</value>
</data>
<data name="TbSetSystemProxy" xml:space="preserve">
<value>Установить системный прокси</value>
@@ -781,16 +781,16 @@
<value>Режим PAC</value>
</data>
<data name="menuShareServer" xml:space="preserve">
<value>Поделиться сервером</value>
<value>Поделиться</value>
</data>
<data name="menuRouting" xml:space="preserve">
<value>Маршрутизация</value>
</data>
<data name="NotRunAsAdmin" xml:space="preserve">
<value>Пользователь</value>
<value>Запущено без прав администратора</value>
</data>
<data name="RunAsAdmin" xml:space="preserve">
<value>Администратор</value>
<value>Запущено от имени администратора</value>
</data>
<data name="menuMoveBottom" xml:space="preserve">
<value>Спуститься вниз</value>
@@ -808,13 +808,13 @@
<value>Фильтр, поддерживает regex</value>
</data>
<data name="menuWebsiteItem" xml:space="preserve">
<value>{0} веб-сайт</value>
<value>Веб-сайт {0}</value>
</data>
<data name="menuRoutingAdvancedAdd" xml:space="preserve">
<value>Добавить</value>
</data>
<data name="menuRoutingAdvancedImportRules" xml:space="preserve">
<value>Добавить расширенные правила</value>
<value>Импортировать правила</value>
</data>
<data name="menuRoutingAdvancedRemove" xml:space="preserve">
<value>Удалить выбранные</value>
@@ -859,7 +859,7 @@
<value>Детальные настройки правил маршрутизации</value>
</data>
<data name="TbAutoSort" xml:space="preserve">
<value>Домен и IP автоматически сортируются при сохранении</value>
<value>Домен, IP и процесс автоматически сортируются при сохранении</value>
</data>
<data name="TbRuleobjectDoc" xml:space="preserve">
<value>Документация RuleObject</value>
@@ -868,7 +868,7 @@
<value>Поддерживаются DNS-объекты, нажмите для просмотра документации</value>
</data>
<data name="SubUrlTips" xml:space="preserve">
<value>Необязательное поле</value>
<value>Для группы оставьте это поле пустым</value>
</data>
<data name="TipChangeRouting" xml:space="preserve">
<value>Настройки маршрутизации изменены</value>
@@ -880,7 +880,7 @@
<value>Только маршрут</value>
</data>
<data name="TbSettingsNotProxyLocalAddress" xml:space="preserve">
<value>Не используйте прокси-серверы для локальных (интранет) адресов</value>
<value>Не использовать прокси для локальных (интранет) адресов</value>
</data>
<data name="menuMixedTestServer" xml:space="preserve">
<value>Тест задержки и скорости всех серверов (Ctrl+E)</value>
@@ -895,13 +895,13 @@
<value>Не удалось запустить ядро, посмотрите логи</value>
</data>
<data name="LvFilter" xml:space="preserve">
<value>Фильтр примечаний (Regex)</value>
<value>Фильтр по примечаниям (регулярные выражения)</value>
</data>
<data name="TbDisplayLog" xml:space="preserve">
<value>Показать логи</value>
<value>Отображать журнал</value>
</data>
<data name="TbEnableTunAs" xml:space="preserve">
<value>Режим VPN</value>
<value>Включить TUN</value>
</data>
<data name="TbSettingsNewPort4LAN" xml:space="preserve">
<value>Новый порт для локальной сети</value>
@@ -910,10 +910,10 @@
<value>Настройки режима TUN</value>
</data>
<data name="menuMoveToGroup" xml:space="preserve">
<value>Перейти в группу</value>
<value>Переместить в группу</value>
</data>
<data name="TbSettingsEnableDragDropSort" xml:space="preserve">
<value>Включить сортировку перетаскиванием сервера (требуется перезагрузка)</value>
<value>Включить сортировку перетаскиванием сервера (требуется перезапуск)</value>
</data>
<data name="TbAutoRefresh" xml:space="preserve">
<value>Автообновление</value>
@@ -922,10 +922,10 @@
<value>Пропустить тест</value>
</data>
<data name="menuEditServer" xml:space="preserve">
<value>Редактировать сервер</value>
<value>Редактировать</value>
</data>
<data name="TbSettingsDoubleClick2Activate" xml:space="preserve">
<value>Двойной клик чтобы сделать сервер активным</value>
<value>Двойной клик для активации конфигурации</value>
</data>
<data name="SpeedtestingCompleted" xml:space="preserve">
<value>Тест завершен</value>
@@ -940,16 +940,16 @@
<value>Параметр действует только для TCP/HTTP и WebSocket (WS)</value>
</data>
<data name="TbSettingsCurrentFontFamily" xml:space="preserve">
<value>Шрифт (требуется перезагрузка)</value>
<value>Шрифт (требуется перезапуск)</value>
</data>
<data name="TbSettingsCurrentFontFamilyTip" xml:space="preserve">
<value>Скопируйте файл шрифта TTF/TTC в каталог guiFonts и заново откройте окно настроек</value>
</data>
<data name="TbSettingsSocksPortTip" xml:space="preserve">
<value>Pac порт = +3,Xray API порт = +4, mihomo API порт = +5</value>
<value>PAC-порт = +3; Xray API порт = +4; mihomo API порт = +5;</value>
</data>
<data name="TbSettingsStartBootTip" xml:space="preserve">
<value>Установите это с правами администратора</value>
<value>Установите с правами администратора; после запуска приложение получит права администратора</value>
</data>
<data name="TbSettingsFontSize" xml:space="preserve">
<value>Размер шрифта</value>
@@ -964,7 +964,7 @@
<value>Переместить вверх/вниз</value>
</data>
<data name="TbPublicKey" xml:space="preserve">
<value>PublicKey</value>
<value>Открытый ключ</value>
</data>
<data name="TbShortId" xml:space="preserve">
<value>ShortId</value>
@@ -973,22 +973,22 @@
<value>SpiderX</value>
</data>
<data name="TbSettingsEnableHWA" xml:space="preserve">
<value>Включить аппаратное ускорение (требуется перезагрузка)</value>
<value>Включить аппаратное ускорение (требуется перезапуск)</value>
</data>
<data name="SpeedtestingWait" xml:space="preserve">
<value>Ожидание тестирования…</value>
<value>Ожидание…</value>
</data>
<data name="SpeedtestingPressEscToExit" xml:space="preserve">
<value>нажмите ESC для отмены</value>
<value>Нажмите ESC для прекращения теста</value>
</data>
<data name="TipDisplayLog" xml:space="preserve">
<value>Отключите при аномальном разрыве соединения</value>
<value>Отключите при нестабильном соединении</value>
</data>
<data name="MsgSkipSubscriptionUpdate" xml:space="preserve">
<value>Обновления не включены — подписка пропущена</value>
</data>
<data name="menuRebootAsAdmin" xml:space="preserve">
<value>Перезагрузить как администратор</value>
<value>Перезапустить от имени администратора</value>
</data>
<data name="LvMoreUrl" xml:space="preserve">
<value>Дополнительные URL через запятую, конвертация подписки недоступна</value>
@@ -1003,7 +1003,7 @@
<value>Включить запись логов в файл</value>
</data>
<data name="LvConvertTarget" xml:space="preserve">
<value>Преобразовать тип цели</value>
<value>Целевой тип конвертации</value>
</data>
<data name="LvConvertTargetTip" xml:space="preserve">
<value>Если преобразование не требуется, оставьте поле пустым</value>
@@ -1012,7 +1012,7 @@
<value>Настройки DNS</value>
</data>
<data name="TbCustomDnsSingbox" xml:space="preserve">
<value>sing-box Custom DNS</value>
<value>Пользовательский DNS для sing-box</value>
</data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>Заполните структуру DNS, нажмите, чтобы открыть документ</value>
@@ -1042,13 +1042,13 @@
<value>Максимальная пропускная способность Hysteria (загрузка/отдача)</value>
</data>
<data name="TbSettingsUseSystemHosts" xml:space="preserve">
<value>Использовать системные узлы</value>
<value>Использовать системный файл hosts</value>
</data>
<data name="menuAddTuicServer" xml:space="preserve">
<value>Добавить сервер [TUIC]</value>
</data>
<data name="TbHeaderType8" xml:space="preserve">
<value>Контроль перегрузок</value>
<value>Управление перегрузками</value>
</data>
<data name="LvPrevProfile" xml:space="preserve">
<value>Примечания к предыдущему прокси</value>
@@ -1084,7 +1084,7 @@
<value>Зарезервировано (2, 3, 4)</value>
</data>
<data name="TbLocalAddress" xml:space="preserve">
<value>Адрес (Ipv4,Ipv6)</value>
<value>Адрес (IPv4, IPv6)</value>
</data>
<data name="TbPath7" xml:space="preserve">
<value>Пароль obfs</value>
@@ -1099,7 +1099,7 @@
<value>URL для быстрой проверки реальной задержки</value>
</data>
<data name="SpeedtestingStop" xml:space="preserve">
<value>Отмена тестирования...</value>
<value>Завершение тестирования...</value>
</data>
<data name="TransportRequestHostTip5" xml:space="preserve">
<value>* gRPC Authority (HTTP/2 псевдозаголовок :authority)</value>
@@ -1117,7 +1117,7 @@
<value>Пользовательский набор правил для sing-box</value>
</data>
<data name="NeedRebootTips" xml:space="preserve">
<value>Операция успешна. Перезапустите приложение</value>
<value>Операция успешна. Закройте приложение, нажав «Выход» в меню трея, и запустите его заново</value>
</data>
<data name="menuOpenTheFileLocation" xml:space="preserve">
<value>Открыть место хранения</value>
@@ -1138,10 +1138,10 @@
<value>Скорость загрузки</value>
</data>
<data name="TbSortingDownTraffic" xml:space="preserve">
<value>Скачанный трафик</value>
<value>Загруженный трафик</value>
</data>
<data name="TbSortingHost" xml:space="preserve">
<value>Узел</value>
<value>Хост</value>
</data>
<data name="TbSortingName" xml:space="preserve">
<value>Имя</value>
@@ -1204,7 +1204,7 @@
<value>Стратегия домена по умолчанию для исходящих</value>
</data>
<data name="TbSettingsMainGirdOrientation" xml:space="preserve">
<value>Основная ориентация макета (требуется перезагрузка)</value>
<value>Основная ориентация макета (требуется перезапуск)</value>
</data>
<data name="TbSettingsDomainDNSAddress" xml:space="preserve">
<value>Исходящий DNS адрес</value>
@@ -1216,13 +1216,13 @@
<value>Экспорт ссылок в формате Base64 в буфер обмена</value>
</data>
<data name="menuExport2ClientConfigClipboard" xml:space="preserve">
<value>Экспортировать выбранный сервер для полной конфигурации в буфер обмена</value>
<value>Экспортировать полную конфигурацию в буфер обмена</value>
</data>
<data name="menuShowOrHideMainWindow" xml:space="preserve">
<value>Показать или скрыть главное окно</value>
</data>
<data name="TbPreSocksPort4Sub" xml:space="preserve">
<value>Пользовательская конфигурация порта SOCKS</value>
<value>Порт SOCKS для пользовательской конфигурации</value>
</data>
<data name="menuBackupAndRestore" xml:space="preserve">
<value>Резервное копирование и восстановление</value>
@@ -1276,13 +1276,13 @@
<value>Источник файлов наборов правил sing-box (необязательно)</value>
</data>
<data name="UpgradeAppNotExistTip" xml:space="preserve">
<value>Программы для обновления не существует</value>
<value>Приложение для обновления не найдено</value>
</data>
<data name="TbSettingsRoutingRulesSource" xml:space="preserve">
<value>Источник правил маршрутизации</value>
<value>Источник правил маршрутизации (необязательно)</value>
</data>
<data name="menuRegionalPresets" xml:space="preserve">
<value>Региональные пресеты</value>
<value>Настройка региональных пресетов</value>
</data>
<data name="menuRegionalPresetsDefault" xml:space="preserve">
<value>По умолчанию (Китай)</value>
@@ -1300,13 +1300,13 @@
<value>Сканировать QR-код с изображения</value>
</data>
<data name="InvalidUrlTip" xml:space="preserve">
<value>Неверный адрес (Url)</value>
<value>Неверный адрес (URL)</value>
</data>
<data name="InsecureUrlProtocol" xml:space="preserve">
<value>Не используйте небезопасный адрес подписки по протоколу HTTP</value>
</data>
<data name="TbSettingsCurrentFontFamilyLinuxTip" xml:space="preserve">
<value>Установите шрифт в систему и перезапустите настройки</value>
<value>Установите шрифт в систему, выберите или введите имя шрифта, перезапустите настройки</value>
</data>
<data name="menuExitTips" xml:space="preserve">
<value>Вы уверены, что хотите выйти?</value>
@@ -1324,16 +1324,16 @@
<value>*XHTTP-режим</value>
</data>
<data name="TransportExtraTip" xml:space="preserve">
<value>Дополнительный сырой JSON для XHTTP, формат: { XHTTP Object }</value>
<value>Дополнительный сырой JSON для XHTTP, формат: { XHTTP Object }</value>
</data>
<data name="TbSettingsHide2TrayWhenClose" xml:space="preserve">
<value>Скрыть в трее при закрытии окна</value>
<value>Сворачивать в трей при закрытии окна</value>
</data>
<data name="TbSettingsMixedConcurrencyCount" xml:space="preserve">
<value>Количество одновременно выполняемых тестов при многоэтапном тестировании</value>
</data>
<data name="TbSettingsExceptionTip2" xml:space="preserve">
<value>Исключение. Не используйте прокси-сервер для адресов с запятой (,)</value>
<value>Исключения: не использовать прокси для указанных адресов. Разделяйте запятой (,)</value>
</data>
<data name="TbSettingsDestOverride" xml:space="preserve">
<value>Тип сниффинга</value>
@@ -1345,7 +1345,7 @@
<value>socks: локальный порт, socks2: второй локальный порт, socks3: LAN порт</value>
</data>
<data name="TbSettingsTheme" xml:space="preserve">
<value>Темы</value>
<value>Тема</value>
</data>
<data name="menuCopyProxyCmdToClipboard" xml:space="preserve">
<value>Копировать команду прокси в буфер обмена</value>
@@ -1675,27 +1675,27 @@
<value>Выбрать все</value>
</data>
<data name="menuEditPaste" xml:space="preserve">
<value>Paste</value>
<value>Вставить</value>
</data>
<data name="menuEditFormat" xml:space="preserve">
<value>Format</value>
<value>Форматировать</value>
</data>
<data name="TbUot" xml:space="preserve">
<value>UDP over TCP</value>
<value>UDP поверх TCP</value>
</data>
<data name="menuAddNaiveServer" xml:space="preserve">
<value>Добавить сервер [NaïveProxy]</value>
</data>
<data name="TbInsecureConcurrency" xml:space="preserve">
<value>Insecure Concurrency</value>
<value>Небезопасная конкурентность (Insecure Concurrency)</value>
</data>
<data name="TbUsername" xml:space="preserve">
<value>Username</value>
<value>Имя пользователя</value>
</data>
<data name="TbIcmpRoutingPolicy" xml:space="preserve">
<value>ICMP routing policy</value>
<value>Политика маршрутизации ICMP</value>
</data>
<data name="TbLegacyProtect" xml:space="preserve">
<value>Legacy TUN Protect</value>
<value>Устаревшая защита TUN (Legacy Protect)</value>
</data>
</root>
</root>
@@ -36,7 +36,7 @@ data class ProfileItem(
var authority: String? = null,
var xhttpMode: String? = null,
var xhttpExtra: String? = null,
var finalMask: String? = null,
var security: String? = null,
var sni: String? = null,
var alpn: String? = null,
@@ -162,7 +162,7 @@ data class V2rayConfig(
var realitySettings: TlsSettingsBean? = null,
var grpcSettings: GrpcSettingsBean? = null,
var hysteriaSettings: HysteriaSettingsBean? = null,
var finalmask: FinalMaskBean? = null,
var finalmask: Any? = null,
val dsSettings: Any? = null,
var sockopt: SockoptBean? = null
) {
@@ -292,20 +292,14 @@ data class V2rayConfig(
data class HysteriaSettingsBean(
var version: Int,
var auth: String? = null,
var up: String? = null,
var down: String? = null,
var udphop: HysteriaUdpHopBean? = null
) {
data class HysteriaUdpHopBean(
var port: String? = null,
var interval: Int? = null
)
}
var auth: String? = null
)
//https://xtls.github.io/config/transport.html#finalmaskobject
data class FinalMaskBean(
var tcp: List<MaskBean>? = null,
var udp: List<MaskBean>? = null
var udp: List<MaskBean>? = null,
var quicParams: QuicParamsBean? = null
) {
data class MaskBean(
var type: String,
@@ -316,6 +310,18 @@ data class V2rayConfig(
var domain: String? = null
)
}
data class QuicParamsBean(
var congestion: String? = null,
var brutalUp: String? = null,
var brutalDown: String? = null,
var udpHop: UdpHopBean? = null,
) {
// Nested data class for the udpHop JSON object
data class UdpHopBean(
var ports: String? = null,
var interval: String? = null
)
}
}
}
@@ -67,6 +67,7 @@ open class FmtBase {
config.authority = queryParam["authority"]
config.xhttpMode = queryParam["mode"]
config.xhttpExtra = queryParam["extra"]
config.finalMask = queryParam["fm"]
config.security = queryParam["security"]
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
@@ -110,6 +111,7 @@ open class FmtBase {
config.spiderX?.nullIfBlank()?.let { dicQuery["spx"] = it }
config.mldsa65Verify?.nullIfBlank()?.let { dicQuery["pqv"] = it }
config.flow?.nullIfBlank()?.let { dicQuery["flow"] = it }
config.finalMask?.nullIfBlank()?.let { dicQuery["fm"] = it }
// Add two keys for compatibility: "insecure" and "allowInsecure"
if (config.security == AppConfig.TLS) {
val insecureFlag = if (config.insecure == true) "1" else "0"
@@ -3,7 +3,6 @@ package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean.FinalMaskBean
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.idnHost
@@ -73,7 +72,15 @@ object Hysteria2Fmt : FmtBase() {
dicQuery["mport"] = config.portHopping.orEmpty()
}
if (config.portHoppingInterval.isNotNullEmpty()) {
dicQuery["mportHopInt"] = config.portHoppingInterval.orEmpty()
var portHoppingInterval = config.portHoppingInterval.orEmpty()
if (portHoppingInterval.contains('-')) {
// interval range
portHoppingInterval = portHoppingInterval.substringBefore('-')
}
val trimmedPortHoppingInterval = portHoppingInterval.trim()
if (trimmedPortHoppingInterval.isNotEmpty()) {
dicQuery["mportHopInt"] = trimmedPortHoppingInterval
}
}
if (config.pinnedCA256.isNotNullEmpty()) {
dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty()
@@ -107,18 +114,6 @@ object Hysteria2Fmt : FmtBase() {
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
}
if (profileItem.obfsPassword.isNotNullEmpty()) {
outboundBean.streamSettings?.finalmask = FinalMaskBean(
udp = listOf(
FinalMaskBean.MaskBean(
type = "salamander",
settings = FinalMaskBean.MaskBean.MaskSettingsBean(
password = profileItem.obfsPassword
)
)
)
)
}
return outboundBean
}
}
@@ -1174,7 +1174,7 @@ object V2rayConfigManager {
val authority = profileItem.authority
val xhttpMode = profileItem.xhttpMode
val xhttpExtra = profileItem.xhttpExtra
val finalMask = profileItem.finalMask
var sni: String? = null
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
when (streamSettings.network) {
@@ -1290,21 +1290,69 @@ object V2rayConfigManager {
val hysteriaSetting = StreamSettingsBean.HysteriaSettingsBean(
version = 2,
auth = profileItem.password.orEmpty(),
up = profileItem.bandwidthUp?.ifEmpty { "0" }.orEmpty(),
down = profileItem.bandwidthDown?.ifEmpty { "0" }.orEmpty(),
udphop = null
)
val quicParams = StreamSettingsBean.FinalMaskBean.QuicParamsBean(
brutalUp = profileItem.bandwidthUp?.nullIfBlank(),
brutalDown = profileItem.bandwidthDown?.nullIfBlank(),
)
quicParams.congestion = if (quicParams.brutalUp != null || quicParams.brutalDown != null) "brutal" else null
if (profileItem.portHopping.isNotNullEmpty()) {
hysteriaSetting.udphop = StreamSettingsBean.HysteriaSettingsBean.HysteriaUdpHopBean(
port = profileItem.portHopping,
interval = profileItem.portHoppingInterval
?.trim()
?.toIntOrNull()
?.takeIf { it >= 5 }
?: 30
val rawInterval = profileItem.portHoppingInterval?.trim().nullIfBlank()
val interval = if (rawInterval == null) {
"30"
} else {
val singleValue = rawInterval.toIntOrNull()
if (singleValue != null) {
if (singleValue < 5) {
"30"
} else {
rawInterval
}
} else {
val parts = rawInterval.split('-')
if (parts.size == 2) {
val start = parts[0].trim().toIntOrNull()
val end = parts[1].trim().toIntOrNull()
if (start != null && end != null) {
val minStart = maxOf(5, start)
val minEnd = maxOf(minStart, end)
"$minStart-$minEnd"
} else {
"30"
}
} else {
"30"
}
}
}
quicParams.udpHop = StreamSettingsBean.FinalMaskBean.QuicParamsBean.UdpHopBean(
ports = profileItem.portHopping,
interval = interval
)
}
val finalmask = StreamSettingsBean.FinalMaskBean(
quicParams = quicParams
)
if (profileItem.obfsPassword.isNotNullEmpty()) {
finalmask.udp = listOf(
StreamSettingsBean.FinalMaskBean.MaskBean(
type = "salamander",
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
password = profileItem.obfsPassword.orEmpty()
)
)
)
}
streamSettings.hysteriaSettings = hysteriaSetting
streamSettings.finalmask = finalmask
}
}
finalMask?.let {
val parsedFinalMask = JsonUtil.parseString(finalMask)
if (parsedFinalMask != null) {
streamSettings.finalmask = parsedFinalMask
} else {
Log.w("V2rayConfigManager", "Invalid finalMask JSON, keeping previously generated finalmask")
}
}
return sni
@@ -24,6 +24,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
@@ -132,6 +133,7 @@ class ServerActivity : BaseActivity() {
private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
private val et_fm: EditText? by lazy { findViewById(R.id.et_fm) }
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
private val et_ech_config_list: EditText? by lazy { findViewById(R.id.et_ech_config_list) }
private val container_ech_config_list: LinearLayout? by lazy { findViewById(R.id.lay_ech_config_list) }
@@ -241,6 +243,7 @@ class ServerActivity : BaseActivity() {
else -> null
}.orEmpty()
)
et_fm?.text = Utils.getEditable(config?.finalMask)
layout_extra?.visibility =
when (networks[position]) {
@@ -489,6 +492,13 @@ class ServerActivity : BaseActivity() {
}
}
if (et_fm?.text?.toString().isNotNullEmpty()) {
if (JsonUtil.parseString(et_fm?.text?.toString()) == null) {
toast(R.string.server_lab_final_mask)
return false
}
}
saveCommon(config)
saveStreamSettings(config)
saveTls(config)
@@ -557,7 +567,8 @@ class ServerActivity : BaseActivity() {
profileItem.serviceName = path
profileItem.authority = requestHost
profileItem.xhttpMode = transportTypes(networks[network])[type]
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim()
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim().nullIfBlank()
profileItem.finalMask = et_fm?.text?.toString()?.trim()?.nullIfBlank()
}
private fun saveTls(config: ProfileItem) {
@@ -118,4 +118,25 @@
android:minLines="4" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_fm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_final_mask" />
<EditText
android:id="@+id/et_fm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textMultiLine"
android:maxLines="20"
android:minLines="4" />
</LinearLayout>
</LinearLayout>
@@ -113,6 +113,7 @@
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">وا روء رئڌن پئنا باند (واهڌ)</string>
<string name="server_lab_xhttp_mode">هالت XHTTP</string>
<string name="server_lab_xhttp_extra">XHTTP Extra خام JSON، قالوو: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">نومگه کانفیگ Ech</string>
<string name="server_lab_ech_force_query">پورس وو جۊ اجباری Ech</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">افزایش پهنای باند (واحد)</string>
<string name="server_lab_xhttp_mode">حالت XHTTP</string>
<string name="server_lab_xhttp_extra">خام JSON XHTTP Extra، قالب: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">لیست کانفیگ Ech</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">Режим XHTTP</string>
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">带宽上行 (支持的单位 k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP 模式</string>
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string>
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">頻寬上行 (支持的單位 k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP 模式</string>
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">證書指紋 (SHA-256)</string>
@@ -113,6 +113,7 @@
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>