| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- package outbound
- import (
- "encoding/json"
- "fmt"
- "net"
- "strconv"
- "sync"
- "time"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- "github.com/mhsanaei/3x-ui/v3/internal/logger"
- "github.com/mhsanaei/3x-ui/v3/internal/xray"
- "gorm.io/gorm"
- )
- // OutboundService provides business logic for managing Xray outbound configurations.
- // It handles outbound traffic monitoring and statistics.
- type OutboundService struct{}
- func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
- var err error
- db := database.GetDB()
- tx := db.Begin()
- defer func() {
- if err != nil {
- tx.Rollback()
- } else {
- tx.Commit()
- }
- }()
- err = s.addOutboundTraffic(tx, traffics)
- if err != nil {
- return err, false
- }
- return nil, false
- }
- func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
- if len(traffics) == 0 {
- return nil
- }
- var err error
- for _, traffic := range traffics {
- if traffic.IsOutbound {
- var outbound model.OutboundTraffics
- err = tx.Model(&model.OutboundTraffics{}).Where("tag = ?", traffic.Tag).
- FirstOrCreate(&outbound).Error
- if err != nil {
- return err
- }
- outbound.Tag = traffic.Tag
- outbound.Up = outbound.Up + traffic.Up
- outbound.Down = outbound.Down + traffic.Down
- outbound.Total = outbound.Up + outbound.Down
- err = tx.Save(&outbound).Error
- if err != nil {
- return err
- }
- }
- }
- return nil
- }
- func (s *OutboundService) GetOutboundsTraffic() ([]*model.OutboundTraffics, error) {
- db := database.GetDB()
- var traffics []*model.OutboundTraffics
- err := db.Model(model.OutboundTraffics{}).Find(&traffics).Error
- if err != nil {
- logger.Warning("Error retrieving OutboundTraffics: ", err)
- return nil, err
- }
- return traffics, nil
- }
- func (s *OutboundService) ResetOutboundTraffic(tag string) error {
- db := database.GetDB()
- whereText := "tag "
- if tag == "-alltags-" {
- whereText += " <> ?"
- } else {
- whereText += " = ?"
- }
- result := db.Model(model.OutboundTraffics{}).
- Where(whereText, tag).
- Updates(map[string]any{"up": 0, "down": 0, "total": 0})
- err := result.Error
- if err != nil {
- return err
- }
- return nil
- }
- // TestOutboundResult represents the result of testing an outbound.
- // Delay is in milliseconds. Endpoints is only populated for TCP-mode
- // probes; HTTP mode reports the time of a real HTTP request routed
- // through the outbound, with an optional timing breakdown.
- type TestOutboundResult struct {
- Tag string `json:"tag,omitempty"`
- Success bool `json:"success"`
- Delay int64 `json:"delay"`
- Error string `json:"error,omitempty"`
- Mode string `json:"mode,omitempty"`
- // HTTP-mode extras. Any HTTP response counts as reachable; HTTPStatus
- // records what the test URL answered. ConnectMs is the dial to the local
- // test inbound; TLSMs covers outbound-chain establishment + target TLS
- // (https URLs only, since xray ACKs the SOCKS CONNECT before dialing
- // upstream); TTFBMs is request start → first response byte.
- HTTPStatus int `json:"httpStatus,omitempty"`
- ConnectMs int64 `json:"connectMs,omitempty"`
- TLSMs int64 `json:"tlsMs,omitempty"`
- TTFBMs int64 `json:"ttfbMs,omitempty"`
- Endpoints []TestEndpointResult `json:"endpoints,omitempty"`
- }
- // TestEndpointResult is one entry in a TCP-mode probe — the per-endpoint
- // dial outcome for outbounds that expose multiple servers/peers.
- type TestEndpointResult struct {
- Address string `json:"address"`
- Success bool `json:"success"`
- Delay int64 `json:"delay"`
- Error string `json:"error,omitempty"`
- }
- func (s *OutboundService) testOutboundTCP(outboundJSON string) (*TestOutboundResult, error) {
- var ob map[string]any
- if err := json.Unmarshal([]byte(outboundJSON), &ob); err != nil {
- return &TestOutboundResult{Mode: "tcp", Success: false, Error: fmt.Sprintf("Invalid outbound JSON: %v", err)}, nil
- }
- tag, _ := ob["tag"].(string)
- protocol, _ := ob["protocol"].(string)
- if protocol == "blackhole" || protocol == "freedom" || tag == "blocked" {
- return &TestOutboundResult{Tag: tag, Mode: "tcp", Success: false, Error: "Outbound has no testable endpoint"}, nil
- }
- endpoints := extractOutboundEndpoints(ob)
- if len(endpoints) == 0 {
- return &TestOutboundResult{Tag: tag, Mode: "tcp", Success: false, Error: "No testable endpoint"}, nil
- }
- results := make([]TestEndpointResult, len(endpoints))
- var wg sync.WaitGroup
- for i := range endpoints {
- wg.Add(1)
- go func(i int) {
- defer wg.Done()
- results[i] = probeTCPEndpoint(endpoints[i], 5*time.Second)
- }(i)
- }
- wg.Wait()
- var bestDelay int64 = -1
- var firstErr string
- for _, r := range results {
- if r.Success {
- if bestDelay < 0 || r.Delay < bestDelay {
- bestDelay = r.Delay
- }
- } else if firstErr == "" {
- firstErr = r.Error
- }
- }
- out := &TestOutboundResult{Tag: tag, Mode: "tcp", Endpoints: results}
- if bestDelay >= 0 {
- out.Success = true
- out.Delay = bestDelay
- } else {
- out.Error = firstErr
- if out.Error == "" {
- out.Error = "All endpoints unreachable"
- }
- }
- return out, nil
- }
- func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult {
- r := TestEndpointResult{Address: endpoint}
- start := time.Now()
- conn, err := net.DialTimeout("tcp", endpoint, timeout)
- r.Delay = time.Since(start).Milliseconds()
- if err != nil {
- r.Error = err.Error()
- return r
- }
- conn.Close()
- r.Success = true
- return r
- }
- // outboundTransportIsUDP reports whether the outbound's proxy speaks UDP
- // (wireguard, hysteria, or a kcp/quic/hysteria stream transport). A bare
- // UDP dial can't probe these — they ignore unauthenticated packets, so a
- // dial neither proves reachability nor measures latency. Such outbounds
- // must go through the real xray handshake probe instead.
- func outboundTransportIsUDP(ob map[string]any) bool {
- if protocol, _ := ob["protocol"].(string); protocol == "hysteria" || protocol == "wireguard" {
- return true
- }
- if stream, ok := ob["streamSettings"].(map[string]any); ok {
- if n, _ := stream["network"].(string); n == "hysteria" || n == "kcp" || n == "quic" {
- return true
- }
- }
- return false
- }
- func extractOutboundEndpoints(ob map[string]any) []string {
- protocol, _ := ob["protocol"].(string)
- settings, _ := ob["settings"].(map[string]any)
- if settings == nil {
- return nil
- }
- var out []string
- addServer := func(addr any, port any) {
- host, _ := addr.(string)
- p := numAsInt(port)
- if host != "" && p > 0 {
- out = append(out, fmt.Sprintf("%s:%d", host, p))
- }
- }
- switch protocol {
- case "vmess":
- if vnext, ok := settings["vnext"].([]any); ok {
- for _, v := range vnext {
- if vm, ok := v.(map[string]any); ok {
- addServer(vm["address"], vm["port"])
- }
- }
- }
- case "vless":
- addServer(settings["address"], settings["port"])
- case "hysteria":
- addServer(settings["address"], settings["port"])
- case "trojan", "shadowsocks", "http", "socks":
- if servers, ok := settings["servers"].([]any); ok {
- for _, sv := range servers {
- if sm, ok := sv.(map[string]any); ok {
- addServer(sm["address"], sm["port"])
- }
- }
- }
- case "wireguard":
- if peers, ok := settings["peers"].([]any); ok {
- for _, p := range peers {
- if pm, ok := p.(map[string]any); ok {
- if ep, _ := pm["endpoint"].(string); ep != "" {
- out = append(out, ep)
- }
- }
- }
- }
- }
- return out
- }
- func numAsInt(v any) int {
- switch n := v.(type) {
- case float64:
- return int(n)
- case int:
- return n
- case int64:
- return int(n)
- case string:
- if i, err := strconv.Atoi(n); err == nil {
- return i
- }
- }
- return 0
- }
|