platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
358
selective-vpn-api/app/dnscfg/benchmark.go
Normal file
358
selective-vpn-api/app/dnscfg/benchmark.go
Normal file
@@ -0,0 +1,358 @@
|
||||
package dnscfg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
BenchmarkProfileQuick = "quick"
|
||||
BenchmarkProfileLoad = "load"
|
||||
|
||||
BenchmarkErrorNXDomain = "nxdomain"
|
||||
BenchmarkErrorTimeout = "timeout"
|
||||
BenchmarkErrorTemporary = "temporary"
|
||||
BenchmarkErrorOther = "other"
|
||||
)
|
||||
|
||||
var BenchmarkDefaultDomains = []string{
|
||||
"cloudflare.com",
|
||||
"google.com",
|
||||
"telegram.org",
|
||||
"github.com",
|
||||
"youtube.com",
|
||||
"twitter.com",
|
||||
}
|
||||
|
||||
type BenchmarkOptions struct {
|
||||
Profile string
|
||||
LoadWorkers int
|
||||
Rounds int
|
||||
SyntheticPerDomain int
|
||||
}
|
||||
|
||||
type BenchmarkResult struct {
|
||||
Upstream string
|
||||
Attempts int
|
||||
OK int
|
||||
Fail int
|
||||
NXDomain int
|
||||
Timeout int
|
||||
Temporary int
|
||||
Other int
|
||||
AvgMS int
|
||||
P95MS int
|
||||
Score float64
|
||||
Color string
|
||||
}
|
||||
|
||||
func NormalizeBenchmarkProfile(raw string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "", BenchmarkProfileLoad:
|
||||
return BenchmarkProfileLoad
|
||||
case BenchmarkProfileQuick:
|
||||
return BenchmarkProfileQuick
|
||||
default:
|
||||
return BenchmarkProfileLoad
|
||||
}
|
||||
}
|
||||
|
||||
func MakeDNSBenchmarkOptions(profile string, concurrency int) BenchmarkOptions {
|
||||
if concurrency < 1 {
|
||||
concurrency = 1
|
||||
}
|
||||
if profile == BenchmarkProfileQuick {
|
||||
return BenchmarkOptions{
|
||||
Profile: BenchmarkProfileQuick,
|
||||
LoadWorkers: 1,
|
||||
Rounds: 1,
|
||||
SyntheticPerDomain: 0,
|
||||
}
|
||||
}
|
||||
workers := concurrency * 2
|
||||
if workers < 4 {
|
||||
workers = 4
|
||||
}
|
||||
if workers > 16 {
|
||||
workers = 16
|
||||
}
|
||||
return BenchmarkOptions{
|
||||
Profile: BenchmarkProfileLoad,
|
||||
LoadWorkers: workers,
|
||||
Rounds: 3,
|
||||
SyntheticPerDomain: 2,
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeBenchmarkUpstreamStrings(in []string, normalizeUpstream func(string, string) string) []string {
|
||||
out := make([]string, 0, len(in))
|
||||
seen := map[string]struct{}{}
|
||||
for _, raw := range in {
|
||||
n := strings.TrimSpace(raw)
|
||||
if normalizeUpstream != nil {
|
||||
n = normalizeUpstream(n, "53")
|
||||
}
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[n]; ok {
|
||||
continue
|
||||
}
|
||||
seen[n] = struct{}{}
|
||||
out = append(out, n)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func NormalizeBenchmarkDomains(in []string) []string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(in))
|
||||
seen := map[string]struct{}{}
|
||||
for _, raw := range in {
|
||||
d := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(raw)), ".")
|
||||
if d == "" || strings.HasPrefix(d, "#") {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[d]; ok {
|
||||
continue
|
||||
}
|
||||
seen[d] = struct{}{}
|
||||
out = append(out, d)
|
||||
}
|
||||
if len(out) > 100 {
|
||||
out = out[:100]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func BenchmarkDNSUpstream(
|
||||
upstream string,
|
||||
domains []string,
|
||||
timeout time.Duration,
|
||||
attempts int,
|
||||
opts BenchmarkOptions,
|
||||
lookupAOnce func(host, upstream string, timeout time.Duration) ([]string, error),
|
||||
classifyErr func(error) string,
|
||||
) BenchmarkResult {
|
||||
res := BenchmarkResult{Upstream: upstream}
|
||||
probes := BuildBenchmarkProbeHosts(domains, attempts, opts)
|
||||
if len(probes) == 0 {
|
||||
return res
|
||||
}
|
||||
durations := make([]int, 0, len(probes))
|
||||
var mu sync.Mutex
|
||||
jobs := make(chan string, len(probes))
|
||||
workers := opts.LoadWorkers
|
||||
if workers < 1 {
|
||||
workers = 1
|
||||
}
|
||||
if workers > len(probes) {
|
||||
workers = len(probes)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for host := range jobs {
|
||||
start := time.Now()
|
||||
_, err := lookupAOnce(host, upstream, timeout)
|
||||
elapsed := int(time.Since(start).Milliseconds())
|
||||
if elapsed < 1 {
|
||||
elapsed = 1
|
||||
}
|
||||
mu.Lock()
|
||||
res.Attempts++
|
||||
durations = append(durations, elapsed)
|
||||
if err != nil {
|
||||
res.Fail++
|
||||
switch strings.ToLower(strings.TrimSpace(classifyErr(err))) {
|
||||
case BenchmarkErrorNXDomain:
|
||||
res.NXDomain++
|
||||
case BenchmarkErrorTimeout:
|
||||
res.Timeout++
|
||||
case BenchmarkErrorTemporary:
|
||||
res.Temporary++
|
||||
default:
|
||||
res.Other++
|
||||
}
|
||||
} else {
|
||||
res.OK++
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
for _, host := range probes {
|
||||
jobs <- host
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
|
||||
if len(durations) > 0 {
|
||||
sort.Ints(durations)
|
||||
sum := 0
|
||||
for _, d := range durations {
|
||||
sum += d
|
||||
}
|
||||
res.AvgMS = sum / len(durations)
|
||||
idx := int(float64(len(durations)-1) * 0.95)
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
}
|
||||
res.P95MS = durations[idx]
|
||||
}
|
||||
|
||||
total := res.Attempts
|
||||
if total > 0 {
|
||||
okRate := float64(res.OK) / float64(total)
|
||||
answeredRate := float64(res.OK+res.NXDomain+res.Temporary+res.Other) / float64(total)
|
||||
timeoutRate := float64(res.Timeout) / float64(total)
|
||||
temporaryRate := float64(res.Temporary) / float64(total)
|
||||
otherRate := float64(res.Other) / float64(total)
|
||||
avg := float64(res.AvgMS)
|
||||
if avg <= 0 {
|
||||
avg = float64(timeout.Milliseconds())
|
||||
}
|
||||
p95 := float64(res.P95MS)
|
||||
if p95 <= 0 {
|
||||
p95 = avg
|
||||
}
|
||||
res.Score = answeredRate*100.0 + okRate*15.0 - timeoutRate*120.0 - temporaryRate*35.0 - otherRate*20.0 - (avg / 25.0) - (p95 / 45.0)
|
||||
}
|
||||
|
||||
timeoutRate := 0.0
|
||||
answeredRate := 0.0
|
||||
if res.Attempts > 0 {
|
||||
timeoutRate = float64(res.Timeout) / float64(res.Attempts)
|
||||
answeredRate = float64(res.OK+res.NXDomain+res.Temporary+res.Other) / float64(res.Attempts)
|
||||
}
|
||||
switch {
|
||||
case answeredRate < 0.85 || timeoutRate >= 0.10 || res.P95MS > 1800:
|
||||
res.Color = "red"
|
||||
case answeredRate >= 0.97 && timeoutRate <= 0.02 && res.P95MS <= 700:
|
||||
res.Color = "green"
|
||||
default:
|
||||
res.Color = "yellow"
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func BuildBenchmarkProbeHosts(domains []string, attempts int, opts BenchmarkOptions) []string {
|
||||
if len(domains) == 0 {
|
||||
return nil
|
||||
}
|
||||
if attempts < 1 {
|
||||
attempts = 1
|
||||
}
|
||||
rounds := opts.Rounds
|
||||
if rounds < 1 {
|
||||
rounds = 1
|
||||
}
|
||||
synth := opts.SyntheticPerDomain
|
||||
if synth < 0 {
|
||||
synth = 0
|
||||
}
|
||||
out := make([]string, 0, len(domains)*attempts*rounds*(1+synth))
|
||||
for round := 0; round < rounds; round++ {
|
||||
for _, host := range domains {
|
||||
for i := 0; i < attempts; i++ {
|
||||
out = append(out, host)
|
||||
}
|
||||
for n := 0; n < synth; n++ {
|
||||
out = append(out, fmt.Sprintf("svpn-bench-%d-%d.%s", round+1, n+1, host))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(out) > 10000 {
|
||||
out = out[:10000]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func DNSLookupAOnce(
|
||||
host string,
|
||||
upstream string,
|
||||
timeout time.Duration,
|
||||
splitDNS func(string) (string, string),
|
||||
isPrivateIPv4 func(string) bool,
|
||||
) ([]string, error) {
|
||||
if splitDNS == nil {
|
||||
return nil, fmt.Errorf("splitDNS callback is nil")
|
||||
}
|
||||
server, port := splitDNS(upstream)
|
||||
if server == "" {
|
||||
return nil, fmt.Errorf("upstream empty")
|
||||
}
|
||||
if port == "" {
|
||||
port = "53"
|
||||
}
|
||||
addr := net.JoinHostPort(server, port)
|
||||
resolver := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{}
|
||||
return d.DialContext(ctx, "udp", addr)
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
records, err := resolver.LookupHost(ctx, host)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
out := make([]string, 0, len(records))
|
||||
for _, ip := range records {
|
||||
if isPrivateIPv4 != nil && isPrivateIPv4(ip) {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[ip]; ok {
|
||||
continue
|
||||
}
|
||||
seen[ip] = struct{}{}
|
||||
out = append(out, ip)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("no public ips")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func BenchmarkTopN(results []BenchmarkResult, n int, fallback []string) []string {
|
||||
out := make([]string, 0, n)
|
||||
for _, item := range results {
|
||||
if item.OK <= 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, item.Upstream)
|
||||
if len(out) >= n {
|
||||
return out
|
||||
}
|
||||
}
|
||||
for _, item := range fallback {
|
||||
if len(out) >= n {
|
||||
break
|
||||
}
|
||||
dup := false
|
||||
for _, e := range out {
|
||||
if e == item {
|
||||
dup = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !dup {
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user