package main import ( "context" "encoding/json" "fmt" "io" "net" "net/http" "os" "sort" "strconv" "strings" "sync" "time" "gopkg.in/yaml.v3" ) const ( speedtestMapPath = "db/speedtest.map.yml" speedtestPath = "db/speedtest.json" speedtestVersion = 1 libreSpeedServers = "https://librespeed.org/backend-servers/servers.php" aliveTimeout = 8 * time.Second aliveParallel = 16 aliveProbeBytes = 64 << 10 ) var libreSpeedCountries = map[string]string{ "Australia": "AU", "Austria": "AT", "Belgium": "BE", "Brazil": "BR", "Bulgaria": "BG", "Canada": "CA", "Czech Republic": "CZ", "Czechia": "CZ", "Denmark": "DK", "England": "GB", "Estonia": "EE", "Finland": "FI", "France": "FR", "Germany": "DE", "Greece": "GR", "Hong Kong": "HK", "Hungary": "HU", "India": "IN", "Ireland": "IE", "Italy": "IT", "Japan": "JP", "Kazakhstan": "KZ", "Latvia": "LV", "Lithuania": "LT", "Netherlands": "NL", "Norway": "NO", "Poland": "PL", "Portugal": "PT", "Romania": "RO", "Russia": "RU", "Serbia": "RS", "Singapore": "SG", "Spain": "ES", "Sweden": "SE", "Switzerland": "CH", "Turkey": "TR", "UK": "GB", "Ukraine": "UA", "United Kingdom": "GB", "United States": "US", "USA": "US", } type speedIperf3 struct { Host string `yaml:"host" json:"host"` Ports string `yaml:"ports" json:"ports"` } type speedLibreSpeed struct { Download string `yaml:"dl" json:"dl"` Upload string `yaml:"ul" json:"ul"` Ping string `yaml:"ping" json:"ping"` } type speedNode struct { Country string `yaml:"country" json:"country"` City string `yaml:"city" json:"city"` Owner string `yaml:"owner" json:"owner"` Iperf3 *speedIperf3 `yaml:"iperf3" json:"iperf3,omitempty"` LibreSpeed *speedLibreSpeed `yaml:"librespeed" json:"librespeed,omitempty"` File string `yaml:"file" json:"file,omitempty"` } type speedtestFile struct { Version int `json:"version"` Updated string `json:"updated"` Nodes []speedNode `json:"nodes"` } func buildSpeedtest(now time.Time) int { data, err := os.ReadFile(speedtestMapPath) if err != nil { fmt.Println(" ", err) return 0 } var nodes []speedNode if err := yaml.Unmarshal(data, &nodes); err != nil { fmt.Println(" ", speedtestMapPath+":", err) return 0 } nodes = append(nodes, importLibreSpeed()...) alive := checkSpeedNodes(nodes) sort.SliceStable(alive, func(i, j int) bool { a, b := alive[i], alive[j] if a.Country != b.Country { return a.Country < b.Country } if a.City != b.City { return a.City < b.City } return a.Owner < b.Owner }) out, err := json.MarshalIndent(speedtestFile{Version: speedtestVersion, Updated: now.Format(time.RFC3339), Nodes: alive}, "", " ") if err != nil { fmt.Println(" ", err) return 0 } if err := os.WriteFile(speedtestPath, append(out, '\n'), 0644); err != nil { fmt.Println(" ", err) return 0 } fmt.Printf(" %d узлов из %d\n", len(alive), len(nodes)) return len(alive) } func importLibreSpeed() []speedNode { data, err := fetch(libreSpeedServers) if err != nil { fmt.Println(" LibreSpeed:", err) return nil } var servers []struct { Name string `json:"name"` Server string `json:"server"` DlURL string `json:"dlURL"` UlURL string `json:"ulURL"` PingURL string `json:"pingURL"` Sponsor string `json:"sponsorName"` } if err := json.Unmarshal(data, &servers); err != nil { fmt.Println(" LibreSpeed:", err) return nil } var nodes []speedNode for _, s := range servers { parts := strings.Split(stripParens(s.Name), ",") country, ok := libreSpeedCountries[strings.TrimSpace(parts[len(parts)-1])] if !ok || len(parts) < 2 { fmt.Println(" LibreSpeed: страна не распознана:", s.Name) continue } base := strings.TrimSuffix(s.Server, "/") + "/" nodes = append(nodes, speedNode{ Country: country, City: strings.TrimSpace(parts[0]), Owner: s.Sponsor, LibreSpeed: &speedLibreSpeed{ Download: base + strings.TrimPrefix(s.DlURL, "/"), Upload: base + strings.TrimPrefix(s.UlURL, "/"), Ping: base + strings.TrimPrefix(s.PingURL, "/"), }, }) } return nodes } func stripParens(s string) string { for { open := strings.Index(s, "(") if open < 0 { return s } end := strings.Index(s[open:], ")") if end < 0 { return s[:open] } s = s[:open] + s[open+end+1:] } } func checkSpeedNodes(nodes []speedNode) []speedNode { alive := make([]*speedNode, len(nodes)) slots := make(chan struct{}, aliveParallel) var wg sync.WaitGroup for i := range nodes { wg.Add(1) go func(i int) { defer wg.Done() slots <- struct{}{} defer func() { <-slots }() n := nodes[i] if n.Iperf3 != nil && !iperf3Alive(n.Iperf3) { fmt.Println(" нет ответа iperf3:", n.Owner, n.City, n.Iperf3.Host) n.Iperf3 = nil } if n.LibreSpeed != nil && !httpAlive(n.LibreSpeed.Download+"?ckSize=1") { fmt.Println(" нет ответа LibreSpeed:", n.Owner, n.City, n.LibreSpeed.Download) n.LibreSpeed = nil } if n.File != "" && !httpAlive(n.File) { fmt.Println(" нет ответа файла:", n.Owner, n.City, n.File) n.File = "" } if n.Iperf3 != nil || n.LibreSpeed != nil || n.File != "" { alive[i] = &n } }(i) } wg.Wait() var out []speedNode for _, n := range alive { if n != nil { out = append(out, *n) } } return out } func iperf3Alive(p *speedIperf3) bool { lo, hi, ok := strings.Cut(p.Ports, "-") if !ok { hi = lo } from, errLo := strconv.Atoi(lo) to, errHi := strconv.Atoi(hi) if errLo != nil || errHi != nil || to < from { return false } for port := from; port <= to && port < from+3; port++ { conn, err := net.DialTimeout("tcp4", net.JoinHostPort(p.Host, strconv.Itoa(port)), aliveTimeout) if err == nil { conn.Close() return true } } return false } var aliveClient = &http.Client{Transport: &http.Transport{ DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) { return (&net.Dialer{Timeout: aliveTimeout}).DialContext(ctx, "tcp4", addr) }, TLSHandshakeTimeout: aliveTimeout, }} func httpAlive(url string) bool { ctx, cancel := context.WithTimeout(context.Background(), aliveTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return false } resp, err := aliveClient.Do(req) if err != nil { return false } defer resp.Body.Close() n, _ := io.CopyN(io.Discard, resp.Body, aliveProbeBytes) return resp.StatusCode == http.StatusOK && n == aliveProbeBytes } func runSpeedtestOnly() { fmt.Println("=== Генерация db/speedtest.json ===") buildSpeedtest(time.Now().UTC()) }