mirror of
https://github.com/SunBK201/UA3F.git
synced 2025-12-16 16:57:08 +00:00
feat: support http mode
This commit is contained in:
parent
29840b64fb
commit
2c8763a918
@ -1,10 +1,10 @@
|
|||||||
# UA3F
|
# UA3F
|
||||||
|
|
||||||
UA3F 是下一代 HTTP User-Agent 重写工具,作为一个 SOCKS5/TPROXY/REDIRECT 服务部署在路由器等设备进行透明 User-Agent 重写。
|
UA3F 是下一代 HTTP User-Agent 重写工具,作为一个 HTTP、SOCKS5、TPROXY、REDIRECT 代理服务对 HTTP 进行 User-Agent 透明重写。
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- 支持多种服务模式:SOCKS5、TPROXY、REDIRECT
|
- 支持多种服务模式:HTTP、SOCKS5、TPROXY、REDIRECT
|
||||||
- 支持正则表达式规则匹配重写 User-Agent
|
- 支持正则表达式规则匹配重写 User-Agent
|
||||||
- 自定义重写 User-Agent 内容
|
- 自定义重写 User-Agent 内容
|
||||||
- LRU 高速缓存非 HTTP 流量,加速非 HTTP 流量转发
|
- LRU 高速缓存非 HTTP 流量,加速非 HTTP 流量转发
|
||||||
@ -56,7 +56,7 @@ UA3F 支持 LuCI Web 页面,可以打开 Services -> UA3F 进行相关配置
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 设置说明:
|
> 设置说明:
|
||||||
>
|
>
|
||||||
> - Server Mode (服务模式): 支持 `SOCKS5`、`TPROXY`、`REDIRECT` 三种模式,默认 `SOCKS5`
|
> - Server Mode (服务模式): 支持 `HTTP`、`SOCKS5`、`TPROXY`、`REDIRECT`,默认 `SOCKS5`
|
||||||
> - Port (监听端口): 默认 `1080`
|
> - Port (监听端口): 默认 `1080`
|
||||||
> - Bind Address (绑定地址): 默认 `0.0.0.0`
|
> - Bind Address (绑定地址): 默认 `0.0.0.0`
|
||||||
> - Log Level (日志等级): 默认 `error`, 如果需要调试排查错误可以设置为 `debug`
|
> - Log Level (日志等级): 默认 `error`, 如果需要调试排查错误可以设置为 `debug`
|
||||||
@ -87,7 +87,7 @@ sudo -u shellcrash /usr/bin/ua3f
|
|||||||
|
|
||||||
相关命令行启动参数:
|
相关命令行启动参数:
|
||||||
|
|
||||||
- `-m <mode>`: 服务模式,支持 SOCKS5、TPROXY、REDIRECT,默认 SOCKS5
|
- `-m <mode>`: 服务模式,支持 HTTP、SOCKS5、TPROXY、REDIRECT,默认 SOCKS5
|
||||||
- `-b <bind addr>`: 自定义绑定监听地址,默认 127.0.0.1
|
- `-b <bind addr>`: 自定义绑定监听地址,默认 127.0.0.1
|
||||||
- `-p <port>`: 端口号,默认 1080
|
- `-p <port>`: 端口号,默认 1080
|
||||||
- `-l <log level>`: 日志等级,默认 info,可选:debug,默认日志位置:`/var/log/ua3f.log`
|
- `-l <log level>`: 日志等级,默认 info,可选:debug,默认日志位置:`/var/log/ua3f.log`
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
ServerModeHTTP = "HTTP"
|
||||||
ServerModeSocks5 = "SOCKS5"
|
ServerModeSocks5 = "SOCKS5"
|
||||||
ServerModeTProxy = "TPROXY"
|
ServerModeTProxy = "TPROXY"
|
||||||
ServerModeRedirect = "REDIRECT"
|
ServerModeRedirect = "REDIRECT"
|
||||||
@ -35,13 +36,13 @@ func Parse() (*Config, bool) {
|
|||||||
showVer bool
|
showVer bool
|
||||||
)
|
)
|
||||||
|
|
||||||
flag.StringVar(&serverMode, "m", ServerModeSocks5, "server mode: SOCKS5, TPROXY, REDIRECT (default: SOCKS5)")
|
flag.StringVar(&serverMode, "m", ServerModeSocks5, "server mode: HTTP, SOCKS5, TPROXY, REDIRECT (default: SOCKS5)")
|
||||||
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 (default: 127.0.0.1)")
|
||||||
flag.IntVar(&port, "p", 1080, "port")
|
flag.IntVar(&port, "p", 1080, "port")
|
||||||
flag.StringVar(&payloadUA, "f", "FFF", "User-Agent")
|
flag.StringVar(&payloadUA, "f", "FFF", "User-Agent")
|
||||||
flag.StringVar(&uaPattern, "r", "", "UA-Pattern")
|
flag.StringVar(&uaPattern, "r", "", "UA-Pattern")
|
||||||
flag.BoolVar(&partial, "s", false, "Enable Regex Partial Replace")
|
flag.BoolVar(&partial, "s", false, "Enable Regex Partial Replace")
|
||||||
flag.StringVar(&loglevel, "l", "error", "Log level (default: error)")
|
flag.StringVar(&loglevel, "l", "info", "Log level (default: info)")
|
||||||
flag.BoolVar(&showVer, "v", false, "show version")
|
flag.BoolVar(&showVer, "v", false, "show version")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
|||||||
@ -83,6 +83,74 @@ func New(cfg *config.Config) (*Rewriter, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RewriteAndForward rewrites the User-Agent header if needed and forwards the request.
|
||||||
|
// Returns pass=true if the request has been forwarded as-is (no rewrite).
|
||||||
|
func (r *Rewriter) RewriteAndForward(dst net.Conn, req *http.Request, destAddr string, srcAddr string) (pass bool, err error) {
|
||||||
|
|
||||||
|
originalUA := req.Header.Get("User-Agent")
|
||||||
|
|
||||||
|
// No UA header: pass-through after writing this first request
|
||||||
|
if originalUA == "" {
|
||||||
|
r.Cache.Add(destAddr, destAddr)
|
||||||
|
log.LogDebugWithAddr(srcAddr, destAddr, "Not found User-Agent, Add LRU Relay Cache")
|
||||||
|
if err = req.Write(dst); err != nil {
|
||||||
|
err = fmt.Errorf("req.Write: %w", err)
|
||||||
|
}
|
||||||
|
pass = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("Original User-Agent: %s", originalUA))
|
||||||
|
|
||||||
|
isWhitelist := r.inWhitelist(originalUA)
|
||||||
|
matches := true
|
||||||
|
if r.pattern != "" {
|
||||||
|
matches, err = r.uaRegex.MatchString(originalUA)
|
||||||
|
if err != nil {
|
||||||
|
log.LogErrorWithAddr(srcAddr, destAddr, fmt.Sprintf("User-Agent Regex Pattern Match Error: %s", err.Error()))
|
||||||
|
matches = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If UA is whitelisted or does not match target pattern, write once then pass-through.
|
||||||
|
if isWhitelist || !matches {
|
||||||
|
if !matches {
|
||||||
|
log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Not Hit User-Agent Regex: %s", originalUA))
|
||||||
|
}
|
||||||
|
if isWhitelist {
|
||||||
|
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("Hit User-Agent Whitelist: %s", originalUA))
|
||||||
|
r.Cache.Add(destAddr, destAddr)
|
||||||
|
}
|
||||||
|
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
|
||||||
|
Host: destAddr,
|
||||||
|
UA: originalUA,
|
||||||
|
})
|
||||||
|
if err = req.Write(dst); err != nil {
|
||||||
|
err = fmt.Errorf("req.Write: %w", err)
|
||||||
|
}
|
||||||
|
pass = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite UA and forward the request (including body)
|
||||||
|
rewritedUA := r.buildNewUA(originalUA)
|
||||||
|
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("Rewrite User-Agent from (%s) to (%s)", originalUA, rewritedUA))
|
||||||
|
req.Header.Set("User-Agent", rewritedUA)
|
||||||
|
if err = req.Write(dst); err != nil {
|
||||||
|
err = fmt.Errorf("req.Write: %w", err)
|
||||||
|
pass = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statistics.AddRewriteRecord(&statistics.RewriteRecord{
|
||||||
|
Host: destAddr,
|
||||||
|
OriginalUA: originalUA,
|
||||||
|
MockedUA: rewritedUA,
|
||||||
|
})
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProxyHTTPOrRaw reads traffic from src and writes to dst.
|
// ProxyHTTPOrRaw reads traffic from src and writes to dst.
|
||||||
// - If target in LRU cache: pass-through (raw).
|
// - If target in LRU cache: pass-through (raw).
|
||||||
// - Else if HTTP: rewrite UA (unless whitelisted or pattern not matched).
|
// - Else if HTTP: rewrite UA (unless whitelisted or pattern not matched).
|
||||||
@ -114,11 +182,12 @@ func (r *Rewriter) ProxyHTTPOrRaw(dst net.Conn, src net.Conn, destAddr string, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
|
var pass bool
|
||||||
|
|
||||||
// HTTP request loop (handles keep-alive)
|
// HTTP request loop (handles keep-alive)
|
||||||
for {
|
for {
|
||||||
isHTTP, err = r.isHTTP(reader)
|
|
||||||
if err != nil {
|
if isHTTP, err = r.isHTTP(reader); err != nil {
|
||||||
err = fmt.Errorf("isHTTP: %w", err)
|
err = fmt.Errorf("isHTTP: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -132,67 +201,13 @@ func (r *Rewriter) ProxyHTTPOrRaw(dst net.Conn, src net.Conn, destAddr string, s
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req, err = http.ReadRequest(reader)
|
if req, err = http.ReadRequest(reader); err != nil {
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("http.ReadRequest: %w", err)
|
err = fmt.Errorf("http.ReadRequest: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if pass, err = r.RewriteAndForward(dst, req, destAddr, srcAddr); pass {
|
||||||
originalUA := req.Header.Get("User-Agent")
|
|
||||||
|
|
||||||
// No UA header: pass-through after writing this first request
|
|
||||||
if originalUA == "" {
|
|
||||||
r.Cache.Add(destAddr, destAddr)
|
|
||||||
log.LogDebugWithAddr(srcAddr, destAddr, "Not found User-Agent, Add LRU Relay Cache")
|
|
||||||
if err = req.Write(dst); err != nil {
|
|
||||||
err = fmt.Errorf("req.Write: %w", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isWhitelist := r.inWhitelist(originalUA)
|
|
||||||
matches := true
|
|
||||||
if r.pattern != "" {
|
|
||||||
matches, err = r.uaRegex.MatchString(originalUA)
|
|
||||||
if err != nil {
|
|
||||||
log.LogErrorWithAddr(srcAddr, destAddr, fmt.Sprintf("User-Agent Regex Pattern Match Error: %s", err.Error()))
|
|
||||||
matches = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If UA is whitelisted or does not match target pattern, write once then pass-through.
|
|
||||||
if isWhitelist || !matches {
|
|
||||||
if !matches {
|
|
||||||
log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Not Hit User-Agent Regex: %s", originalUA))
|
|
||||||
}
|
|
||||||
if isWhitelist {
|
|
||||||
log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Hit User-Agent Whitelist: %s", originalUA))
|
|
||||||
r.Cache.Add(destAddr, destAddr)
|
|
||||||
}
|
|
||||||
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
|
|
||||||
Host: destAddr,
|
|
||||||
UA: originalUA,
|
|
||||||
})
|
|
||||||
if err = req.Write(dst); err != nil {
|
|
||||||
err = fmt.Errorf("req.Write: %w", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rewrite UA and forward the request (including body)
|
|
||||||
log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Hit User-Agent: %s", originalUA))
|
|
||||||
mockedUA := r.buildNewUA(originalUA)
|
|
||||||
req.Header.Set("User-Agent", mockedUA)
|
|
||||||
if err = req.Write(dst); err != nil {
|
|
||||||
err = fmt.Errorf("req.Write: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
statistics.AddRewriteRecord(&statistics.RewriteRecord{
|
|
||||||
Host: destAddr,
|
|
||||||
OriginalUA: originalUA,
|
|
||||||
MockedUA: mockedUA,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
116
src/internal/server/http/http.go
Normal file
116
src/internal/server/http/http.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/sunbk201/ua3f/internal/config"
|
||||||
|
"github.com/sunbk201/ua3f/internal/rewrite"
|
||||||
|
"github.com/sunbk201/ua3f/internal/server/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
cfg *config.Config
|
||||||
|
rw *rewrite.Rewriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg *config.Config, rw *rewrite.Rewriter) *Server {
|
||||||
|
return &Server{
|
||||||
|
cfg: cfg,
|
||||||
|
rw: rw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() (err error) {
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: s.cfg.ListenAddr,
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method == http.MethodConnect {
|
||||||
|
s.handleTunneling(w, req)
|
||||||
|
} else {
|
||||||
|
s.handleHTTP(w, req)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return server.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
req.RequestURI = ""
|
||||||
|
req.URL.Scheme = "http"
|
||||||
|
req.URL.Host = req.Host
|
||||||
|
destPort := req.URL.Port()
|
||||||
|
if destPort == "" {
|
||||||
|
destPort = "80"
|
||||||
|
}
|
||||||
|
destAddr := fmt.Sprintf("%s:%s", req.URL.Hostname(), destPort)
|
||||||
|
|
||||||
|
logrus.Infof("HTTP request for %s", destAddr)
|
||||||
|
|
||||||
|
target, err := utils.Connect(destAddr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer target.Close()
|
||||||
|
|
||||||
|
_, err = s.rw.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)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
for _, vv := range v {
|
||||||
|
w.Header().Add(k, vv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
io.Copy(w, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleTunneling(w http.ResponseWriter, req *http.Request) {
|
||||||
|
logrus.Infof("HTTP CONNECT request for %s", req.Host)
|
||||||
|
destAddr := req.Host
|
||||||
|
dest, err := utils.Connect(destAddr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hijacker, ok := w.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, _, err := hijacker.Hijack()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
|
||||||
|
s.ForwardTCP(client, dest, destAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) HandleClient(client net.Conn) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForwardTCP proxies traffic in both directions.
|
||||||
|
// target->client uses raw copy.
|
||||||
|
// client->target is processed by the rewriter (or raw if cached).
|
||||||
|
func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
|
||||||
|
// Server -> Client (raw)
|
||||||
|
go utils.CopyHalf(client, target)
|
||||||
|
|
||||||
|
// Client -> Server (rewriter)
|
||||||
|
go utils.ProxyHalf(target, client, s.rw, destAddr)
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/sunbk201/ua3f/internal/config"
|
"github.com/sunbk201/ua3f/internal/config"
|
||||||
"github.com/sunbk201/ua3f/internal/rewrite"
|
"github.com/sunbk201/ua3f/internal/rewrite"
|
||||||
|
"github.com/sunbk201/ua3f/internal/server/http"
|
||||||
"github.com/sunbk201/ua3f/internal/server/redirect"
|
"github.com/sunbk201/ua3f/internal/server/redirect"
|
||||||
"github.com/sunbk201/ua3f/internal/server/socks5"
|
"github.com/sunbk201/ua3f/internal/server/socks5"
|
||||||
"github.com/sunbk201/ua3f/internal/server/tproxy"
|
"github.com/sunbk201/ua3f/internal/server/tproxy"
|
||||||
@ -19,6 +20,8 @@ type Server interface {
|
|||||||
|
|
||||||
func NewServer(cfg *config.Config, rw *rewrite.Rewriter) (Server, error) {
|
func NewServer(cfg *config.Config, rw *rewrite.Rewriter) (Server, error) {
|
||||||
switch cfg.ServerMode {
|
switch cfg.ServerMode {
|
||||||
|
case config.ServerModeHTTP:
|
||||||
|
return http.New(cfg, rw), nil
|
||||||
case config.ServerModeSocks5:
|
case config.ServerModeSocks5:
|
||||||
return socks5.New(cfg, rw), nil
|
return socks5.New(cfg, rw), nil
|
||||||
case config.ServerModeTProxy:
|
case config.ServerModeTProxy:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user