mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Wed Apr 1 21:14:59 CEST 2026
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道)
|
||||
# tls: true # 可选:仅在 mode 为 stream/poll/auto/ws 时生效;true 强制 https/wss;false 强制 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(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道)
|
||||
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto/ws 时生效;true 强制 https/wss;false 强制 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 连接)、on(Sudoku 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(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道)
|
||||
# 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(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道)
|
||||
# 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 {
|
||||
|
||||
+119
@@ -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,
|
||||
)
|
||||
|
||||
+58
@@ -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")
|
||||
}
|
||||
}
|
||||
+93
@@ -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"
|
||||
}
|
||||
+56
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+68
-2
@@ -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)
|
||||
|
||||
+45
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Generated
+27
-27
@@ -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",
|
||||
|
||||
@@ -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,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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+129
-129
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
-116
@@ -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);
|
||||
+2
-2
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
-- 本地策略
|
||||
|
||||
Generated
+15
-22
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
}
|
||||
-%}
|
||||
|
||||
table inet nikki {
|
||||
table inet momo {
|
||||
set dns_hijack_nfproto {
|
||||
type nf_proto
|
||||
flags interval
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user