feat: add direct forwarding option

This commit is contained in:
SunBK201 2025-11-06 13:24:03 +08:00
parent 47a2a1d29a
commit 77ca782fa2
14 changed files with 97 additions and 49 deletions

View File

@ -66,6 +66,11 @@ partialRepalce.description =
translate("Replace only the matched part of the User-Agent, only works when User-Agent Regex Pattern is not empty") translate("Replace only the matched part of the User-Agent, only works when User-Agent Regex Pattern is not empty")
partialRepalce.default = "0" partialRepalce.default = "0"
directForward = general:taboption("general", Flag, "direct_forward", translate("Direct Forward"))
directForward.description =
translate("Directly forward packets without rewriting")
directForward.default = "0"
log = general:taboption("log", TextValue, "log") log = general:taboption("log", TextValue, "log")
log.readonly = true log.readonly = true
log.rows = 30 log.rows = 30

View File

@ -455,7 +455,7 @@ start_service() {
LOG "Starting $NAME service..." LOG "Starting $NAME service..."
local port bind ua log_level ua_regex partial_replace set_ttl local port bind ua log_level ua_regex partial_replace set_ttl direct_forward
config_get server_mode "main" "server_mode" "SOCKS5" config_get server_mode "main" "server_mode" "SOCKS5"
config_get port "main" "port" "1080" config_get port "main" "port" "1080"
config_get bind "main" "bind" "127.0.0.1" config_get bind "main" "bind" "127.0.0.1"
@ -464,6 +464,7 @@ start_service() {
config_get_bool partial_replace "main" "partial_replace" 0 config_get_bool partial_replace "main" "partial_replace" 0
config_get log_level "main" "log_level" "info" config_get log_level "main" "log_level" "info"
config_get_bool set_ttl "main" "set_ttl" 0 config_get_bool set_ttl "main" "set_ttl" 0
config_get_bool direct_forward "main" "direct_forward" 0
SERVER_MODE="$(echo "$server_mode" | tr '[:lower:]' '[:upper:]')" SERVER_MODE="$(echo "$server_mode" | tr '[:lower:]' '[:upper:]')"
SERVER_MODE="$server_mode" SERVER_MODE="$server_mode"
@ -477,6 +478,7 @@ start_service() {
LOG "User-Agent Regex: $ua_regex" LOG "User-Agent Regex: $ua_regex"
LOG "Log level: $log_level" LOG "Log level: $log_level"
LOG "Partial Replace: $partial_replace" LOG "Partial Replace: $partial_replace"
LOG "Direct Forward: $direct_forward"
LOG "Set TTL: $SET_TTL" LOG "Set TTL: $SET_TTL"
set_ua3f_group set_ua3f_group
@ -579,6 +581,7 @@ start_service() {
procd_append_param command -r "$ua_regex" procd_append_param command -r "$ua_regex"
procd_append_param command -l "$log_level" procd_append_param command -l "$log_level"
[ "$partial_replace" = "1" ] && procd_append_param command -s [ "$partial_replace" = "1" ] && procd_append_param command -s
[ "$direct_forward" = "1" ] && procd_append_param command -d
procd_set_param respawn procd_set_param respawn
procd_set_param stdout 1 procd_set_param stdout 1

View File

@ -7,7 +7,8 @@ config 'ua3f' 'main'
option bind '0.0.0.0' option bind '0.0.0.0'
option ua 'FFF' option ua 'FFF'
option ua_regex '(Apple|iPhone|iPad|Macintosh|Mac OS X|Mac|Darwin|Microsoft|Windows|Linux|Android|OpenHarmony|HUAWEI|OPPO|Vivo|XiaoMi|Mobile|Dalvik)' option ua_regex '(Apple|iPhone|iPad|Macintosh|Mac OS X|Mac|Darwin|Microsoft|Windows|Linux|Android|OpenHarmony|HUAWEI|OPPO|Vivo|XiaoMi|Mobile|Dalvik)'
option partial_replace false option partial_replace '0'
option direct_forward '0'
option log_level 'error' option log_level 'error'
option log_lines '1000' option log_lines '1000'
option set_ttl '0' option set_ttl '0'

View File

@ -56,6 +56,12 @@ msgstr "部分替换"
msgid "Replace only the matched part of the User-Agent, only works when User-Agent Regex Pattern is not empty" msgid "Replace only the matched part of the User-Agent, only works when User-Agent Regex Pattern is not empty"
msgstr "仅替换 User-Agent 正则匹配的部分,仅在 User-Agent 正则表达式非空时有效" msgstr "仅替换 User-Agent 正则匹配的部分,仅在 User-Agent 正则表达式非空时有效"
msgid "Direct Forward"
msgstr "直接转发"
msgid "Directly forward packets without rewriting"
msgstr "不进行重写直接转发数据包"
msgid "User-Agent Rewrite Statistics" msgid "User-Agent Rewrite Statistics"
msgstr "User-Agent 重写次数实时统计" msgstr "User-Agent 重写次数实时统计"

View File

@ -6,7 +6,7 @@ require (
github.com/dlclark/regexp2 v1.11.4 github.com/dlclark/regexp2 v1.11.4
github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
golang.org/x/sys v0.26.0 golang.org/x/sys v0.30.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )

View File

@ -14,8 +14,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=

View File

@ -14,47 +14,51 @@ const (
) )
type Config struct { type Config struct {
ServerMode string ServerMode string
BindAddr string BindAddr string
Port int Port int
ListenAddr string ListenAddr string
LogLevel string LogLevel string
PayloadUA string PayloadUA string
UARegex string UARegex string
EnablePartialReplace bool PartialReplace bool
DirectForward bool
} }
func Parse() (*Config, bool) { func Parse() (*Config, bool) {
var ( var (
serverMode string serverMode string
bindAddr string bindAddr string
port int port int
loglevel string loglevel string
payloadUA string payloadUA string
uaRegx string uaRegx string
partial bool partial bool
showVer bool directForward bool
showVer bool
) )
flag.StringVar(&serverMode, "m", ServerModeSocks5, "server mode: HTTP, SOCKS5, TPROXY, REDIRECT (default: SOCKS5)") flag.StringVar(&serverMode, "m", ServerModeSocks5, "Server mode: HTTP, SOCKS5, TPROXY, REDIRECT")
flag.StringVar(&bindAddr, "b", "127.0.0.1", "bind address (default: 127.0.0.1)") flag.StringVar(&bindAddr, "b", "127.0.0.1", "Bind address")
flag.IntVar(&port, "p", 1080, "port") flag.IntVar(&port, "p", 1080, "Port")
flag.StringVar(&loglevel, "l", "info", "Log level")
flag.StringVar(&payloadUA, "f", "FFF", "User-Agent") flag.StringVar(&payloadUA, "f", "FFF", "User-Agent")
flag.StringVar(&uaRegx, "r", "", "UA-Pattern") flag.StringVar(&uaRegx, "r", "", "User-Agent regex")
flag.BoolVar(&partial, "s", false, "Enable Regex Partial Replace") flag.BoolVar(&partial, "s", false, "Enable regex partial replace")
flag.StringVar(&loglevel, "l", "info", "Log level (default: info)") flag.BoolVar(&directForward, "d", false, "Pure Forwarding (no User-Agent rewriting)")
flag.BoolVar(&showVer, "v", false, "show version") flag.BoolVar(&showVer, "v", false, "Show version")
flag.Parse() flag.Parse()
cfg := &Config{ cfg := &Config{
ServerMode: strings.ToUpper(serverMode), ServerMode: strings.ToUpper(serverMode),
BindAddr: bindAddr, BindAddr: bindAddr,
Port: port, Port: port,
ListenAddr: fmt.Sprintf("%s:%d", bindAddr, port), ListenAddr: fmt.Sprintf("%s:%d", bindAddr, port),
LogLevel: loglevel, LogLevel: loglevel,
PayloadUA: payloadUA, PayloadUA: payloadUA,
UARegex: uaRegx, UARegex: uaRegx,
EnablePartialReplace: partial, DirectForward: directForward,
PartialReplace: partial,
} }
if serverMode == ServerModeRedirect { if serverMode == ServerModeRedirect {
cfg.BindAddr = "0.0.0.0" cfg.BindAddr = "0.0.0.0"

View File

@ -74,7 +74,8 @@ func LogHeader(version string, cfg *config.Config) {
logrus.Infof("Listen on %s", cfg.ListenAddr) logrus.Infof("Listen on %s", cfg.ListenAddr)
logrus.Infof("User-Agent: %s", cfg.PayloadUA) logrus.Infof("User-Agent: %s", cfg.PayloadUA)
logrus.Infof("User-Agent Regex: '%s'", cfg.UARegex) logrus.Infof("User-Agent Regex: '%s'", cfg.UARegex)
logrus.Infof("Partial Replace: %v", cfg.EnablePartialReplace) logrus.Infof("Partial Replace: %v", cfg.PartialReplace)
logrus.Infof("Direct Forward: %v", cfg.DirectForward)
logrus.Infof("Log level: %s", cfg.LogLevel) logrus.Infof("Log level: %s", cfg.LogLevel)
} }

View File

@ -44,7 +44,7 @@ func New(cfg *config.Config) (*Rewriter, error) {
return &Rewriter{ return &Rewriter{
payloadUA: cfg.PayloadUA, payloadUA: cfg.PayloadUA,
pattern: cfg.UARegex, pattern: cfg.UARegex,
partialReplace: cfg.EnablePartialReplace, partialReplace: cfg.PartialReplace,
uaRegex: uaRegex, uaRegex: uaRegex,
Cache: expirable.NewLRU[string, struct{}](1024, nil, 30*time.Minute), Cache: expirable.NewLRU[string, struct{}](1024, nil, 30*time.Minute),
whitelist: []string{ whitelist: []string{

View File

@ -26,9 +26,9 @@ func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
func newTestRewriter(t *testing.T) *Rewriter { func newTestRewriter(t *testing.T) *Rewriter {
cfg := &config.Config{ cfg := &config.Config{
UARegex: "TestUA", UARegex: "TestUA",
PayloadUA: "MockUA/1.0", PayloadUA: "MockUA/1.0",
EnablePartialReplace: false, PartialReplace: false,
} }
rewriter, err := New(cfg) rewriter, err := New(cfg)
assert.NoError(t, err) assert.NoError(t, err)
@ -37,15 +37,15 @@ func newTestRewriter(t *testing.T) *Rewriter {
func TestNewRewriter(t *testing.T) { func TestNewRewriter(t *testing.T) {
cfg := &config.Config{ cfg := &config.Config{
UARegex: "TestUA", UARegex: "TestUA",
PayloadUA: "FFF0", PayloadUA: "FFF0",
EnablePartialReplace: false, PartialReplace: false,
} }
rewriter, err := New(cfg) rewriter, err := New(cfg)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, cfg.PayloadUA, rewriter.payloadUA) assert.Equal(t, cfg.PayloadUA, rewriter.payloadUA)
assert.Equal(t, cfg.UARegex, rewriter.pattern) assert.Equal(t, cfg.UARegex, rewriter.pattern)
assert.Equal(t, cfg.EnablePartialReplace, rewriter.partialReplace) assert.Equal(t, cfg.PartialReplace, rewriter.partialReplace)
assert.NotNil(t, rewriter.uaRegex) assert.NotNil(t, rewriter.uaRegex)
assert.NotNil(t, rewriter.Cache) assert.NotNil(t, rewriter.Cache)
} }

View File

@ -58,10 +58,18 @@ func (s *Server) handleHTTP(w http.ResponseWriter, req *http.Request) {
} }
defer target.Close() defer target.Close()
err = s.rewriteAndForward(target, req, req.Host, req.RemoteAddr) if s.cfg.DirectForward {
if err != nil { err = req.Write(target)
http.Error(w, err.Error(), http.StatusServiceUnavailable) if err != nil {
return http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
} else {
err = s.rewriteAndForward(target, req, req.Host, req.RemoteAddr)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
} }
resp, err := http.ReadResponse(bufio.NewReader(target), req) resp, err := http.ReadResponse(bufio.NewReader(target), req)
if err != nil { if err != nil {
@ -123,6 +131,11 @@ func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
// Server -> Client (raw) // Server -> Client (raw)
go utils.CopyHalf(client, target) go utils.CopyHalf(client, target)
if s.cfg.DirectForward {
// Client -> Server (raw)
go utils.CopyHalf(target, client)
return
}
// Client -> Server (rewriter) // Client -> Server (rewriter)
go utils.ProxyHalf(target, client, s.rw, destAddr) go utils.ProxyHalf(target, client, s.rw, destAddr)
} }

View File

@ -71,6 +71,11 @@ func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
// Server -> Client (raw) // Server -> Client (raw)
go utils.CopyHalf(client, target) go utils.CopyHalf(client, target)
if s.cfg.DirectForward {
// Client -> Server (raw)
go utils.CopyHalf(target, client)
return
}
// Client -> Server (rewriter) // Client -> Server (rewriter)
go utils.ProxyHalf(target, client, s.rw, destAddr) go utils.ProxyHalf(target, client, s.rw, destAddr)
} }

View File

@ -213,6 +213,11 @@ func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
// Server -> Client (raw) // Server -> Client (raw)
go utils.CopyHalf(client, target) go utils.CopyHalf(client, target)
if s.cfg.DirectForward {
// Client -> Server (raw)
go utils.CopyHalf(target, client)
return
}
// Client -> Server (rewriter) // Client -> Server (rewriter)
go utils.ProxyHalf(target, client, s.rw, destAddr) go utils.ProxyHalf(target, client, s.rw, destAddr)
} }

View File

@ -92,6 +92,11 @@ func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
// Server -> Client (raw) // Server -> Client (raw)
go utils.CopyHalf(client, target) go utils.CopyHalf(client, target)
if s.cfg.DirectForward {
// Client -> Server (raw)
go utils.CopyHalf(target, client)
return
}
// Client -> Server (rewriter) // Client -> Server (rewriter)
go utils.ProxyHalf(target, client, s.rw, destAddr) go utils.ProxyHalf(target, client, s.rw, destAddr)
} }