2025-12-25

This commit is contained in:
Qin tian
2025-12-25 05:43:38 +08:00
parent c2d349b2d6
commit 14c65602d2
6 changed files with 850 additions and 455 deletions

View File

@@ -226,11 +226,11 @@ func HTTPSendBin(Context int, data []byte) {
random = public.GetTLSValues
}
k.respBody = nil
resp, _, _, f := httpClient.Do(k.req, k.proxy, k.redirect, k.tlsConfig, time.Duration(k.outTime)*time.Millisecond, random, nil)
r := httpClient.Do(k.req, k.proxy, k.redirect, k.tlsConfig, time.Duration(k.outTime)*time.Millisecond, random, nil)
//resp, _, _, f
defer func() {
if f != nil && resp != nil {
f()
if r.Close != nil && r.Response != nil {
r.Close()
}
}()
if k.resp != nil {
@@ -238,7 +238,7 @@ func HTTPSendBin(Context int, data []byte) {
_ = k.resp.Body.Close()
}
}
k.resp = resp
k.resp = r.Response
if k.resp != nil {
if k.resp.Body != nil {
i, _ := io.ReadAll(k.resp.Body)

View File

@@ -1040,17 +1040,13 @@ func (s *proxyRequest) doRequest() error {
if s.Request.URL == nil {
return errors.New("request.url is nil")
}
var do *http.Response
var n net.Conn
var err error
var Close func()
do, n, err, Close = httpClient.Do(s.Request, s.Proxy, false, s.TlsConfig, s.SendTimeout, s.getTLSValues, s.Conn)
if err == nil && do != nil {
r := httpClient.Do(s.Request, s.Proxy, false, s.TlsConfig, s.SendTimeout, s.getTLSValues, s.Conn)
if r.Err == nil && r.Response != nil {
if s.rawTarget != 0 {
if s.Request.URL.Scheme != "https" {
s.Global.cache.updateType(s.rawTarget, whoisNoHTTPS)
} else {
if do.ProtoMajor == 2 {
if r.Response.ProtoMajor == 2 {
s.Global.cache.updateType(s.rawTarget, whoisHTTPS2)
} else {
s.Global.cache.updateType(s.rawTarget, whoisHTTPS1)
@@ -1058,16 +1054,16 @@ func (s *proxyRequest) doRequest() error {
}
}
}
s.Response.Conn = n
s.Response.Conn = r.Conn
ip, _ := s.Request.Context().Value(public.SunnyNetServerIpTags).(string)
if ip != "" {
s.Response.ServerIP = ip
} else {
s.Response.ServerIP = "unknown"
}
s.Response.Response = do
s.Response.Close = Close
return err
s.Response.Response = r.Response
s.Response.Close = r.Close
return r.Err
}
func (s *proxyRequest) sendHttps(req *http.Request) {
s.Target.Parse(req.Host, public.HttpsDefaultPort)
@@ -1094,8 +1090,11 @@ func (s *proxyRequest) https() {
}
//是否开启了强制走TCP And 如果是DNS请求则不用判断了直接强制走TCP
if (s.Global.isMustTcp || s.Target.Port == 853) && !s.targetIsInterfaceAdders() {
if s.Global.disableTCP {
return
if !loop.IsFilterConn(s.Conn) {
if s.Global.disableTCP {
return
}
}
s.NoRepairHttp = true
//开启了强制走TCP则按TCP流程处理
@@ -2570,8 +2569,10 @@ func (s *Sunny) handleClientConn(conn net.Conn) {
}
}
if s.isMustTcp && !req.targetIsInterfaceAdders() {
if s.disableTCP {
return
if !loop.IsFilterConn(req.Conn) {
if s.disableTCP {
return
}
}
//如果开启了强制走TCP 则按TCP处理流程处理
req.MustTcpProcessing(public.TagMustTCP)

View File

@@ -3,16 +3,13 @@ package httpClient
import (
"context"
"errors"
"fmt"
"net"
"strings"
"sync"
"time"
"unsafe"
"github.com/qtgolang/SunnyNet/src/SunnyProxy"
tls "github.com/qtgolang/SunnyNet/src/crypto/tls"
"github.com/qtgolang/SunnyNet/src/dns"
"github.com/qtgolang/SunnyNet/src/http"
"github.com/qtgolang/SunnyNet/src/loop"
"github.com/qtgolang/SunnyNet/src/public"
@@ -36,178 +33,97 @@ func init() {
}
}()
}
func Do(req *http.Request, RequestProxy *SunnyProxy.Proxy, CheckRedirect bool, config *tls.Config, outTime time.Duration, GetTLSValues func() []uint16, MConn net.Conn) (Response *http.Response, Conn net.Conn, err error, Close func()) {
if req.ProtoMajor == 2 {
switch req.Method {
case public.HttpMethodHEAD, public.HttpMethodGET, public.HttpMethodTRACE, public.HttpMethodOPTIONS:
if req.Body != nil {
_ = req.Body.Close()
req.Body = nil
}
}
}
{
if req != nil && req.Header != nil {
Cookies := req.Header.GetArray("Cookie")
if len(Cookies) > 1 {
req.Header.Set("Cookie", strings.Join(Cookies, "; "))
}
}
}
cfg := config.Clone()
if req.URL != nil && req.URL.Scheme != "http" {
if cfg == nil {
cfg = &tls.Config{}
}
cfg.InsecureSkipVerify = true
}
_hashCode := public.SumHashCode(req.Host)
_mustHTTP11_lock.Lock()
if _mustHTTP11[_hashCode] != nil {
cfg.NextProtos = public.HTTP1NextProtos
x := time.Now()
_mustHTTP11[_hashCode] = &x
}
_mustHTTP11_lock.Unlock()
handshakeCount := 0
for {
if cfg != nil && GetTLSValues != nil {
tv := GetTLSValues()
if len(tv) > 0 {
cfg.CipherSuites = tv
}
}
Response, Conn, err, Close = do(req, RequestProxy, CheckRedirect, cfg, outTime, MConn)
if err != nil {
if Conn != nil {
_ = Conn.Close()
func Do(req *http.Request, RequestProxy *SunnyProxy.Proxy, CheckRedirect bool, config *tls.Config, outTime time.Duration, GetTLSValues func() []uint16, MConn net.Conn) Result {
return DoOptions(req, Options{
RequestProxy: RequestProxy,
CheckRedirect: CheckRedirect,
TLSConfig: config,
OutTime: outTime,
GetTLSValues: GetTLSValues,
MConn: MConn,
})
}
// DoOptions 优化参数/返回值与拆分函数
func DoOptions(req *http.Request, opt Options) (r Result) {
normalizeHTTP2Body(req) // HTTP/2 下清理不允许携带 Body 的方法
normalizeCookieHeader(req) // 合并多个 Cookie 头为一个
cfg := buildTLSConfig(req, Options(opt)) // 克隆并构建 TLS 配置
_hashCode := applyMustHTTP11(req.Host, cfg) // 根据 host 命中缓存时强制 HTTP/1.1
handshakeCount := 0 // 握手/连接类错误计数
for { // 重试循环开始
applyTLSValues(cfg, opt) // 动态设置 CipherSuites
dr := do(doArgs{ // 调用底层 do 执行一次请求
req: req, // 请求对象
RequestProxy: opt.RequestProxy, // 代理配置
CheckRedirect: opt.CheckRedirect, // 是否允许重定向
config: cfg, // TLS 配置
outTime: opt.OutTime, // 超时时间
MConn: opt.MConn, // 客户端连接
Event: opt.Event,
})
r.Response, r.Conn, r.Err, r.Close = dr.resp, dr.conn, dr.err, dr.closeFn // 拆包结果
if r.Err != nil { // 如果请求出错
closeConnOnErr(r.Conn) // 出错时关闭底层连接
if needDowngradeHTTP11(r.Err, cfg, _hashCode) { // HTTP2 stream error 降级 HTTP/1.1
continue // 继续下一次重试
}
ers := err.Error()
if strings.Contains(ers, "stream error: stream ID") && len(cfg.NextProtos) == 2 {
cfg.NextProtos = public.HTTP1NextProtos
_mustHTTP11_lock.Lock()
x := time.Now()
_mustHTTP11[_hashCode] = &x
_mustHTTP11_lock.Unlock()
continue
shouldRetry, shouldReturn := handleRetryableHandshakeError(req, r.Err, &handshakeCount) // 处理握手/连接/EOF 错误
if shouldReturn { // 达到最大重试次数
r.Close = nil // 禁用 Close 回调
return // 直接返回
}
//Get "https://www.zjwubei.com:520/": stream error: stream ID 1; CANCEL; received from peer
if strings.Contains(ers, "handshake") || strings.Contains(ers, "connection") || strings.Contains(ers, "EOF") {
handshakeCount++
if handshakeCount > 10 {
Close = nil
return
}
if strings.Contains(ers, "EOF") && handshakeCount > 3 {
if req.IsSetHTTP2Config() {
req.SetHTTP2Config(nil)
}
}
continue
if shouldRetry { // 仍可重试
continue // 进入下一轮
}
}
return
return // 成功或不可重试错误直接返回
}
}
func do(req *http.Request, RequestProxy *SunnyProxy.Proxy, CheckRedirect bool, config *tls.Config, outTime time.Duration, MConn net.Conn) (*http.Response, net.Conn, error, func()) {
if req != nil {
if req.Header != nil {
//请求时,删除协议头中的长度,请求时会自动添加
ContentLengthName := ""
var ContentLengthValue []string
sName := "Content-Length"
for k, v := range req.Header {
if strings.EqualFold(k, sName) {
ContentLengthName = k //保留原本的大小写名称
ContentLengthValue = v
break
}
}
if ContentLengthName != "" {
req.Header.Del(sName)
defer func() {
req.Header.Del(sName)
req.Header.SetArray(sName, ContentLengthValue)
}()
}
}
func do(a doArgs) (r doResult) {
req := a.req // 请求对象
RequestProxy := a.RequestProxy // 代理配置
CheckRedirect := a.CheckRedirect // 是否允许重定向
config := a.config // TLS 配置
outTime := a.outTime // 超时时间
MConn := a.MConn // 客户端连接
if restore := stripContentLengthHeader(req); restore != nil { // 移除 Content-Length 头
defer restore() // 函数返回前恢复 Content-Length
}
SendTimeout := 30 * 1000 * time.Millisecond
outTime = SendTimeout
client := httpClientGet(req, RequestProxy, config, outTime)
if CheckRedirect {
client.Client.CheckRedirect = public.HTTPAllowRedirect
} else {
client.Client.CheckRedirect = public.HTTPBanRedirect
if outTime < 100*time.Millisecond { // 超时时间过小
outTime = 30 * time.Second // 使用默认 30 秒
}
if MConn != nil {
//防止客户端与 SunnyNet 断开连接,但是 SunnyNet 与 目标服务器 一直交互
ticker := time.NewTicker(3 * time.Second)
stop := make(chan struct{}) // 退出信号
var mu sync.WaitGroup
mu.Add(1) // 提前加 1确保 Done() 被执行
Cancel := req.WithCancel()
go func() {
defer mu.Done()
ms := make([]byte, 1)
for {
select {
case <-ticker.C:
_ = MConn.SetReadDeadline(time.Now().Add(1 * time.Millisecond))
_, er := MConn.Read(ms)
if er != nil {
if strings.Contains(er.Error(), "close") {
if client.Conn != nil {
Conn := client.Conn
_ = Conn.Close()
} else {
Cancel()
}
}
}
case <-stop: // 监听退出信号
return
}
}
}()
defer func() {
ticker.Stop()
close(stop)
mu.Wait()
_ = MConn.SetDeadline(time.Time{})
}()
client := httpClientGet(req, RequestProxy, config, outTime, a.Event) // 获取 HTTP 客户端
applyRedirectPolicy(client, CheckRedirect) // 设置重定向策略
cleanup := watchClientConn(req, client, MConn) // 启动客户端连接监控
defer cleanup() // 函数返回时停止监控并清理
stripTEForHTTP2(client, req) // HTTP/2 场景下删除 TE 头
r.resp, r.err = client.Client.Do(req) // 发起 HTTP 请求
if errors.Is(r.err, context.Canceled) { // 判断是否为取消错误
r.err = httpCancel // 转换为内部取消错误
}
if client.h2 && req != nil {
//部分HTTP2服务器不支持此协议头,导致出现协议错误
req.Header.Del("TE")
}
reqs, err := client.Client.Do(req)
if errors.Is(err, context.Canceled) {
err = httpCancel
}
var rConn net.Conn
if client.Conn != nil {
rConn = client.Conn
}
address, proxy, _ := net.SplitHostPort(client.RequestProxy.DialAddr)
if req != nil {
ip := net.ParseIP(address)
if ip == nil {
req.SetContext(public.SunnyNetServerIpTags, client.RequestProxy.DialAddr)
} else {
req.SetContext(public.SunnyNetServerIpTags, SunnyProxy.FormatIP(ip, proxy))
}
}
return reqs, rConn, err, func() {
if err != nil {
return
}
httpClientPop(client)
if client.Conn != nil { // 如果底层连接存在
r.conn = client.Conn // 返回该连接
}
setServerIPTag(req, client.RequestProxy.DialAddr) // 写入服务端 IP 信息到 context
r.closeFn = buildCloseFn(client, r.err) // 构建 Close 回调函数
return // 返回 doResult
}
var httpCancel = errors.New("客户端取消请求")
@@ -216,237 +132,38 @@ var httpClientMap map[uint32]clientList
type clientList map[uintptr]*clientPart
func httpClientGet(req *http.Request, Proxy *SunnyProxy.Proxy, cfg *tls.Config, timeout time.Duration) *clientPart {
func httpClientGet(req *http.Request, Proxy *SunnyProxy.Proxy, cfg *tls.Config, timeout time.Duration, event Event) *clientPart {
httpLock.Lock()
defer httpLock.Unlock()
outRouterIP, _ := req.Context().Value(public.OutRouterIPKey).(*net.TCPAddr)
s := dns.GetDnsServer()
if outRouterIP != nil {
s += outRouterIP.String() + "|"
} else {
s += "|"
}
if req != nil && req.URL != nil {
s += req.URL.Host + "|" + req.Proto + "|" + req.URL.Scheme
}
s += "|" + Proxy.String() + "|"
if cfg != nil {
if len(cfg.NextProtos) < 1 {
cfg.NextProtos = []string{http.H11Proto, http.H2Proto}
}
s += strings.Join(cfg.NextProtos, "-")
}
hash := public.SumHashCode(s)
if clients, ok := httpClientMap[hash]; ok {
if len(clients) > 0 {
for key, client := range clients {
delete(clients, key)
var nproxy *SunnyProxy.Proxy
if Proxy != nil {
nproxy = Proxy.Clone()
} else {
nproxy = new(SunnyProxy.Proxy)
}
if client.RequestProxy != nil {
nproxy.DialAddr = client.RequestProxy.DialAddr
}
client.RequestProxy = nproxy
if client.Conn != nil {
Conn := client.Conn
if timeout == 0 {
_ = Conn.SetDeadline(time.Time{})
_ = Conn.SetWriteDeadline(time.Time{})
_ = Conn.SetDeadline(time.Time{})
} else {
_ = Conn.SetDeadline(time.Now().Add(timeout))
_ = Conn.SetWriteDeadline(time.Now().Add(timeout))
_ = Conn.SetDeadline(time.Now().Add(timeout))
}
client.Client.Timeout = 24 * time.Hour
client.Transport.ResponseHeaderTimeout = 24 * time.Hour // 读取响应头超时
client.Transport.IdleConnTimeout = 24 * time.Hour // 空闲连接超时
client.Transport.TLSHandshakeTimeout = 24 * time.Hour // TLS 握手超时
}
return client
}
}
}
if cfg != nil {
if len(cfg.NextProtos) > 0 {
cfg.GetConfigForServer = func(info *tls.ServerHelloMsg) error {
for _, proto := range cfg.NextProtos {
if proto == http.H2Proto && info.SupportedVersion == 772 {
return nil // 如果支持,则返回 nil
}
if proto == http.H11Proto && (info.SupportedVersion == 0 || info.Vers == 771) {
return nil // 如果支持,则返回 nil
}
}
ver := info.SupportedVersion
if ver == 0 {
ver = info.Vers
}
Proto, _ := http.ProtoVersions[info.Vers]
if Proto == "" {
return fmt.Errorf("服务器不支持您所选HTTP协议版本")
}
return fmt.Errorf("服务器不支持您所选HTTP协议版本: 需要协议[%s],请检查您的配置", strings.ToUpper(Proto))
}
}
}
Tr := &http.Transport{TLSClientConfig: cfg}
if timeout == 0 {
Tr.ResponseHeaderTimeout = 60 * time.Second // 读取响应头超时
Tr.IdleConnTimeout = 60 * time.Second // 空闲连接超时
Tr.TLSHandshakeTimeout = 60 * time.Second // TLS 握手超时
} else {
Tr.ResponseHeaderTimeout = timeout // 读取响应头超时
Tr.IdleConnTimeout = timeout // 空闲连接超时
Tr.TLSHandshakeTimeout = timeout // TLS 握手超时
}
h2 := false
if cfg != nil {
if len(cfg.NextProtos) < 1 {
configureHTTP2Transport(Tr, cfg)
h2 = true
} else {
for _, proto := range cfg.NextProtos {
if proto == http.H2Proto {
configureHTTP2Transport(Tr, cfg)
h2 = true
break
}
}
}
}
var ips []net.IP
var isLookupIP bool
var ProxyHost string
var LookupIPdial func(network string, addr string, OutRouterIP *net.TCPAddr) (net.Conn, error)
var nproxy *SunnyProxy.Proxy
var LookupIPproxy *SunnyProxy.Proxy
if Proxy != nil {
nproxy = Proxy.Clone()
LookupIPproxy = Proxy.Clone()
LookupIPdial = LookupIPproxy.Dial
ProxyHost = Proxy.Host
} else {
nproxy = new(SunnyProxy.Proxy)
LookupIPdial = LookupIPproxy.Dial
}
cc := http.Client{Transport: Tr, Timeout: timeout}
res := &clientPart{Client: cc, key: hash, RequestProxy: nproxy, Transport: Tr, h2: h2}
if outRouterIP != nil {
res.outRouterIP = &net.TCPAddr{IP: outRouterIP.IP}
}
Tr.DialContext = func(ctx context.Context, network, addr string) (cnn net.Conn, _ error) {
defer func() {
if cnn != nil {
res.Conn = cnn
loop.Add(cnn)
if timeout != 0 {
_ = cnn.SetDeadline(time.Now().Add(timeout))
_ = cnn.SetWriteDeadline(time.Now().Add(timeout))
_ = cnn.SetDeadline(time.Now().Add(timeout))
} else {
_ = cnn.SetDeadline(time.Time{})
_ = cnn.SetWriteDeadline(time.Time{})
_ = cnn.SetDeadline(time.Time{})
}
Tr.ResponseHeaderTimeout = 24 * time.Hour // 读取响应头超时
Tr.IdleConnTimeout = 24 * time.Hour // 空闲连接超时
Tr.TLSHandshakeTimeout = 24 * time.Hour // TLS 握手超时
cc.Timeout = 24 * time.Hour
}
}()
serveripFunc, ok := req.Context().Value(public.Connect_Raw_Address).(func() string)
if ok && serveripFunc != nil {
_serverIP_ := serveripFunc()
if _serverIP_ != "" {
address2, _, err2 := net.SplitHostPort(_serverIP_)
if err2 == nil {
ip := net.ParseIP(address2)
if ip != nil {
conn, er := res.RequestProxy.DialWithTimeout(network, _serverIP_, 3*time.Second, res.outRouterIP)
if conn != nil {
return conn, er
}
}
}
}
}
if dns.IsRemoteDnsServer() {
conn, er := res.RequestProxy.DialWithTimeout(network, addr, 3*time.Second, res.outRouterIP)
return conn, er
}
address, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
i := net.ParseIP(address)
if i != nil {
if len(i) == net.IPv4len {
return res.RequestProxy.Dial(network, i.String()+":"+port, res.outRouterIP)
}
return res.RequestProxy.Dial(network, fmt.Sprintf("[%s]:%s", address, port), res.outRouterIP)
}
if strings.ToLower(address) == "localhost" {
return res.RequestProxy.Dial(network, "127.0.0.1:"+port, res.outRouterIP)
}
var retries bool
for {
if !isLookupIP {
isLookupIP = true
first := dns.GetFirstIP(address, ProxyHost)
if first != nil {
if first.To4() != nil {
return res.RequestProxy.Dial(network, fmt.Sprintf("%s:%s", first.String(), port), res.outRouterIP)
} else {
return res.RequestProxy.Dial(network, fmt.Sprintf("[%s]:%s", first.String(), port), res.outRouterIP)
}
}
ips, _ = dns.LookupIP(address, ProxyHost, res.outRouterIP, LookupIPdial)
if len(ips) < 1 {
return nil, noIP
}
}
if len(ips) < 1 {
dns.SetFirstIP(address, ProxyHost, nil)
if retries {
return nil, connectionFailed
}
isLookupIP = false
retries = true
continue
}
var AllLocalIP = true
for _, ip := range ips {
if ip.String() != "127.0.0.1" {
AllLocalIP = false
break
}
}
if AllLocalIP && len(ips) != 0 {
return nil, errors.New(fmt.Sprintf("Address [%s] points to 127.0.0.1", address))
}
ip := extractAndRemoveIP(&ips)
if ip != nil && ip.String() != "127.0.0.1" {
if ip.To4() != nil {
conn, er := res.RequestProxy.DialWithTimeout(network, fmt.Sprintf("%s:%s", ip.String(), port), 2*time.Second, res.outRouterIP)
if conn != nil {
dns.SetFirstIP(address, ProxyHost, ip)
return conn, er
}
continue
}
conn, er := res.RequestProxy.DialWithTimeout(network, fmt.Sprintf("[%s]:%s", ip.String(), port), 2*time.Second, res.outRouterIP)
if conn != nil {
dns.SetFirstIP(address, ProxyHost, ip)
return conn, er
}
}
}
outRouterIP := getOutRouterIP(req) //从上下文取出出口路由IP
keyStr := buildHTTPClientKey(req, Proxy, cfg, outRouterIP) //构建客户端缓存key字符串
hash := public.SumHashCode(keyStr) //计算hash
if c := tryPopCachedClient(hash, Proxy, timeout); c != nil { //尝试从缓存取出可复用客户端
return c
}
applyTLSProtoCheck(cfg) //设置TLS协议版本校验逻辑
Tr := newTransport(cfg, timeout) //创建Transport并设置超时
h2 := configureH2IfNeeded(Tr, cfg) //按NextProtos配置HTTP2
nproxy, lookupProxy, lookupDial, proxyHost := buildProxySet(Proxy) //构建代理对象与LookupIP拨号器
cc := http.Client{Transport: Tr, Timeout: timeout} //创建HTTP客户端
res := newClientPart(cc, Tr, hash, nproxy, h2, outRouterIP) //创建clientPart
bindDialContext(dialCtxArgs{
Tr: Tr,
Req: req,
Res: res,
Timeout: timeout,
Event: event,
ProxyHost: proxyHost,
LookupDial: lookupDial,
})
_ = lookupProxy //保持原有变量结构,不改变逻辑意图
return res
}
@@ -484,7 +201,7 @@ func configureHTTP2Transport(Tr *http.Transport, cfg *tls.Config) {
// 如果找到了 HTTP/2.0 协议,则配置 HTTP/2.0 传输
if protoFound || len(cfg.NextProtos) == 0 {
http.HTTP2configureTransport(Tr)
_, _ = http.HTTP2configureTransport(Tr)
}
}

636
src/httpClient/utlis.go Normal file
View File

@@ -0,0 +1,636 @@
package httpClient
import (
"context"
"errors"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/qtgolang/SunnyNet/src/SunnyProxy"
"github.com/qtgolang/SunnyNet/src/crypto/tls"
"github.com/qtgolang/SunnyNet/src/dns"
"github.com/qtgolang/SunnyNet/src/http"
"github.com/qtgolang/SunnyNet/src/loop"
"github.com/qtgolang/SunnyNet/src/public"
)
type Event struct {
Connection func(conn net.Conn)
Read func(conn net.Conn, bs []byte)
Write func(conn net.Conn, bs []byte)
Close func(conn net.Conn)
}
type hConn struct {
net.Conn
Event
}
func (h hConn) Read(b []byte) (n int, err error) {
n, e := h.Conn.Read(b)
if h.Event.Read != nil {
h.Event.Read(h.Conn, b[:n])
}
return n, e
}
func (h hConn) Write(b []byte) (n int, err error) {
if h.Event.Write != nil {
h.Event.Write(h.Conn, b)
}
return h.Conn.Write(b)
}
func (h hConn) Close() error {
if h.Event.Close != nil {
h.Event.Close(h.Conn)
}
return h.Conn.Close()
}
func (h hConn) LocalAddr() net.Addr {
return h.Conn.LocalAddr()
}
func (h hConn) RemoteAddr() net.Addr {
return h.Conn.RemoteAddr()
}
func newHConn(event Event, conn net.Conn) net.Conn {
if conn == nil {
return nil
}
if event.Connection != nil {
event.Connection(conn)
}
return &hConn{Event: event, Conn: conn}
}
// Options 收敛DoOptions的参数
type Options struct {
RequestProxy *SunnyProxy.Proxy //代理配置
CheckRedirect bool //是否允许重定向
TLSConfig *tls.Config //TLS配置
OutTime time.Duration //超时(注意: do里会强制覆盖成30s)
GetTLSValues func() []uint16 //动态CipherSuites
MConn net.Conn //客户端连接(用于探测断开)
Event Event
}
// Result 收敛DoOptions的返回值
type Result struct {
Response *http.Response //响应
Conn net.Conn //底层连接
Err error //错误
Close func() //成功时归还连接池
}
// doArgs 收敛do的参数
type doArgs struct {
req *http.Request //请求
RequestProxy *SunnyProxy.Proxy //代理配置
CheckRedirect bool //是否允许重定向
config *tls.Config //TLS配置
outTime time.Duration //超时(会在do里覆盖)
MConn net.Conn //客户端连接
Event Event
}
// doResult 收敛do的返回值
type doResult struct {
resp *http.Response //响应
conn net.Conn //连接
err error //错误
closeFn func() //成功时归还
}
// 移除Content-Length并在defer中恢复(保持原逻辑)
func stripContentLengthHeader(req *http.Request) func() {
if req == nil || req.Header == nil {
return nil
}
ContentLengthName := ""
var ContentLengthValue []string
sName := "Content-Length"
for k, v := range req.Header {
if strings.EqualFold(k, sName) {
ContentLengthName = k //保留原本的大小写名称
ContentLengthValue = v
break
}
}
if ContentLengthName == "" {
return nil
}
req.Header.Del(sName)
return func() {
req.Header.Del(sName)
req.Header.SetArray(sName, ContentLengthValue)
}
}
// 设置重定向策略(保持原逻辑)
func applyRedirectPolicy(client *clientPart, checkRedirect bool) {
if checkRedirect {
client.Client.CheckRedirect = public.HTTPAllowRedirect
} else {
client.Client.CheckRedirect = public.HTTPBanRedirect
}
}
// HTTP2下删除TE头(保持原逻辑)
func stripTEForHTTP2(client *clientPart, req *http.Request) {
if client.h2 && req != nil {
req.Header.Del("TE")
}
}
// 写入SunnyNetServerIpTags(保持原逻辑)
func setServerIPTag(req *http.Request, dialAddr string) {
address, proxy, _ := net.SplitHostPort(dialAddr)
if req != nil {
ip := net.ParseIP(address)
if ip == nil {
req.SetContext(public.SunnyNetServerIpTags, dialAddr)
} else {
req.SetContext(public.SunnyNetServerIpTags, SunnyProxy.FormatIP(ip, proxy))
}
}
}
// 监控MConn断开并清理资源(保持原逻辑)
func watchClientConn(req *http.Request, client *clientPart, MConn net.Conn) func() {
if MConn == nil {
return func() {}
}
ticker := time.NewTicker(3 * time.Second)
stop := make(chan struct{}) // 退出信号
var mu sync.WaitGroup
mu.Add(1) // 提前加 1确保 Done() 被执行
Cancel := req.WithCancel()
go func() {
defer mu.Done()
ms := make([]byte, 1)
for {
select {
case <-ticker.C:
_ = MConn.SetReadDeadline(time.Now().Add(1 * time.Millisecond))
_, er := MConn.Read(ms)
if er != nil {
if strings.Contains(er.Error(), "close") {
if client.Conn != nil {
Conn := client.Conn
_ = Conn.Close()
} else {
Cancel()
}
}
}
case <-stop: // 监听退出信号
return
}
}
}()
return func() {
ticker.Stop()
close(stop)
mu.Wait()
_ = MConn.SetDeadline(time.Time{})
}
}
// 构建do的closeFn(保持原逻辑)
func buildCloseFn(client *clientPart, err error) func() {
return func() {
if err != nil {
return
}
httpClientPop(client)
}
}
// 处理HTTP2下某些方法的Body清理
func normalizeHTTP2Body(req *http.Request) {
if req.ProtoMajor == 2 {
switch req.Method {
case public.HttpMethodHEAD, public.HttpMethodGET, public.HttpMethodTRACE, public.HttpMethodOPTIONS:
if req.Body != nil {
_ = req.Body.Close()
req.Body = nil
}
}
}
}
// 合并重复的Cookie头
func normalizeCookieHeader(req *http.Request) {
if req != nil && req.Header != nil {
Cookies := req.Header.GetArray("Cookie")
if len(Cookies) > 1 {
req.Header.Set("Cookie", strings.Join(Cookies, "; "))
}
}
}
// 克隆并按Scheme调整TLS配置
func buildTLSConfig(req *http.Request, opt Options) *tls.Config {
cfg := opt.TLSConfig.Clone()
if req.URL != nil && req.URL.Scheme != "http" {
if cfg == nil {
cfg = &tls.Config{}
}
cfg.InsecureSkipVerify = true
}
return cfg
}
// 按mustHTTP11缓存强制HTTP/1.1并刷新时间
func applyMustHTTP11(host string, cfg *tls.Config) uint32 {
_hashCode := public.SumHashCode(host)
_mustHTTP11_lock.Lock()
if _mustHTTP11[_hashCode] != nil {
cfg.NextProtos = public.HTTP1NextProtos
x := time.Now()
_mustHTTP11[_hashCode] = &x
}
_mustHTTP11_lock.Unlock()
return _hashCode
}
// 每次循环动态刷新CipherSuites
func applyTLSValues(cfg *tls.Config, opt Options) {
if cfg != nil && opt.GetTLSValues != nil {
tv := opt.GetTLSValues()
if len(tv) > 0 {
cfg.CipherSuites = tv
}
}
}
// 处理错误是否需要HTTP2->HTTP/1.1降级并重试
func needDowngradeHTTP11(err error, cfg *tls.Config, hashCode uint32) bool {
ers := err.Error()
if strings.Contains(ers, "stream error: stream ID") && len(cfg.NextProtos) == 2 {
cfg.NextProtos = public.HTTP1NextProtos
_mustHTTP11_lock.Lock()
x := time.Now()
_mustHTTP11[hashCode] = &x
_mustHTTP11_lock.Unlock()
return true
}
return false
}
// 处理握手/连接/EOF类错误是否需要继续重试以及是否清理HTTP2Config
func handleRetryableHandshakeError(req *http.Request, err error, handshakeCount *int) (shouldRetry bool, shouldReturn bool) {
ers := err.Error()
if strings.Contains(ers, "handshake") || strings.Contains(ers, "connection") || strings.Contains(ers, "EOF") {
*handshakeCount++
if *handshakeCount > 10 {
return false, true
}
if strings.Contains(ers, "EOF") && *handshakeCount > 3 {
if req.IsSetHTTP2Config() {
req.SetHTTP2Config(nil)
}
}
return true, false
}
return false, false
}
// 请求失败时关闭连接
func closeConnOnErr(conn net.Conn) {
if conn != nil {
_ = conn.Close()
}
}
func getOutRouterIP(req *http.Request) *net.TCPAddr { //读取出口路由IP
if req == nil {
return nil
}
outRouterIP, _ := req.Context().Value(public.OutRouterIPKey).(*net.TCPAddr)
return outRouterIP
}
func buildHTTPClientKey(req *http.Request, Proxy *SunnyProxy.Proxy, cfg *tls.Config, outRouterIP *net.TCPAddr) string { //构建缓存key
s := dns.GetDnsServer()
if outRouterIP != nil {
s += outRouterIP.String() + "|"
} else {
s += "|"
}
if req != nil && req.URL != nil {
s += req.URL.Host + "|" + req.Proto + "|" + req.URL.Scheme
}
s += "|" + Proxy.String() + "|"
if cfg != nil {
if len(cfg.NextProtos) < 1 {
cfg.NextProtos = []string{http.H11Proto, http.H2Proto}
}
s += strings.Join(cfg.NextProtos, "-")
}
return s
}
func tryPopCachedClient(hash uint32, Proxy *SunnyProxy.Proxy, timeout time.Duration) *clientPart { //从缓存取client
if clients, ok := httpClientMap[hash]; ok {
if len(clients) > 0 {
for key, client := range clients {
delete(clients, key)
refreshClientProxy(client, Proxy) //刷新代理参数
refreshClientConnDeadline(client, timeout) //刷新连接deadline与transport超时
return client
}
}
}
return nil
}
func refreshClientProxy(client *clientPart, Proxy *SunnyProxy.Proxy) { //刷新RequestProxy
var nproxy *SunnyProxy.Proxy
if Proxy != nil {
nproxy = Proxy.Clone()
} else {
nproxy = new(SunnyProxy.Proxy)
}
if client.RequestProxy != nil {
nproxy.DialAddr = client.RequestProxy.DialAddr
}
client.RequestProxy = nproxy
}
func refreshClientConnDeadline(client *clientPart, timeout time.Duration) { //刷新连接deadline与transport超时
if client.Conn != nil {
Conn := client.Conn
if timeout == 0 {
_ = Conn.SetDeadline(time.Time{})
_ = Conn.SetWriteDeadline(time.Time{})
_ = Conn.SetDeadline(time.Time{})
} else {
_ = Conn.SetDeadline(time.Now().Add(timeout))
_ = Conn.SetWriteDeadline(time.Now().Add(timeout))
_ = Conn.SetDeadline(time.Now().Add(timeout))
}
client.Client.Timeout = 24 * time.Hour
client.Transport.ResponseHeaderTimeout = 24 * time.Hour // 读取响应头超时
client.Transport.IdleConnTimeout = 24 * time.Hour // 空闲连接超时
client.Transport.TLSHandshakeTimeout = 24 * time.Hour // TLS 握手超时
}
}
func applyTLSProtoCheck(cfg *tls.Config) { //设置TLS协议版本校验
if cfg != nil {
if len(cfg.NextProtos) > 0 {
cfg.GetConfigForServer = func(info *tls.ServerHelloMsg) error {
for _, proto := range cfg.NextProtos {
if proto == http.H2Proto && info.SupportedVersion == 772 {
return nil // 如果支持,则返回 nil
}
if proto == http.H11Proto && (info.SupportedVersion == 0 || info.Vers == 771) {
return nil // 如果支持,则返回 nil
}
}
ver := info.SupportedVersion
if ver == 0 {
ver = info.Vers
}
Proto, _ := http.ProtoVersions[info.Vers]
if Proto == "" {
return fmt.Errorf("服务器不支持您所选HTTP协议版本")
}
return fmt.Errorf("服务器不支持您所选HTTP协议版本: 需要协议[%s],请检查您的配置", strings.ToUpper(Proto))
}
}
}
}
func newTransport(cfg *tls.Config, timeout time.Duration) *http.Transport { //创建Transport并设置超时
Tr := &http.Transport{TLSClientConfig: cfg}
if timeout == 0 {
Tr.ResponseHeaderTimeout = 60 * time.Second // 读取响应头超时
Tr.IdleConnTimeout = 60 * time.Second // 空闲连接超时
Tr.TLSHandshakeTimeout = 60 * time.Second // TLS 握手超时
} else {
Tr.ResponseHeaderTimeout = timeout // 读取响应头超时
Tr.IdleConnTimeout = timeout // 空闲连接超时
Tr.TLSHandshakeTimeout = timeout // TLS 握手超时
}
return Tr
}
func configureH2IfNeeded(Tr *http.Transport, cfg *tls.Config) bool { //按NextProtos配置HTTP2
h2 := false
if cfg != nil {
if len(cfg.NextProtos) < 1 {
configureHTTP2Transport(Tr, cfg)
h2 = true
} else {
for _, proto := range cfg.NextProtos {
if proto == http.H2Proto {
configureHTTP2Transport(Tr, cfg)
h2 = true
break
}
}
}
}
return h2
}
func buildProxySet(Proxy *SunnyProxy.Proxy) (nproxy *SunnyProxy.Proxy, lookupProxy *SunnyProxy.Proxy, lookupDial func(network string, addr string, OutRouterIP *net.TCPAddr) (net.Conn, error), proxyHost string) { //构建代理与LookupIP拨号
if Proxy != nil {
nproxy = Proxy.Clone()
lookupProxy = Proxy.Clone()
lookupDial = lookupProxy.Dial
proxyHost = Proxy.Host
} else {
nproxy = new(SunnyProxy.Proxy)
lookupDial = lookupProxy.Dial
}
return
}
func newClientPart(cc http.Client, Tr *http.Transport, hash uint32, nproxy *SunnyProxy.Proxy, h2 bool, outRouterIP *net.TCPAddr) *clientPart { //创建clientPart
res := &clientPart{Client: cc, key: hash, RequestProxy: nproxy, Transport: Tr, h2: h2}
if outRouterIP != nil {
res.outRouterIP = &net.TCPAddr{IP: outRouterIP.IP}
}
return res
}
// dialCtxArgs 收敛bindDialContext入参
type dialCtxArgs struct {
Tr *http.Transport //Transport对象
Req *http.Request //请求对象
Res *clientPart //客户端结构
Timeout time.Duration //超时
Event Event //事件回调
ProxyHost string //代理Host
LookupDial func(network string, addr string, OutRouterIP *net.TCPAddr) (net.Conn, error) //DNS回源拨号
}
// bindDialContext 参数收敛版(不改内部逻辑)
func bindDialContext(a dialCtxArgs) { //绑定拨号逻辑
var ips []net.IP
var isLookupIP bool
var retries bool
Tr := a.Tr
req := a.Req
res := a.Res
timeout := a.Timeout
event := a.Event
proxyHost := a.ProxyHost
lookupDial := a.LookupDial
Tr.DialContext = func(ctx context.Context, network, addr string) (cnn net.Conn, _ error) {
defer func() {
if cnn != nil {
attachConnAndTimeout(res, Tr, &res.Client, cnn, timeout) //绑定连接与超时设置
}
}()
if conn, er, ok := tryDialRawServerIP(req, res, network, event); ok { //优先按上下文指定IP连接
return conn, er
}
if dns.IsRemoteDnsServer() { //远程DNS直接拨号
conn, er := res.RequestProxy.DialWithTimeout(network, addr, 3*time.Second, res.outRouterIP)
return newHConn(event, conn), er
}
address, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
if conn, er, ok := tryDialDirectIP(res, network, address, port, event); ok { //addr就是IP时直接拨号
return conn, er
}
if strings.ToLower(address) == "localhost" { //localhost强制走127.0.0.1
r, e := res.RequestProxy.Dial(network, "127.0.0.1:"+port, res.outRouterIP)
return newHConn(event, r), e
}
for { //域名解析后尝试多IP
if !isLookupIP {
isLookupIP = true
first := dns.GetFirstIP(address, proxyHost)
if first != nil {
if first.To4() != nil {
r, e := res.RequestProxy.Dial(network, fmt.Sprintf("%s:%s", first.String(), port), res.outRouterIP)
return newHConn(event, r), e
} else {
r, e := res.RequestProxy.Dial(network, fmt.Sprintf("[%s]:%s", first.String(), port), res.outRouterIP)
return newHConn(event, r), e
}
}
ips, _ = dns.LookupIP(address, proxyHost, res.outRouterIP, lookupDial)
if len(ips) < 1 {
return nil, noIP
}
}
if len(ips) < 1 {
dns.SetFirstIP(address, proxyHost, nil)
if retries {
return nil, connectionFailed
}
isLookupIP = false
retries = true
continue
}
if allLocal127(ips) && len(ips) != 0 {
return nil, errors.New(fmt.Sprintf("Address [%s] points to 127.0.0.1", address))
}
ip := extractAndRemoveIP(&ips)
if ip != nil && ip.String() != "127.0.0.1" {
if ip.To4() != nil {
conn, er := res.RequestProxy.DialWithTimeout(network, fmt.Sprintf("%s:%s", ip.String(), port), 2*time.Second, res.outRouterIP)
if conn != nil {
dns.SetFirstIP(address, proxyHost, ip)
return newHConn(event, conn), er
}
continue
}
conn, er := res.RequestProxy.DialWithTimeout(network, fmt.Sprintf("[%s]:%s", ip.String(), port), 2*time.Second, res.outRouterIP)
if conn != nil {
dns.SetFirstIP(address, proxyHost, ip)
return newHConn(event, conn), er
}
}
}
}
}
func attachConnAndTimeout(res *clientPart, Tr *http.Transport, cc *http.Client, cnn net.Conn, timeout time.Duration) { //连接成功后设置超时并缓存
res.Conn = cnn
loop.Add(cnn)
if timeout != 0 {
_ = cnn.SetDeadline(time.Now().Add(timeout))
_ = cnn.SetWriteDeadline(time.Now().Add(timeout))
_ = cnn.SetDeadline(time.Now().Add(timeout))
} else {
_ = cnn.SetDeadline(time.Time{})
_ = cnn.SetWriteDeadline(time.Time{})
_ = cnn.SetDeadline(time.Time{})
}
Tr.ResponseHeaderTimeout = 24 * time.Hour // 读取响应头超时
Tr.IdleConnTimeout = 24 * time.Hour // 空闲连接超时
Tr.TLSHandshakeTimeout = 24 * time.Hour // TLS 握手超时
cc.Timeout = 24 * time.Hour
}
func tryDialRawServerIP(req *http.Request, res *clientPart, network string, event Event) (net.Conn, error, bool) { //按上下文指定IP连接
serveripFunc, ok := req.Context().Value(public.Connect_Raw_Address).(func() string)
if ok && serveripFunc != nil {
_serverIP_ := serveripFunc()
if _serverIP_ != "" {
address2, _, err2 := net.SplitHostPort(_serverIP_)
if err2 == nil {
ip := net.ParseIP(address2)
if ip != nil {
conn, er := res.RequestProxy.DialWithTimeout(network, _serverIP_, 3*time.Second, res.outRouterIP)
if conn != nil {
return newHConn(event, conn), er, true
}
}
}
}
}
return nil, nil, false
}
func tryDialDirectIP(res *clientPart, network, address, port string, event Event) (net.Conn, error, bool) { //addr本身是IP时直接拨号
i := net.ParseIP(address)
if i == nil {
return nil, nil, false
}
if len(i) == net.IPv4len {
r, e := res.RequestProxy.Dial(network, i.String()+":"+port, res.outRouterIP)
return newHConn(event, r), e, true
}
r, e := res.RequestProxy.Dial(network, fmt.Sprintf("[%s]:%s", address, port), res.outRouterIP)
return newHConn(event, r), e, true
}
func allLocal127(ips []net.IP) bool { //判断是否全是127.0.0.1
for _, ip := range ips {
if ip.String() != "127.0.0.1" {
return false
}
}
return true
}

View File

@@ -1,75 +1,116 @@
package loop
import (
"errors"
"net"
"sync"
"errors" //错误定义
"net" //网络连接
"sync" //并发锁
)
// 读写锁用于保护端口映射表
// 错误定义
var (
mu sync.RWMutex
portMap = map[uint16]uint16{} // key: 本地端口value: 远端端口
errNotTCP = errors.New("connection is not TCP") //非TCP连接错误
)
// 端口映射与过滤表
var (
mu sync.RWMutex //读写锁保护共享map
portMap = make(map[uint16]uint16) //key: 本地端口 value: 远端端口
filter = make(map[uint16]bool) //需要过滤的端口集合
)
// extractPorts 从连接中提取本地端口和远端端口
func extractPorts(conn net.Conn) (uint16, uint16, error) {
tcpLocal, ok1 := conn.LocalAddr().(*net.TCPAddr) // 本地 TCP 地址
tcpRemote, ok2 := conn.RemoteAddr().(*net.TCPAddr) // 对端 TCP 地址
if !ok1 || !ok2 {
return 0, 0, errors.New("connection is not TCP")
func extractPorts(conn net.Conn) (local uint16, remote uint16, err error) {
if conn == nil { //连接为空直接报错
return 0, 0, errNotTCP
}
tcpLocal, ok1 := conn.LocalAddr().(*net.TCPAddr) //本地TCP地址
tcpRemote, ok2 := conn.RemoteAddr().(*net.TCPAddr) //对端TCP地址
if !ok1 || !ok2 { //非TCP连接
return 0, 0, errNotTCP
}
return uint16(tcpLocal.Port), uint16(tcpRemote.Port), nil
}
// Add 记录一条新的连接端口映射local -> remote
// Add 记录一条新的连接端口映射(local -> remote)
func Add(conn net.Conn) {
localPort, remotePort, err := extractPorts(conn)
localPort, remotePort, err := extractPorts(conn) //提取端口
if err != nil {
return
}
mu.Lock()
portMap[localPort] = remotePort
mu.Unlock()
mu.Lock() //加写锁
portMap[localPort] = remotePort //写入映射
mu.Unlock() //解锁
}
// Un 移除一条连接的端口映射
func Un(conn net.Conn) {
localPort, _, err := extractPorts(conn)
localPort, _, err := extractPorts(conn) //提取本地端口
if err != nil {
return
}
mu.Lock()
delete(portMap, localPort)
mu.Unlock()
mu.Lock() //加写锁
delete(portMap, localPort) //删除映射
mu.Unlock() //解锁
}
// Check 检测当前连接是否形成“反向环路”
// 规则:
//
// 已记录过一条 remote -> local
// 当前出现 local -> remote
// 并且其中一端是 ServerPort用于限定只检测从服务器端口发起的回连
// 则判定为环路
func Check(conn net.Conn, ServerPort uint16) (rs bool) {
localPort, remotePort, err := extractPorts(conn)
// AddLoopFilter 加入过滤集合
func AddLoopFilter(t uint16) {
mu.Lock() //加写锁
filter[t] = true //记录远端端口
mu.Unlock() //解锁
}
// UnLoopFilter 从过滤集合移除
func UnLoopFilter(t uint16) {
mu.Lock() //加写锁
delete(filter, t) //删除远端端口
mu.Unlock() //解锁
}
// IsFiltered 判断端口是否在过滤集合中
func IsFiltered(port uint16) bool {
mu.RLock() //加读锁
_, ok := filter[port] //查询端口
mu.RUnlock() //解锁
return ok
}
// IsFilterConn 判断端口是否在过滤集合中
func IsFilterConn(conn net.Conn) bool {
_, remotePort, err := extractPorts(conn) //提取本地端口
if err != nil {
return false
}
mu.RLock() //加读锁
ok, _ := filter[remotePort] //查询端口
mu.RUnlock() //解锁
return ok
}
mu.RLock()
defer mu.RUnlock()
// Check 检测当前连接是否形成“反向环路”
// 判定条件:
// 1) 已记录过 remote -> local 的映射
// 2) 当前出现 local -> remote端口对调
// 3) localPort == ServerPort只限定从服务器端口发起的回连
func Check(conn net.Conn, ServerPort uint16) bool {
localPort, remotePort, err := extractPorts(conn) //提取端口
if err != nil {
return false
}
mu.RLock() //加读锁
mapped, ok := portMap[remotePort] //查找是否存在remotePort作为本地端口的旧映射
isFilter := filter[remotePort]
mu.RUnlock() //解锁
// 查找是否存在 remotePort 作为本地端口的旧映射
if mapped, ok := portMap[remotePort]; ok {
// 要求 mapped == localPort 表示出现端口对调
// 再额外要求 localPort == ServerPort 避免端口误判
if mapped == localPort && localPort == ServerPort {
return true
}
if !ok { //未命中映射
return false
}
if isFilter || mapped != localPort {
return false
}
return false
if localPort != ServerPort { //不是限定的服务器端口
return false
}
return true
}

View File

@@ -17,7 +17,7 @@ import (
"github.com/qtgolang/SunnyNet/src/websocket"
)
const SunnyVersion = "2025-12-23"
const SunnyVersion = "2025-12-25"
const Information = `
------------------------------------------------------
欢迎使用 SunnyNet 网络中间件 - V` + SunnyVersion + `