Update On Wed Jan 14 19:44:20 CET 2026

This commit is contained in:
github-action[bot]
2026-01-14 19:44:20 +01:00
parent df786b8522
commit cbfa8dd42c
146 changed files with 3230 additions and 1623 deletions
+1
View File
@@ -1242,3 +1242,4 @@ Update On Sat Jan 10 19:39:24 CET 2026
Update On Sun Jan 11 19:40:53 CET 2026
Update On Mon Jan 12 19:44:25 CET 2026
Update On Tue Jan 13 19:42:52 CET 2026
Update On Wed Jan 14 19:44:12 CET 2026
+25 -11
View File
@@ -43,6 +43,7 @@ type SudokuOption struct {
HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
HTTPMaskTLS bool `proxy:"http-mask-tls,omitempty"` // only for http-mask-mode stream/poll/auto
HTTPMaskHost string `proxy:"http-mask-host,omitempty"` // optional Host/SNI override (domain or domain:port)
PathRoot string `proxy:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
HTTPMaskMultiplex string `proxy:"http-mask-multiplex,omitempty"` // "off" (default), "auto" (reuse h1/h2), "on" (single tunnel, multi-target)
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
@@ -183,6 +184,7 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
HTTPMaskMode: defaultConf.HTTPMaskMode,
HTTPMaskTLSEnabled: option.HTTPMaskTLS,
HTTPMaskHost: option.HTTPMaskHost,
HTTPMaskPathRoot: strings.TrimSpace(option.PathRoot),
HTTPMaskMultiplex: defaultConf.HTTPMaskMultiplex,
}
if option.HTTPMaskMode != "" {
@@ -257,7 +259,19 @@ func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfi
return nil, fmt.Errorf("config is required")
}
var c net.Conn
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask && httpTunnelModeEnabled(handshakeCfg.HTTPMaskMode) {
handshakeCfg.DisableHTTPMask = true
}
upgrade := func(raw net.Conn) (net.Conn, error) {
return sudoku.ClientHandshakeWithOptions(raw, &handshakeCfg, sudoku.ClientHandshakeOptions{})
}
var (
c net.Conn
handshakeDone bool
)
if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
switch muxMode {
@@ -266,9 +280,12 @@ func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfi
if errX != nil {
return nil, errX
}
c, err = client.Dial(ctx)
c, err = client.Dial(ctx, upgrade)
default:
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext, upgrade)
}
if err == nil && c != nil {
handshakeDone = true
}
}
if c == nil && err == nil {
@@ -285,14 +302,11 @@ func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfi
defer done(&err)
}
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask && httpTunnelModeEnabled(handshakeCfg.HTTPMaskMode) {
handshakeCfg.DisableHTTPMask = true
}
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{})
if err != nil {
return nil, err
if !handshakeDone {
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{})
if err != nil {
return nil, err
}
}
return c, nil
+1
View File
@@ -356,6 +356,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{
ServiceName: option.GrpcOpts.GrpcServiceName,
UserAgent: option.GrpcOpts.GrpcUserAgent,
Host: option.SNI,
ClientFingerprint: option.ClientFingerprint,
}
+1
View File
@@ -462,6 +462,7 @@ func NewVless(option VlessOption) (*Vless, error) {
gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName,
UserAgent: v.option.GrpcOpts.GrpcUserAgent,
Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
}
+2
View File
@@ -86,6 +86,7 @@ type HTTP2Options struct {
type GrpcOptions struct {
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
GrpcUserAgent string `proxy:"grpc-user-agent,omitempty"`
}
type WSOptions struct {
@@ -467,6 +468,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName,
UserAgent: v.option.GrpcOpts.GrpcUserAgent,
Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
}
+7 -1
View File
@@ -669,6 +669,7 @@ proxies: # socks5
# skip-cert-verify: true
grpc-opts:
grpc-service-name: "example"
# grpc-user-agent: "grpc-go/1.36.0"
# ip-version: ipv4
# vless
@@ -757,6 +758,8 @@ proxies: # socks5
servername: testingcf.jsdelivr.net
grpc-opts:
grpc-service-name: "grpc"
# grpc-user-agent: "grpc-go/1.36.0"
reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
short-id: 10f897e26c4b9478
@@ -825,6 +828,7 @@ proxies: # socks5
udp: true
grpc-opts:
grpc-service-name: "example"
# grpc-user-agent: "grpc-go/1.36.0"
- name: trojan-ws
server: server
@@ -1068,6 +1072,7 @@ proxies: # socks5
# http-mask-mode: legacy # 可选:legacy(默认)、stream、poll、autostream/poll/auto 支持走 CDN/反代
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 httpsfalse 强制 http(不会根据端口自动推断)
# http-mask-host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 http-mask-mode 为 stream/poll/auto 时生效
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
# http-mask-multiplex: off # 可选:off(默认)、auto(复用 h1.1 keep-alive / h2 连接,减少每次建链 RTT)、on(单条隧道内多路复用多个目标连接;仅在 http-mask-mode=stream/poll/auto 生效)
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与服务端端保持相同(如果此处为false,则要求aead不可为none)
@@ -1371,7 +1376,7 @@ listeners:
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: reidr-in-1
- name: redir-in-1
type: redir
port: 10811 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503
listen: 0.0.0.0
@@ -1617,6 +1622,7 @@ listeners:
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与客户端保持相同(如果此处为false,则要求aead不可为none)
disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false
# http-mask-mode: legacy # 可选:legacy(默认)、stream、poll、autostream/poll/auto 支持走 CDN/反代
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
+1 -1
View File
@@ -41,7 +41,7 @@ require (
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443
github.com/metacubex/tls v0.1.0
github.com/metacubex/utls v1.8.3
github.com/metacubex/utls v1.8.4
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f
github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20
github.com/mroth/weightedrand/v2 v2.1.0
+2 -2
View File
@@ -144,8 +144,8 @@ github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTiz
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/tls v0.1.0 h1:1kjR/1q2uU1cZIwiHYEnWzS4L+0Cu1/X3yfIQ76BzNY=
github.com/metacubex/tls v0.1.0/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/utls v1.8.3 h1:0m/yCxm3SK6kWve2lKiFb1pue1wHitJ8sQQD4Ikqde4=
github.com/metacubex/utls v1.8.3/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4=
github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 h1:lhlqpYHopuTLx9xQt22kSA9HtnyTDmk5XjjQVCGHe2E=
+3
View File
@@ -424,6 +424,9 @@ func updateGeneral(general *config.General, logging bool) {
mihomoHttp.SetUA(general.GlobalUA)
resource.SetETag(general.ETagSupport)
if general.GlobalClientFingerprint != "" {
log.Warnln("The `global-client-fingerprint` configuration is deprecated, please set `client-fingerprint` directly on the proxy instead")
}
tlsC.SetGlobalFingerprint(general.GlobalClientFingerprint)
}
+1
View File
@@ -22,6 +22,7 @@ type SudokuServer struct {
CustomTables []string `json:"custom-tables,omitempty"`
DisableHTTPMask bool `json:"disable-http-mask,omitempty"`
HTTPMaskMode string `json:"http-mask-mode,omitempty"`
PathRoot string `json:"path-root,omitempty"`
// mihomo private extension (not the part of standard Sudoku protocol)
MuxOption sing.MuxOption `json:"mux-option,omitempty"`
+2
View File
@@ -24,6 +24,7 @@ type SudokuOption struct {
CustomTables []string `inbound:"custom-tables,omitempty"`
DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"`
HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
PathRoot string `inbound:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
// mihomo private extension (not the part of standard Sudoku protocol)
MuxOption MuxOption `inbound:"mux-option,omitempty"`
@@ -63,6 +64,7 @@ func NewSudoku(options *SudokuOption) (*Sudoku, error) {
CustomTables: options.CustomTables,
DisableHTTPMask: options.DisableHTTPMask,
HTTPMaskMode: options.HTTPMaskMode,
PathRoot: strings.TrimSpace(options.PathRoot),
}
serverConf.MuxOption = options.MuxOption.Build()
+1
View File
@@ -229,6 +229,7 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
HandshakeTimeoutSeconds: handshakeTimeout,
DisableHTTPMask: config.DisableHTTPMask,
HTTPMaskMode: config.HTTPMaskMode,
HTTPMaskPathRoot: strings.TrimSpace(config.PathRoot),
}
if len(tables) == 1 {
protoConf.Table = tables[0]
+8 -1
View File
@@ -60,6 +60,7 @@ type Conn struct {
type Config struct {
ServiceName string
UserAgent string
Host string
ClientFingerprint string
}
@@ -347,6 +348,12 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
path := ServiceNameToPath(serviceName)
reader, writer := io.Pipe()
header := defaultHeader.Clone()
if cfg.UserAgent != "" {
header["user-agent"] = []string{cfg.UserAgent}
}
request := &http.Request{
Method: http.MethodPost,
Body: reader,
@@ -360,7 +367,7 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
Proto: "HTTP/2",
ProtoMajor: 2,
ProtoMinor: 0,
Header: defaultHeader,
Header: header,
}
request = request.WithContext(transport.ctx)
+21
View File
@@ -58,6 +58,10 @@ type ProtocolConfig struct {
// HTTPMaskHost optionally overrides the HTTP Host header / SNI host for HTTP tunnel modes (client-side).
HTTPMaskHost string
// HTTPMaskPathRoot optionally prefixes all HTTP mask paths with a first-level segment.
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
HTTPMaskPathRoot string
// HTTPMaskMultiplex controls multiplex behavior when HTTPMask tunnel modes are enabled:
// - "off": disable reuse; each Dial establishes its own HTTPMask tunnel
// - "auto": reuse underlying HTTP connections across multiple tunnel dials (HTTP/1.1 keep-alive / HTTP/2)
@@ -109,6 +113,23 @@ func (c *ProtocolConfig) Validate() error {
return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto", c.HTTPMaskMode)
}
if v := strings.TrimSpace(c.HTTPMaskPathRoot); v != "" {
if strings.Contains(v, "/") {
return fmt.Errorf("invalid http-mask-path-root: must be a single path segment")
}
for i := 0; i < len(v); i++ {
ch := v[i]
switch {
case ch >= 'a' && ch <= 'z':
case ch >= 'A' && ch <= 'Z':
case ch >= '0' && ch <= '9':
case ch == '_' || ch == '-':
default:
return fmt.Errorf("invalid http-mask-path-root: contains invalid character %q", ch)
}
}
}
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMultiplex)) {
case "", "off", "auto", "on":
default:
+13 -24
View File
@@ -2,7 +2,6 @@ package sudoku
import (
"bufio"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
@@ -153,14 +152,17 @@ func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table,
func buildHandshakePayload(key string) [16]byte {
var payload [16]byte
binary.BigEndian.PutUint64(payload[:8], uint64(time.Now().Unix()))
// Hash the decoded HEX bytes of the key, not the HEX string itself.
// This ensures the user hash is computed on the actual key bytes.
keyBytes, err := hex.DecodeString(key)
if err != nil {
// Fallback: if key is not valid HEX (e.g., a UUID or plain string), hash the string bytes
keyBytes = []byte(key)
// Align with upstream: only decode hex bytes when this key is an ED25519 key material.
// For plain UUID/strings (even if they look like hex), hash the string bytes as-is.
src := []byte(key)
if _, err := crypto.RecoverPublicKey(key); err == nil {
if keyBytes, decErr := hex.DecodeString(key); decErr == nil && len(keyBytes) > 0 {
src = keyBytes
}
}
hash := sha256.Sum256(keyBytes)
hash := sha256.Sum256(src)
copy(payload[8:], hash[:8])
return payload
}
@@ -211,12 +213,12 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *ProtocolConfig, opt Clien
}
if !cfg.DisableHTTPMask {
if err := WriteHTTPMaskHeader(rawConn, cfg.ServerAddress, opt.HTTPMaskStrategy); err != nil {
if err := WriteHTTPMaskHeader(rawConn, cfg.ServerAddress, cfg.HTTPMaskPathRoot, opt.HTTPMaskStrategy); err != nil {
return nil, fmt.Errorf("write http mask failed: %w", err)
}
}
table, tableID, err := pickClientTable(cfg)
table, err := pickClientTable(cfg)
if err != nil {
return nil, err
}
@@ -228,9 +230,6 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *ProtocolConfig, opt Clien
}
handshake := buildHandshakePayload(cfg.Key)
if len(cfg.tableCandidates()) > 1 {
handshake[8] = tableID
}
if _, err := cConn.Write(handshake[:]); err != nil {
cConn.Close()
return nil, fmt.Errorf("send handshake failed: %w", err)
@@ -376,19 +375,9 @@ func normalizeHTTPMaskStrategy(strategy string) string {
}
}
// randomByte returns a cryptographically random byte (with a math/rand fallback).
func randomByte() byte {
var b [1]byte
if _, err := rand.Read(b[:]); err == nil {
return b[0]
}
return byte(time.Now().UnixNano())
}
func userHashFromHandshake(handshakeBuf []byte) string {
if len(handshakeBuf) < 16 {
return ""
}
// handshake[8] may be a table ID when table rotation is enabled; use [9:16] as stable user hash bytes.
return hex.EncodeToString(handshakeBuf[9:16])
return hex.EncodeToString(handshakeBuf[8:16])
}
@@ -7,6 +7,7 @@ import (
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
@@ -92,24 +93,24 @@ func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
// WriteHTTPMaskHeader writes an HTTP/1.x request header as a mask, according to strategy.
// Supported strategies: ""/"random", "post", "websocket".
func WriteHTTPMaskHeader(w io.Writer, host string, strategy string) error {
func WriteHTTPMaskHeader(w io.Writer, host string, pathRoot string, strategy string) error {
switch normalizeHTTPMaskStrategy(strategy) {
case "random":
return httpmask.WriteRandomRequestHeader(w, host)
return httpmask.WriteRandomRequestHeaderWithPathRoot(w, host, pathRoot)
case "post":
return writeHTTPMaskPOST(w, host)
return writeHTTPMaskPOST(w, host, pathRoot)
case "websocket":
return writeHTTPMaskWebSocket(w, host)
return writeHTTPMaskWebSocket(w, host, pathRoot)
default:
return fmt.Errorf("unsupported http-mask-strategy: %s", strategy)
}
}
func writeHTTPMaskPOST(w io.Writer, host string) error {
func writeHTTPMaskPOST(w io.Writer, host string, pathRoot string) error {
r := httpMaskRngPool.Get().(*rand.Rand)
defer httpMaskRngPool.Put(r)
path := httpMaskPaths[r.Intn(len(httpMaskPaths))]
path := joinPathRoot(pathRoot, httpMaskPaths[r.Intn(len(httpMaskPaths))])
ctype := httpMaskContentTypes[r.Intn(len(httpMaskContentTypes))]
bufPtr := httpMaskBufPool.Get().(*[]byte)
@@ -140,11 +141,11 @@ func writeHTTPMaskPOST(w io.Writer, host string) error {
return err
}
func writeHTTPMaskWebSocket(w io.Writer, host string) error {
func writeHTTPMaskWebSocket(w io.Writer, host string, pathRoot string) error {
r := httpMaskRngPool.Get().(*rand.Rand)
defer httpMaskRngPool.Put(r)
path := httpMaskPaths[r.Intn(len(httpMaskPaths))]
path := joinPathRoot(pathRoot, httpMaskPaths[r.Intn(len(httpMaskPaths))])
bufPtr := httpMaskBufPool.Get().(*[]byte)
buf := *bufPtr
@@ -177,3 +178,37 @@ func writeHTTPMaskWebSocket(w io.Writer, host string) error {
_, err := w.Write(buf)
return err
}
func normalizePathRoot(root string) string {
root = strings.TrimSpace(root)
root = strings.Trim(root, "/")
if root == "" {
return ""
}
for i := 0; i < len(root); i++ {
c := root[i]
switch {
case c >= 'a' && c <= 'z':
case c >= 'A' && c <= 'Z':
case c >= '0' && c <= '9':
case c == '_' || c == '-':
default:
return ""
}
}
return "/" + root
}
func joinPathRoot(root, path string) string {
root = normalizePathRoot(root)
if root == "" {
return path
}
if path == "" {
return root
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return root + path
}
+24 -8
View File
@@ -23,7 +23,11 @@ func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{Mode: cfg.HTTPMaskMode})
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{
Mode: cfg.HTTPMaskMode,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ClientAEADSeed(cfg.Key),
})
}
}
return &HTTPMaskTunnelServer{cfg: cfg, ts: ts}
@@ -67,7 +71,7 @@ func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Con
type TunnelDialer func(ctx context.Context, network, addr string) (net.Conn, error)
// DialHTTPMaskTunnel dials a CDN-capable HTTP tunnel (stream/poll/auto) and returns a stream carrying raw Sudoku bytes.
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (net.Conn, error) {
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
@@ -83,14 +87,19 @@ func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *Protocol
Mode: cfg.HTTPMaskMode,
TLSEnabled: cfg.HTTPMaskTLSEnabled,
HostOverride: cfg.HTTPMaskHost,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ClientAEADSeed(cfg.Key),
Upgrade: upgrade,
Multiplex: cfg.HTTPMaskMultiplex,
DialContext: dial,
})
}
type HTTPMaskTunnelClient struct {
mode string
client *httpmask.TunnelClient
mode string
pathRoot string
authKey string
client *httpmask.TunnelClient
}
func NewHTTPMaskTunnelClient(serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (*HTTPMaskTunnelClient, error) {
@@ -121,16 +130,23 @@ func NewHTTPMaskTunnelClient(serverAddress string, cfg *ProtocolConfig, dial Tun
}
return &HTTPMaskTunnelClient{
mode: cfg.HTTPMaskMode,
client: c,
mode: cfg.HTTPMaskMode,
pathRoot: cfg.HTTPMaskPathRoot,
authKey: ClientAEADSeed(cfg.Key),
client: c,
}, nil
}
func (c *HTTPMaskTunnelClient) Dial(ctx context.Context) (net.Conn, error) {
func (c *HTTPMaskTunnelClient) Dial(ctx context.Context, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
if c == nil || c.client == nil {
return nil, fmt.Errorf("nil httpmask tunnel client")
}
return c.client.DialTunnel(ctx, c.mode)
return c.client.DialTunnel(ctx, httpmask.TunnelDialOptions{
Mode: c.mode,
PathRoot: c.pathRoot,
AuthKey: c.authKey,
Upgrade: upgrade,
})
}
func (c *HTTPMaskTunnelClient) CloseIdleConnections() {
@@ -154,7 +154,7 @@ func TestHTTPMaskTunnel_Stream_TCPRoundTrip(t *testing.T) {
clientCfg.ServerAddress = addr
clientCfg.HTTPMaskHost = "example.com"
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
@@ -225,7 +225,7 @@ func TestHTTPMaskTunnel_Poll_UoTRoundTrip(t *testing.T) {
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
@@ -287,7 +287,7 @@ func TestHTTPMaskTunnel_Auto_TCPRoundTrip(t *testing.T) {
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
@@ -331,13 +331,13 @@ func TestHTTPMaskTunnel_Validation(t *testing.T) {
cfg.DisableHTTPMask = true
cfg.HTTPMaskMode = "stream"
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext, nil); err == nil {
t.Fatalf("expected error for disabled http mask")
}
cfg.DisableHTTPMask = false
cfg.HTTPMaskMode = "legacy"
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext, nil); err == nil {
t.Fatalf("expected error for legacy mode")
}
}
@@ -385,7 +385,7 @@ func TestHTTPMaskTunnel_Soak_Concurrent(t *testing.T) {
clientCfg.ServerAddress = addr
clientCfg.HTTPMaskHost = strings.TrimSpace(clientCfg.HTTPMaskHost)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
runErr <- fmt.Errorf("dial: %w", err)
return
@@ -99,7 +99,7 @@ func TestUserHash_StableAcrossTableRotation(t *testing.T) {
if h == "" {
t.Fatalf("empty user hash")
}
if len(h) != 14 {
if len(h) != 16 {
t.Fatalf("unexpected user hash length: %d", len(h))
}
unique[h] = struct{}{}
@@ -258,4 +258,3 @@ func TestMultiplex_Boundary_InvalidVersion(t *testing.T) {
t.Fatalf("expected error")
}
}
@@ -0,0 +1,137 @@
package httpmask
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"strings"
"time"
)
const (
tunnelAuthHeaderKey = "Authorization"
tunnelAuthHeaderPrefix = "Bearer "
)
type tunnelAuth struct {
key [32]byte // derived HMAC key
skew time.Duration
}
func newTunnelAuth(key string, skew time.Duration) *tunnelAuth {
key = strings.TrimSpace(key)
if key == "" {
return nil
}
if skew <= 0 {
skew = 60 * time.Second
}
// Domain separation: keep this HMAC key independent from other uses of cfg.Key.
h := sha256.New()
_, _ = h.Write([]byte("sudoku-httpmask-auth-v1:"))
_, _ = h.Write([]byte(key))
var sum [32]byte
h.Sum(sum[:0])
return &tunnelAuth{key: sum, skew: skew}
}
func (a *tunnelAuth) token(mode TunnelMode, method, path string, now time.Time) string {
if a == nil {
return ""
}
ts := now.Unix()
sig := a.sign(mode, method, path, ts)
var buf [8 + 16]byte
binary.BigEndian.PutUint64(buf[:8], uint64(ts))
copy(buf[8:], sig[:])
return base64.RawURLEncoding.EncodeToString(buf[:])
}
func (a *tunnelAuth) verify(headers map[string]string, mode TunnelMode, method, path string, now time.Time) bool {
if a == nil {
return true
}
if headers == nil {
return false
}
val := strings.TrimSpace(headers["authorization"])
if val == "" {
return false
}
// Accept both "Bearer <token>" and raw token forms (for forward proxies / CDNs that may normalize headers).
if len(val) > len(tunnelAuthHeaderPrefix) && strings.EqualFold(val[:len(tunnelAuthHeaderPrefix)], tunnelAuthHeaderPrefix) {
val = strings.TrimSpace(val[len(tunnelAuthHeaderPrefix):])
}
if val == "" {
return false
}
raw, err := base64.RawURLEncoding.DecodeString(val)
if err != nil || len(raw) != 8+16 {
return false
}
ts := int64(binary.BigEndian.Uint64(raw[:8]))
nowTS := now.Unix()
delta := nowTS - ts
if delta < 0 {
delta = -delta
}
if delta > int64(a.skew.Seconds()) {
return false
}
want := a.sign(mode, method, path, ts)
return subtle.ConstantTimeCompare(raw[8:], want[:]) == 1
}
func (a *tunnelAuth) sign(mode TunnelMode, method, path string, ts int64) [16]byte {
method = strings.ToUpper(strings.TrimSpace(method))
if method == "" {
method = "GET"
}
path = strings.TrimSpace(path)
var tsBuf [8]byte
binary.BigEndian.PutUint64(tsBuf[:], uint64(ts))
mac := hmac.New(sha256.New, a.key[:])
_, _ = mac.Write([]byte(mode))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write([]byte(method))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write([]byte(path))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write(tsBuf[:])
var full [32]byte
mac.Sum(full[:0])
var out [16]byte
copy(out[:], full[:16])
return out
}
type headerSetter interface {
Set(key, value string)
}
func applyTunnelAuthHeader(h headerSetter, auth *tunnelAuth, mode TunnelMode, method, path string) {
if auth == nil || h == nil {
return
}
token := auth.token(mode, method, path, time.Now())
if token == "" {
return
}
h.Set(tunnelAuthHeaderKey, tunnelAuthHeaderPrefix+token)
}
@@ -129,11 +129,17 @@ func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
// WriteRandomRequestHeader writes a plausible HTTP/1.1 request header as a mask.
func WriteRandomRequestHeader(w io.Writer, host string) error {
return WriteRandomRequestHeaderWithPathRoot(w, host, "")
}
// WriteRandomRequestHeaderWithPathRoot is like WriteRandomRequestHeader but prefixes all paths with pathRoot.
// pathRoot must be a single segment (e.g. "aabbcc"); invalid inputs are treated as empty (disabled).
func WriteRandomRequestHeaderWithPathRoot(w io.Writer, host string, pathRoot string) error {
// Get RNG from pool
r := rngPool.Get().(*rand.Rand)
defer rngPool.Put(r)
path := paths[r.Intn(len(paths))]
path := joinPathRoot(pathRoot, paths[r.Intn(len(paths))])
ctype := contentTypes[r.Intn(len(contentTypes))]
// Use buffer pool
@@ -0,0 +1,52 @@
package httpmask
import "strings"
// normalizePathRoot normalizes the configured path root into "/<segment>" form.
//
// It is intentionally strict: only a single path segment is allowed, consisting of
// [A-Za-z0-9_-]. Invalid inputs are treated as empty (disabled).
func normalizePathRoot(root string) string {
root = strings.TrimSpace(root)
root = strings.Trim(root, "/")
if root == "" {
return ""
}
for i := 0; i < len(root); i++ {
c := root[i]
switch {
case c >= 'a' && c <= 'z':
case c >= 'A' && c <= 'Z':
case c >= '0' && c <= '9':
case c == '_' || c == '-':
default:
return ""
}
}
return "/" + root
}
func joinPathRoot(root, path string) string {
root = normalizePathRoot(root)
if root == "" {
return path
}
if path == "" {
return root
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return root + path
}
func stripPathRoot(root, fullPath string) (string, bool) {
root = normalizePathRoot(root)
if root == "" {
return fullPath, true
}
if !strings.HasPrefix(fullPath, root+"/") {
return "", false
}
return strings.TrimPrefix(fullPath, root), true
}
@@ -62,6 +62,15 @@ type TunnelDialOptions struct {
Mode string
TLSEnabled bool // when true, use HTTPS; otherwise, use HTTP (no port-based inference)
HostOverride string // optional Host header / SNI host (without scheme); accepts "example.com" or "example.com:443"
// PathRoot is an optional first-level path prefix for all HTTP tunnel endpoints.
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
PathRoot string
// AuthKey enables short-term HMAC auth for HTTP tunnel requests (anti-probing).
// When set (non-empty), each HTTP request carries an Authorization bearer token derived from AuthKey.
AuthKey string
// Upgrade optionally wraps the raw tunnel conn and/or writes a small prelude before DialTunnel returns.
// It is called with the raw tunnel conn; if it returns a non-nil conn, that conn is returned by DialTunnel.
Upgrade func(raw net.Conn) (net.Conn, error)
// Multiplex controls whether the caller should reuse underlying HTTP connections (HTTP/1.1 keep-alive / HTTP/2).
// To reuse across multiple dials, create a TunnelClient per proxy and reuse it.
// Values: "off" disables reuse; "auto"/"on" enables it.
@@ -109,34 +118,34 @@ func (c *TunnelClient) CloseIdleConnections() {
c.transport.CloseIdleConnections()
}
func (c *TunnelClient) DialTunnel(ctx context.Context, mode string) (net.Conn, error) {
func (c *TunnelClient) DialTunnel(ctx context.Context, opts TunnelDialOptions) (net.Conn, error) {
if c == nil || c.client == nil {
return nil, fmt.Errorf("nil tunnel client")
}
tm := normalizeTunnelMode(mode)
tm := normalizeTunnelMode(opts.Mode)
if tm == TunnelModeLegacy {
return nil, fmt.Errorf("legacy mode does not use http tunnel")
}
switch tm {
case TunnelModeStream:
return dialStreamWithClient(ctx, c.client, c.target)
return dialStreamWithClient(ctx, c.client, c.target, opts)
case TunnelModePoll:
return dialPollWithClient(ctx, c.client, c.target)
return dialPollWithClient(ctx, c.client, c.target, opts)
case TunnelModeAuto:
streamCtx, cancelX := context.WithTimeout(ctx, 3*time.Second)
c1, errX := dialStreamWithClient(streamCtx, c.client, c.target)
c1, errX := dialStreamWithClient(streamCtx, c.client, c.target, opts)
cancelX()
if errX == nil {
return c1, nil
}
c2, errP := dialPollWithClient(ctx, c.client, c.target)
c2, errP := dialPollWithClient(ctx, c.client, c.target, opts)
if errP == nil {
return c2, nil
}
return nil, fmt.Errorf("auto tunnel failed: stream: %v; poll: %w", errX, errP)
default:
return dialStreamWithClient(ctx, c.client, c.target)
return dialStreamWithClient(ctx, c.client, c.target, opts)
}
}
@@ -248,8 +257,13 @@ func (c *httpStreamConn) Close() error {
if c.cancel != nil {
c.cancel()
}
_ = c.writer.CloseWithError(io.ErrClosedPipe)
return c.reader.Close()
if c.writer != nil {
_ = c.writer.CloseWithError(io.ErrClosedPipe)
}
if c.reader != nil {
return c.reader.Close()
}
return nil
}
func (c *httpStreamConn) LocalAddr() net.Addr { return c.localAddr }
@@ -320,20 +334,23 @@ type sessionDialInfo struct {
pullURL string
closeURL string
headerHost string
auth *tunnelAuth
}
func dialSessionWithClient(ctx context.Context, client *http.Client, target httpClientTarget, mode TunnelMode) (*sessionDialInfo, error) {
func dialSessionWithClient(ctx context.Context, client *http.Client, target httpClientTarget, mode TunnelMode, opts TunnelDialOptions) (*sessionDialInfo, error) {
if client == nil {
return nil, fmt.Errorf("nil http client")
}
authorizeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: "/session"}).String()
auth := newTunnelAuth(opts.AuthKey, 0)
authorizeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/session")}).String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, authorizeURL, nil)
if err != nil {
return nil, err
}
req.Host = target.headerHost
applyTunnelHeaders(req.Header, target.headerHost, mode)
applyTunnelAuthHeader(req.Header, auth, mode, http.MethodGet, "/session")
resp, err := client.Do(req)
if err != nil {
@@ -356,9 +373,9 @@ func dialSessionWithClient(ctx context.Context, client *http.Client, target http
return nil, fmt.Errorf("%s authorize empty token", mode)
}
pushURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: "/api/v1/upload", RawQuery: "token=" + url.QueryEscape(token)}).String()
pullURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: "/stream", RawQuery: "token=" + url.QueryEscape(token)}).String()
closeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: "/api/v1/upload", RawQuery: "token=" + url.QueryEscape(token) + "&close=1"}).String()
pushURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/api/v1/upload"), RawQuery: "token=" + url.QueryEscape(token)}).String()
pullURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/stream"), RawQuery: "token=" + url.QueryEscape(token)}).String()
closeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/api/v1/upload"), RawQuery: "token=" + url.QueryEscape(token) + "&close=1"}).String()
return &sessionDialInfo{
client: client,
@@ -366,6 +383,7 @@ func dialSessionWithClient(ctx context.Context, client *http.Client, target http
pullURL: pullURL,
closeURL: closeURL,
headerHost: target.headerHost,
auth: auth,
}, nil
}
@@ -374,10 +392,10 @@ func dialSession(ctx context.Context, serverAddress string, opts TunnelDialOptio
if err != nil {
return nil, err
}
return dialSessionWithClient(ctx, client, target, mode)
return dialSessionWithClient(ctx, client, target, mode, opts)
}
func bestEffortCloseSession(client *http.Client, closeURL, headerHost string, mode TunnelMode) {
func bestEffortCloseSession(client *http.Client, closeURL, headerHost string, mode TunnelMode, auth *tunnelAuth) {
if client == nil || closeURL == "" || headerHost == "" {
return
}
@@ -391,6 +409,7 @@ func bestEffortCloseSession(client *http.Client, closeURL, headerHost string, mo
}
req.Host = headerHost
applyTunnelHeaders(req.Header, headerHost, mode)
applyTunnelAuthHeader(req.Header, auth, mode, http.MethodPost, "/api/v1/upload")
resp, err := client.Do(req)
if err != nil || resp == nil {
@@ -400,13 +419,13 @@ func bestEffortCloseSession(client *http.Client, closeURL, headerHost string, mo
_ = resp.Body.Close()
}
func dialStreamWithClient(ctx context.Context, client *http.Client, target httpClientTarget) (net.Conn, error) {
// Prefer split session (Cloudflare-friendly). Fall back to stream-one for older servers / environments.
c, errSplit := dialStreamSplitWithClient(ctx, client, target)
func dialStreamWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) {
// Prefer split-session (Cloudflare-friendly). Fall back to stream-one for older servers / environments.
c, errSplit := dialStreamSplitWithClient(ctx, client, target, opts)
if errSplit == nil {
return c, nil
}
c2, errOne := dialStreamOneWithClient(ctx, client, target)
c2, errOne := dialStreamOneWithClient(ctx, client, target, opts)
if errOne == nil {
return c2, nil
}
@@ -414,7 +433,7 @@ func dialStreamWithClient(ctx context.Context, client *http.Client, target httpC
}
func dialStream(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
// Prefer split session (Cloudflare-friendly). Fall back to stream-one for older servers / environments.
// Prefer split-session (Cloudflare-friendly). Fall back to stream-one for older servers / environments.
c, errSplit := dialStreamSplit(ctx, serverAddress, opts)
if errSplit == nil {
return c, nil
@@ -426,13 +445,15 @@ func dialStream(ctx context.Context, serverAddress string, opts TunnelDialOption
return nil, fmt.Errorf("dial stream failed: split: %v; stream-one: %w", errSplit, errOne)
}
func dialStreamOneWithClient(ctx context.Context, client *http.Client, target httpClientTarget) (net.Conn, error) {
func dialStreamOneWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) {
if client == nil {
return nil, fmt.Errorf("nil http client")
}
auth := newTunnelAuth(opts.AuthKey, 0)
r := rngPool.Get().(*mrand.Rand)
path := paths[r.Intn(len(paths))]
basePath := paths[r.Intn(len(paths))]
path := joinPathRoot(opts.PathRoot, basePath)
ctype := contentTypes[r.Intn(len(contentTypes))]
rngPool.Put(r)
@@ -454,6 +475,7 @@ func dialStreamOneWithClient(ctx context.Context, client *http.Client, target ht
req.Host = target.headerHost
applyTunnelHeaders(req.Header, target.headerHost, TunnelModeStream)
applyTunnelAuthHeader(req.Header, auth, TunnelModeStream, http.MethodPost, basePath)
req.Header.Set("Content-Type", ctype)
type doResult struct {
@@ -466,33 +488,84 @@ func dialStreamOneWithClient(ctx context.Context, client *http.Client, target ht
doCh <- doResult{resp: resp, err: doErr}
}()
select {
case <-ctx.Done():
connCancel()
_ = reqBodyW.Close()
return nil, ctx.Err()
case r := <-doCh:
if r.err != nil {
connCancel()
_ = reqBodyW.Close()
return nil, r.err
}
if r.resp.StatusCode != http.StatusOK {
defer r.resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(r.resp.Body, 4*1024))
connCancel()
_ = reqBodyW.Close()
return nil, fmt.Errorf("stream bad status: %s (%s)", r.resp.Status, strings.TrimSpace(string(body)))
}
return &httpStreamConn{
reader: r.resp.Body,
writer: reqBodyW,
cancel: connCancel,
localAddr: &net.TCPAddr{},
remoteAddr: &net.TCPAddr{},
}, nil
streamConn := &httpStreamConn{
writer: reqBodyW,
cancel: connCancel,
localAddr: &net.TCPAddr{},
remoteAddr: &net.TCPAddr{},
}
type upgradeResult struct {
conn net.Conn
err error
}
upgradeCh := make(chan upgradeResult, 1)
if opts.Upgrade == nil {
upgradeCh <- upgradeResult{conn: streamConn, err: nil}
} else {
go func() {
upgradeConn, err := opts.Upgrade(streamConn)
if err != nil {
upgradeCh <- upgradeResult{conn: nil, err: err}
return
}
if upgradeConn == nil {
upgradeConn = streamConn
}
upgradeCh <- upgradeResult{conn: upgradeConn, err: nil}
}()
}
var (
outConn net.Conn
upgradeDone bool
responseReady bool
)
for !(upgradeDone && responseReady) {
select {
case <-ctx.Done():
_ = streamConn.Close()
if outConn != nil && outConn != streamConn {
_ = outConn.Close()
}
return nil, ctx.Err()
case u := <-upgradeCh:
if u.err != nil {
_ = streamConn.Close()
return nil, u.err
}
outConn = u.conn
if outConn == nil {
outConn = streamConn
}
upgradeDone = true
case r := <-doCh:
if r.err != nil {
_ = streamConn.Close()
if outConn != nil && outConn != streamConn {
_ = outConn.Close()
}
return nil, r.err
}
if r.resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(r.resp.Body, 4*1024))
_ = r.resp.Body.Close()
_ = streamConn.Close()
if outConn != nil && outConn != streamConn {
_ = outConn.Close()
}
return nil, fmt.Errorf("stream bad status: %s (%s)", r.resp.Status, strings.TrimSpace(string(body)))
}
streamConn.reader = r.resp.Body
responseReady = true
}
}
return outConn, nil
}
func dialStreamOne(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
@@ -500,7 +573,7 @@ func dialStreamOne(ctx context.Context, serverAddress string, opts TunnelDialOpt
if err != nil {
return nil, err
}
return dialStreamOneWithClient(ctx, client, target)
return dialStreamOneWithClient(ctx, client, target, opts)
}
type queuedConn struct {
@@ -599,6 +672,7 @@ type streamSplitConn struct {
pullURL string
closeURL string
headerHost string
auth *tunnelAuth
}
func (c *streamSplitConn) Close() error {
@@ -607,7 +681,7 @@ func (c *streamSplitConn) Close() error {
if c.cancel != nil {
c.cancel()
}
bestEffortCloseSession(c.client, c.closeURL, c.headerHost, TunnelModeStream)
bestEffortCloseSession(c.client, c.closeURL, c.headerHost, TunnelModeStream, c.auth)
return nil
}
@@ -625,6 +699,7 @@ func newStreamSplitConnFromInfo(info *sessionDialInfo) *streamSplitConn {
pullURL: info.pullURL,
closeURL: info.closeURL,
headerHost: info.headerHost,
auth: info.auth,
queuedConn: queuedConn{
rxc: make(chan []byte, 256),
closed: make(chan struct{}),
@@ -639,8 +714,8 @@ func newStreamSplitConnFromInfo(info *sessionDialInfo) *streamSplitConn {
return c
}
func dialStreamSplitWithClient(ctx context.Context, client *http.Client, target httpClientTarget) (net.Conn, error) {
info, err := dialSessionWithClient(ctx, client, target, TunnelModeStream)
func dialStreamSplitWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) {
info, err := dialSessionWithClient(ctx, client, target, TunnelModeStream, opts)
if err != nil {
return nil, err
}
@@ -648,7 +723,18 @@ func dialStreamSplitWithClient(ctx context.Context, client *http.Client, target
if c == nil {
return nil, fmt.Errorf("failed to build stream split conn")
}
return c, nil
outConn := net.Conn(c)
if opts.Upgrade != nil {
upgraded, err := opts.Upgrade(c)
if err != nil {
_ = c.Close()
return nil, err
}
if upgraded != nil {
outConn = upgraded
}
}
return outConn, nil
}
func dialStreamSplit(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
@@ -660,7 +746,18 @@ func dialStreamSplit(ctx context.Context, serverAddress string, opts TunnelDialO
if c == nil {
return nil, fmt.Errorf("failed to build stream split conn")
}
return c, nil
outConn := net.Conn(c)
if opts.Upgrade != nil {
upgraded, err := opts.Upgrade(c)
if err != nil {
_ = c.Close()
return nil, err
}
if upgraded != nil {
outConn = upgraded
}
}
return outConn, nil
}
func (c *streamSplitConn) pullLoop() {
@@ -696,6 +793,7 @@ func (c *streamSplitConn) pullLoop() {
}
req.Host = c.headerHost
applyTunnelHeaders(req.Header, c.headerHost, TunnelModeStream)
applyTunnelAuthHeader(req.Header, c.auth, TunnelModeStream, http.MethodGet, "/stream")
resp, err := c.client.Do(req)
if err != nil {
@@ -793,6 +891,7 @@ func (c *streamSplitConn) pushLoop() {
}
req.Host = c.headerHost
applyTunnelHeaders(req.Header, c.headerHost, TunnelModeStream)
applyTunnelAuthHeader(req.Header, c.auth, TunnelModeStream, http.MethodPost, "/api/v1/upload")
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := c.client.Do(req)
@@ -896,6 +995,7 @@ type pollConn struct {
pullURL string
closeURL string
headerHost string
auth *tunnelAuth
}
func isDialError(err error) bool {
@@ -917,7 +1017,7 @@ func (c *pollConn) closeWithError(err error) error {
if c.cancel != nil {
c.cancel()
}
bestEffortCloseSession(c.client, c.closeURL, c.headerHost, TunnelModePoll)
bestEffortCloseSession(c.client, c.closeURL, c.headerHost, TunnelModePoll, c.auth)
return nil
}
@@ -939,6 +1039,7 @@ func newPollConnFromInfo(info *sessionDialInfo) *pollConn {
pullURL: info.pullURL,
closeURL: info.closeURL,
headerHost: info.headerHost,
auth: info.auth,
queuedConn: queuedConn{
rxc: make(chan []byte, 128),
closed: make(chan struct{}),
@@ -953,8 +1054,8 @@ func newPollConnFromInfo(info *sessionDialInfo) *pollConn {
return c
}
func dialPollWithClient(ctx context.Context, client *http.Client, target httpClientTarget) (net.Conn, error) {
info, err := dialSessionWithClient(ctx, client, target, TunnelModePoll)
func dialPollWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) {
info, err := dialSessionWithClient(ctx, client, target, TunnelModePoll, opts)
if err != nil {
return nil, err
}
@@ -962,7 +1063,18 @@ func dialPollWithClient(ctx context.Context, client *http.Client, target httpCli
if c == nil {
return nil, fmt.Errorf("failed to build poll conn")
}
return c, nil
outConn := net.Conn(c)
if opts.Upgrade != nil {
upgraded, err := opts.Upgrade(c)
if err != nil {
_ = c.Close()
return nil, err
}
if upgraded != nil {
outConn = upgraded
}
}
return outConn, nil
}
func dialPoll(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
@@ -974,7 +1086,18 @@ func dialPoll(ctx context.Context, serverAddress string, opts TunnelDialOptions)
if c == nil {
return nil, fmt.Errorf("failed to build poll conn")
}
return c, nil
outConn := net.Conn(c)
if opts.Upgrade != nil {
upgraded, err := opts.Upgrade(c)
if err != nil {
_ = c.Close()
return nil, err
}
if upgraded != nil {
outConn = upgraded
}
}
return outConn, nil
}
func (c *pollConn) pullLoop() {
@@ -1001,6 +1124,7 @@ func (c *pollConn) pullLoop() {
}
req.Host = c.headerHost
applyTunnelHeaders(req.Header, c.headerHost, TunnelModePoll)
applyTunnelAuthHeader(req.Header, c.auth, TunnelModePoll, http.MethodGet, "/stream")
resp, err := c.client.Do(req)
if err != nil {
@@ -1084,6 +1208,7 @@ func (c *pollConn) pushLoop() {
}
req.Host = c.headerHost
applyTunnelHeaders(req.Header, c.headerHost, TunnelModePoll)
applyTunnelAuthHeader(req.Header, c.auth, TunnelModePoll, http.MethodPost, "/api/v1/upload")
req.Header.Set("Content-Type", "text/plain")
resp, err := c.client.Do(req)
@@ -1246,6 +1371,18 @@ func applyTunnelHeaders(h http.Header, host string, mode TunnelMode) {
type TunnelServerOptions struct {
Mode string
// PathRoot is an optional first-level path prefix for all HTTP tunnel endpoints.
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
PathRoot string
// AuthKey enables short-term HMAC auth for HTTP tunnel requests (anti-probing).
// When set (non-empty), the server requires each request to carry a valid Authorization bearer token.
AuthKey string
// AuthSkew controls allowed clock skew / replay window for AuthKey. 0 uses a conservative default.
AuthSkew time.Duration
// PassThroughOnReject controls how the server handles "recognized but rejected" tunnel requests
// (e.g., wrong mode / wrong path / invalid token). When true, the request bytes are replayed back
// to the caller as HandlePassThrough to allow higher-level fallback handling.
PassThroughOnReject bool
// PullReadTimeout controls how long the server long-poll waits for tunnel downlink data before replying with a keepalive newline.
PullReadTimeout time.Duration
// SessionTTL is a best-effort TTL to prevent leaked sessions. 0 uses a conservative default.
@@ -1253,7 +1390,10 @@ type TunnelServerOptions struct {
}
type TunnelServer struct {
mode TunnelMode
mode TunnelMode
pathRoot string
passThroughOnReject bool
auth *tunnelAuth
pullReadTimeout time.Duration
sessionTTL time.Duration
@@ -1272,6 +1412,8 @@ func NewTunnelServer(opts TunnelServerOptions) *TunnelServer {
if mode == TunnelModeLegacy {
// Server-side "legacy" means: don't accept stream/poll tunnels; only passthrough.
}
pathRoot := normalizePathRoot(opts.PathRoot)
auth := newTunnelAuth(opts.AuthKey, opts.AuthSkew)
timeout := opts.PullReadTimeout
if timeout <= 0 {
timeout = 10 * time.Second
@@ -1281,10 +1423,13 @@ func NewTunnelServer(opts TunnelServerOptions) *TunnelServer {
ttl = 2 * time.Minute
}
return &TunnelServer{
mode: mode,
pullReadTimeout: timeout,
sessionTTL: ttl,
sessions: make(map[string]*tunnelSession),
mode: mode,
pathRoot: pathRoot,
auth: auth,
passThroughOnReject: opts.PassThroughOnReject,
pullReadTimeout: timeout,
sessionTTL: ttl,
sessions: make(map[string]*tunnelSession),
}
}
@@ -1340,6 +1485,12 @@ func (s *TunnelServer) HandleConn(rawConn net.Conn) (HandleResult, net.Conn, err
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
if s.mode == TunnelModeLegacy {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
@@ -1348,19 +1499,37 @@ func (s *TunnelServer) HandleConn(rawConn net.Conn) (HandleResult, net.Conn, err
switch TunnelMode(tunnelHeader) {
case TunnelModeStream:
if s.mode != TunnelModeStream && s.mode != TunnelModeAuto {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
}
return s.handleStream(rawConn, req, buffered)
return s.handleStream(rawConn, req, headerBytes, buffered)
case TunnelModePoll:
if s.mode != TunnelModePoll && s.mode != TunnelModeAuto {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
}
return s.handlePoll(rawConn, req, buffered)
return s.handlePoll(rawConn, req, headerBytes, buffered)
default:
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
@@ -1507,19 +1676,31 @@ func (c *bodyConn) Close() error {
return firstErr
}
func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, buffered []byte) (HandleResult, net.Conn, error) {
u, err := url.ParseRequestURI(req.target)
if err != nil {
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, headerBytes []byte, buffered []byte) (HandleResult, net.Conn, error) {
rejectOrReply := func(code int, body string) (HandleResult, net.Conn, error) {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, code, body)
_ = rawConn.Close()
return HandleDone, nil, nil
}
u, err := url.ParseRequestURI(req.target)
if err != nil {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
// Only accept plausible paths to reduce accidental exposure.
if !isAllowedPath(req.target) {
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
path, ok := stripPathRoot(s.pathRoot, u.Path)
if !ok || !s.isAllowedBasePath(path) {
return rejectOrReply(http.StatusNotFound, "not found")
}
if !s.auth.verify(req.headers, TunnelModeStream, req.method, path, time.Now()) {
return rejectOrReply(http.StatusNotFound, "not found")
}
token := u.Query().Get("token")
@@ -1528,31 +1709,25 @@ func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, bu
switch strings.ToUpper(req.method) {
case http.MethodGet:
// Stream split-session: GET /session (no token) => token + start tunnel on a server-side pipe.
if token == "" && u.Path == "/session" {
if token == "" && path == "/session" {
return s.authorizeSession(rawConn)
}
// Stream split-session: GET /stream?token=... => downlink poll.
if token != "" && u.Path == "/stream" {
if token != "" && path == "/stream" {
return s.streamPull(rawConn, token)
}
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusBadRequest, "bad request")
case http.MethodPost:
// Stream split-session: POST /api/v1/upload?token=... => uplink push.
if token != "" && u.Path == "/api/v1/upload" {
if token != "" && path == "/api/v1/upload" {
if closeFlag {
s.closeSession(token)
_ = writeSimpleHTTPResponse(rawConn, http.StatusOK, "")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusOK, "")
}
bodyReader, err := newRequestBodyReader(newPreBufferedConn(rawConn, buffered), req.headers)
if err != nil {
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusBadRequest, "bad request")
}
return s.streamPush(rawConn, token, bodyReader)
}
@@ -1581,19 +1756,13 @@ func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, bu
return HandleStartTunnel, stream, nil
default:
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusBadRequest, "bad request")
}
}
func isAllowedPath(target string) bool {
u, err := url.ParseRequestURI(target)
if err != nil {
return false
}
func (s *TunnelServer) isAllowedBasePath(path string) bool {
for _, p := range paths {
if u.Path == p {
if path == p {
return true
}
}
@@ -1650,51 +1819,58 @@ func writeTokenHTTPResponse(w io.Writer, token string) error {
return err
}
func (s *TunnelServer) handlePoll(rawConn net.Conn, req *httpRequestHeader, buffered []byte) (HandleResult, net.Conn, error) {
u, err := url.ParseRequestURI(req.target)
if err != nil {
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
func (s *TunnelServer) handlePoll(rawConn net.Conn, req *httpRequestHeader, headerBytes []byte, buffered []byte) (HandleResult, net.Conn, error) {
rejectOrReply := func(code int, body string) (HandleResult, net.Conn, error) {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, code, body)
_ = rawConn.Close()
return HandleDone, nil, nil
}
if !isAllowedPath(req.target) {
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
u, err := url.ParseRequestURI(req.target)
if err != nil {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
path, ok := stripPathRoot(s.pathRoot, u.Path)
if !ok || !s.isAllowedBasePath(path) {
return rejectOrReply(http.StatusNotFound, "not found")
}
if !s.auth.verify(req.headers, TunnelModePoll, req.method, path, time.Now()) {
return rejectOrReply(http.StatusNotFound, "not found")
}
token := u.Query().Get("token")
closeFlag := u.Query().Get("close") == "1"
switch strings.ToUpper(req.method) {
case http.MethodGet:
if token == "" {
if token == "" && path == "/session" {
return s.authorizeSession(rawConn)
}
return s.pollPull(rawConn, token)
if token != "" && path == "/stream" {
return s.pollPull(rawConn, token)
}
return rejectOrReply(http.StatusBadRequest, "bad request")
case http.MethodPost:
if token == "" {
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "missing token")
_ = rawConn.Close()
return HandleDone, nil, nil
if token == "" || path != "/api/v1/upload" {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
if closeFlag {
s.closeSession(token)
_ = writeSimpleHTTPResponse(rawConn, http.StatusOK, "")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusOK, "")
}
bodyReader, err := newRequestBodyReader(newPreBufferedConn(rawConn, buffered), req.headers)
if err != nil {
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusBadRequest, "bad request")
}
return s.pollPush(rawConn, token, bodyReader)
default:
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusBadRequest, "bad request")
}
}
@@ -20,7 +20,11 @@ type byteLayout struct {
}
func (l *byteLayout) isHint(b byte) bool {
return (b & l.hintMask) == l.hintValue
if (b & l.hintMask) == l.hintValue {
return true
}
// ASCII layout maps the single non-printable marker (0x7F) to '\n' on the wire.
return l.name == "ascii" && b == '\n'
}
// resolveLayout picks the byte layout based on ASCII preference and optional custom pattern.
@@ -53,12 +57,25 @@ func newASCIILayout() *byteLayout {
padMarker: 0x3F,
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return 0x40 | ((val & 0x03) << 4) | (pos & 0x0F)
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 {
return 0x40 | (group & 0x3F)
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
}
+10 -5
View File
@@ -3,6 +3,7 @@ package sudoku
import (
"bufio"
"bytes"
crand "crypto/rand"
"encoding/binary"
"errors"
"fmt"
@@ -14,16 +15,20 @@ import (
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
func pickClientTable(cfg *ProtocolConfig) (*sudoku.Table, byte, error) {
func pickClientTable(cfg *ProtocolConfig) (*sudoku.Table, error) {
candidates := cfg.tableCandidates()
if len(candidates) == 0 {
return nil, 0, fmt.Errorf("no table configured")
return nil, fmt.Errorf("no table configured")
}
if len(candidates) == 1 {
return candidates[0], 0, nil
return candidates[0], nil
}
idx := int(randomByte()) % len(candidates)
return candidates[idx], byte(idx), nil
var b [1]byte
if _, err := crand.Read(b[:]); err != nil {
return nil, fmt.Errorf("random table pick failed: %w", err)
}
idx := int(b[0]) % len(candidates)
return candidates[idx], nil
}
type readOnlyConn struct {
+2 -2
View File
@@ -5,7 +5,7 @@
"mihomo_alpha": "alpha-c5b0f00",
"clash_rs": "v0.9.3",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.3-alpha+sha.03dc0e4"
"clash_rs_alpha": "0.9.3-alpha+sha.d65b14e"
},
"arch_template": {
"mihomo": {
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2026-01-12T22:21:20.908Z"
"updated_at": "2026-01-13T22:22:05.276Z"
}
+1 -1
View File
@@ -1,4 +1,4 @@
# GeoIP 简介 [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Loyalsoldier/geoip)
# GeoIP 简介 [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Loyalsoldier/geoip) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Loyalsoldier/geoip/total?logo=github) ![GitHub Downloads (all assets, latest release)](https://img.shields.io/github/downloads/Loyalsoldier/geoip/latest/total?logo=github) [![jsdelivr stats](https://data.jsdelivr.com/v1/package/gh/Loyalsoldier/geoip/badge?style=rounded)](https://www.jsdelivr.com/package/gh/Loyalsoldier/geoip)
本项目每周四自动生成多种格式 GeoIP 文件,同时提供命令行界面(CLI)工具供用户自行定制 GeoIP 文件,包括但不限于 V2Ray `dat` 格式文件 `geoip.dat`、MaxMind `mmdb` 格式文件 `Country.mmdb`、sing-box `SRS` 格式文件、mihomo `MRS` 格式文件、Clash ruleset 和 Surge ruleset。
@@ -0,0 +1,84 @@
From b98994cb9bc24f5c7575c86650f96c384576fdfa Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Mon, 17 Nov 2025 02:54:19 +0000
Subject: [PATCH] mtd: spinand: esmt: add support for F50L1G41LC
This adds support for ESMT F50L1G41LC, which appears to be an updated
version of the already supported F50L1G41LB.
Add esmt_8c SPI_NAND manufacturer to account for the newly used vendor
ID with support for the ESMT F50L1G41LC chip.
Link: https://github.com/openwrt/openwrt/pull/15214#issuecomment-3514824435
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Signed-off-by: Miquel Raynal <miquel.raynal@bootlin.com>
---
drivers/mtd/nand/spi/core.c | 1 +
drivers/mtd/nand/spi/esmt.c | 24 ++++++++++++++++++++++++
include/linux/mtd/spinand.h | 1 +
3 files changed, 26 insertions(+)
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -1114,6 +1114,7 @@ static const struct nand_ops spinand_ops
static const struct spinand_manufacturer *spinand_manufacturers[] = {
&alliancememory_spinand_manufacturer,
&ato_spinand_manufacturer,
+ &esmt_8c_spinand_manufacturer,
&esmt_c8_spinand_manufacturer,
&fmsh_spinand_manufacturer,
&foresee_spinand_manufacturer,
--- a/drivers/mtd/nand/spi/esmt.c
+++ b/drivers/mtd/nand/spi/esmt.c
@@ -11,6 +11,7 @@
/* ESMT uses GigaDevice 0xc8 JECDEC ID on some SPI NANDs */
#define SPINAND_MFR_ESMT_C8 0xc8
+#define SPINAND_MFR_ESMT_8C 0x8c
static SPINAND_OP_VARIANTS(read_cache_variants,
SPINAND_PAGE_READ_FROM_CACHE_X4_OP(0, 1, NULL, 0),
@@ -102,6 +103,19 @@ static const struct mtd_ooblayout_ops f5
.free = f50l1g41lb_ooblayout_free,
};
+
+static const struct spinand_info esmt_8c_spinand_table[] = {
+ SPINAND_INFO("F50L1G41LC",
+ SPINAND_ID(SPINAND_READID_METHOD_OPCODE_ADDR, 0x2C),
+ NAND_MEMORG(1, 2048, 64, 64, 1024, 20, 1, 1, 1),
+ NAND_ECCREQ(1, 512),
+ SPINAND_INFO_OP_VARIANTS(&read_cache_variants,
+ &write_cache_variants,
+ &update_cache_variants),
+ 0,
+ SPINAND_ECCINFO(&f50l1g41lb_ooblayout, NULL)),
+};
+
static const struct spinand_info esmt_c8_spinand_table[] = {
SPINAND_INFO("F50L1G41LB",
SPINAND_ID(SPINAND_READID_METHOD_OPCODE_ADDR, 0x01, 0x7f,
@@ -138,6 +152,14 @@ static const struct spinand_info esmt_c8
static const struct spinand_manufacturer_ops esmt_spinand_manuf_ops = {
};
+const struct spinand_manufacturer esmt_8c_spinand_manufacturer = {
+ .id = SPINAND_MFR_ESMT_8C,
+ .name = "ESMT",
+ .chips = esmt_8c_spinand_table,
+ .nchips = ARRAY_SIZE(esmt_8c_spinand_table),
+ .ops = &esmt_spinand_manuf_ops,
+};
+
const struct spinand_manufacturer esmt_c8_spinand_manufacturer = {
.id = SPINAND_MFR_ESMT_C8,
.name = "ESMT",
--- a/include/linux/mtd/spinand.h
+++ b/include/linux/mtd/spinand.h
@@ -262,6 +262,7 @@ struct spinand_manufacturer {
/* SPI NAND manufacturers */
extern const struct spinand_manufacturer alliancememory_spinand_manufacturer;
extern const struct spinand_manufacturer ato_spinand_manufacturer;
+extern const struct spinand_manufacturer esmt_8c_spinand_manufacturer;
extern const struct spinand_manufacturer esmt_c8_spinand_manufacturer;
extern const struct spinand_manufacturer fmsh_spinand_manufacturer;
extern const struct spinand_manufacturer foresee_spinand_manufacturer;
@@ -0,0 +1,84 @@
From b98994cb9bc24f5c7575c86650f96c384576fdfa Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Mon, 17 Nov 2025 02:54:19 +0000
Subject: [PATCH] mtd: spinand: esmt: add support for F50L1G41LC
This adds support for ESMT F50L1G41LC, which appears to be an updated
version of the already supported F50L1G41LB.
Add esmt_8c SPI_NAND manufacturer to account for the newly used vendor
ID with support for the ESMT F50L1G41LC chip.
Link: https://github.com/openwrt/openwrt/pull/15214#issuecomment-3514824435
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Signed-off-by: Miquel Raynal <miquel.raynal@bootlin.com>
---
drivers/mtd/nand/spi/core.c | 1 +
drivers/mtd/nand/spi/esmt.c | 24 ++++++++++++++++++++++++
include/linux/mtd/spinand.h | 1 +
3 files changed, 26 insertions(+)
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -942,6 +942,7 @@ static const struct nand_ops spinand_ops
static const struct spinand_manufacturer *spinand_manufacturers[] = {
&alliancememory_spinand_manufacturer,
&ato_spinand_manufacturer,
+ &esmt_8c_spinand_manufacturer,
&esmt_c8_spinand_manufacturer,
&fmsh_spinand_manufacturer,
&foresee_spinand_manufacturer,
--- a/drivers/mtd/nand/spi/esmt.c
+++ b/drivers/mtd/nand/spi/esmt.c
@@ -11,6 +11,7 @@
/* ESMT uses GigaDevice 0xc8 JECDEC ID on some SPI NANDs */
#define SPINAND_MFR_ESMT_C8 0xc8
+#define SPINAND_MFR_ESMT_8C 0x8c
static SPINAND_OP_VARIANTS(read_cache_variants,
SPINAND_PAGE_READ_FROM_CACHE_X4_OP(0, 1, NULL, 0),
@@ -102,6 +103,19 @@ static const struct mtd_ooblayout_ops f5
.free = f50l1g41lb_ooblayout_free,
};
+
+static const struct spinand_info esmt_8c_spinand_table[] = {
+ SPINAND_INFO("F50L1G41LC",
+ SPINAND_ID(SPINAND_READID_METHOD_OPCODE_ADDR, 0x2C),
+ NAND_MEMORG(1, 2048, 64, 64, 1024, 20, 1, 1, 1),
+ NAND_ECCREQ(1, 512),
+ SPINAND_INFO_OP_VARIANTS(&read_cache_variants,
+ &write_cache_variants,
+ &update_cache_variants),
+ 0,
+ SPINAND_ECCINFO(&f50l1g41lb_ooblayout, NULL)),
+};
+
static const struct spinand_info esmt_c8_spinand_table[] = {
SPINAND_INFO("F50L1G41LB",
SPINAND_ID(SPINAND_READID_METHOD_OPCODE_ADDR, 0x01, 0x7f,
@@ -138,6 +152,14 @@ static const struct spinand_info esmt_c8
static const struct spinand_manufacturer_ops esmt_spinand_manuf_ops = {
};
+const struct spinand_manufacturer esmt_8c_spinand_manufacturer = {
+ .id = SPINAND_MFR_ESMT_8C,
+ .name = "ESMT",
+ .chips = esmt_8c_spinand_table,
+ .nchips = ARRAY_SIZE(esmt_8c_spinand_table),
+ .ops = &esmt_spinand_manuf_ops,
+};
+
const struct spinand_manufacturer esmt_c8_spinand_manufacturer = {
.id = SPINAND_MFR_ESMT_C8,
.name = "ESMT",
--- a/include/linux/mtd/spinand.h
+++ b/include/linux/mtd/spinand.h
@@ -262,6 +262,7 @@ struct spinand_manufacturer {
/* SPI NAND manufacturers */
extern const struct spinand_manufacturer alliancememory_spinand_manufacturer;
extern const struct spinand_manufacturer ato_spinand_manufacturer;
+extern const struct spinand_manufacturer esmt_8c_spinand_manufacturer;
extern const struct spinand_manufacturer esmt_c8_spinand_manufacturer;
extern const struct spinand_manufacturer fmsh_spinand_manufacturer;
extern const struct spinand_manufacturer foresee_spinand_manufacturer;
@@ -49,9 +49,9 @@ Submitted-by: Daniel Danzberger <daniel@dd-wrt.com>
obj-$(CONFIG_MTD_SPI_NAND) += spinand.o
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -1115,6 +1115,7 @@ static const struct spinand_manufacturer
&alliancememory_spinand_manufacturer,
@@ -1116,6 +1116,7 @@ static const struct spinand_manufacturer
&ato_spinand_manufacturer,
&esmt_8c_spinand_manufacturer,
&esmt_c8_spinand_manufacturer,
+ &etron_spinand_manufacturer,
&foresee_spinand_manufacturer,
@@ -160,9 +160,9 @@ Submitted-by: Daniel Danzberger <daniel@dd-wrt.com>
+};
--- a/include/linux/mtd/spinand.h
+++ b/include/linux/mtd/spinand.h
@@ -263,6 +263,7 @@ struct spinand_manufacturer {
extern const struct spinand_manufacturer alliancememory_spinand_manufacturer;
@@ -264,6 +264,7 @@ extern const struct spinand_manufacturer
extern const struct spinand_manufacturer ato_spinand_manufacturer;
extern const struct spinand_manufacturer esmt_8c_spinand_manufacturer;
extern const struct spinand_manufacturer esmt_c8_spinand_manufacturer;
+extern const struct spinand_manufacturer etron_spinand_manufacturer;
extern const struct spinand_manufacturer foresee_spinand_manufacturer;
@@ -49,9 +49,9 @@ Submitted-by: Daniel Danzberger <daniel@dd-wrt.com>
obj-$(CONFIG_MTD_SPI_NAND) += spinand.o
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -943,6 +943,7 @@ static const struct spinand_manufacturer
&alliancememory_spinand_manufacturer,
@@ -944,6 +944,7 @@ static const struct spinand_manufacturer
&ato_spinand_manufacturer,
&esmt_8c_spinand_manufacturer,
&esmt_c8_spinand_manufacturer,
+ &etron_spinand_manufacturer,
&foresee_spinand_manufacturer,
@@ -160,9 +160,9 @@ Submitted-by: Daniel Danzberger <daniel@dd-wrt.com>
+};
--- a/include/linux/mtd/spinand.h
+++ b/include/linux/mtd/spinand.h
@@ -263,6 +263,7 @@ struct spinand_manufacturer {
extern const struct spinand_manufacturer alliancememory_spinand_manufacturer;
@@ -264,6 +264,7 @@ extern const struct spinand_manufacturer
extern const struct spinand_manufacturer ato_spinand_manufacturer;
extern const struct spinand_manufacturer esmt_8c_spinand_manufacturer;
extern const struct spinand_manufacturer esmt_c8_spinand_manufacturer;
+extern const struct spinand_manufacturer etron_spinand_manufacturer;
extern const struct spinand_manufacturer foresee_spinand_manufacturer;
+25 -11
View File
@@ -43,6 +43,7 @@ type SudokuOption struct {
HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
HTTPMaskTLS bool `proxy:"http-mask-tls,omitempty"` // only for http-mask-mode stream/poll/auto
HTTPMaskHost string `proxy:"http-mask-host,omitempty"` // optional Host/SNI override (domain or domain:port)
PathRoot string `proxy:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
HTTPMaskMultiplex string `proxy:"http-mask-multiplex,omitempty"` // "off" (default), "auto" (reuse h1/h2), "on" (single tunnel, multi-target)
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
@@ -183,6 +184,7 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
HTTPMaskMode: defaultConf.HTTPMaskMode,
HTTPMaskTLSEnabled: option.HTTPMaskTLS,
HTTPMaskHost: option.HTTPMaskHost,
HTTPMaskPathRoot: strings.TrimSpace(option.PathRoot),
HTTPMaskMultiplex: defaultConf.HTTPMaskMultiplex,
}
if option.HTTPMaskMode != "" {
@@ -257,7 +259,19 @@ func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfi
return nil, fmt.Errorf("config is required")
}
var c net.Conn
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask && httpTunnelModeEnabled(handshakeCfg.HTTPMaskMode) {
handshakeCfg.DisableHTTPMask = true
}
upgrade := func(raw net.Conn) (net.Conn, error) {
return sudoku.ClientHandshakeWithOptions(raw, &handshakeCfg, sudoku.ClientHandshakeOptions{})
}
var (
c net.Conn
handshakeDone bool
)
if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
switch muxMode {
@@ -266,9 +280,12 @@ func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfi
if errX != nil {
return nil, errX
}
c, err = client.Dial(ctx)
c, err = client.Dial(ctx, upgrade)
default:
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext)
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext, upgrade)
}
if err == nil && c != nil {
handshakeDone = true
}
}
if c == nil && err == nil {
@@ -285,14 +302,11 @@ func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfi
defer done(&err)
}
handshakeCfg := *cfg
if !handshakeCfg.DisableHTTPMask && httpTunnelModeEnabled(handshakeCfg.HTTPMaskMode) {
handshakeCfg.DisableHTTPMask = true
}
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{})
if err != nil {
return nil, err
if !handshakeDone {
c, err = sudoku.ClientHandshakeWithOptions(c, &handshakeCfg, sudoku.ClientHandshakeOptions{})
if err != nil {
return nil, err
}
}
return c, nil
+1
View File
@@ -356,6 +356,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{
ServiceName: option.GrpcOpts.GrpcServiceName,
UserAgent: option.GrpcOpts.GrpcUserAgent,
Host: option.SNI,
ClientFingerprint: option.ClientFingerprint,
}
+1
View File
@@ -462,6 +462,7 @@ func NewVless(option VlessOption) (*Vless, error) {
gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName,
UserAgent: v.option.GrpcOpts.GrpcUserAgent,
Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
}
+2
View File
@@ -86,6 +86,7 @@ type HTTP2Options struct {
type GrpcOptions struct {
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
GrpcUserAgent string `proxy:"grpc-user-agent,omitempty"`
}
type WSOptions struct {
@@ -467,6 +468,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName,
UserAgent: v.option.GrpcOpts.GrpcUserAgent,
Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
}
+7 -1
View File
@@ -669,6 +669,7 @@ proxies: # socks5
# skip-cert-verify: true
grpc-opts:
grpc-service-name: "example"
# grpc-user-agent: "grpc-go/1.36.0"
# ip-version: ipv4
# vless
@@ -757,6 +758,8 @@ proxies: # socks5
servername: testingcf.jsdelivr.net
grpc-opts:
grpc-service-name: "grpc"
# grpc-user-agent: "grpc-go/1.36.0"
reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
short-id: 10f897e26c4b9478
@@ -825,6 +828,7 @@ proxies: # socks5
udp: true
grpc-opts:
grpc-service-name: "example"
# grpc-user-agent: "grpc-go/1.36.0"
- name: trojan-ws
server: server
@@ -1068,6 +1072,7 @@ proxies: # socks5
# http-mask-mode: legacy # 可选:legacy(默认)、stream、poll、autostream/poll/auto 支持走 CDN/反代
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 httpsfalse 强制 http(不会根据端口自动推断)
# http-mask-host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 http-mask-mode 为 stream/poll/auto 时生效
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
# http-mask-multiplex: off # 可选:off(默认)、auto(复用 h1.1 keep-alive / h2 连接,减少每次建链 RTT)、on(单条隧道内多路复用多个目标连接;仅在 http-mask-mode=stream/poll/auto 生效)
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与服务端端保持相同(如果此处为false,则要求aead不可为none)
@@ -1371,7 +1376,7 @@ listeners:
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: reidr-in-1
- name: redir-in-1
type: redir
port: 10811 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503
listen: 0.0.0.0
@@ -1617,6 +1622,7 @@ listeners:
enable-pure-downlink: false # 是否启用混淆下行,false的情况下能在保证数据安全的前提下极大提升下行速度,与客户端保持相同(如果此处为false,则要求aead不可为none)
disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false
# http-mask-mode: legacy # 可选:legacy(默认)、stream、poll、autostream/poll/auto 支持走 CDN/反代
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
+1 -1
View File
@@ -41,7 +41,7 @@ require (
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443
github.com/metacubex/tls v0.1.0
github.com/metacubex/utls v1.8.3
github.com/metacubex/utls v1.8.4
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f
github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20
github.com/mroth/weightedrand/v2 v2.1.0
+2 -2
View File
@@ -144,8 +144,8 @@ github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443 h1:H6TnfM12tOoTiz
github.com/metacubex/tfo-go v0.0.0-20251130171125-413e892ac443/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/tls v0.1.0 h1:1kjR/1q2uU1cZIwiHYEnWzS4L+0Cu1/X3yfIQ76BzNY=
github.com/metacubex/tls v0.1.0/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM=
github.com/metacubex/utls v1.8.3 h1:0m/yCxm3SK6kWve2lKiFb1pue1wHitJ8sQQD4Ikqde4=
github.com/metacubex/utls v1.8.3/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk=
github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4=
github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 h1:lhlqpYHopuTLx9xQt22kSA9HtnyTDmk5XjjQVCGHe2E=
+3
View File
@@ -424,6 +424,9 @@ func updateGeneral(general *config.General, logging bool) {
mihomoHttp.SetUA(general.GlobalUA)
resource.SetETag(general.ETagSupport)
if general.GlobalClientFingerprint != "" {
log.Warnln("The `global-client-fingerprint` configuration is deprecated, please set `client-fingerprint` directly on the proxy instead")
}
tlsC.SetGlobalFingerprint(general.GlobalClientFingerprint)
}
+1
View File
@@ -22,6 +22,7 @@ type SudokuServer struct {
CustomTables []string `json:"custom-tables,omitempty"`
DisableHTTPMask bool `json:"disable-http-mask,omitempty"`
HTTPMaskMode string `json:"http-mask-mode,omitempty"`
PathRoot string `json:"path-root,omitempty"`
// mihomo private extension (not the part of standard Sudoku protocol)
MuxOption sing.MuxOption `json:"mux-option,omitempty"`
+2
View File
@@ -24,6 +24,7 @@ type SudokuOption struct {
CustomTables []string `inbound:"custom-tables,omitempty"`
DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"`
HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
PathRoot string `inbound:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
// mihomo private extension (not the part of standard Sudoku protocol)
MuxOption MuxOption `inbound:"mux-option,omitempty"`
@@ -63,6 +64,7 @@ func NewSudoku(options *SudokuOption) (*Sudoku, error) {
CustomTables: options.CustomTables,
DisableHTTPMask: options.DisableHTTPMask,
HTTPMaskMode: options.HTTPMaskMode,
PathRoot: strings.TrimSpace(options.PathRoot),
}
serverConf.MuxOption = options.MuxOption.Build()
+1
View File
@@ -229,6 +229,7 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
HandshakeTimeoutSeconds: handshakeTimeout,
DisableHTTPMask: config.DisableHTTPMask,
HTTPMaskMode: config.HTTPMaskMode,
HTTPMaskPathRoot: strings.TrimSpace(config.PathRoot),
}
if len(tables) == 1 {
protoConf.Table = tables[0]
+8 -1
View File
@@ -60,6 +60,7 @@ type Conn struct {
type Config struct {
ServiceName string
UserAgent string
Host string
ClientFingerprint string
}
@@ -347,6 +348,12 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
path := ServiceNameToPath(serviceName)
reader, writer := io.Pipe()
header := defaultHeader.Clone()
if cfg.UserAgent != "" {
header["user-agent"] = []string{cfg.UserAgent}
}
request := &http.Request{
Method: http.MethodPost,
Body: reader,
@@ -360,7 +367,7 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
Proto: "HTTP/2",
ProtoMajor: 2,
ProtoMinor: 0,
Header: defaultHeader,
Header: header,
}
request = request.WithContext(transport.ctx)
+21
View File
@@ -58,6 +58,10 @@ type ProtocolConfig struct {
// HTTPMaskHost optionally overrides the HTTP Host header / SNI host for HTTP tunnel modes (client-side).
HTTPMaskHost string
// HTTPMaskPathRoot optionally prefixes all HTTP mask paths with a first-level segment.
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
HTTPMaskPathRoot string
// HTTPMaskMultiplex controls multiplex behavior when HTTPMask tunnel modes are enabled:
// - "off": disable reuse; each Dial establishes its own HTTPMask tunnel
// - "auto": reuse underlying HTTP connections across multiple tunnel dials (HTTP/1.1 keep-alive / HTTP/2)
@@ -109,6 +113,23 @@ func (c *ProtocolConfig) Validate() error {
return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto", c.HTTPMaskMode)
}
if v := strings.TrimSpace(c.HTTPMaskPathRoot); v != "" {
if strings.Contains(v, "/") {
return fmt.Errorf("invalid http-mask-path-root: must be a single path segment")
}
for i := 0; i < len(v); i++ {
ch := v[i]
switch {
case ch >= 'a' && ch <= 'z':
case ch >= 'A' && ch <= 'Z':
case ch >= '0' && ch <= '9':
case ch == '_' || ch == '-':
default:
return fmt.Errorf("invalid http-mask-path-root: contains invalid character %q", ch)
}
}
}
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMultiplex)) {
case "", "off", "auto", "on":
default:
+13 -24
View File
@@ -2,7 +2,6 @@ package sudoku
import (
"bufio"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
@@ -153,14 +152,17 @@ func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table,
func buildHandshakePayload(key string) [16]byte {
var payload [16]byte
binary.BigEndian.PutUint64(payload[:8], uint64(time.Now().Unix()))
// Hash the decoded HEX bytes of the key, not the HEX string itself.
// This ensures the user hash is computed on the actual key bytes.
keyBytes, err := hex.DecodeString(key)
if err != nil {
// Fallback: if key is not valid HEX (e.g., a UUID or plain string), hash the string bytes
keyBytes = []byte(key)
// Align with upstream: only decode hex bytes when this key is an ED25519 key material.
// For plain UUID/strings (even if they look like hex), hash the string bytes as-is.
src := []byte(key)
if _, err := crypto.RecoverPublicKey(key); err == nil {
if keyBytes, decErr := hex.DecodeString(key); decErr == nil && len(keyBytes) > 0 {
src = keyBytes
}
}
hash := sha256.Sum256(keyBytes)
hash := sha256.Sum256(src)
copy(payload[8:], hash[:8])
return payload
}
@@ -211,12 +213,12 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *ProtocolConfig, opt Clien
}
if !cfg.DisableHTTPMask {
if err := WriteHTTPMaskHeader(rawConn, cfg.ServerAddress, opt.HTTPMaskStrategy); err != nil {
if err := WriteHTTPMaskHeader(rawConn, cfg.ServerAddress, cfg.HTTPMaskPathRoot, opt.HTTPMaskStrategy); err != nil {
return nil, fmt.Errorf("write http mask failed: %w", err)
}
}
table, tableID, err := pickClientTable(cfg)
table, err := pickClientTable(cfg)
if err != nil {
return nil, err
}
@@ -228,9 +230,6 @@ func ClientHandshakeWithOptions(rawConn net.Conn, cfg *ProtocolConfig, opt Clien
}
handshake := buildHandshakePayload(cfg.Key)
if len(cfg.tableCandidates()) > 1 {
handshake[8] = tableID
}
if _, err := cConn.Write(handshake[:]); err != nil {
cConn.Close()
return nil, fmt.Errorf("send handshake failed: %w", err)
@@ -376,19 +375,9 @@ func normalizeHTTPMaskStrategy(strategy string) string {
}
}
// randomByte returns a cryptographically random byte (with a math/rand fallback).
func randomByte() byte {
var b [1]byte
if _, err := rand.Read(b[:]); err == nil {
return b[0]
}
return byte(time.Now().UnixNano())
}
func userHashFromHandshake(handshakeBuf []byte) string {
if len(handshakeBuf) < 16 {
return ""
}
// handshake[8] may be a table ID when table rotation is enabled; use [9:16] as stable user hash bytes.
return hex.EncodeToString(handshakeBuf[9:16])
return hex.EncodeToString(handshakeBuf[8:16])
}
+43 -8
View File
@@ -7,6 +7,7 @@ import (
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
@@ -92,24 +93,24 @@ func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
// WriteHTTPMaskHeader writes an HTTP/1.x request header as a mask, according to strategy.
// Supported strategies: ""/"random", "post", "websocket".
func WriteHTTPMaskHeader(w io.Writer, host string, strategy string) error {
func WriteHTTPMaskHeader(w io.Writer, host string, pathRoot string, strategy string) error {
switch normalizeHTTPMaskStrategy(strategy) {
case "random":
return httpmask.WriteRandomRequestHeader(w, host)
return httpmask.WriteRandomRequestHeaderWithPathRoot(w, host, pathRoot)
case "post":
return writeHTTPMaskPOST(w, host)
return writeHTTPMaskPOST(w, host, pathRoot)
case "websocket":
return writeHTTPMaskWebSocket(w, host)
return writeHTTPMaskWebSocket(w, host, pathRoot)
default:
return fmt.Errorf("unsupported http-mask-strategy: %s", strategy)
}
}
func writeHTTPMaskPOST(w io.Writer, host string) error {
func writeHTTPMaskPOST(w io.Writer, host string, pathRoot string) error {
r := httpMaskRngPool.Get().(*rand.Rand)
defer httpMaskRngPool.Put(r)
path := httpMaskPaths[r.Intn(len(httpMaskPaths))]
path := joinPathRoot(pathRoot, httpMaskPaths[r.Intn(len(httpMaskPaths))])
ctype := httpMaskContentTypes[r.Intn(len(httpMaskContentTypes))]
bufPtr := httpMaskBufPool.Get().(*[]byte)
@@ -140,11 +141,11 @@ func writeHTTPMaskPOST(w io.Writer, host string) error {
return err
}
func writeHTTPMaskWebSocket(w io.Writer, host string) error {
func writeHTTPMaskWebSocket(w io.Writer, host string, pathRoot string) error {
r := httpMaskRngPool.Get().(*rand.Rand)
defer httpMaskRngPool.Put(r)
path := httpMaskPaths[r.Intn(len(httpMaskPaths))]
path := joinPathRoot(pathRoot, httpMaskPaths[r.Intn(len(httpMaskPaths))])
bufPtr := httpMaskBufPool.Get().(*[]byte)
buf := *bufPtr
@@ -177,3 +178,37 @@ func writeHTTPMaskWebSocket(w io.Writer, host string) error {
_, err := w.Write(buf)
return err
}
func normalizePathRoot(root string) string {
root = strings.TrimSpace(root)
root = strings.Trim(root, "/")
if root == "" {
return ""
}
for i := 0; i < len(root); i++ {
c := root[i]
switch {
case c >= 'a' && c <= 'z':
case c >= 'A' && c <= 'Z':
case c >= '0' && c <= '9':
case c == '_' || c == '-':
default:
return ""
}
}
return "/" + root
}
func joinPathRoot(root, path string) string {
root = normalizePathRoot(root)
if root == "" {
return path
}
if path == "" {
return root
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return root + path
}
+24 -8
View File
@@ -23,7 +23,11 @@ func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
if !cfg.DisableHTTPMask {
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
case "stream", "poll", "auto":
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{Mode: cfg.HTTPMaskMode})
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{
Mode: cfg.HTTPMaskMode,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ClientAEADSeed(cfg.Key),
})
}
}
return &HTTPMaskTunnelServer{cfg: cfg, ts: ts}
@@ -67,7 +71,7 @@ func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Con
type TunnelDialer func(ctx context.Context, network, addr string) (net.Conn, error)
// DialHTTPMaskTunnel dials a CDN-capable HTTP tunnel (stream/poll/auto) and returns a stream carrying raw Sudoku bytes.
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (net.Conn, error) {
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
if cfg == nil {
return nil, fmt.Errorf("config is required")
}
@@ -83,14 +87,19 @@ func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *Protocol
Mode: cfg.HTTPMaskMode,
TLSEnabled: cfg.HTTPMaskTLSEnabled,
HostOverride: cfg.HTTPMaskHost,
PathRoot: cfg.HTTPMaskPathRoot,
AuthKey: ClientAEADSeed(cfg.Key),
Upgrade: upgrade,
Multiplex: cfg.HTTPMaskMultiplex,
DialContext: dial,
})
}
type HTTPMaskTunnelClient struct {
mode string
client *httpmask.TunnelClient
mode string
pathRoot string
authKey string
client *httpmask.TunnelClient
}
func NewHTTPMaskTunnelClient(serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (*HTTPMaskTunnelClient, error) {
@@ -121,16 +130,23 @@ func NewHTTPMaskTunnelClient(serverAddress string, cfg *ProtocolConfig, dial Tun
}
return &HTTPMaskTunnelClient{
mode: cfg.HTTPMaskMode,
client: c,
mode: cfg.HTTPMaskMode,
pathRoot: cfg.HTTPMaskPathRoot,
authKey: ClientAEADSeed(cfg.Key),
client: c,
}, nil
}
func (c *HTTPMaskTunnelClient) Dial(ctx context.Context) (net.Conn, error) {
func (c *HTTPMaskTunnelClient) Dial(ctx context.Context, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
if c == nil || c.client == nil {
return nil, fmt.Errorf("nil httpmask tunnel client")
}
return c.client.DialTunnel(ctx, c.mode)
return c.client.DialTunnel(ctx, httpmask.TunnelDialOptions{
Mode: c.mode,
PathRoot: c.pathRoot,
AuthKey: c.authKey,
Upgrade: upgrade,
})
}
func (c *HTTPMaskTunnelClient) CloseIdleConnections() {
@@ -154,7 +154,7 @@ func TestHTTPMaskTunnel_Stream_TCPRoundTrip(t *testing.T) {
clientCfg.ServerAddress = addr
clientCfg.HTTPMaskHost = "example.com"
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
@@ -225,7 +225,7 @@ func TestHTTPMaskTunnel_Poll_UoTRoundTrip(t *testing.T) {
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
@@ -287,7 +287,7 @@ func TestHTTPMaskTunnel_Auto_TCPRoundTrip(t *testing.T) {
clientCfg := *serverCfg
clientCfg.ServerAddress = addr
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
t.Fatalf("dial tunnel: %v", err)
}
@@ -331,13 +331,13 @@ func TestHTTPMaskTunnel_Validation(t *testing.T) {
cfg.DisableHTTPMask = true
cfg.HTTPMaskMode = "stream"
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext, nil); err == nil {
t.Fatalf("expected error for disabled http mask")
}
cfg.DisableHTTPMask = false
cfg.HTTPMaskMode = "legacy"
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext); err == nil {
if _, err := DialHTTPMaskTunnel(context.Background(), cfg.ServerAddress, cfg, (&net.Dialer{}).DialContext, nil); err == nil {
t.Fatalf("expected error for legacy mode")
}
}
@@ -385,7 +385,7 @@ func TestHTTPMaskTunnel_Soak_Concurrent(t *testing.T) {
clientCfg.ServerAddress = addr
clientCfg.HTTPMaskHost = strings.TrimSpace(clientCfg.HTTPMaskHost)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext)
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
if err != nil {
runErr <- fmt.Errorf("dial: %w", err)
return
+1 -2
View File
@@ -99,7 +99,7 @@ func TestUserHash_StableAcrossTableRotation(t *testing.T) {
if h == "" {
t.Fatalf("empty user hash")
}
if len(h) != 14 {
if len(h) != 16 {
t.Fatalf("unexpected user hash length: %d", len(h))
}
unique[h] = struct{}{}
@@ -258,4 +258,3 @@ func TestMultiplex_Boundary_InvalidVersion(t *testing.T) {
t.Fatalf("expected error")
}
}
@@ -0,0 +1,137 @@
package httpmask
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"strings"
"time"
)
const (
tunnelAuthHeaderKey = "Authorization"
tunnelAuthHeaderPrefix = "Bearer "
)
type tunnelAuth struct {
key [32]byte // derived HMAC key
skew time.Duration
}
func newTunnelAuth(key string, skew time.Duration) *tunnelAuth {
key = strings.TrimSpace(key)
if key == "" {
return nil
}
if skew <= 0 {
skew = 60 * time.Second
}
// Domain separation: keep this HMAC key independent from other uses of cfg.Key.
h := sha256.New()
_, _ = h.Write([]byte("sudoku-httpmask-auth-v1:"))
_, _ = h.Write([]byte(key))
var sum [32]byte
h.Sum(sum[:0])
return &tunnelAuth{key: sum, skew: skew}
}
func (a *tunnelAuth) token(mode TunnelMode, method, path string, now time.Time) string {
if a == nil {
return ""
}
ts := now.Unix()
sig := a.sign(mode, method, path, ts)
var buf [8 + 16]byte
binary.BigEndian.PutUint64(buf[:8], uint64(ts))
copy(buf[8:], sig[:])
return base64.RawURLEncoding.EncodeToString(buf[:])
}
func (a *tunnelAuth) verify(headers map[string]string, mode TunnelMode, method, path string, now time.Time) bool {
if a == nil {
return true
}
if headers == nil {
return false
}
val := strings.TrimSpace(headers["authorization"])
if val == "" {
return false
}
// Accept both "Bearer <token>" and raw token forms (for forward proxies / CDNs that may normalize headers).
if len(val) > len(tunnelAuthHeaderPrefix) && strings.EqualFold(val[:len(tunnelAuthHeaderPrefix)], tunnelAuthHeaderPrefix) {
val = strings.TrimSpace(val[len(tunnelAuthHeaderPrefix):])
}
if val == "" {
return false
}
raw, err := base64.RawURLEncoding.DecodeString(val)
if err != nil || len(raw) != 8+16 {
return false
}
ts := int64(binary.BigEndian.Uint64(raw[:8]))
nowTS := now.Unix()
delta := nowTS - ts
if delta < 0 {
delta = -delta
}
if delta > int64(a.skew.Seconds()) {
return false
}
want := a.sign(mode, method, path, ts)
return subtle.ConstantTimeCompare(raw[8:], want[:]) == 1
}
func (a *tunnelAuth) sign(mode TunnelMode, method, path string, ts int64) [16]byte {
method = strings.ToUpper(strings.TrimSpace(method))
if method == "" {
method = "GET"
}
path = strings.TrimSpace(path)
var tsBuf [8]byte
binary.BigEndian.PutUint64(tsBuf[:], uint64(ts))
mac := hmac.New(sha256.New, a.key[:])
_, _ = mac.Write([]byte(mode))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write([]byte(method))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write([]byte(path))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write(tsBuf[:])
var full [32]byte
mac.Sum(full[:0])
var out [16]byte
copy(out[:], full[:16])
return out
}
type headerSetter interface {
Set(key, value string)
}
func applyTunnelAuthHeader(h headerSetter, auth *tunnelAuth, mode TunnelMode, method, path string) {
if auth == nil || h == nil {
return
}
token := auth.token(mode, method, path, time.Now())
if token == "" {
return
}
h.Set(tunnelAuthHeaderKey, tunnelAuthHeaderPrefix+token)
}
@@ -129,11 +129,17 @@ func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
// WriteRandomRequestHeader writes a plausible HTTP/1.1 request header as a mask.
func WriteRandomRequestHeader(w io.Writer, host string) error {
return WriteRandomRequestHeaderWithPathRoot(w, host, "")
}
// WriteRandomRequestHeaderWithPathRoot is like WriteRandomRequestHeader but prefixes all paths with pathRoot.
// pathRoot must be a single segment (e.g. "aabbcc"); invalid inputs are treated as empty (disabled).
func WriteRandomRequestHeaderWithPathRoot(w io.Writer, host string, pathRoot string) error {
// Get RNG from pool
r := rngPool.Get().(*rand.Rand)
defer rngPool.Put(r)
path := paths[r.Intn(len(paths))]
path := joinPathRoot(pathRoot, paths[r.Intn(len(paths))])
ctype := contentTypes[r.Intn(len(contentTypes))]
// Use buffer pool
@@ -0,0 +1,52 @@
package httpmask
import "strings"
// normalizePathRoot normalizes the configured path root into "/<segment>" form.
//
// It is intentionally strict: only a single path segment is allowed, consisting of
// [A-Za-z0-9_-]. Invalid inputs are treated as empty (disabled).
func normalizePathRoot(root string) string {
root = strings.TrimSpace(root)
root = strings.Trim(root, "/")
if root == "" {
return ""
}
for i := 0; i < len(root); i++ {
c := root[i]
switch {
case c >= 'a' && c <= 'z':
case c >= 'A' && c <= 'Z':
case c >= '0' && c <= '9':
case c == '_' || c == '-':
default:
return ""
}
}
return "/" + root
}
func joinPathRoot(root, path string) string {
root = normalizePathRoot(root)
if root == "" {
return path
}
if path == "" {
return root
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return root + path
}
func stripPathRoot(root, fullPath string) (string, bool) {
root = normalizePathRoot(root)
if root == "" {
return fullPath, true
}
if !strings.HasPrefix(fullPath, root+"/") {
return "", false
}
return strings.TrimPrefix(fullPath, root), true
}
+295 -119
View File
@@ -62,6 +62,15 @@ type TunnelDialOptions struct {
Mode string
TLSEnabled bool // when true, use HTTPS; otherwise, use HTTP (no port-based inference)
HostOverride string // optional Host header / SNI host (without scheme); accepts "example.com" or "example.com:443"
// PathRoot is an optional first-level path prefix for all HTTP tunnel endpoints.
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
PathRoot string
// AuthKey enables short-term HMAC auth for HTTP tunnel requests (anti-probing).
// When set (non-empty), each HTTP request carries an Authorization bearer token derived from AuthKey.
AuthKey string
// Upgrade optionally wraps the raw tunnel conn and/or writes a small prelude before DialTunnel returns.
// It is called with the raw tunnel conn; if it returns a non-nil conn, that conn is returned by DialTunnel.
Upgrade func(raw net.Conn) (net.Conn, error)
// Multiplex controls whether the caller should reuse underlying HTTP connections (HTTP/1.1 keep-alive / HTTP/2).
// To reuse across multiple dials, create a TunnelClient per proxy and reuse it.
// Values: "off" disables reuse; "auto"/"on" enables it.
@@ -109,34 +118,34 @@ func (c *TunnelClient) CloseIdleConnections() {
c.transport.CloseIdleConnections()
}
func (c *TunnelClient) DialTunnel(ctx context.Context, mode string) (net.Conn, error) {
func (c *TunnelClient) DialTunnel(ctx context.Context, opts TunnelDialOptions) (net.Conn, error) {
if c == nil || c.client == nil {
return nil, fmt.Errorf("nil tunnel client")
}
tm := normalizeTunnelMode(mode)
tm := normalizeTunnelMode(opts.Mode)
if tm == TunnelModeLegacy {
return nil, fmt.Errorf("legacy mode does not use http tunnel")
}
switch tm {
case TunnelModeStream:
return dialStreamWithClient(ctx, c.client, c.target)
return dialStreamWithClient(ctx, c.client, c.target, opts)
case TunnelModePoll:
return dialPollWithClient(ctx, c.client, c.target)
return dialPollWithClient(ctx, c.client, c.target, opts)
case TunnelModeAuto:
streamCtx, cancelX := context.WithTimeout(ctx, 3*time.Second)
c1, errX := dialStreamWithClient(streamCtx, c.client, c.target)
c1, errX := dialStreamWithClient(streamCtx, c.client, c.target, opts)
cancelX()
if errX == nil {
return c1, nil
}
c2, errP := dialPollWithClient(ctx, c.client, c.target)
c2, errP := dialPollWithClient(ctx, c.client, c.target, opts)
if errP == nil {
return c2, nil
}
return nil, fmt.Errorf("auto tunnel failed: stream: %v; poll: %w", errX, errP)
default:
return dialStreamWithClient(ctx, c.client, c.target)
return dialStreamWithClient(ctx, c.client, c.target, opts)
}
}
@@ -248,8 +257,13 @@ func (c *httpStreamConn) Close() error {
if c.cancel != nil {
c.cancel()
}
_ = c.writer.CloseWithError(io.ErrClosedPipe)
return c.reader.Close()
if c.writer != nil {
_ = c.writer.CloseWithError(io.ErrClosedPipe)
}
if c.reader != nil {
return c.reader.Close()
}
return nil
}
func (c *httpStreamConn) LocalAddr() net.Addr { return c.localAddr }
@@ -320,20 +334,23 @@ type sessionDialInfo struct {
pullURL string
closeURL string
headerHost string
auth *tunnelAuth
}
func dialSessionWithClient(ctx context.Context, client *http.Client, target httpClientTarget, mode TunnelMode) (*sessionDialInfo, error) {
func dialSessionWithClient(ctx context.Context, client *http.Client, target httpClientTarget, mode TunnelMode, opts TunnelDialOptions) (*sessionDialInfo, error) {
if client == nil {
return nil, fmt.Errorf("nil http client")
}
authorizeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: "/session"}).String()
auth := newTunnelAuth(opts.AuthKey, 0)
authorizeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/session")}).String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, authorizeURL, nil)
if err != nil {
return nil, err
}
req.Host = target.headerHost
applyTunnelHeaders(req.Header, target.headerHost, mode)
applyTunnelAuthHeader(req.Header, auth, mode, http.MethodGet, "/session")
resp, err := client.Do(req)
if err != nil {
@@ -356,9 +373,9 @@ func dialSessionWithClient(ctx context.Context, client *http.Client, target http
return nil, fmt.Errorf("%s authorize empty token", mode)
}
pushURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: "/api/v1/upload", RawQuery: "token=" + url.QueryEscape(token)}).String()
pullURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: "/stream", RawQuery: "token=" + url.QueryEscape(token)}).String()
closeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: "/api/v1/upload", RawQuery: "token=" + url.QueryEscape(token) + "&close=1"}).String()
pushURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/api/v1/upload"), RawQuery: "token=" + url.QueryEscape(token)}).String()
pullURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/stream"), RawQuery: "token=" + url.QueryEscape(token)}).String()
closeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/api/v1/upload"), RawQuery: "token=" + url.QueryEscape(token) + "&close=1"}).String()
return &sessionDialInfo{
client: client,
@@ -366,6 +383,7 @@ func dialSessionWithClient(ctx context.Context, client *http.Client, target http
pullURL: pullURL,
closeURL: closeURL,
headerHost: target.headerHost,
auth: auth,
}, nil
}
@@ -374,10 +392,10 @@ func dialSession(ctx context.Context, serverAddress string, opts TunnelDialOptio
if err != nil {
return nil, err
}
return dialSessionWithClient(ctx, client, target, mode)
return dialSessionWithClient(ctx, client, target, mode, opts)
}
func bestEffortCloseSession(client *http.Client, closeURL, headerHost string, mode TunnelMode) {
func bestEffortCloseSession(client *http.Client, closeURL, headerHost string, mode TunnelMode, auth *tunnelAuth) {
if client == nil || closeURL == "" || headerHost == "" {
return
}
@@ -391,6 +409,7 @@ func bestEffortCloseSession(client *http.Client, closeURL, headerHost string, mo
}
req.Host = headerHost
applyTunnelHeaders(req.Header, headerHost, mode)
applyTunnelAuthHeader(req.Header, auth, mode, http.MethodPost, "/api/v1/upload")
resp, err := client.Do(req)
if err != nil || resp == nil {
@@ -400,13 +419,13 @@ func bestEffortCloseSession(client *http.Client, closeURL, headerHost string, mo
_ = resp.Body.Close()
}
func dialStreamWithClient(ctx context.Context, client *http.Client, target httpClientTarget) (net.Conn, error) {
// Prefer split session (Cloudflare-friendly). Fall back to stream-one for older servers / environments.
c, errSplit := dialStreamSplitWithClient(ctx, client, target)
func dialStreamWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) {
// Prefer split-session (Cloudflare-friendly). Fall back to stream-one for older servers / environments.
c, errSplit := dialStreamSplitWithClient(ctx, client, target, opts)
if errSplit == nil {
return c, nil
}
c2, errOne := dialStreamOneWithClient(ctx, client, target)
c2, errOne := dialStreamOneWithClient(ctx, client, target, opts)
if errOne == nil {
return c2, nil
}
@@ -414,7 +433,7 @@ func dialStreamWithClient(ctx context.Context, client *http.Client, target httpC
}
func dialStream(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
// Prefer split session (Cloudflare-friendly). Fall back to stream-one for older servers / environments.
// Prefer split-session (Cloudflare-friendly). Fall back to stream-one for older servers / environments.
c, errSplit := dialStreamSplit(ctx, serverAddress, opts)
if errSplit == nil {
return c, nil
@@ -426,13 +445,15 @@ func dialStream(ctx context.Context, serverAddress string, opts TunnelDialOption
return nil, fmt.Errorf("dial stream failed: split: %v; stream-one: %w", errSplit, errOne)
}
func dialStreamOneWithClient(ctx context.Context, client *http.Client, target httpClientTarget) (net.Conn, error) {
func dialStreamOneWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) {
if client == nil {
return nil, fmt.Errorf("nil http client")
}
auth := newTunnelAuth(opts.AuthKey, 0)
r := rngPool.Get().(*mrand.Rand)
path := paths[r.Intn(len(paths))]
basePath := paths[r.Intn(len(paths))]
path := joinPathRoot(opts.PathRoot, basePath)
ctype := contentTypes[r.Intn(len(contentTypes))]
rngPool.Put(r)
@@ -454,6 +475,7 @@ func dialStreamOneWithClient(ctx context.Context, client *http.Client, target ht
req.Host = target.headerHost
applyTunnelHeaders(req.Header, target.headerHost, TunnelModeStream)
applyTunnelAuthHeader(req.Header, auth, TunnelModeStream, http.MethodPost, basePath)
req.Header.Set("Content-Type", ctype)
type doResult struct {
@@ -466,33 +488,84 @@ func dialStreamOneWithClient(ctx context.Context, client *http.Client, target ht
doCh <- doResult{resp: resp, err: doErr}
}()
select {
case <-ctx.Done():
connCancel()
_ = reqBodyW.Close()
return nil, ctx.Err()
case r := <-doCh:
if r.err != nil {
connCancel()
_ = reqBodyW.Close()
return nil, r.err
}
if r.resp.StatusCode != http.StatusOK {
defer r.resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(r.resp.Body, 4*1024))
connCancel()
_ = reqBodyW.Close()
return nil, fmt.Errorf("stream bad status: %s (%s)", r.resp.Status, strings.TrimSpace(string(body)))
}
return &httpStreamConn{
reader: r.resp.Body,
writer: reqBodyW,
cancel: connCancel,
localAddr: &net.TCPAddr{},
remoteAddr: &net.TCPAddr{},
}, nil
streamConn := &httpStreamConn{
writer: reqBodyW,
cancel: connCancel,
localAddr: &net.TCPAddr{},
remoteAddr: &net.TCPAddr{},
}
type upgradeResult struct {
conn net.Conn
err error
}
upgradeCh := make(chan upgradeResult, 1)
if opts.Upgrade == nil {
upgradeCh <- upgradeResult{conn: streamConn, err: nil}
} else {
go func() {
upgradeConn, err := opts.Upgrade(streamConn)
if err != nil {
upgradeCh <- upgradeResult{conn: nil, err: err}
return
}
if upgradeConn == nil {
upgradeConn = streamConn
}
upgradeCh <- upgradeResult{conn: upgradeConn, err: nil}
}()
}
var (
outConn net.Conn
upgradeDone bool
responseReady bool
)
for !(upgradeDone && responseReady) {
select {
case <-ctx.Done():
_ = streamConn.Close()
if outConn != nil && outConn != streamConn {
_ = outConn.Close()
}
return nil, ctx.Err()
case u := <-upgradeCh:
if u.err != nil {
_ = streamConn.Close()
return nil, u.err
}
outConn = u.conn
if outConn == nil {
outConn = streamConn
}
upgradeDone = true
case r := <-doCh:
if r.err != nil {
_ = streamConn.Close()
if outConn != nil && outConn != streamConn {
_ = outConn.Close()
}
return nil, r.err
}
if r.resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(r.resp.Body, 4*1024))
_ = r.resp.Body.Close()
_ = streamConn.Close()
if outConn != nil && outConn != streamConn {
_ = outConn.Close()
}
return nil, fmt.Errorf("stream bad status: %s (%s)", r.resp.Status, strings.TrimSpace(string(body)))
}
streamConn.reader = r.resp.Body
responseReady = true
}
}
return outConn, nil
}
func dialStreamOne(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
@@ -500,7 +573,7 @@ func dialStreamOne(ctx context.Context, serverAddress string, opts TunnelDialOpt
if err != nil {
return nil, err
}
return dialStreamOneWithClient(ctx, client, target)
return dialStreamOneWithClient(ctx, client, target, opts)
}
type queuedConn struct {
@@ -599,6 +672,7 @@ type streamSplitConn struct {
pullURL string
closeURL string
headerHost string
auth *tunnelAuth
}
func (c *streamSplitConn) Close() error {
@@ -607,7 +681,7 @@ func (c *streamSplitConn) Close() error {
if c.cancel != nil {
c.cancel()
}
bestEffortCloseSession(c.client, c.closeURL, c.headerHost, TunnelModeStream)
bestEffortCloseSession(c.client, c.closeURL, c.headerHost, TunnelModeStream, c.auth)
return nil
}
@@ -625,6 +699,7 @@ func newStreamSplitConnFromInfo(info *sessionDialInfo) *streamSplitConn {
pullURL: info.pullURL,
closeURL: info.closeURL,
headerHost: info.headerHost,
auth: info.auth,
queuedConn: queuedConn{
rxc: make(chan []byte, 256),
closed: make(chan struct{}),
@@ -639,8 +714,8 @@ func newStreamSplitConnFromInfo(info *sessionDialInfo) *streamSplitConn {
return c
}
func dialStreamSplitWithClient(ctx context.Context, client *http.Client, target httpClientTarget) (net.Conn, error) {
info, err := dialSessionWithClient(ctx, client, target, TunnelModeStream)
func dialStreamSplitWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) {
info, err := dialSessionWithClient(ctx, client, target, TunnelModeStream, opts)
if err != nil {
return nil, err
}
@@ -648,7 +723,18 @@ func dialStreamSplitWithClient(ctx context.Context, client *http.Client, target
if c == nil {
return nil, fmt.Errorf("failed to build stream split conn")
}
return c, nil
outConn := net.Conn(c)
if opts.Upgrade != nil {
upgraded, err := opts.Upgrade(c)
if err != nil {
_ = c.Close()
return nil, err
}
if upgraded != nil {
outConn = upgraded
}
}
return outConn, nil
}
func dialStreamSplit(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
@@ -660,7 +746,18 @@ func dialStreamSplit(ctx context.Context, serverAddress string, opts TunnelDialO
if c == nil {
return nil, fmt.Errorf("failed to build stream split conn")
}
return c, nil
outConn := net.Conn(c)
if opts.Upgrade != nil {
upgraded, err := opts.Upgrade(c)
if err != nil {
_ = c.Close()
return nil, err
}
if upgraded != nil {
outConn = upgraded
}
}
return outConn, nil
}
func (c *streamSplitConn) pullLoop() {
@@ -696,6 +793,7 @@ func (c *streamSplitConn) pullLoop() {
}
req.Host = c.headerHost
applyTunnelHeaders(req.Header, c.headerHost, TunnelModeStream)
applyTunnelAuthHeader(req.Header, c.auth, TunnelModeStream, http.MethodGet, "/stream")
resp, err := c.client.Do(req)
if err != nil {
@@ -793,6 +891,7 @@ func (c *streamSplitConn) pushLoop() {
}
req.Host = c.headerHost
applyTunnelHeaders(req.Header, c.headerHost, TunnelModeStream)
applyTunnelAuthHeader(req.Header, c.auth, TunnelModeStream, http.MethodPost, "/api/v1/upload")
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := c.client.Do(req)
@@ -896,6 +995,7 @@ type pollConn struct {
pullURL string
closeURL string
headerHost string
auth *tunnelAuth
}
func isDialError(err error) bool {
@@ -917,7 +1017,7 @@ func (c *pollConn) closeWithError(err error) error {
if c.cancel != nil {
c.cancel()
}
bestEffortCloseSession(c.client, c.closeURL, c.headerHost, TunnelModePoll)
bestEffortCloseSession(c.client, c.closeURL, c.headerHost, TunnelModePoll, c.auth)
return nil
}
@@ -939,6 +1039,7 @@ func newPollConnFromInfo(info *sessionDialInfo) *pollConn {
pullURL: info.pullURL,
closeURL: info.closeURL,
headerHost: info.headerHost,
auth: info.auth,
queuedConn: queuedConn{
rxc: make(chan []byte, 128),
closed: make(chan struct{}),
@@ -953,8 +1054,8 @@ func newPollConnFromInfo(info *sessionDialInfo) *pollConn {
return c
}
func dialPollWithClient(ctx context.Context, client *http.Client, target httpClientTarget) (net.Conn, error) {
info, err := dialSessionWithClient(ctx, client, target, TunnelModePoll)
func dialPollWithClient(ctx context.Context, client *http.Client, target httpClientTarget, opts TunnelDialOptions) (net.Conn, error) {
info, err := dialSessionWithClient(ctx, client, target, TunnelModePoll, opts)
if err != nil {
return nil, err
}
@@ -962,7 +1063,18 @@ func dialPollWithClient(ctx context.Context, client *http.Client, target httpCli
if c == nil {
return nil, fmt.Errorf("failed to build poll conn")
}
return c, nil
outConn := net.Conn(c)
if opts.Upgrade != nil {
upgraded, err := opts.Upgrade(c)
if err != nil {
_ = c.Close()
return nil, err
}
if upgraded != nil {
outConn = upgraded
}
}
return outConn, nil
}
func dialPoll(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
@@ -974,7 +1086,18 @@ func dialPoll(ctx context.Context, serverAddress string, opts TunnelDialOptions)
if c == nil {
return nil, fmt.Errorf("failed to build poll conn")
}
return c, nil
outConn := net.Conn(c)
if opts.Upgrade != nil {
upgraded, err := opts.Upgrade(c)
if err != nil {
_ = c.Close()
return nil, err
}
if upgraded != nil {
outConn = upgraded
}
}
return outConn, nil
}
func (c *pollConn) pullLoop() {
@@ -1001,6 +1124,7 @@ func (c *pollConn) pullLoop() {
}
req.Host = c.headerHost
applyTunnelHeaders(req.Header, c.headerHost, TunnelModePoll)
applyTunnelAuthHeader(req.Header, c.auth, TunnelModePoll, http.MethodGet, "/stream")
resp, err := c.client.Do(req)
if err != nil {
@@ -1084,6 +1208,7 @@ func (c *pollConn) pushLoop() {
}
req.Host = c.headerHost
applyTunnelHeaders(req.Header, c.headerHost, TunnelModePoll)
applyTunnelAuthHeader(req.Header, c.auth, TunnelModePoll, http.MethodPost, "/api/v1/upload")
req.Header.Set("Content-Type", "text/plain")
resp, err := c.client.Do(req)
@@ -1246,6 +1371,18 @@ func applyTunnelHeaders(h http.Header, host string, mode TunnelMode) {
type TunnelServerOptions struct {
Mode string
// PathRoot is an optional first-level path prefix for all HTTP tunnel endpoints.
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
PathRoot string
// AuthKey enables short-term HMAC auth for HTTP tunnel requests (anti-probing).
// When set (non-empty), the server requires each request to carry a valid Authorization bearer token.
AuthKey string
// AuthSkew controls allowed clock skew / replay window for AuthKey. 0 uses a conservative default.
AuthSkew time.Duration
// PassThroughOnReject controls how the server handles "recognized but rejected" tunnel requests
// (e.g., wrong mode / wrong path / invalid token). When true, the request bytes are replayed back
// to the caller as HandlePassThrough to allow higher-level fallback handling.
PassThroughOnReject bool
// PullReadTimeout controls how long the server long-poll waits for tunnel downlink data before replying with a keepalive newline.
PullReadTimeout time.Duration
// SessionTTL is a best-effort TTL to prevent leaked sessions. 0 uses a conservative default.
@@ -1253,7 +1390,10 @@ type TunnelServerOptions struct {
}
type TunnelServer struct {
mode TunnelMode
mode TunnelMode
pathRoot string
passThroughOnReject bool
auth *tunnelAuth
pullReadTimeout time.Duration
sessionTTL time.Duration
@@ -1272,6 +1412,8 @@ func NewTunnelServer(opts TunnelServerOptions) *TunnelServer {
if mode == TunnelModeLegacy {
// Server-side "legacy" means: don't accept stream/poll tunnels; only passthrough.
}
pathRoot := normalizePathRoot(opts.PathRoot)
auth := newTunnelAuth(opts.AuthKey, opts.AuthSkew)
timeout := opts.PullReadTimeout
if timeout <= 0 {
timeout = 10 * time.Second
@@ -1281,10 +1423,13 @@ func NewTunnelServer(opts TunnelServerOptions) *TunnelServer {
ttl = 2 * time.Minute
}
return &TunnelServer{
mode: mode,
pullReadTimeout: timeout,
sessionTTL: ttl,
sessions: make(map[string]*tunnelSession),
mode: mode,
pathRoot: pathRoot,
auth: auth,
passThroughOnReject: opts.PassThroughOnReject,
pullReadTimeout: timeout,
sessionTTL: ttl,
sessions: make(map[string]*tunnelSession),
}
}
@@ -1340,6 +1485,12 @@ func (s *TunnelServer) HandleConn(rawConn net.Conn) (HandleResult, net.Conn, err
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
if s.mode == TunnelModeLegacy {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
@@ -1348,19 +1499,37 @@ func (s *TunnelServer) HandleConn(rawConn net.Conn) (HandleResult, net.Conn, err
switch TunnelMode(tunnelHeader) {
case TunnelModeStream:
if s.mode != TunnelModeStream && s.mode != TunnelModeAuto {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
}
return s.handleStream(rawConn, req, buffered)
return s.handleStream(rawConn, req, headerBytes, buffered)
case TunnelModePoll:
if s.mode != TunnelModePoll && s.mode != TunnelModeAuto {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
}
return s.handlePoll(rawConn, req, buffered)
return s.handlePoll(rawConn, req, headerBytes, buffered)
default:
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
@@ -1507,19 +1676,31 @@ func (c *bodyConn) Close() error {
return firstErr
}
func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, buffered []byte) (HandleResult, net.Conn, error) {
u, err := url.ParseRequestURI(req.target)
if err != nil {
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, headerBytes []byte, buffered []byte) (HandleResult, net.Conn, error) {
rejectOrReply := func(code int, body string) (HandleResult, net.Conn, error) {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, code, body)
_ = rawConn.Close()
return HandleDone, nil, nil
}
u, err := url.ParseRequestURI(req.target)
if err != nil {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
// Only accept plausible paths to reduce accidental exposure.
if !isAllowedPath(req.target) {
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
path, ok := stripPathRoot(s.pathRoot, u.Path)
if !ok || !s.isAllowedBasePath(path) {
return rejectOrReply(http.StatusNotFound, "not found")
}
if !s.auth.verify(req.headers, TunnelModeStream, req.method, path, time.Now()) {
return rejectOrReply(http.StatusNotFound, "not found")
}
token := u.Query().Get("token")
@@ -1528,31 +1709,25 @@ func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, bu
switch strings.ToUpper(req.method) {
case http.MethodGet:
// Stream split-session: GET /session (no token) => token + start tunnel on a server-side pipe.
if token == "" && u.Path == "/session" {
if token == "" && path == "/session" {
return s.authorizeSession(rawConn)
}
// Stream split-session: GET /stream?token=... => downlink poll.
if token != "" && u.Path == "/stream" {
if token != "" && path == "/stream" {
return s.streamPull(rawConn, token)
}
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusBadRequest, "bad request")
case http.MethodPost:
// Stream split-session: POST /api/v1/upload?token=... => uplink push.
if token != "" && u.Path == "/api/v1/upload" {
if token != "" && path == "/api/v1/upload" {
if closeFlag {
s.closeSession(token)
_ = writeSimpleHTTPResponse(rawConn, http.StatusOK, "")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusOK, "")
}
bodyReader, err := newRequestBodyReader(newPreBufferedConn(rawConn, buffered), req.headers)
if err != nil {
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusBadRequest, "bad request")
}
return s.streamPush(rawConn, token, bodyReader)
}
@@ -1581,19 +1756,13 @@ func (s *TunnelServer) handleStream(rawConn net.Conn, req *httpRequestHeader, bu
return HandleStartTunnel, stream, nil
default:
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusBadRequest, "bad request")
}
}
func isAllowedPath(target string) bool {
u, err := url.ParseRequestURI(target)
if err != nil {
return false
}
func (s *TunnelServer) isAllowedBasePath(path string) bool {
for _, p := range paths {
if u.Path == p {
if path == p {
return true
}
}
@@ -1650,51 +1819,58 @@ func writeTokenHTTPResponse(w io.Writer, token string) error {
return err
}
func (s *TunnelServer) handlePoll(rawConn net.Conn, req *httpRequestHeader, buffered []byte) (HandleResult, net.Conn, error) {
u, err := url.ParseRequestURI(req.target)
if err != nil {
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
func (s *TunnelServer) handlePoll(rawConn net.Conn, req *httpRequestHeader, headerBytes []byte, buffered []byte) (HandleResult, net.Conn, error) {
rejectOrReply := func(code int, body string) (HandleResult, net.Conn, error) {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, code, body)
_ = rawConn.Close()
return HandleDone, nil, nil
}
if !isAllowedPath(req.target) {
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
_ = rawConn.Close()
return HandleDone, nil, nil
u, err := url.ParseRequestURI(req.target)
if err != nil {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
path, ok := stripPathRoot(s.pathRoot, u.Path)
if !ok || !s.isAllowedBasePath(path) {
return rejectOrReply(http.StatusNotFound, "not found")
}
if !s.auth.verify(req.headers, TunnelModePoll, req.method, path, time.Now()) {
return rejectOrReply(http.StatusNotFound, "not found")
}
token := u.Query().Get("token")
closeFlag := u.Query().Get("close") == "1"
switch strings.ToUpper(req.method) {
case http.MethodGet:
if token == "" {
if token == "" && path == "/session" {
return s.authorizeSession(rawConn)
}
return s.pollPull(rawConn, token)
if token != "" && path == "/stream" {
return s.pollPull(rawConn, token)
}
return rejectOrReply(http.StatusBadRequest, "bad request")
case http.MethodPost:
if token == "" {
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "missing token")
_ = rawConn.Close()
return HandleDone, nil, nil
if token == "" || path != "/api/v1/upload" {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
if closeFlag {
s.closeSession(token)
_ = writeSimpleHTTPResponse(rawConn, http.StatusOK, "")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusOK, "")
}
bodyReader, err := newRequestBodyReader(newPreBufferedConn(rawConn, buffered), req.headers)
if err != nil {
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusBadRequest, "bad request")
}
return s.pollPush(rawConn, token, bodyReader)
default:
_ = writeSimpleHTTPResponse(rawConn, http.StatusBadRequest, "bad request")
_ = rawConn.Close()
return HandleDone, nil, nil
return rejectOrReply(http.StatusBadRequest, "bad request")
}
}
+20 -3
View File
@@ -20,7 +20,11 @@ type byteLayout struct {
}
func (l *byteLayout) isHint(b byte) bool {
return (b & l.hintMask) == l.hintValue
if (b & l.hintMask) == l.hintValue {
return true
}
// ASCII layout maps the single non-printable marker (0x7F) to '\n' on the wire.
return l.name == "ascii" && b == '\n'
}
// resolveLayout picks the byte layout based on ASCII preference and optional custom pattern.
@@ -53,12 +57,25 @@ func newASCIILayout() *byteLayout {
padMarker: 0x3F,
paddingPool: padding,
encodeHint: func(val, pos byte) byte {
return 0x40 | ((val & 0x03) << 4) | (pos & 0x0F)
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 {
return 0x40 | (group & 0x3F)
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
}
+10 -5
View File
@@ -3,6 +3,7 @@ package sudoku
import (
"bufio"
"bytes"
crand "crypto/rand"
"encoding/binary"
"errors"
"fmt"
@@ -14,16 +15,20 @@ import (
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
)
func pickClientTable(cfg *ProtocolConfig) (*sudoku.Table, byte, error) {
func pickClientTable(cfg *ProtocolConfig) (*sudoku.Table, error) {
candidates := cfg.tableCandidates()
if len(candidates) == 0 {
return nil, 0, fmt.Errorf("no table configured")
return nil, fmt.Errorf("no table configured")
}
if len(candidates) == 1 {
return candidates[0], 0, nil
return candidates[0], nil
}
idx := int(randomByte()) % len(candidates)
return candidates[idx], byte(idx), nil
var b [1]byte
if _, err := crand.Read(b[:]); err != nil {
return nil, fmt.Errorf("random table pick failed: %w", err)
}
idx := int(b[0]) % len(candidates)
return candidates[idx], nil
}
type readOnlyConn struct {
+20 -10
View File
@@ -20,15 +20,23 @@ Please read this page carefully, which includes precautions and instructions on
#### One click automatic formatting of partitions, expansion, and automatic mounting of plugins, designed specifically for OPENWRT to simplify the tedious operation of partition mounting for OPENWRT. This plugin requires a lot of effort from Sirpdboy to create and test. Please do not delete the creator's information!!
## Version
- Latest update version number: V1.3.1
- Update Date: March 26, 2025
- Update content:
- Reorganize the partition expansion code and address some unreasonable areas.
- Add the format for the target partition, which can be specified as ext4, ntfs, Btrfs, or no format.
- When used as a root directory/or/overlay, it will be formatted in ext4 format.
- At present, the testing on X86 machines is completely normal, and it has not been tested on other routing devices. Please submit the hard disk partition status and error message if there are any issues.
### Update Date: January 14, 2026
- Latest updated version number: V2.0.2
- Update:
- The newly upgraded JavaScript version supports the OpenWRT 25.12 version.
- Add a progress bar display function and support for more partition formats.
- Add more detailed reporting on log partition status.
- Currently, testing on X86 machines is completely normal, but it has not been tested on other routing devices. If there are any issues, please provide the hard disk partition details and error messages.
### Update Date: March 26, 2025
- Latest updated version number: V1.3.1
- Update:
- Reorganize the partition expansion code to address some unreasonable aspects.
- Add the format option for the target partition, allowing users to specify formatting options such as ext4, NTFS, Btrfs, or no formatting.
- When used as the root directory or as /overlay, the partition will be automatically formatted as ext4.
- Currently, testing on X86 machines is completely normal, but it has not been tested on other routing devices. If there are any issues, please provide the hard disk partition information and error messages.
@@ -68,12 +76,14 @@ Luci app parexp automatically obtains formatted partition expansion and automati
## interface
![screenshots](./doc/partexp0.png)
![screenshots](./doc/partexp1.png)
![screenshots](./doc/partexp2.png)
![screenshots](./doc/partexp3.png)
![screenshots](./doc/partexp4.png)
# My other project
+12 -6
View File
@@ -15,15 +15,12 @@
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg)
[luci-app-partexp](https://github.com/sirpdboy/luci-app-partexp) 一键自动格式化分区、扩容、自动挂载插件
**认真阅读完毕** 本页面,本页面包含注意事项和如何使用。
## 功能说明:
#### 一键自动格式化分区、扩容、自动挂载插件,专为OPENWRT设计,简化OPENWRT在分区挂载上烦锁的操作。本插件是sirpdboy耗费大量精力制作测试,请勿删除制作者信息!!
<!-- TOC -->
@@ -39,8 +36,17 @@
## 版本
### 更新日期:2026年1月14日
- 最新更新版本号: V2.0.2
- 更新内容:
- 全新升级js版,支持openwrt25.12版本 。
- 增加进度条显示功能和更多的分区格式。
- 增加更详细的日志分区情况汇报。
- 目前在X86的机器上测试完全正常,其它路由设备上未测试。有问题请提交硬盘分区情况和错误提示。
### 更新日期:2025年3月26日
- 最新更新版本号: V1.3.1
- 更新日期:2025年3月26日
- 更新内容:
- 重新整理分区扩容代码,解决一些不合理的地方。
- 加入对目标分区的格式,可以指定格式化为ext4,ntfs和Btrfs以及不格式化。
@@ -89,15 +95,15 @@
![screenshots](./doc/partexp2.png)
![screenshots](./doc/partexp3.png)
![screenshots](./doc/partexp4.png)
## 使用与授权相关说明
- 本人开源的所有源码,任何引用需注明本处出处,如需修改二次发布必告之本人,未经许可不得做于任何商用用途。
# My other project
- 路由安全看门狗 https://github.com/sirpdboy/luci-app-watchdog
Binary file not shown.

Before

Width:  |  Height:  |  Size: 794 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 998 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 KiB

After

Width:  |  Height:  |  Size: 1022 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

@@ -12,6 +12,7 @@ local trojan_type = {}
local vmess_type = {}
local vless_type = {}
local hysteria2_type = {}
local xray_version = api.get_app_version("xray")
if has_ss then
local s = "shadowsocks-libev"
table.insert(ss_type, s)
@@ -38,6 +39,9 @@ if has_xray then
table.insert(ss_type, s)
table.insert(vmess_type, s)
table.insert(vless_type, s)
if api.compare_versions(xray_version, ">=", "26.1.13") then
table.insert(hysteria2_type, s)
end
end
if has_hysteria2 then
local s = "hysteria2"
@@ -27,6 +27,7 @@ local trojan_type = {}
local vmess_type = {}
local vless_type = {}
local hysteria2_type = {}
local xray_version = api.get_app_version("xray")
if has_ss then
local s = "shadowsocks-libev"
table.insert(ss_type, s)
@@ -53,6 +54,9 @@ if has_xray then
table.insert(ss_type, s)
table.insert(vmess_type, s)
table.insert(vless_type, s)
if api.compare_versions(xray_version, ">=", "26.1.13") then
table.insert(hysteria2_type, s)
end
end
if has_hysteria2 then
local s = "hysteria2"
@@ -40,6 +40,9 @@ o:value("socks", translate("Socks"))
o:value("shadowsocks", translate("Shadowsocks"))
o:value("trojan", translate("Trojan"))
o:value("wireguard", translate("WireGuard"))
if api.compare_versions(xray_version, ">=", "26.1.13") then
o:value("hysteria2", translate("Hysteria2"))
end
if api.compare_versions(xray_version, ">=", "1.8.12") then
o:value("_balancing", translate("Balancing"))
end
@@ -399,6 +402,42 @@ o:value("", translate("Disable"))
o:value("xtls-rprx-vision")
o:depends({ [_n("protocol")] = "vless" })
---- [[hysteria2]]
o = s:option(Value, _n("hysteria2_hop"), translate("Port hopping range"))
o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).")
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_hop_interval"), translate("Hop Interval"), translate("Example:") .. "30s (≥5s)")
o.placeholder = "30s"
o.default = "30s"
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps"))
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps"))
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_obfs_password"), translate("Obfs Password"))
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_auth_password"), translate("Auth Password"))
o.password = true
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Value, _n("hysteria2_idle_timeout"), translate("Idle Timeout"), translate("Example:") .. "30s (4s-120s)")
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Flag, _n("hysteria2_disable_mtu_discovery"), translate("Disable MTU detection"))
o.default = "0"
o:depends({ [_n("protocol")] = "hysteria2"})
---- [[hysteria2 end]]
o = s:option(Flag, _n("tls"), translate("TLS"))
o.default = 0
o:depends({ [_n("protocol")] = "vmess" })
@@ -426,6 +465,7 @@ o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
-- o = s:option(Value, _n("minversion"), translate("minversion"))
-- o.default = "1.3"
@@ -434,10 +474,12 @@ o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o = s:option(Value, _n("tls_serverName"), translate("Domain"))
o:depends({ [_n("tls")] = true })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("tls_chain_fingerprint"), translate("TLS Chain Fingerprint (SHA256)"), translate("Once set, connects only when the servers chain fingerprint matches."))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
@@ -445,6 +487,7 @@ o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o = s:option(Flag, _n("ech"), translate("ECH"))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("flow")] = "", [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(TextValue, _n("ech_config"), translate("ECH Config"))
o.default = ""
@@ -734,11 +777,6 @@ o:depends({ [_n("mux")] = true })
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = 0
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "socks" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "trojan" })
--[[tcpMptcp]]
o = s:option(Flag, _n("tcpMptcp"), "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration."))
@@ -779,7 +817,8 @@ for k, v in pairs(nodes_table) do
end
for i, v in ipairs(s.fields[_n("protocol")].keylist) do
if not v:find("_") then
if not v:find("_") and v ~= "hysteria2" then
s.fields[_n("tcp_fast_open")]:depends({ [_n("protocol")] = v })
s.fields[_n("tcpMptcp")]:depends({ [_n("protocol")] = v })
s.fields[_n("chain_proxy")]:depends({ [_n("protocol")] = v })
end
@@ -1159,7 +1159,7 @@ function get_version()
if not version or #version == 0 then
version = sys.exec("apk list luci-app-passwall 2>/dev/null | awk '/installed/ {print $1}' | cut -d'-' -f4-")
end
return (version or ""):gsub("\n", "")
return (version or ""):gsub("\n", ""):match("^([^-]+)")
end
function to_check_self()
@@ -130,6 +130,12 @@ function gen_outbound(flag, node, tag, proxy_table)
node.wireguard_reserved = #bytes > 0 and bytes or nil
end
if node.protocol == "hysteria2" then
node.protocol = "hysteria"
node.transport = "hysteria"
node.stream_security = "tls"
end
result = {
_id = node_id,
_flag = flag,
@@ -143,7 +149,7 @@ function gen_outbound(flag, node, tag, proxy_table)
xudpConcurrency = (node.mux == "1" and ((node.xudp_concurrency) and tonumber(node.xudp_concurrency) or 8)) or nil
} or nil,
-- 底层传输配置
streamSettings = (node.streamSettings or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan") and {
streamSettings = (node.streamSettings or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan" or node.protocol == "hysteria") and {
sockopt = {
mark = 255,
tcpFastOpen = (node.tcp_fast_open == "1") and true or nil,
@@ -257,6 +263,36 @@ function gen_outbound(flag, node, tag, proxy_table)
return next(extra_tbl) ~= nil and extra_tbl or nil
end)()
} or nil,
hysteriaSettings = (node.transport == "hysteria") and {
version = 2,
auth = node.hysteria2_auth_password,
up = (node.hysteria2_up_mbps and tonumber(node.hysteria2_up_mbps)) and tonumber(node.hysteria2_up_mbps) .. "mbps" or nil,
down = (node.hysteria2_down_mbps and tonumber(node.hysteria2_down_mbps)) and tonumber(node.hysteria2_down_mbps) .. "mbps" or nil,
udphop = (node.hysteria2_hop) and {
port = string.gsub(node.hysteria2_hop, ":", "-"),
interval = (function()
local v = tonumber((node.hysteria2_hop_interval or "30s"):match("^%d+"))
return (v and v >= 5) and (v .. "s") or "30s"
end)()
} or nil,
maxIdleTimeout = (function()
local timeoutStr = tostring(node.hysteria2_idle_timeout or "")
local timeout = tonumber(timeoutStr:match("^%d+"))
if timeout and timeout >= 4 and timeout <= 120 then
return timeout
end
return 30
end)(),
disablePathMTUDiscovery = (node.hysteria2_disable_mtu_discovery) and true or false
} or nil,
udpmasks = (node.transport == "hysteria" and node.hysteria2_obfs_type and node.hysteria2_obfs_type ~= "") and {
{
type = node.hysteria2_obfs_type,
settings = node.hysteria2_obfs_password and {
password = node.hysteria2_obfs_password
} or nil
}
} or nil
} or nil,
settings = {
vnext = (node.protocol == "vmess" or node.protocol == "vless") and {
@@ -295,7 +331,7 @@ function gen_outbound(flag, node, tag, proxy_table)
} or nil
}
} or nil,
address = (node.protocol == "wireguard" and node.wireguard_local_address) and node.wireguard_local_address or nil,
address = (node.protocol == "wireguard" and node.wireguard_local_address) or (node.protocol == "hysteria" and node.address) or nil,
secretKey = (node.protocol == "wireguard") and node.wireguard_secret_key or nil,
peers = (node.protocol == "wireguard") and {
{
@@ -306,7 +342,9 @@ function gen_outbound(flag, node, tag, proxy_table)
}
} or nil,
mtu = (node.protocol == "wireguard" and node.wireguard_mtu) and tonumber(node.wireguard_mtu) or nil,
reserved = (node.protocol == "wireguard" and node.wireguard_reserved) and node.wireguard_reserved or nil
reserved = (node.protocol == "wireguard" and node.wireguard_reserved) and node.wireguard_reserved or nil,
port = (node.protocol == "hysteria" and node.port) and tonumber(node.port) or nil,
version = node.protocol == "hysteria" and 2 or nil
}
}
@@ -1634,7 +1672,8 @@ function gen_config(var)
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)) then
(s.peers and s.peers[1] and s.peers[1].endpoint) or
(s.address and s.port)) 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
@@ -138,7 +138,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
</div>
</div>
</div>
<div class="pure-u-1-4 check" onclick="check_connect('baidu', 'https://www.baidu.com')">
<div class="pure-u-1-4 check" onclick="check_connect('baidu', 'https://www.baidu.com')" title="<%:TLS handshake test, latency for reference only%>">
<div class="block pure-g">
<div class="pure-u-1-3">
<div class="img-con">
@@ -150,7 +150,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
</div>
</div>
</div>
<div class="pure-u-1-4 check" onclick="check_connect('google', 'https://www.google.com/generate_204')">
<div class="pure-u-1-4 check" onclick="check_connect('google', 'https://www.google.com/generate_204')" title="<%:TLS handshake test, latency for reference only%>">
<div class="block pure-g">
<div class="pure-u-1-3">
<div class="img-con">
@@ -162,7 +162,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
</div>
</div>
</div>
<div class="pure-u-1-4 check" onclick="check_connect('github', 'https://github.com')">
<div class="pure-u-1-4 check" onclick="check_connect('github', 'https://github.com')" title="<%:TLS handshake test, latency for reference only%>">
<div class="block pure-g">
<div class="pure-u-1-3">
<div class="img-con">
@@ -174,7 +174,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
</div>
</div>
</div>
<div class="pure-u-1-4 check" onclick="check_connect('instagram', 'https://www.instagram.com')">
<div class="pure-u-1-4 check" onclick="check_connect('instagram', 'https://www.instagram.com')" title="<%:TLS handshake test, latency for reference only%>">
<div class="block pure-g">
<div class="pure-u-1-3">
<div class="img-con">
@@ -25,7 +25,7 @@ local ss_type = get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"
local trojan_type = get_core("trojan_type", {{has_trojan_plus,"trojan-plus"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local vmess_type = get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local vless_type = get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local hysteria2_type = get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"}})
local hysteria2_type = get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"}, {has_xray,"xray"}})
-%>
<script src="<%=resource%>/view/<%=appname%>/qrcode.min.js"></script>
<script type="text/javascript">//<![CDATA[
@@ -564,7 +564,7 @@ local hysteria2_type = get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{
params = params.substring(1);
}
url += params;
} else if ((v_type === "Hysteria2") || (v_type === "sing-box" && opt.get(dom_prefix + "protocol").value === "hysteria2")) {
} else if ((v_type === "Hysteria2") || ((v_type === "sing-box" || v_type === "Xray") && opt.get(dom_prefix + "protocol").value === "hysteria2")) {
protocol = "hysteria2"
var v_port = opt.get(dom_prefix + "port");
var params = "";
@@ -1512,9 +1512,10 @@ local hysteria2_type = get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{
queryParam[decodeURIComponent(params[0])] = decodeURIComponent(params[1] || '');
}
}
if (hysteria2_type == "sing-box" && has_singbox) {
opt.set('type', "sing-box");
dom_prefix = "singbox_"
if ((hysteria2_type == "sing-box" && has_singbox) || (hysteria2_type == "xray" && has_xray)) {
var is_singbox = hysteria2_type == "sing-box" && has_singbox;
opt.set('type', is_singbox ? "sing-box" : "Xray");
dom_prefix = is_singbox ? "singbox_" : "xray_";
opt.set(dom_prefix + 'protocol', "hysteria2");
opt.set(dom_prefix + 'hysteria2_auth_password', decodeURIComponent(password));
if (queryParam["obfs-password"] || queryParam["obfs_password"]) {
@@ -909,7 +909,7 @@ table td, .table .td {
} else {
innerHTML = innerHTML.split("{{tcping}}").join('<span class="tcping_value" cbiid="{{id}}">---</span>');
}
innerHTML = innerHTML.split("{{url_test}}").join('<span class="ping"><a href="javascript:void(0)" onclick="javascript:urltest_node(\'{{id}}\', this)"><%:Test%></a></span>');
innerHTML = innerHTML.split("{{url_test}}").join('<span class="ping"><a href="javascript:void(0)" onclick="javascript:urltest_node(\'{{id}}\', this)" title="<%:TLS handshake test, latency for reference only%>"><%:Test%></a></span>');
innerHTML = innerHTML.split("{{id}}").join(o[".name"]);
innerHTML = innerHTML.split("{{group}}").join(o["group"] || "");
let node_remarks = get_remarks_name(o);
@@ -19,6 +19,9 @@ msgstr "连接失败"
msgid "Touch Check"
msgstr "点我检测"
msgid "TLS handshake test, latency for reference only"
msgstr "TLS握手测试,延时仅供参考"
msgid "Kernel Unsupported"
msgstr "内核不支持"
@@ -1,36 +1,45 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
set dhcp.@dnsmasq[0].localuse=1
commit dhcp
[ -e "/etc/config/ucitrack" ] && {
delete ucitrack.@passwall[-1]
add ucitrack passwall
set ucitrack.@passwall[-1].init=passwall
commit ucitrack
}
if [ -e "/etc/config/ucitrack" ]; then
uci -q batch <<-EOF
delete ucitrack.@passwall[-1]
add ucitrack passwall
set ucitrack.@passwall[-1].init=passwall
delete ucitrack.@passwall_server[-1]
add ucitrack passwall_server
set ucitrack.@passwall_server[-1].init=passwall_server
commit ucitrack
EOF
fi
uci -q batch <<-EOF
delete firewall.passwall
set firewall.passwall=include
set firewall.passwall.type=script
set firewall.passwall.path=/var/etc/passwall.include
set firewall.passwall.reload=1
commit firewall
[ -e "/etc/config/ucitrack" ] && {
delete ucitrack.@passwall_server[-1]
add ucitrack passwall_server
set ucitrack.@passwall_server[-1].init=passwall_server
commit ucitrack
}
set firewall.passwall.type='script'
set firewall.passwall.path='/var/etc/passwall.include'
delete firewall.passwall_server
set firewall.passwall_server=include
set firewall.passwall_server.type=script
set firewall.passwall_server.path=/var/etc/passwall_server.include
set firewall.passwall_server.reload=1
commit firewall
set firewall.passwall_server.type='script'
set firewall.passwall_server.path='/var/etc/passwall_server.include'
set dhcp.@dnsmasq[0].localuse=1
commit dhcp
set uhttpd.main.max_requests=50
commit uhttpd
EOF
if [ -x "/sbin/fw4" ]; then
uci -q delete firewall.passwall.reload
uci -q delete firewall.passwall.fw4_compatible
uci -q delete firewall.passwall_server.reload
uci -q delete firewall.passwall_server.fw4_compatible
else
uci -q set firewall.passwall.reload='1'
uci -q set firewall.passwall_server.reload='1'
fi
uci commit firewall
[ ! -s "/etc/config/passwall" ] && cp -f /usr/share/passwall/0_default_config /etc/config/passwall
chmod +x /usr/share/passwall/*.sh
@@ -3,6 +3,7 @@
DIR="$(cd "$(dirname "$0")" && pwd)"
MY_PATH=$DIR/iptables.sh
IPSET_LOCAL="passwall_local"
IPSET_WAN="passwall_wan"
IPSET_LAN="passwall_lan"
IPSET_VPS="passwall_vps"
IPSET_SHUNT="passwall_shunt"
@@ -13,6 +14,7 @@ IPSET_WHITE="passwall_white"
IPSET_BLOCK="passwall_block"
IPSET_LOCAL6="passwall_local6"
IPSET_WAN6="passwall_wan6"
IPSET_LAN6="passwall_lan6"
IPSET_VPS6="passwall_vps6"
IPSET_SHUNT6="passwall_shunt6"
@@ -201,38 +203,31 @@ gen_lanlist_6() {
cat $RULES_PATH/lanlist_ipv6 | tr -s '\n' | grep -v "^#"
}
get_wan_ip() {
get_wan_ips() {
local family="$1"
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv4
network_get_ipaddr ipv4 "$iface"
if [ -n "$ipv4" ] && [ "$ipv4" != "0.0.0.0" ]; then
case " $NET_ADDR " in
*" $ipv4 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv4" ;;
local addr
if [ "$family" = "ip6" ]; then
network_get_ipaddr6 addr "$iface"
case "$addr" in
""|fe80*) continue ;;
esac
else
network_get_ipaddr addr "$iface"
case "$addr" in
""|"0.0.0.0") continue ;;
esac
fi
done
echo $NET_ADDR
}
get_wan6_ip() {
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv6
network_get_ipaddr6 ipv6 "$iface"
if [ -n "$ipv6" ] && ! echo "$ipv6" | grep -q "^fe80:"; then
case " $NET_ADDR " in
*" $ipv6 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv6" ;;
esac
fi
case " $NET_ADDR " in
*" $addr "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$addr" ;;
esac
done
echo $NET_ADDR
echo "$NET_ADDR"
}
load_acl() {
@@ -823,6 +818,7 @@ filter_direct_node_list() {
add_firewall_rule() {
echolog "开始加载 iptables 防火墙规则..."
ipset -! create $IPSET_LOCAL nethash maxelem 1048576
ipset -! create $IPSET_WAN nethash maxelem 1048576
ipset -! create $IPSET_LAN nethash maxelem 1048576
ipset -! create $IPSET_VPS nethash maxelem 1048576
ipset -! create $IPSET_SHUNT nethash maxelem 1048576 timeout 172800
@@ -833,6 +829,7 @@ add_firewall_rule() {
ipset -! create $IPSET_BLOCK nethash maxelem 1048576 timeout 172800
ipset -! create $IPSET_LOCAL6 nethash family inet6 maxelem 1048576
ipset -! create $IPSET_WAN6 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_SHUNT6 nethash family inet6 maxelem 1048576 timeout 172800
@@ -1000,7 +997,7 @@ add_firewall_rule() {
$ipt_n -A PSW $(dst $IPSET_LAN) -j RETURN
$ipt_n -A PSW $(dst $IPSET_VPS) -j RETURN
WAN_IP=$(get_wan_ip)
WAN_IP=$(get_wan_ips ip4)
[ ! -z "${WAN_IP}" ] && {
for wan_ip in $WAN_IP; do
$ipt_n -A PSW $(comment "WAN_IP_RETURN") -d "${wan_ip}" -j RETURN
@@ -1040,10 +1037,12 @@ add_firewall_rule() {
$ipt_m -A PSW $(dst $IPSET_VPS) -j RETURN
[ ! -z "${WAN_IP}" ] && {
ipset -F $IPSET_WAN
for wan_ip in $WAN_IP; do
$ipt_m -A PSW $(comment "WAN_IP_RETURN") -d "${wan_ip}" -j RETURN
echolog " - [$?]加WAN IPv4到iptables${wan_ip}"
ipset -! add $IPSET_WAN ${wan_ip}
echolog " - [$?]加WAN IPv4到ipset[$IPSET_WAN]${wan_ip}"
done
$ipt_m -A PSW $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
}
unset WAN_IP wan_ip
@@ -1114,12 +1113,14 @@ add_firewall_rule() {
$ip6t_m -A PSW $(dst $IPSET_LAN6) -j RETURN
$ip6t_m -A PSW $(dst $IPSET_VPS6) -j RETURN
WAN6_IP=$(get_wan6_ip)
WAN6_IP=$(get_wan_ips ip6)
[ ! -z "${WAN6_IP}" ] && {
ipset -F $IPSET_WAN6
for wan6_ip in $WAN6_IP; do
$ip6t_m -A PSW $(comment "WAN6_IP_RETURN") -d ${wan6_ip} -j RETURN
echolog " - [$?]加WAN IPv6到iptables${wan6_ip}"
ipset -! add $IPSET_WAN6 ${wan6_ip}
echolog " - [$?]加WAN IPv6到ipset[$IPSET_WAN6]${wan6_ip}"
done
$ip6t_m -A PSW $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN
}
unset WAN6_IP wan6_ip
@@ -1379,6 +1380,7 @@ del_firewall_rule() {
ip -6 route del local ::/0 dev lo table 100 2>/dev/null
destroy_ipset $IPSET_LOCAL
destroy_ipset $IPSET_WAN
destroy_ipset $IPSET_LAN
destroy_ipset $IPSET_VPS
#destroy_ipset $IPSET_SHUNT
@@ -1389,6 +1391,7 @@ del_firewall_rule() {
destroy_ipset $IPSET_WHITE
destroy_ipset $IPSET_LOCAL6
destroy_ipset $IPSET_WAN6
destroy_ipset $IPSET_LAN6
destroy_ipset $IPSET_VPS6
#destroy_ipset $IPSET_SHUNT6
@@ -1443,24 +1446,13 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW")
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT")
WAN_IP=\$(${MY_PATH} get_wan_ip)
PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ipt_n" PSW WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
[ ! -z "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
$ipt_n -R PSW \$PR_INDEX $(comment "WAN_IP_RETURN") -d "\${wan_ip}" -j RETURN
done
}
fi
PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ipt_m" PSW WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
[ ! -z "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
$ipt_m -R PSW \$PR_INDEX $(comment "WAN_IP_RETURN") -d "\${wan_ip}" -j RETURN
done
}
WAN_IP=\$(${MY_PATH} get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
ipset -F $IPSET_WAN
for wan_ip in \$WAN_IP; do
ipset -! add $IPSET_WAN \${wan_ip}
done
}
fi
EOF
)
@@ -1484,15 +1476,13 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW")
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT")
PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ip6t_m" PSW WAN6_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN6_IP=\$(${MY_PATH} get_wan6_ip)
[ ! -z "\${WAN6_IP}" ] && {
for wan6_ip in \$WAN6_IP; do
$ip6t_m -R PSW \$PR_INDEX $(comment "WAN6_IP_RETURN") -d "\${wan6_ip}" -j RETURN
done
}
fi
WAN6_IP=\$(${MY_PATH} get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
ipset -F $IPSET_WAN6
for wan6_ip in \$WAN6_IP; do
ipset -! add $IPSET_WAN6 \${wan6_ip}
done
}
EOF
)
}
@@ -1550,11 +1540,8 @@ get_ipt_bin)
get_ip6t_bin)
get_ip6t_bin
;;
get_wan_ip)
get_wan_ip
;;
get_wan6_ip)
get_wan6_ip
get_wan_ips)
get_wan_ips
;;
filter_direct_node_list)
filter_direct_node_list
@@ -1,9 +1,10 @@
#!/bin/bash
#!/bin/sh
DIR="$(cd "$(dirname "$0")" && pwd)"
MY_PATH=$DIR/nftables.sh
NFTABLE_NAME="inet passwall"
NFTSET_LOCAL="passwall_local"
NFTSET_WAN="passwall_wan"
NFTSET_LAN="passwall_lan"
NFTSET_VPS="passwall_vps"
NFTSET_SHUNT="passwall_shunt"
@@ -14,6 +15,7 @@ NFTSET_WHITE="passwall_white"
NFTSET_BLOCK="passwall_block"
NFTSET_LOCAL6="passwall_local6"
NFTSET_WAN6="passwall_wan6"
NFTSET_LAN6="passwall_lan6"
NFTSET_VPS6="passwall_vps6"
NFTSET_SHUNT6="passwall_shunt6"
@@ -142,7 +144,7 @@ destroy_nftset() {
}
gen_nft_tables() {
if ! nft list tables | grep -q "^table inet passwall$"; then
if ! nft list table "$NFTABLE_NAME" >/dev/null 2>&1; then
nft -f - <<-EOF
table $NFTABLE_NAME {
chain dstnat {
@@ -165,22 +167,30 @@ gen_nft_tables() {
insert_nftset() {
local nftset_name="${1}"; shift
local timeout_argument="${1}"; shift
local default_timeout_argument="365d"
[ -n "${1}" ] && {
local nftset_elements
local default_timeout="365d"
local suffix=""
if [ -n "$nftset_name" ] && { [ $# -gt 0 ] || [ ! -t 0 ]; }; then
case "$timeout_argument" in
"-1") nftset_elements=$(echo -e $@ | sed 's/\s/, /g') ;;
"0") nftset_elements=$(echo -e $@ | sed "s/\s/ timeout $default_timeout_argument, /g" | sed "s/$/ timeout $default_timeout_argument/") ;;
*) nftset_elements=$(echo -e $@ | sed "s/\s/ timeout $timeout_argument, /g" | sed "s/$/ timeout $timeout_argument/") ;;
"-1") suffix="" ;;
"0") suffix=" timeout $default_timeout" ;;
*) suffix=" timeout $timeout_argument" ;;
esac
mkdir -p $TMP_PATH2/nftset
cat > "$TMP_PATH2/nftset/$nftset_name" <<-EOF
define $nftset_name = {$nftset_elements}
add element $NFTABLE_NAME $nftset_name \$$nftset_name
EOF
nft -f "$TMP_PATH2/nftset/$nftset_name"
rm -rf "$TMP_PATH2/nftset"
}
{
if [ $# -gt 0 ]; then
echo "add element $NFTABLE_NAME $nftset_name { "
printf "%s\n" "$@" | awk -v s="$suffix" '{if (NR > 1) printf ",\n";printf "%s%s", $0, s}'
echo " }"
else
local first_line
if IFS= read -r first_line; then
echo "add element $NFTABLE_NAME $nftset_name { "
{ echo "$first_line"; cat; } | awk -v s="$suffix" '{if (NR > 1) printf ",\n";printf "%s%s", $0, s}'
echo " }"
fi
fi
} | nft -f -
fi
}
gen_nftset() {
@@ -191,16 +201,16 @@ gen_nftset() {
# 0 - don't let element timeout(365 days) when set's timeout parameters be seted
# -1 - follow the set's timeout parameters
local timeout_argument_element="${1}"; shift
local gc_interval_time="1h"
nft "list set $NFTABLE_NAME $nftset_name" &>/dev/null
if [ $? -ne 0 ]; then
if ! nft list set $NFTABLE_NAME $nftset_name >/dev/null 2>&1; then
if [ "$timeout_argument_set" == "0" ]; then
nft "add set $NFTABLE_NAME $nftset_name { type $ip_type; flags interval, timeout; auto-merge; }"
else
nft "add set $NFTABLE_NAME $nftset_name { type $ip_type; flags interval, timeout; timeout $timeout_argument_set; gc-interval $timeout_argument_set; auto-merge; }"
nft "add set $NFTABLE_NAME $nftset_name { type $ip_type; flags interval, timeout; timeout $timeout_argument_set; gc-interval $gc_interval_time; auto-merge; }"
fi
fi
[ -n "${1}" ] && insert_nftset $nftset_name $timeout_argument_element $@
[ $# -gt 0 ] || [ ! -t 0 ] && insert_nftset "$nftset_name" "$timeout_argument_element" "$@"
}
get_jump_ipt() {
@@ -226,38 +236,31 @@ gen_lanlist_6() {
cat $RULES_PATH/lanlist_ipv6 | tr -s '\n' | grep -v "^#"
}
get_wan_ip() {
get_wan_ips() {
local family="$1"
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv4
network_get_ipaddr ipv4 "$iface"
if [ -n "$ipv4" ] && [ "$ipv4" != "0.0.0.0" ]; then
case " $NET_ADDR " in
*" $ipv4 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv4" ;;
local addr
if [ "$family" = "ip6" ]; then
network_get_ipaddr6 addr "$iface"
case "$addr" in
""|fe80*) continue ;;
esac
else
network_get_ipaddr addr "$iface"
case "$addr" in
""|"0.0.0.0") continue ;;
esac
fi
done
echo $NET_ADDR
}
get_wan6_ip() {
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv6
network_get_ipaddr6 ipv6 "$iface"
if [ -n "$ipv6" ] && ! echo "$ipv6" | grep -q "^fe80:"; then
case " $NET_ADDR " in
*" $ipv6 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv6" ;;
esac
fi
case " $NET_ADDR " in
*" $addr "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$addr" ;;
esac
done
echo $NET_ADDR
echo "$NET_ADDR"
}
load_acl() {
@@ -805,9 +808,9 @@ filter_vps_addr() {
}
filter_vpsip() {
insert_nftset $NFTSET_VPS "-1" $(uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | sed -e "/^$/d")
uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS "-1"
echolog " - [$?]加入所有IPv4节点到nftset[$NFTSET_VPS]直连完成"
insert_nftset $NFTSET_VPS6 "-1" $(uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "/^$/d")
uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS6 "-1"
echolog " - [$?]加入所有IPv6节点到nftset[$NFTSET_VPS6]直连完成"
}
@@ -856,6 +859,7 @@ filter_direct_node_list() {
add_firewall_rule() {
echolog "开始加载 nftables 防火墙规则..."
gen_nft_tables
gen_nftset $NFTSET_WAN ipv4_addr 0 0
gen_nftset $NFTSET_VPS ipv4_addr 0 0
gen_nftset $NFTSET_GFW ipv4_addr "2d" 0
gen_nftset $NFTSET_LOCAL ipv4_addr 0 "-1"
@@ -864,13 +868,14 @@ add_firewall_rule() {
#echolog "使用缓存加载chnroute..."
nft -f $RULES_PATH/chnroute.nft
else
gen_nftset $NFTSET_CHN ipv4_addr "2d" 0 $(cat $RULES_PATH/chnroute | tr -s '\n' | grep -v "^#")
cat $RULES_PATH/chnroute | tr -s '\n' | grep -v "^#" | gen_nftset $NFTSET_CHN ipv4_addr "2d" 0
fi
gen_nftset $NFTSET_BLACK ipv4_addr "2d" 0
gen_nftset $NFTSET_WHITE ipv4_addr "2d" 0
gen_nftset $NFTSET_BLOCK ipv4_addr "2d" 0
gen_nftset $NFTSET_SHUNT ipv4_addr "2d" 0
gen_nftset $NFTSET_WAN6 ipv6_addr 0 0
gen_nftset $NFTSET_VPS6 ipv6_addr 0 0
gen_nftset $NFTSET_GFW6 ipv6_addr "2d" 0
gen_nftset $NFTSET_LOCAL6 ipv6_addr 0 "-1"
@@ -879,7 +884,7 @@ add_firewall_rule() {
#echolog "使用缓存加载chnroute6..."
nft -f $RULES_PATH/chnroute6.nft
else
gen_nftset $NFTSET_CHN6 ipv6_addr "2d" 0 $(cat $RULES_PATH/chnroute6 | tr -s '\n' | grep -v "^#")
cat $RULES_PATH/chnroute6 | tr -s '\n' | grep -v "^#" | gen_nftset $NFTSET_CHN6 ipv6_addr "2d" 0
fi
gen_nftset $NFTSET_BLACK6 ipv6_addr "2d" 0
gen_nftset $NFTSET_WHITE6 ipv6_addr "2d" 0
@@ -919,8 +924,8 @@ add_firewall_rule() {
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/direct_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ] && type geoview &> /dev/null; then
insert_nftset $NFTSET_WHITE "0" $(get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $NFTSET_WHITE6 "0" $(get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_WHITE "0"
get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_WHITE6 "0"
echolog " - [$?]解析并加入[直连列表] GeoIP 到 NFTSET 完成"
fi
}
@@ -933,8 +938,8 @@ add_firewall_rule() {
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/proxy_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ] && type geoview &> /dev/null; then
insert_nftset $NFTSET_BLACK "0" $(get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $NFTSET_BLACK6 "0" $(get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_BLACK "0"
get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLACK6 "0"
echolog " - [$?]解析并加入[代理列表] GeoIP 到 NFTSET 完成"
fi
}
@@ -947,8 +952,8 @@ add_firewall_rule() {
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/block_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ] && type geoview &> /dev/null; then
insert_nftset $NFTSET_BLOCK "0" $(get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $NFTSET_BLOCK6 "0" $(get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_BLOCK "0"
get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLOCK6 "0"
echolog " - [$?]解析并加入[屏蔽列表] GeoIP 到 NFTSET 完成"
fi
}
@@ -959,22 +964,22 @@ add_firewall_rule() {
local GEOIP_CODE=""
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
for shunt_id in $shunt_ids; do
insert_nftset $NFTSET_SHUNT "0" $(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $NFTSET_SHUNT6 "0" $(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT "0"
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6 "0"
[ "$USE_GEOVIEW" = "1" ] && {
local geoip_code=$(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
[ -n "$geoip_code" ] && GEOIP_CODE="${GEOIP_CODE:+$GEOIP_CODE,}$geoip_code"
}
done
if [ -n "$GEOIP_CODE" ] && type geoview &> /dev/null; then
insert_nftset $NFTSET_SHUNT "0" $(get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $NFTSET_SHUNT6 "0" $(get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT "0"
get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6 "0"
echolog " - [$?]解析并加入[分流节点] GeoIP 到 NFTSET 完成"
fi
}
insert_nftset $NFTSET_LOCAL "-1" $(ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g")
insert_nftset $NFTSET_LOCAL6 "-1" $(ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g")
ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g" | insert_nftset $NFTSET_LOCAL "-1"
ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g" | insert_nftset $NFTSET_LOCAL6 "-1"
# 忽略特殊IP段
local lan_ifname lan_ip
@@ -1064,9 +1069,9 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME PSW_OUTPUT_MANGLE meta mark 0xff counter return"
# jump chains
nft "add rule $NFTABLE_NAME mangle_prerouting counter jump PSW_DIVERT"
nft "add rule $NFTABLE_NAME mangle_prerouting ip protocol udp counter jump PSW_MANGLE"
[ -n "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME mangle_prerouting ip protocol tcp counter jump PSW_MANGLE"
insert_rule_before "$NFTABLE_NAME" "mangle_prerouting" "PSW_MANGLE" "counter jump PSW_DIVERT"
#ipv4 tcp redirect mode
[ -z "${is_tproxy}" ] && {
@@ -1101,12 +1106,14 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME nat_output meta l4proto {icmp,icmpv6} counter jump PSW_ICMP_REDIRECT"
fi
WAN_IP=$(get_wan_ip)
WAN_IP=$(get_wan_ips ip4)
if [ -n "${WAN_IP}" ]; then
nft flush set $NFTABLE_NAME $NFTSET_WAN
insert_nftset $NFTSET_WAN "-1" $WAN_IP
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
nft "add rule $NFTABLE_NAME PSW_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
for wan_ip in $WAN_IP; do
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW_NAT ip daddr ${wan_ip} counter return comment \"WAN_IP_RETURN\""
nft "add rule $NFTABLE_NAME PSW_MANGLE ip daddr ${wan_ip} counter return comment \"WAN_IP_RETURN\""
echolog " - [$?]追加WAN IPv4到nftables${wan_ip}"
echolog " - [$?]加入WAN IPv4到nftset[$NFTSET_WAN]${wan_ip}"
done
fi
unset WAN_IP wan_ip
@@ -1150,11 +1157,13 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto {ipv6} counter jump PSW_MANGLE_V6"
nft "add rule $NFTABLE_NAME mangle_output meta nfproto {ipv6} counter jump PSW_OUTPUT_MANGLE_V6 comment \"PSW_OUTPUT_MANGLE\""
WAN6_IP=$(get_wan6_ip)
WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
insert_nftset $NFTSET_WAN6 "-1" $WAN6_IP
nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
for wan6_ip in $WAN6_IP; do
nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 ip6 daddr ${wan6_ip} counter return comment \"WAN6_IP_RETURN\""
echolog " - [$?]追加WAN IPv6到nftables${wan6_ip}"
echolog " - [$?]加入WAN IPv6到nftset[$NFTSET_WAN6]${wan6_ip}"
done
}
unset WAN6_IP wan6_ip
@@ -1398,6 +1407,7 @@ del_firewall_rule() {
ip -6 route del local ::/0 dev lo table 100 2>/dev/null
destroy_nftset $NFTSET_LOCAL
destroy_nftset $NFTSET_WAN
destroy_nftset $NFTSET_LAN
destroy_nftset $NFTSET_VPS
#destroy_nftset $NFTSET_SHUNT
@@ -1408,6 +1418,7 @@ del_firewall_rule() {
destroy_nftset $NFTSET_WHITE
destroy_nftset $NFTSET_LOCAL6
destroy_nftset $NFTSET_WAN6
destroy_nftset $NFTSET_LAN6
destroy_nftset $NFTSET_VPS6
#destroy_nftset $NFTSET_SHUNT6
@@ -1422,7 +1433,7 @@ del_firewall_rule() {
flush_nftset() {
$DIR/app.sh echolog "清空 NFTSet。"
for _name in $(nft -a list sets | grep -E "passwall" | awk -F 'set ' '{print $2}' | awk '{print $1}'); do
for _name in $(nft -a list sets | grep -E "passwall_" | awk -F 'set ' '{print $2}' | awk '{print $1}'); do
destroy_nftset ${_name}
done
}
@@ -1445,38 +1456,17 @@ gen_include() {
local __nft=" "
__nft=$(cat <<- EOF
[ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW_DIVERT)" ] && nft -f ${nft_chain_file}
[ -z "${is_tproxy}" ] && {
PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "$NFTABLE_NAME" PSW_NAT WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN_IP=\$(sh ${MY_PATH} get_wan_ip)
[ ! -z "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
nft "replace rule $NFTABLE_NAME PSW_NAT handle \$PR_INDEX ip daddr "\${wan_ip}" counter return comment \"WAN_IP_RETURN\""
done
}
fi
WAN_IP=\$(sh ${MY_PATH} get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN
sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1" \$WAN_IP
}
PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "$NFTABLE_NAME" PSW_MANGLE WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN_IP=\$(sh ${MY_PATH} get_wan_ip)
[ ! -z "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
nft "replace rule $NFTABLE_NAME PSW_MANGLE handle \$PR_INDEX ip daddr "\${wan_ip}" counter return comment \"WAN_IP_RETURN\""
done
}
fi
[ "$PROXY_IPV6" == "1" ] && {
PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "$NFTABLE_NAME" PSW_MANGLE_V6 WAN6_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN6_IP=\$(sh ${MY_PATH} get_wan6_ip)
[ ! -z "\${WAN6_IP}" ] && {
for wan6_ip in \$WAN6_IP; do
nft "replace rule $NFTABLE_NAME PSW_MANGLE_V6 handle \$PR_INDEX ip6 daddr "\${wan6_ip}" counter return comment \"WAN6_IP_RETURN\""
done
}
fi
WAN6_IP=\$(sh ${MY_PATH} get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1" \$WAN6_IP
}
}
EOF
)
@@ -1511,20 +1501,11 @@ stop() {
arg1=$1
shift
case $arg1 in
RULE_LAST_INDEX)
RULE_LAST_INDEX "$@"
insert_nftset)
insert_nftset "$@"
;;
insert_rule_before)
insert_rule_before "$@"
;;
insert_rule_after)
insert_rule_after "$@"
;;
get_wan_ip)
get_wan_ip
;;
get_wan6_ip)
get_wan6_ip
get_wan_ips)
get_wan_ips "$@"
;;
filter_direct_node_list)
filter_direct_node_list
@@ -50,32 +50,43 @@ local log = function(...)
end
end
local function gen_nftset(set_name, ip_type, tmp_file, input_file)
f = io.open(input_file, "r")
local element = f:read("*all")
f:close()
nft_file, err = io.open(tmp_file, "w")
nft_file:write('#!/usr/sbin/nft -f\n')
nft_file:write(string.format('define %s = {%s}\n', set_name, string.gsub(element, "%s*%c+", " timeout 3650d, ")))
if sys.call(string.format('nft "list set %s %s" >/dev/null 2>&1', nftable_name, set_name)) ~= 0 then
nft_file:write(string.format('add set %s %s { type %s; flags interval, timeout; timeout 2d; gc-interval 2d; auto-merge; }\n', nftable_name, set_name, ip_type))
end
nft_file:write(string.format('add element %s %s $%s\n', nftable_name, set_name, set_name))
nft_file:close()
sys.call(string.format('nft -f %s &>/dev/null',tmp_file))
os.remove(tmp_file)
end
--gen cache for nftset from file
local function gen_cache(set_name, ip_type, input_file, output_file)
local tmp_dir = "/tmp/"
local tmp_file = output_file .. "_tmp"
local tmp_set_name = set_name .. "_tmp"
gen_nftset(tmp_set_name, ip_type, tmp_file, input_file)
sys.call(string.format('nft list set %s %s | sed "s/%s/%s/g" | cat > %s', nftable_name, tmp_set_name, tmp_set_name, set_name, output_file))
sys.call(string.format('nft flush set %s %s', nftable_name, tmp_set_name))
sys.call(string.format('nft delete set %s %s', nftable_name, tmp_set_name))
local tmp_set_name = set_name .. "_tmp_" .. os.time()
local f_in = io.open(input_file, "r")
if not f_in then return false end
local nft_pipe = io.popen("nft -f -", "w")
if not nft_pipe then
f_in:close()
return false
end
nft_pipe:write('#!/usr/sbin/nft -f\n')
nft_pipe:write(string.format('add table %s\n', nftable_name))
nft_pipe:write(string.format('add set %s %s { type %s; flags interval, timeout; timeout 2d; gc-interval 1h; auto-merge; }\n', nftable_name, tmp_set_name, ip_type))
nft_pipe:write(string.format('add element %s %s { ', nftable_name, tmp_set_name))
local count = 0
local batch_size = 500
for line in f_in:lines() do
local ip = line:match("^%s*(.-)%s*$")
if ip and ip ~= "" then
nft_pipe:write(ip, "timeout 365d, ")
count = count + 1
if count % batch_size == 0 then
nft_pipe:write("}\n")
nft_pipe:write(string.format('add element %s %s { ', nftable_name, tmp_set_name))
end
end
end
nft_pipe:write("}\n")
f_in:close()
local success = nft_pipe:close()
if not (success == true or success == 0) then
os.execute(string.format('nft delete set %s %s 2>/dev/null', nftable_name, tmp_set_name))
return false
end
os.execute(string.format('nft list set %s %s | sed "s/%s/%s/g" > %s', nftable_name, tmp_set_name, tmp_set_name, set_name, output_file))
os.execute(string.format('nft delete set %s %s 2>/dev/null', nftable_name, tmp_set_name))
end
-- curl
@@ -322,25 +333,29 @@ local function extract_domain(s)
end
local function non_file_check(file_path, vali_file)
if fs.readfile(file_path, 10) then
local size_str = sys.exec("grep -i 'Content-Length' " .. vali_file .. " | tail -n1 | sed 's/[^0-9]//g'")
local remote_file_size = tonumber(size_str)
remote_file_size = (remote_file_size and remote_file_size > 0) and remote_file_size or nil
local local_file_size = tonumber(fs.stat(file_path, "size"))
if remote_file_size and local_file_size then
if remote_file_size == local_file_size then
return nil;
else
log("下载文件大小校验出错,原始文件大小" .. remote_file_size .. "B,下载文件大小:" .. local_file_size .. "B。")
return true;
end
else
return nil;
end
else
log("下载文件读取出错。")
return true;
local local_file_size = tonumber(fs.stat(file_path, "size")) or 0
if local_file_size == 0 then
log("下载文件为空或读取出错。")
return true
end
local remote_file_size = nil
local f = io.open(vali_file, "r")
if f then
local header_content = f:read("*a")
f:close()
for size in header_content:gmatch("[Cc]ontent%-[Ll]ength:%s*(%d+)") do
local s = tonumber(size)
if s and s > 0 then
remote_file_size = s
end
end
end
if remote_file_size and remote_file_size ~= local_file_size then
log(string.format("校验出错:远程 %dB, 下载 %dB", remote_file_size, local_file_size))
return true
end
return nil
end
local function GeoToRule(rule_name, rule_type, out_path)
@@ -506,10 +521,10 @@ local function fetch_rule(rule_name,rule_type,url,exclude_domain)
end
gen_cache(set_name, "ipv6_addr", file_tmp, output_file)
end
sys.call(string.format('mv -f %s %s', output_file, rule_path .. "/" ..rule_name.. ".nft"))
os.execute(string.format('mv -f %s %s', output_file, rule_path .. "/" ..rule_name.. ".nft"))
os.remove(output_file)
end
sys.call("mv -f "..file_tmp .. " " ..rule_path .. "/" ..rule_name)
os.execute("mv -f "..file_tmp .. " " ..rule_path .. "/" ..rule_name)
reboot = 1
log(rule_name.. " 更新成功,总规则数 " ..count.. " 条。")
else
@@ -43,7 +43,7 @@ local ss_type_default = get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_s
local trojan_type_default = get_core("trojan_type", {{has_trojan_plus,"trojan-plus"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local vmess_type_default = get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local vless_type_default = get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local hysteria2_type_default = get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"}})
local hysteria2_type_default = get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
----
local domain_strategy_default = uci:get(appname, "@global_subscribe[0]", "domain_strategy") or ""
local domain_strategy_node = ""
@@ -1379,8 +1379,9 @@ local function processData(szType, content, add_mode, group)
result.hysteria2_tls_pinSHA256 = params.pinSHA256
result.hysteria2_hop = params.mport
if hysteria2_type_default == "sing-box" and has_singbox then
result.type = 'sing-box'
if (hysteria2_type_default == "sing-box" and has_singbox) or (hysteria2_type_default == "xray" and has_xray) then
local is_singbox = hysteria2_type_default == "sing-box" and has_singbox
result.type = is_singbox and 'sing-box' or 'Xray'
result.protocol = "hysteria2"
if params["obfs-password"] or params["obfs_password"] then
result.hysteria2_obfs_type = "salamander"
@@ -1549,8 +1550,10 @@ local function curl(url, file, ua, mode)
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3"
}
if ua and ua ~= "" and ua ~= "curl" then
ua = (ua == "passwall") and ("passwall/" .. api.get_version()) or ua
curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"'
end
curl_args[#curl_args + 1] = get_headers()
local return_code, result
if mode == "direct" then
return_code, result = api.curl_direct(url, file, curl_args)
@@ -1562,6 +1565,57 @@ local function curl(url, file, ua, mode)
return tonumber(result)
end
function get_headers()
local cache_file = "/tmp/etc/" .. appname .. "_tmp/sub_curl_headers"
if fs.access(cache_file) then
return luci.sys.exec("cat " .. cache_file)
end
local headers = {}
local function readfile(path)
local f = io.open(path, "r")
if not f then return nil end
local c = f:read("*a")
f:close()
return api.trim(c)
end
headers[#headers + 1] = "x-device-os: OpenWrt"
local rel = readfile("/etc/openwrt_release")
local os_ver = rel and rel:match("DISTRIB_RELEASE='([^']+)'")
if os_ver then
headers[#headers + 1] = "x-ver-os: " .. os_ver
end
local model = readfile("/tmp/sysinfo/model")
if model then
headers[#headers + 1] = "x-device-model: " .. model
end
local mac = readfile("/sys/class/net/eth0/address")
if mac and model then
local raw = mac .. "-" .. model
local p = io.popen("printf '%s' '" .. raw:gsub("'", "'\\''") .. "' | sha256sum")
if p then
local hash = p:read("*l")
p:close()
hash = hash and hash:match("^%w+")
if hash then
headers[#headers + 1] = "x-hwid: " .. hash
end
end
end
local out = {}
for i = 1, #headers do
out[i] = "-H '" .. headers[i]:gsub("'", "'\\''") .. "'"
end
local headers_str = table.concat(out, " ")
local f = io.open(cache_file, "w"); if f then f:write(headers_str); f:close() end
return headers_str
end
local function truncate_nodes(group)
for _, config in pairs(CONFIG) do
if config.currentNodes and #config.currentNodes > 0 then
@@ -274,13 +274,36 @@ if (has_singbox or has_xray) and #nodes_table > 0 then
end
end
o = s:taboption("Main", Flag, "localhost_proxy", translate("Localhost Proxy"), translate("When selected, localhost can transparent proxy."))
o.default = "1"
o.rmempty = false
---- Check the transparent proxy component
local handle = io.popen("lsmod")
local mods = ""
if handle then
mods = handle:read("*a") or ""
handle:close()
end
o = s:taboption("Main", Flag, "client_proxy", translate("Client Proxy"), translate("When selected, devices in LAN can transparent proxy. Otherwise, it will not be proxy. But you can still use access control to allow the designated device to proxy."))
o.default = "1"
o.rmempty = false
if (mods:find("REDIRECT") and mods:find("TPROXY")) or (mods:find("nft_redir") and mods:find("nft_tproxy")) then
o = s:taboption("Main", Flag, "localhost_proxy", translate("Localhost Proxy"), translate("When selected, localhost can transparent proxy."))
o.default = "1"
o.rmempty = false
o = s:taboption("Main", Flag, "client_proxy", translate("Client Proxy"), translate("When selected, devices in LAN can transparent proxy. Otherwise, it will not be proxy. But you can still use access control to allow the designated device to proxy."))
o.default = "1"
o.rmempty = false
else
local html = string.format([[<div class="cbi-checkbox"><input class="cbi-input-checkbox" type="checkbox" disabled></div><div class="cbi-value-description"><font color="red">%s</font></div>]], translate("Missing components, transparent proxy is unavailable."))
o = s:taboption("Proxy", DummyValue, "localhost_proxy", translate("Localhost Proxy"))
o.rawhtml = true
function o.cfgvalue(self, section)
return html
end
o = s:taboption("Proxy", DummyValue, "client_proxy", translate("Client Proxy"))
o.rawhtml = true
function o.cfgvalue(self, section)
return html
end
end
node_socks_port = s:taboption("Main", Value, "node_socks_port", translate("Node") .. " Socks " .. translate("Listen Port"))
node_socks_port.default = 1070
@@ -240,7 +240,7 @@ o.default = "v2rayN/9.99"
o:value("curl", "Curl")
o:value("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Linux")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Windows")
o:value("Passwall2/OpenWrt", "PassWall2")
o:value("passwall2", "PassWall2")
o:value("v2rayN/9.99", "v2rayN")
o = s:option(ListValue, "chain_proxy", translate("Chain Proxy"))
@@ -112,7 +112,15 @@ if has_fw4 then
o:value("1", "NFtables")
end
if (os.execute("lsmod | grep -i REDIRECT >/dev/null") == 0 and os.execute("lsmod | grep -i TPROXY >/dev/null") == 0) or (os.execute("lsmod | grep -i nft_redir >/dev/null") == 0 and os.execute("lsmod | grep -i nft_tproxy >/dev/null") == 0) then
---- Check the transparent proxy component
local handle = io.popen("lsmod")
local mods = ""
if handle then
mods = handle:read("*a") or ""
handle:close()
end
if (mods:find("REDIRECT") and mods:find("TPROXY")) or (mods:find("nft_redir") and mods:find("nft_tproxy")) then
o = s:option(ListValue, "tcp_proxy_way", translate("TCP Proxy Way"))
o.default = "redirect"
o:value("redirect", "REDIRECT")
@@ -130,7 +138,7 @@ if (os.execute("lsmod | grep -i REDIRECT >/dev/null") == 0 and os.execute("lsmod
self.map:set(section, "tcp_proxy_way", value)
end
if os.execute("lsmod | grep -i ip6table_mangle >/dev/null") == 0 or os.execute("lsmod | grep -i nft_tproxy >/dev/null") == 0 then
if mods:find("ip6table_mangle") or mods:find("nft_tproxy") then
---- IPv6 TProxy
o = s:option(Flag, "ipv6_tproxy", translate("IPv6 TProxy"),
"<font color='red'>" ..
@@ -1178,7 +1178,7 @@ function get_version()
if not version or #version == 0 then
version = sys.exec("apk list luci-app-passwall2 2>/dev/null | awk '/installed/ {print $1}' | cut -d'-' -f4-")
end
return (version or ""):gsub("\n", "")
return (version or ""):gsub("\n", ""):match("^([^-]+)")
end
function to_check_self()
@@ -94,6 +94,9 @@ msgstr "客户端代理"
msgid "When selected, devices in LAN can transparent proxy. Otherwise, it will not be proxy. But you can still use access control to allow the designated device to proxy."
msgstr "当勾选时,局域网内的设备可以透明代理。否则,将不代理。但您仍然可以使用访问控制允许指定的设备代理。"
msgid "Missing components, transparent proxy is unavailable."
msgstr "缺少组件,透明代理不可用。"
msgid "Socks Config"
msgstr "Socks 配置"
@@ -2131,8 +2134,8 @@ msgstr "开始更新规则..."
msgid "Download file size verification error. Original file size: %sB. Downloaded file size: %sB."
msgstr "下载文件大小校验出错,原始文件大小 %sB,下载文件大小:%sB。"
msgid "Error reading downloaded file."
msgstr "下载文件读取出错。"
msgid "Downloaded file is empty or an error occurred while reading it."
msgstr "下载文件为空或读取文件时发生错误。"
msgid "%s Start updating..."
msgstr "%s 开始更新..."
@@ -94,6 +94,9 @@ msgstr "客戶端代理"
msgid "When selected, devices in LAN can transparent proxy. Otherwise, it will not be proxy. But you can still use access control to allow the designated device to proxy."
msgstr "當勾選時,局域網內的設備可以透明代理。否則,將不代理。但您仍然可以使用訪問控制允許指定的設備代理。"
msgid "Missing components, transparent proxy is unavailable."
msgstr "缺少組件,透明代理無法使用。"
msgid "Socks Config"
msgstr "Socks 配置"
@@ -2131,8 +2134,8 @@ msgstr "開始更新規則..."
msgid "Download file size verification error. Original file size: %sB. Downloaded file size: %sB."
msgstr "下載檔案大小校驗出錯,原始檔案大小 %sB,下載檔案大小:%sB。"
msgid "Error reading downloaded file."
msgstr "下載檔案讀取出錯。"
msgid "Downloaded file is empty or an error occurred while reading it."
msgstr "下載的文件為空或讀取文件時發生錯誤。"
msgid "%s Start updating..."
msgstr "%s 開始更新..."
@@ -1,36 +1,47 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
set dhcp.@dnsmasq[0].localuse=1
commit dhcp
[ -e "/etc/config/ucitrack" ] && {
delete ucitrack.@passwall2[-1]
add ucitrack passwall2
set ucitrack.@passwall2[-1].init=passwall2
commit ucitrack
}
if [ -e "/etc/config/ucitrack" ]; then
uci -q batch <<-EOF
delete ucitrack.@passwall2[-1]
add ucitrack passwall2
set ucitrack.@passwall2[-1].init=passwall2
delete ucitrack.@passwall2_server[-1]
add ucitrack passwall2_server
set ucitrack.@passwall2_server[-1].init=passwall2_server
commit ucitrack
EOF
fi
uci -q batch <<-EOF
delete firewall.passwall2
set firewall.passwall2=include
set firewall.passwall2.type=script
set firewall.passwall2.path=/var/etc/passwall2.include
set firewall.passwall2.reload=1
commit firewall
[ -e "/etc/config/ucitrack" ] && {
delete ucitrack.@passwall2_server[-1]
add ucitrack passwall2_server
set ucitrack.@passwall2_server[-1].init=passwall2_server
commit ucitrack
}
set firewall.passwall2.type='script'
set firewall.passwall2.path='/var/etc/passwall2.include'
delete firewall.passwall2_server
set firewall.passwall2_server=include
set firewall.passwall2_server.type=script
set firewall.passwall2_server.path=/var/etc/passwall2_server.include
set firewall.passwall2_server.reload=1
commit firewall
set firewall.passwall2_server.type='script'
set firewall.passwall2_server.path='/var/etc/passwall2_server.include'
set dhcp.@dnsmasq[0].localuse=1
commit dhcp
set uhttpd.main.max_requests=50
commit uhttpd
EOF
if [ -x "/sbin/fw4" ]; then
uci -q delete firewall.passwall2.reload
uci -q delete firewall.passwall2.fw4_compatible
uci -q delete firewall.passwall2_server.reload
uci -q delete firewall.passwall2_server.fw4_compatible
else
uci -q set firewall.passwall2.reload='1'
uci -q set firewall.passwall2_server.reload='1'
fi
uci commit firewall
[ ! -s "/etc/config/passwall2" ] && cp -f /usr/share/passwall2/0_default_config /etc/config/passwall2
chmod +x /usr/share/passwall2/*.sh
@@ -3,10 +3,12 @@
DIR="$(cd "$(dirname "$0")" && pwd)"
MY_PATH=$DIR/iptables.sh
IPSET_LOCAL="passwall2_local"
IPSET_WAN="passwall2_wan"
IPSET_LAN="passwall2_lan"
IPSET_VPS="passwall2_vps"
IPSET_LOCAL6="passwall2_local6"
IPSET_WAN6="passwall2_wan6"
IPSET_LAN6="passwall2_lan6"
IPSET_VPS6="passwall2_vps6"
@@ -198,38 +200,31 @@ gen_lanlist_6() {
EOF
}
get_wan_ip() {
get_wan_ips() {
local family="$1"
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv4
network_get_ipaddr ipv4 "$iface"
if [ -n "$ipv4" ] && [ "$ipv4" != "0.0.0.0" ]; then
case " $NET_ADDR " in
*" $ipv4 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv4" ;;
local addr
if [ "$family" = "ip6" ]; then
network_get_ipaddr6 addr "$iface"
case "$addr" in
""|fe80*) continue ;;
esac
else
network_get_ipaddr addr "$iface"
case "$addr" in
""|"0.0.0.0") continue ;;
esac
fi
done
echo $NET_ADDR
}
get_wan6_ip() {
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv6
network_get_ipaddr6 ipv6 "$iface"
if [ -n "$ipv6" ] && ! echo "$ipv6" | grep -q "^fe80:"; then
case " $NET_ADDR " in
*" $ipv6 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv6" ;;
esac
fi
case " $NET_ADDR " in
*" $addr "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$addr" ;;
esac
done
echo $NET_ADDR
echo "$NET_ADDR"
}
gen_shunt_list() {
@@ -675,10 +670,12 @@ 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_LAN nethash maxelem 1048576
ipset -! create $IPSET_VPS nethash maxelem 1048576
ipset -! create $IPSET_LOCAL6 nethash family inet6 maxelem 1048576
ipset -! create $IPSET_WAN6 nethash family inet6 maxelem 1048576
ipset -! create $IPSET_LAN6 nethash family inet6 maxelem 1048576
ipset -! create $IPSET_VPS6 nethash family inet6 maxelem 1048576
@@ -754,13 +751,6 @@ add_firewall_rule() {
$ipt_n -N PSW2
$ipt_n -A PSW2 $(dst $IPSET_LAN) -j RETURN
$ipt_n -A PSW2 $(dst $IPSET_VPS) -j RETURN
WAN_IP=$(get_wan_ip)
[ -n "${WAN_IP}" ] && {
for wan_ip in $WAN_IP; do
$ipt_n -A PSW2 $(comment "WAN_IP_RETURN") -d "${wan_ip}" -j RETURN
done
}
[ "$accept_icmp" = "1" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW2"
[ -z "${is_tproxy}" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW2"
@@ -790,10 +780,14 @@ add_firewall_rule() {
$ipt_m -A PSW2 $(dst $IPSET_VPS) -j RETURN
$ipt_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN
WAN_IP=$(get_wan_ips ip4)
[ -n "${WAN_IP}" ] && {
ipset -F $IPSET_WAN
for wan_ip in $WAN_IP; do
$ipt_m -A PSW2 $(comment "WAN_IP_RETURN") -d "${wan_ip}" -j RETURN
ipset -! add $IPSET_WAN ${wan_ip}
done
$ipt_n -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
$ipt_m -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
}
unset WAN_IP wan_ip
@@ -848,11 +842,13 @@ add_firewall_rule() {
$ip6t_m -A PSW2 $(dst $IPSET_VPS6) -j RETURN
$ip6t_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN
WAN6_IP=$(get_wan6_ip)
WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
ipset -F $IPSET_WAN6
for wan6_ip in $WAN6_IP; do
$ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") -d ${wan6_ip} -j RETURN
ipset -! add $IPSET_WAN6 ${wan6_ip}
done
$ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN
}
unset WAN6_IP wan6_ip
@@ -1059,25 +1055,13 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2")
WAN_IP=\$(${MY_PATH} get_wan_ip)
PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ipt_n" PSW2 WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
[ -n "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
$ipt_n -R PSW2 \$PR_INDEX $(comment "WAN_IP_RETURN") -d "\${wan_ip}" -j RETURN
done
}
fi
PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ipt_m" PSW2 WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
[ -n "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
$ipt_m -R PSW2 \$PR_INDEX $(comment "WAN_IP_RETURN") -d "\${wan_ip}" -j RETURN
done
}
fi
WAN_IP=\$(${MY_PATH} get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
ipset -F $IPSET_WAN
for wan_ip in \$WAN_IP; do
ipset -! add $IPSET_WAN \${wan_ip}
done
}
EOF
)
}
@@ -1094,15 +1078,13 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2")
PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ip6t_m" PSW2 WAN6_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN6_IP=\$(${MY_PATH} get_wan6_ip)
[ -n "\${WAN6_IP}" ] && {
for wan6_ip in \$WAN6_IP; do
$ip6t_m -R PSW2 \$PR_INDEX $(comment "WAN6_IP_RETURN") -d "\${wan6_ip}" -j RETURN
done
}
fi
WAN6_IP=\$(${MY_PATH} get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
ipset -F $IPSET_WAN6
for wan6_ip in \$WAN6_IP; do
ipset -! add $IPSET_WAN6 \${wan6_ip}
done
}
EOF
)
}
@@ -1160,11 +1142,8 @@ get_ipt_bin)
get_ip6t_bin)
get_ip6t_bin
;;
get_wan_ip)
get_wan_ip
;;
get_wan6_ip)
get_wan6_ip
get_wan_ips)
get_wan_ips
;;
filter_direct_node_list)
filter_direct_node_list
@@ -1,13 +1,15 @@
#!/bin/bash
#!/bin/sh
DIR="$(cd "$(dirname "$0")" && pwd)"
MY_PATH=$DIR/nftables.sh
NFTABLE_NAME="inet passwall2"
NFTSET_LOCAL="passwall2_local"
NFTSET_WAN="passwall2_wan"
NFTSET_LAN="passwall2_lan"
NFTSET_VPS="passwall2_vps"
NFTSET_LOCAL6="passwall2_local6"
NFTSET_WAN6="passwall2_wan6"
NFTSET_LAN6="passwall2_lan6"
NFTSET_VPS6="passwall2_vps6"
@@ -125,7 +127,7 @@ destroy_nftset() {
}
gen_nft_tables() {
if [ -z "$(nft list tables | grep 'inet passwall2')" ]; then
if ! nft list table "$NFTABLE_NAME" >/dev/null 2>&1; then
local nft_table_file="$TMP_PATH/PSW2_TABLE.nft"
# Set the correct priority to fit fw4
cat > "$nft_table_file" <<-EOF
@@ -153,25 +155,30 @@ gen_nft_tables() {
insert_nftset() {
local nftset_name="${1}"; shift
local timeout_argument="${1}"; shift
local defalut_timeout_argument="3650d"
local nftset_elements
local default_timeout="365d"
local suffix=""
[ -n "${1}" ] && {
if [ "$timeout_argument" == "-1" ]; then
nftset_elements=$(echo -e $@ | sed 's/\s/, /g')
elif [ "$timeout_argument" == "0" ]; then
nftset_elements=$(echo -e $@ | sed "s/\s/ timeout $defalut_timeout_argument, /g" | sed "s/$/ timeout $defalut_timeout_argument/")
if [ -n "$nftset_name" ] && { [ $# -gt 0 ] || [ ! -t 0 ]; }; then
case "$timeout_argument" in
"-1") suffix="" ;;
"0") suffix=" timeout $default_timeout" ;;
*) suffix=" timeout $timeout_argument" ;;
esac
{
if [ $# -gt 0 ]; then
echo "add element $NFTABLE_NAME $nftset_name { "
printf "%s\n" "$@" | awk -v s="$suffix" '{if (NR > 1) printf ",\n";printf "%s%s", $0, s}'
echo " }"
else
nftset_elements=$(echo -e $@ | sed "s/\s/ timeout $timeout_argument, /g" | sed "s/$/ timeout $timeout_argument/")
local first_line
if IFS= read -r first_line; then
echo "add element $NFTABLE_NAME $nftset_name { "
{ echo "$first_line"; cat; } | awk -v s="$suffix" '{if (NR > 1) printf ",\n";printf "%s%s", $0, s}'
echo " }"
fi
fi
mkdir -p $TMP_PATH2/nftset
cat > "$TMP_PATH2/nftset/$nftset_name" <<-EOF
define $nftset_name = {$nftset_elements}
add element $NFTABLE_NAME $nftset_name \$$nftset_name
EOF
nft -f "$TMP_PATH2/nftset/$nftset_name"
rm -rf "$TMP_PATH2/nftset"
}
} | nft -f -
fi
}
gen_nftset() {
@@ -179,19 +186,19 @@ gen_nftset() {
local ip_type="${1}"; shift
# 0 - don't set defalut timeout
local timeout_argument_set="${1}"; shift
# 0 - don't let element timeout(3650 days) when set's timeout parameters be seted
# 0 - don't let element timeout(365 days) when set's timeout parameters be seted
# -1 - follow the set's timeout parameters
local timeout_argument_element="${1}"; shift
local gc_interval_time="1h"
nft "list set $NFTABLE_NAME $nftset_name" &>/dev/null
if [ $? -ne 0 ]; then
if ! nft list set $NFTABLE_NAME $nftset_name >/dev/null 2>&1; then
if [ "$timeout_argument_set" == "0" ]; then
nft "add set $NFTABLE_NAME $nftset_name { type $ip_type; flags interval, timeout; auto-merge; }"
else
nft "add set $NFTABLE_NAME $nftset_name { type $ip_type; flags interval, timeout; timeout $timeout_argument_set; gc-interval $timeout_argument_set; auto-merge; }"
nft "add set $NFTABLE_NAME $nftset_name { type $ip_type; flags interval, timeout; timeout $timeout_argument_set; gc-interval $gc_interval_time; auto-merge; }"
fi
fi
[ -n "${1}" ] && insert_nftset $nftset_name $timeout_argument_element $@
[ $# -gt 0 ] || [ ! -t 0 ] && insert_nftset "$nftset_name" "$timeout_argument_element" "$@"
}
gen_lanlist() {
@@ -226,38 +233,31 @@ gen_lanlist_6() {
EOF
}
get_wan_ip() {
get_wan_ips() {
local family="$1"
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv4
network_get_ipaddr ipv4 "$iface"
if [ -n "$ipv4" ] && [ "$ipv4" != "0.0.0.0" ]; then
case " $NET_ADDR " in
*" $ipv4 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv4" ;;
local addr
if [ "$family" = "ip6" ]; then
network_get_ipaddr6 addr "$iface"
case "$addr" in
""|fe80*) continue ;;
esac
else
network_get_ipaddr addr "$iface"
case "$addr" in
""|"0.0.0.0") continue ;;
esac
fi
done
echo $NET_ADDR
}
get_wan6_ip() {
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv6
network_get_ipaddr6 ipv6 "$iface"
if [ -n "$ipv6" ] && ! echo "$ipv6" | grep -q "^fe80:"; then
case " $NET_ADDR " in
*" $ipv6 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv6" ;;
esac
fi
case " $NET_ADDR " in
*" $addr "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$addr" ;;
esac
done
echo $NET_ADDR
echo "$NET_ADDR"
}
gen_shunt_list() {
@@ -296,13 +296,13 @@ gen_shunt_list() {
[ "$shunt_node" = "_default" ] && outbound="${default_outbound}"
_SHUNT_LIST4="${_SHUNT_LIST4} ${nftset_v4}:${outbound}"
_SHUNT_LIST6="${_SHUNT_LIST6} ${nftset_v6}:${outbound}"
insert_nftset $nftset_v4 "0" $(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $nftset_v6 "0" $(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $nftset_v4 "0"
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $nftset_v6 "0"
[ "${enable_geoview}" = "1" ] && {
local _geoip_code=$(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
[ -n "$_geoip_code" ] && {
insert_nftset $nftset_v4 "0" $(get_geoip $_geoip_code ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $nftset_v6 "0" $(get_geoip $_geoip_code ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
get_geoip $_geoip_code ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $nftset_v4 "0"
get_geoip $_geoip_code ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $nftset_v6 "0"
log 1 "$(i18n "parse the traffic splitting rules[%s]-[geoip:%s] add to %s to complete." "${shunt_id}" "${_geoip_code}" "NFTSET")"
}
}
@@ -653,9 +653,9 @@ filter_vps_addr() {
}
filter_vpsip() {
insert_nftset $NFTSET_VPS "-1" $(uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | sed -e "/^$/d")
uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS "-1"
#log 1 "$(i18n "Add all %s nodes to %s[%s] direct connection complete." "IPv4" "nftset" "${$NFTSET_VPS}")"
insert_nftset $NFTSET_VPS6 "-1" $(uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "/^$/d")
uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS6 "-1"
#log 1 "$(i18n "Add all %s nodes to %s[%s] direct connection complete." "IPv6" "nftset" "${$NFTSET_VPS6}")"
}
@@ -704,16 +704,18 @@ filter_direct_node_list() {
add_firewall_rule() {
log_i18n 0 "Starting to load %s firewall rules..." "nftables"
gen_nft_tables
gen_nftset $NFTSET_WAN ipv4_addr 0 0
gen_nftset $NFTSET_LOCAL ipv4_addr 0 "-1"
gen_nftset $NFTSET_LAN ipv4_addr 0 "-1" $(gen_lanlist)
gen_nftset $NFTSET_VPS ipv4_addr 0 0
gen_nftset $NFTSET_WAN6 ipv6_addr 0 0
gen_nftset $NFTSET_LOCAL6 ipv6_addr 0 "-1"
gen_nftset $NFTSET_LAN6 ipv6_addr 0 "-1" $(gen_lanlist_6)
gen_nftset $NFTSET_VPS6 ipv6_addr 0 0
insert_nftset $NFTSET_LOCAL "-1" $(ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g")
insert_nftset $NFTSET_LOCAL6 "-1" $(ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g")
ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g" | insert_nftset $NFTSET_LOCAL "-1"
ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g" | insert_nftset $NFTSET_LOCAL6 "-1"
# Ignore special IP ranges
local lan_ifname lan_ip
@@ -846,14 +848,14 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME nat_output meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT"
fi
WAN_IP=$(get_wan_ip)
WAN_IP=$(get_wan_ips ip4)
[ -n "${WAN_IP}" ] && {
for wan_ip in $WAN_IP; do
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr ${wan_ip} counter return comment \"WAN_IP_RETURN\""
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr ${wan_ip} counter return comment \"WAN_IP_RETURN\""
done
nft flush set $NFTABLE_NAME $NFTSET_WAN
insert_nftset $NFTSET_WAN "-1" $WAN_IP
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
}
unset WAN_IP wan_ip
unset WAN_IP
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
@@ -877,13 +879,13 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto {ipv6} counter jump PSW2_MANGLE_V6"
nft "add rule $NFTABLE_NAME mangle_output meta nfproto {ipv6} counter jump PSW2_OUTPUT_MANGLE_V6 comment \"PSW2_OUTPUT_MANGLE\""
WAN6_IP=$(get_wan6_ip)
WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
for wan6_ip in $WAN6_IP; do
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr ${wan6_ip} counter return comment \"WAN6_IP_RETURN\""
done
nft flush set $NFTABLE_NAME $NFTSET_WAN6
insert_nftset $NFTSET_WAN6 "-1" $WAN6_IP
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
}
unset WAN6_IP wan6_ip
unset WAN6_IP
ip -6 rule add fwmark 1 table 100
ip -6 route add local ::/0 dev lo table 100
@@ -1038,10 +1040,12 @@ del_firewall_rule() {
ip -6 route del local ::/0 dev lo table 100 2>/dev/null
destroy_nftset $NFTSET_LOCAL
destroy_nftset $NFTSET_WAN
destroy_nftset $NFTSET_LAN
destroy_nftset $NFTSET_VPS
destroy_nftset $NFTSET_LOCAL6
destroy_nftset $NFTSET_WAN6
destroy_nftset $NFTSET_LAN6
destroy_nftset $NFTSET_VPS6
@@ -1050,7 +1054,7 @@ del_firewall_rule() {
flush_nftset() {
$DIR/app.sh log_i18n 0 "Clear %s." "NFTSet"
for _name in $(nft -a list sets | grep -E "passwall2" | awk -F 'set ' '{print $2}' | awk '{print $1}'); do
for _name in $(nft -a list sets | grep -E "passwall2_" | awk -F 'set ' '{print $2}' | awk '{print $1}'); do
destroy_nftset ${_name}
done
}
@@ -1073,38 +1077,17 @@ gen_include() {
local __nft=" "
__nft=$(cat <<- EOF
[ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW2)" ] && nft -f ${nft_chain_file}
[ -z "${is_tproxy}" ] && {
PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "$NFTABLE_NAME" PSW2_NAT WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN_IP=\$(sh ${MY_PATH} get_wan_ip)
[ -n "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
nft "replace rule $NFTABLE_NAME PSW2_NAT handle \$PR_INDEX ip daddr "\${wan_ip}" counter return comment \"WAN_IP_RETURN\""
done
}
fi
WAN_IP=\$(sh ${MY_PATH} get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN
sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1" \$WAN_IP
}
PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "$NFTABLE_NAME" PSW2_MANGLE WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN_IP=\$(sh ${MY_PATH} get_wan_ip)
[ -n "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
nft "replace rule $NFTABLE_NAME PSW2_MANGLE handle \$PR_INDEX ip daddr "\${wan_ip}" counter return comment \"WAN_IP_RETURN\""
done
}
fi
[ "$PROXY_IPV6" == "1" ] && {
PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "$NFTABLE_NAME" PSW2_MANGLE_V6 WAN6_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN6_IP=\$(sh ${MY_PATH} get_wan6_ip)
[ -n "\${WAN6_IP}" ] && {
for wan6_ip in \$WAN6_IP; do
nft "replace rule $NFTABLE_NAME PSW2_MANGLE_V6 handle \$PR_INDEX ip6 daddr "\${wan6_ip}" counter return comment \"WAN6_IP_RETURN\""
done
}
fi
WAN6_IP=\$(sh ${MY_PATH} get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1" \$WAN6_IP
}
}
EOF
)
@@ -1139,20 +1122,11 @@ stop() {
arg1=$1
shift
case $arg1 in
RULE_LAST_INDEX)
RULE_LAST_INDEX "$@"
insert_nftset)
insert_nftset "$@"
;;
insert_rule_before)
insert_rule_before "$@"
;;
insert_rule_after)
insert_rule_after "$@"
;;
get_wan_ip)
get_wan_ip
;;
get_wan6_ip)
get_wan6_ip
get_wan_ips)
get_wan_ips "$@"
;;
filter_direct_node_list)
filter_direct_node_list
@@ -42,25 +42,29 @@ local function curl(url, file, valifile)
end
local function non_file_check(file_path, vali_file)
if fs.readfile(file_path, 10) then
local size_str = sys.exec("grep -i 'Content-Length' " .. vali_file .. " | tail -n1 | sed 's/[^0-9]//g'")
local remote_file_size = tonumber(size_str)
remote_file_size = (remote_file_size and remote_file_size > 0) and remote_file_size or nil
local local_file_size = tonumber(fs.stat(file_path, "size"))
if remote_file_size and local_file_size then
if remote_file_size == local_file_size then
return nil;
else
log(2, api.i18n.translatef("Download file size verification error. Original file size: %sB. Downloaded file size: %sB.", remote_file_size, local_file_size))
return true;
end
else
return nil;
end
else
log(2, api.i18n.translate("Error reading downloaded file."))
return true;
local local_file_size = tonumber(fs.stat(file_path, "size")) or 0
if local_file_size == 0 then
log(2, api.i18n.translate("Downloaded file is empty or an error occurred while reading it."))
return true
end
local remote_file_size = nil
local f = io.open(vali_file, "r")
if f then
local header_content = f:read("*a")
f:close()
for size in header_content:gmatch("[Cc]ontent%-[Ll]ength:%s*(%d+)") do
local s = tonumber(size)
if s and s > 0 then
remote_file_size = s
end
end
end
if remote_file_size and remote_file_size ~= local_file_size then
log(2, api.i18n.translatef("Download file size verification error. Original file size: %sB. Downloaded file size: %sB.", remote_file_size, local_file_size))
return true
end
return nil
end
local function fetch_geofile(geo_name, geo_type, url)
@@ -1541,8 +1541,10 @@ local function curl(url, file, ua, mode)
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3"
}
if ua and ua ~= "" and ua ~= "curl" then
ua = (ua == "passwall2") and ("passwall2/" .. api.get_version()) or ua
curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"'
end
curl_args[#curl_args + 1] = get_headers()
local return_code, result
if mode == "direct" then
return_code, result = api.curl_direct(url, file, curl_args)
@@ -1554,6 +1556,57 @@ local function curl(url, file, ua, mode)
return tonumber(result)
end
function get_headers()
local cache_file = "/tmp/etc/" .. appname .. "_tmp/sub_curl_headers"
if fs.access(cache_file) then
return luci.sys.exec("cat " .. cache_file)
end
local headers = {}
local function readfile(path)
local f = io.open(path, "r")
if not f then return nil end
local c = f:read("*a")
f:close()
return api.trim(c)
end
headers[#headers + 1] = "x-device-os: OpenWrt"
local rel = readfile("/etc/openwrt_release")
local os_ver = rel and rel:match("DISTRIB_RELEASE='([^']+)'")
if os_ver then
headers[#headers + 1] = "x-ver-os: " .. os_ver
end
local model = readfile("/tmp/sysinfo/model")
if model then
headers[#headers + 1] = "x-device-model: " .. model
end
local mac = readfile("/sys/class/net/eth0/address")
if mac and model then
local raw = mac .. "-" .. model
local p = io.popen("printf '%s' '" .. raw:gsub("'", "'\\''") .. "' | sha256sum")
if p then
local hash = p:read("*l")
p:close()
hash = hash and hash:match("^%w+")
if hash then
headers[#headers + 1] = "x-hwid: " .. hash
end
end
end
local out = {}
for i = 1, #headers do
out[i] = "-H '" .. headers[i]:gsub("'", "'\\''") .. "'"
end
local headers_str = table.concat(out, " ")
local f = io.open(cache_file, "w"); if f then f:write(headers_str); f:close() end
return headers_str
end
local function truncate_nodes(group)
for _, config in pairs(CONFIG) do
if config.currentNodes and #config.currentNodes > 0 then
+27
View File
@@ -4,6 +4,7 @@ import (
"context"
"os"
"runtime"
"sort"
"sync"
"time"
@@ -677,6 +678,10 @@ func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsReque
ticker := time.NewTicker(time.Duration(request.Interval))
defer ticker.Stop()
trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager()
const (
maxClosedConnections = 1000
closedConnectionMaxAge = int64(time.Hour / time.Millisecond)
)
var (
connections = make(map[uuid.UUID]*Connection)
outConnections []*Connection
@@ -693,6 +698,7 @@ func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsReque
if err != nil {
return err
}
evictClosedConnections(connections, closedConnectionMaxAge, maxClosedConnections, time.Now().UnixMilli())
select {
case <-s.ctx.Done():
return s.ctx.Err()
@@ -775,6 +781,27 @@ func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol
return connection
}
func evictClosedConnections(connections map[uuid.UUID]*Connection, maxAgeMs int64, maxCount int, nowMs int64) {
var closedIDs []uuid.UUID
for id, conn := range connections {
if conn.ClosedAt != 0 {
if nowMs-conn.ClosedAt > maxAgeMs {
delete(connections, id)
} else {
closedIDs = append(closedIDs, id)
}
}
}
if len(closedIDs) > maxCount {
sort.Slice(closedIDs, func(i, j int) bool {
return connections[closedIDs[i]].ClosedAt < connections[closedIDs[j]].ClosedAt
})
for i := 0; i < len(closedIDs)-maxCount; i++ {
delete(connections, closedIDs[i])
}
}
}
func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConnectionRequest) (*emptypb.Empty, error) {
s.serviceAccess.RLock()
switch s.serviceStatus.Status {
+90 -107
View File
@@ -27,6 +27,7 @@ type CommandClient struct {
ctx context.Context
cancel context.CancelFunc
clientMutex sync.RWMutex
standalone bool
}
type CommandClientOptions struct {
@@ -73,7 +74,7 @@ func SetXPCDialer(dialer XPCDialer) {
}
func NewStandaloneCommandClient() *CommandClient {
return new(CommandClient)
return &CommandClient{standalone: true}
}
func NewCommandClient(handler CommandClientHandler, options *CommandClientOptions) *CommandClient {
@@ -332,6 +333,28 @@ func (c *CommandClient) getClientForCall() (daemon.StartedServiceClient, error)
return c.grpcClient, nil
}
func (c *CommandClient) closeConnection() {
c.clientMutex.Lock()
defer c.clientMutex.Unlock()
if c.grpcConn != nil {
c.grpcConn.Close()
c.grpcConn = nil
c.grpcClient = nil
}
}
func callWithResult[T any](c *CommandClient, call func(client daemon.StartedServiceClient) (T, error)) (T, error) {
client, err := c.getClientForCall()
if err != nil {
var zero T
return zero, err
}
if c.standalone {
defer c.closeConnection()
}
return call(client)
}
func (c *CommandClient) getStreamContext() (daemon.StartedServiceClient, context.Context) {
c.clientMutex.RLock()
defer c.clientMutex.RUnlock()
@@ -481,162 +504,122 @@ func (c *CommandClient) handleConnectionsStream() {
}
func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.SelectOutbound(context.Background(), &daemon.SelectOutboundRequest{
GroupTag: groupTag,
OutboundTag: outboundTag,
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.SelectOutbound(context.Background(), &daemon.SelectOutboundRequest{
GroupTag: groupTag,
OutboundTag: outboundTag,
})
})
return err
}
func (c *CommandClient) URLTest(groupTag string) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.URLTest(context.Background(), &daemon.URLTestRequest{
OutboundTag: groupTag,
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.URLTest(context.Background(), &daemon.URLTestRequest{
OutboundTag: groupTag,
})
})
return err
}
func (c *CommandClient) SetClashMode(newMode string) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.SetClashMode(context.Background(), &daemon.ClashMode{
Mode: newMode,
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.SetClashMode(context.Background(), &daemon.ClashMode{
Mode: newMode,
})
})
return err
}
func (c *CommandClient) CloseConnection(connId string) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.CloseConnection(context.Background(), &daemon.CloseConnectionRequest{
Id: connId,
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.CloseConnection(context.Background(), &daemon.CloseConnectionRequest{
Id: connId,
})
})
return err
}
func (c *CommandClient) CloseConnections() error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.CloseAllConnections(context.Background(), &emptypb.Empty{})
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.CloseAllConnections(context.Background(), &emptypb.Empty{})
})
return err
}
func (c *CommandClient) ServiceReload() error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.ReloadService(context.Background(), &emptypb.Empty{})
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.ReloadService(context.Background(), &emptypb.Empty{})
})
return err
}
func (c *CommandClient) ServiceClose() error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.StopService(context.Background(), &emptypb.Empty{})
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.StopService(context.Background(), &emptypb.Empty{})
})
return err
}
func (c *CommandClient) ClearLogs() error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.ClearLogs(context.Background(), &emptypb.Empty{})
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.ClearLogs(context.Background(), &emptypb.Empty{})
})
return err
}
func (c *CommandClient) GetSystemProxyStatus() (*SystemProxyStatus, error) {
client, err := c.getClientForCall()
if err != nil {
return nil, err
}
status, err := client.GetSystemProxyStatus(context.Background(), &emptypb.Empty{})
if err != nil {
return nil, err
}
return SystemProxyStatusFromGRPC(status), nil
return callWithResult(c, func(client daemon.StartedServiceClient) (*SystemProxyStatus, error) {
status, err := client.GetSystemProxyStatus(context.Background(), &emptypb.Empty{})
if err != nil {
return nil, err
}
return SystemProxyStatusFromGRPC(status), nil
})
}
func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.SetSystemProxyEnabled(context.Background(), &daemon.SetSystemProxyEnabledRequest{
Enabled: isEnabled,
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.SetSystemProxyEnabled(context.Background(), &daemon.SetSystemProxyEnabledRequest{
Enabled: isEnabled,
})
})
return err
}
func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) {
client, err := c.getClientForCall()
if err != nil {
return nil, err
}
warnings, err := client.GetDeprecatedWarnings(context.Background(), &emptypb.Empty{})
if err != nil {
return nil, err
}
var notes []*DeprecatedNote
for _, warning := range warnings.Warnings {
notes = append(notes, &DeprecatedNote{
Description: warning.Message,
MigrationLink: warning.MigrationLink,
})
}
return newIterator(notes), nil
return callWithResult(c, func(client daemon.StartedServiceClient) (DeprecatedNoteIterator, error) {
warnings, err := client.GetDeprecatedWarnings(context.Background(), &emptypb.Empty{})
if err != nil {
return nil, err
}
var notes []*DeprecatedNote
for _, warning := range warnings.Warnings {
notes = append(notes, &DeprecatedNote{
Description: warning.Message,
MigrationLink: warning.MigrationLink,
})
}
return newIterator(notes), nil
})
}
func (c *CommandClient) GetStartedAt() (int64, error) {
client, err := c.getClientForCall()
if err != nil {
return 0, err
}
startedAt, err := client.GetStartedAt(context.Background(), &emptypb.Empty{})
if err != nil {
return 0, err
}
return startedAt.StartedAt, nil
return callWithResult(c, func(client daemon.StartedServiceClient) (int64, error) {
startedAt, err := client.GetStartedAt(context.Background(), &emptypb.Empty{})
if err != nil {
return 0, err
}
return startedAt.StartedAt, nil
})
}
func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.SetGroupExpand(context.Background(), &daemon.SetGroupExpandRequest{
GroupTag: groupTag,
IsExpand: isExpand,
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.SetGroupExpand(context.Background(), &daemon.SetGroupExpandRequest{
GroupTag: groupTag,
IsExpand: isExpand,
})
})
return err
}
@@ -334,10 +334,7 @@ function connect_status()
local proxy_mode = uci:get(appname, "@global[0]", "tcp_proxy_mode") or "proxy"
local localhost_proxy = uci:get(appname, "@global[0]", "localhost_proxy") or "1"
local socks_server = (localhost_proxy == "0") and api.get_cache_var("GLOBAL_TCP_SOCKS_server") or ""
-- 兼容 curl 8.6 time_starttransfer 错误
local curl_ver = api.get_bin_version_cache("/usr/bin/curl", "-V 2>/dev/null | head -n 1 | awk '{print $2}' | cut -d. -f1,2 | tr -d ' \n'") or "0"
url = (curl_ver == "8.6") and "-w %{http_code}:%{time_appconnect} https://" .. url
or "-w %{http_code}:%{time_starttransfer} http://" .. url
url = "-w %{http_code}:%{time_pretransfer} " .. url
if socks_server and socks_server ~= "" then
if (chn_list == "proxy" and gfw_list == "0" and proxy_mode ~= "proxy" and baidu ~= nil) or (chn_list == "0" and gfw_list == "0" and proxy_mode == "proxy") then
-- 中国列表+百度 or 全局
@@ -682,13 +682,36 @@ o.default = "proxy"
o = s:taboption("Proxy", DummyValue, "switch_mode", " ")
o.template = appname .. "/global/proxy"
o = s:taboption("Proxy", Flag, "localhost_proxy", translate("Localhost Proxy"), translate("When selected, localhost can transparent proxy."))
o.default = "1"
o.rmempty = false
---- Check the transparent proxy component
local handle = io.popen("lsmod")
local mods = ""
if handle then
mods = handle:read("*a") or ""
handle:close()
end
o = s:taboption("Proxy", Flag, "client_proxy", translate("Client Proxy"), translate("When selected, devices in LAN can transparent proxy. Otherwise, it will not be proxy. But you can still use access control to allow the designated device to proxy."))
o.default = "1"
o.rmempty = false
if (mods:find("REDIRECT") and mods:find("TPROXY")) or (mods:find("nft_redir") and mods:find("nft_tproxy")) then
o = s:taboption("Proxy", Flag, "localhost_proxy", translate("Localhost Proxy"), translate("When selected, localhost can transparent proxy."))
o.default = "1"
o.rmempty = false
o = s:taboption("Proxy", Flag, "client_proxy", translate("Client Proxy"), translate("When selected, devices in LAN can transparent proxy. Otherwise, it will not be proxy. But you can still use access control to allow the designated device to proxy."))
o.default = "1"
o.rmempty = false
else
local html = string.format([[<div class="cbi-checkbox"><input class="cbi-input-checkbox" type="checkbox" disabled></div><div class="cbi-value-description"><font color="red">%s</font></div>]], translate("Missing components, transparent proxy is unavailable."))
o = s:taboption("Proxy", DummyValue, "localhost_proxy", translate("Localhost Proxy"))
o.rawhtml = true
function o.cfgvalue(self, section)
return html
end
o = s:taboption("Proxy", DummyValue, "client_proxy", translate("Client Proxy"))
o.rawhtml = true
function o.cfgvalue(self, section)
return html
end
end
o = s:taboption("Proxy", DummyValue, "_proxy_tips", " ")
o.rawhtml = true
@@ -124,7 +124,15 @@ if has_fw4 then
o:value("1", "NFtables")
end
if (os.execute("lsmod | grep -i REDIRECT >/dev/null") == 0 and os.execute("lsmod | grep -i TPROXY >/dev/null") == 0) or (os.execute("lsmod | grep -i nft_redir >/dev/null") == 0 and os.execute("lsmod | grep -i nft_tproxy >/dev/null") == 0) then
---- Check the transparent proxy component
local handle = io.popen("lsmod")
local mods = ""
if handle then
mods = handle:read("*a") or ""
handle:close()
end
if (mods:find("REDIRECT") and mods:find("TPROXY")) or (mods:find("nft_redir") and mods:find("nft_tproxy")) then
o = s:option(ListValue, "tcp_proxy_way", translate("TCP Proxy Way"))
o.default = "redirect"
o:value("redirect", "REDIRECT")
@@ -142,7 +150,7 @@ if (os.execute("lsmod | grep -i REDIRECT >/dev/null") == 0 and os.execute("lsmod
self.map:set(section, "tcp_proxy_way", value)
end
if os.execute("lsmod | grep -i ip6table_mangle >/dev/null") == 0 or os.execute("lsmod | grep -i nft_tproxy >/dev/null") == 0 then
if mods:find("ip6table_mangle") or mods:find("nft_tproxy") then
---- IPv6 TProxy
o = s:option(Flag, "ipv6_tproxy", translate("IPv6 TProxy"),
"<font color='red'>" .. translate(
@@ -1159,7 +1159,7 @@ function get_version()
if not version or #version == 0 then
version = sys.exec("apk list luci-app-passwall 2>/dev/null | awk '/installed/ {print $1}' | cut -d'-' -f4-")
end
return (version or ""):gsub("\n", "")
return (version or ""):gsub("\n", ""):match("^([^-]+)")
end
function to_check_self()
@@ -138,7 +138,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
</div>
</div>
</div>
<div class="pure-u-1-4 check" onclick="check_connect('baidu', 'http://www.baidu.com')">
<div class="pure-u-1-4 check" onclick="check_connect('baidu', 'https://www.baidu.com')" title="<%:TLS handshake test, latency for reference only%>">
<div class="block pure-g">
<div class="pure-u-1-3">
<div class="img-con">
@@ -150,7 +150,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
</div>
</div>
</div>
<div class="pure-u-1-4 check" onclick="check_connect('google', 'http://www.google.com/generate_204')">
<div class="pure-u-1-4 check" onclick="check_connect('google', 'https://www.google.com/generate_204')" title="<%:TLS handshake test, latency for reference only%>">
<div class="block pure-g">
<div class="pure-u-1-3">
<div class="img-con">
@@ -162,7 +162,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
</div>
</div>
</div>
<div class="pure-u-1-4 check" onclick="check_connect('github', 'http://github.com')">
<div class="pure-u-1-4 check" onclick="check_connect('github', 'https://github.com')" title="<%:TLS handshake test, latency for reference only%>">
<div class="block pure-g">
<div class="pure-u-1-3">
<div class="img-con">
@@ -174,7 +174,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
</div>
</div>
</div>
<div class="pure-u-1-4 check" onclick="check_connect('instagram', 'http://www.instagram.com')">
<div class="pure-u-1-4 check" onclick="check_connect('instagram', 'https://www.instagram.com')" title="<%:TLS handshake test, latency for reference only%>">
<div class="block pure-g">
<div class="pure-u-1-3">
<div class="img-con">
@@ -909,7 +909,7 @@ table td, .table .td {
} else {
innerHTML = innerHTML.split("{{tcping}}").join('<span class="tcping_value" cbiid="{{id}}">---</span>');
}
innerHTML = innerHTML.split("{{url_test}}").join('<span class="ping"><a href="javascript:void(0)" onclick="javascript:urltest_node(\'{{id}}\', this)"><%:Test%></a></span>');
innerHTML = innerHTML.split("{{url_test}}").join('<span class="ping"><a href="javascript:void(0)" onclick="javascript:urltest_node(\'{{id}}\', this)" title="<%:TLS handshake test, latency for reference only%>"><%:Test%></a></span>');
innerHTML = innerHTML.split("{{id}}").join(o[".name"]);
innerHTML = innerHTML.split("{{group}}").join(o["group"] || "");
let node_remarks = get_remarks_name(o);
@@ -19,6 +19,9 @@ msgstr "连接失败"
msgid "Touch Check"
msgstr "点我检测"
msgid "TLS handshake test, latency for reference only"
msgstr "TLS握手测试,延时仅供参考"
msgid "Kernel Unsupported"
msgstr "内核不支持"
@@ -352,6 +355,9 @@ msgstr "客户端代理"
msgid "When selected, devices in LAN can transparent proxy. Otherwise, it will not be proxy. But you can still use access control to allow the designated device to proxy."
msgstr "当勾选时,局域网内的设备可以透明代理。否则,将不代理。但您仍然可以使用访问控制允许指定的设备代理。"
msgid "Missing components, transparent proxy is unavailable."
msgstr "缺少组件,透明代理不可用。"
msgid "Want different devices to use different proxy modes/ports/nodes? Please use access control."
msgstr "希望不同设备使用不同的代理模式/端口/节点?请使用访问控制。"
@@ -1,36 +1,45 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
set dhcp.@dnsmasq[0].localuse=1
commit dhcp
[ -e "/etc/config/ucitrack" ] && {
delete ucitrack.@passwall[-1]
add ucitrack passwall
set ucitrack.@passwall[-1].init=passwall
commit ucitrack
}
if [ -e "/etc/config/ucitrack" ]; then
uci -q batch <<-EOF
delete ucitrack.@passwall[-1]
add ucitrack passwall
set ucitrack.@passwall[-1].init=passwall
delete ucitrack.@passwall_server[-1]
add ucitrack passwall_server
set ucitrack.@passwall_server[-1].init=passwall_server
commit ucitrack
EOF
fi
uci -q batch <<-EOF
delete firewall.passwall
set firewall.passwall=include
set firewall.passwall.type=script
set firewall.passwall.path=/var/etc/passwall.include
set firewall.passwall.reload=1
commit firewall
[ -e "/etc/config/ucitrack" ] && {
delete ucitrack.@passwall_server[-1]
add ucitrack passwall_server
set ucitrack.@passwall_server[-1].init=passwall_server
commit ucitrack
}
set firewall.passwall.type='script'
set firewall.passwall.path='/var/etc/passwall.include'
delete firewall.passwall_server
set firewall.passwall_server=include
set firewall.passwall_server.type=script
set firewall.passwall_server.path=/var/etc/passwall_server.include
set firewall.passwall_server.reload=1
commit firewall
set firewall.passwall_server.type='script'
set firewall.passwall_server.path='/var/etc/passwall_server.include'
set dhcp.@dnsmasq[0].localuse=1
commit dhcp
set uhttpd.main.max_requests=50
commit uhttpd
EOF
if [ -x "/sbin/fw4" ]; then
uci -q delete firewall.passwall.reload
uci -q delete firewall.passwall.fw4_compatible
uci -q delete firewall.passwall_server.reload
uci -q delete firewall.passwall_server.fw4_compatible
else
uci -q set firewall.passwall.reload='1'
uci -q set firewall.passwall_server.reload='1'
fi
uci commit firewall
[ ! -s "/etc/config/passwall" ] && cp -f /usr/share/passwall/0_default_config /etc/config/passwall
chmod +x /usr/share/passwall/*.sh
@@ -3,6 +3,7 @@
DIR="$(cd "$(dirname "$0")" && pwd)"
MY_PATH=$DIR/iptables.sh
IPSET_LOCAL="passwall_local"
IPSET_WAN="passwall_wan"
IPSET_LAN="passwall_lan"
IPSET_VPS="passwall_vps"
IPSET_SHUNT="passwall_shunt"
@@ -13,6 +14,7 @@ IPSET_WHITE="passwall_white"
IPSET_BLOCK="passwall_block"
IPSET_LOCAL6="passwall_local6"
IPSET_WAN6="passwall_wan6"
IPSET_LAN6="passwall_lan6"
IPSET_VPS6="passwall_vps6"
IPSET_SHUNT6="passwall_shunt6"
@@ -201,38 +203,31 @@ gen_lanlist_6() {
cat $RULES_PATH/lanlist_ipv6 | tr -s '\n' | grep -v "^#"
}
get_wan_ip() {
get_wan_ips() {
local family="$1"
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv4
network_get_ipaddr ipv4 "$iface"
if [ -n "$ipv4" ] && [ "$ipv4" != "0.0.0.0" ]; then
case " $NET_ADDR " in
*" $ipv4 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv4" ;;
local addr
if [ "$family" = "ip6" ]; then
network_get_ipaddr6 addr "$iface"
case "$addr" in
""|fe80*) continue ;;
esac
else
network_get_ipaddr addr "$iface"
case "$addr" in
""|"0.0.0.0") continue ;;
esac
fi
done
echo $NET_ADDR
}
get_wan6_ip() {
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv6
network_get_ipaddr6 ipv6 "$iface"
if [ -n "$ipv6" ] && ! echo "$ipv6" | grep -q "^fe80:"; then
case " $NET_ADDR " in
*" $ipv6 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv6" ;;
esac
fi
case " $NET_ADDR " in
*" $addr "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$addr" ;;
esac
done
echo $NET_ADDR
echo "$NET_ADDR"
}
load_acl() {
@@ -823,6 +818,7 @@ filter_direct_node_list() {
add_firewall_rule() {
echolog "开始加载 iptables 防火墙规则..."
ipset -! create $IPSET_LOCAL nethash maxelem 1048576
ipset -! create $IPSET_WAN nethash maxelem 1048576
ipset -! create $IPSET_LAN nethash maxelem 1048576
ipset -! create $IPSET_VPS nethash maxelem 1048576
ipset -! create $IPSET_SHUNT nethash maxelem 1048576 timeout 172800
@@ -833,6 +829,7 @@ add_firewall_rule() {
ipset -! create $IPSET_BLOCK nethash maxelem 1048576 timeout 172800
ipset -! create $IPSET_LOCAL6 nethash family inet6 maxelem 1048576
ipset -! create $IPSET_WAN6 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_SHUNT6 nethash family inet6 maxelem 1048576 timeout 172800
@@ -1000,7 +997,7 @@ add_firewall_rule() {
$ipt_n -A PSW $(dst $IPSET_LAN) -j RETURN
$ipt_n -A PSW $(dst $IPSET_VPS) -j RETURN
WAN_IP=$(get_wan_ip)
WAN_IP=$(get_wan_ips ip4)
[ ! -z "${WAN_IP}" ] && {
for wan_ip in $WAN_IP; do
$ipt_n -A PSW $(comment "WAN_IP_RETURN") -d "${wan_ip}" -j RETURN
@@ -1040,10 +1037,12 @@ add_firewall_rule() {
$ipt_m -A PSW $(dst $IPSET_VPS) -j RETURN
[ ! -z "${WAN_IP}" ] && {
ipset -F $IPSET_WAN
for wan_ip in $WAN_IP; do
$ipt_m -A PSW $(comment "WAN_IP_RETURN") -d "${wan_ip}" -j RETURN
echolog " - [$?]加WAN IPv4到iptables${wan_ip}"
ipset -! add $IPSET_WAN ${wan_ip}
echolog " - [$?]加WAN IPv4到ipset[$IPSET_WAN]${wan_ip}"
done
$ipt_m -A PSW $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
}
unset WAN_IP wan_ip
@@ -1114,12 +1113,14 @@ add_firewall_rule() {
$ip6t_m -A PSW $(dst $IPSET_LAN6) -j RETURN
$ip6t_m -A PSW $(dst $IPSET_VPS6) -j RETURN
WAN6_IP=$(get_wan6_ip)
WAN6_IP=$(get_wan_ips ip6)
[ ! -z "${WAN6_IP}" ] && {
ipset -F $IPSET_WAN6
for wan6_ip in $WAN6_IP; do
$ip6t_m -A PSW $(comment "WAN6_IP_RETURN") -d ${wan6_ip} -j RETURN
echolog " - [$?]加WAN IPv6到iptables${wan6_ip}"
ipset -! add $IPSET_WAN6 ${wan6_ip}
echolog " - [$?]加WAN IPv6到ipset[$IPSET_WAN6]${wan6_ip}"
done
$ip6t_m -A PSW $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN
}
unset WAN6_IP wan6_ip
@@ -1379,6 +1380,7 @@ del_firewall_rule() {
ip -6 route del local ::/0 dev lo table 100 2>/dev/null
destroy_ipset $IPSET_LOCAL
destroy_ipset $IPSET_WAN
destroy_ipset $IPSET_LAN
destroy_ipset $IPSET_VPS
#destroy_ipset $IPSET_SHUNT
@@ -1389,6 +1391,7 @@ del_firewall_rule() {
destroy_ipset $IPSET_WHITE
destroy_ipset $IPSET_LOCAL6
destroy_ipset $IPSET_WAN6
destroy_ipset $IPSET_LAN6
destroy_ipset $IPSET_VPS6
#destroy_ipset $IPSET_SHUNT6
@@ -1443,24 +1446,13 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW")
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT")
WAN_IP=\$(${MY_PATH} get_wan_ip)
PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ipt_n" PSW WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
[ ! -z "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
$ipt_n -R PSW \$PR_INDEX $(comment "WAN_IP_RETURN") -d "\${wan_ip}" -j RETURN
done
}
fi
PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ipt_m" PSW WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
[ ! -z "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
$ipt_m -R PSW \$PR_INDEX $(comment "WAN_IP_RETURN") -d "\${wan_ip}" -j RETURN
done
}
WAN_IP=\$(${MY_PATH} get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
ipset -F $IPSET_WAN
for wan_ip in \$WAN_IP; do
ipset -! add $IPSET_WAN \${wan_ip}
done
}
fi
EOF
)
@@ -1484,15 +1476,13 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW")
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT")
PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ip6t_m" PSW WAN6_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN6_IP=\$(${MY_PATH} get_wan6_ip)
[ ! -z "\${WAN6_IP}" ] && {
for wan6_ip in \$WAN6_IP; do
$ip6t_m -R PSW \$PR_INDEX $(comment "WAN6_IP_RETURN") -d "\${wan6_ip}" -j RETURN
done
}
fi
WAN6_IP=\$(${MY_PATH} get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
ipset -F $IPSET_WAN6
for wan6_ip in \$WAN6_IP; do
ipset -! add $IPSET_WAN6 \${wan6_ip}
done
}
EOF
)
}
@@ -1550,11 +1540,8 @@ get_ipt_bin)
get_ip6t_bin)
get_ip6t_bin
;;
get_wan_ip)
get_wan_ip
;;
get_wan6_ip)
get_wan6_ip
get_wan_ips)
get_wan_ips
;;
filter_direct_node_list)
filter_direct_node_list
@@ -1,9 +1,10 @@
#!/bin/bash
#!/bin/sh
DIR="$(cd "$(dirname "$0")" && pwd)"
MY_PATH=$DIR/nftables.sh
NFTABLE_NAME="inet passwall"
NFTSET_LOCAL="passwall_local"
NFTSET_WAN="passwall_wan"
NFTSET_LAN="passwall_lan"
NFTSET_VPS="passwall_vps"
NFTSET_SHUNT="passwall_shunt"
@@ -14,6 +15,7 @@ NFTSET_WHITE="passwall_white"
NFTSET_BLOCK="passwall_block"
NFTSET_LOCAL6="passwall_local6"
NFTSET_WAN6="passwall_wan6"
NFTSET_LAN6="passwall_lan6"
NFTSET_VPS6="passwall_vps6"
NFTSET_SHUNT6="passwall_shunt6"
@@ -142,7 +144,7 @@ destroy_nftset() {
}
gen_nft_tables() {
if ! nft list tables | grep -q "^table inet passwall$"; then
if ! nft list table "$NFTABLE_NAME" >/dev/null 2>&1; then
nft -f - <<-EOF
table $NFTABLE_NAME {
chain dstnat {
@@ -165,22 +167,30 @@ gen_nft_tables() {
insert_nftset() {
local nftset_name="${1}"; shift
local timeout_argument="${1}"; shift
local default_timeout_argument="365d"
[ -n "${1}" ] && {
local nftset_elements
local default_timeout="365d"
local suffix=""
if [ -n "$nftset_name" ] && { [ $# -gt 0 ] || [ ! -t 0 ]; }; then
case "$timeout_argument" in
"-1") nftset_elements=$(echo -e $@ | sed 's/\s/, /g') ;;
"0") nftset_elements=$(echo -e $@ | sed "s/\s/ timeout $default_timeout_argument, /g" | sed "s/$/ timeout $default_timeout_argument/") ;;
*) nftset_elements=$(echo -e $@ | sed "s/\s/ timeout $timeout_argument, /g" | sed "s/$/ timeout $timeout_argument/") ;;
"-1") suffix="" ;;
"0") suffix=" timeout $default_timeout" ;;
*) suffix=" timeout $timeout_argument" ;;
esac
mkdir -p $TMP_PATH2/nftset
cat > "$TMP_PATH2/nftset/$nftset_name" <<-EOF
define $nftset_name = {$nftset_elements}
add element $NFTABLE_NAME $nftset_name \$$nftset_name
EOF
nft -f "$TMP_PATH2/nftset/$nftset_name"
rm -rf "$TMP_PATH2/nftset"
}
{
if [ $# -gt 0 ]; then
echo "add element $NFTABLE_NAME $nftset_name { "
printf "%s\n" "$@" | awk -v s="$suffix" '{if (NR > 1) printf ",\n";printf "%s%s", $0, s}'
echo " }"
else
local first_line
if IFS= read -r first_line; then
echo "add element $NFTABLE_NAME $nftset_name { "
{ echo "$first_line"; cat; } | awk -v s="$suffix" '{if (NR > 1) printf ",\n";printf "%s%s", $0, s}'
echo " }"
fi
fi
} | nft -f -
fi
}
gen_nftset() {
@@ -191,16 +201,16 @@ gen_nftset() {
# 0 - don't let element timeout(365 days) when set's timeout parameters be seted
# -1 - follow the set's timeout parameters
local timeout_argument_element="${1}"; shift
local gc_interval_time="1h"
nft "list set $NFTABLE_NAME $nftset_name" &>/dev/null
if [ $? -ne 0 ]; then
if ! nft list set $NFTABLE_NAME $nftset_name >/dev/null 2>&1; then
if [ "$timeout_argument_set" == "0" ]; then
nft "add set $NFTABLE_NAME $nftset_name { type $ip_type; flags interval, timeout; auto-merge; }"
else
nft "add set $NFTABLE_NAME $nftset_name { type $ip_type; flags interval, timeout; timeout $timeout_argument_set; gc-interval $timeout_argument_set; auto-merge; }"
nft "add set $NFTABLE_NAME $nftset_name { type $ip_type; flags interval, timeout; timeout $timeout_argument_set; gc-interval $gc_interval_time; auto-merge; }"
fi
fi
[ -n "${1}" ] && insert_nftset $nftset_name $timeout_argument_element $@
[ $# -gt 0 ] || [ ! -t 0 ] && insert_nftset "$nftset_name" "$timeout_argument_element" "$@"
}
get_jump_ipt() {
@@ -226,38 +236,31 @@ gen_lanlist_6() {
cat $RULES_PATH/lanlist_ipv6 | tr -s '\n' | grep -v "^#"
}
get_wan_ip() {
get_wan_ips() {
local family="$1"
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv4
network_get_ipaddr ipv4 "$iface"
if [ -n "$ipv4" ] && [ "$ipv4" != "0.0.0.0" ]; then
case " $NET_ADDR " in
*" $ipv4 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv4" ;;
local addr
if [ "$family" = "ip6" ]; then
network_get_ipaddr6 addr "$iface"
case "$addr" in
""|fe80*) continue ;;
esac
else
network_get_ipaddr addr "$iface"
case "$addr" in
""|"0.0.0.0") continue ;;
esac
fi
done
echo $NET_ADDR
}
get_wan6_ip() {
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e '@.interface[@.route[0]].interface')
for iface in $INTERFACES; do
local ipv6
network_get_ipaddr6 ipv6 "$iface"
if [ -n "$ipv6" ] && ! echo "$ipv6" | grep -q "^fe80:"; then
case " $NET_ADDR " in
*" $ipv6 "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ipv6" ;;
esac
fi
case " $NET_ADDR " in
*" $addr "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$addr" ;;
esac
done
echo $NET_ADDR
echo "$NET_ADDR"
}
load_acl() {
@@ -805,9 +808,9 @@ filter_vps_addr() {
}
filter_vpsip() {
insert_nftset $NFTSET_VPS "-1" $(uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | sed -e "/^$/d")
uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS "-1"
echolog " - [$?]加入所有IPv4节点到nftset[$NFTSET_VPS]直连完成"
insert_nftset $NFTSET_VPS6 "-1" $(uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "/^$/d")
uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS6 "-1"
echolog " - [$?]加入所有IPv6节点到nftset[$NFTSET_VPS6]直连完成"
}
@@ -856,6 +859,7 @@ filter_direct_node_list() {
add_firewall_rule() {
echolog "开始加载 nftables 防火墙规则..."
gen_nft_tables
gen_nftset $NFTSET_WAN ipv4_addr 0 0
gen_nftset $NFTSET_VPS ipv4_addr 0 0
gen_nftset $NFTSET_GFW ipv4_addr "2d" 0
gen_nftset $NFTSET_LOCAL ipv4_addr 0 "-1"
@@ -864,13 +868,14 @@ add_firewall_rule() {
#echolog "使用缓存加载chnroute..."
nft -f $RULES_PATH/chnroute.nft
else
gen_nftset $NFTSET_CHN ipv4_addr "2d" 0 $(cat $RULES_PATH/chnroute | tr -s '\n' | grep -v "^#")
cat $RULES_PATH/chnroute | tr -s '\n' | grep -v "^#" | gen_nftset $NFTSET_CHN ipv4_addr "2d" 0
fi
gen_nftset $NFTSET_BLACK ipv4_addr "2d" 0
gen_nftset $NFTSET_WHITE ipv4_addr "2d" 0
gen_nftset $NFTSET_BLOCK ipv4_addr "2d" 0
gen_nftset $NFTSET_SHUNT ipv4_addr "2d" 0
gen_nftset $NFTSET_WAN6 ipv6_addr 0 0
gen_nftset $NFTSET_VPS6 ipv6_addr 0 0
gen_nftset $NFTSET_GFW6 ipv6_addr "2d" 0
gen_nftset $NFTSET_LOCAL6 ipv6_addr 0 "-1"
@@ -879,7 +884,7 @@ add_firewall_rule() {
#echolog "使用缓存加载chnroute6..."
nft -f $RULES_PATH/chnroute6.nft
else
gen_nftset $NFTSET_CHN6 ipv6_addr "2d" 0 $(cat $RULES_PATH/chnroute6 | tr -s '\n' | grep -v "^#")
cat $RULES_PATH/chnroute6 | tr -s '\n' | grep -v "^#" | gen_nftset $NFTSET_CHN6 ipv6_addr "2d" 0
fi
gen_nftset $NFTSET_BLACK6 ipv6_addr "2d" 0
gen_nftset $NFTSET_WHITE6 ipv6_addr "2d" 0
@@ -919,8 +924,8 @@ add_firewall_rule() {
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/direct_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ] && type geoview &> /dev/null; then
insert_nftset $NFTSET_WHITE "0" $(get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $NFTSET_WHITE6 "0" $(get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_WHITE "0"
get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_WHITE6 "0"
echolog " - [$?]解析并加入[直连列表] GeoIP 到 NFTSET 完成"
fi
}
@@ -933,8 +938,8 @@ add_firewall_rule() {
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/proxy_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ] && type geoview &> /dev/null; then
insert_nftset $NFTSET_BLACK "0" $(get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $NFTSET_BLACK6 "0" $(get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_BLACK "0"
get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLACK6 "0"
echolog " - [$?]解析并加入[代理列表] GeoIP 到 NFTSET 完成"
fi
}
@@ -947,8 +952,8 @@ add_firewall_rule() {
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/block_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ] && type geoview &> /dev/null; then
insert_nftset $NFTSET_BLOCK "0" $(get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $NFTSET_BLOCK6 "0" $(get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_BLOCK "0"
get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLOCK6 "0"
echolog " - [$?]解析并加入[屏蔽列表] GeoIP 到 NFTSET 完成"
fi
}
@@ -959,22 +964,22 @@ add_firewall_rule() {
local GEOIP_CODE=""
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
for shunt_id in $shunt_ids; do
insert_nftset $NFTSET_SHUNT "0" $(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $NFTSET_SHUNT6 "0" $(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT "0"
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6 "0"
[ "$USE_GEOVIEW" = "1" ] && {
local geoip_code=$(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
[ -n "$geoip_code" ] && GEOIP_CODE="${GEOIP_CODE:+$GEOIP_CODE,}$geoip_code"
}
done
if [ -n "$GEOIP_CODE" ] && type geoview &> /dev/null; then
insert_nftset $NFTSET_SHUNT "0" $(get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
insert_nftset $NFTSET_SHUNT6 "0" $(get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
get_geoip $GEOIP_CODE ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT "0"
get_geoip $GEOIP_CODE ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6 "0"
echolog " - [$?]解析并加入[分流节点] GeoIP 到 NFTSET 完成"
fi
}
insert_nftset $NFTSET_LOCAL "-1" $(ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g")
insert_nftset $NFTSET_LOCAL6 "-1" $(ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g")
ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g" | insert_nftset $NFTSET_LOCAL "-1"
ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g" | insert_nftset $NFTSET_LOCAL6 "-1"
# 忽略特殊IP段
local lan_ifname lan_ip
@@ -1064,9 +1069,9 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME PSW_OUTPUT_MANGLE meta mark 0xff counter return"
# jump chains
nft "add rule $NFTABLE_NAME mangle_prerouting counter jump PSW_DIVERT"
nft "add rule $NFTABLE_NAME mangle_prerouting ip protocol udp counter jump PSW_MANGLE"
[ -n "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME mangle_prerouting ip protocol tcp counter jump PSW_MANGLE"
insert_rule_before "$NFTABLE_NAME" "mangle_prerouting" "PSW_MANGLE" "counter jump PSW_DIVERT"
#ipv4 tcp redirect mode
[ -z "${is_tproxy}" ] && {
@@ -1101,12 +1106,14 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME nat_output meta l4proto {icmp,icmpv6} counter jump PSW_ICMP_REDIRECT"
fi
WAN_IP=$(get_wan_ip)
WAN_IP=$(get_wan_ips ip4)
if [ -n "${WAN_IP}" ]; then
nft flush set $NFTABLE_NAME $NFTSET_WAN
insert_nftset $NFTSET_WAN "-1" $WAN_IP
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
nft "add rule $NFTABLE_NAME PSW_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
for wan_ip in $WAN_IP; do
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW_NAT ip daddr ${wan_ip} counter return comment \"WAN_IP_RETURN\""
nft "add rule $NFTABLE_NAME PSW_MANGLE ip daddr ${wan_ip} counter return comment \"WAN_IP_RETURN\""
echolog " - [$?]追加WAN IPv4到nftables${wan_ip}"
echolog " - [$?]加入WAN IPv4到nftset[$NFTSET_WAN]${wan_ip}"
done
fi
unset WAN_IP wan_ip
@@ -1150,11 +1157,13 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto {ipv6} counter jump PSW_MANGLE_V6"
nft "add rule $NFTABLE_NAME mangle_output meta nfproto {ipv6} counter jump PSW_OUTPUT_MANGLE_V6 comment \"PSW_OUTPUT_MANGLE\""
WAN6_IP=$(get_wan6_ip)
WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
insert_nftset $NFTSET_WAN6 "-1" $WAN6_IP
nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
for wan6_ip in $WAN6_IP; do
nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 ip6 daddr ${wan6_ip} counter return comment \"WAN6_IP_RETURN\""
echolog " - [$?]追加WAN IPv6到nftables${wan6_ip}"
echolog " - [$?]加入WAN IPv6到nftset[$NFTSET_WAN6]${wan6_ip}"
done
}
unset WAN6_IP wan6_ip
@@ -1398,6 +1407,7 @@ del_firewall_rule() {
ip -6 route del local ::/0 dev lo table 100 2>/dev/null
destroy_nftset $NFTSET_LOCAL
destroy_nftset $NFTSET_WAN
destroy_nftset $NFTSET_LAN
destroy_nftset $NFTSET_VPS
#destroy_nftset $NFTSET_SHUNT
@@ -1408,6 +1418,7 @@ del_firewall_rule() {
destroy_nftset $NFTSET_WHITE
destroy_nftset $NFTSET_LOCAL6
destroy_nftset $NFTSET_WAN6
destroy_nftset $NFTSET_LAN6
destroy_nftset $NFTSET_VPS6
#destroy_nftset $NFTSET_SHUNT6
@@ -1422,7 +1433,7 @@ del_firewall_rule() {
flush_nftset() {
$DIR/app.sh echolog "清空 NFTSet。"
for _name in $(nft -a list sets | grep -E "passwall" | awk -F 'set ' '{print $2}' | awk '{print $1}'); do
for _name in $(nft -a list sets | grep -E "passwall_" | awk -F 'set ' '{print $2}' | awk '{print $1}'); do
destroy_nftset ${_name}
done
}
@@ -1445,38 +1456,17 @@ gen_include() {
local __nft=" "
__nft=$(cat <<- EOF
[ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW_DIVERT)" ] && nft -f ${nft_chain_file}
[ -z "${is_tproxy}" ] && {
PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "$NFTABLE_NAME" PSW_NAT WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN_IP=\$(sh ${MY_PATH} get_wan_ip)
[ ! -z "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
nft "replace rule $NFTABLE_NAME PSW_NAT handle \$PR_INDEX ip daddr "\${wan_ip}" counter return comment \"WAN_IP_RETURN\""
done
}
fi
WAN_IP=\$(sh ${MY_PATH} get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN
sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1" \$WAN_IP
}
PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "$NFTABLE_NAME" PSW_MANGLE WAN_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN_IP=\$(sh ${MY_PATH} get_wan_ip)
[ ! -z "\${WAN_IP}" ] && {
for wan_ip in \$WAN_IP; do
nft "replace rule $NFTABLE_NAME PSW_MANGLE handle \$PR_INDEX ip daddr "\${wan_ip}" counter return comment \"WAN_IP_RETURN\""
done
}
fi
[ "$PROXY_IPV6" == "1" ] && {
PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "$NFTABLE_NAME" PSW_MANGLE_V6 WAN6_IP_RETURN -1)
if [ \$PR_INDEX -ge 0 ]; then
WAN6_IP=\$(sh ${MY_PATH} get_wan6_ip)
[ ! -z "\${WAN6_IP}" ] && {
for wan6_ip in \$WAN6_IP; do
nft "replace rule $NFTABLE_NAME PSW_MANGLE_V6 handle \$PR_INDEX ip6 daddr "\${wan6_ip}" counter return comment \"WAN6_IP_RETURN\""
done
}
fi
WAN6_IP=\$(sh ${MY_PATH} get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1" \$WAN6_IP
}
}
EOF
)
@@ -1511,20 +1501,11 @@ stop() {
arg1=$1
shift
case $arg1 in
RULE_LAST_INDEX)
RULE_LAST_INDEX "$@"
insert_nftset)
insert_nftset "$@"
;;
insert_rule_before)
insert_rule_before "$@"
;;
insert_rule_after)
insert_rule_after "$@"
;;
get_wan_ip)
get_wan_ip
;;
get_wan6_ip)
get_wan6_ip
get_wan_ips)
get_wan_ips "$@"
;;
filter_direct_node_list)
filter_direct_node_list
@@ -50,32 +50,43 @@ local log = function(...)
end
end
local function gen_nftset(set_name, ip_type, tmp_file, input_file)
f = io.open(input_file, "r")
local element = f:read("*all")
f:close()
nft_file, err = io.open(tmp_file, "w")
nft_file:write('#!/usr/sbin/nft -f\n')
nft_file:write(string.format('define %s = {%s}\n', set_name, string.gsub(element, "%s*%c+", " timeout 3650d, ")))
if sys.call(string.format('nft "list set %s %s" >/dev/null 2>&1', nftable_name, set_name)) ~= 0 then
nft_file:write(string.format('add set %s %s { type %s; flags interval, timeout; timeout 2d; gc-interval 2d; auto-merge; }\n', nftable_name, set_name, ip_type))
end
nft_file:write(string.format('add element %s %s $%s\n', nftable_name, set_name, set_name))
nft_file:close()
sys.call(string.format('nft -f %s &>/dev/null',tmp_file))
os.remove(tmp_file)
end
--gen cache for nftset from file
local function gen_cache(set_name, ip_type, input_file, output_file)
local tmp_dir = "/tmp/"
local tmp_file = output_file .. "_tmp"
local tmp_set_name = set_name .. "_tmp"
gen_nftset(tmp_set_name, ip_type, tmp_file, input_file)
sys.call(string.format('nft list set %s %s | sed "s/%s/%s/g" | cat > %s', nftable_name, tmp_set_name, tmp_set_name, set_name, output_file))
sys.call(string.format('nft flush set %s %s', nftable_name, tmp_set_name))
sys.call(string.format('nft delete set %s %s', nftable_name, tmp_set_name))
local tmp_set_name = set_name .. "_tmp_" .. os.time()
local f_in = io.open(input_file, "r")
if not f_in then return false end
local nft_pipe = io.popen("nft -f -", "w")
if not nft_pipe then
f_in:close()
return false
end
nft_pipe:write('#!/usr/sbin/nft -f\n')
nft_pipe:write(string.format('add table %s\n', nftable_name))
nft_pipe:write(string.format('add set %s %s { type %s; flags interval, timeout; timeout 2d; gc-interval 1h; auto-merge; }\n', nftable_name, tmp_set_name, ip_type))
nft_pipe:write(string.format('add element %s %s { ', nftable_name, tmp_set_name))
local count = 0
local batch_size = 500
for line in f_in:lines() do
local ip = line:match("^%s*(.-)%s*$")
if ip and ip ~= "" then
nft_pipe:write(ip, "timeout 365d, ")
count = count + 1
if count % batch_size == 0 then
nft_pipe:write("}\n")
nft_pipe:write(string.format('add element %s %s { ', nftable_name, tmp_set_name))
end
end
end
nft_pipe:write("}\n")
f_in:close()
local success = nft_pipe:close()
if not (success == true or success == 0) then
os.execute(string.format('nft delete set %s %s 2>/dev/null', nftable_name, tmp_set_name))
return false
end
os.execute(string.format('nft list set %s %s | sed "s/%s/%s/g" > %s', nftable_name, tmp_set_name, tmp_set_name, set_name, output_file))
os.execute(string.format('nft delete set %s %s 2>/dev/null', nftable_name, tmp_set_name))
end
-- curl
@@ -322,25 +333,29 @@ local function extract_domain(s)
end
local function non_file_check(file_path, vali_file)
if fs.readfile(file_path, 10) then
local size_str = sys.exec("grep -i 'Content-Length' " .. vali_file .. " | tail -n1 | sed 's/[^0-9]//g'")
local remote_file_size = tonumber(size_str)
remote_file_size = (remote_file_size and remote_file_size > 0) and remote_file_size or nil
local local_file_size = tonumber(fs.stat(file_path, "size"))
if remote_file_size and local_file_size then
if remote_file_size == local_file_size then
return nil;
else
log("下载文件大小校验出错,原始文件大小" .. remote_file_size .. "B,下载文件大小:" .. local_file_size .. "B。")
return true;
end
else
return nil;
end
else
log("下载文件读取出错。")
return true;
local local_file_size = tonumber(fs.stat(file_path, "size")) or 0
if local_file_size == 0 then
log("下载文件为空或读取出错。")
return true
end
local remote_file_size = nil
local f = io.open(vali_file, "r")
if f then
local header_content = f:read("*a")
f:close()
for size in header_content:gmatch("[Cc]ontent%-[Ll]ength:%s*(%d+)") do
local s = tonumber(size)
if s and s > 0 then
remote_file_size = s
end
end
end
if remote_file_size and remote_file_size ~= local_file_size then
log(string.format("校验出错:远程 %dB, 下载 %dB", remote_file_size, local_file_size))
return true
end
return nil
end
local function GeoToRule(rule_name, rule_type, out_path)
@@ -506,10 +521,10 @@ local function fetch_rule(rule_name,rule_type,url,exclude_domain)
end
gen_cache(set_name, "ipv6_addr", file_tmp, output_file)
end
sys.call(string.format('mv -f %s %s', output_file, rule_path .. "/" ..rule_name.. ".nft"))
os.execute(string.format('mv -f %s %s', output_file, rule_path .. "/" ..rule_name.. ".nft"))
os.remove(output_file)
end
sys.call("mv -f "..file_tmp .. " " ..rule_path .. "/" ..rule_name)
os.execute("mv -f "..file_tmp .. " " ..rule_path .. "/" ..rule_name)
reboot = 1
log(rule_name.. " 更新成功,总规则数 " ..count.. " 条。")
else

Some files were not shown because too many files have changed in this diff Show More