From dbff9aa53fdd9365860bed143cdcb470bf286835 Mon Sep 17 00:00:00 2001 From: "github-action[bot]" Date: Sun, 5 Apr 2026 20:57:47 +0200 Subject: [PATCH] Update On Sun Apr 5 20:57:47 CEST 2026 --- .github/update.log | 1 + clash-meta/adapter/outbound/trojan.go | 24 ++- clash-meta/adapter/outbound/vless.go | 21 +- clash-meta/adapter/outbound/vmess.go | 27 ++- clash-meta/common/convert/v.go | 39 ++++ clash-meta/docs/config.yaml | 9 + clash-meta/transport/gun/gun.go | 82 ++++++++ clash-meta/transport/gun/server.go | 5 - clash-meta/transport/sudoku/kip.go | 8 +- .../transport/sudoku/multiplex/session.go | 19 +- .../sudoku/multiplex/write_chunks.go | 19 ++ .../transport/sudoku/obfs/sudoku/conn.go | 106 ++++------ .../transport/sudoku/obfs/sudoku/encode.go | 36 ++++ .../transport/sudoku/obfs/sudoku/layout.go | 190 +++++++++++------- .../transport/sudoku/obfs/sudoku/packed.go | 62 +++--- .../sudoku/obfs/sudoku/padding_prob.go | 6 +- .../transport/sudoku/obfs/sudoku/pending.go | 57 ++++++ .../transport/sudoku/obfs/sudoku/rand.go | 56 ++++++ .../transport/sudoku/obfs/sudoku/table.go | 2 +- clash-meta/transport/sudoku/uot.go | 102 ++++++---- clash-meta/transport/sudoku/write_chunks.go | 19 ++ clash-nyanpasu/manifest/version.json | 4 +- filebrowser/CHANGELOG.md | 10 + filebrowser/auth/proxy.go | 3 + filebrowser/auth/proxy_test.go | 79 ++++++++ filebrowser/http/public.go | 4 + filebrowser/http/public_test.go | 37 +++- filebrowser/http/resource.go | 5 +- filebrowser/rules/rules.go | 11 +- filebrowser/rules/rules_test.go | 31 +++ mihomo/adapter/outbound/trojan.go | 24 ++- mihomo/adapter/outbound/vless.go | 21 +- mihomo/adapter/outbound/vmess.go | 27 ++- mihomo/common/convert/v.go | 39 ++++ mihomo/docs/config.yaml | 9 + mihomo/transport/gun/gun.go | 82 ++++++++ mihomo/transport/gun/server.go | 5 - mihomo/transport/sudoku/kip.go | 8 +- mihomo/transport/sudoku/multiplex/session.go | 19 +- .../sudoku/multiplex/write_chunks.go | 19 ++ mihomo/transport/sudoku/obfs/sudoku/conn.go | 106 ++++------ mihomo/transport/sudoku/obfs/sudoku/encode.go | 36 ++++ mihomo/transport/sudoku/obfs/sudoku/layout.go | 190 +++++++++++------- mihomo/transport/sudoku/obfs/sudoku/packed.go | 62 +++--- .../sudoku/obfs/sudoku/padding_prob.go | 6 +- .../transport/sudoku/obfs/sudoku/pending.go | 57 ++++++ mihomo/transport/sudoku/obfs/sudoku/rand.go | 56 ++++++ mihomo/transport/sudoku/obfs/sudoku/table.go | 2 +- mihomo/transport/sudoku/uot.go | 102 ++++++---- mihomo/transport/sudoku/write_chunks.go | 19 ++ openwrt-packages/filebrowser/Makefile | 4 +- .../luasrc/passwall/util_sing-box.lua | 2 +- .../luasrc/passwall/util_xray.lua | 22 +- .../root/usr/share/passwall/app.sh | 5 + .../root/usr/share/passwall/iptables.sh | 63 +++--- .../root/usr/share/passwall/nftables.sh | 53 +++-- openwrt-passwall2/luci-app-passwall2/Makefile | 2 +- .../model/cbi/passwall2/client/other.lua | 3 + .../luasrc/passwall2/util_sing-box.lua | 61 ++++-- .../luasrc/passwall2/util_xray.lua | 60 +++--- .../luci-app-passwall2/po/fa/passwall2.po | 6 + .../luci-app-passwall2/po/ru/passwall2.po | 6 + .../luci-app-passwall2/po/zh-cn/passwall2.po | 6 + .../luci-app-passwall2/po/zh-tw/passwall2.po | 6 + .../root/usr/share/passwall2/iptables.sh | 69 ++++++- .../root/usr/share/passwall2/nftables.sh | 51 ++++- small/gn/Makefile | 6 +- small/gn/src/out/last_commit_position.h | 4 +- .../luasrc/passwall/util_sing-box.lua | 2 +- .../luasrc/passwall/util_xray.lua | 22 +- .../root/usr/share/passwall/app.sh | 5 + .../root/usr/share/passwall/iptables.sh | 63 +++--- .../root/usr/share/passwall/nftables.sh | 53 +++-- small/luci-app-passwall2/Makefile | 2 +- .../model/cbi/passwall2/client/other.lua | 3 + .../luasrc/passwall2/util_sing-box.lua | 61 ++++-- .../luasrc/passwall2/util_xray.lua | 60 +++--- small/luci-app-passwall2/po/fa/passwall2.po | 6 + small/luci-app-passwall2/po/ru/passwall2.po | 6 + .../luci-app-passwall2/po/zh-cn/passwall2.po | 6 + .../luci-app-passwall2/po/zh-tw/passwall2.po | 6 + .../root/usr/share/passwall2/iptables.sh | 69 ++++++- .../root/usr/share/passwall2/nftables.sh | 51 ++++- small/v2ray-geodata/Makefile | 4 +- xray-core/go.mod | 2 +- xray-core/go.sum | 4 +- xray-core/infra/conf/transport_internet.go | 13 ++ xray-core/proxy/tun/stack_gvisor.go | 22 +- xray-core/proxy/tun/udp_fullcone.go | 128 ++++++++---- xray-core/proxy/wireguard/client.go | 16 +- xray-core/proxy/wireguard/tun.go | 69 +++++-- xray-core/proxy/wireguard/tun_linux.go | 51 ++++- xray-core/transport/internet/config.pb.go | 60 +++--- xray-core/transport/internet/config.proto | 23 ++- .../hysteria/congestion/bbr/bbr_sender.go | 184 +++++++++++++---- .../congestion/bbr/bbr_sender_test.go | 130 ++++++++++++ .../internet/hysteria/congestion/utils.go | 39 +++- .../transport/internet/hysteria/dialer.go | 17 +- xray-core/transport/internet/hysteria/hub.go | 14 +- .../transport/internet/splithttp/dialer.go | 13 +- xray-core/transport/internet/splithttp/hub.go | 10 +- xray-core/transport/internet/system_dialer.go | 5 +- .../transport/internet/system_listener.go | 7 +- .../internet/system_listener_test.go | 6 +- yt-dlp/pyproject.toml | 4 + .../extractor/youtube/jsc/_builtin/quickjs.py | 16 +- yt-dlp/yt_dlp/update.py | 2 +- 107 files changed, 2624 insertions(+), 1022 deletions(-) create mode 100644 clash-meta/transport/sudoku/multiplex/write_chunks.go create mode 100644 clash-meta/transport/sudoku/obfs/sudoku/encode.go create mode 100644 clash-meta/transport/sudoku/obfs/sudoku/pending.go create mode 100644 clash-meta/transport/sudoku/obfs/sudoku/rand.go create mode 100644 clash-meta/transport/sudoku/write_chunks.go create mode 100644 filebrowser/auth/proxy_test.go create mode 100644 mihomo/transport/sudoku/multiplex/write_chunks.go create mode 100644 mihomo/transport/sudoku/obfs/sudoku/encode.go create mode 100644 mihomo/transport/sudoku/obfs/sudoku/pending.go create mode 100644 mihomo/transport/sudoku/obfs/sudoku/rand.go create mode 100644 mihomo/transport/sudoku/write_chunks.go create mode 100644 xray-core/transport/internet/hysteria/congestion/bbr/bbr_sender_test.go diff --git a/.github/update.log b/.github/update.log index bbbe504bdc..b3e340a1a5 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1320,3 +1320,4 @@ Update On Wed Apr 1 21:14:50 CEST 2026 Update On Thu Apr 2 21:09:36 CEST 2026 Update On Fri Apr 3 21:00:13 CEST 2026 Update On Sat Apr 4 20:56:37 CEST 2026 +Update On Sun Apr 5 20:57:38 CEST 2026 diff --git a/clash-meta/adapter/outbound/trojan.go b/clash-meta/adapter/outbound/trojan.go index 2c04da1204..fc3a6f8271 100644 --- a/clash-meta/adapter/outbound/trojan.go +++ b/clash-meta/adapter/outbound/trojan.go @@ -27,7 +27,7 @@ type Trojan struct { hexPassword [trojan.KeyLength]byte // for gun mux - gunTransport *gun.Transport + gunClient *gun.Client realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -115,7 +115,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C. c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) case "grpc": - break // already handle in gun transport + break // already handle in dialContext default: // default tcp network // handle TLS @@ -175,7 +175,7 @@ func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C func (t *Trojan) dialContext(ctx context.Context) (c net.Conn, err error) { switch t.option.Network { case "grpc": // gun transport - return t.gunTransport.Dial() + return t.gunClient.Dial() default: } return t.dialer.DialContext(ctx, "tcp", t.addr) @@ -236,10 +236,13 @@ func (t *Trojan) ProxyInfo() C.ProxyInfo { // Close implements C.ProxyAdapter func (t *Trojan) Close() error { - if t.gunTransport != nil { - return t.gunTransport.Close() + var errs []error + if t.gunClient != nil { + if err := t.gunClient.Close(); err != nil { + errs = append(errs, err) + } } - return nil + return errors.Join(errs...) } func NewTrojan(option TrojanOption) (*Trojan, error) { @@ -320,7 +323,14 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { PingInterval: option.GrpcOpts.PingInterval, } - t.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) + t.gunClient = gun.NewClient( + func() *gun.Transport { + return gun.NewTransport(dialFn, tlsConfig, gunConfig) + }, + option.GrpcOpts.MaxConnections, + option.GrpcOpts.MinStreams, + option.GrpcOpts.MaxStreams, + ) } return t, nil diff --git a/clash-meta/adapter/outbound/vless.go b/clash-meta/adapter/outbound/vless.go index 07526edd76..93b1086770 100644 --- a/clash-meta/adapter/outbound/vless.go +++ b/clash-meta/adapter/outbound/vless.go @@ -36,7 +36,7 @@ type Vless struct { encryption *encryption.ClientInstance // for gun mux - gunTransport *gun.Transport + gunClient *gun.Client // for xhttp xhttpClient *xhttp.Client @@ -196,9 +196,9 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M c, err = vmess.StreamH2Conn(ctx, c, h2Opts) case "grpc": - break // already handle in gun transport + break // already handle in dialContext case "xhttp": - break // already handle in xhttp client + break // already handle in dialContext default: // default tcp network // handle TLS @@ -280,7 +280,7 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne func (v *Vless) dialContext(ctx context.Context) (c net.Conn, err error) { switch v.option.Network { case "grpc": // gun transport - return v.gunTransport.Dial() + return v.gunClient.Dial() case "xhttp": return v.xhttpClient.Dial() default: @@ -358,8 +358,8 @@ func (v *Vless) ProxyInfo() C.ProxyInfo { // Close implements C.ProxyAdapter func (v *Vless) Close() error { var errs []error - if v.gunTransport != nil { - if err := v.gunTransport.Close(); err != nil { + if v.gunClient != nil { + if err := v.gunClient.Close(); err != nil { errs = append(errs, err) } } @@ -505,7 +505,14 @@ func NewVless(option VlessOption) (*Vless, error) { } } - v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) + v.gunClient = gun.NewClient( + func() *gun.Transport { + return gun.NewTransport(dialFn, tlsConfig, gunConfig) + }, + option.GrpcOpts.MaxConnections, + option.GrpcOpts.MinStreams, + option.GrpcOpts.MaxStreams, + ) case "xhttp": requestHost := v.option.XHTTPOpts.Host if requestHost == "" { diff --git a/clash-meta/adapter/outbound/vmess.go b/clash-meta/adapter/outbound/vmess.go index c12950d30f..c2237986cb 100644 --- a/clash-meta/adapter/outbound/vmess.go +++ b/clash-meta/adapter/outbound/vmess.go @@ -34,7 +34,7 @@ type Vmess struct { option *VmessOption // for gun mux - gunTransport *gun.Transport + gunClient *gun.Client realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -86,6 +86,9 @@ type GrpcOptions struct { GrpcServiceName string `proxy:"grpc-service-name,omitempty"` GrpcUserAgent string `proxy:"grpc-user-agent,omitempty"` PingInterval int `proxy:"ping-interval,omitempty"` + MaxConnections int `proxy:"max-connections,omitempty"` + MinStreams int `proxy:"min-streams,omitempty"` + MaxStreams int `proxy:"max-streams,omitempty"` } type WSOptions struct { @@ -172,7 +175,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts) case "grpc": - break // already handle in gun transport + break // already handle in dialContext default: // default tcp network // handle TLS @@ -274,7 +277,7 @@ func (v *Vmess) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne func (v *Vmess) dialContext(ctx context.Context) (c net.Conn, err error) { switch v.option.Network { case "grpc": // gun transport - return v.gunTransport.Dial() + return v.gunClient.Dial() default: } return v.dialer.DialContext(ctx, "tcp", v.addr) @@ -331,10 +334,13 @@ func (v *Vmess) ProxyInfo() C.ProxyInfo { // Close implements C.ProxyAdapter func (v *Vmess) Close() error { - if v.gunTransport != nil { - return v.gunTransport.Close() + var errs []error + if v.gunClient != nil { + if err := v.gunClient.Close(); err != nil { + errs = append(errs, err) + } } - return nil + return errors.Join(errs...) } // SupportUOT implements C.ProxyAdapter @@ -438,7 +444,14 @@ func NewVmess(option VmessOption) (*Vmess, error) { } } - v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) + v.gunClient = gun.NewClient( + func() *gun.Transport { + return gun.NewTransport(dialFn, tlsConfig, gunConfig) + }, + option.GrpcOpts.MaxConnections, + option.GrpcOpts.MinStreams, + option.GrpcOpts.MaxStreams, + ) } return v, nil diff --git a/clash-meta/common/convert/v.go b/clash-meta/common/convert/v.go index cebf8e95a3..334acf3756 100644 --- a/clash-meta/common/convert/v.go +++ b/clash-meta/common/convert/v.go @@ -167,6 +167,29 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m // parseXHTTPExtra maps xray-core extra JSON fields to mihomo xhttp-opts fields. func parseXHTTPExtra(extra map[string]any, opts map[string]any) { + // xmuxToReuse converts an xmux map to mihomo reuse-settings. + xmuxToReuse := func(xmux map[string]any) map[string]any { + reuse := make(map[string]any) + set := func(src, dst string) { + if v, ok := xmux[src]; ok { + switch val := v.(type) { + case string: + if val != "" { + reuse[dst] = val + } + case float64: + reuse[dst] = strconv.FormatInt(int64(val), 10) + } + } + } + set("maxConnections", "max-connections") + set("maxConcurrency", "max-concurrency") + set("cMaxReuseTimes", "c-max-reuse-times") + set("hMaxRequestTimes", "h-max-request-times") + set("hMaxReusableSecs", "h-max-reusable-secs") + return reuse + } + if v, ok := extra["noGRPCHeader"].(bool); ok && v { opts["no-grpc-header"] = true } @@ -175,6 +198,13 @@ func parseXHTTPExtra(extra map[string]any, opts map[string]any) { opts["x-padding-bytes"] = v } + // xmux in root extra → reuse-settings + if xmuxAny, ok := extra["xmux"].(map[string]any); ok && len(xmuxAny) > 0 { + if reuse := xmuxToReuse(xmuxAny); len(reuse) > 0 { + opts["reuse-settings"] = reuse + } + } + if dsAny, ok := extra["downloadSettings"].(map[string]any); ok { ds := make(map[string]any) @@ -223,6 +253,15 @@ func parseXHTTPExtra(extra map[string]any, opts map[string]any) { if v, ok := xhttpAny["xPaddingBytes"].(string); ok && v != "" { ds["x-padding-bytes"] = v } + + // xmux inside downloadSettings.xhttpSettings.extra → download-settings.reuse-settings + if dsExtraAny, ok := xhttpAny["extra"].(map[string]any); ok { + if xmuxAny, ok := dsExtraAny["xmux"].(map[string]any); ok && len(xmuxAny) > 0 { + if reuse := xmuxToReuse(xmuxAny); len(reuse) > 0 { + ds["reuse-settings"] = reuse + } + } + } } if len(ds) > 0 { diff --git a/clash-meta/docs/config.yaml b/clash-meta/docs/config.yaml index 99fed80c46..ea0fdb9adc 100644 --- a/clash-meta/docs/config.yaml +++ b/clash-meta/docs/config.yaml @@ -670,6 +670,9 @@ proxies: # socks5 grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" # ping-interval: 0 # 默认关闭,单位为秒 + # max-connections: 1 # Maximum connections. Conflict with max-streams. + # min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. + # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. # ip-version: ipv4 # vless @@ -761,6 +764,9 @@ proxies: # socks5 grpc-service-name: "grpc" # grpc-user-agent: "grpc-go/1.36.0" # ping-interval: 0 # 默认关闭,单位为秒 + # max-connections: 1 # Maximum connections. Conflict with max-streams. + # min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. + # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. reality-opts: public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE @@ -896,6 +902,9 @@ proxies: # socks5 grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" # ping-interval: 0 # 默认关闭,单位为秒 + # max-connections: 1 # Maximum connections. Conflict with max-streams. + # min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. + # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. - name: trojan-ws server: server diff --git a/clash-meta/transport/gun/gun.go b/clash-meta/transport/gun/gun.go index aec7e4e05a..da747f88db 100644 --- a/clash-meta/transport/gun/gun.go +++ b/clash-meta/transport/gun/gun.go @@ -13,6 +13,7 @@ import ( "net/url" "strings" "sync" + "sync/atomic" "time" "github.com/metacubex/mihomo/common/buf" @@ -51,6 +52,7 @@ type Conn struct { closeMutex sync.Mutex closed bool + onClose func() // deadlines deadline *time.Timer @@ -209,6 +211,10 @@ func (g *Conn) Close() error { } } + if g.onClose != nil { + g.onClose() + } + return errors.Join(errorArr...) } @@ -240,6 +246,7 @@ type Transport struct { ctx context.Context cancel context.CancelFunc closeOnce sync.Once + count atomic.Int64 } func (t *Transport) Close() error { @@ -349,6 +356,9 @@ func (t *Transport) Dial() (net.Conn, error) { writer: writer, } + t.count.Add(1) + conn.onClose = func() { t.count.Add(-1) } + go conn.Init() // ensure conn.initOnce.Do has been called before return @@ -358,6 +368,78 @@ func (t *Transport) Dial() (net.Conn, error) { return conn, nil } +type Client struct { + mutex sync.Mutex + maxConnections int + minStreams int + maxStreams int + transports []*Transport + maker func() *Transport +} + +func NewClient(maker func() *Transport, maxConnections, minStreams, maxStreams int) *Client { + if maxConnections == 0 && minStreams == 0 && maxStreams == 0 { + maxConnections = 1 + } + return &Client{ + maxConnections: maxConnections, + minStreams: minStreams, + maxStreams: maxStreams, + maker: maker, + } +} + +func (c *Client) Dial() (net.Conn, error) { + return c.getTransport().Dial() +} + +func (c *Client) Close() error { + c.mutex.Lock() + defer c.mutex.Unlock() + var errs []error + for _, t := range c.transports { + if err := t.Close(); err != nil { + errs = append(errs, err) + } + } + c.transports = nil + return errors.Join(errs...) +} + +func (c *Client) getTransport() *Transport { + c.mutex.Lock() + defer c.mutex.Unlock() + var transport *Transport + for _, t := range c.transports { + if transport == nil || t.count.Load() < transport.count.Load() { + transport = t + } + } + if transport == nil { + return c.newTransportLocked() + } + numStreams := int(transport.count.Load()) + if numStreams == 0 { + return transport + } + if c.maxConnections > 0 { + if len(c.transports) >= c.maxConnections || numStreams < c.minStreams { + return transport + } + } else { + if c.maxStreams > 0 && numStreams < c.maxStreams { + return transport + } + } + return c.newTransportLocked() +} + +func (c *Client) newTransportLocked() *Transport { + transport := c.maker() + c.transports = append(c.transports, transport) + return transport +} + func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, gunCfg *Config) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { return conn, nil diff --git a/clash-meta/transport/gun/server.go b/clash-meta/transport/gun/server.go index 4f27891f7c..4c5280f7dd 100644 --- a/clash-meta/transport/gun/server.go +++ b/clash-meta/transport/gun/server.go @@ -112,11 +112,6 @@ func (w *h2ConnWrapper) CloseWrapper() { w.closed = true } -func (w *h2ConnWrapper) Close() error { - w.CloseWrapper() - return w.ExtendedConn.Close() -} - func (w *h2ConnWrapper) Upstream() any { return w.ExtendedConn } diff --git a/clash-meta/transport/sudoku/kip.go b/clash-meta/transport/sudoku/kip.go index 2f965034e9..354f0b5129 100644 --- a/clash-meta/transport/sudoku/kip.go +++ b/clash-meta/transport/sudoku/kip.go @@ -66,13 +66,7 @@ func WriteKIPMessage(w io.Writer, typ byte, payload []byte) error { hdr[3] = typ binary.BigEndian.PutUint16(hdr[4:], uint16(len(payload))) - if err := writeFull(w, hdr[:]); err != nil { - return err - } - if len(payload) == 0 { - return nil - } - return writeFull(w, payload) + return writeAllChunks(w, hdr[:], payload) } func ReadKIPMessage(r io.Reader) (*KIPMessage, error) { diff --git a/clash-meta/transport/sudoku/multiplex/session.go b/clash-meta/transport/sudoku/multiplex/session.go index 779fcafb5b..4344d8e7d3 100644 --- a/clash-meta/transport/sudoku/multiplex/session.go +++ b/clash-meta/transport/sudoku/multiplex/session.go @@ -173,16 +173,10 @@ func (s *Session) sendFrame(frameType byte, streamID uint32, payload []byte) err s.writeMu.Lock() defer s.writeMu.Unlock() - if err := writeFull(s.conn, header[:]); err != nil { + if err := writeAllChunks(s.conn, header[:], payload); err != nil { s.closeWithError(err) return err } - if len(payload) > 0 { - if err := writeFull(s.conn, payload); err != nil { - s.closeWithError(err) - return err - } - } return nil } @@ -315,17 +309,6 @@ func (s *Session) readLoop() { } } -func writeFull(w io.Writer, b []byte) error { - for len(b) > 0 { - n, err := w.Write(b) - if err != nil { - return err - } - b = b[n:] - } - return nil -} - func trimASCII(b []byte) string { i := 0 j := len(b) diff --git a/clash-meta/transport/sudoku/multiplex/write_chunks.go b/clash-meta/transport/sudoku/multiplex/write_chunks.go new file mode 100644 index 0000000000..dd381d3a1f --- /dev/null +++ b/clash-meta/transport/sudoku/multiplex/write_chunks.go @@ -0,0 +1,19 @@ +package multiplex + +import "io" + +func writeAllChunks(w io.Writer, chunks ...[]byte) error { + for _, chunk := range chunks { + for len(chunk) > 0 { + n, err := w.Write(chunk) + if err != nil { + return err + } + if n == 0 { + return io.ErrShortWrite + } + chunk = chunk[n:] + } + } + return nil +} diff --git a/clash-meta/transport/sudoku/obfs/sudoku/conn.go b/clash-meta/transport/sudoku/obfs/sudoku/conn.go index 7a18d0f29a..12998d5e6a 100644 --- a/clash-meta/transport/sudoku/obfs/sudoku/conn.go +++ b/clash-meta/transport/sudoku/obfs/sudoku/conn.go @@ -3,12 +3,9 @@ package sudoku import ( "bufio" "bytes" - crypto_rand "crypto/rand" - "encoding/binary" - "errors" - "math/rand" "net" "sync" + "sync/atomic" ) const IOBufferSize = 32 * 1024 @@ -45,14 +42,17 @@ type Conn struct { table *Table reader *bufio.Reader recorder *bytes.Buffer - recording bool + recording atomic.Bool recordLock sync.Mutex rawBuf []byte - pendingData []byte - hintBuf []byte + pendingData pendingBuffer + hintBuf [4]byte + hintCount int + writeMu sync.Mutex + writeBuf []byte - rng *rand.Rand + rng randomSource paddingThreshold uint64 } @@ -77,33 +77,28 @@ func (sc *Conn) CloseRead() error { } func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn { - var seedBytes [8]byte - if _, err := crypto_rand.Read(seedBytes[:]); err != nil { - binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63())) - } - seed := int64(binary.BigEndian.Uint64(seedBytes[:])) - localRng := rand.New(rand.NewSource(seed)) + localRng := newSeededRand() sc := &Conn{ Conn: c, table: table, reader: bufio.NewReaderSize(c, IOBufferSize), rawBuf: make([]byte, IOBufferSize), - pendingData: make([]byte, 0, 4096), - hintBuf: make([]byte, 0, 4), + pendingData: newPendingBuffer(4096), + writeBuf: make([]byte, 0, 4096), rng: localRng, paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax), } if record { sc.recorder = new(bytes.Buffer) - sc.recording = true + sc.recording.Store(true) } return sc } func (sc *Conn) StopRecording() { sc.recordLock.Lock() - sc.recording = false + sc.recording.Store(false) sc.recorder = nil sc.recordLock.Unlock() } @@ -137,74 +132,50 @@ func (sc *Conn) Write(p []byte) (n int, err error) { return 0, nil } - outCapacity := len(p) * 6 - out := make([]byte, 0, outCapacity) - pads := sc.table.PaddingPool - padLen := len(pads) + sc.writeMu.Lock() + defer sc.writeMu.Unlock() - for _, b := range p { - if shouldPad(sc.rng, sc.paddingThreshold) { - out = append(out, pads[sc.rng.Intn(padLen)]) - } - - puzzles := sc.table.EncodeTable[b] - puzzle := puzzles[sc.rng.Intn(len(puzzles))] - - perm := perm4[sc.rng.Intn(len(perm4))] - for _, idx := range perm { - if shouldPad(sc.rng, sc.paddingThreshold) { - out = append(out, pads[sc.rng.Intn(padLen)]) - } - out = append(out, puzzle[idx]) - } - } - - if shouldPad(sc.rng, sc.paddingThreshold) { - out = append(out, pads[sc.rng.Intn(padLen)]) - } - - return len(p), writeFull(sc.Conn, out) + sc.writeBuf = encodeSudokuPayload(sc.writeBuf[:0], sc.table, sc.rng, sc.paddingThreshold, p) + return len(p), writeFull(sc.Conn, sc.writeBuf) } func (sc *Conn) Read(p []byte) (n int, err error) { - if len(sc.pendingData) > 0 { - n = copy(p, sc.pendingData) - if n == len(sc.pendingData) { - sc.pendingData = sc.pendingData[:0] - } else { - sc.pendingData = sc.pendingData[n:] - } + if n, ok := drainPending(p, &sc.pendingData); ok { return n, nil } for { - if len(sc.pendingData) > 0 { + if sc.pendingData.available() > 0 { break } nr, rErr := sc.reader.Read(sc.rawBuf) if nr > 0 { chunk := sc.rawBuf[:nr] - sc.recordLock.Lock() - if sc.recording { - sc.recorder.Write(chunk) + if sc.recording.Load() { + sc.recordLock.Lock() + if sc.recording.Load() && sc.recorder != nil { + sc.recorder.Write(chunk) + } + sc.recordLock.Unlock() } - sc.recordLock.Unlock() + layout := sc.table.layout for _, b := range chunk { - if !sc.table.layout.isHint(b) { + if !layout.hintTable[b] { continue } - sc.hintBuf = append(sc.hintBuf, b) - if len(sc.hintBuf) == 4 { - key := packHintsToKey([4]byte{sc.hintBuf[0], sc.hintBuf[1], sc.hintBuf[2], sc.hintBuf[3]}) + sc.hintBuf[sc.hintCount] = b + sc.hintCount++ + if sc.hintCount == len(sc.hintBuf) { + key := packHintsToKey(sc.hintBuf) val, ok := sc.table.DecodeMap[key] if !ok { - return 0, errors.New("INVALID_SUDOKU_MAP_MISS") + return 0, ErrInvalidSudokuMapMiss } - sc.pendingData = append(sc.pendingData, val) - sc.hintBuf = sc.hintBuf[:0] + sc.pendingData.appendByte(val) + sc.hintCount = 0 } } } @@ -212,16 +183,11 @@ func (sc *Conn) Read(p []byte) (n int, err error) { if rErr != nil { return 0, rErr } - if len(sc.pendingData) > 0 { + if sc.pendingData.available() > 0 { break } } - n = copy(p, sc.pendingData) - if n == len(sc.pendingData) { - sc.pendingData = sc.pendingData[:0] - } else { - sc.pendingData = sc.pendingData[n:] - } + n, _ = drainPending(p, &sc.pendingData) return n, nil } diff --git a/clash-meta/transport/sudoku/obfs/sudoku/encode.go b/clash-meta/transport/sudoku/obfs/sudoku/encode.go new file mode 100644 index 0000000000..cfcf571e38 --- /dev/null +++ b/clash-meta/transport/sudoku/obfs/sudoku/encode.go @@ -0,0 +1,36 @@ +package sudoku + +func encodeSudokuPayload(dst []byte, table *Table, rng randomSource, paddingThreshold uint64, p []byte) []byte { + if len(p) == 0 { + return dst[:0] + } + + outCapacity := len(p)*6 + 1 + if cap(dst) < outCapacity { + dst = make([]byte, 0, outCapacity) + } + out := dst[:0] + pads := table.PaddingPool + padLen := len(pads) + + for _, b := range p { + if shouldPad(rng, paddingThreshold) { + out = append(out, pads[rng.Intn(padLen)]) + } + + puzzles := table.EncodeTable[b] + puzzle := puzzles[rng.Intn(len(puzzles))] + perm := perm4[rng.Intn(len(perm4))] + for _, idx := range perm { + if shouldPad(rng, paddingThreshold) { + out = append(out, pads[rng.Intn(padLen)]) + } + out = append(out, puzzle[idx]) + } + } + + if shouldPad(rng, paddingThreshold) { + out = append(out, pads[rng.Intn(padLen)]) + } + return out +} diff --git a/clash-meta/transport/sudoku/obfs/sudoku/layout.go b/clash-meta/transport/sudoku/obfs/sudoku/layout.go index b89933f361..2e1ec236a4 100644 --- a/clash-meta/transport/sudoku/obfs/sudoku/layout.go +++ b/clash-meta/transport/sudoku/obfs/sudoku/layout.go @@ -14,17 +14,30 @@ type byteLayout struct { padMarker byte paddingPool []byte - encodeHint func(val, pos byte) byte - encodeGroup func(group byte) byte - decodeGroup func(b byte) (byte, bool) + hintTable [256]bool + encodeHint [4][16]byte + encodeGroup [64]byte + decodeGroup [256]byte + groupValid [256]bool } func (l *byteLayout) isHint(b byte) bool { - if (b & l.hintMask) == l.hintValue { - return true + return l != nil && l.hintTable[b] +} + +func (l *byteLayout) hintByte(val, pos byte) byte { + return l.encodeHint[val&0x03][pos&0x0F] +} + +func (l *byteLayout) groupByte(group byte) byte { + return l.encodeGroup[group&0x3F] +} + +func (l *byteLayout) decodePackedGroup(b byte) (byte, bool) { + if l == nil { + return 0, false } - // ASCII layout maps the single non-printable marker (0x7F) to '\n' on the wire. - return l.name == "ascii" && b == '\n' + return l.decodeGroup[b], l.groupValid[b] } // resolveLayout picks the byte layout for a single traffic direction. @@ -50,38 +63,44 @@ func newASCIILayout() *byteLayout { for i := 0; i < 32; i++ { padding = append(padding, byte(0x20+i)) } - return &byteLayout{ + + layout := &byteLayout{ name: "ascii", hintMask: 0x40, hintValue: 0x40, padMarker: 0x3F, paddingPool: padding, - encodeHint: func(val, pos byte) byte { - b := 0x40 | ((val & 0x03) << 4) | (pos & 0x0F) - // Avoid DEL (0x7F) in prefer_ascii mode; map it to '\n' to reduce fingerprint. - if b == 0x7F { - return '\n' - } - return b - }, - encodeGroup: func(group byte) byte { - b := 0x40 | (group & 0x3F) - // Avoid DEL (0x7F) in prefer_ascii mode; map it to '\n' to reduce fingerprint. - if b == 0x7F { - return '\n' - } - return b - }, - decodeGroup: func(b byte) (byte, bool) { - if b == '\n' { - return 0x3F, true - } - if (b & 0x40) == 0 { - return 0, false - } - return b & 0x3F, true - }, } + + for val := 0; val < 4; val++ { + for pos := 0; pos < 16; pos++ { + b := byte(0x40 | (byte(val) << 4) | byte(pos)) + if b == 0x7F { + b = '\n' + } + layout.encodeHint[val][pos] = b + } + } + for group := 0; group < 64; group++ { + b := byte(0x40 | byte(group)) + if b == 0x7F { + b = '\n' + } + layout.encodeGroup[group] = b + } + for b := 0; b < 256; b++ { + wire := byte(b) + if (wire & 0x40) == 0x40 { + layout.hintTable[wire] = true + layout.decodeGroup[wire] = wire & 0x3F + layout.groupValid[wire] = true + } + } + layout.hintTable['\n'] = true + layout.decodeGroup['\n'] = 0x3F + layout.groupValid['\n'] = true + + return layout } func newEntropyLayout() *byteLayout { @@ -90,26 +109,35 @@ func newEntropyLayout() *byteLayout { padding = append(padding, byte(0x80+i)) padding = append(padding, byte(0x10+i)) } - return &byteLayout{ + + layout := &byteLayout{ name: "entropy", hintMask: 0x90, hintValue: 0x00, padMarker: 0x80, paddingPool: padding, - encodeHint: func(val, pos byte) byte { - return ((val & 0x03) << 5) | (pos & 0x0F) - }, - encodeGroup: func(group byte) byte { - v := group & 0x3F - return ((v & 0x30) << 1) | (v & 0x0F) - }, - decodeGroup: func(b byte) (byte, bool) { - if (b & 0x90) != 0 { - return 0, false - } - return ((b >> 1) & 0x30) | (b & 0x0F), true - }, } + + for val := 0; val < 4; val++ { + for pos := 0; pos < 16; pos++ { + layout.encodeHint[val][pos] = (byte(val) << 5) | byte(pos) + } + } + for group := 0; group < 64; group++ { + v := byte(group) + layout.encodeGroup[group] = ((v & 0x30) << 1) | (v & 0x0F) + } + for b := 0; b < 256; b++ { + wire := byte(b) + if (wire & 0x90) != 0 { + continue + } + layout.hintTable[wire] = true + layout.decodeGroup[wire] = ((wire >> 1) & 0x30) | (wire & 0x0F) + layout.groupValid[wire] = true + } + + return layout } func newCustomLayout(pattern string) (*byteLayout, error) { @@ -162,26 +190,6 @@ func newCustomLayout(pattern string) (*byteLayout, error) { return out } - decodeGroup := func(b byte) (byte, bool) { - if (b & xMask) != xMask { - return 0, false - } - var val, pos byte - if b&(1<> 4) & 0x03 - pos := group & 0x0F - return encodeBits(val, pos, -1) - }, - decodeGroup: decodeGroup, - }, nil + } + + for val := 0; val < 4; val++ { + for pos := 0; pos < 16; pos++ { + layout.encodeHint[val][pos] = encodeBits(byte(val), byte(pos), -1) + } + } + for group := 0; group < 64; group++ { + val := byte(group>>4) & 0x03 + pos := byte(group) & 0x0F + layout.encodeGroup[group] = encodeBits(val, pos, -1) + } + for b := 0; b < 256; b++ { + wire := byte(b) + if (wire & xMask) != xMask { + continue + } + layout.hintTable[wire] = true + + var val, pos byte + if wire&(1< 0 { - return pc.drainPendingData(p), nil + if n, ok := drainPending(p, &pc.pendingData); ok { + return n, nil } for { @@ -320,7 +302,7 @@ func (pc *PackedConn) Read(p []byte) (int, error) { layout := pc.table.layout for _, b := range pc.rawBuf[:nr] { - if !layout.isHint(b) { + if !layout.hintTable[b] { if b == padMarker { rBuf = 0 rBits = 0 @@ -328,7 +310,7 @@ func (pc *PackedConn) Read(p []byte) (int, error) { continue } - group, ok := layout.decodeGroup(b) + group, ok := layout.decodePackedGroup(b) if !ok { return 0, ErrInvalidSudokuMapMiss } @@ -339,7 +321,12 @@ func (pc *PackedConn) Read(p []byte) (int, error) { if rBits >= 8 { rBits -= 8 val := byte(rBuf >> rBits) - pc.pendingData = append(pc.pendingData, val) + pc.pendingData.appendByte(val) + if rBits == 0 { + rBuf = 0 + } else { + rBuf &= (uint64(1) << rBits) - 1 + } } } @@ -352,24 +339,21 @@ func (pc *PackedConn) Read(p []byte) (int, error) { pc.readBitBuf = 0 pc.readBits = 0 } - if len(pc.pendingData) > 0 { + if pc.pendingData.available() > 0 { break } return 0, rErr } - if len(pc.pendingData) > 0 { + if pc.pendingData.available() > 0 { break } } - return pc.drainPendingData(p), nil + n, _ := drainPending(p, &pc.pendingData) + return n, nil } func (pc *PackedConn) getPaddingByte() byte { return pc.padPool[pc.rng.Intn(len(pc.padPool))] } - -func (pc *PackedConn) encodeGroup(group byte) byte { - return pc.table.layout.encodeGroup(group) -} diff --git a/clash-meta/transport/sudoku/obfs/sudoku/padding_prob.go b/clash-meta/transport/sudoku/obfs/sudoku/padding_prob.go index f23a883b86..00ff68ffe4 100644 --- a/clash-meta/transport/sudoku/obfs/sudoku/padding_prob.go +++ b/clash-meta/transport/sudoku/obfs/sudoku/padding_prob.go @@ -1,10 +1,8 @@ package sudoku -import "math/rand" - const probOne = uint64(1) << 32 -func pickPaddingThreshold(r *rand.Rand, pMin, pMax int) uint64 { +func pickPaddingThreshold(r randomSource, pMin, pMax int) uint64 { if r == nil { return 0 } @@ -30,7 +28,7 @@ func pickPaddingThreshold(r *rand.Rand, pMin, pMax int) uint64 { return min + (u * (max - min) >> 32) } -func shouldPad(r *rand.Rand, threshold uint64) bool { +func shouldPad(r randomSource, threshold uint64) bool { if threshold == 0 { return false } diff --git a/clash-meta/transport/sudoku/obfs/sudoku/pending.go b/clash-meta/transport/sudoku/obfs/sudoku/pending.go new file mode 100644 index 0000000000..60f09a12f7 --- /dev/null +++ b/clash-meta/transport/sudoku/obfs/sudoku/pending.go @@ -0,0 +1,57 @@ +package sudoku + +type pendingBuffer struct { + data []byte + off int +} + +func newPendingBuffer(capacity int) pendingBuffer { + return pendingBuffer{data: make([]byte, 0, capacity)} +} + +func (p *pendingBuffer) available() int { + if p == nil { + return 0 + } + return len(p.data) - p.off +} + +func (p *pendingBuffer) reset() { + if p == nil { + return + } + p.data = p.data[:0] + p.off = 0 +} + +func (p *pendingBuffer) ensureAppendCapacity(extra int) { + if p == nil || extra <= 0 || p.off == 0 { + return + } + if cap(p.data)-len(p.data) >= extra { + return + } + + unread := len(p.data) - p.off + copy(p.data[:unread], p.data[p.off:]) + p.data = p.data[:unread] + p.off = 0 +} + +func (p *pendingBuffer) appendByte(b byte) { + p.ensureAppendCapacity(1) + p.data = append(p.data, b) +} + +func drainPending(dst []byte, pending *pendingBuffer) (int, bool) { + if pending == nil || pending.available() == 0 { + return 0, false + } + + n := copy(dst, pending.data[pending.off:]) + pending.off += n + if pending.off == len(pending.data) { + pending.reset() + } + return n, true +} diff --git a/clash-meta/transport/sudoku/obfs/sudoku/rand.go b/clash-meta/transport/sudoku/obfs/sudoku/rand.go new file mode 100644 index 0000000000..abe72d8054 --- /dev/null +++ b/clash-meta/transport/sudoku/obfs/sudoku/rand.go @@ -0,0 +1,56 @@ +package sudoku + +import ( + crypto_rand "crypto/rand" + "encoding/binary" + "time" +) + +type randomSource interface { + Uint32() uint32 + Uint64() uint64 + Intn(n int) int +} + +type sudokuRand struct { + state uint64 +} + +func newSeededRand() *sudokuRand { + seed := time.Now().UnixNano() + var seedBytes [8]byte + if _, err := crypto_rand.Read(seedBytes[:]); err == nil { + seed = int64(binary.BigEndian.Uint64(seedBytes[:])) + } + return newSudokuRand(seed) +} + +func newSudokuRand(seed int64) *sudokuRand { + state := uint64(seed) + if state == 0 { + state = 0x9e3779b97f4a7c15 + } + return &sudokuRand{state: state} +} + +func (r *sudokuRand) Uint64() uint64 { + if r == nil { + return 0 + } + r.state += 0x9e3779b97f4a7c15 + z := r.state + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9 + z = (z ^ (z >> 27)) * 0x94d049bb133111eb + return z ^ (z >> 31) +} + +func (r *sudokuRand) Uint32() uint32 { + return uint32(r.Uint64() >> 32) +} + +func (r *sudokuRand) Intn(n int) int { + if n <= 1 { + return 0 + } + return int((uint64(r.Uint32()) * uint64(n)) >> 32) +} diff --git a/clash-meta/transport/sudoku/obfs/sudoku/table.go b/clash-meta/transport/sudoku/obfs/sudoku/table.go index 201ca2cd80..b32506ae5e 100644 --- a/clash-meta/transport/sudoku/obfs/sudoku/table.go +++ b/clash-meta/transport/sudoku/obfs/sudoku/table.go @@ -146,7 +146,7 @@ func newSingleDirectionTable(key string, mode string, customPattern string) (*Ta if matchCount == 1 { // 唯一确定,生成最终编码字节 for i, p := range rawParts { - currentHints[i] = t.layout.encodeHint(p.val-1, p.pos) + currentHints[i] = t.layout.hintByte(p.val-1, p.pos) } t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints) diff --git a/clash-meta/transport/sudoku/uot.go b/clash-meta/transport/sudoku/uot.go index 054108a6bb..854b3b5035 100644 --- a/clash-meta/transport/sudoku/uot.go +++ b/clash-meta/transport/sudoku/uot.go @@ -8,7 +8,6 @@ import ( "io" "net" "net/netip" - "strconv" "sync" "time" @@ -37,42 +36,15 @@ func WriteDatagram(w io.Writer, addr string, payload []byte) error { binary.BigEndian.PutUint16(header[:2], uint16(len(addrBuf))) binary.BigEndian.PutUint16(header[2:], uint16(len(payload))) - if err := writeFull(w, header[:]); err != nil { - return err - } - if err := writeFull(w, addrBuf); err != nil { - return err - } - return writeFull(w, payload) + return writeAllChunks(w, header[:], addrBuf, payload) } // ReadDatagram parses a single UDP datagram frame from the reliable stream. func ReadDatagram(r io.Reader) (string, []byte, error) { - var header [4]byte - if _, err := io.ReadFull(r, header[:]); err != nil { - return "", nil, err - } - - addrLen := int(binary.BigEndian.Uint16(header[:2])) - payloadLen := int(binary.BigEndian.Uint16(header[2:])) - - if addrLen <= 0 || addrLen > maxUoTPayload { - return "", nil, fmt.Errorf("invalid address length: %d", addrLen) - } - if payloadLen < 0 || payloadLen > maxUoTPayload { - return "", nil, fmt.Errorf("invalid payload length: %d", payloadLen) - } - - addrBuf := make([]byte, addrLen) - if _, err := io.ReadFull(r, addrBuf); err != nil { - return "", nil, err - } - - addr, err := DecodeAddress(bytes.NewReader(addrBuf)) + addr, payloadLen, err := readDatagramHeaderAndAddress(r) if err != nil { - return "", nil, fmt.Errorf("decode address: %w", err) + return "", nil, err } - payload := make([]byte, payloadLen) if _, err := io.ReadFull(r, payload); err != nil { return "", nil, err @@ -93,26 +65,29 @@ func NewUoTPacketConn(conn net.Conn) *UoTPacketConn { func (c *UoTPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { for { - addrStr, payload, err := ReadDatagram(c.conn) + addrStr, payloadLen, err := readDatagramHeaderAndAddress(c.conn) if err != nil { return 0, nil, err } - if len(payload) > len(p) { + udpAddr, err := parseDatagramUDPAddr(addrStr) + if payloadLen > len(p) { + if discardErr := discardBytes(c.conn, payloadLen); discardErr != nil { + return 0, nil, discardErr + } return 0, nil, io.ErrShortBuffer } - - host, port, _ := net.SplitHostPort(addrStr) - portInt, _ := strconv.ParseUint(port, 10, 16) - ip, err := netip.ParseAddr(host) - if err != nil { // disallow domain addr at here, just ignore + if err != nil { + if discardErr := discardBytes(c.conn, payloadLen); discardErr != nil { + return 0, nil, discardErr + } log.Debugln("[Sudoku][UoT] discard datagram with invalid address %s: %v", addrStr, err) continue } - udpAddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip.Unmap(), uint16(portInt))) - - copy(p, payload) - return len(payload), udpAddr, nil + if _, err := io.ReadFull(c.conn, p[:payloadLen]); err != nil { + return 0, nil, err + } + return payloadLen, udpAddr, nil } } @@ -147,3 +122,46 @@ func (c *UoTPacketConn) SetReadDeadline(t time.Time) error { func (c *UoTPacketConn) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) } + +func readDatagramHeaderAndAddress(r io.Reader) (string, int, error) { + var header [4]byte + if _, err := io.ReadFull(r, header[:]); err != nil { + return "", 0, err + } + + addrLen := int(binary.BigEndian.Uint16(header[:2])) + payloadLen := int(binary.BigEndian.Uint16(header[2:])) + if addrLen <= 0 || addrLen > maxUoTPayload { + return "", 0, fmt.Errorf("invalid address length: %d", addrLen) + } + if payloadLen < 0 || payloadLen > maxUoTPayload { + return "", 0, fmt.Errorf("invalid payload length: %d", payloadLen) + } + + addrBuf := make([]byte, addrLen) + if _, err := io.ReadFull(r, addrBuf); err != nil { + return "", 0, err + } + + addr, err := DecodeAddress(bytes.NewReader(addrBuf)) + if err != nil { + return "", 0, fmt.Errorf("decode address: %w", err) + } + return addr, payloadLen, nil +} + +func parseDatagramUDPAddr(addr string) (*net.UDPAddr, error) { + addrPort, err := netip.ParseAddrPort(addr) + if err != nil { + return nil, err + } + return net.UDPAddrFromAddrPort(netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())), nil +} + +func discardBytes(r io.Reader, n int) error { + if n <= 0 { + return nil + } + _, err := io.CopyN(io.Discard, r, int64(n)) + return err +} diff --git a/clash-meta/transport/sudoku/write_chunks.go b/clash-meta/transport/sudoku/write_chunks.go new file mode 100644 index 0000000000..9c4ee8c784 --- /dev/null +++ b/clash-meta/transport/sudoku/write_chunks.go @@ -0,0 +1,19 @@ +package sudoku + +import "io" + +func writeAllChunks(w io.Writer, chunks ...[]byte) error { + for _, chunk := range chunks { + for len(chunk) > 0 { + n, err := w.Write(chunk) + if err != nil { + return err + } + if n == 0 { + return io.ErrShortWrite + } + chunk = chunk[n:] + } + } + return nil +} diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json index a8d73f0235..209ff3718b 100644 --- a/clash-nyanpasu/manifest/version.json +++ b/clash-nyanpasu/manifest/version.json @@ -2,7 +2,7 @@ "manifest_version": 1, "latest": { "mihomo": "v1.19.22", - "mihomo_alpha": "alpha-f3b0581", + "mihomo_alpha": "alpha-3ef8a0f", "clash_rs": "v0.9.6", "clash_premium": "2023-09-05-gdcc8d87", "clash_rs_alpha": "0.9.6-alpha+sha.c414fb7" @@ -69,5 +69,5 @@ "linux-armv7hf": "clash-rs-armv7-unknown-linux-gnueabihf" } }, - "updated_at": "2026-04-03T22:23:27.347Z" + "updated_at": "2026-04-04T22:22:52.085Z" } diff --git a/filebrowser/CHANGELOG.md b/filebrowser/CHANGELOG.md index 21c40b7972..94782f041a 100644 --- a/filebrowser/CHANGELOG.md +++ b/filebrowser/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [2.63.1](https://github.com/filebrowser/filebrowser/compare/v2.63.0...v2.63.1) (2026-04-04) + + +### Bug Fixes + +* check download permission in resource handler ([#5891](https://github.com/filebrowser/filebrowser/issues/5891)) ([1e03fea](https://github.com/filebrowser/filebrowser/commit/1e03feadb550e4414b5589a6a8df57f538efba15)) +* check share owner permissions on public share access ([#5888](https://github.com/filebrowser/filebrowser/issues/5888)) ([7dbf7a3](https://github.com/filebrowser/filebrowser/commit/7dbf7a3528234b2a9ee9c4115e8ecf58d258ca51)) +* enforce directory boundary in rule path matching ([#5889](https://github.com/filebrowser/filebrowser/issues/5889)) ([8adf127](https://github.com/filebrowser/filebrowser/commit/8adf127c7d33585333b8030869f6f318e6517179)) +* restrict default permissions for proxy-auth auto-provisioned users ([#5890](https://github.com/filebrowser/filebrowser/issues/5890)) ([f13c7c8](https://github.com/filebrowser/filebrowser/commit/f13c7c8cffd6d58ff29c4a6763ced1385f69961e)) + ## [2.63.0](https://github.com/filebrowser/filebrowser/compare/v2.62.2...v2.63.0) (2026-04-04) diff --git a/filebrowser/auth/proxy.go b/filebrowser/auth/proxy.go index 3f4a627ca1..57eddd4aad 100644 --- a/filebrowser/auth/proxy.go +++ b/filebrowser/auth/proxy.go @@ -46,6 +46,9 @@ func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv * LockPassword: true, } setting.Defaults.Apply(user) + user.Perm.Admin = false + user.Perm.Execute = false + user.Commands = []string{} var userHome string userHome, err = setting.MakeUserDir(user.Username, user.Scope, srv.Root) diff --git a/filebrowser/auth/proxy_test.go b/filebrowser/auth/proxy_test.go new file mode 100644 index 0000000000..86fb7102d7 --- /dev/null +++ b/filebrowser/auth/proxy_test.go @@ -0,0 +1,79 @@ +package auth + +import ( + "net/http" + "testing" + + fberrors "github.com/filebrowser/filebrowser/v2/errors" + "github.com/filebrowser/filebrowser/v2/settings" + "github.com/filebrowser/filebrowser/v2/users" +) + +type mockUserStore struct { + users map[string]*users.User +} + +func (m *mockUserStore) Get(_ string, id interface{}) (*users.User, error) { + if v, ok := id.(string); ok { + if u, ok := m.users[v]; ok { + return u, nil + } + } + return nil, fberrors.ErrNotExist +} + +func (m *mockUserStore) Gets(_ string) ([]*users.User, error) { return nil, nil } +func (m *mockUserStore) Update(_ *users.User, _ ...string) error { return nil } +func (m *mockUserStore) Save(user *users.User) error { + m.users[user.Username] = user + return nil +} +func (m *mockUserStore) Delete(_ interface{}) error { return nil } +func (m *mockUserStore) LastUpdate(_ uint) int64 { return 0 } + +func TestProxyAuthCreateUserRestrictsDefaults(t *testing.T) { + t.Parallel() + + store := &mockUserStore{users: make(map[string]*users.User)} + srv := &settings.Server{Root: t.TempDir()} + + s := &settings.Settings{ + Key: []byte("key"), + AuthMethod: MethodProxyAuth, + Defaults: settings.UserDefaults{ + Perm: users.Permissions{ + Admin: true, + Execute: true, + Create: true, + Rename: true, + Modify: true, + Delete: true, + Share: true, + Download: true, + }, + Commands: []string{"git", "ls", "cat", "id"}, + }, + } + + auth := ProxyAuth{Header: "X-Remote-User"} + req, _ := http.NewRequest(http.MethodGet, "/", http.NoBody) + req.Header.Set("X-Remote-User", "newproxyuser") + + user, err := auth.Auth(req, store, s, srv) + if err != nil { + t.Fatalf("Auth() error: %v", err) + } + + if user.Perm.Admin { + t.Error("auto-provisioned proxy user should not have Admin permission") + } + if user.Perm.Execute { + t.Error("auto-provisioned proxy user should not have Execute permission") + } + if len(user.Commands) != 0 { + t.Errorf("auto-provisioned proxy user should have empty Commands, got %v", user.Commands) + } + if !user.Perm.Create { + t.Error("auto-provisioned proxy user should retain Create permission from defaults") + } +} diff --git a/filebrowser/http/public.go b/filebrowser/http/public.go index 596a03c972..29679be85d 100644 --- a/filebrowser/http/public.go +++ b/filebrowser/http/public.go @@ -33,6 +33,10 @@ var withHashFile = func(fn handleFunc) handleFunc { return errToStatus(err), err } + if !user.Perm.Share || !user.Perm.Download { + return http.StatusForbidden, nil + } + d.user = user file, err := files.NewFileInfo(&files.FileOptions{ diff --git a/filebrowser/http/public_test.go b/filebrowser/http/public_test.go index 9a9c7f2eb1..ea38ce51af 100644 --- a/filebrowser/http/public_test.go +++ b/filebrowser/http/public_test.go @@ -23,38 +23,66 @@ func TestPublicShareHandlerAuthentication(t *testing.T) { testCases := map[string]struct { share *share.Link req *http.Request + sharePerm bool + downloadPerm bool expectedStatusCode int }{ "Public share, no auth required": { share: &share.Link{Hash: "h", UserID: 1}, req: newHTTPRequest(t), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 200, }, "Private share, no auth provided, 401": { share: &share.Link{Hash: "h", UserID: 1, PasswordHash: passwordBcrypt, Token: "123"}, req: newHTTPRequest(t), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 401, }, "Private share, authentication via token": { share: &share.Link{Hash: "h", UserID: 1, PasswordHash: passwordBcrypt, Token: "123"}, req: newHTTPRequest(t, func(r *http.Request) { r.URL.RawQuery = "token=123" }), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 200, }, "Private share, authentication via invalid token, 401": { share: &share.Link{Hash: "h", UserID: 1, PasswordHash: passwordBcrypt, Token: "123"}, req: newHTTPRequest(t, func(r *http.Request) { r.URL.RawQuery = "token=1234" }), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 401, }, "Private share, authentication via password": { share: &share.Link{Hash: "h", UserID: 1, PasswordHash: passwordBcrypt, Token: "123"}, req: newHTTPRequest(t, func(r *http.Request) { r.Header.Set("X-SHARE-PASSWORD", "password") }), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 200, }, "Private share, authentication via invalid password, 401": { share: &share.Link{Hash: "h", UserID: 1, PasswordHash: passwordBcrypt, Token: "123"}, req: newHTTPRequest(t, func(r *http.Request) { r.Header.Set("X-SHARE-PASSWORD", "wrong-password") }), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 401, }, + "Share owner lost share permission, 403": { + share: &share.Link{Hash: "h", UserID: 1}, + req: newHTTPRequest(t), + sharePerm: false, + downloadPerm: true, + expectedStatusCode: 403, + }, + "Share owner lost download permission, 403": { + share: &share.Link{Hash: "h", UserID: 1}, + req: newHTTPRequest(t), + sharePerm: true, + downloadPerm: false, + expectedStatusCode: 403, + }, } for name, tc := range testCases { @@ -82,7 +110,14 @@ func TestPublicShareHandlerAuthentication(t *testing.T) { if err := storage.Share.Save(tc.share); err != nil { t.Fatalf("failed to save share: %v", err) } - if err := storage.Users.Save(&users.User{Username: "username", Password: "pw"}); err != nil { + if err := storage.Users.Save(&users.User{ + Username: "username", + Password: "pw", + Perm: users.Permissions{ + Share: tc.sharePerm, + Download: tc.downloadPerm, + }, + }); err != nil { t.Fatalf("failed to save user: %v", err) } if err := storage.Settings.Save(&settings.Settings{Key: []byte("key")}); err != nil { diff --git a/filebrowser/http/resource.go b/filebrowser/http/resource.go index 3f78cb0c0e..dfe722a978 100644 --- a/filebrowser/http/resource.go +++ b/filebrowser/http/resource.go @@ -30,7 +30,7 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d Expand: true, ReadHeader: d.server.TypeDetectionByHeader, Checker: d, - Content: true, + Content: d.user.Perm.Download, }) if err != nil { return errToStatus(err), err @@ -42,6 +42,9 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d file.ApplySort() return renderJSON(w, r, file) } else if encoding == "true" { + if !d.user.Perm.Download { + return http.StatusAccepted, nil + } if file.Type != "text" { return renderJSON(w, r, file) } diff --git a/filebrowser/rules/rules.go b/filebrowser/rules/rules.go index 7c6ef11bec..71f6693060 100644 --- a/filebrowser/rules/rules.go +++ b/filebrowser/rules/rules.go @@ -31,7 +31,16 @@ func (r *Rule) Matches(path string) bool { return r.Regexp.MatchString(path) } - return strings.HasPrefix(path, r.Path) + if path == r.Path { + return true + } + + prefix := r.Path + if prefix != "/" && !strings.HasSuffix(prefix, "/") { + prefix += "/" + } + + return strings.HasPrefix(path, prefix) } // Regexp is a wrapper to the native regexp type where we diff --git a/filebrowser/rules/rules_test.go b/filebrowser/rules/rules_test.go index 570f921f3d..3046a2bddf 100644 --- a/filebrowser/rules/rules_test.go +++ b/filebrowser/rules/rules_test.go @@ -2,6 +2,37 @@ package rules import "testing" +func TestRuleMatches(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + rulePath string + testPath string + want bool + }{ + {"exact match", "/uploads", "/uploads", true}, + {"child path", "/uploads", "/uploads/file.txt", true}, + {"sibling prefix", "/uploads", "/uploads_backup/secret.txt", false}, + {"root rule", "/", "/anything", true}, + {"trailing slash rule", "/uploads/", "/uploads/file.txt", true}, + {"trailing slash no sibling", "/uploads/", "/uploads_backup/file.txt", false}, + {"nested child", "/data/shared", "/data/shared/docs/file.txt", true}, + {"nested sibling", "/data/shared", "/data/shared_private/file.txt", false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + r := &Rule{Path: tc.rulePath} + got := r.Matches(tc.testPath) + if got != tc.want { + t.Errorf("Rule{Path: %q}.Matches(%q) = %v; want %v", tc.rulePath, tc.testPath, got, tc.want) + } + }) + } +} + func TestMatchHidden(t *testing.T) { cases := map[string]bool{ "/": false, diff --git a/mihomo/adapter/outbound/trojan.go b/mihomo/adapter/outbound/trojan.go index 2c04da1204..fc3a6f8271 100644 --- a/mihomo/adapter/outbound/trojan.go +++ b/mihomo/adapter/outbound/trojan.go @@ -27,7 +27,7 @@ type Trojan struct { hexPassword [trojan.KeyLength]byte // for gun mux - gunTransport *gun.Transport + gunClient *gun.Client realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -115,7 +115,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C. c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) case "grpc": - break // already handle in gun transport + break // already handle in dialContext default: // default tcp network // handle TLS @@ -175,7 +175,7 @@ func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C func (t *Trojan) dialContext(ctx context.Context) (c net.Conn, err error) { switch t.option.Network { case "grpc": // gun transport - return t.gunTransport.Dial() + return t.gunClient.Dial() default: } return t.dialer.DialContext(ctx, "tcp", t.addr) @@ -236,10 +236,13 @@ func (t *Trojan) ProxyInfo() C.ProxyInfo { // Close implements C.ProxyAdapter func (t *Trojan) Close() error { - if t.gunTransport != nil { - return t.gunTransport.Close() + var errs []error + if t.gunClient != nil { + if err := t.gunClient.Close(); err != nil { + errs = append(errs, err) + } } - return nil + return errors.Join(errs...) } func NewTrojan(option TrojanOption) (*Trojan, error) { @@ -320,7 +323,14 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { PingInterval: option.GrpcOpts.PingInterval, } - t.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) + t.gunClient = gun.NewClient( + func() *gun.Transport { + return gun.NewTransport(dialFn, tlsConfig, gunConfig) + }, + option.GrpcOpts.MaxConnections, + option.GrpcOpts.MinStreams, + option.GrpcOpts.MaxStreams, + ) } return t, nil diff --git a/mihomo/adapter/outbound/vless.go b/mihomo/adapter/outbound/vless.go index 07526edd76..93b1086770 100644 --- a/mihomo/adapter/outbound/vless.go +++ b/mihomo/adapter/outbound/vless.go @@ -36,7 +36,7 @@ type Vless struct { encryption *encryption.ClientInstance // for gun mux - gunTransport *gun.Transport + gunClient *gun.Client // for xhttp xhttpClient *xhttp.Client @@ -196,9 +196,9 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M c, err = vmess.StreamH2Conn(ctx, c, h2Opts) case "grpc": - break // already handle in gun transport + break // already handle in dialContext case "xhttp": - break // already handle in xhttp client + break // already handle in dialContext default: // default tcp network // handle TLS @@ -280,7 +280,7 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne func (v *Vless) dialContext(ctx context.Context) (c net.Conn, err error) { switch v.option.Network { case "grpc": // gun transport - return v.gunTransport.Dial() + return v.gunClient.Dial() case "xhttp": return v.xhttpClient.Dial() default: @@ -358,8 +358,8 @@ func (v *Vless) ProxyInfo() C.ProxyInfo { // Close implements C.ProxyAdapter func (v *Vless) Close() error { var errs []error - if v.gunTransport != nil { - if err := v.gunTransport.Close(); err != nil { + if v.gunClient != nil { + if err := v.gunClient.Close(); err != nil { errs = append(errs, err) } } @@ -505,7 +505,14 @@ func NewVless(option VlessOption) (*Vless, error) { } } - v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) + v.gunClient = gun.NewClient( + func() *gun.Transport { + return gun.NewTransport(dialFn, tlsConfig, gunConfig) + }, + option.GrpcOpts.MaxConnections, + option.GrpcOpts.MinStreams, + option.GrpcOpts.MaxStreams, + ) case "xhttp": requestHost := v.option.XHTTPOpts.Host if requestHost == "" { diff --git a/mihomo/adapter/outbound/vmess.go b/mihomo/adapter/outbound/vmess.go index c12950d30f..c2237986cb 100644 --- a/mihomo/adapter/outbound/vmess.go +++ b/mihomo/adapter/outbound/vmess.go @@ -34,7 +34,7 @@ type Vmess struct { option *VmessOption // for gun mux - gunTransport *gun.Transport + gunClient *gun.Client realityConfig *tlsC.RealityConfig echConfig *ech.Config @@ -86,6 +86,9 @@ type GrpcOptions struct { GrpcServiceName string `proxy:"grpc-service-name,omitempty"` GrpcUserAgent string `proxy:"grpc-user-agent,omitempty"` PingInterval int `proxy:"ping-interval,omitempty"` + MaxConnections int `proxy:"max-connections,omitempty"` + MinStreams int `proxy:"min-streams,omitempty"` + MaxStreams int `proxy:"max-streams,omitempty"` } type WSOptions struct { @@ -172,7 +175,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts) case "grpc": - break // already handle in gun transport + break // already handle in dialContext default: // default tcp network // handle TLS @@ -274,7 +277,7 @@ func (v *Vmess) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne func (v *Vmess) dialContext(ctx context.Context) (c net.Conn, err error) { switch v.option.Network { case "grpc": // gun transport - return v.gunTransport.Dial() + return v.gunClient.Dial() default: } return v.dialer.DialContext(ctx, "tcp", v.addr) @@ -331,10 +334,13 @@ func (v *Vmess) ProxyInfo() C.ProxyInfo { // Close implements C.ProxyAdapter func (v *Vmess) Close() error { - if v.gunTransport != nil { - return v.gunTransport.Close() + var errs []error + if v.gunClient != nil { + if err := v.gunClient.Close(); err != nil { + errs = append(errs, err) + } } - return nil + return errors.Join(errs...) } // SupportUOT implements C.ProxyAdapter @@ -438,7 +444,14 @@ func NewVmess(option VmessOption) (*Vmess, error) { } } - v.gunTransport = gun.NewTransport(dialFn, tlsConfig, gunConfig) + v.gunClient = gun.NewClient( + func() *gun.Transport { + return gun.NewTransport(dialFn, tlsConfig, gunConfig) + }, + option.GrpcOpts.MaxConnections, + option.GrpcOpts.MinStreams, + option.GrpcOpts.MaxStreams, + ) } return v, nil diff --git a/mihomo/common/convert/v.go b/mihomo/common/convert/v.go index cebf8e95a3..334acf3756 100644 --- a/mihomo/common/convert/v.go +++ b/mihomo/common/convert/v.go @@ -167,6 +167,29 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m // parseXHTTPExtra maps xray-core extra JSON fields to mihomo xhttp-opts fields. func parseXHTTPExtra(extra map[string]any, opts map[string]any) { + // xmuxToReuse converts an xmux map to mihomo reuse-settings. + xmuxToReuse := func(xmux map[string]any) map[string]any { + reuse := make(map[string]any) + set := func(src, dst string) { + if v, ok := xmux[src]; ok { + switch val := v.(type) { + case string: + if val != "" { + reuse[dst] = val + } + case float64: + reuse[dst] = strconv.FormatInt(int64(val), 10) + } + } + } + set("maxConnections", "max-connections") + set("maxConcurrency", "max-concurrency") + set("cMaxReuseTimes", "c-max-reuse-times") + set("hMaxRequestTimes", "h-max-request-times") + set("hMaxReusableSecs", "h-max-reusable-secs") + return reuse + } + if v, ok := extra["noGRPCHeader"].(bool); ok && v { opts["no-grpc-header"] = true } @@ -175,6 +198,13 @@ func parseXHTTPExtra(extra map[string]any, opts map[string]any) { opts["x-padding-bytes"] = v } + // xmux in root extra → reuse-settings + if xmuxAny, ok := extra["xmux"].(map[string]any); ok && len(xmuxAny) > 0 { + if reuse := xmuxToReuse(xmuxAny); len(reuse) > 0 { + opts["reuse-settings"] = reuse + } + } + if dsAny, ok := extra["downloadSettings"].(map[string]any); ok { ds := make(map[string]any) @@ -223,6 +253,15 @@ func parseXHTTPExtra(extra map[string]any, opts map[string]any) { if v, ok := xhttpAny["xPaddingBytes"].(string); ok && v != "" { ds["x-padding-bytes"] = v } + + // xmux inside downloadSettings.xhttpSettings.extra → download-settings.reuse-settings + if dsExtraAny, ok := xhttpAny["extra"].(map[string]any); ok { + if xmuxAny, ok := dsExtraAny["xmux"].(map[string]any); ok && len(xmuxAny) > 0 { + if reuse := xmuxToReuse(xmuxAny); len(reuse) > 0 { + ds["reuse-settings"] = reuse + } + } + } } if len(ds) > 0 { diff --git a/mihomo/docs/config.yaml b/mihomo/docs/config.yaml index 99fed80c46..ea0fdb9adc 100644 --- a/mihomo/docs/config.yaml +++ b/mihomo/docs/config.yaml @@ -670,6 +670,9 @@ proxies: # socks5 grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" # ping-interval: 0 # 默认关闭,单位为秒 + # max-connections: 1 # Maximum connections. Conflict with max-streams. + # min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. + # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. # ip-version: ipv4 # vless @@ -761,6 +764,9 @@ proxies: # socks5 grpc-service-name: "grpc" # grpc-user-agent: "grpc-go/1.36.0" # ping-interval: 0 # 默认关闭,单位为秒 + # max-connections: 1 # Maximum connections. Conflict with max-streams. + # min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. + # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. reality-opts: public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE @@ -896,6 +902,9 @@ proxies: # socks5 grpc-service-name: "example" # grpc-user-agent: "grpc-go/1.36.0" # ping-interval: 0 # 默认关闭,单位为秒 + # max-connections: 1 # Maximum connections. Conflict with max-streams. + # min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams. + # max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams. - name: trojan-ws server: server diff --git a/mihomo/transport/gun/gun.go b/mihomo/transport/gun/gun.go index aec7e4e05a..da747f88db 100644 --- a/mihomo/transport/gun/gun.go +++ b/mihomo/transport/gun/gun.go @@ -13,6 +13,7 @@ import ( "net/url" "strings" "sync" + "sync/atomic" "time" "github.com/metacubex/mihomo/common/buf" @@ -51,6 +52,7 @@ type Conn struct { closeMutex sync.Mutex closed bool + onClose func() // deadlines deadline *time.Timer @@ -209,6 +211,10 @@ func (g *Conn) Close() error { } } + if g.onClose != nil { + g.onClose() + } + return errors.Join(errorArr...) } @@ -240,6 +246,7 @@ type Transport struct { ctx context.Context cancel context.CancelFunc closeOnce sync.Once + count atomic.Int64 } func (t *Transport) Close() error { @@ -349,6 +356,9 @@ func (t *Transport) Dial() (net.Conn, error) { writer: writer, } + t.count.Add(1) + conn.onClose = func() { t.count.Add(-1) } + go conn.Init() // ensure conn.initOnce.Do has been called before return @@ -358,6 +368,78 @@ func (t *Transport) Dial() (net.Conn, error) { return conn, nil } +type Client struct { + mutex sync.Mutex + maxConnections int + minStreams int + maxStreams int + transports []*Transport + maker func() *Transport +} + +func NewClient(maker func() *Transport, maxConnections, minStreams, maxStreams int) *Client { + if maxConnections == 0 && minStreams == 0 && maxStreams == 0 { + maxConnections = 1 + } + return &Client{ + maxConnections: maxConnections, + minStreams: minStreams, + maxStreams: maxStreams, + maker: maker, + } +} + +func (c *Client) Dial() (net.Conn, error) { + return c.getTransport().Dial() +} + +func (c *Client) Close() error { + c.mutex.Lock() + defer c.mutex.Unlock() + var errs []error + for _, t := range c.transports { + if err := t.Close(); err != nil { + errs = append(errs, err) + } + } + c.transports = nil + return errors.Join(errs...) +} + +func (c *Client) getTransport() *Transport { + c.mutex.Lock() + defer c.mutex.Unlock() + var transport *Transport + for _, t := range c.transports { + if transport == nil || t.count.Load() < transport.count.Load() { + transport = t + } + } + if transport == nil { + return c.newTransportLocked() + } + numStreams := int(transport.count.Load()) + if numStreams == 0 { + return transport + } + if c.maxConnections > 0 { + if len(c.transports) >= c.maxConnections || numStreams < c.minStreams { + return transport + } + } else { + if c.maxStreams > 0 && numStreams < c.maxStreams { + return transport + } + } + return c.newTransportLocked() +} + +func (c *Client) newTransportLocked() *Transport { + transport := c.maker() + c.transports = append(c.transports, transport) + return transport +} + func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, gunCfg *Config) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { return conn, nil diff --git a/mihomo/transport/gun/server.go b/mihomo/transport/gun/server.go index 4f27891f7c..4c5280f7dd 100644 --- a/mihomo/transport/gun/server.go +++ b/mihomo/transport/gun/server.go @@ -112,11 +112,6 @@ func (w *h2ConnWrapper) CloseWrapper() { w.closed = true } -func (w *h2ConnWrapper) Close() error { - w.CloseWrapper() - return w.ExtendedConn.Close() -} - func (w *h2ConnWrapper) Upstream() any { return w.ExtendedConn } diff --git a/mihomo/transport/sudoku/kip.go b/mihomo/transport/sudoku/kip.go index 2f965034e9..354f0b5129 100644 --- a/mihomo/transport/sudoku/kip.go +++ b/mihomo/transport/sudoku/kip.go @@ -66,13 +66,7 @@ func WriteKIPMessage(w io.Writer, typ byte, payload []byte) error { hdr[3] = typ binary.BigEndian.PutUint16(hdr[4:], uint16(len(payload))) - if err := writeFull(w, hdr[:]); err != nil { - return err - } - if len(payload) == 0 { - return nil - } - return writeFull(w, payload) + return writeAllChunks(w, hdr[:], payload) } func ReadKIPMessage(r io.Reader) (*KIPMessage, error) { diff --git a/mihomo/transport/sudoku/multiplex/session.go b/mihomo/transport/sudoku/multiplex/session.go index 779fcafb5b..4344d8e7d3 100644 --- a/mihomo/transport/sudoku/multiplex/session.go +++ b/mihomo/transport/sudoku/multiplex/session.go @@ -173,16 +173,10 @@ func (s *Session) sendFrame(frameType byte, streamID uint32, payload []byte) err s.writeMu.Lock() defer s.writeMu.Unlock() - if err := writeFull(s.conn, header[:]); err != nil { + if err := writeAllChunks(s.conn, header[:], payload); err != nil { s.closeWithError(err) return err } - if len(payload) > 0 { - if err := writeFull(s.conn, payload); err != nil { - s.closeWithError(err) - return err - } - } return nil } @@ -315,17 +309,6 @@ func (s *Session) readLoop() { } } -func writeFull(w io.Writer, b []byte) error { - for len(b) > 0 { - n, err := w.Write(b) - if err != nil { - return err - } - b = b[n:] - } - return nil -} - func trimASCII(b []byte) string { i := 0 j := len(b) diff --git a/mihomo/transport/sudoku/multiplex/write_chunks.go b/mihomo/transport/sudoku/multiplex/write_chunks.go new file mode 100644 index 0000000000..dd381d3a1f --- /dev/null +++ b/mihomo/transport/sudoku/multiplex/write_chunks.go @@ -0,0 +1,19 @@ +package multiplex + +import "io" + +func writeAllChunks(w io.Writer, chunks ...[]byte) error { + for _, chunk := range chunks { + for len(chunk) > 0 { + n, err := w.Write(chunk) + if err != nil { + return err + } + if n == 0 { + return io.ErrShortWrite + } + chunk = chunk[n:] + } + } + return nil +} diff --git a/mihomo/transport/sudoku/obfs/sudoku/conn.go b/mihomo/transport/sudoku/obfs/sudoku/conn.go index 7a18d0f29a..12998d5e6a 100644 --- a/mihomo/transport/sudoku/obfs/sudoku/conn.go +++ b/mihomo/transport/sudoku/obfs/sudoku/conn.go @@ -3,12 +3,9 @@ package sudoku import ( "bufio" "bytes" - crypto_rand "crypto/rand" - "encoding/binary" - "errors" - "math/rand" "net" "sync" + "sync/atomic" ) const IOBufferSize = 32 * 1024 @@ -45,14 +42,17 @@ type Conn struct { table *Table reader *bufio.Reader recorder *bytes.Buffer - recording bool + recording atomic.Bool recordLock sync.Mutex rawBuf []byte - pendingData []byte - hintBuf []byte + pendingData pendingBuffer + hintBuf [4]byte + hintCount int + writeMu sync.Mutex + writeBuf []byte - rng *rand.Rand + rng randomSource paddingThreshold uint64 } @@ -77,33 +77,28 @@ func (sc *Conn) CloseRead() error { } func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn { - var seedBytes [8]byte - if _, err := crypto_rand.Read(seedBytes[:]); err != nil { - binary.BigEndian.PutUint64(seedBytes[:], uint64(rand.Int63())) - } - seed := int64(binary.BigEndian.Uint64(seedBytes[:])) - localRng := rand.New(rand.NewSource(seed)) + localRng := newSeededRand() sc := &Conn{ Conn: c, table: table, reader: bufio.NewReaderSize(c, IOBufferSize), rawBuf: make([]byte, IOBufferSize), - pendingData: make([]byte, 0, 4096), - hintBuf: make([]byte, 0, 4), + pendingData: newPendingBuffer(4096), + writeBuf: make([]byte, 0, 4096), rng: localRng, paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax), } if record { sc.recorder = new(bytes.Buffer) - sc.recording = true + sc.recording.Store(true) } return sc } func (sc *Conn) StopRecording() { sc.recordLock.Lock() - sc.recording = false + sc.recording.Store(false) sc.recorder = nil sc.recordLock.Unlock() } @@ -137,74 +132,50 @@ func (sc *Conn) Write(p []byte) (n int, err error) { return 0, nil } - outCapacity := len(p) * 6 - out := make([]byte, 0, outCapacity) - pads := sc.table.PaddingPool - padLen := len(pads) + sc.writeMu.Lock() + defer sc.writeMu.Unlock() - for _, b := range p { - if shouldPad(sc.rng, sc.paddingThreshold) { - out = append(out, pads[sc.rng.Intn(padLen)]) - } - - puzzles := sc.table.EncodeTable[b] - puzzle := puzzles[sc.rng.Intn(len(puzzles))] - - perm := perm4[sc.rng.Intn(len(perm4))] - for _, idx := range perm { - if shouldPad(sc.rng, sc.paddingThreshold) { - out = append(out, pads[sc.rng.Intn(padLen)]) - } - out = append(out, puzzle[idx]) - } - } - - if shouldPad(sc.rng, sc.paddingThreshold) { - out = append(out, pads[sc.rng.Intn(padLen)]) - } - - return len(p), writeFull(sc.Conn, out) + sc.writeBuf = encodeSudokuPayload(sc.writeBuf[:0], sc.table, sc.rng, sc.paddingThreshold, p) + return len(p), writeFull(sc.Conn, sc.writeBuf) } func (sc *Conn) Read(p []byte) (n int, err error) { - if len(sc.pendingData) > 0 { - n = copy(p, sc.pendingData) - if n == len(sc.pendingData) { - sc.pendingData = sc.pendingData[:0] - } else { - sc.pendingData = sc.pendingData[n:] - } + if n, ok := drainPending(p, &sc.pendingData); ok { return n, nil } for { - if len(sc.pendingData) > 0 { + if sc.pendingData.available() > 0 { break } nr, rErr := sc.reader.Read(sc.rawBuf) if nr > 0 { chunk := sc.rawBuf[:nr] - sc.recordLock.Lock() - if sc.recording { - sc.recorder.Write(chunk) + if sc.recording.Load() { + sc.recordLock.Lock() + if sc.recording.Load() && sc.recorder != nil { + sc.recorder.Write(chunk) + } + sc.recordLock.Unlock() } - sc.recordLock.Unlock() + layout := sc.table.layout for _, b := range chunk { - if !sc.table.layout.isHint(b) { + if !layout.hintTable[b] { continue } - sc.hintBuf = append(sc.hintBuf, b) - if len(sc.hintBuf) == 4 { - key := packHintsToKey([4]byte{sc.hintBuf[0], sc.hintBuf[1], sc.hintBuf[2], sc.hintBuf[3]}) + sc.hintBuf[sc.hintCount] = b + sc.hintCount++ + if sc.hintCount == len(sc.hintBuf) { + key := packHintsToKey(sc.hintBuf) val, ok := sc.table.DecodeMap[key] if !ok { - return 0, errors.New("INVALID_SUDOKU_MAP_MISS") + return 0, ErrInvalidSudokuMapMiss } - sc.pendingData = append(sc.pendingData, val) - sc.hintBuf = sc.hintBuf[:0] + sc.pendingData.appendByte(val) + sc.hintCount = 0 } } } @@ -212,16 +183,11 @@ func (sc *Conn) Read(p []byte) (n int, err error) { if rErr != nil { return 0, rErr } - if len(sc.pendingData) > 0 { + if sc.pendingData.available() > 0 { break } } - n = copy(p, sc.pendingData) - if n == len(sc.pendingData) { - sc.pendingData = sc.pendingData[:0] - } else { - sc.pendingData = sc.pendingData[n:] - } + n, _ = drainPending(p, &sc.pendingData) return n, nil } diff --git a/mihomo/transport/sudoku/obfs/sudoku/encode.go b/mihomo/transport/sudoku/obfs/sudoku/encode.go new file mode 100644 index 0000000000..cfcf571e38 --- /dev/null +++ b/mihomo/transport/sudoku/obfs/sudoku/encode.go @@ -0,0 +1,36 @@ +package sudoku + +func encodeSudokuPayload(dst []byte, table *Table, rng randomSource, paddingThreshold uint64, p []byte) []byte { + if len(p) == 0 { + return dst[:0] + } + + outCapacity := len(p)*6 + 1 + if cap(dst) < outCapacity { + dst = make([]byte, 0, outCapacity) + } + out := dst[:0] + pads := table.PaddingPool + padLen := len(pads) + + for _, b := range p { + if shouldPad(rng, paddingThreshold) { + out = append(out, pads[rng.Intn(padLen)]) + } + + puzzles := table.EncodeTable[b] + puzzle := puzzles[rng.Intn(len(puzzles))] + perm := perm4[rng.Intn(len(perm4))] + for _, idx := range perm { + if shouldPad(rng, paddingThreshold) { + out = append(out, pads[rng.Intn(padLen)]) + } + out = append(out, puzzle[idx]) + } + } + + if shouldPad(rng, paddingThreshold) { + out = append(out, pads[rng.Intn(padLen)]) + } + return out +} diff --git a/mihomo/transport/sudoku/obfs/sudoku/layout.go b/mihomo/transport/sudoku/obfs/sudoku/layout.go index b89933f361..2e1ec236a4 100644 --- a/mihomo/transport/sudoku/obfs/sudoku/layout.go +++ b/mihomo/transport/sudoku/obfs/sudoku/layout.go @@ -14,17 +14,30 @@ type byteLayout struct { padMarker byte paddingPool []byte - encodeHint func(val, pos byte) byte - encodeGroup func(group byte) byte - decodeGroup func(b byte) (byte, bool) + hintTable [256]bool + encodeHint [4][16]byte + encodeGroup [64]byte + decodeGroup [256]byte + groupValid [256]bool } func (l *byteLayout) isHint(b byte) bool { - if (b & l.hintMask) == l.hintValue { - return true + return l != nil && l.hintTable[b] +} + +func (l *byteLayout) hintByte(val, pos byte) byte { + return l.encodeHint[val&0x03][pos&0x0F] +} + +func (l *byteLayout) groupByte(group byte) byte { + return l.encodeGroup[group&0x3F] +} + +func (l *byteLayout) decodePackedGroup(b byte) (byte, bool) { + if l == nil { + return 0, false } - // ASCII layout maps the single non-printable marker (0x7F) to '\n' on the wire. - return l.name == "ascii" && b == '\n' + return l.decodeGroup[b], l.groupValid[b] } // resolveLayout picks the byte layout for a single traffic direction. @@ -50,38 +63,44 @@ func newASCIILayout() *byteLayout { for i := 0; i < 32; i++ { padding = append(padding, byte(0x20+i)) } - return &byteLayout{ + + layout := &byteLayout{ name: "ascii", hintMask: 0x40, hintValue: 0x40, padMarker: 0x3F, paddingPool: padding, - encodeHint: func(val, pos byte) byte { - b := 0x40 | ((val & 0x03) << 4) | (pos & 0x0F) - // Avoid DEL (0x7F) in prefer_ascii mode; map it to '\n' to reduce fingerprint. - if b == 0x7F { - return '\n' - } - return b - }, - encodeGroup: func(group byte) byte { - b := 0x40 | (group & 0x3F) - // Avoid DEL (0x7F) in prefer_ascii mode; map it to '\n' to reduce fingerprint. - if b == 0x7F { - return '\n' - } - return b - }, - decodeGroup: func(b byte) (byte, bool) { - if b == '\n' { - return 0x3F, true - } - if (b & 0x40) == 0 { - return 0, false - } - return b & 0x3F, true - }, } + + for val := 0; val < 4; val++ { + for pos := 0; pos < 16; pos++ { + b := byte(0x40 | (byte(val) << 4) | byte(pos)) + if b == 0x7F { + b = '\n' + } + layout.encodeHint[val][pos] = b + } + } + for group := 0; group < 64; group++ { + b := byte(0x40 | byte(group)) + if b == 0x7F { + b = '\n' + } + layout.encodeGroup[group] = b + } + for b := 0; b < 256; b++ { + wire := byte(b) + if (wire & 0x40) == 0x40 { + layout.hintTable[wire] = true + layout.decodeGroup[wire] = wire & 0x3F + layout.groupValid[wire] = true + } + } + layout.hintTable['\n'] = true + layout.decodeGroup['\n'] = 0x3F + layout.groupValid['\n'] = true + + return layout } func newEntropyLayout() *byteLayout { @@ -90,26 +109,35 @@ func newEntropyLayout() *byteLayout { padding = append(padding, byte(0x80+i)) padding = append(padding, byte(0x10+i)) } - return &byteLayout{ + + layout := &byteLayout{ name: "entropy", hintMask: 0x90, hintValue: 0x00, padMarker: 0x80, paddingPool: padding, - encodeHint: func(val, pos byte) byte { - return ((val & 0x03) << 5) | (pos & 0x0F) - }, - encodeGroup: func(group byte) byte { - v := group & 0x3F - return ((v & 0x30) << 1) | (v & 0x0F) - }, - decodeGroup: func(b byte) (byte, bool) { - if (b & 0x90) != 0 { - return 0, false - } - return ((b >> 1) & 0x30) | (b & 0x0F), true - }, } + + for val := 0; val < 4; val++ { + for pos := 0; pos < 16; pos++ { + layout.encodeHint[val][pos] = (byte(val) << 5) | byte(pos) + } + } + for group := 0; group < 64; group++ { + v := byte(group) + layout.encodeGroup[group] = ((v & 0x30) << 1) | (v & 0x0F) + } + for b := 0; b < 256; b++ { + wire := byte(b) + if (wire & 0x90) != 0 { + continue + } + layout.hintTable[wire] = true + layout.decodeGroup[wire] = ((wire >> 1) & 0x30) | (wire & 0x0F) + layout.groupValid[wire] = true + } + + return layout } func newCustomLayout(pattern string) (*byteLayout, error) { @@ -162,26 +190,6 @@ func newCustomLayout(pattern string) (*byteLayout, error) { return out } - decodeGroup := func(b byte) (byte, bool) { - if (b & xMask) != xMask { - return 0, false - } - var val, pos byte - if b&(1<> 4) & 0x03 - pos := group & 0x0F - return encodeBits(val, pos, -1) - }, - decodeGroup: decodeGroup, - }, nil + } + + for val := 0; val < 4; val++ { + for pos := 0; pos < 16; pos++ { + layout.encodeHint[val][pos] = encodeBits(byte(val), byte(pos), -1) + } + } + for group := 0; group < 64; group++ { + val := byte(group>>4) & 0x03 + pos := byte(group) & 0x0F + layout.encodeGroup[group] = encodeBits(val, pos, -1) + } + for b := 0; b < 256; b++ { + wire := byte(b) + if (wire & xMask) != xMask { + continue + } + layout.hintTable[wire] = true + + var val, pos byte + if wire&(1< 0 { - return pc.drainPendingData(p), nil + if n, ok := drainPending(p, &pc.pendingData); ok { + return n, nil } for { @@ -320,7 +302,7 @@ func (pc *PackedConn) Read(p []byte) (int, error) { layout := pc.table.layout for _, b := range pc.rawBuf[:nr] { - if !layout.isHint(b) { + if !layout.hintTable[b] { if b == padMarker { rBuf = 0 rBits = 0 @@ -328,7 +310,7 @@ func (pc *PackedConn) Read(p []byte) (int, error) { continue } - group, ok := layout.decodeGroup(b) + group, ok := layout.decodePackedGroup(b) if !ok { return 0, ErrInvalidSudokuMapMiss } @@ -339,7 +321,12 @@ func (pc *PackedConn) Read(p []byte) (int, error) { if rBits >= 8 { rBits -= 8 val := byte(rBuf >> rBits) - pc.pendingData = append(pc.pendingData, val) + pc.pendingData.appendByte(val) + if rBits == 0 { + rBuf = 0 + } else { + rBuf &= (uint64(1) << rBits) - 1 + } } } @@ -352,24 +339,21 @@ func (pc *PackedConn) Read(p []byte) (int, error) { pc.readBitBuf = 0 pc.readBits = 0 } - if len(pc.pendingData) > 0 { + if pc.pendingData.available() > 0 { break } return 0, rErr } - if len(pc.pendingData) > 0 { + if pc.pendingData.available() > 0 { break } } - return pc.drainPendingData(p), nil + n, _ := drainPending(p, &pc.pendingData) + return n, nil } func (pc *PackedConn) getPaddingByte() byte { return pc.padPool[pc.rng.Intn(len(pc.padPool))] } - -func (pc *PackedConn) encodeGroup(group byte) byte { - return pc.table.layout.encodeGroup(group) -} diff --git a/mihomo/transport/sudoku/obfs/sudoku/padding_prob.go b/mihomo/transport/sudoku/obfs/sudoku/padding_prob.go index f23a883b86..00ff68ffe4 100644 --- a/mihomo/transport/sudoku/obfs/sudoku/padding_prob.go +++ b/mihomo/transport/sudoku/obfs/sudoku/padding_prob.go @@ -1,10 +1,8 @@ package sudoku -import "math/rand" - const probOne = uint64(1) << 32 -func pickPaddingThreshold(r *rand.Rand, pMin, pMax int) uint64 { +func pickPaddingThreshold(r randomSource, pMin, pMax int) uint64 { if r == nil { return 0 } @@ -30,7 +28,7 @@ func pickPaddingThreshold(r *rand.Rand, pMin, pMax int) uint64 { return min + (u * (max - min) >> 32) } -func shouldPad(r *rand.Rand, threshold uint64) bool { +func shouldPad(r randomSource, threshold uint64) bool { if threshold == 0 { return false } diff --git a/mihomo/transport/sudoku/obfs/sudoku/pending.go b/mihomo/transport/sudoku/obfs/sudoku/pending.go new file mode 100644 index 0000000000..60f09a12f7 --- /dev/null +++ b/mihomo/transport/sudoku/obfs/sudoku/pending.go @@ -0,0 +1,57 @@ +package sudoku + +type pendingBuffer struct { + data []byte + off int +} + +func newPendingBuffer(capacity int) pendingBuffer { + return pendingBuffer{data: make([]byte, 0, capacity)} +} + +func (p *pendingBuffer) available() int { + if p == nil { + return 0 + } + return len(p.data) - p.off +} + +func (p *pendingBuffer) reset() { + if p == nil { + return + } + p.data = p.data[:0] + p.off = 0 +} + +func (p *pendingBuffer) ensureAppendCapacity(extra int) { + if p == nil || extra <= 0 || p.off == 0 { + return + } + if cap(p.data)-len(p.data) >= extra { + return + } + + unread := len(p.data) - p.off + copy(p.data[:unread], p.data[p.off:]) + p.data = p.data[:unread] + p.off = 0 +} + +func (p *pendingBuffer) appendByte(b byte) { + p.ensureAppendCapacity(1) + p.data = append(p.data, b) +} + +func drainPending(dst []byte, pending *pendingBuffer) (int, bool) { + if pending == nil || pending.available() == 0 { + return 0, false + } + + n := copy(dst, pending.data[pending.off:]) + pending.off += n + if pending.off == len(pending.data) { + pending.reset() + } + return n, true +} diff --git a/mihomo/transport/sudoku/obfs/sudoku/rand.go b/mihomo/transport/sudoku/obfs/sudoku/rand.go new file mode 100644 index 0000000000..abe72d8054 --- /dev/null +++ b/mihomo/transport/sudoku/obfs/sudoku/rand.go @@ -0,0 +1,56 @@ +package sudoku + +import ( + crypto_rand "crypto/rand" + "encoding/binary" + "time" +) + +type randomSource interface { + Uint32() uint32 + Uint64() uint64 + Intn(n int) int +} + +type sudokuRand struct { + state uint64 +} + +func newSeededRand() *sudokuRand { + seed := time.Now().UnixNano() + var seedBytes [8]byte + if _, err := crypto_rand.Read(seedBytes[:]); err == nil { + seed = int64(binary.BigEndian.Uint64(seedBytes[:])) + } + return newSudokuRand(seed) +} + +func newSudokuRand(seed int64) *sudokuRand { + state := uint64(seed) + if state == 0 { + state = 0x9e3779b97f4a7c15 + } + return &sudokuRand{state: state} +} + +func (r *sudokuRand) Uint64() uint64 { + if r == nil { + return 0 + } + r.state += 0x9e3779b97f4a7c15 + z := r.state + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9 + z = (z ^ (z >> 27)) * 0x94d049bb133111eb + return z ^ (z >> 31) +} + +func (r *sudokuRand) Uint32() uint32 { + return uint32(r.Uint64() >> 32) +} + +func (r *sudokuRand) Intn(n int) int { + if n <= 1 { + return 0 + } + return int((uint64(r.Uint32()) * uint64(n)) >> 32) +} diff --git a/mihomo/transport/sudoku/obfs/sudoku/table.go b/mihomo/transport/sudoku/obfs/sudoku/table.go index 201ca2cd80..b32506ae5e 100644 --- a/mihomo/transport/sudoku/obfs/sudoku/table.go +++ b/mihomo/transport/sudoku/obfs/sudoku/table.go @@ -146,7 +146,7 @@ func newSingleDirectionTable(key string, mode string, customPattern string) (*Ta if matchCount == 1 { // 唯一确定,生成最终编码字节 for i, p := range rawParts { - currentHints[i] = t.layout.encodeHint(p.val-1, p.pos) + currentHints[i] = t.layout.hintByte(p.val-1, p.pos) } t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints) diff --git a/mihomo/transport/sudoku/uot.go b/mihomo/transport/sudoku/uot.go index 054108a6bb..854b3b5035 100644 --- a/mihomo/transport/sudoku/uot.go +++ b/mihomo/transport/sudoku/uot.go @@ -8,7 +8,6 @@ import ( "io" "net" "net/netip" - "strconv" "sync" "time" @@ -37,42 +36,15 @@ func WriteDatagram(w io.Writer, addr string, payload []byte) error { binary.BigEndian.PutUint16(header[:2], uint16(len(addrBuf))) binary.BigEndian.PutUint16(header[2:], uint16(len(payload))) - if err := writeFull(w, header[:]); err != nil { - return err - } - if err := writeFull(w, addrBuf); err != nil { - return err - } - return writeFull(w, payload) + return writeAllChunks(w, header[:], addrBuf, payload) } // ReadDatagram parses a single UDP datagram frame from the reliable stream. func ReadDatagram(r io.Reader) (string, []byte, error) { - var header [4]byte - if _, err := io.ReadFull(r, header[:]); err != nil { - return "", nil, err - } - - addrLen := int(binary.BigEndian.Uint16(header[:2])) - payloadLen := int(binary.BigEndian.Uint16(header[2:])) - - if addrLen <= 0 || addrLen > maxUoTPayload { - return "", nil, fmt.Errorf("invalid address length: %d", addrLen) - } - if payloadLen < 0 || payloadLen > maxUoTPayload { - return "", nil, fmt.Errorf("invalid payload length: %d", payloadLen) - } - - addrBuf := make([]byte, addrLen) - if _, err := io.ReadFull(r, addrBuf); err != nil { - return "", nil, err - } - - addr, err := DecodeAddress(bytes.NewReader(addrBuf)) + addr, payloadLen, err := readDatagramHeaderAndAddress(r) if err != nil { - return "", nil, fmt.Errorf("decode address: %w", err) + return "", nil, err } - payload := make([]byte, payloadLen) if _, err := io.ReadFull(r, payload); err != nil { return "", nil, err @@ -93,26 +65,29 @@ func NewUoTPacketConn(conn net.Conn) *UoTPacketConn { func (c *UoTPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { for { - addrStr, payload, err := ReadDatagram(c.conn) + addrStr, payloadLen, err := readDatagramHeaderAndAddress(c.conn) if err != nil { return 0, nil, err } - if len(payload) > len(p) { + udpAddr, err := parseDatagramUDPAddr(addrStr) + if payloadLen > len(p) { + if discardErr := discardBytes(c.conn, payloadLen); discardErr != nil { + return 0, nil, discardErr + } return 0, nil, io.ErrShortBuffer } - - host, port, _ := net.SplitHostPort(addrStr) - portInt, _ := strconv.ParseUint(port, 10, 16) - ip, err := netip.ParseAddr(host) - if err != nil { // disallow domain addr at here, just ignore + if err != nil { + if discardErr := discardBytes(c.conn, payloadLen); discardErr != nil { + return 0, nil, discardErr + } log.Debugln("[Sudoku][UoT] discard datagram with invalid address %s: %v", addrStr, err) continue } - udpAddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip.Unmap(), uint16(portInt))) - - copy(p, payload) - return len(payload), udpAddr, nil + if _, err := io.ReadFull(c.conn, p[:payloadLen]); err != nil { + return 0, nil, err + } + return payloadLen, udpAddr, nil } } @@ -147,3 +122,46 @@ func (c *UoTPacketConn) SetReadDeadline(t time.Time) error { func (c *UoTPacketConn) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) } + +func readDatagramHeaderAndAddress(r io.Reader) (string, int, error) { + var header [4]byte + if _, err := io.ReadFull(r, header[:]); err != nil { + return "", 0, err + } + + addrLen := int(binary.BigEndian.Uint16(header[:2])) + payloadLen := int(binary.BigEndian.Uint16(header[2:])) + if addrLen <= 0 || addrLen > maxUoTPayload { + return "", 0, fmt.Errorf("invalid address length: %d", addrLen) + } + if payloadLen < 0 || payloadLen > maxUoTPayload { + return "", 0, fmt.Errorf("invalid payload length: %d", payloadLen) + } + + addrBuf := make([]byte, addrLen) + if _, err := io.ReadFull(r, addrBuf); err != nil { + return "", 0, err + } + + addr, err := DecodeAddress(bytes.NewReader(addrBuf)) + if err != nil { + return "", 0, fmt.Errorf("decode address: %w", err) + } + return addr, payloadLen, nil +} + +func parseDatagramUDPAddr(addr string) (*net.UDPAddr, error) { + addrPort, err := netip.ParseAddrPort(addr) + if err != nil { + return nil, err + } + return net.UDPAddrFromAddrPort(netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())), nil +} + +func discardBytes(r io.Reader, n int) error { + if n <= 0 { + return nil + } + _, err := io.CopyN(io.Discard, r, int64(n)) + return err +} diff --git a/mihomo/transport/sudoku/write_chunks.go b/mihomo/transport/sudoku/write_chunks.go new file mode 100644 index 0000000000..9c4ee8c784 --- /dev/null +++ b/mihomo/transport/sudoku/write_chunks.go @@ -0,0 +1,19 @@ +package sudoku + +import "io" + +func writeAllChunks(w io.Writer, chunks ...[]byte) error { + for _, chunk := range chunks { + for len(chunk) > 0 { + n, err := w.Write(chunk) + if err != nil { + return err + } + if n == 0 { + return io.ErrShortWrite + } + chunk = chunk[n:] + } + } + return nil +} diff --git a/openwrt-packages/filebrowser/Makefile b/openwrt-packages/filebrowser/Makefile index 4f6e7332f4..7ca6c0d12b 100644 --- a/openwrt-packages/filebrowser/Makefile +++ b/openwrt-packages/filebrowser/Makefile @@ -5,12 +5,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=filebrowser -PKG_VERSION:=2.63.0 +PKG_VERSION:=2.63.1 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/filebrowser/filebrowser/tar.gz/v${PKG_VERSION}? -PKG_HASH:=68ef79b4e48aa80a4921f395a55b6ba9efa58c0fa95a6de0915fd27348af59bf +PKG_HASH:=6f1b63bc8add3807d67c09d03ee60d2282255fe11b6ab6fcfc06266e9fb743fd PKG_LICENSE:=Apache-2.0 PKG_LICENSE_FILES:=LICENSE diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 5ee76b87db..4abd662730 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -1955,7 +1955,7 @@ function gen_config(var) } }) for index, value in ipairs(config.outbounds) do - if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and value.server_port and not no_run then + if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and (value.server_port or value.server_ports) and not no_run then sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list")) end for k, v in pairs(config.outbounds[index]) do diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua index 730ce8035f..a44259ed17 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -1064,17 +1064,17 @@ function gen_config(var) local to_node = get_node_by_id(node.to_node) if to_node then -- Landing Node not support use special node. - if to_node.protocol:find("^_") then + if to_node.protocol and to_node.protocol:find("^_") then to_node = nil end end if to_node then local to_outbound if to_node.type ~= "Xray" then - local tag = to_node[".name"] + local in_tag = "inbound_" .. to_node[".name"] .. "_" .. tostring(outbound.tag) local new_port = api.get_new_port() table.insert(inbounds, { - tag = tag, + tag = in_tag, listen = "127.0.0.1", port = new_port, protocol = "dokodemo-door", @@ -1086,11 +1086,11 @@ function gen_config(var) to_node.address = "127.0.0.1" to_node.port = new_port table.insert(rules, 1, { - inboundTag = {tag}, + inboundTag = {in_tag}, outboundTag = outbound.tag }) - to_outbound = gen_outbound(node[".name"], to_node, tag, { - tag = tag, + to_outbound = gen_outbound(node[".name"], to_node, to_node[".name"], { + tag = to_node[".name"], run_socks_instance = not no_run }) else @@ -1734,14 +1734,10 @@ function gen_config(var) else table.insert(outbounds, blackhole_outbound) end - for index, value in ipairs(config.outbounds) do - local s = value.settings - if not value["_flag_proxy_tag"] and value["_id"] and s and not no_run and - ((s.vnext and s.vnext[1] and s.vnext[1].address and s.vnext[1].port) or - (s.servers and s.servers[1] and s.servers[1].address and s.servers[1].port) or - (s.peers and s.peers[1] and s.peers[1].endpoint) or - (s.address and s.port)) then + local pt = value.protocol + local exclude = { blackhole=1, dns=1, freedom=1, loopback=1 } + if not value["_flag_proxy_tag"] and value["_id"] and pt and not exclude[pt] and not no_run then sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list")) end for k, v in pairs(config.outbounds[index]) do diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh index 4acc19cf57..fc427ad2bf 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh @@ -1101,6 +1101,9 @@ socks_node_switch() { LOG_FILE="/dev/null" run_socks flag=$flag node=$new_node bind=$bind socks_port=$port config_file=$config_file http_port=$http_port http_config_file=$http_config_file log_file=$log_file set_cache_var "socks_${flag}" "$new_node" + local ENABLED_DEFAULT_ACL=$(get_cache_var "ENABLED_DEFAULT_ACL") + local ENABLED_ACLS=$(get_cache_var "ENABLED_ACLS") + [ "$ENABLED_DEFAULT_ACL" != "1" -a "$ENABLED_ACLS" != "1" ] && return local USE_TABLES=$(get_cache_var "USE_TABLES") [ -n "$USE_TABLES" ] && source $APP_PATH/${USE_TABLES}.sh filter_direct_node_list } @@ -1979,6 +1982,8 @@ get_config() { [ "$ENABLED_ACLS" = 1 ] && { [ "$(uci show ${CONFIG} | grep "@acl_rule" | grep "enabled='1'" | wc -l)" == 0 ] && ENABLED_ACLS=0 } + set_cache_var ENABLED_DEFAULT_ACL $ENABLED_DEFAULT_ACL + set_cache_var ENABLED_ACLS $ENABLED_ACLS TCP_PROXY_WAY=$(config_t_get global_forwarding tcp_proxy_way redirect) PROXY_IPV6=$(config_t_get global_forwarding ipv6_tproxy 0) diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/iptables.sh b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/iptables.sh index b14ee20daf..f399a2e0cc 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/iptables.sh +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/iptables.sh @@ -742,43 +742,50 @@ filter_vpsip() { } filter_server_port() { - local address=${1} - local port=${2} - local stream=${3} - stream=$(echo ${3} | tr 'A-Z' 'a-z') - local _is_tproxy ipt_tmp - ipt_tmp=$ipt_n - _is_tproxy=${is_tproxy} - [ "$stream" == "udp" ] && _is_tproxy="TPROXY" - [ -n "${_is_tproxy}" ] && ipt_tmp=$ipt_m - - for _ipt in 4 6; do - [ "$_ipt" == "4" ] && _ipt=$ipt_tmp - [ "$_ipt" == "6" ] && _ipt=$ip6t_m - $_ipt -n -L PSW_OUTPUT | grep -q "${address}:${port}" - if [ $? -ne 0 ]; then - $_ipt -I PSW_OUTPUT $(comment "${address}:${port}") -p $stream -d $address --dport $port -j RETURN 2>/dev/null + local address="$1" + local port=$(echo "$2" | tr '-' ':' | tr -d ' ') + local stream=$(echo "$3" | tr 'A-Z' 'a-z') + local ipt_tmp="$ipt_n" _is_tproxy _ipt_cmd _ver multi_ports p ports + [ "$(config_t_get global_forwarding tcp_proxy_way redirect)" = "tproxy" ] && _is_tproxy="TPROXY" + [ "$stream" = "udp" ] && _is_tproxy="TPROXY" + [ -n "$_is_tproxy" ] && ipt_tmp="$ipt_m" + for _ver in 4 6; do + [ "$_ver" = "4" ] && _ipt_cmd="$ipt_tmp" + [ "$_ver" = "6" ] && _ipt_cmd="$ip6t_m" + multi_ports="" + for p in $(echo "$port" | tr ',' ' '); do + case "$p" in + *:* ) + $_ipt_cmd -n -L PSW_OUTPUT 2>/dev/null | grep -q "${address}:${p}:${stream}" || \ + $_ipt_cmd -I PSW_OUTPUT $(comment "${address}:${p}:${stream}") -p "$stream" -d "$address" --dport "$p" -j RETURN 2>/dev/null + ;; + * ) + multi_ports="${multi_ports},$p" + ;; + esac + done + if [ -n "$multi_ports" ]; then + ports=$(printf "%s\n" "${multi_ports#,}" | tr ',' '\n' | sort -n | tr '\n' ',' | sed 's/,$//') + $_ipt_cmd -n -L PSW_OUTPUT 2>/dev/null | grep -q "${address}:${ports}:${stream}" || \ + $_ipt_cmd -I PSW_OUTPUT $(comment "${address}:${ports}:${stream}") -p "$stream" -d "$address" -m multiport --dports "$ports" -j RETURN 2>/dev/null fi done } filter_node() { - local node=${1} - local stream=${2} - if [ -n "$node" ]; then - local address=$(config_n_get $node address) - local port=$(config_n_get $node port) - [ -z "$address" ] && [ -z "$port" ] && { - return 1 - } - filter_server_port $address $port $stream - filter_server_port $address $port $stream - fi + local node="$1" stream="$2" + [ -z "$node" ] && return 1 + local address=$(config_n_get "$node" address) + local port=$(config_n_get "$node" port) + local hop=$(config_n_get "$node" hysteria2_hop) + [ -n "$hop" ] && port="${port:+$port,}$hop" + [ -z "$address" -o -z "$port" ] && return 1 + filter_server_port "$address" "$port" "$stream" } filter_direct_node_list() { [ ! -s "$TMP_PATH/direct_node_list" ] && return - for _node_id in $(cat $TMP_PATH/direct_node_list | awk '!seen[$0]++'); do + awk '!seen[$0]++' "$TMP_PATH/direct_node_list" | while read -r _node_id; do filter_node "$_node_id" TCP filter_node "$_node_id" UDP unset _node_id diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/nftables.sh b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/nftables.sh index 12a64e4203..de6b3506b9 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/nftables.sh +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/nftables.sh @@ -793,41 +793,40 @@ filter_vpsip() { } filter_server_port() { - local address=${1} - local port=${2} - local stream=${3} - stream=$(echo ${3} | tr 'A-Z' 'a-z') - local _is_tproxy - _is_tproxy=${is_tproxy} - [ "$stream" == "udp" ] && _is_tproxy="TPROXY" - - for _ipt in 4 6; do - [ "$_ipt" == "4" ] && _ip_type=ip - [ "$_ipt" == "6" ] && _ip_type=ip6 - nft "list chain $NFTABLE_NAME $nft_output_chain" 2>/dev/null | grep -q "${address}:${port}" - if [ $? -ne 0 ]; then - nft "insert rule $NFTABLE_NAME $nft_output_chain meta l4proto $stream $_ip_type daddr $address $stream dport $port return comment \"${address}:${port}\"" 2>/dev/null - fi + local address="$1" + local port=$(echo "$2" | tr ':' '-' | tr -d ' ') + local stream=$(echo "$3" | tr 'A-Z' 'a-z') + local _ip_type _port_expr _ver _is_tproxy + local _nft_output_chain="PSW_OUTPUT_NAT" + [ "$(config_t_get global_forwarding tcp_proxy_way redirect)" = "tproxy" ] && _is_tproxy="TPROXY" + [ "$stream" = "udp" ] && _is_tproxy="TPROXY" + [ -n "$_is_tproxy" ] && _nft_output_chain="PSW_OUTPUT_MANGLE" + case "$port" in + *,*) _port_expr="{ $port }" ;; + *) _port_expr="$port" ;; + esac + for _ver in 4 6; do + [ "$_ver" = "4" ] && _ip_type="ip" + [ "$_ver" = "6" ] && _ip_type="ip6" && _nft_output_chain="PSW_OUTPUT_MANGLE_V6" + nft list chain "$NFTABLE_NAME" "$_nft_output_chain" 2>/dev/null | grep -q "comment \"${address}:${port}:${stream}\"" || \ + nft insert rule "$NFTABLE_NAME" "$_nft_output_chain" meta l4proto "$stream" $_ip_type daddr "$address" "$stream" dport $_port_expr return comment "\"${address}:${port}:${stream}\"" 2>/dev/null done } filter_node() { - local node=${1} - local stream=${2} - if [ -n "$node" ]; then - local address=$(config_n_get $node address) - local port=$(config_n_get $node port) - [ -z "$address" ] && [ -z "$port" ] && { - return 1 - } - filter_server_port $address $port $stream - filter_server_port $address $port $stream - fi + local node="$1" stream="$2" + [ -z "$node" ] && return 1 + local address=$(config_n_get "$node" address) + local port=$(config_n_get "$node" port) + local hop=$(config_n_get "$node" hysteria2_hop) + [ -n "$hop" ] && port="${port:+$port,}$hop" + [ -z "$address" -o -z "$port" ] && return 1 + filter_server_port "$address" "$port" "$stream" } filter_direct_node_list() { [ ! -s "$TMP_PATH/direct_node_list" ] && return - for _node_id in $(cat $TMP_PATH/direct_node_list | awk '!seen[$0]++'); do + awk '!seen[$0]++' "$TMP_PATH/direct_node_list" | while read -r _node_id; do filter_node "$_node_id" TCP filter_node "$_node_id" UDP unset _node_id diff --git a/openwrt-passwall2/luci-app-passwall2/Makefile b/openwrt-passwall2/luci-app-passwall2/Makefile index 0f78aadabd..dc04466d25 100644 --- a/openwrt-passwall2/luci-app-passwall2/Makefile +++ b/openwrt-passwall2/luci-app-passwall2/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 -PKG_VERSION:=26.4.2 +PKG_VERSION:=26.4.5 PKG_RELEASE:=1 PKG_PO_VERSION:=$(PKG_VERSION) diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua index 99adab0683..25913c94d8 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua @@ -150,6 +150,9 @@ o = s:option(Flag, "accept_icmpv6", translate("Hijacking ICMPv6 (IPv6 PING)")) o:depends("ipv6_tproxy", true) o.default = 0 +o = s:option(DynamicList, "force_proxy_lan_ip", translate("Force Proxy LAN IP"), translate("By default, commonly used internal network IP ranges will be connect directly (not entering the core). If you want a certain network range to go through a proxy, please add it here.")) +o.datatype = "or(ipmask4,ipmask6)" + if has_xray then s_xray = m:section(TypedSection, "global_xray", "Xray " .. translate("Settings")) s_xray.anonymous = true diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index 9e9a67a7e1..fa25087004 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -1119,9 +1119,22 @@ function gen_config(var) end if node then + if node.protocol ~= "_shunt" then + -- create shunt logic + local tmp_node = { + remarks = node.remarks, + type = "sing-box", + protocol = "_shunt", + default_node = node[".name"], + } + tmp_node.fakedns = remote_dns_fake + tmp_node.default_fakedns = remote_dns_fake + node = tmp_node + end + if server_host and server_port then - node.address = server_host - node.port = server_port + default_node_address = server_host + default_node_port = server_port end function gen_socks_config_node(node_id, socks_id, remarks) @@ -1369,6 +1382,12 @@ function gen_config(var) sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface)) end else + if tag == "default" then + if default_node_address and default_node_port then + node.address = default_node_address + node.port = default_node_port + end + end for _, _outbound in ipairs(outbounds) do -- Avoid generating duplicate nested processes if _outbound["_flag_proxy_tag"] and _outbound["_flag_proxy_tag"]:find("socks <- " .. node[".name"], 1, true) then @@ -1631,12 +1650,6 @@ function gen_config(var) table.insert(rules, rule) end end) - else - COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node, nil, { - fragment = singbox_settings.fragment == "1" or nil, - record_fragment = singbox_settings.record_fragment == "1" or nil, - run_socks_instance = not no_run - }) end for index, value in ipairs(rules) do @@ -1875,17 +1888,27 @@ function gen_config(var) end end end - - if remote_dns_fake and default_dns_flag == "remote" then - -- When default is not direct and enable fakedns, default DNS use FakeDNS. - local fakedns_dns_rule = { - query_type = { - "A", "AAAA" - }, - server = fakedns_tag, - disable_cache = true - } - table.insert(dns.rules, fakedns_dns_rule) + if default_dns_flag == "remote" then + if remote_dns_fake then + -- When default is not direct and enable fakedns, default DNS use FakeDNS. + local fakedns_dns_rule = { + query_type = { + "A", "AAAA" + }, + server = fakedns_tag, + disable_cache = true, + rewrite_ttl = 30, + strategy = remote_strategy, + } + table.insert(dns.rules, fakedns_dns_rule) + else + local remote_dns_rule = { + server = "remote", + disable_cache = true, + strategy = remote_strategy, + } + table.insert(dns.rules, remote_dns_rule) + end end local dns_in_inbound = { type = "direct", diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua index ce9d713afc..6093ad4fae 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -1035,14 +1035,25 @@ function gen_config(var) else local preproxy_node = get_node_by_id(node.preproxy_node) if preproxy_node then - local preproxy_outbound = gen_outbound(node[".name"], preproxy_node) + local preproxy_outbound, exist + if preproxy_node.protocol == "_balancing" then + local balancer_tag, loopback_outbound = gen_balancer(preproxy_node) + if loopback_outbound then + preproxy_outbound = loopback_outbound + exist = true + end + else + preproxy_outbound = gen_outbound(node[".name"], preproxy_node) + end if preproxy_outbound then outbound.tag = preproxy_outbound.tag .. " -> " .. outbound.tag outbound.proxySettings = { tag = preproxy_outbound.tag, transportLayer = true } - last_insert_outbound = preproxy_outbound + if not exist then + last_insert_outbound = preproxy_outbound + end default_outTag = outbound.tag end end @@ -1118,6 +1129,12 @@ function gen_config(var) proxy_table.preproxy_node = nil proxy_table.to_node = nil end + if tag == "default" then + if default_node_address and default_node_port then + node.address = default_node_address + node.port = default_node_port + end + end local outbound, has_add_outbound for _, _outbound in ipairs(outbounds) do -- Avoid generating duplicate nested processes @@ -1170,10 +1187,24 @@ function gen_config(var) end if node then - if server_host and server_port then - node.address = server_host - node.port = server_port + if node.protocol ~= "_shunt" then + -- create shunt logic + local tmp_node = { + remarks = node.remarks, + type = "Xray", + protocol = "_shunt", + default_node = node[".name"], + } + tmp_node.fakedns = remote_dns_fake + tmp_node.default_fakedns = remote_dns_fake + node = tmp_node end + + if server_host and server_port then + default_node_address = server_host + default_node_port = server_port + end + if node.protocol == "_shunt" then inner_fakedns = node.fakedns or "0" @@ -1332,25 +1363,6 @@ function gen_config(var) balancers = #balancers > 0 and balancers or nil, rules = rules } - else - COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node, nil, { - fragment = xray_settings.fragment == "1" or nil, - noise = xray_settings.noise == "1" or nil, - run_socks_instance = not no_run - }) - if COMMON.default_outbound_tag then - routing = { - domainStrategy = "AsIs", - domainMatcher = "hybrid", - balancers = #balancers > 0 and balancers or nil, - rules = rules - } - table.insert(routing.rules, { - ruleTag = "default", - network = "tcp,udp", - outboundTag = COMMON.default_outbound_tag - }) - end end end diff --git a/openwrt-passwall2/luci-app-passwall2/po/fa/passwall2.po b/openwrt-passwall2/luci-app-passwall2/po/fa/passwall2.po index f5375db2c3..177d1409d9 100644 --- a/openwrt-passwall2/luci-app-passwall2/po/fa/passwall2.po +++ b/openwrt-passwall2/luci-app-passwall2/po/fa/passwall2.po @@ -747,6 +747,12 @@ msgstr "ربودن ICMP (PING)" msgid "Hijacking ICMPv6 (IPv6 PING)" msgstr "ربودن ICMPv6 (IPv6 PING)" +msgid "Force Proxy LAN IP" +msgstr "IP پروکسی LAN را مجبور کنید" + +msgid "By default, commonly used internal network IP ranges will be connect directly (not entering the core). If you want a certain network range to go through a proxy, please add it here." +msgstr "به طور پیش‌فرض، محدوده‌های IP شبکه داخلی که معمولاً استفاده می‌شوند، مستقیماً متصل می‌شوند (و وارد هسته نمی‌شوند). اگر می‌خواهید محدوده شبکه خاصی از طریق پروکسی عبور کند، لطفاً آن را اینجا اضافه کنید." + msgid "Sniffing" msgstr "شنود (Sniffing)" diff --git a/openwrt-passwall2/luci-app-passwall2/po/ru/passwall2.po b/openwrt-passwall2/luci-app-passwall2/po/ru/passwall2.po index 9f017e442c..c413809f7a 100644 --- a/openwrt-passwall2/luci-app-passwall2/po/ru/passwall2.po +++ b/openwrt-passwall2/luci-app-passwall2/po/ru/passwall2.po @@ -748,6 +748,12 @@ msgstr "Перехват ICMP (PING)" msgid "Hijacking ICMPv6 (IPv6 PING)" msgstr "Перехват ICMPv6 (IPv6 PING)" +msgid "Force Proxy LAN IP" +msgstr "Принудительное использование IP-адреса локальной сети (Local IP)" + +msgid "By default, commonly used internal network IP ranges will be connect directly (not entering the core). If you want a certain network range to go through a proxy, please add it here." +msgstr "По умолчанию, часто используемые диапазоны IP-адресов внутренней сети будут подключаться напрямую (без доступа к ядру сети). Если вы хотите, чтобы определенный диапазон IP-адресов сети проходил через прокси-сервер, укажите это здесь." + msgid "Sniffing" msgstr "Анализ трафика" diff --git a/openwrt-passwall2/luci-app-passwall2/po/zh-cn/passwall2.po b/openwrt-passwall2/luci-app-passwall2/po/zh-cn/passwall2.po index 64afcf38d9..7539794329 100644 --- a/openwrt-passwall2/luci-app-passwall2/po/zh-cn/passwall2.po +++ b/openwrt-passwall2/luci-app-passwall2/po/zh-cn/passwall2.po @@ -739,6 +739,12 @@ msgstr "劫持ICMP (PING)" msgid "Hijacking ICMPv6 (IPv6 PING)" msgstr "劫持ICMPv6 (IPv6 PING)" +msgid "Force Proxy LAN IP" +msgstr "强制代理内网IP段" + +msgid "By default, commonly used internal network IP ranges will be connect directly (not entering the core). If you want a certain network range to go through a proxy, please add it here." +msgstr "默认情况下,常用内网IP网段将直连(不进入内核),如果你希望某个网段走代理,请在此添加。" + msgid "Sniffing" msgstr "流量嗅探" diff --git a/openwrt-passwall2/luci-app-passwall2/po/zh-tw/passwall2.po b/openwrt-passwall2/luci-app-passwall2/po/zh-tw/passwall2.po index 4f3b2e284b..a5bbe35182 100644 --- a/openwrt-passwall2/luci-app-passwall2/po/zh-tw/passwall2.po +++ b/openwrt-passwall2/luci-app-passwall2/po/zh-tw/passwall2.po @@ -739,6 +739,12 @@ msgstr "劫持ICMP (PING)" msgid "Hijacking ICMPv6 (IPv6 PING)" msgstr "劫持ICMPv6 (IPv6 PING)" +msgid "Force Proxy LAN IP" +msgstr "強制代理內網IP段" + +msgid "By default, commonly used internal network IP ranges will be connect directly (not entering the core). If you want a certain network range to go through a proxy, please add it here." +msgstr "預設情況下,常用內網IP網段將直連(不進入核心),如果你希望某個網段走代理,請在此新增。" + msgid "Sniffing" msgstr "流量嗅探" diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/iptables.sh b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/iptables.sh index c46ef51493..35f5df9689 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/iptables.sh +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/iptables.sh @@ -4,14 +4,16 @@ DIR="$(cd "$(dirname "$0")" && pwd)" MY_PATH=$DIR/iptables.sh UTILS_PATH=$DIR/utils.sh IPSET_LOCAL="passwall2_local" -IPSET_WAN="passwall2_wan" +IPSET_PROXY_LAN="passwall2_proxy_lan" IPSET_LAN="passwall2_lan" IPSET_VPS="passwall2_vps" +IPSET_WAN="passwall2_wan" IPSET_LOCAL6="passwall2_local6" -IPSET_WAN6="passwall2_wan6" +IPSET_PROXY_LAN6="passwall2_proxy_lan6" IPSET_LAN6="passwall2_lan6" IPSET_VPS6="passwall2_vps6" +IPSET_WAN6="passwall2_wan6" FWMARK="0x50535732" @@ -398,23 +400,27 @@ load_acl() { fi [ "$accept_icmp" = "1" ] && { + $ipt_n -I PSW2 $(comment "$remarks") -p icmp ${_ipt_source} $(dst $IPSET_PROXY_LAN) $(REDIRECT) $ipt_n -A PSW2 $(comment "$remarks") -p icmp ${_ipt_source} -d $FAKE_IP $(REDIRECT) add_shunt_t_rule "${shunt_list4}" "$ipt_n -A PSW2 $(comment "$remarks") -p icmp ${_ipt_source}" "$(REDIRECT)" $ipt_n -A PSW2 $(comment "$remarks") -p icmp ${_ipt_source} $(REDIRECT) } [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + $ip6t_n -I PSW2 $(comment "$remarks") -p ipv6-icmp ${_ipt_source} $(dst $IPSET_PROXY_LAN6) $(REDIRECT) 2>/dev/null $ip6t_n -A PSW2 $(comment "$remarks") -p ipv6-icmp ${_ipt_source} -d $FAKE_IP_6 $(REDIRECT) 2>/dev/null add_shunt_t_rule "${shunt_list6}" "$ip6t_n -A PSW2 $(comment "$remarks") -p ipv6-icmp ${_ipt_source}" "$(REDIRECT)" 2>/dev/null $ip6t_n -A PSW2 $(comment "$remarks") -p ipv6-icmp ${_ipt_source} $(REDIRECT) 2>/dev/null } + $ipt_tmp -I PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(dst $IPSET_PROXY_LAN) ${ipt_j} $ipt_tmp -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} -d $FAKE_IP ${ipt_j} add_shunt_t_rule "${shunt_list4}" "$ipt_tmp -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source}" "${ipt_j}" $tcp_redir_ports add_port_rules "$ipt_tmp -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source}" $tcp_redir_ports "${ipt_j}" [ -n "${is_tproxy}" ] && $ipt_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(REDIRECT $redir_port TPROXY) [ "$PROXY_IPV6" == "1" ] && [ "$_ipv4" != "1" ] && { + $ip6t_m -I PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE 2>/dev/null $ip6t_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} -d $FAKE_IP_6 -j PSW2_RULE 2>/dev/null add_shunt_t_rule "${shunt_list6}" "$ip6t_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source}" "${ipt_j}" $tcp_redir_ports 2>/dev/null add_port_rules "$ip6t_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source}" $tcp_redir_ports "-j PSW2_RULE" 2>/dev/null @@ -428,12 +434,14 @@ load_acl() { [ "$udp_proxy_mode" != "disable" ] && [ -n "$redir_port" ] && { msg2="${msg}$(i18n "Use the %s node [%s]" "UDP" "${node_remark}")(TPROXY:${redir_port})" + $ipt_m -I PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(dst $IPSET_PROXY_LAN) -j PSW2_RULE $ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} -d $FAKE_IP -j PSW2_RULE add_shunt_t_rule "${shunt_list4}" "$ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source}" "-j PSW2_RULE" $udp_redir_ports add_port_rules "$ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source}" $udp_redir_ports "-j PSW2_RULE" $ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(REDIRECT $redir_port TPROXY) [ "$PROXY_IPV6" == "1" ] && [ "$_ipv4" != "1" ] && { + $ip6t_m -I PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE 2>/dev/null $ip6t_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} -d $FAKE_IP_6 -j PSW2_RULE 2>/dev/null add_shunt_t_rule "${shunt_list6}" "$ip6t_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source}" "-j PSW2_RULE" $udp_redir_ports 2>/dev/null add_port_rules "$ip6t_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source}" $udp_redir_ports "-j PSW2_RULE" 2>/dev/null @@ -498,23 +506,27 @@ load_acl() { fi [ "$accept_icmp" = "1" ] && { + $ipt_n -I PSW2 $(comment "${comment_d}") -p icmp $(dst $IPSET_PROXY_LAN) $(REDIRECT) $ipt_n -A PSW2 $(comment "${comment_d}") -p icmp -d $FAKE_IP $(REDIRECT) add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_n -A PSW2 $(comment "${comment_d}") -p icmp" "$(REDIRECT)" $ipt_n -A PSW2 $(comment "${comment_d}") -p icmp $(REDIRECT) } [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + $ip6t_n -I PSW2 $(comment "${comment_d}") -p ipv6-icmp $(dst $IPSET_PROXY_LAN6) $(REDIRECT) $ip6t_n -A PSW2 $(comment "${comment_d}") -p ipv6-icmp -d $FAKE_IP_6 $(REDIRECT) add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_n -A PSW2 $(comment "${comment_d}") -p ipv6-icmp" "$(REDIRECT)" $ip6t_n -A PSW2 $(comment "${comment_d}") -p ipv6-icmp $(REDIRECT) } + $ipt_tmp -I PSW2 $(comment "${comment_d}") -p tcp $(dst $IPSET_PROXY_LAN) ${ipt_j} $ipt_tmp -A PSW2 $(comment "${comment_d}") -p tcp -d $FAKE_IP ${ipt_j} add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_tmp -A PSW2 $(comment "${comment_d}") -p tcp" "${ipt_j}" $TCP_REDIR_PORTS add_port_rules "$ipt_tmp -A PSW2 $(comment "${comment_d}") -p tcp" $TCP_REDIR_PORTS "${ipt_j}" [ -n "${is_tproxy}" ] && $ipt_m -A PSW2 $(comment "${comment_d}") -p tcp $(REDIRECT $REDIR_PORT TPROXY) [ "$PROXY_IPV6" == "1" ] && { + $ip6t_m -I PSW2 $(comment "${comment_d}") -p tcp $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE $ip6t_m -A PSW2 $(comment "${comment_d}") -p tcp -d $FAKE_IP_6 -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_m -A PSW2 $(comment "${comment_d}") -p tcp" "-j PSW2_RULE" $TCP_REDIR_PORTS add_port_rules "$ip6t_m -A PSW2 $(comment "${comment_d}") -p tcp" $TCP_REDIR_PORTS "-j PSW2_RULE" @@ -527,12 +539,14 @@ load_acl() { if [ "$UDP_PROXY_MODE" != "disable" ] && [ -n "$NODE" ]; then msg2="${msg}$(i18n "Use the %s node [%s]" "UDP" "$(config_n_get $NODE remarks)")(TPROXY:${REDIR_PORT})" + $ipt_m -I PSW2 $(comment "${comment_d}") -p udp $(dst $IPSET_PROXY_LAN) -j PSW2_RULE $ipt_m -A PSW2 $(comment "${comment_d}") -p udp -d $FAKE_IP -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_m -A PSW2 $(comment "${comment_d}") -p udp" "-j PSW2_RULE" $UDP_REDIR_PORTS add_port_rules "$ipt_m -A PSW2 $(comment "${comment_d}") -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE" $ipt_m -A PSW2 $(comment "${comment_d}") -p udp $(REDIRECT $REDIR_PORT TPROXY) [ "$PROXY_IPV6" == "1" ] && { + $ip6t_m -I PSW2 $(comment "${comment_d}") -p udp -d $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE $ip6t_m -A PSW2 $(comment "${comment_d}") -p udp -d $FAKE_IP_6 -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_m -A PSW2 $(comment "${comment_d}") -p udp" "-j PSW2_RULE" $UDP_REDIR_PORTS add_port_rules "$ip6t_m -A PSW2 $(comment "${comment_d}") -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE" @@ -607,14 +621,16 @@ add_firewall_rule() { log_i18n 0 "Starting to load %s firewall rules..." "iptables" ipset -! create $IPSET_LOCAL nethash maxelem 1048576 - ipset -! create $IPSET_WAN nethash maxelem 1048576 + ipset -! create $IPSET_PROXY_LAN nethash maxelem 1048576 ipset -! create $IPSET_LAN nethash maxelem 1048576 ipset -! create $IPSET_VPS nethash maxelem 1048576 - + ipset -! create $IPSET_WAN nethash maxelem 1048576 + ipset -! create $IPSET_LOCAL6 nethash family inet6 maxelem 1048576 - ipset -! create $IPSET_WAN6 nethash family inet6 maxelem 1048576 + ipset -! create $IPSET_PROXY_LAN6 nethash family inet6 maxelem 1048576 ipset -! create $IPSET_LAN6 nethash family inet6 maxelem 1048576 ipset -! create $IPSET_VPS6 nethash family inet6 maxelem 1048576 + ipset -! create $IPSET_WAN6 nethash family inet6 maxelem 1048576 ipset -! -R <<-EOF $(ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/^/add $IPSET_LOCAL /") @@ -663,6 +679,16 @@ add_firewall_rule() { done } + # Force proxy LAN IP CIDR + force_proxy_lan_ip=$(config_t_get global_forwarding force_proxy_lan_ip) + for ip in $force_proxy_lan_ip; do + if [[ "$ip" == *::* ]]; then + ipset -! add $IPSET_PROXY_LAN6 $ip + else + ipset -! add $IPSET_PROXY_LAN $ip + fi + done + # Shunt rules IP list (import when use shunt node) gen_shunt_list "${NODE}" SHUNT_LIST4 SHUNT_LIST6 @@ -679,6 +705,14 @@ add_firewall_rule() { is_tproxy="TPROXY" fi + if [ -z "${is_tproxy}" ] || [ "$accept_icmp" = "1" ]; then + IPT_N=1 + fi + + if [ -z "${is_tproxy}" ] || [ "$accept_icmpv6" = "1" ]; then + IP6T_N=1 + fi + $ipt_n -N PSW2 $ipt_n -A PSW2 $(dst $IPSET_LAN) -j RETURN $ipt_n -A PSW2 $(dst $IPSET_VPS) -j RETURN @@ -851,6 +885,7 @@ add_firewall_rule() { if [ -n "$NODE" ] && [ "$TCP_LOCALHOST_PROXY" = "1" ]; then [ "$accept_icmp" = "1" ] && { $ipt_n -A OUTPUT -p icmp -j PSW2_OUTPUT + $ipt_n -I PSW2_OUTPUT -p icmp $(dst $IPSET_PROXY_LAN) $(REDIRECT) $ipt_n -A PSW2_OUTPUT -p icmp -d $FAKE_IP $(REDIRECT) add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_n -A PSW2_OUTPUT -p icmp" "$(REDIRECT)" $ipt_n -A PSW2_OUTPUT -p icmp $(REDIRECT) @@ -858,6 +893,7 @@ add_firewall_rule() { [ "$accept_icmpv6" = "1" ] && { $ip6t_n -A OUTPUT -p ipv6-icmp -j PSW2_OUTPUT + $ip6t_n -I PSW2_OUTPUT -p ipv6-icmp $(dst $IPSET_PROXY_LAN6) $(REDIRECT) $ip6t_n -A PSW2_OUTPUT -p ipv6-icmp -d $FAKE_IP_6 $(REDIRECT) add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_n -A PSW2_OUTPUT -p ipv6-icmp" "$(REDIRECT)" $ip6t_n -A PSW2_OUTPUT -p ipv6-icmp $(REDIRECT) @@ -869,20 +905,24 @@ add_firewall_rule() { ipt_j="$(REDIRECT $REDIR_PORT)" fi + $ipt_tmp -I PSW2_OUTPUT -p tcp $(dst $IPSET_PROXY_LAN) ${ipt_j} $ipt_tmp -A PSW2_OUTPUT -p tcp -d $FAKE_IP ${ipt_j} add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_tmp -A PSW2_OUTPUT -p tcp" "${ipt_j}" $TCP_REDIR_PORTS add_port_rules "$ipt_tmp -A PSW2_OUTPUT -p tcp" $TCP_REDIR_PORTS "${ipt_j}" [ -z "${is_tproxy}" ] && $ipt_n -A OUTPUT -p tcp -j PSW2_OUTPUT [ -n "${is_tproxy}" ] && { + $ipt_m -I PSW2 $(comment "${comment_l}") -p tcp -i lo $(dst $IPSET_PROXY_LAN) $(REDIRECT $REDIR_PORT TPROXY) $ipt_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo $(REDIRECT $REDIR_PORT TPROXY) $ipt_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo -j RETURN insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p tcp -j PSW2_OUTPUT" } [ "$PROXY_IPV6" == "1" ] && { + $ip6t_m -I PSW2_OUTPUT -p tcp $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE $ip6t_m -A PSW2_OUTPUT -p tcp -d $FAKE_IP_6 -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_m -A PSW2_OUTPUT -p tcp" "-j PSW2_RULE" $TCP_REDIR_PORTS add_port_rules "$ip6t_m -A PSW2_OUTPUT -p tcp" $TCP_REDIR_PORTS "-j PSW2_RULE" + $ip6t_m -I PSW2 $(comment "${comment_l}") -p tcp -i lo $(dst $IPSET_PROXY_LAN6) $(REDIRECT $REDIR_PORT TPROXY) $ip6t_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo $(REDIRECT $REDIR_PORT TPROXY) $ip6t_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo -j RETURN insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p tcp -j PSW2_OUTPUT" @@ -898,17 +938,21 @@ add_firewall_rule() { # Loading local router proxy UDP if [ -n "$NODE" ] && [ "$UDP_LOCALHOST_PROXY" = "1" ]; then + $ipt_m -I PSW2_OUTPUT -p udp $(dst $IPSET_PROXY_LAN) -j PSW2_RULE $ipt_m -A PSW2_OUTPUT -p udp -d $FAKE_IP -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_m -A PSW2_OUTPUT -p udp" "-j PSW2_RULE" $UDP_REDIR_PORTS add_port_rules "$ipt_m -A PSW2_OUTPUT -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE" + $ipt_m -I PSW2 $(comment "${comment_l}") -p udp -i lo $(dst $IPSET_PROXY_LAN) $(REDIRECT $REDIR_PORT TPROXY) $ipt_m -A PSW2 $(comment "${comment_l}") -p udp -i lo $(REDIRECT $REDIR_PORT TPROXY) $ipt_m -A PSW2 $(comment "${comment_l}") -p udp -i lo -j RETURN insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p udp -j PSW2_OUTPUT" [ "$PROXY_IPV6" == "1" ] && { + $ip6t_m -I PSW2_OUTPUT -p udp $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE $ip6t_m -A PSW2_OUTPUT -p udp -d $FAKE_IP_6 -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_m -A PSW2_OUTPUT -p udp" "-j PSW2_RULE" $UDP_REDIR_PORTS add_port_rules "$ip6t_m -A PSW2_OUTPUT -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE" + $ip6t_m -I PSW2 $(comment "${comment_l}") -p udp -i lo $(dst $IPSET_PROXY_LAN6) $(REDIRECT $REDIR_PORT TPROXY) $ip6t_m -A PSW2 $(comment "${comment_l}") -p udp -i lo $(REDIRECT $REDIR_PORT TPROXY) $ip6t_m -A PSW2 $(comment "${comment_l}") -p udp -i lo -j RETURN insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p udp -j PSW2_OUTPUT" @@ -936,6 +980,19 @@ add_firewall_rule() { filter_direct_node_list > /dev/null 2>&1 & + [ -z "${IPT_N}" ] && { + for chain in "PSW2" "PSW2_OUTPUT"; do + $ipt_n -F $chain 2>/dev/null + $ipt_n -X $chain 2>/dev/null + done + } + [ -z "${IP6T_N}" ] && { + for chain in "PSW2" "PSW2_OUTPUT"; do + $ip6t_n -F $chain 2>/dev/null + $ip6t_n -X $chain 2>/dev/null + done + } + log_i18n 0 "%s firewall rules load complete!" "iptables" } @@ -1062,6 +1119,8 @@ start() { stop() { [ -z "$(command -v log_i18n)" ] && . /usr/share/passwall2/utils.sh del_firewall_rule + destroy_ipset $IPSET_PROXY_LAN + destroy_ipset $IPSET_PROXY_LAN6 [ $(config_t_get global flush_set "0") = "1" ] && { uci -q delete ${CONFIG}.@global[0].flush_set uci -q commit ${CONFIG} diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/nftables.sh b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/nftables.sh index 09a4a1e6b7..4f5f1b46e3 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/nftables.sh +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/nftables.sh @@ -5,14 +5,16 @@ MY_PATH=$DIR/nftables.sh UTILS_PATH=$DIR/utils.sh NFTABLE_NAME="inet passwall2" NFTSET_LOCAL="passwall2_local" -NFTSET_WAN="passwall2_wan" +NFTSET_PROXY_LAN="passwall2_proxy_lan" NFTSET_LAN="passwall2_lan" NFTSET_VPS="passwall2_vps" +NFTSET_WAN="passwall2_wan" NFTSET_LOCAL6="passwall2_local6" -NFTSET_WAN6="passwall2_wan6" +NFTSET_PROXY_LAN6="passwall2_proxy_lan6" NFTSET_LAN6="passwall2_lan6" NFTSET_VPS6="passwall2_vps6" +NFTSET_WAN6="passwall2_wan6" FWMARK="0x50535732" @@ -425,6 +427,7 @@ load_acl() { fi [ "$accept_icmp" = "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} ip daddr @$NFTSET_PROXY_LAN $(REDIRECT) comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} ip daddr $FAKE_IP $(REDIRECT) comment \"$remarks\"" add_shunt_t_rule "${shunt_list4}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} ip daddr" "$(REDIRECT)" "$remarks" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} $(REDIRECT) comment \"$remarks\"" @@ -432,18 +435,21 @@ load_acl() { } [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} ip6 daddr @$NFTSET_PROXY_LAN6 $(REDIRECT) comment \"$remarks\"" 2>/dev/null nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} ip6 daddr $FAKE_IP_6 $(REDIRECT) comment \"$remarks\"" 2>/dev/null add_shunt_t_rule "${shunt_list6}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} ip6 daddr" "$(REDIRECT)" "$remarks" 2>/dev/null nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} $(REDIRECT) comment \"$remarks\"" 2>/dev/null nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} return comment \"$remarks\"" 2>/dev/null } + nft "insert rule $NFTABLE_NAME $nft_chain ip protocol tcp ${_ipt_source} ip daddr @$NFTSET_PROXY_LAN ${nft_j} comment \"$remarks\"" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp ${_ipt_source} ip daddr $FAKE_IP ${nft_j} comment \"$remarks\"" add_shunt_t_rule "${shunt_list4}" "nft add rule $NFTABLE_NAME $nft_chain ip protocol tcp ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") ip daddr" "${nft_j}" "$remarks" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") ${nft_j} comment \"$remarks\"" [ -n "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp ${_ipt_source} $(REDIRECT $redir_port TPROXY4) comment \"$remarks\"" [ "$PROXY_IPV6" == "1" ] && [ "$_ipv4" != "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} ip6 daddr @$NFTSET_PROXY_LAN6 counter jump PSW2_RULE comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} ip6 daddr $FAKE_IP_6 counter jump PSW2_RULE comment \"$remarks\"" add_shunt_t_rule "${shunt_list6}" "nft add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") ip6 daddr" "counter jump PSW2_RULE" "$remarks" 2>/dev/null nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") counter jump PSW2_RULE comment \"$remarks\"" 2>/dev/null @@ -457,12 +463,14 @@ load_acl() { [ "$udp_proxy_mode" != "disable" ] && [ -n "$redir_port" ] && { msg2="${msg}$(i18n "Use the %s node [%s]" "UDP" "${node_remark}")(TPROXY:${redir_port})" + nft "insert rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} ip daddr @$NFTSET_PROXY_LAN counter jump PSW2_RULE comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} ip daddr $FAKE_IP counter jump PSW2_RULE comment \"$remarks\"" add_shunt_t_rule "${shunt_list4}" "nft add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} $(factor $udp_redir_ports "udp dport") ip daddr" "counter jump PSW2_RULE" "$remarks" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} $(factor $udp_redir_ports "udp dport") counter jump PSW2_RULE comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} $(REDIRECT $redir_port TPROXY4) comment \"$remarks\"" [ "$PROXY_IPV6" == "1" ] && [ "$_ipv4" != "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} ip6 daddr @$NFTSET_PROXY_LAN6 counter jump PSW2_RULE comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} ip6 daddr $FAKE_IP_6 counter jump PSW2_RULE comment \"$remarks\"" add_shunt_t_rule "${shunt_list6}" "nft add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} $(factor $udp_redir_ports "udp dport") ip6 daddr" "counter jump PSW2_RULE" "$remarks" 2>/dev/null nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} $(factor $udp_redir_ports "udp dport") counter jump PSW2_RULE comment \"$remarks\"" 2>/dev/null @@ -527,6 +535,7 @@ load_acl() { fi [ "$accept_icmp" = "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ip daddr @$NFTSET_PROXY_LAN $(REDIRECT) comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ip daddr $FAKE_IP $(REDIRECT) comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ip daddr" "$(REDIRECT)" "${comment}" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp $(REDIRECT) comment \"${comment}\"" @@ -534,18 +543,21 @@ load_acl() { } [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ip6 daddr @$NFTSET_PROXY_LAN6 $(REDIRECT) comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ip6 daddr $FAKE_IP_6 $(REDIRECT) comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ip6 daddr" "$(REDIRECT)" "${comment}" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 $(REDIRECT) comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 return comment \"${comment}\"" } + nft "insert rule $NFTABLE_NAME $nft_chain ip protocol tcp ip daddr @$NFTSET_PROXY_LAN ${nft_j} comment \"${comment}\"" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp ip daddr $FAKE_IP ${nft_j} comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME $nft_chain ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") ip daddr" "${nft_j}" "${comment}" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") ${nft_j} comment \"${comment}\"" [ -n "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment}\"" [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ip6 daddr @$NFTSET_PROXY_LAN6 jump PSW2_RULE comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ip6 daddr $FAKE_IP_6 jump PSW2_RULE comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp $(factor $TCP_REDIR_PORTS "tcp dport") ip6 daddr" "${nft_j}" "${comment}" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp $(factor $TCP_REDIR_PORTS "tcp dport") counter jump PSW2_RULE comment \"${comment}\"" @@ -558,12 +570,14 @@ load_acl() { if [ "$UDP_PROXY_MODE" != "disable" ] && [ -n "$NODE" ]; then msg2="${msg}$(i18n "Use the %s node [%s]" "UDP" "$(config_n_get $NODE remarks)")(TPROXY:${REDIR_PORT})" + nft "insert rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ip daddr @$NFTSET_PROXY_LAN counter jump PSW2_RULE comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ip daddr $FAKE_IP counter jump PSW2_RULE comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") ip daddr" "counter jump PSW2_RULE" "${comment}" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") counter jump PSW2_RULE comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment}\"" [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ip6 daddr @$NFTSET_PROXY_LAN6 jump PSW2_RULE comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ip6 daddr $FAKE_IP_6 jump PSW2_RULE comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp $(factor $UDP_REDIR_PORTS "udp dport") ip6 daddr" "counter jump PSW2_RULE" "${comment}" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp $(factor $UDP_REDIR_PORTS "udp dport") counter jump PSW2_RULE comment \"${comment}\"" @@ -671,15 +685,18 @@ add_firewall_rule() { gen_nft_tables add_script_mwan3 mwan3_start - gen_nftset $NFTSET_WAN ipv4_addr 0 "-1" + gen_nftset $NFTSET_LOCAL ipv4_addr 0 "-1" + gen_nftset $NFTSET_PROXY_LAN ipv4_addr 0 "-1" gen_nftset $NFTSET_LAN ipv4_addr 0 "-1" $(gen_lanlist) gen_nftset $NFTSET_VPS ipv4_addr 0 "-1" + gen_nftset $NFTSET_WAN ipv4_addr 0 "-1" - gen_nftset $NFTSET_WAN6 ipv6_addr 0 "-1" gen_nftset $NFTSET_LOCAL6 ipv6_addr 0 "-1" + gen_nftset $NFTSET_PROXY_LAN6 ipv6_addr 0 "-1" gen_nftset $NFTSET_LAN6 ipv6_addr 0 "-1" $(gen_lanlist_6) gen_nftset $NFTSET_VPS6 ipv6_addr 0 "-1" + gen_nftset $NFTSET_WAN6 ipv6_addr 0 "-1" ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | insert_nftset $NFTSET_LOCAL "-1" ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | insert_nftset $NFTSET_LOCAL6 "-1" @@ -711,6 +728,16 @@ add_firewall_rule() { done } + # Force proxy LAN IP CIDR + force_proxy_lan_ip=$(config_t_get global_forwarding force_proxy_lan_ip) + for ip in $force_proxy_lan_ip; do + if [[ "$ip" == *::* ]]; then + echo "$ip" | insert_nftset $NFTSET_PROXY_LAN6 0 + else + echo "$ip" | insert_nftset $NFTSET_PROXY_LAN 0 + fi + done + # Shunt rules IP list (import when use shunt node) gen_shunt_list "${NODE}" SHUNT_LIST4 SHUNT_LIST6 @@ -900,6 +927,7 @@ add_firewall_rule() { # Loading local router proxy TCP if [ -n "$NODE" ] && [ "$TCP_LOCALHOST_PROXY" = "1" ]; then [ "$accept_icmp" = "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo ip protocol icmp ip daddr @$NFTSET_PROXY_LAN counter redirect" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo ip protocol icmp ip daddr $FAKE_IP counter redirect" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo ip protocol icmp ip daddr" "counter redirect" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo ip protocol icmp counter redirect" @@ -907,6 +935,7 @@ add_firewall_rule() { } [ "$accept_icmpv6" = "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 ip6 daddr @$NFTSET_PROXY_LAN6 counter redirect" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 ip6 daddr $FAKE_IP_6 counter redirect" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 ip6 daddr" "counter redirect" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 counter redirect" @@ -921,21 +950,25 @@ add_firewall_rule() { nft_j="$(REDIRECT $REDIR_PORT)" fi + nft "insert rule $NFTABLE_NAME $nft_chain ip protocol tcp ip daddr @$NFTSET_PROXY_LAN ${nft_j}" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp ip daddr $FAKE_IP ${nft_j}" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME $nft_chain ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") ip daddr" "${nft_j}" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") ${nft_j}" [ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME nat_output ip protocol tcp counter jump PSW2_OUTPUT_NAT" [ -n "${is_tproxy}" ] && { + nft "insert rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp iif lo ip daddr @$NFTSET_PROXY_LAN $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp iif lo $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp iif lo counter return comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME mangle_output ip protocol tcp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\"" } [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp ip6 daddr @$NFTSET_PROXY_LAN6 jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp ip6 daddr $FAKE_IP_6 jump PSW2_RULE" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp $(factor $TCP_REDIR_PORTS "tcp dport") ip6 daddr" "counter jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp $(factor $TCP_REDIR_PORTS "tcp dport") counter jump PSW2_RULE" - nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp iif lo $(REDIRECT $REDIR_PORT TPROXY) comment \"${comment_l}\"" + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp iif lo ip6 daddr @$NFTSET_PROXY_LAN6 $(REDIRECT $REDIR_PORT TPROXY6) comment \"${comment_l}\"" + nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp iif lo $(REDIRECT $REDIR_PORT TPROXY6) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp iif lo counter return comment \"${comment_l}\"" } @@ -949,18 +982,22 @@ add_firewall_rule() { # Loading local router proxy UDP if [ -n "$NODE" ] && [ "$UDP_LOCALHOST_PROXY" = "1" ]; then + nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp ip daddr @$NFTSET_PROXY_LAN counter jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp ip daddr $FAKE_IP counter jump PSW2_RULE" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") ip daddr" "counter jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") counter jump PSW2_RULE" + nft "insert rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp iif lo ip daddr @$NFTSET_PROXY_LAN $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp iif lo $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp iif lo counter return comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME mangle_output ip protocol udp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\"" [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr @$NFTSET_PROXY_LAN6 jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr $FAKE_IP_6 jump PSW2_RULE" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp $(factor $UDP_REDIR_PORTS "udp dport") ip6 daddr" "counter jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp $(factor $UDP_REDIR_PORTS "udp dport") counter jump PSW2_RULE" - nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp iif lo $(REDIRECT $REDIR_PORT TPROXY) comment \"${comment_l}\"" + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp iif lo ip6 daddr @$NFTSET_PROXY_LAN6 $(REDIRECT $REDIR_PORT TPROXY6) comment \"${comment_l}\"" + nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp iif lo $(REDIRECT $REDIR_PORT TPROXY6) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp iif lo counter return comment \"${comment_l}\"" } @@ -1080,6 +1117,8 @@ start() { stop() { [ -z "$(command -v log_i18n)" ] && . /usr/share/passwall2/utils.sh del_firewall_rule + destroy_nftset $NFTSET_PROXY_LAN + destroy_nftset $NFTSET_PROXY_LAN6 [ $(config_t_get global flush_set "0") = "1" ] && { uci -q delete ${CONFIG}.@global[0].flush_set uci -q commit ${CONFIG} diff --git a/small/gn/Makefile b/small/gn/Makefile index e9c9d4a1e1..572bdfc045 100644 --- a/small/gn/Makefile +++ b/small/gn/Makefile @@ -9,9 +9,9 @@ PKG_RELEASE:=1 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://gn.googlesource.com/gn.git -PKG_SOURCE_DATE:=2026-03-24 -PKG_SOURCE_VERSION:=b2ac0e7a9089039e62b84d246eca83f84c540f76 -PKG_MIRROR_HASH:=75a552f5b910a44202f85d03dcf278d81fdeb7f1a0589f9577f83030f5febb1e +PKG_SOURCE_DATE:=2026-03-31 +PKG_SOURCE_VERSION:=6e8dcdebbadf4f8aa75e6a4b6e0bdf89dce1513a +PKG_MIRROR_HASH:=746e218a5674d4a4b61ebcf393ce5d4e7dc0068d02084fe5a809c870859fdedb PKG_LICENSE:=BSD 3-Clause PKG_LICENSE_FILES:=LICENSE diff --git a/small/gn/src/out/last_commit_position.h b/small/gn/src/out/last_commit_position.h index d2b3c00a65..92edaab20d 100644 --- a/small/gn/src/out/last_commit_position.h +++ b/small/gn/src/out/last_commit_position.h @@ -3,7 +3,7 @@ #ifndef OUT_LAST_COMMIT_POSITION_H_ #define OUT_LAST_COMMIT_POSITION_H_ -#define LAST_COMMIT_POSITION_NUM 2353 -#define LAST_COMMIT_POSITION "2353 (b2ac0e7a9089)" +#define LAST_COMMIT_POSITION_NUM 2355 +#define LAST_COMMIT_POSITION "2355 (6e8dcdebbadf)" #endif // OUT_LAST_COMMIT_POSITION_H_ diff --git a/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 5ee76b87db..4abd662730 100644 --- a/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -1955,7 +1955,7 @@ function gen_config(var) } }) for index, value in ipairs(config.outbounds) do - if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and value.server_port and not no_run then + if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and (value.server_port or value.server_ports) and not no_run then sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list")) end for k, v in pairs(config.outbounds[index]) do diff --git a/small/luci-app-passwall/luasrc/passwall/util_xray.lua b/small/luci-app-passwall/luasrc/passwall/util_xray.lua index 730ce8035f..a44259ed17 100644 --- a/small/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/small/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -1064,17 +1064,17 @@ function gen_config(var) local to_node = get_node_by_id(node.to_node) if to_node then -- Landing Node not support use special node. - if to_node.protocol:find("^_") then + if to_node.protocol and to_node.protocol:find("^_") then to_node = nil end end if to_node then local to_outbound if to_node.type ~= "Xray" then - local tag = to_node[".name"] + local in_tag = "inbound_" .. to_node[".name"] .. "_" .. tostring(outbound.tag) local new_port = api.get_new_port() table.insert(inbounds, { - tag = tag, + tag = in_tag, listen = "127.0.0.1", port = new_port, protocol = "dokodemo-door", @@ -1086,11 +1086,11 @@ function gen_config(var) to_node.address = "127.0.0.1" to_node.port = new_port table.insert(rules, 1, { - inboundTag = {tag}, + inboundTag = {in_tag}, outboundTag = outbound.tag }) - to_outbound = gen_outbound(node[".name"], to_node, tag, { - tag = tag, + to_outbound = gen_outbound(node[".name"], to_node, to_node[".name"], { + tag = to_node[".name"], run_socks_instance = not no_run }) else @@ -1734,14 +1734,10 @@ function gen_config(var) else table.insert(outbounds, blackhole_outbound) end - for index, value in ipairs(config.outbounds) do - local s = value.settings - if not value["_flag_proxy_tag"] and value["_id"] and s and not no_run and - ((s.vnext and s.vnext[1] and s.vnext[1].address and s.vnext[1].port) or - (s.servers and s.servers[1] and s.servers[1].address and s.servers[1].port) or - (s.peers and s.peers[1] and s.peers[1].endpoint) or - (s.address and s.port)) then + local pt = value.protocol + local exclude = { blackhole=1, dns=1, freedom=1, loopback=1 } + if not value["_flag_proxy_tag"] and value["_id"] and pt and not exclude[pt] and not no_run then sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list")) end for k, v in pairs(config.outbounds[index]) do diff --git a/small/luci-app-passwall/root/usr/share/passwall/app.sh b/small/luci-app-passwall/root/usr/share/passwall/app.sh index 4acc19cf57..fc427ad2bf 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/small/luci-app-passwall/root/usr/share/passwall/app.sh @@ -1101,6 +1101,9 @@ socks_node_switch() { LOG_FILE="/dev/null" run_socks flag=$flag node=$new_node bind=$bind socks_port=$port config_file=$config_file http_port=$http_port http_config_file=$http_config_file log_file=$log_file set_cache_var "socks_${flag}" "$new_node" + local ENABLED_DEFAULT_ACL=$(get_cache_var "ENABLED_DEFAULT_ACL") + local ENABLED_ACLS=$(get_cache_var "ENABLED_ACLS") + [ "$ENABLED_DEFAULT_ACL" != "1" -a "$ENABLED_ACLS" != "1" ] && return local USE_TABLES=$(get_cache_var "USE_TABLES") [ -n "$USE_TABLES" ] && source $APP_PATH/${USE_TABLES}.sh filter_direct_node_list } @@ -1979,6 +1982,8 @@ get_config() { [ "$ENABLED_ACLS" = 1 ] && { [ "$(uci show ${CONFIG} | grep "@acl_rule" | grep "enabled='1'" | wc -l)" == 0 ] && ENABLED_ACLS=0 } + set_cache_var ENABLED_DEFAULT_ACL $ENABLED_DEFAULT_ACL + set_cache_var ENABLED_ACLS $ENABLED_ACLS TCP_PROXY_WAY=$(config_t_get global_forwarding tcp_proxy_way redirect) PROXY_IPV6=$(config_t_get global_forwarding ipv6_tproxy 0) diff --git a/small/luci-app-passwall/root/usr/share/passwall/iptables.sh b/small/luci-app-passwall/root/usr/share/passwall/iptables.sh index b14ee20daf..f399a2e0cc 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/iptables.sh +++ b/small/luci-app-passwall/root/usr/share/passwall/iptables.sh @@ -742,43 +742,50 @@ filter_vpsip() { } filter_server_port() { - local address=${1} - local port=${2} - local stream=${3} - stream=$(echo ${3} | tr 'A-Z' 'a-z') - local _is_tproxy ipt_tmp - ipt_tmp=$ipt_n - _is_tproxy=${is_tproxy} - [ "$stream" == "udp" ] && _is_tproxy="TPROXY" - [ -n "${_is_tproxy}" ] && ipt_tmp=$ipt_m - - for _ipt in 4 6; do - [ "$_ipt" == "4" ] && _ipt=$ipt_tmp - [ "$_ipt" == "6" ] && _ipt=$ip6t_m - $_ipt -n -L PSW_OUTPUT | grep -q "${address}:${port}" - if [ $? -ne 0 ]; then - $_ipt -I PSW_OUTPUT $(comment "${address}:${port}") -p $stream -d $address --dport $port -j RETURN 2>/dev/null + local address="$1" + local port=$(echo "$2" | tr '-' ':' | tr -d ' ') + local stream=$(echo "$3" | tr 'A-Z' 'a-z') + local ipt_tmp="$ipt_n" _is_tproxy _ipt_cmd _ver multi_ports p ports + [ "$(config_t_get global_forwarding tcp_proxy_way redirect)" = "tproxy" ] && _is_tproxy="TPROXY" + [ "$stream" = "udp" ] && _is_tproxy="TPROXY" + [ -n "$_is_tproxy" ] && ipt_tmp="$ipt_m" + for _ver in 4 6; do + [ "$_ver" = "4" ] && _ipt_cmd="$ipt_tmp" + [ "$_ver" = "6" ] && _ipt_cmd="$ip6t_m" + multi_ports="" + for p in $(echo "$port" | tr ',' ' '); do + case "$p" in + *:* ) + $_ipt_cmd -n -L PSW_OUTPUT 2>/dev/null | grep -q "${address}:${p}:${stream}" || \ + $_ipt_cmd -I PSW_OUTPUT $(comment "${address}:${p}:${stream}") -p "$stream" -d "$address" --dport "$p" -j RETURN 2>/dev/null + ;; + * ) + multi_ports="${multi_ports},$p" + ;; + esac + done + if [ -n "$multi_ports" ]; then + ports=$(printf "%s\n" "${multi_ports#,}" | tr ',' '\n' | sort -n | tr '\n' ',' | sed 's/,$//') + $_ipt_cmd -n -L PSW_OUTPUT 2>/dev/null | grep -q "${address}:${ports}:${stream}" || \ + $_ipt_cmd -I PSW_OUTPUT $(comment "${address}:${ports}:${stream}") -p "$stream" -d "$address" -m multiport --dports "$ports" -j RETURN 2>/dev/null fi done } filter_node() { - local node=${1} - local stream=${2} - if [ -n "$node" ]; then - local address=$(config_n_get $node address) - local port=$(config_n_get $node port) - [ -z "$address" ] && [ -z "$port" ] && { - return 1 - } - filter_server_port $address $port $stream - filter_server_port $address $port $stream - fi + local node="$1" stream="$2" + [ -z "$node" ] && return 1 + local address=$(config_n_get "$node" address) + local port=$(config_n_get "$node" port) + local hop=$(config_n_get "$node" hysteria2_hop) + [ -n "$hop" ] && port="${port:+$port,}$hop" + [ -z "$address" -o -z "$port" ] && return 1 + filter_server_port "$address" "$port" "$stream" } filter_direct_node_list() { [ ! -s "$TMP_PATH/direct_node_list" ] && return - for _node_id in $(cat $TMP_PATH/direct_node_list | awk '!seen[$0]++'); do + awk '!seen[$0]++' "$TMP_PATH/direct_node_list" | while read -r _node_id; do filter_node "$_node_id" TCP filter_node "$_node_id" UDP unset _node_id diff --git a/small/luci-app-passwall/root/usr/share/passwall/nftables.sh b/small/luci-app-passwall/root/usr/share/passwall/nftables.sh index 12a64e4203..de6b3506b9 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/nftables.sh +++ b/small/luci-app-passwall/root/usr/share/passwall/nftables.sh @@ -793,41 +793,40 @@ filter_vpsip() { } filter_server_port() { - local address=${1} - local port=${2} - local stream=${3} - stream=$(echo ${3} | tr 'A-Z' 'a-z') - local _is_tproxy - _is_tproxy=${is_tproxy} - [ "$stream" == "udp" ] && _is_tproxy="TPROXY" - - for _ipt in 4 6; do - [ "$_ipt" == "4" ] && _ip_type=ip - [ "$_ipt" == "6" ] && _ip_type=ip6 - nft "list chain $NFTABLE_NAME $nft_output_chain" 2>/dev/null | grep -q "${address}:${port}" - if [ $? -ne 0 ]; then - nft "insert rule $NFTABLE_NAME $nft_output_chain meta l4proto $stream $_ip_type daddr $address $stream dport $port return comment \"${address}:${port}\"" 2>/dev/null - fi + local address="$1" + local port=$(echo "$2" | tr ':' '-' | tr -d ' ') + local stream=$(echo "$3" | tr 'A-Z' 'a-z') + local _ip_type _port_expr _ver _is_tproxy + local _nft_output_chain="PSW_OUTPUT_NAT" + [ "$(config_t_get global_forwarding tcp_proxy_way redirect)" = "tproxy" ] && _is_tproxy="TPROXY" + [ "$stream" = "udp" ] && _is_tproxy="TPROXY" + [ -n "$_is_tproxy" ] && _nft_output_chain="PSW_OUTPUT_MANGLE" + case "$port" in + *,*) _port_expr="{ $port }" ;; + *) _port_expr="$port" ;; + esac + for _ver in 4 6; do + [ "$_ver" = "4" ] && _ip_type="ip" + [ "$_ver" = "6" ] && _ip_type="ip6" && _nft_output_chain="PSW_OUTPUT_MANGLE_V6" + nft list chain "$NFTABLE_NAME" "$_nft_output_chain" 2>/dev/null | grep -q "comment \"${address}:${port}:${stream}\"" || \ + nft insert rule "$NFTABLE_NAME" "$_nft_output_chain" meta l4proto "$stream" $_ip_type daddr "$address" "$stream" dport $_port_expr return comment "\"${address}:${port}:${stream}\"" 2>/dev/null done } filter_node() { - local node=${1} - local stream=${2} - if [ -n "$node" ]; then - local address=$(config_n_get $node address) - local port=$(config_n_get $node port) - [ -z "$address" ] && [ -z "$port" ] && { - return 1 - } - filter_server_port $address $port $stream - filter_server_port $address $port $stream - fi + local node="$1" stream="$2" + [ -z "$node" ] && return 1 + local address=$(config_n_get "$node" address) + local port=$(config_n_get "$node" port) + local hop=$(config_n_get "$node" hysteria2_hop) + [ -n "$hop" ] && port="${port:+$port,}$hop" + [ -z "$address" -o -z "$port" ] && return 1 + filter_server_port "$address" "$port" "$stream" } filter_direct_node_list() { [ ! -s "$TMP_PATH/direct_node_list" ] && return - for _node_id in $(cat $TMP_PATH/direct_node_list | awk '!seen[$0]++'); do + awk '!seen[$0]++' "$TMP_PATH/direct_node_list" | while read -r _node_id; do filter_node "$_node_id" TCP filter_node "$_node_id" UDP unset _node_id diff --git a/small/luci-app-passwall2/Makefile b/small/luci-app-passwall2/Makefile index 0f78aadabd..dc04466d25 100644 --- a/small/luci-app-passwall2/Makefile +++ b/small/luci-app-passwall2/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 -PKG_VERSION:=26.4.2 +PKG_VERSION:=26.4.5 PKG_RELEASE:=1 PKG_PO_VERSION:=$(PKG_VERSION) diff --git a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua index 99adab0683..25913c94d8 100644 --- a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua +++ b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua @@ -150,6 +150,9 @@ o = s:option(Flag, "accept_icmpv6", translate("Hijacking ICMPv6 (IPv6 PING)")) o:depends("ipv6_tproxy", true) o.default = 0 +o = s:option(DynamicList, "force_proxy_lan_ip", translate("Force Proxy LAN IP"), translate("By default, commonly used internal network IP ranges will be connect directly (not entering the core). If you want a certain network range to go through a proxy, please add it here.")) +o.datatype = "or(ipmask4,ipmask6)" + if has_xray then s_xray = m:section(TypedSection, "global_xray", "Xray " .. translate("Settings")) s_xray.anonymous = true diff --git a/small/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/small/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index 9e9a67a7e1..fa25087004 100644 --- a/small/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/small/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -1119,9 +1119,22 @@ function gen_config(var) end if node then + if node.protocol ~= "_shunt" then + -- create shunt logic + local tmp_node = { + remarks = node.remarks, + type = "sing-box", + protocol = "_shunt", + default_node = node[".name"], + } + tmp_node.fakedns = remote_dns_fake + tmp_node.default_fakedns = remote_dns_fake + node = tmp_node + end + if server_host and server_port then - node.address = server_host - node.port = server_port + default_node_address = server_host + default_node_port = server_port end function gen_socks_config_node(node_id, socks_id, remarks) @@ -1369,6 +1382,12 @@ function gen_config(var) sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface)) end else + if tag == "default" then + if default_node_address and default_node_port then + node.address = default_node_address + node.port = default_node_port + end + end for _, _outbound in ipairs(outbounds) do -- Avoid generating duplicate nested processes if _outbound["_flag_proxy_tag"] and _outbound["_flag_proxy_tag"]:find("socks <- " .. node[".name"], 1, true) then @@ -1631,12 +1650,6 @@ function gen_config(var) table.insert(rules, rule) end end) - else - COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node, nil, { - fragment = singbox_settings.fragment == "1" or nil, - record_fragment = singbox_settings.record_fragment == "1" or nil, - run_socks_instance = not no_run - }) end for index, value in ipairs(rules) do @@ -1875,17 +1888,27 @@ function gen_config(var) end end end - - if remote_dns_fake and default_dns_flag == "remote" then - -- When default is not direct and enable fakedns, default DNS use FakeDNS. - local fakedns_dns_rule = { - query_type = { - "A", "AAAA" - }, - server = fakedns_tag, - disable_cache = true - } - table.insert(dns.rules, fakedns_dns_rule) + if default_dns_flag == "remote" then + if remote_dns_fake then + -- When default is not direct and enable fakedns, default DNS use FakeDNS. + local fakedns_dns_rule = { + query_type = { + "A", "AAAA" + }, + server = fakedns_tag, + disable_cache = true, + rewrite_ttl = 30, + strategy = remote_strategy, + } + table.insert(dns.rules, fakedns_dns_rule) + else + local remote_dns_rule = { + server = "remote", + disable_cache = true, + strategy = remote_strategy, + } + table.insert(dns.rules, remote_dns_rule) + end end local dns_in_inbound = { type = "direct", diff --git a/small/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/small/luci-app-passwall2/luasrc/passwall2/util_xray.lua index ce9d713afc..6093ad4fae 100644 --- a/small/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/small/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -1035,14 +1035,25 @@ function gen_config(var) else local preproxy_node = get_node_by_id(node.preproxy_node) if preproxy_node then - local preproxy_outbound = gen_outbound(node[".name"], preproxy_node) + local preproxy_outbound, exist + if preproxy_node.protocol == "_balancing" then + local balancer_tag, loopback_outbound = gen_balancer(preproxy_node) + if loopback_outbound then + preproxy_outbound = loopback_outbound + exist = true + end + else + preproxy_outbound = gen_outbound(node[".name"], preproxy_node) + end if preproxy_outbound then outbound.tag = preproxy_outbound.tag .. " -> " .. outbound.tag outbound.proxySettings = { tag = preproxy_outbound.tag, transportLayer = true } - last_insert_outbound = preproxy_outbound + if not exist then + last_insert_outbound = preproxy_outbound + end default_outTag = outbound.tag end end @@ -1118,6 +1129,12 @@ function gen_config(var) proxy_table.preproxy_node = nil proxy_table.to_node = nil end + if tag == "default" then + if default_node_address and default_node_port then + node.address = default_node_address + node.port = default_node_port + end + end local outbound, has_add_outbound for _, _outbound in ipairs(outbounds) do -- Avoid generating duplicate nested processes @@ -1170,10 +1187,24 @@ function gen_config(var) end if node then - if server_host and server_port then - node.address = server_host - node.port = server_port + if node.protocol ~= "_shunt" then + -- create shunt logic + local tmp_node = { + remarks = node.remarks, + type = "Xray", + protocol = "_shunt", + default_node = node[".name"], + } + tmp_node.fakedns = remote_dns_fake + tmp_node.default_fakedns = remote_dns_fake + node = tmp_node end + + if server_host and server_port then + default_node_address = server_host + default_node_port = server_port + end + if node.protocol == "_shunt" then inner_fakedns = node.fakedns or "0" @@ -1332,25 +1363,6 @@ function gen_config(var) balancers = #balancers > 0 and balancers or nil, rules = rules } - else - COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node, nil, { - fragment = xray_settings.fragment == "1" or nil, - noise = xray_settings.noise == "1" or nil, - run_socks_instance = not no_run - }) - if COMMON.default_outbound_tag then - routing = { - domainStrategy = "AsIs", - domainMatcher = "hybrid", - balancers = #balancers > 0 and balancers or nil, - rules = rules - } - table.insert(routing.rules, { - ruleTag = "default", - network = "tcp,udp", - outboundTag = COMMON.default_outbound_tag - }) - end end end diff --git a/small/luci-app-passwall2/po/fa/passwall2.po b/small/luci-app-passwall2/po/fa/passwall2.po index f5375db2c3..177d1409d9 100644 --- a/small/luci-app-passwall2/po/fa/passwall2.po +++ b/small/luci-app-passwall2/po/fa/passwall2.po @@ -747,6 +747,12 @@ msgstr "ربودن ICMP (PING)" msgid "Hijacking ICMPv6 (IPv6 PING)" msgstr "ربودن ICMPv6 (IPv6 PING)" +msgid "Force Proxy LAN IP" +msgstr "IP پروکسی LAN را مجبور کنید" + +msgid "By default, commonly used internal network IP ranges will be connect directly (not entering the core). If you want a certain network range to go through a proxy, please add it here." +msgstr "به طور پیش‌فرض، محدوده‌های IP شبکه داخلی که معمولاً استفاده می‌شوند، مستقیماً متصل می‌شوند (و وارد هسته نمی‌شوند). اگر می‌خواهید محدوده شبکه خاصی از طریق پروکسی عبور کند، لطفاً آن را اینجا اضافه کنید." + msgid "Sniffing" msgstr "شنود (Sniffing)" diff --git a/small/luci-app-passwall2/po/ru/passwall2.po b/small/luci-app-passwall2/po/ru/passwall2.po index 9f017e442c..c413809f7a 100644 --- a/small/luci-app-passwall2/po/ru/passwall2.po +++ b/small/luci-app-passwall2/po/ru/passwall2.po @@ -748,6 +748,12 @@ msgstr "Перехват ICMP (PING)" msgid "Hijacking ICMPv6 (IPv6 PING)" msgstr "Перехват ICMPv6 (IPv6 PING)" +msgid "Force Proxy LAN IP" +msgstr "Принудительное использование IP-адреса локальной сети (Local IP)" + +msgid "By default, commonly used internal network IP ranges will be connect directly (not entering the core). If you want a certain network range to go through a proxy, please add it here." +msgstr "По умолчанию, часто используемые диапазоны IP-адресов внутренней сети будут подключаться напрямую (без доступа к ядру сети). Если вы хотите, чтобы определенный диапазон IP-адресов сети проходил через прокси-сервер, укажите это здесь." + msgid "Sniffing" msgstr "Анализ трафика" diff --git a/small/luci-app-passwall2/po/zh-cn/passwall2.po b/small/luci-app-passwall2/po/zh-cn/passwall2.po index 64afcf38d9..7539794329 100644 --- a/small/luci-app-passwall2/po/zh-cn/passwall2.po +++ b/small/luci-app-passwall2/po/zh-cn/passwall2.po @@ -739,6 +739,12 @@ msgstr "劫持ICMP (PING)" msgid "Hijacking ICMPv6 (IPv6 PING)" msgstr "劫持ICMPv6 (IPv6 PING)" +msgid "Force Proxy LAN IP" +msgstr "强制代理内网IP段" + +msgid "By default, commonly used internal network IP ranges will be connect directly (not entering the core). If you want a certain network range to go through a proxy, please add it here." +msgstr "默认情况下,常用内网IP网段将直连(不进入内核),如果你希望某个网段走代理,请在此添加。" + msgid "Sniffing" msgstr "流量嗅探" diff --git a/small/luci-app-passwall2/po/zh-tw/passwall2.po b/small/luci-app-passwall2/po/zh-tw/passwall2.po index 4f3b2e284b..a5bbe35182 100644 --- a/small/luci-app-passwall2/po/zh-tw/passwall2.po +++ b/small/luci-app-passwall2/po/zh-tw/passwall2.po @@ -739,6 +739,12 @@ msgstr "劫持ICMP (PING)" msgid "Hijacking ICMPv6 (IPv6 PING)" msgstr "劫持ICMPv6 (IPv6 PING)" +msgid "Force Proxy LAN IP" +msgstr "強制代理內網IP段" + +msgid "By default, commonly used internal network IP ranges will be connect directly (not entering the core). If you want a certain network range to go through a proxy, please add it here." +msgstr "預設情況下,常用內網IP網段將直連(不進入核心),如果你希望某個網段走代理,請在此新增。" + msgid "Sniffing" msgstr "流量嗅探" diff --git a/small/luci-app-passwall2/root/usr/share/passwall2/iptables.sh b/small/luci-app-passwall2/root/usr/share/passwall2/iptables.sh index c46ef51493..35f5df9689 100755 --- a/small/luci-app-passwall2/root/usr/share/passwall2/iptables.sh +++ b/small/luci-app-passwall2/root/usr/share/passwall2/iptables.sh @@ -4,14 +4,16 @@ DIR="$(cd "$(dirname "$0")" && pwd)" MY_PATH=$DIR/iptables.sh UTILS_PATH=$DIR/utils.sh IPSET_LOCAL="passwall2_local" -IPSET_WAN="passwall2_wan" +IPSET_PROXY_LAN="passwall2_proxy_lan" IPSET_LAN="passwall2_lan" IPSET_VPS="passwall2_vps" +IPSET_WAN="passwall2_wan" IPSET_LOCAL6="passwall2_local6" -IPSET_WAN6="passwall2_wan6" +IPSET_PROXY_LAN6="passwall2_proxy_lan6" IPSET_LAN6="passwall2_lan6" IPSET_VPS6="passwall2_vps6" +IPSET_WAN6="passwall2_wan6" FWMARK="0x50535732" @@ -398,23 +400,27 @@ load_acl() { fi [ "$accept_icmp" = "1" ] && { + $ipt_n -I PSW2 $(comment "$remarks") -p icmp ${_ipt_source} $(dst $IPSET_PROXY_LAN) $(REDIRECT) $ipt_n -A PSW2 $(comment "$remarks") -p icmp ${_ipt_source} -d $FAKE_IP $(REDIRECT) add_shunt_t_rule "${shunt_list4}" "$ipt_n -A PSW2 $(comment "$remarks") -p icmp ${_ipt_source}" "$(REDIRECT)" $ipt_n -A PSW2 $(comment "$remarks") -p icmp ${_ipt_source} $(REDIRECT) } [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + $ip6t_n -I PSW2 $(comment "$remarks") -p ipv6-icmp ${_ipt_source} $(dst $IPSET_PROXY_LAN6) $(REDIRECT) 2>/dev/null $ip6t_n -A PSW2 $(comment "$remarks") -p ipv6-icmp ${_ipt_source} -d $FAKE_IP_6 $(REDIRECT) 2>/dev/null add_shunt_t_rule "${shunt_list6}" "$ip6t_n -A PSW2 $(comment "$remarks") -p ipv6-icmp ${_ipt_source}" "$(REDIRECT)" 2>/dev/null $ip6t_n -A PSW2 $(comment "$remarks") -p ipv6-icmp ${_ipt_source} $(REDIRECT) 2>/dev/null } + $ipt_tmp -I PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(dst $IPSET_PROXY_LAN) ${ipt_j} $ipt_tmp -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} -d $FAKE_IP ${ipt_j} add_shunt_t_rule "${shunt_list4}" "$ipt_tmp -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source}" "${ipt_j}" $tcp_redir_ports add_port_rules "$ipt_tmp -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source}" $tcp_redir_ports "${ipt_j}" [ -n "${is_tproxy}" ] && $ipt_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(REDIRECT $redir_port TPROXY) [ "$PROXY_IPV6" == "1" ] && [ "$_ipv4" != "1" ] && { + $ip6t_m -I PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE 2>/dev/null $ip6t_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} -d $FAKE_IP_6 -j PSW2_RULE 2>/dev/null add_shunt_t_rule "${shunt_list6}" "$ip6t_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source}" "${ipt_j}" $tcp_redir_ports 2>/dev/null add_port_rules "$ip6t_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source}" $tcp_redir_ports "-j PSW2_RULE" 2>/dev/null @@ -428,12 +434,14 @@ load_acl() { [ "$udp_proxy_mode" != "disable" ] && [ -n "$redir_port" ] && { msg2="${msg}$(i18n "Use the %s node [%s]" "UDP" "${node_remark}")(TPROXY:${redir_port})" + $ipt_m -I PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(dst $IPSET_PROXY_LAN) -j PSW2_RULE $ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} -d $FAKE_IP -j PSW2_RULE add_shunt_t_rule "${shunt_list4}" "$ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source}" "-j PSW2_RULE" $udp_redir_ports add_port_rules "$ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source}" $udp_redir_ports "-j PSW2_RULE" $ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(REDIRECT $redir_port TPROXY) [ "$PROXY_IPV6" == "1" ] && [ "$_ipv4" != "1" ] && { + $ip6t_m -I PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE 2>/dev/null $ip6t_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} -d $FAKE_IP_6 -j PSW2_RULE 2>/dev/null add_shunt_t_rule "${shunt_list6}" "$ip6t_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source}" "-j PSW2_RULE" $udp_redir_ports 2>/dev/null add_port_rules "$ip6t_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source}" $udp_redir_ports "-j PSW2_RULE" 2>/dev/null @@ -498,23 +506,27 @@ load_acl() { fi [ "$accept_icmp" = "1" ] && { + $ipt_n -I PSW2 $(comment "${comment_d}") -p icmp $(dst $IPSET_PROXY_LAN) $(REDIRECT) $ipt_n -A PSW2 $(comment "${comment_d}") -p icmp -d $FAKE_IP $(REDIRECT) add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_n -A PSW2 $(comment "${comment_d}") -p icmp" "$(REDIRECT)" $ipt_n -A PSW2 $(comment "${comment_d}") -p icmp $(REDIRECT) } [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + $ip6t_n -I PSW2 $(comment "${comment_d}") -p ipv6-icmp $(dst $IPSET_PROXY_LAN6) $(REDIRECT) $ip6t_n -A PSW2 $(comment "${comment_d}") -p ipv6-icmp -d $FAKE_IP_6 $(REDIRECT) add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_n -A PSW2 $(comment "${comment_d}") -p ipv6-icmp" "$(REDIRECT)" $ip6t_n -A PSW2 $(comment "${comment_d}") -p ipv6-icmp $(REDIRECT) } + $ipt_tmp -I PSW2 $(comment "${comment_d}") -p tcp $(dst $IPSET_PROXY_LAN) ${ipt_j} $ipt_tmp -A PSW2 $(comment "${comment_d}") -p tcp -d $FAKE_IP ${ipt_j} add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_tmp -A PSW2 $(comment "${comment_d}") -p tcp" "${ipt_j}" $TCP_REDIR_PORTS add_port_rules "$ipt_tmp -A PSW2 $(comment "${comment_d}") -p tcp" $TCP_REDIR_PORTS "${ipt_j}" [ -n "${is_tproxy}" ] && $ipt_m -A PSW2 $(comment "${comment_d}") -p tcp $(REDIRECT $REDIR_PORT TPROXY) [ "$PROXY_IPV6" == "1" ] && { + $ip6t_m -I PSW2 $(comment "${comment_d}") -p tcp $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE $ip6t_m -A PSW2 $(comment "${comment_d}") -p tcp -d $FAKE_IP_6 -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_m -A PSW2 $(comment "${comment_d}") -p tcp" "-j PSW2_RULE" $TCP_REDIR_PORTS add_port_rules "$ip6t_m -A PSW2 $(comment "${comment_d}") -p tcp" $TCP_REDIR_PORTS "-j PSW2_RULE" @@ -527,12 +539,14 @@ load_acl() { if [ "$UDP_PROXY_MODE" != "disable" ] && [ -n "$NODE" ]; then msg2="${msg}$(i18n "Use the %s node [%s]" "UDP" "$(config_n_get $NODE remarks)")(TPROXY:${REDIR_PORT})" + $ipt_m -I PSW2 $(comment "${comment_d}") -p udp $(dst $IPSET_PROXY_LAN) -j PSW2_RULE $ipt_m -A PSW2 $(comment "${comment_d}") -p udp -d $FAKE_IP -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_m -A PSW2 $(comment "${comment_d}") -p udp" "-j PSW2_RULE" $UDP_REDIR_PORTS add_port_rules "$ipt_m -A PSW2 $(comment "${comment_d}") -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE" $ipt_m -A PSW2 $(comment "${comment_d}") -p udp $(REDIRECT $REDIR_PORT TPROXY) [ "$PROXY_IPV6" == "1" ] && { + $ip6t_m -I PSW2 $(comment "${comment_d}") -p udp -d $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE $ip6t_m -A PSW2 $(comment "${comment_d}") -p udp -d $FAKE_IP_6 -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_m -A PSW2 $(comment "${comment_d}") -p udp" "-j PSW2_RULE" $UDP_REDIR_PORTS add_port_rules "$ip6t_m -A PSW2 $(comment "${comment_d}") -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE" @@ -607,14 +621,16 @@ add_firewall_rule() { log_i18n 0 "Starting to load %s firewall rules..." "iptables" ipset -! create $IPSET_LOCAL nethash maxelem 1048576 - ipset -! create $IPSET_WAN nethash maxelem 1048576 + ipset -! create $IPSET_PROXY_LAN nethash maxelem 1048576 ipset -! create $IPSET_LAN nethash maxelem 1048576 ipset -! create $IPSET_VPS nethash maxelem 1048576 - + ipset -! create $IPSET_WAN nethash maxelem 1048576 + ipset -! create $IPSET_LOCAL6 nethash family inet6 maxelem 1048576 - ipset -! create $IPSET_WAN6 nethash family inet6 maxelem 1048576 + ipset -! create $IPSET_PROXY_LAN6 nethash family inet6 maxelem 1048576 ipset -! create $IPSET_LAN6 nethash family inet6 maxelem 1048576 ipset -! create $IPSET_VPS6 nethash family inet6 maxelem 1048576 + ipset -! create $IPSET_WAN6 nethash family inet6 maxelem 1048576 ipset -! -R <<-EOF $(ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/^/add $IPSET_LOCAL /") @@ -663,6 +679,16 @@ add_firewall_rule() { done } + # Force proxy LAN IP CIDR + force_proxy_lan_ip=$(config_t_get global_forwarding force_proxy_lan_ip) + for ip in $force_proxy_lan_ip; do + if [[ "$ip" == *::* ]]; then + ipset -! add $IPSET_PROXY_LAN6 $ip + else + ipset -! add $IPSET_PROXY_LAN $ip + fi + done + # Shunt rules IP list (import when use shunt node) gen_shunt_list "${NODE}" SHUNT_LIST4 SHUNT_LIST6 @@ -679,6 +705,14 @@ add_firewall_rule() { is_tproxy="TPROXY" fi + if [ -z "${is_tproxy}" ] || [ "$accept_icmp" = "1" ]; then + IPT_N=1 + fi + + if [ -z "${is_tproxy}" ] || [ "$accept_icmpv6" = "1" ]; then + IP6T_N=1 + fi + $ipt_n -N PSW2 $ipt_n -A PSW2 $(dst $IPSET_LAN) -j RETURN $ipt_n -A PSW2 $(dst $IPSET_VPS) -j RETURN @@ -851,6 +885,7 @@ add_firewall_rule() { if [ -n "$NODE" ] && [ "$TCP_LOCALHOST_PROXY" = "1" ]; then [ "$accept_icmp" = "1" ] && { $ipt_n -A OUTPUT -p icmp -j PSW2_OUTPUT + $ipt_n -I PSW2_OUTPUT -p icmp $(dst $IPSET_PROXY_LAN) $(REDIRECT) $ipt_n -A PSW2_OUTPUT -p icmp -d $FAKE_IP $(REDIRECT) add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_n -A PSW2_OUTPUT -p icmp" "$(REDIRECT)" $ipt_n -A PSW2_OUTPUT -p icmp $(REDIRECT) @@ -858,6 +893,7 @@ add_firewall_rule() { [ "$accept_icmpv6" = "1" ] && { $ip6t_n -A OUTPUT -p ipv6-icmp -j PSW2_OUTPUT + $ip6t_n -I PSW2_OUTPUT -p ipv6-icmp $(dst $IPSET_PROXY_LAN6) $(REDIRECT) $ip6t_n -A PSW2_OUTPUT -p ipv6-icmp -d $FAKE_IP_6 $(REDIRECT) add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_n -A PSW2_OUTPUT -p ipv6-icmp" "$(REDIRECT)" $ip6t_n -A PSW2_OUTPUT -p ipv6-icmp $(REDIRECT) @@ -869,20 +905,24 @@ add_firewall_rule() { ipt_j="$(REDIRECT $REDIR_PORT)" fi + $ipt_tmp -I PSW2_OUTPUT -p tcp $(dst $IPSET_PROXY_LAN) ${ipt_j} $ipt_tmp -A PSW2_OUTPUT -p tcp -d $FAKE_IP ${ipt_j} add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_tmp -A PSW2_OUTPUT -p tcp" "${ipt_j}" $TCP_REDIR_PORTS add_port_rules "$ipt_tmp -A PSW2_OUTPUT -p tcp" $TCP_REDIR_PORTS "${ipt_j}" [ -z "${is_tproxy}" ] && $ipt_n -A OUTPUT -p tcp -j PSW2_OUTPUT [ -n "${is_tproxy}" ] && { + $ipt_m -I PSW2 $(comment "${comment_l}") -p tcp -i lo $(dst $IPSET_PROXY_LAN) $(REDIRECT $REDIR_PORT TPROXY) $ipt_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo $(REDIRECT $REDIR_PORT TPROXY) $ipt_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo -j RETURN insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p tcp -j PSW2_OUTPUT" } [ "$PROXY_IPV6" == "1" ] && { + $ip6t_m -I PSW2_OUTPUT -p tcp $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE $ip6t_m -A PSW2_OUTPUT -p tcp -d $FAKE_IP_6 -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_m -A PSW2_OUTPUT -p tcp" "-j PSW2_RULE" $TCP_REDIR_PORTS add_port_rules "$ip6t_m -A PSW2_OUTPUT -p tcp" $TCP_REDIR_PORTS "-j PSW2_RULE" + $ip6t_m -I PSW2 $(comment "${comment_l}") -p tcp -i lo $(dst $IPSET_PROXY_LAN6) $(REDIRECT $REDIR_PORT TPROXY) $ip6t_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo $(REDIRECT $REDIR_PORT TPROXY) $ip6t_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo -j RETURN insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p tcp -j PSW2_OUTPUT" @@ -898,17 +938,21 @@ add_firewall_rule() { # Loading local router proxy UDP if [ -n "$NODE" ] && [ "$UDP_LOCALHOST_PROXY" = "1" ]; then + $ipt_m -I PSW2_OUTPUT -p udp $(dst $IPSET_PROXY_LAN) -j PSW2_RULE $ipt_m -A PSW2_OUTPUT -p udp -d $FAKE_IP -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_m -A PSW2_OUTPUT -p udp" "-j PSW2_RULE" $UDP_REDIR_PORTS add_port_rules "$ipt_m -A PSW2_OUTPUT -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE" + $ipt_m -I PSW2 $(comment "${comment_l}") -p udp -i lo $(dst $IPSET_PROXY_LAN) $(REDIRECT $REDIR_PORT TPROXY) $ipt_m -A PSW2 $(comment "${comment_l}") -p udp -i lo $(REDIRECT $REDIR_PORT TPROXY) $ipt_m -A PSW2 $(comment "${comment_l}") -p udp -i lo -j RETURN insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p udp -j PSW2_OUTPUT" [ "$PROXY_IPV6" == "1" ] && { + $ip6t_m -I PSW2_OUTPUT -p udp $(dst $IPSET_PROXY_LAN6) -j PSW2_RULE $ip6t_m -A PSW2_OUTPUT -p udp -d $FAKE_IP_6 -j PSW2_RULE add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_m -A PSW2_OUTPUT -p udp" "-j PSW2_RULE" $UDP_REDIR_PORTS add_port_rules "$ip6t_m -A PSW2_OUTPUT -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE" + $ip6t_m -I PSW2 $(comment "${comment_l}") -p udp -i lo $(dst $IPSET_PROXY_LAN6) $(REDIRECT $REDIR_PORT TPROXY) $ip6t_m -A PSW2 $(comment "${comment_l}") -p udp -i lo $(REDIRECT $REDIR_PORT TPROXY) $ip6t_m -A PSW2 $(comment "${comment_l}") -p udp -i lo -j RETURN insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p udp -j PSW2_OUTPUT" @@ -936,6 +980,19 @@ add_firewall_rule() { filter_direct_node_list > /dev/null 2>&1 & + [ -z "${IPT_N}" ] && { + for chain in "PSW2" "PSW2_OUTPUT"; do + $ipt_n -F $chain 2>/dev/null + $ipt_n -X $chain 2>/dev/null + done + } + [ -z "${IP6T_N}" ] && { + for chain in "PSW2" "PSW2_OUTPUT"; do + $ip6t_n -F $chain 2>/dev/null + $ip6t_n -X $chain 2>/dev/null + done + } + log_i18n 0 "%s firewall rules load complete!" "iptables" } @@ -1062,6 +1119,8 @@ start() { stop() { [ -z "$(command -v log_i18n)" ] && . /usr/share/passwall2/utils.sh del_firewall_rule + destroy_ipset $IPSET_PROXY_LAN + destroy_ipset $IPSET_PROXY_LAN6 [ $(config_t_get global flush_set "0") = "1" ] && { uci -q delete ${CONFIG}.@global[0].flush_set uci -q commit ${CONFIG} diff --git a/small/luci-app-passwall2/root/usr/share/passwall2/nftables.sh b/small/luci-app-passwall2/root/usr/share/passwall2/nftables.sh index 09a4a1e6b7..4f5f1b46e3 100755 --- a/small/luci-app-passwall2/root/usr/share/passwall2/nftables.sh +++ b/small/luci-app-passwall2/root/usr/share/passwall2/nftables.sh @@ -5,14 +5,16 @@ MY_PATH=$DIR/nftables.sh UTILS_PATH=$DIR/utils.sh NFTABLE_NAME="inet passwall2" NFTSET_LOCAL="passwall2_local" -NFTSET_WAN="passwall2_wan" +NFTSET_PROXY_LAN="passwall2_proxy_lan" NFTSET_LAN="passwall2_lan" NFTSET_VPS="passwall2_vps" +NFTSET_WAN="passwall2_wan" NFTSET_LOCAL6="passwall2_local6" -NFTSET_WAN6="passwall2_wan6" +NFTSET_PROXY_LAN6="passwall2_proxy_lan6" NFTSET_LAN6="passwall2_lan6" NFTSET_VPS6="passwall2_vps6" +NFTSET_WAN6="passwall2_wan6" FWMARK="0x50535732" @@ -425,6 +427,7 @@ load_acl() { fi [ "$accept_icmp" = "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} ip daddr @$NFTSET_PROXY_LAN $(REDIRECT) comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} ip daddr $FAKE_IP $(REDIRECT) comment \"$remarks\"" add_shunt_t_rule "${shunt_list4}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} ip daddr" "$(REDIRECT)" "$remarks" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} $(REDIRECT) comment \"$remarks\"" @@ -432,18 +435,21 @@ load_acl() { } [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} ip6 daddr @$NFTSET_PROXY_LAN6 $(REDIRECT) comment \"$remarks\"" 2>/dev/null nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} ip6 daddr $FAKE_IP_6 $(REDIRECT) comment \"$remarks\"" 2>/dev/null add_shunt_t_rule "${shunt_list6}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} ip6 daddr" "$(REDIRECT)" "$remarks" 2>/dev/null nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} $(REDIRECT) comment \"$remarks\"" 2>/dev/null nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} return comment \"$remarks\"" 2>/dev/null } + nft "insert rule $NFTABLE_NAME $nft_chain ip protocol tcp ${_ipt_source} ip daddr @$NFTSET_PROXY_LAN ${nft_j} comment \"$remarks\"" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp ${_ipt_source} ip daddr $FAKE_IP ${nft_j} comment \"$remarks\"" add_shunt_t_rule "${shunt_list4}" "nft add rule $NFTABLE_NAME $nft_chain ip protocol tcp ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") ip daddr" "${nft_j}" "$remarks" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") ${nft_j} comment \"$remarks\"" [ -n "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp ${_ipt_source} $(REDIRECT $redir_port TPROXY4) comment \"$remarks\"" [ "$PROXY_IPV6" == "1" ] && [ "$_ipv4" != "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} ip6 daddr @$NFTSET_PROXY_LAN6 counter jump PSW2_RULE comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} ip6 daddr $FAKE_IP_6 counter jump PSW2_RULE comment \"$remarks\"" add_shunt_t_rule "${shunt_list6}" "nft add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") ip6 daddr" "counter jump PSW2_RULE" "$remarks" 2>/dev/null nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") counter jump PSW2_RULE comment \"$remarks\"" 2>/dev/null @@ -457,12 +463,14 @@ load_acl() { [ "$udp_proxy_mode" != "disable" ] && [ -n "$redir_port" ] && { msg2="${msg}$(i18n "Use the %s node [%s]" "UDP" "${node_remark}")(TPROXY:${redir_port})" + nft "insert rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} ip daddr @$NFTSET_PROXY_LAN counter jump PSW2_RULE comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} ip daddr $FAKE_IP counter jump PSW2_RULE comment \"$remarks\"" add_shunt_t_rule "${shunt_list4}" "nft add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} $(factor $udp_redir_ports "udp dport") ip daddr" "counter jump PSW2_RULE" "$remarks" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} $(factor $udp_redir_ports "udp dport") counter jump PSW2_RULE comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} $(REDIRECT $redir_port TPROXY4) comment \"$remarks\"" [ "$PROXY_IPV6" == "1" ] && [ "$_ipv4" != "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} ip6 daddr @$NFTSET_PROXY_LAN6 counter jump PSW2_RULE comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} ip6 daddr $FAKE_IP_6 counter jump PSW2_RULE comment \"$remarks\"" add_shunt_t_rule "${shunt_list6}" "nft add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} $(factor $udp_redir_ports "udp dport") ip6 daddr" "counter jump PSW2_RULE" "$remarks" 2>/dev/null nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} $(factor $udp_redir_ports "udp dport") counter jump PSW2_RULE comment \"$remarks\"" 2>/dev/null @@ -527,6 +535,7 @@ load_acl() { fi [ "$accept_icmp" = "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ip daddr @$NFTSET_PROXY_LAN $(REDIRECT) comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ip daddr $FAKE_IP $(REDIRECT) comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp ip daddr" "$(REDIRECT)" "${comment}" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip protocol icmp $(REDIRECT) comment \"${comment}\"" @@ -534,18 +543,21 @@ load_acl() { } [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ip6 daddr @$NFTSET_PROXY_LAN6 $(REDIRECT) comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ip6 daddr $FAKE_IP_6 $(REDIRECT) comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 ip6 daddr" "$(REDIRECT)" "${comment}" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 $(REDIRECT) comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT meta l4proto icmpv6 return comment \"${comment}\"" } + nft "insert rule $NFTABLE_NAME $nft_chain ip protocol tcp ip daddr @$NFTSET_PROXY_LAN ${nft_j} comment \"${comment}\"" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp ip daddr $FAKE_IP ${nft_j} comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME $nft_chain ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") ip daddr" "${nft_j}" "${comment}" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") ${nft_j} comment \"${comment}\"" [ -n "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment}\"" [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ip6 daddr @$NFTSET_PROXY_LAN6 jump PSW2_RULE comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ip6 daddr $FAKE_IP_6 jump PSW2_RULE comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp $(factor $TCP_REDIR_PORTS "tcp dport") ip6 daddr" "${nft_j}" "${comment}" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp $(factor $TCP_REDIR_PORTS "tcp dport") counter jump PSW2_RULE comment \"${comment}\"" @@ -558,12 +570,14 @@ load_acl() { if [ "$UDP_PROXY_MODE" != "disable" ] && [ -n "$NODE" ]; then msg2="${msg}$(i18n "Use the %s node [%s]" "UDP" "$(config_n_get $NODE remarks)")(TPROXY:${REDIR_PORT})" + nft "insert rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ip daddr @$NFTSET_PROXY_LAN counter jump PSW2_RULE comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ip daddr $FAKE_IP counter jump PSW2_RULE comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") ip daddr" "counter jump PSW2_RULE" "${comment}" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") counter jump PSW2_RULE comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment}\"" [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ip6 daddr @$NFTSET_PROXY_LAN6 jump PSW2_RULE comment \"${comment}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ip6 daddr $FAKE_IP_6 jump PSW2_RULE comment \"${comment}\"" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp $(factor $UDP_REDIR_PORTS "udp dport") ip6 daddr" "counter jump PSW2_RULE" "${comment}" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp $(factor $UDP_REDIR_PORTS "udp dport") counter jump PSW2_RULE comment \"${comment}\"" @@ -671,15 +685,18 @@ add_firewall_rule() { gen_nft_tables add_script_mwan3 mwan3_start - gen_nftset $NFTSET_WAN ipv4_addr 0 "-1" + gen_nftset $NFTSET_LOCAL ipv4_addr 0 "-1" + gen_nftset $NFTSET_PROXY_LAN ipv4_addr 0 "-1" gen_nftset $NFTSET_LAN ipv4_addr 0 "-1" $(gen_lanlist) gen_nftset $NFTSET_VPS ipv4_addr 0 "-1" + gen_nftset $NFTSET_WAN ipv4_addr 0 "-1" - gen_nftset $NFTSET_WAN6 ipv6_addr 0 "-1" gen_nftset $NFTSET_LOCAL6 ipv6_addr 0 "-1" + gen_nftset $NFTSET_PROXY_LAN6 ipv6_addr 0 "-1" gen_nftset $NFTSET_LAN6 ipv6_addr 0 "-1" $(gen_lanlist_6) gen_nftset $NFTSET_VPS6 ipv6_addr 0 "-1" + gen_nftset $NFTSET_WAN6 ipv6_addr 0 "-1" ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | insert_nftset $NFTSET_LOCAL "-1" ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | insert_nftset $NFTSET_LOCAL6 "-1" @@ -711,6 +728,16 @@ add_firewall_rule() { done } + # Force proxy LAN IP CIDR + force_proxy_lan_ip=$(config_t_get global_forwarding force_proxy_lan_ip) + for ip in $force_proxy_lan_ip; do + if [[ "$ip" == *::* ]]; then + echo "$ip" | insert_nftset $NFTSET_PROXY_LAN6 0 + else + echo "$ip" | insert_nftset $NFTSET_PROXY_LAN 0 + fi + done + # Shunt rules IP list (import when use shunt node) gen_shunt_list "${NODE}" SHUNT_LIST4 SHUNT_LIST6 @@ -900,6 +927,7 @@ add_firewall_rule() { # Loading local router proxy TCP if [ -n "$NODE" ] && [ "$TCP_LOCALHOST_PROXY" = "1" ]; then [ "$accept_icmp" = "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo ip protocol icmp ip daddr @$NFTSET_PROXY_LAN counter redirect" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo ip protocol icmp ip daddr $FAKE_IP counter redirect" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo ip protocol icmp ip daddr" "counter redirect" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo ip protocol icmp counter redirect" @@ -907,6 +935,7 @@ add_firewall_rule() { } [ "$accept_icmpv6" = "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 ip6 daddr @$NFTSET_PROXY_LAN6 counter redirect" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 ip6 daddr $FAKE_IP_6 counter redirect" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 ip6 daddr" "counter redirect" nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 counter redirect" @@ -921,21 +950,25 @@ add_firewall_rule() { nft_j="$(REDIRECT $REDIR_PORT)" fi + nft "insert rule $NFTABLE_NAME $nft_chain ip protocol tcp ip daddr @$NFTSET_PROXY_LAN ${nft_j}" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp ip daddr $FAKE_IP ${nft_j}" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME $nft_chain ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") ip daddr" "${nft_j}" nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") ${nft_j}" [ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME nat_output ip protocol tcp counter jump PSW2_OUTPUT_NAT" [ -n "${is_tproxy}" ] && { + nft "insert rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp iif lo ip daddr @$NFTSET_PROXY_LAN $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp iif lo $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp iif lo counter return comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME mangle_output ip protocol tcp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\"" } [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp ip6 daddr @$NFTSET_PROXY_LAN6 jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp ip6 daddr $FAKE_IP_6 jump PSW2_RULE" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp $(factor $TCP_REDIR_PORTS "tcp dport") ip6 daddr" "counter jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp $(factor $TCP_REDIR_PORTS "tcp dport") counter jump PSW2_RULE" - nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp iif lo $(REDIRECT $REDIR_PORT TPROXY) comment \"${comment_l}\"" + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp iif lo ip6 daddr @$NFTSET_PROXY_LAN6 $(REDIRECT $REDIR_PORT TPROXY6) comment \"${comment_l}\"" + nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp iif lo $(REDIRECT $REDIR_PORT TPROXY6) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp iif lo counter return comment \"${comment_l}\"" } @@ -949,18 +982,22 @@ add_firewall_rule() { # Loading local router proxy UDP if [ -n "$NODE" ] && [ "$UDP_LOCALHOST_PROXY" = "1" ]; then + nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp ip daddr @$NFTSET_PROXY_LAN counter jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp ip daddr $FAKE_IP counter jump PSW2_RULE" add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") ip daddr" "counter jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") counter jump PSW2_RULE" + nft "insert rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp iif lo ip daddr @$NFTSET_PROXY_LAN $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp iif lo $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp iif lo counter return comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME mangle_output ip protocol udp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\"" [ "$PROXY_IPV6" == "1" ] && { + nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr @$NFTSET_PROXY_LAN6 jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr $FAKE_IP_6 jump PSW2_RULE" add_shunt_t_rule "${SHUNT_LIST6}" "nft add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp $(factor $UDP_REDIR_PORTS "udp dport") ip6 daddr" "counter jump PSW2_RULE" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp $(factor $UDP_REDIR_PORTS "udp dport") counter jump PSW2_RULE" - nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp iif lo $(REDIRECT $REDIR_PORT TPROXY) comment \"${comment_l}\"" + nft "insert rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp iif lo ip6 daddr @$NFTSET_PROXY_LAN6 $(REDIRECT $REDIR_PORT TPROXY6) comment \"${comment_l}\"" + nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp iif lo $(REDIRECT $REDIR_PORT TPROXY6) comment \"${comment_l}\"" nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp iif lo counter return comment \"${comment_l}\"" } @@ -1080,6 +1117,8 @@ start() { stop() { [ -z "$(command -v log_i18n)" ] && . /usr/share/passwall2/utils.sh del_firewall_rule + destroy_nftset $NFTSET_PROXY_LAN + destroy_nftset $NFTSET_PROXY_LAN6 [ $(config_t_get global flush_set "0") = "1" ] && { uci -q delete ${CONFIG}.@global[0].flush_set uci -q commit ${CONFIG} diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile index 742226a73a..1e45579508 100644 --- a/small/v2ray-geodata/Makefile +++ b/small/v2ray-geodata/Makefile @@ -12,13 +12,13 @@ PKG_MAINTAINER:=Tianling Shen include $(INCLUDE_DIR)/package.mk -GEOIP_VER:=202603050223 +GEOIP_VER:=202604050243 GEOIP_FILE:=geoip.dat.$(GEOIP_VER) define Download/geoip URL:=https://github.com/v2fly/geoip/releases/download/$(GEOIP_VER)/ URL_FILE:=geoip.dat FILE:=$(GEOIP_FILE) - HASH:=c6c1d1be0d28defef55b153e87cb430f94fb480c8f523bf901c5e4ca18d58a00 + HASH:=16dbd19ff8dddb69960f313a3b0c0623cae82dc9725687110c28740226d3b285 endef GEOSITE_VER:=20260404050103 diff --git a/xray-core/go.mod b/xray-core/go.mod index 83041dcdb6..c9dcff8cfd 100644 --- a/xray-core/go.mod +++ b/xray-core/go.mod @@ -3,7 +3,7 @@ module github.com/xtls/xray-core go 1.26 require ( - github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 + github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 github.com/cloudflare/circl v1.6.3 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 diff --git a/xray-core/go.sum b/xray-core/go.sum index 81137e64f4..da6e58fe1c 100644 --- a/xray-core/go.sum +++ b/xray-core/go.sum @@ -1,7 +1,7 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU= -github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= +github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 h1:cbF95uMsQwCwAzH2i8+2lNO2TReoELLuqeeMfyBjFbY= +github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= diff --git a/xray-core/infra/conf/transport_internet.go b/xray-core/infra/conf/transport_internet.go index 849f0e38ee..70259507de 100644 --- a/xray-core/infra/conf/transport_internet.go +++ b/xray-core/infra/conf/transport_internet.go @@ -36,6 +36,7 @@ import ( "github.com/xtls/xray-core/transport/internet/finalmask/xicmp" "github.com/xtls/xray-core/transport/internet/httpupgrade" "github.com/xtls/xray-core/transport/internet/hysteria" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" "github.com/xtls/xray-core/transport/internet/kcp" "github.com/xtls/xray-core/transport/internet/reality" "github.com/xtls/xray-core/transport/internet/splithttp" @@ -630,6 +631,7 @@ func (c *TLSCertConfig) Build() (*tls.Certificate, error) { type QuicParamsConfig struct { Congestion string `json:"congestion"` Debug bool `json:"debug"` + BbrProfile string `json:"bbrProfile"` BrutalUp Bandwidth `json:"brutalUp"` BrutalDown Bandwidth `json:"brutalDown"` UdpHop UdpHop `json:"udpHop"` @@ -1894,6 +1896,16 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { config.Udpmasks = append(config.Udpmasks, serial.ToTypedMessage(u)) } if c.FinalMask.QuicParams != nil { + profile := strings.ToLower(c.FinalMask.QuicParams.BbrProfile) + switch profile { + case "", string(bbr.ProfileConservative), string(bbr.ProfileStandard), string(bbr.ProfileAggressive): + if profile == "" { + profile = string(bbr.ProfileStandard) + } + default: + return nil, errors.New("unknown bbr profile") + } + up, err := c.FinalMask.QuicParams.BrutalUp.Bps() if err != nil { return nil, err @@ -1965,6 +1977,7 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { config.QuicParams = &internet.QuicParams{ Congestion: c.FinalMask.QuicParams.Congestion, + BbrProfile: profile, BrutalUp: up, BrutalDown: down, UdpHop: &internet.UdpHop{ diff --git a/xray-core/proxy/tun/stack_gvisor.go b/xray-core/proxy/tun/stack_gvisor.go index ab767c6108..8bcc4ebef2 100644 --- a/xray-core/proxy/tun/stack_gvisor.go +++ b/xray-core/proxy/tun/stack_gvisor.go @@ -105,17 +105,23 @@ func (t *stackGVisor) Start() error { // Use custom UDP packet handler, instead of strict gVisor forwarder, for FullCone NAT support udpForwarder := newUdpConnectionHandler(t.handler.HandleConnection, t.writeRawUDPPacket) ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, func(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool { - data := pkt.Data().AsRange().ToSlice() - if len(data) == 0 { - return false - } + data := pkt.Clone().Data().AsRange().ToSlice() + // if len(data) == 0 { + // return false + // } // source/destination of the packet we process as incoming, on gVisor side are Remote/Local // in other terms, src is the side behind tun, dst is the side behind gVisor // this function handle packets passing from the tun to the gVisor, therefore the src/dst assignement - src := net.UDPDestination(net.IPAddress(id.RemoteAddress.AsSlice()), net.Port(id.RemotePort)) - dst := net.UDPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)) - - return udpForwarder.HandlePacket(src, dst, data) + srcIP := net.IPAddress(id.RemoteAddress.AsSlice()) + dstIP := net.IPAddress(id.LocalAddress.AsSlice()) + if srcIP == nil || dstIP == nil { + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " > invalid ip address ", id.RemoteAddress.AsSlice(), " ", id.LocalAddress.AsSlice()) + return true + } + src := net.UDPDestination(srcIP, net.Port(id.RemotePort)) + dst := net.UDPDestination(dstIP, net.Port(id.LocalPort)) + udpForwarder.HandlePacket(src, dst, data) + return true }) t.stack = ipStack diff --git a/xray-core/proxy/tun/udp_fullcone.go b/xray-core/proxy/tun/udp_fullcone.go index df58ce4e7b..44612100d7 100644 --- a/xray-core/proxy/tun/udp_fullcone.go +++ b/xray-core/proxy/tun/udp_fullcone.go @@ -1,16 +1,24 @@ package tun import ( + "context" "io" "sync" + "time" "github.com/xtls/xray-core/common/buf" + "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" ) +type packet struct { + data []byte + dest *net.Destination +} + // sub-handler specifically for udp connections under main handler type udpConnectionHandler struct { - sync.Mutex + sync.RWMutex udpConns map[net.Destination]*udpConn @@ -30,25 +38,44 @@ func newUdpConnectionHandler(handleConnection func(conn net.Conn, dest net.Desti // HandlePacket handles UDP packets coming from tun, to forward to the dispatcher // this custom handler support FullCone NAT of returning packets, binding connection only by the source addr:port -func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destination, data []byte) bool { - u.Lock() +func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destination, data []byte) { + u.RLock() conn, found := u.udpConns[src] + if found { + select { + case conn.egress <- &packet{ + data: data, + dest: &dst, + }: + default: + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full") + } + u.RUnlock() + return + } + u.RUnlock() + + u.Lock() + defer u.Unlock() + + conn, found = u.udpConns[src] if !found { - egress := make(chan []byte, 16) + egress := make(chan *packet, 1024) conn = &udpConn{handler: u, egress: egress, src: src, dst: dst} u.udpConns[src] = conn go u.handleConnection(conn, dst) } - u.Unlock() // send packet data to the egress channel, if it has buffer, or discard select { - case conn.egress <- data: + case conn.egress <- &packet{ + data: data, + dest: &dst, + }: default: + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", conn.dst.NetAddr(), " > queue full") } - - return true } func (u *udpConnectionHandler) connectionFinished(src net.Destination) { @@ -63,27 +90,64 @@ func (u *udpConnectionHandler) connectionFinished(src net.Destination) { // udp connection abstraction type udpConn struct { - net.Conn - buf.Writer - handler *udpConnectionHandler - egress chan []byte + egress chan *packet src net.Destination dst net.Destination } +func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) { + for { + e, ok := <-c.egress + if !ok { + return nil, io.EOF + } + + b := buf.New() + + _, err := b.Write(e.data) + if err != nil { + errors.LogDebugInner(context.Background(), err, "drop udp with size ", len(e.data), " to ", e.dest.NetAddr(), " original ", c.dst.NetAddr()) + b.Release() + continue + } + + b.UDP = e.dest + + return buf.MultiBuffer{b}, nil + } +} + // Read packets from the connection func (c *udpConn) Read(p []byte) (int, error) { - data, ok := <-c.egress + e, ok := <-c.egress if !ok { return 0, io.EOF } - - n := copy(p, data) + n := copy(p, e.data) + if n != len(e.data) { + return 0, io.ErrShortBuffer + } return n, nil } +func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error { + for i, b := range mb { + dst := c.dst + if b.UDP != nil { + dst = *b.UDP + } + err := c.handler.writePacket(b.Bytes(), dst, c.src) + if err != nil { + buf.ReleaseMulti(mb[i:]) + return err + } + b.Release() + } + return nil +} + // Write returning packets back func (c *udpConn) Write(p []byte) (int, error) { // sending packets back mean sending payload with source/destination reversed @@ -102,33 +166,21 @@ func (c *udpConn) Close() error { } func (c *udpConn) LocalAddr() net.Addr { - return &net.UDPAddr{IP: c.dst.Address.IP(), Port: int(c.dst.Port.Value())} + return c.dst.RawNetAddr() } func (c *udpConn) RemoteAddr() net.Addr { - return &net.UDPAddr{IP: c.src.Address.IP(), Port: int(c.src.Port.Value())} + return c.src.RawNetAddr() } -// Write returning packets back -func (c *udpConn) WriteMultiBuffer(mb buf.MultiBuffer) error { - for _, b := range mb { - dst := c.dst - if b.UDP != nil { - dst = *b.UDP - } - - // validate address family matches between buffer packet and the connection - if dst.Address.Family() != c.dst.Address.Family() { - continue - } - - // sending packets back mean sending payload with source/destination reversed - err := c.handler.writePacket(b.Bytes(), dst, c.src) - if err != nil { - // udp doesn't guarantee delivery, so in any failure we just continue to the next packet - continue - } - } - +func (c *udpConn) SetDeadline(t time.Time) error { + return nil +} + +func (c *udpConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *udpConn) SetWriteDeadline(t time.Time) error { return nil } diff --git a/xray-core/proxy/wireguard/client.go b/xray-core/proxy/wireguard/client.go index 3030ac78c3..3ee3a2c5bf 100644 --- a/xray-core/proxy/wireguard/client.go +++ b/xray-core/proxy/wireguard/client.go @@ -370,6 +370,18 @@ func (c *udpConnClient) ReadMultiBuffer() (buf.MultiBuffer, error) { return buf.MultiBuffer{b}, nil } -func (c *udpConnClient) Write(p []byte) (int, error) { - return c.Conn.(net.PacketConn).WriteTo(p, c.dest.RawNetAddr()) +func (c *udpConnClient) WriteMultiBuffer(mb buf.MultiBuffer) error { + for i, b := range mb { + dst := c.dest + if b.UDP != nil { + dst = *b.UDP + } + _, err := c.Conn.(net.PacketConn).WriteTo(b.Bytes(), dst.RawNetAddr()) + if err != nil { + buf.ReleaseMulti(mb[i:]) + return err + } + b.Release() + } + return nil } diff --git a/xray-core/proxy/wireguard/tun.go b/xray-core/proxy/wireguard/tun.go index deea7cd614..86ff9f458d 100644 --- a/xray-core/proxy/wireguard/tun.go +++ b/xray-core/proxy/wireguard/tun.go @@ -189,8 +189,14 @@ func createGVisorTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo // if len(data) == 0 { // return false // } - src := net.UDPDestination(net.IPAddress(id.RemoteAddress.AsSlice()), net.Port(id.RemotePort)) - dst := net.UDPDestination(net.IPAddress(id.LocalAddress.AsSlice()), net.Port(id.LocalPort)) + srcIP := net.IPAddress(id.RemoteAddress.AsSlice()) + dstIP := net.IPAddress(id.LocalAddress.AsSlice()) + if srcIP == nil || dstIP == nil { + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " > invalid ip address ", id.RemoteAddress.AsSlice(), " ", id.LocalAddress.AsSlice()) + return true + } + src := net.UDPDestination(srcIP, net.Port(id.RemotePort)) + dst := net.UDPDestination(dstIP, net.Port(id.LocalPort)) manager.feed(src, dst, data) return true }) @@ -212,8 +218,12 @@ func (m *udpManager) feed(src net.Destination, dst net.Destination, data []byte) uc, ok := m.m[src.NetAddr()] if ok { select { - case uc.ch <- data: + case uc.queue <- &packet{ + p: data, + dest: &dst, + }: default: + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", uc.dst.NetAddr(), " > queue full") } m.mutex.RUnlock() return @@ -226,9 +236,9 @@ func (m *udpManager) feed(src net.Destination, dst net.Destination, data []byte) uc, ok = m.m[src.NetAddr()] if !ok { uc = &udpConn{ - ch: make(chan []byte, 1024), - src: src, - dst: dst, + queue: make(chan *packet, 1024), + src: src, + dst: dst, } uc.writeFunc = m.writeRawUDPPacket uc.closeFunc = func() { @@ -241,15 +251,19 @@ func (m *udpManager) feed(src net.Destination, dst net.Destination, data []byte) } select { - case uc.ch <- data: + case uc.queue <- &packet{ + p: data, + dest: &dst, + }: default: + errors.LogDebug(context.Background(), "drop udp with size ", len(data), " to ", dst.NetAddr(), " original ", uc.dst.NetAddr(), " > queue full") } } func (m *udpManager) close(uc *udpConn) { if !uc.closed { uc.closed = true - close(uc.ch) + close(uc.queue) delete(m.m, uc.src.NetAddr()) } } @@ -317,8 +331,13 @@ func (m *udpManager) writeRawUDPPacket(payload []byte, src net.Destination, dst return nil } +type packet struct { + p []byte + dest *net.Destination +} + type udpConn struct { - ch chan []byte + queue chan *packet src net.Destination dst net.Destination writeFunc func(payload []byte, src net.Destination, dst net.Destination) error @@ -326,13 +345,35 @@ type udpConn struct { closed bool } +func (c *udpConn) ReadMultiBuffer() (buf.MultiBuffer, error) { + for { + q, ok := <-c.queue + if !ok { + return nil, io.EOF + } + + b := buf.New() + + _, err := b.Write(q.p) + if err != nil { + errors.LogDebugInner(context.Background(), err, "drop udp with size ", len(q.p), " to ", q.dest.NetAddr(), " original ", c.dst.NetAddr()) + b.Release() + continue + } + + b.UDP = q.dest + + return buf.MultiBuffer{b}, nil + } +} + func (c *udpConn) Read(p []byte) (int, error) { - b, ok := <-c.ch + q, ok := <-c.queue if !ok { return 0, io.EOF } - n := copy(p, b) - if n != len(b) { + n := copy(p, q.p) + if n != len(q.p) { return 0, io.ErrShortBuffer } return n, nil @@ -368,11 +409,11 @@ func (c *udpConn) Close() error { } func (c *udpConn) LocalAddr() net.Addr { - return c.src.RawNetAddr() // fake + return c.dst.RawNetAddr() } func (c *udpConn) RemoteAddr() net.Addr { - return c.src.RawNetAddr() // src + return c.src.RawNetAddr() } func (c *udpConn) SetDeadline(t time.Time) error { diff --git a/xray-core/proxy/wireguard/tun_linux.go b/xray-core/proxy/wireguard/tun_linux.go index 7a46138a0b..068e21eec9 100644 --- a/xray-core/proxy/wireguard/tun_linux.go +++ b/xray-core/proxy/wireguard/tun_linux.go @@ -10,18 +10,20 @@ import ( "net/netip" "os" "sync" + "syscall" "golang.org/x/sys/unix" - "github.com/sagernet/sing/common/control" "github.com/vishvananda/netlink" "github.com/xtls/xray-core/common/errors" - wgtun "golang.zx2c4.com/wireguard/tun" + "github.com/xtls/xray-core/transport/internet" + "golang.zx2c4.com/wireguard/tun" ) type deviceNet struct { tunnel - dialer net.Dialer + dialer *net.Dialer + lc *net.ListenConfig handle *netlink.Handle linkAddrs []netlink.Addr @@ -47,10 +49,23 @@ func allocateIPv6TableIndex() int { } func newDeviceNet(interfaceName string) *deviceNet { - var dialer net.Dialer - bindControl := control.BindToInterface(control.NewDefaultInterfaceFinder(), interfaceName, -1) - dialer.Control = control.Append(dialer.Control, bindControl) - return &deviceNet{dialer: dialer} + dialer := &net.Dialer{} + dialer.Control = func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + if err := syscall.BindToDevice(int(fd), interfaceName); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to bind to device") + } + }) + } + lc := &net.ListenConfig{} + lc.Control = func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + if err := syscall.BindToDevice(int(fd), interfaceName); err != nil { + errors.LogInfoInner(context.Background(), err, "failed to bind to device") + } + }) + } + return &deviceNet{dialer: dialer, lc: lc} } func (d *deviceNet) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) ( @@ -60,9 +75,23 @@ func (d *deviceNet) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrP } func (d *deviceNet) DialUDPAddrPort(laddr, raddr netip.AddrPort) (net.Conn, error) { - dialer := d.dialer - dialer.LocalAddr = &net.UDPAddr{IP: laddr.Addr().AsSlice(), Port: int(laddr.Port())} - return dialer.DialContext(context.Background(), "udp", raddr.String()) + var conn net.PacketConn + var err error + if raddr.Addr().Is4() { + conn, err = d.lc.ListenPacket(context.Background(), "udp4", ":0") + } else { + conn, err = d.lc.ListenPacket(context.Background(), "udp6", ":0") + } + if err != nil { + return nil, err + } + return &internet.PacketConnWrapper{ + PacketConn: conn, + Dest: &net.UDPAddr{ + IP: raddr.Addr().AsSlice(), + Port: int(raddr.Port()), + }, + }, nil } func (d *deviceNet) Close() (err error) { @@ -134,7 +163,7 @@ func createKernelTun(localAddresses []netip.Addr, mtu int, handler promiscuousMo } n := CalculateInterfaceName("wg") - wgt, err := wgtun.CreateTUN(n, mtu) + wgt, err := tun.CreateTUN(n, mtu) if err != nil { return nil, err } diff --git a/xray-core/transport/internet/config.pb.go b/xray-core/transport/internet/config.pb.go index 7e1da3f6f3..e2339fe8da 100644 --- a/xray-core/transport/internet/config.pb.go +++ b/xray-core/transport/internet/config.pb.go @@ -445,17 +445,18 @@ func (x *UdpHop) GetIntervalMax() int64 { type QuicParams struct { state protoimpl.MessageState `protogen:"open.v1"` Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"` - BrutalUp uint64 `protobuf:"varint,2,opt,name=brutal_up,json=brutalUp,proto3" json:"brutal_up,omitempty"` - BrutalDown uint64 `protobuf:"varint,3,opt,name=brutal_down,json=brutalDown,proto3" json:"brutal_down,omitempty"` - UdpHop *UdpHop `protobuf:"bytes,4,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"` - InitStreamReceiveWindow uint64 `protobuf:"varint,5,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` - MaxStreamReceiveWindow uint64 `protobuf:"varint,6,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` - InitConnReceiveWindow uint64 `protobuf:"varint,7,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` - MaxConnReceiveWindow uint64 `protobuf:"varint,8,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` - MaxIdleTimeout int64 `protobuf:"varint,9,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` - KeepAlivePeriod int64 `protobuf:"varint,10,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` - DisablePathMtuDiscovery bool `protobuf:"varint,11,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` - MaxIncomingStreams int64 `protobuf:"varint,12,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"` + BbrProfile string `protobuf:"bytes,2,opt,name=bbr_profile,json=bbrProfile,proto3" json:"bbr_profile,omitempty"` + BrutalUp uint64 `protobuf:"varint,3,opt,name=brutal_up,json=brutalUp,proto3" json:"brutal_up,omitempty"` + BrutalDown uint64 `protobuf:"varint,4,opt,name=brutal_down,json=brutalDown,proto3" json:"brutal_down,omitempty"` + UdpHop *UdpHop `protobuf:"bytes,5,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"` + InitStreamReceiveWindow uint64 `protobuf:"varint,6,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"` + MaxStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"` + InitConnReceiveWindow uint64 `protobuf:"varint,8,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"` + MaxConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"` + MaxIdleTimeout int64 `protobuf:"varint,10,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"` + KeepAlivePeriod int64 `protobuf:"varint,11,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"` + DisablePathMtuDiscovery bool `protobuf:"varint,12,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"` + MaxIncomingStreams int64 `protobuf:"varint,13,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -497,6 +498,13 @@ func (x *QuicParams) GetCongestion() string { return "" } +func (x *QuicParams) GetBbrProfile() string { + if x != nil { + return x.BbrProfile + } + return "" +} + func (x *QuicParams) GetBrutalUp() uint64 { if x != nil { return x.BrutalUp @@ -1028,25 +1036,27 @@ const file_transport_internet_config_proto_rawDesc = "" + "\x06UdpHop\x12\x14\n" + "\x05ports\x18\x01 \x03(\rR\x05ports\x12!\n" + "\finterval_min\x18\x02 \x01(\x03R\vintervalMin\x12!\n" + - "\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xd1\x04\n" + + "\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xf2\x04\n" + "\n" + "QuicParams\x12\x1e\n" + "\n" + "congestion\x18\x01 \x01(\tR\n" + - "congestion\x12\x1b\n" + - "\tbrutal_up\x18\x02 \x01(\x04R\bbrutalUp\x12\x1f\n" + - "\vbrutal_down\x18\x03 \x01(\x04R\n" + + "congestion\x12\x1f\n" + + "\vbbr_profile\x18\x02 \x01(\tR\n" + + "bbrProfile\x12\x1b\n" + + "\tbrutal_up\x18\x03 \x01(\x04R\bbrutalUp\x12\x1f\n" + + "\vbrutal_down\x18\x04 \x01(\x04R\n" + "brutalDown\x128\n" + - "\audp_hop\x18\x04 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" + - "\x1ainit_stream_receive_window\x18\x05 \x01(\x04R\x17initStreamReceiveWindow\x129\n" + - "\x19max_stream_receive_window\x18\x06 \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + - "\x18init_conn_receive_window\x18\a \x01(\x04R\x15initConnReceiveWindow\x125\n" + - "\x17max_conn_receive_window\x18\b \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + - "\x10max_idle_timeout\x18\t \x01(\x03R\x0emaxIdleTimeout\x12*\n" + - "\x11keep_alive_period\x18\n" + - " \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + - "\x1adisable_path_mtu_discovery\x18\v \x01(\bR\x17disablePathMtuDiscovery\x120\n" + - "\x14max_incoming_streams\x18\f \x01(\x03R\x12maxIncomingStreams\"Q\n" + + "\audp_hop\x18\x05 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" + + "\x1ainit_stream_receive_window\x18\x06 \x01(\x04R\x17initStreamReceiveWindow\x129\n" + + "\x19max_stream_receive_window\x18\a \x01(\x04R\x16maxStreamReceiveWindow\x127\n" + + "\x18init_conn_receive_window\x18\b \x01(\x04R\x15initConnReceiveWindow\x125\n" + + "\x17max_conn_receive_window\x18\t \x01(\x04R\x14maxConnReceiveWindow\x12(\n" + + "\x10max_idle_timeout\x18\n" + + " \x01(\x03R\x0emaxIdleTimeout\x12*\n" + + "\x11keep_alive_period\x18\v \x01(\x03R\x0fkeepAlivePeriod\x12;\n" + + "\x1adisable_path_mtu_discovery\x18\f \x01(\bR\x17disablePathMtuDiscovery\x120\n" + + "\x14max_incoming_streams\x18\r \x01(\x03R\x12maxIncomingStreams\"Q\n" + "\vProxyConfig\x12\x10\n" + "\x03tag\x18\x01 \x01(\tR\x03tag\x120\n" + "\x13transportLayerProxy\x18\x02 \x01(\bR\x13transportLayerProxy\"\x93\x01\n" + diff --git a/xray-core/transport/internet/config.proto b/xray-core/transport/internet/config.proto index 653cb9aa1f..ad23f047ea 100644 --- a/xray-core/transport/internet/config.proto +++ b/xray-core/transport/internet/config.proto @@ -72,17 +72,18 @@ message UdpHop { message QuicParams { string congestion = 1; - uint64 brutal_up = 2; - uint64 brutal_down = 3; - UdpHop udp_hop = 4; - uint64 init_stream_receive_window = 5; - uint64 max_stream_receive_window = 6; - uint64 init_conn_receive_window = 7; - uint64 max_conn_receive_window = 8; - int64 max_idle_timeout = 9; - int64 keep_alive_period = 10; - bool disable_path_mtu_discovery = 11; - int64 max_incoming_streams = 12; + string bbr_profile = 2; + uint64 brutal_up = 3; + uint64 brutal_down = 4; + UdpHop udp_hop = 5; + uint64 init_stream_receive_window = 6; + uint64 max_stream_receive_window = 7; + uint64 init_conn_receive_window = 8; + uint64 max_conn_receive_window = 9; + int64 max_idle_timeout = 10; + int64 keep_alive_period = 11; + bool disable_path_mtu_discovery = 12; + int64 max_incoming_streams = 13; } message ProxyConfig { diff --git a/xray-core/transport/internet/hysteria/congestion/bbr/bbr_sender.go b/xray-core/transport/internet/hysteria/congestion/bbr/bbr_sender.go index e8787f15a0..bcbf8133dc 100644 --- a/xray-core/transport/internet/hysteria/congestion/bbr/bbr_sender.go +++ b/xray-core/transport/internet/hysteria/congestion/bbr/bbr_sender.go @@ -6,6 +6,7 @@ import ( "net" "os" "strconv" + "strings" "time" "github.com/apernet/quic-go/congestion" @@ -28,16 +29,13 @@ const ( invalidPacketNumber = -1 initialCongestionWindowPackets = 32 + minCongestionWindowPackets = 4 // Constants based on TCP defaults. // The minimum CWND to ensure delayed acks don't reduce bandwidth measurements. // Does not inflate the pacing rate. - defaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSize) - // The gain used for the STARTUP, equal to 2/ln(2). defaultHighGain = 2.885 - // The newly derived gain for STARTUP, equal to 4 * ln(2) - derivedHighGain = 2.773 // The newly derived CWND gain for STARTUP, 2. derivedHighCWNDGain = 2.0 @@ -66,7 +64,6 @@ const ( // Flag. defaultStartupFullLossCount = 8 quicBbr2DefaultLossThreshold = 0.02 - maxBbrBurstPackets = 10 ) type bbrMode int @@ -97,6 +94,76 @@ const ( bbrRecoveryStateGrowth ) +type Profile string + +const ( + ProfileConservative Profile = "conservative" + ProfileStandard Profile = "standard" + ProfileAggressive Profile = "aggressive" +) + +type profileConfig struct { + highGain float64 + highCwndGain float64 + congestionWindowGainConstant float64 + numStartupRtts int64 + drainToTarget bool + detectOvershooting bool + bytesLostMultiplier uint8 + enableAckAggregationStartup bool + expireAckAggregationStartup bool + enableOverestimateAvoidance bool + reduceExtraAckedOnBandwidthIncrease bool +} + +func ParseProfile(profile string) (Profile, error) { + switch normalized := strings.ToLower(profile); normalized { + case "", string(ProfileStandard): + return ProfileStandard, nil + case string(ProfileConservative): + return ProfileConservative, nil + case string(ProfileAggressive): + return ProfileAggressive, nil + default: + return "", fmt.Errorf("unsupported BBR profile %q", profile) + } +} + +func configForProfile(profile Profile) profileConfig { + switch profile { + case ProfileConservative: + return profileConfig{ + highGain: 2.25, + highCwndGain: 1.75, + congestionWindowGainConstant: 1.75, + numStartupRtts: 2, + drainToTarget: true, + detectOvershooting: true, + bytesLostMultiplier: 1, + enableOverestimateAvoidance: true, + reduceExtraAckedOnBandwidthIncrease: true, + } + case ProfileAggressive: + return profileConfig{ + highGain: 3.0, + highCwndGain: 2.25, + congestionWindowGainConstant: 2.5, + numStartupRtts: 4, + bytesLostMultiplier: 2, + enableAckAggregationStartup: true, + expireAckAggregationStartup: true, + } + default: + return profileConfig{ + highGain: defaultHighGain, + highCwndGain: derivedHighCWNDGain, + congestionWindowGainConstant: 2.0, + numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, + bytesLostMultiplier: 2, + } + } +} + type bbrSender struct { rttStats congestion.RTTStatsProvider clock Clock @@ -145,6 +212,9 @@ type bbrSender struct { // The smallest value the |congestion_window_| can achieve. minCongestionWindow congestion.ByteCount + // The BBR profile used by the sender. + profile Profile + // The pacing gain applied during the STARTUP phase. highGain float64 @@ -251,12 +321,14 @@ var _ congestion.CongestionControl = &bbrSender{} func NewBbrSender( clock Clock, initialMaxDatagramSize congestion.ByteCount, + profile Profile, ) *bbrSender { return newBbrSender( clock, initialMaxDatagramSize, initialCongestionWindowPackets*initialMaxDatagramSize, congestion.MaxCongestionWindowPackets*initialMaxDatagramSize, + profile, ) } @@ -265,6 +337,7 @@ func newBbrSender( initialMaxDatagramSize, initialCongestionWindow, initialMaxCongestionWindow congestion.ByteCount, + profile Profile, ) *bbrSender { debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) b := &bbrSender{ @@ -277,9 +350,10 @@ func newBbrSender( congestionWindow: initialCongestionWindow, initialCongestionWindow: initialCongestionWindow, maxCongestionWindow: initialMaxCongestionWindow, - minCongestionWindow: defaultMinimumCongestionWindow, + minCongestionWindow: minCongestionWindowForMaxDatagramSize(initialMaxDatagramSize), + profile: ProfileStandard, highGain: defaultHighGain, - highCwndGain: defaultHighGain, + highCwndGain: derivedHighCWNDGain, drainGain: 1.0 / defaultHighGain, pacingGain: 1.0, congestionWindowGain: 1.0, @@ -295,20 +369,63 @@ func newBbrSender( debug: debug, } b.pacer = common.NewPacer(b.bandwidthForPacer) - - /* - if b.tracer != nil { - b.lastState = logging.CongestionStateStartup - b.tracer.UpdatedCongestionState(logging.CongestionStateStartup) - } - */ + b.applyProfile(profile) + if b.debug { + b.debugPrint("Profile: %s", b.profile) + } b.enterStartupMode(b.clock.Now()) - b.setHighCwndGain(derivedHighCWNDGain) return b } +func (b *bbrSender) applyProfile(profile Profile) { + if profile == "" { + profile = ProfileStandard + } + cfg := configForProfile(profile) + b.profile = profile + b.highGain = cfg.highGain + b.highCwndGain = cfg.highCwndGain + b.drainGain = 1.0 / cfg.highGain + b.congestionWindowGainConstant = cfg.congestionWindowGainConstant + b.numStartupRtts = cfg.numStartupRtts + b.drainToTarget = cfg.drainToTarget + b.detectOvershooting = cfg.detectOvershooting + b.bytesLostMultiplierWhileDetectingOvershooting = cfg.bytesLostMultiplier + b.enableAckAggregationDuringStartup = cfg.enableAckAggregationStartup + b.expireAckAggregationInStartup = cfg.expireAckAggregationStartup + if cfg.enableOverestimateAvoidance { + b.sampler.EnableOverestimateAvoidance() + } + b.sampler.SetReduceExtraAckedOnBandwidthIncrease(cfg.reduceExtraAckedOnBandwidthIncrease) +} + +func minCongestionWindowForMaxDatagramSize(maxDatagramSize congestion.ByteCount) congestion.ByteCount { + return minCongestionWindowPackets * maxDatagramSize +} + +func scaleByteWindowForDatagramSize(window, oldMaxDatagramSize, newMaxDatagramSize congestion.ByteCount) congestion.ByteCount { + if oldMaxDatagramSize == newMaxDatagramSize { + return window + } + return congestion.ByteCount(uint64(window) * uint64(newMaxDatagramSize) / uint64(oldMaxDatagramSize)) +} + +func (b *bbrSender) rescalePacketSizedWindows(maxDatagramSize congestion.ByteCount) { + oldMaxDatagramSize := b.maxDatagramSize + b.maxDatagramSize = maxDatagramSize + b.initialCongestionWindow = scaleByteWindowForDatagramSize(b.initialCongestionWindow, oldMaxDatagramSize, maxDatagramSize) + b.maxCongestionWindow = scaleByteWindowForDatagramSize(b.maxCongestionWindow, oldMaxDatagramSize, maxDatagramSize) + b.minCongestionWindow = minCongestionWindowForMaxDatagramSize(maxDatagramSize) + b.cwndToCalculateMinPacingRate = scaleByteWindowForDatagramSize(b.cwndToCalculateMinPacingRate, oldMaxDatagramSize, maxDatagramSize) + b.maxCongestionWindowWithNetworkParametersAdjusted = scaleByteWindowForDatagramSize( + b.maxCongestionWindowWithNetworkParametersAdjusted, + oldMaxDatagramSize, + maxDatagramSize, + ) +} + func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { b.rttStats = provider } @@ -370,14 +487,24 @@ func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) { // SetMaxDatagramSize implements the SendAlgorithm interface. func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { + if b.debug { + b.debugPrint("Max Datagram Size: %d", s) + } if s < b.maxDatagramSize { panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s)) } - cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow - b.maxDatagramSize = s - if cwndIsMinCwnd { + oldMinCongestionWindow := b.minCongestionWindow + oldInitialCongestionWindow := b.initialCongestionWindow + b.rescalePacketSizedWindows(s) + switch b.congestionWindow { + case oldMinCongestionWindow: b.congestionWindow = b.minCongestionWindow + case oldInitialCongestionWindow: + b.congestionWindow = b.initialCongestionWindow + default: + b.congestionWindow = min(b.maxCongestionWindow, max(b.congestionWindow, b.minCongestionWindow)) } + b.recoveryWindow = min(b.maxCongestionWindow, max(b.recoveryWindow, b.minCongestionWindow)) b.pacer.SetMaxDatagramSize(s) } @@ -519,22 +646,6 @@ func (b *bbrSender) PacingRate() Bandwidth { return b.pacingRate } -func (b *bbrSender) hasGoodBandwidthEstimateForResumption() bool { - return b.hasNonAppLimitedSample() -} - -func (b *bbrSender) hasNonAppLimitedSample() bool { - return b.hasNoAppLimitedSample -} - -// Sets the pacing gain used in STARTUP. Must be greater than 1. -func (b *bbrSender) setHighGain(highGain float64) { - b.highGain = highGain - if b.mode == bbrModeStartup { - b.pacingGain = highGain - } -} - // Sets the CWND gain used in STARTUP. Must be greater than 1. func (b *bbrSender) setHighCwndGain(highCwndGain float64) { b.highCwndGain = highCwndGain @@ -543,11 +654,6 @@ func (b *bbrSender) setHighCwndGain(highCwndGain float64) { } } -// Sets the gain used in DRAIN. Must be less than 1. -func (b *bbrSender) setDrainGain(drainGain float64) { - b.drainGain = drainGain -} - // Get the current bandwidth estimate. Note that Bandwidth is in bits per second. func (b *bbrSender) bandwidthEstimate() Bandwidth { return b.maxBandwidth.GetBest() diff --git a/xray-core/transport/internet/hysteria/congestion/bbr/bbr_sender_test.go b/xray-core/transport/internet/hysteria/congestion/bbr/bbr_sender_test.go new file mode 100644 index 0000000000..5aff552f64 --- /dev/null +++ b/xray-core/transport/internet/hysteria/congestion/bbr/bbr_sender_test.go @@ -0,0 +1,130 @@ +package bbr + +import ( + "testing" + + "github.com/apernet/quic-go/congestion" + "github.com/stretchr/testify/require" +) + +func TestSetMaxDatagramSizeRescalesPacketSizedWindows(t *testing.T) { + const oldMaxDatagramSize = congestion.ByteCount(1000) + const newMaxDatagramSize = congestion.ByteCount(1400) + const initialCongestionWindowPackets = congestion.ByteCount(20) + const maxCongestionWindowPackets = congestion.ByteCount(80) + + b := newBbrSender( + DefaultClock{}, + oldMaxDatagramSize, + initialCongestionWindowPackets*oldMaxDatagramSize, + maxCongestionWindowPackets*oldMaxDatagramSize, + ProfileStandard, + ) + b.congestionWindow = b.initialCongestionWindow + + b.SetMaxDatagramSize(newMaxDatagramSize) + + require.Equal(t, initialCongestionWindowPackets*newMaxDatagramSize, b.initialCongestionWindow) + require.Equal(t, maxCongestionWindowPackets*newMaxDatagramSize, b.maxCongestionWindow) + require.Equal(t, minCongestionWindowPackets*newMaxDatagramSize, b.minCongestionWindow) + require.Equal(t, initialCongestionWindowPackets*newMaxDatagramSize, b.congestionWindow) +} + +func TestSetMaxDatagramSizeClampsCongestionWindow(t *testing.T) { + const oldMaxDatagramSize = congestion.ByteCount(1000) + const newMaxDatagramSize = congestion.ByteCount(1400) + + b := NewBbrSender(DefaultClock{}, oldMaxDatagramSize, ProfileStandard) + b.congestionWindow = b.minCongestionWindow + oldMaxDatagramSize + b.recoveryWindow = b.minCongestionWindow + oldMaxDatagramSize + + b.SetMaxDatagramSize(newMaxDatagramSize) + + require.Equal(t, b.minCongestionWindow, b.congestionWindow) + require.Equal(t, b.minCongestionWindow, b.recoveryWindow) +} + +func TestNewBbrSenderAppliesProfiles(t *testing.T) { + testCases := []struct { + name string + profile Profile + highGain float64 + highCwndGain float64 + congestionWindowGainConstant float64 + numStartupRtts int64 + drainToTarget bool + detectOvershooting bool + bytesLostMultiplier uint8 + enableAckAggregationDuringStartup bool + expireAckAggregationInStartup bool + enableOverestimateAvoidance bool + reduceExtraAckedOnBandwidthIncrease bool + }{ + { + name: "standard", + profile: ProfileStandard, + highGain: defaultHighGain, + highCwndGain: derivedHighCWNDGain, + congestionWindowGainConstant: 2.0, + numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, + bytesLostMultiplier: 2, + }, + { + name: "conservative", + profile: ProfileConservative, + highGain: 2.25, + highCwndGain: 1.75, + congestionWindowGainConstant: 1.75, + numStartupRtts: 2, + drainToTarget: true, + detectOvershooting: true, + bytesLostMultiplier: 1, + enableOverestimateAvoidance: true, + reduceExtraAckedOnBandwidthIncrease: true, + }, + { + name: "aggressive", + profile: ProfileAggressive, + highGain: 3.0, + highCwndGain: 2.25, + congestionWindowGainConstant: 2.5, + numStartupRtts: 4, + bytesLostMultiplier: 2, + enableAckAggregationDuringStartup: true, + expireAckAggregationInStartup: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b := NewBbrSender(DefaultClock{}, congestion.InitialPacketSize, tc.profile) + require.Equal(t, tc.profile, b.profile) + require.Equal(t, tc.highGain, b.highGain) + require.Equal(t, tc.highCwndGain, b.highCwndGain) + require.Equal(t, tc.congestionWindowGainConstant, b.congestionWindowGainConstant) + require.Equal(t, tc.numStartupRtts, b.numStartupRtts) + require.Equal(t, tc.drainToTarget, b.drainToTarget) + require.Equal(t, tc.detectOvershooting, b.detectOvershooting) + require.Equal(t, tc.bytesLostMultiplier, b.bytesLostMultiplierWhileDetectingOvershooting) + require.Equal(t, tc.enableAckAggregationDuringStartup, b.enableAckAggregationDuringStartup) + require.Equal(t, tc.expireAckAggregationInStartup, b.expireAckAggregationInStartup) + require.Equal(t, tc.enableOverestimateAvoidance, b.sampler.IsOverestimateAvoidanceEnabled()) + require.Equal(t, tc.reduceExtraAckedOnBandwidthIncrease, b.sampler.maxAckHeightTracker.reduceExtraAckedOnBandwidthIncrease) + require.Equal(t, b.highGain, b.pacingGain) + require.Equal(t, b.highCwndGain, b.congestionWindowGain) + }) + } +} + +func TestParseProfile(t *testing.T) { + profile, err := ParseProfile("") + require.NoError(t, err) + require.Equal(t, ProfileStandard, profile) + + profile, err = ParseProfile("Aggressive") + require.NoError(t, err) + require.Equal(t, ProfileAggressive, profile) + + _, err = ParseProfile("turbo") + require.EqualError(t, err, `unsupported BBR profile "turbo"`) +} diff --git a/xray-core/transport/internet/hysteria/congestion/utils.go b/xray-core/transport/internet/hysteria/congestion/utils.go index 1036760eef..0f04318db5 100644 --- a/xray-core/transport/internet/hysteria/congestion/utils.go +++ b/xray-core/transport/internet/hysteria/congestion/utils.go @@ -1,18 +1,55 @@ package congestion import ( + "fmt" + "strings" + "github.com/apernet/quic-go" "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" "github.com/xtls/xray-core/transport/internet/hysteria/congestion/brutal" ) -func UseBBR(conn *quic.Conn) { +const ( + TypeBBR = "bbr" + TypeReno = "reno" +) + +func NormalizeType(congestionType string) (string, error) { + switch normalized := strings.ToLower(congestionType); normalized { + case "", TypeBBR: + return TypeBBR, nil + case TypeReno: + return TypeReno, nil + default: + return "", fmt.Errorf("unsupported congestion type %q", congestionType) + } +} + +func NormalizeBBRProfile(profile string) (string, error) { + normalized, err := bbr.ParseProfile(profile) + if err != nil { + return "", err + } + return string(normalized), nil +} + +func UseBBR(conn *quic.Conn, profile bbr.Profile) { conn.SetCongestionControl(bbr.NewBbrSender( bbr.DefaultClock{}, bbr.GetInitialPacketSize(conn.RemoteAddr()), + profile, )) } func UseBrutal(conn *quic.Conn, tx uint64) { conn.SetCongestionControl(brutal.NewBrutalSender(tx)) } + +func UseConfigured(conn *quic.Conn, congestionType, bbrProfile string) { + switch congestionType { + case TypeReno: + return + default: + UseBBR(conn, bbr.Profile(bbrProfile)) + } +} diff --git a/xray-core/transport/internet/hysteria/dialer.go b/xray-core/transport/internet/hysteria/dialer.go index d305b00df3..b4ce8e4b1e 100644 --- a/xray-core/transport/internet/hysteria/dialer.go +++ b/xray-core/transport/internet/hysteria/dialer.go @@ -23,6 +23,7 @@ import ( "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/finalmask" "github.com/xtls/xray-core/transport/internet/hysteria/congestion" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" "github.com/xtls/xray-core/transport/internet/hysteria/udphop" "github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/tls" @@ -157,10 +158,10 @@ func (c *client) dial() error { quicParams := c.quicParams if quicParams == nil { - quicParams = &internet.QuicParams{} - } - if quicParams.UdpHop == nil { - quicParams.UdpHop = &internet.UdpHop{} + quicParams = &internet.QuicParams{ + BbrProfile: string(bbr.ProfileStandard), + UdpHop: &internet.UdpHop{}, + } } var index int @@ -298,12 +299,12 @@ func (c *client) dial() error { case "reno": errors.LogDebug(c.ctx, "congestion reno") case "bbr": - errors.LogDebug(c.ctx, "congestion bbr") - congestion.UseBBR(quicConn) + errors.LogDebug(c.ctx, "congestion bbr ", quicParams.BbrProfile) + congestion.UseBBR(quicConn, bbr.Profile(quicParams.BbrProfile)) case "brutal", "": if serverAuto == "auto" || quicParams.BrutalUp == 0 || serverDown == 0 { - errors.LogDebug(c.ctx, "congestion bbr") - congestion.UseBBR(quicConn) + errors.LogDebug(c.ctx, "congestion bbr ", quicParams.BbrProfile) + congestion.UseBBR(quicConn, bbr.Profile(quicParams.BbrProfile)) } else { errors.LogDebug(c.ctx, "congestion brutal bytes per second ", min(quicParams.BrutalUp, serverDown)) congestion.UseBrutal(quicConn, min(quicParams.BrutalUp, serverDown)) diff --git a/xray-core/transport/internet/hysteria/hub.go b/xray-core/transport/internet/hysteria/hub.go index 992680d7ab..c7a685a155 100644 --- a/xray-core/transport/internet/hysteria/hub.go +++ b/xray-core/transport/internet/hysteria/hub.go @@ -23,6 +23,7 @@ import ( hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/hysteria/congestion" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" "github.com/xtls/xray-core/transport/internet/tls" ) @@ -188,12 +189,12 @@ func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { case "reno": errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion reno") case "bbr": - errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion bbr") - congestion.UseBBR(h.conn) + errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion bbr ", h.quicParams.BbrProfile) + congestion.UseBBR(h.conn, bbr.Profile(h.quicParams.BbrProfile)) case "brutal", "": if h.quicParams.BrutalUp == 0 || clientDown == 0 { - errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion bbr") - congestion.UseBBR(h.conn) + errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion bbr ", h.quicParams.BbrProfile) + congestion.UseBBR(h.conn, bbr.Profile(h.quicParams.BbrProfile)) } else { errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion brutal bytes per second ", min(h.quicParams.BrutalUp, clientDown)) congestion.UseBrutal(h.conn, min(h.quicParams.BrutalUp, clientDown)) @@ -389,7 +390,10 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti quicParams := streamSettings.QuicParams if quicParams == nil { - quicParams = &internet.QuicParams{} + quicParams = &internet.QuicParams{ + BbrProfile: string(bbr.ProfileStandard), + UdpHop: &internet.UdpHop{}, + } } quicConfig := &quic.Config{ diff --git a/xray-core/transport/internet/splithttp/dialer.go b/xray-core/transport/internet/splithttp/dialer.go index 0c351a5ae7..6f4ec1d83f 100644 --- a/xray-core/transport/internet/splithttp/dialer.go +++ b/xray-core/transport/internet/splithttp/dialer.go @@ -26,6 +26,7 @@ import ( "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/browser_dialer" "github.com/xtls/xray-core/transport/internet/hysteria/congestion" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" "github.com/xtls/xray-core/transport/internet/hysteria/udphop" "github.com/xtls/xray-core/transport/internet/reality" "github.com/xtls/xray-core/transport/internet/stat" @@ -158,10 +159,10 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea if httpVersion == "3" { quicParams := streamSettings.QuicParams if quicParams == nil { - quicParams = &internet.QuicParams{} - } - if quicParams.UdpHop == nil { - quicParams.UdpHop = &internet.UdpHop{} + quicParams = &internet.QuicParams{ + BbrProfile: string(bbr.ProfileStandard), + UdpHop: &internet.UdpHop{}, + } } quicConfig := &quic.Config{ @@ -292,8 +293,8 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea case "reno": errors.LogDebug(context.Background(), quicConn.RemoteAddr(), " ", "congestion reno") default: - errors.LogDebug(context.Background(), quicConn.RemoteAddr(), " ", "congestion bbr") - congestion.UseBBR(quicConn) + errors.LogDebug(context.Background(), quicConn.RemoteAddr(), " ", "congestion bbr ", quicParams.BbrProfile) + congestion.UseBBR(quicConn, bbr.Profile(quicParams.BbrProfile)) } return quicConn, nil diff --git a/xray-core/transport/internet/splithttp/hub.go b/xray-core/transport/internet/splithttp/hub.go index 1ffdf6f247..8b28145798 100644 --- a/xray-core/transport/internet/splithttp/hub.go +++ b/xray-core/transport/internet/splithttp/hub.go @@ -25,6 +25,7 @@ import ( "github.com/xtls/xray-core/common/signal/done" "github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet/hysteria/congestion" + "github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr" "github.com/xtls/xray-core/transport/internet/reality" "github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/tls" @@ -496,7 +497,10 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet quicParams := streamSettings.QuicParams if quicParams == nil { - quicParams = &internet.QuicParams{} + quicParams = &internet.QuicParams{ + BbrProfile: string(bbr.ProfileStandard), + UdpHop: &internet.UdpHop{}, + } } quicConfig := &quic.Config{ @@ -535,8 +539,8 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet case "reno": errors.LogDebug(context.Background(), conn.RemoteAddr(), " ", "congestion reno") default: - errors.LogDebug(context.Background(), conn.RemoteAddr(), " ", "congestion bbr") - congestion.UseBBR(conn) + errors.LogDebug(context.Background(), conn.RemoteAddr(), " ", "congestion bbr ", quicParams.BbrProfile) + congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile)) } go func() { diff --git a/xray-core/transport/internet/system_dialer.go b/xray-core/transport/internet/system_dialer.go index 16b3e9b0b4..2d604481e9 100644 --- a/xray-core/transport/internet/system_dialer.go +++ b/xray-core/transport/internet/system_dialer.go @@ -5,7 +5,6 @@ import ( "syscall" "time" - "github.com/sagernet/sing/common/control" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/features/dns" @@ -20,7 +19,7 @@ type SystemDialer interface { } type DefaultSystemDialer struct { - controllers []control.Func + controllers []func(network, address string, c syscall.RawConn) error dns dns.Client obm outbound.Manager } @@ -204,7 +203,7 @@ func UseAlternativeSystemDialer(dialer SystemDialer) { // It only works when effective dialer is the default dialer. // // xray:api:beta -func RegisterDialerController(ctl control.Func) error { +func RegisterDialerController(ctl func(network, address string, c syscall.RawConn) error) error { if ctl == nil { return errors.New("nil listener controller") } diff --git a/xray-core/transport/internet/system_listener.go b/xray-core/transport/internet/system_listener.go index 2ac28eda9c..1999953ebc 100644 --- a/xray-core/transport/internet/system_listener.go +++ b/xray-core/transport/internet/system_listener.go @@ -10,7 +10,6 @@ import ( "time" "github.com/pires/go-proxyproto" - "github.com/sagernet/sing/common/control" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" ) @@ -18,10 +17,10 @@ import ( var effectiveListener = DefaultListener{} type DefaultListener struct { - controllers []control.Func + controllers []func(network, address string, c syscall.RawConn) error } -func getControlFunc(ctx context.Context, sockopt *SocketConfig, controllers []control.Func) func(network, address string, c syscall.RawConn) error { +func getControlFunc(ctx context.Context, sockopt *SocketConfig, controllers []func(network, address string, c syscall.RawConn) error) func(network, address string, c syscall.RawConn) error { return func(network, address string, c syscall.RawConn) error { return c.Control(func(fd uintptr) { for _, controller := range controllers { @@ -186,7 +185,7 @@ func (dl *DefaultListener) ListenPacket(ctx context.Context, addr net.Addr, sock // The controller can be used to operate on file descriptors before they are put into use. // // xray:api:beta -func RegisterListenerController(controller control.Func) error { +func RegisterListenerController(controller func(network, address string, c syscall.RawConn) error) error { if controller == nil { return errors.New("nil listener controller") } diff --git a/xray-core/transport/internet/system_listener_test.go b/xray-core/transport/internet/system_listener_test.go index 390888e7a7..b80fdfa8b3 100644 --- a/xray-core/transport/internet/system_listener_test.go +++ b/xray-core/transport/internet/system_listener_test.go @@ -6,7 +6,6 @@ import ( "syscall" "testing" - "github.com/sagernet/sing/common/control" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/transport/internet" ) @@ -14,10 +13,9 @@ import ( func TestRegisterListenerController(t *testing.T) { var gotFd uintptr - common.Must(internet.RegisterListenerController(func(network, address string, conn syscall.RawConn) error { - return control.Raw(conn, func(fd uintptr) error { + common.Must(internet.RegisterListenerController(func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { gotFd = fd - return nil }) })) diff --git a/yt-dlp/pyproject.toml b/yt-dlp/pyproject.toml index c767f57282..1ec6a36dc1 100644 --- a/yt-dlp/pyproject.toml +++ b/yt-dlp/pyproject.toml @@ -175,6 +175,10 @@ lint-check = "ruff check {args:.}" lint-fix = "ruff check --fix {args:.}" [tool.hatch.envs.hatch-test] +features = [ + "curl-cffi", + "default", +] dependency-groups = [ "test", ] diff --git a/yt-dlp/yt_dlp/extractor/youtube/jsc/_builtin/quickjs.py b/yt-dlp/yt_dlp/extractor/youtube/jsc/_builtin/quickjs.py index f87725baed..73786cd0cc 100644 --- a/yt-dlp/yt_dlp/extractor/youtube/jsc/_builtin/quickjs.py +++ b/yt-dlp/yt_dlp/extractor/youtube/jsc/_builtin/quickjs.py @@ -21,12 +21,20 @@ from yt_dlp.utils import Popen class QuickJSJCP(EJSBaseJCP, BuiltinIEContentProvider): PROVIDER_NAME = 'quickjs' JS_RUNTIME_NAME = 'quickjs' + _QJS_MIN_RECOMMENDED = { + 'quickjs': (2025, 4, 26), + 'quickjs-ng': (0, 12, 0), + } + _QJS_WARNING_TMPL = ( + '{name} versions older than {version} are missing important optimizations ' + 'and will solve the JS challenges very slowly. Consider upgrading.') def _run_js_runtime(self, stdin: str, /) -> str: - if self.runtime_info.name == 'quickjs-ng': - self.logger.warning('QuickJS-NG is missing some optimizations making this very slow. Consider using upstream QuickJS instead.') - elif self.runtime_info.version_tuple < (2025, 4, 26): - self.logger.warning('Older QuickJS versions are missing optimizations making this very slow. Consider upgrading.') + min_recommended_version = self._QJS_MIN_RECOMMENDED[self.runtime_info.name] + if self.runtime_info.version_tuple < min_recommended_version: + self.logger.warning(self._QJS_WARNING_TMPL.format( + name=self.runtime_info.name, + version='.'.join(map(str, min_recommended_version)))) # QuickJS does not support reading from stdin, so we have to use a temp file temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False, encoding='utf-8') diff --git a/yt-dlp/yt_dlp/update.py b/yt-dlp/yt_dlp/update.py index c957e0d07d..c22843d891 100644 --- a/yt-dlp/yt_dlp/update.py +++ b/yt-dlp/yt_dlp/update.py @@ -316,7 +316,7 @@ class Updater: return json.loads(self.ydl.urlopen(Request(url, headers={ 'Accept': 'application/vnd.github+json', 'User-Agent': 'yt-dlp', - 'X-GitHub-Api-Version': '2022-11-28', + 'X-GitHub-Api-Version': '2026-03-10', })).read().decode()) def _get_version_info(self, tag: str) -> tuple[str | None, str | None]: