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:
2026-06-18 22:50:33 +02:00
parent 4d99074bd5
commit 63240a6cf7
+67 -8
View File
@@ -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 ""
}
// 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
if err := geoASN.Lookup(ip, &rec); err != nil || rec.Number == 0 {
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
}
return fmt.Sprintf("AS%d %s", rec.Number, rec.Org)
if cmp := c.count.Cmp(best.count); cmp > 0 || (cmp == 0 && num < bestNum) {
best, bestNum = c, num
}
}
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)