增加reality域名延迟探测
This commit is contained in:
@@ -88,6 +88,7 @@ func main() {
|
||||
mux.HandleFunc("/api/child/routing", manageHandler.HandleRouting)
|
||||
mux.HandleFunc("/api/child/scan", manageHandler.HandleScan)
|
||||
mux.HandleFunc("/api/child/cert/deploy", manageHandler.HandleCertDeploy)
|
||||
mux.HandleFunc("/api/child/domains/latency", manageHandler.HandleDomainLatencyProbe)
|
||||
|
||||
// SSE streaming install/remove
|
||||
mux.HandleFunc("/api/child/xray/install-stream", manageHandler.HandleXrayInstallStream)
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DomainLatencyProbeRequest struct {
|
||||
Domains []string `json:"domains"`
|
||||
TimeoutMs int `json:"timeout_ms,omitempty"`
|
||||
}
|
||||
|
||||
type DomainLatencyProbeResult struct {
|
||||
Domain string `json:"domain"`
|
||||
Target string `json:"target"`
|
||||
Success bool `json:"success"`
|
||||
LatencyMs int64 `json:"latency_ms,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// HandleDomainLatencyProbe handles POST /api/child/domains/latency
|
||||
func (h *ManageHandler) HandleDomainLatencyProbe(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
if !h.authenticate(r) {
|
||||
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
var req DomainLatencyProbeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Domains) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "domains is required")
|
||||
return
|
||||
}
|
||||
|
||||
timeoutMs := req.TimeoutMs
|
||||
if timeoutMs <= 0 {
|
||||
timeoutMs = 2000
|
||||
}
|
||||
if timeoutMs < 200 {
|
||||
timeoutMs = 200
|
||||
}
|
||||
if timeoutMs > 10000 {
|
||||
timeoutMs = 10000
|
||||
}
|
||||
timeout := time.Duration(timeoutMs) * time.Millisecond
|
||||
|
||||
domains := uniqueProbeDomains(req.Domains)
|
||||
if len(domains) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "no valid domain to probe")
|
||||
return
|
||||
}
|
||||
if len(domains) > 200 {
|
||||
domains = domains[:200]
|
||||
}
|
||||
|
||||
results := make([]DomainLatencyProbeResult, 0, len(domains))
|
||||
resultCh := make(chan DomainLatencyProbeResult, len(domains))
|
||||
sem := make(chan struct{}, 16)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, domain := range domains {
|
||||
wg.Add(1)
|
||||
domain := domain
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
resultCh <- probeOneDomainLatency(domain, timeout)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(resultCh)
|
||||
|
||||
for result := range resultCh {
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
if results[i].Success != results[j].Success {
|
||||
return results[i].Success
|
||||
}
|
||||
if !results[i].Success {
|
||||
return results[i].Domain < results[j].Domain
|
||||
}
|
||||
if results[i].LatencyMs == results[j].LatencyMs {
|
||||
return results[i].Domain < results[j].Domain
|
||||
}
|
||||
return results[i].LatencyMs < results[j].LatencyMs
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"results": results,
|
||||
"count": len(results),
|
||||
})
|
||||
}
|
||||
|
||||
func uniqueProbeDomains(rawDomains []string) []string {
|
||||
out := make([]string, 0, len(rawDomains))
|
||||
seen := make(map[string]struct{}, len(rawDomains))
|
||||
for _, raw := range rawDomains {
|
||||
normalized := normalizeProbeInput(raw)
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[normalized]; ok {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = struct{}{}
|
||||
out = append(out, normalized)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeProbeInput(raw string) string {
|
||||
s := strings.TrimSpace(raw)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if strings.Contains(s, "://") {
|
||||
if idx := strings.Index(s, "://"); idx >= 0 && idx+3 < len(s) {
|
||||
s = s[idx+3:]
|
||||
}
|
||||
}
|
||||
|
||||
if idx := strings.Index(s, "/"); idx >= 0 {
|
||||
s = s[:idx]
|
||||
}
|
||||
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
s = strings.TrimPrefix(s, "[")
|
||||
s = strings.TrimSuffix(s, "]")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func probeOneDomainLatency(domain string, timeout time.Duration) DomainLatencyProbeResult {
|
||||
host := domain
|
||||
port := "443"
|
||||
|
||||
if h, p, ok := splitHostPortLoose(domain); ok {
|
||||
host = h
|
||||
port = p
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
return DomainLatencyProbeResult{
|
||||
Domain: domain,
|
||||
Target: domain,
|
||||
Success: false,
|
||||
Error: "empty host",
|
||||
}
|
||||
}
|
||||
|
||||
target := net.JoinHostPort(host, port)
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
return DomainLatencyProbeResult{
|
||||
Domain: host,
|
||||
Target: target,
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
return DomainLatencyProbeResult{
|
||||
Domain: host,
|
||||
Target: target,
|
||||
Success: true,
|
||||
LatencyMs: time.Since(start).Milliseconds(),
|
||||
}
|
||||
}
|
||||
|
||||
func splitHostPortLoose(input string) (host string, port string, ok bool) {
|
||||
s := strings.TrimSpace(input)
|
||||
if s == "" {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
if h, p, err := net.SplitHostPort(s); err == nil {
|
||||
if h != "" && p != "" {
|
||||
return h, p, true
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for "domain:443" without brackets handling.
|
||||
idx := strings.LastIndex(s, ":")
|
||||
if idx <= 0 || idx >= len(s)-1 {
|
||||
return "", "", false
|
||||
}
|
||||
h := s[:idx]
|
||||
p := s[idx+1:]
|
||||
if _, err := strconv.Atoi(p); err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
return h, p, true
|
||||
}
|
||||
Reference in New Issue
Block a user