Add urltest outbound
This commit is contained in:
@@ -3,23 +3,21 @@ package clashapi
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badjson"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/outbound"
|
||||
"github.com/sagernet/sing/common"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func proxyRouter(server *Server, router adapter.Router) http.Handler {
|
||||
@@ -62,7 +60,7 @@ func findProxyByName(router adapter.Router) func(next http.Handler) http.Handler
|
||||
func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
|
||||
var info badjson.JSONObject
|
||||
var clashType string
|
||||
var isSelector bool
|
||||
var isGroup bool
|
||||
switch detour.Type() {
|
||||
case C.TypeDirect:
|
||||
clashType = "Direct"
|
||||
@@ -78,28 +76,26 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
|
||||
clashType = "Vmess"
|
||||
case C.TypeSelector:
|
||||
clashType = "Selector"
|
||||
isSelector = true
|
||||
isGroup = true
|
||||
case C.TypeURLTest:
|
||||
clashType = "URLTest"
|
||||
isGroup = true
|
||||
default:
|
||||
clashType = "Unknown"
|
||||
}
|
||||
info.Put("type", clashType)
|
||||
info.Put("name", detour.Tag())
|
||||
info.Put("udp", common.Contains(detour.Network(), C.NetworkUDP))
|
||||
|
||||
var delayHistory *DelayHistory
|
||||
var loaded bool
|
||||
if isSelector {
|
||||
selector := detour.(*outbound.Selector)
|
||||
delayHistory := server.router.URLTestHistoryStorage(false).LoadURLTestHistory(outbound.RealTag(detour))
|
||||
if delayHistory != nil {
|
||||
info.Put("history", []*urltest.History{delayHistory})
|
||||
} else {
|
||||
info.Put("history", []*urltest.History{})
|
||||
}
|
||||
if isGroup {
|
||||
selector := detour.(adapter.OutboundGroup)
|
||||
info.Put("now", selector.Now())
|
||||
info.Put("all", selector.All())
|
||||
delayHistory, loaded = server.delayHistory[selector.Now()]
|
||||
} else {
|
||||
delayHistory, loaded = server.delayHistory[detour.Tag()]
|
||||
}
|
||||
if loaded {
|
||||
info.Put("history", []*DelayHistory{delayHistory})
|
||||
} else {
|
||||
info.Put("history", []*DelayHistory{})
|
||||
}
|
||||
return &info
|
||||
}
|
||||
@@ -135,7 +131,7 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite
|
||||
"type": "Fallback",
|
||||
"name": "GLOBAL",
|
||||
"udp": true,
|
||||
"history": []*DelayHistory{},
|
||||
"history": []*urltest.History{},
|
||||
"all": allProxies,
|
||||
"now": defaultTag,
|
||||
})
|
||||
@@ -218,7 +214,19 @@ func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
|
||||
defer cancel()
|
||||
|
||||
delay, err := URLTest(ctx, url, proxy)
|
||||
delay, err := urltest.URLTest(ctx, url, proxy)
|
||||
defer func() {
|
||||
realTag := outbound.RealTag(proxy)
|
||||
if err != nil {
|
||||
server.router.URLTestHistoryStorage(true).DeleteURLTestHistory(realTag)
|
||||
} else {
|
||||
server.router.URLTestHistoryStorage(true).StoreURLTestHistory(realTag, &urltest.History{
|
||||
Time: time.Now(),
|
||||
Delay: delay,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
if ctx.Err() != nil {
|
||||
render.Status(r, http.StatusGatewayTimeout)
|
||||
render.JSON(w, r, ErrRequestTimeout)
|
||||
@@ -231,70 +239,8 @@ func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
server.delayHistory[proxy.Tag()] = &DelayHistory{
|
||||
Time: time.Now(),
|
||||
Delay: delay,
|
||||
}
|
||||
|
||||
render.JSON(w, r, render.M{
|
||||
"delay": delay,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func URLTest(ctx context.Context, link string, detour adapter.Outbound) (t uint16, err error) {
|
||||
linkURL, err := url.Parse(link)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hostname := linkURL.Hostname()
|
||||
port := linkURL.Port()
|
||||
if port == "" {
|
||||
switch linkURL.Scheme {
|
||||
case "http":
|
||||
port = "80"
|
||||
case "https":
|
||||
port = "443"
|
||||
}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
instance, err := detour.DialContext(ctx, "tcp", M.ParseSocksaddrHostPortStr(hostname, port))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer instance.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodHead, link, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
transport := &http.Transport{
|
||||
Dial: func(string, string) (net.Conn, error) {
|
||||
return instance, nil
|
||||
},
|
||||
// from http.DefaultTransport
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
t = uint16(time.Since(start) / time.Millisecond)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -23,34 +24,28 @@ import (
|
||||
"github.com/go-chi/render"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gorilla/websocket"
|
||||
"os"
|
||||
)
|
||||
|
||||
var _ adapter.ClashServer = (*Server)(nil)
|
||||
|
||||
type Server struct {
|
||||
router adapter.Router
|
||||
logger log.Logger
|
||||
httpServer *http.Server
|
||||
trafficManager *trafficontrol.Manager
|
||||
delayHistory map[string]*DelayHistory
|
||||
}
|
||||
|
||||
type DelayHistory struct {
|
||||
Time time.Time `json:"time"`
|
||||
Delay uint16 `json:"delay"`
|
||||
}
|
||||
|
||||
func NewServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) *Server {
|
||||
trafficManager := trafficontrol.NewManager()
|
||||
chiRouter := chi.NewRouter()
|
||||
server := &Server{
|
||||
router,
|
||||
logFactory.NewLogger("clash-api"),
|
||||
&http.Server{
|
||||
Addr: options.ExternalController,
|
||||
Handler: chiRouter,
|
||||
},
|
||||
trafficManager,
|
||||
make(map[string]*DelayHistory),
|
||||
}
|
||||
cors := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
@@ -107,11 +102,11 @@ func (s *Server) Close() error {
|
||||
}
|
||||
|
||||
func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) net.Conn {
|
||||
return trafficontrol.NewTCPTracker(conn, s.trafficManager, castMetadata(metadata), matchedRule)
|
||||
return trafficontrol.NewTCPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule)
|
||||
}
|
||||
|
||||
func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) N.PacketConn {
|
||||
return trafficontrol.NewUDPTracker(conn, s.trafficManager, castMetadata(metadata), matchedRule)
|
||||
return trafficontrol.NewUDPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule)
|
||||
}
|
||||
|
||||
func castMetadata(metadata adapter.InboundContext) trafficontrol.Metadata {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -73,9 +75,29 @@ func (tt *tcpTracker) Close() error {
|
||||
return tt.Conn.Close()
|
||||
}
|
||||
|
||||
func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, rule adapter.Rule) *tcpTracker {
|
||||
func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *tcpTracker {
|
||||
uuid, _ := uuid.NewV4()
|
||||
|
||||
var chain []string
|
||||
var next string
|
||||
if rule == nil {
|
||||
next = router.DefaultOutbound(C.NetworkTCP).Tag()
|
||||
} else {
|
||||
next = rule.Outbound()
|
||||
}
|
||||
for {
|
||||
chain = append(chain, next)
|
||||
detour, loaded := router.Outbound(next)
|
||||
if !loaded {
|
||||
break
|
||||
}
|
||||
group, isGroup := detour.(adapter.OutboundGroup)
|
||||
if !isGroup {
|
||||
break
|
||||
}
|
||||
next = group.Now()
|
||||
}
|
||||
|
||||
t := &tcpTracker{
|
||||
Conn: conn,
|
||||
manager: manager,
|
||||
@@ -83,7 +105,7 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, rule adap
|
||||
UUID: uuid,
|
||||
Start: time.Now(),
|
||||
Metadata: metadata,
|
||||
Chain: []string{},
|
||||
Chain: common.Reverse(chain),
|
||||
Rule: "",
|
||||
UploadTotal: atomic.NewInt64(0),
|
||||
DownloadTotal: atomic.NewInt64(0),
|
||||
@@ -91,8 +113,9 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, rule adap
|
||||
}
|
||||
|
||||
if rule != nil {
|
||||
t.trackerInfo.Rule = rule.Outbound()
|
||||
t.trackerInfo.RulePayload = rule.String()
|
||||
t.trackerInfo.Rule = rule.String() + " => " + rule.Outbound()
|
||||
} else {
|
||||
t.trackerInfo.Rule = "final"
|
||||
}
|
||||
|
||||
manager.Join(t)
|
||||
@@ -135,9 +158,29 @@ func (ut *udpTracker) Close() error {
|
||||
return ut.PacketConn.Close()
|
||||
}
|
||||
|
||||
func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, rule adapter.Rule) *udpTracker {
|
||||
func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *udpTracker {
|
||||
uuid, _ := uuid.NewV4()
|
||||
|
||||
var chain []string
|
||||
var next string
|
||||
if rule == nil {
|
||||
next = router.DefaultOutbound(C.NetworkUDP).Tag()
|
||||
} else {
|
||||
next = rule.Outbound()
|
||||
}
|
||||
for {
|
||||
chain = append(chain, next)
|
||||
detour, loaded := router.Outbound(next)
|
||||
if !loaded {
|
||||
break
|
||||
}
|
||||
group, isGroup := detour.(adapter.OutboundGroup)
|
||||
if !isGroup {
|
||||
break
|
||||
}
|
||||
next = group.Now()
|
||||
}
|
||||
|
||||
ut := &udpTracker{
|
||||
PacketConn: conn,
|
||||
manager: manager,
|
||||
@@ -145,7 +188,7 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, rule
|
||||
UUID: uuid,
|
||||
Start: time.Now(),
|
||||
Metadata: metadata,
|
||||
Chain: []string{},
|
||||
Chain: common.Reverse(chain),
|
||||
Rule: "",
|
||||
UploadTotal: atomic.NewInt64(0),
|
||||
DownloadTotal: atomic.NewInt64(0),
|
||||
@@ -153,8 +196,9 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, rule
|
||||
}
|
||||
|
||||
if rule != nil {
|
||||
ut.trackerInfo.Rule = rule.Outbound()
|
||||
ut.trackerInfo.RulePayload = rule.String()
|
||||
ut.trackerInfo.Rule = rule.String() + " => " + rule.Outbound()
|
||||
} else {
|
||||
ut.trackerInfo.Rule = "final"
|
||||
}
|
||||
|
||||
manager.Join(ut)
|
||||
|
||||
Reference in New Issue
Block a user