diff --git a/stats.go b/stats.go index 4d8e958..4d46d2a 100644 --- a/stats.go +++ b/stats.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "math/big" "net" "net/http" "time" @@ -70,22 +71,80 @@ func maskIP(ip net.IP) net.IP { } // lookupASN löst das bereits anonymisierte Netz zum autonomen System auf. Die -// volle IP wird dafür bewusst NICHT verwendet -> die Auflösung ist auf -// Netzebene grob (innerhalb eines /16 mit mehreren ASNs nicht immer exakt), aber -// es verlässt nie eine vollständige IP diese Funktion. Liefert z.B. -// "AS3320 Deutsche Telekom AG" oder "" (keine DB/kein Treffer). +// volle IP wird dafür bewusst NICHT verwendet -> wir kennen nur das maskierte +// Netz (/16 bei IPv4, /32 bei IPv6). +// +// Ein solches Netz gehört oft nicht einem einzigen AS: gerade im APNIC-Raum ist +// ein /16 in viele kleine Blöcke zerstückelt. Ein Lookup auf die Basisadresse +// liefert dann nur den (zufälligen) Inhaber von Offset 0. Stattdessen zählen wir +// per NetworksWithin alle ASNs im Netz und wählen das mit der größten +// Adress-Abdeckung; gibt es weitere, hängen wir "(n weitere Treffer)" an. So +// steht bei einem homogenen Netz schlicht der Betreiber, bei einem +// zersplitterten der dominante plus ein ehrlicher Unsicherheits-Hinweis. +// +// Liefert z.B. "AS3320 Deutsche Telekom AG", "AS8881 1&1 Versatel GmbH +// (2 weitere Treffer)" oder "" (keine DB/kein Treffer). func lookupASN(ip net.IP) string { if geoASN == nil || ip == nil { return "" } - var rec asnRecord - if err := geoASN.Lookup(ip, &rec); err != nil || rec.Number == 0 { + // Das anonymisierte Netz rekonstruieren — gleiche Maske wie maskIP. + var network *net.IPNet + if v4 := ip.To4(); v4 != nil { + network = &net.IPNet{IP: v4, Mask: net.CIDRMask(16, 32)} + } else { + network = &net.IPNet{IP: ip, Mask: net.CIDRMask(32, 128)} + } + + // Adress-Abdeckung je AS aufsummieren. big.Int, weil ein IPv6-Teilnetz im + // /32 bis zu 2^96 Adressen umfasst und int64 sprengen würde. + type cover struct { + org string + count *big.Int + } + covers := map[uint]*cover{} + nets := geoASN.NetworksWithin(network) + for nets.Next() { + var rec asnRecord + subnet, err := nets.Network(&rec) + if err != nil || rec.Number == 0 { + continue + } + ones, bits := subnet.Mask.Size() + size := new(big.Int).Lsh(big.NewInt(1), uint(bits-ones)) + c := covers[rec.Number] + if c == nil { + c = &cover{org: rec.Org, count: new(big.Int)} + covers[rec.Number] = c + } + c.count.Add(c.count, size) + } + if len(covers) == 0 { return "" } - if rec.Org == "" { - return fmt.Sprintf("AS%d", rec.Number) + + // AS mit der größten Abdeckung wählen. Bei Gleichstand das kleinere ASN, + // damit das Ergebnis bei zufälliger Map-Reihenfolge stabil bleibt. + var bestNum uint + var best *cover + for num, c := range covers { + if best == nil { + best, bestNum = c, num + continue + } + if cmp := c.count.Cmp(best.count); cmp > 0 || (cmp == 0 && num < bestNum) { + best, bestNum = c, num + } } - return fmt.Sprintf("AS%d %s", rec.Number, rec.Org) + + label := fmt.Sprintf("AS%d", bestNum) + if best.org != "" { + label += " " + best.org + } + if extra := len(covers) - 1; extra > 0 { + label += fmt.Sprintf(" (%d weitere Treffer)", extra) + } + return label } // trackImpression zählt einen Seitenaufruf pro (Tag, anonymisiertes Netz, Pfad)