Update On Wed Jan 14 19:44:20 CET 2026
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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、auto;stream/poll/auto 支持走 CDN/反代
|
||||
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 https;false 强制 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、auto;stream/poll/auto 支持走 CDN/反代
|
||||
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
# GeoIP 简介 [](https://deepwiki.com/Loyalsoldier/geoip)
|
||||
# GeoIP 简介 [](https://deepwiki.com/Loyalsoldier/geoip)   [](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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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、auto;stream/poll/auto 支持走 CDN/反代
|
||||
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 https;false 强制 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、auto;stream/poll/auto 支持走 CDN/反代
|
||||
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
# My other project
|
||||
|
||||
|
||||
@@ -15,15 +15,12 @@
|
||||
|
||||

|
||||
|
||||
|
||||
[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 @@
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 使用与授权相关说明
|
||||
|
||||
- 本人开源的所有源码,任何引用需注明本处出处,如需修改二次发布必告之本人,未经许可不得做于任何商用用途。
|
||||
|
||||
|
||||
|
||||
|
||||
# My other project
|
||||
|
||||
- 路由安全看门狗 :https://github.com/sirpdboy/luci-app-watchdog
|
||||
|
||||
|
Before Width: | Height: | Size: 794 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 998 KiB |
|
Before Width: | Height: | Size: 804 KiB After Width: | Height: | Size: 1022 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
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 server’s 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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||