ASN-Auflösung: dominanter Betreiber statt Offset-0-Treffer
Das anonymisierte /16 (bzw. /32 bei IPv6) gehört oft nicht einem einzigen AS -- gerade im APNIC-Raum ist es in viele kleine Bloecke zerstueckelt. Der bisherige Lookup auf die Basisadresse lieferte dann nur den zufaelligen Inhaber von Offset 0 (z.B. faelschlich "Rakuten Mobile" fuer einen Besucher, der irgendwo im selben /16 sitzt). Jetzt: per NetworksWithin alle ASNs im Netz zaehlen, das mit der groessten Adress-Abdeckung waehlen und bei weiteren "(n weitere Treffer)" anhaengen. Homogene Netze stehen sauber mit Betreibername, zersplitterte tragen den dominanten plus ehrlichen Unsicherheits-Marker. Arbeitet weiter ausschliesslich auf dem maskierten Netz, nie auf der vollen IP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@@ -70,22 +71,80 @@ func maskIP(ip net.IP) net.IP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// lookupASN löst das bereits anonymisierte Netz zum autonomen System auf. Die
|
// 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
|
// volle IP wird dafür bewusst NICHT verwendet -> wir kennen nur das maskierte
|
||||||
// Netzebene grob (innerhalb eines /16 mit mehreren ASNs nicht immer exakt), aber
|
// Netz (/16 bei IPv4, /32 bei IPv6).
|
||||||
// es verlässt nie eine vollständige IP diese Funktion. Liefert z.B.
|
//
|
||||||
// "AS3320 Deutsche Telekom AG" oder "" (keine DB/kein Treffer).
|
// 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 {
|
func lookupASN(ip net.IP) string {
|
||||||
if geoASN == nil || ip == nil {
|
if geoASN == nil || ip == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var rec asnRecord
|
// Das anonymisierte Netz rekonstruieren — gleiche Maske wie maskIP.
|
||||||
if err := geoASN.Lookup(ip, &rec); err != nil || rec.Number == 0 {
|
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 ""
|
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)
|
// trackImpression zählt einen Seitenaufruf pro (Tag, anonymisiertes Netz, Pfad)
|
||||||
|
|||||||
Reference in New Issue
Block a user