Release 2.2.0: Happ-style multi-node list with ping and auto best connect.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"vpnclient/internal/config"
|
||||
@@ -24,11 +26,78 @@ type Result struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Target describes one profile to ping.
|
||||
type Target struct {
|
||||
Name string
|
||||
Protocol config.Protocol
|
||||
Proxy string
|
||||
}
|
||||
|
||||
// PingProxyURI measures connect time to the host in a share/proxy URI.
|
||||
func PingProxyURI(ctx context.Context, name, proxyURI string) Result {
|
||||
return PingProfile(ctx, name, "", proxyURI)
|
||||
}
|
||||
|
||||
// PingAll pings targets in parallel (bounded concurrency) and sorts OK results by latency.
|
||||
func PingAll(ctx context.Context, targets []Target) []Result {
|
||||
out := make([]Result, len(targets))
|
||||
if len(targets) == 0 {
|
||||
return out
|
||||
}
|
||||
workers := 12
|
||||
if len(targets) < workers {
|
||||
workers = len(targets)
|
||||
}
|
||||
sem := make(chan struct{}, workers)
|
||||
var wg sync.WaitGroup
|
||||
for i, t := range targets {
|
||||
wg.Add(1)
|
||||
go func(i int, t Target) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
if strings.TrimSpace(t.Proxy) == "" {
|
||||
out[i] = Result{Name: t.Name, Error: "нет ссылки сервера"}
|
||||
return
|
||||
}
|
||||
out[i] = PingProfile(ctx, t.Name, t.Protocol, t.Proxy)
|
||||
}(i, t)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
sorted := append([]Result(nil), out...)
|
||||
sort.SliceStable(sorted, func(i, j int) bool {
|
||||
a, b := sorted[i], sorted[j]
|
||||
if a.OK != b.OK {
|
||||
return a.OK
|
||||
}
|
||||
if !a.OK {
|
||||
return a.Name < b.Name
|
||||
}
|
||||
if a.Ms != b.Ms {
|
||||
return a.Ms < b.Ms
|
||||
}
|
||||
return a.Name < b.Name
|
||||
})
|
||||
return sorted
|
||||
}
|
||||
|
||||
// BestOK returns the lowest-latency successful result, if any.
|
||||
func BestOK(results []Result) (Result, bool) {
|
||||
var best Result
|
||||
found := false
|
||||
for _, r := range results {
|
||||
if !r.OK {
|
||||
continue
|
||||
}
|
||||
if !found || r.Ms < best.Ms {
|
||||
best = r
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return best, found
|
||||
}
|
||||
|
||||
// PingProfile pings using protocol hints when available.
|
||||
// Naive uses TCP; Hysteria 2 uses UDP (QUIC) — TCP would always look "refused".
|
||||
func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyURI string) Result {
|
||||
@@ -111,7 +180,7 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
|
||||
}
|
||||
|
||||
func probeTCP(ctx context.Context, host, port string) error {
|
||||
d := net.Dialer{Timeout: 4 * time.Second}
|
||||
d := net.Dialer{Timeout: 3 * time.Second}
|
||||
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -121,20 +190,19 @@ func probeTCP(ctx context.Context, host, port string) error {
|
||||
}
|
||||
|
||||
func probeUDP(ctx context.Context, host, port string) error {
|
||||
d := net.Dialer{Timeout: 4 * time.Second}
|
||||
d := net.Dialer{Timeout: 3 * time.Second}
|
||||
conn, err := d.DialContext(ctx, "udp", net.JoinHostPort(host, port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
deadline := time.Now().Add(1500 * time.Millisecond)
|
||||
deadline := time.Now().Add(1200 * time.Millisecond)
|
||||
if dl, ok := ctx.Deadline(); ok && dl.Before(deadline) {
|
||||
deadline = dl
|
||||
}
|
||||
_ = conn.SetDeadline(deadline)
|
||||
|
||||
// Any datagram is enough to exercise the UDP path; QUIC rarely answers a bare probe.
|
||||
if _, err := conn.Write([]byte{0}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -144,7 +212,6 @@ func probeUDP(ctx context.Context, host, port string) error {
|
||||
return nil
|
||||
}
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
// No ICMP unreachable → port is typically open or filtered; treat as reachable for UI ping.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user