feat: add Eduroam Analyzer service
This commit is contained in:
11
.env.example
Normal file
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
||||
# MaxMind (create a free GeoLite2 license key in your MaxMind account)
|
||||
MAXMIND_LICENSE_KEY=your_maxmind_license_key
|
||||
|
||||
# PeeringDB (optional; reduces rate limits)
|
||||
PDB_API_KEY=your_peeringdb_api_key
|
||||
|
||||
# existing Traefik/proxy network name (must already exist)
|
||||
PROXY_NETWORK=proxy
|
||||
|
||||
# update interval in seconds (30 days)
|
||||
UPDATE_INTERVAL_SECONDS=2592000
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.env
|
||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM golang:1.22-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod ./
|
||||
RUN go mod download
|
||||
COPY main.go ./
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/asn-header-service
|
||||
|
||||
FROM alpine:3.20
|
||||
RUN adduser -D -H -u 10001 app
|
||||
USER 10001
|
||||
WORKDIR /app
|
||||
COPY --from=build /out/asn-header-service /app/asn-header-service
|
||||
EXPOSE 8080
|
||||
ENV ADDR=:8080
|
||||
ENTRYPOINT ["/app/asn-header-service"]
|
||||
|
||||
89
README.md
Normal file
89
README.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# NREN / ASN Detection Service
|
||||
|
||||
Dieses Projekt stellt einen **minimalen Microservice** bereit, um **Hochschul- und Forschungsnetzwerke (NRENs)** anhand der **Autonomous System Number (ASN)** zu erkennen.
|
||||
|
||||
Der Zweck ist es, **Anfragen aus Hochschulnetzen (z. B. eduroam)** zu identifizieren, um **Research-bezogene Services kostenlos oder bevorzugt bereitzustellen**.
|
||||
|
||||
Das System dient ausschließlich der **Netzwerk-Klassifikation** und **ersetzt keine Authentifizierung**.
|
||||
|
||||
---
|
||||
|
||||
## Ziel
|
||||
|
||||
- Erkennen, ob eine Anfrage aus einem **Hochschul- oder Forschungsnetz** stammt
|
||||
- Bereitstellung eines **Header-Hinweises** für nachgelagerte Services
|
||||
- Grundlage für Entscheidungen wie:
|
||||
- kostenfreie Research-Features
|
||||
- angepasste UI-Hinweise
|
||||
- alternative Rate-Limits
|
||||
|
||||
---
|
||||
|
||||
## Funktionsweise (Kurzfassung)
|
||||
|
||||
```
|
||||
Client
|
||||
→ Traefik
|
||||
→ ForwardAuth
|
||||
→ ASN Detection Service
|
||||
→ Header wird ergänzt
|
||||
```
|
||||
|
||||
1. Die Client-IP wird ermittelt
|
||||
2. Die zugehörige ASN wird lokal nachgeschlagen
|
||||
3. Die ASN wird mit einer NREN-ASN-Liste verglichen
|
||||
4. Das Ergebnis wird als HTTP-Header zurückgegeben
|
||||
|
||||
---
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- **GeoLite2 ASN (MaxMind)**
|
||||
- kostenlos
|
||||
- lokal
|
||||
- monatliche Aktualisierung
|
||||
|
||||
- **NREN-ASN-Liste**
|
||||
- abgeleitet aus PeeringDB
|
||||
- Kategorie: `Research and Education`
|
||||
- monatliche Aktualisierung
|
||||
|
||||
---
|
||||
|
||||
## Bereitgestellte Header
|
||||
|
||||
| Header | Beschreibung |
|
||||
|------|-------------|
|
||||
| `X-ASN` | ASN der Client-IP |
|
||||
| `X-ASN-ORG` | Organisation (optional) |
|
||||
| `X-NREN` | `1` wenn ASN zu einem Hochschul-/Forschungsnetz gehört, sonst `0` |
|
||||
|
||||
---
|
||||
|
||||
## Integration
|
||||
|
||||
Der Service wird als **Traefik ForwardAuth Middleware** eingebunden.
|
||||
Die Header werden über `authResponseHeaders` an die eigentliche Anwendung weitergereicht.
|
||||
|
||||
Der Service ist **nicht öffentlich exponiert** und kommuniziert ausschließlich über das interne Docker-Netzwerk.
|
||||
|
||||
---
|
||||
|
||||
## Update-Strategie
|
||||
|
||||
- monatliche Aktualisierung der ASN-Daten
|
||||
- keine externen Requests während der Anfrageverarbeitung
|
||||
|
||||
---
|
||||
|
||||
## Einschränkungen
|
||||
|
||||
- Die Erkennung ist **heuristisch**
|
||||
- Es gibt **keine Garantie**, dass jede Anfrage aus einem Hochschulnetz erkannt wird
|
||||
- Die Information darf **nicht als Authentifizierungsmerkmal** verwendet werden
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Dieses Projekt ermöglicht eine **performante, datenschutzfreundliche Erkennung von Hochschulnetzen**, um **Research-Angebote kontextabhängig bereitzustellen**, ohne Nutzer zu identifizieren oder externe Dienste zur Laufzeit zu kontaktieren.
|
||||
89
README_technical.md
Normal file
89
README_technical.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# NREN / ASN Detection Service
|
||||
|
||||
Dieses Projekt stellt einen **minimalen Microservice** bereit, um **Hochschul- und Forschungsnetzwerke (NRENs)** anhand der **Autonomous System Number (ASN)** zu erkennen.
|
||||
|
||||
Der Zweck ist es, **Anfragen aus Hochschulnetzen (z. B. eduroam)** zu identifizieren, um **Research-bezogene Services kostenlos oder bevorzugt bereitzustellen**.
|
||||
|
||||
Das System dient ausschließlich der **Netzwerk-Klassifikation** und **ersetzt keine Authentifizierung**.
|
||||
|
||||
---
|
||||
|
||||
## Ziel
|
||||
|
||||
- Erkennen, ob eine Anfrage aus einem **Hochschul- oder Forschungsnetz** stammt
|
||||
- Bereitstellung eines **Header-Hinweises** für nachgelagerte Services
|
||||
- Grundlage für Entscheidungen wie:
|
||||
- kostenfreie Research-Features
|
||||
- angepasste UI-Hinweise
|
||||
- alternative Rate-Limits
|
||||
|
||||
---
|
||||
|
||||
## Funktionsweise (Kurzfassung)
|
||||
|
||||
```
|
||||
Client
|
||||
→ Traefik
|
||||
→ ForwardAuth
|
||||
→ ASN Detection Service
|
||||
→ Header wird ergänzt
|
||||
```
|
||||
|
||||
1. Die Client-IP wird ermittelt
|
||||
2. Die zugehörige ASN wird lokal nachgeschlagen
|
||||
3. Die ASN wird mit einer NREN-ASN-Liste verglichen
|
||||
4. Das Ergebnis wird als HTTP-Header zurückgegeben
|
||||
|
||||
---
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- **GeoLite2 ASN (MaxMind)**
|
||||
- kostenlos
|
||||
- lokal
|
||||
- monatliche Aktualisierung
|
||||
|
||||
- **NREN-ASN-Liste**
|
||||
- abgeleitet aus PeeringDB
|
||||
- Kategorie: `Research and Education`
|
||||
- monatliche Aktualisierung
|
||||
|
||||
---
|
||||
|
||||
## Bereitgestellte Header
|
||||
|
||||
| Header | Beschreibung |
|
||||
|------|-------------|
|
||||
| `X-ASN` | ASN der Client-IP |
|
||||
| `X-ASN-ORG` | Organisation (optional) |
|
||||
| `X-NREN` | `1` wenn ASN zu einem Hochschul-/Forschungsnetz gehört, sonst `0` |
|
||||
|
||||
---
|
||||
|
||||
## Integration
|
||||
|
||||
Der Service wird als **Traefik ForwardAuth Middleware** eingebunden.
|
||||
Die Header werden über `authResponseHeaders` an die eigentliche Anwendung weitergereicht.
|
||||
|
||||
Der Service ist **nicht öffentlich exponiert** und kommuniziert ausschließlich über das interne Docker-Netzwerk.
|
||||
|
||||
---
|
||||
|
||||
## Update-Strategie
|
||||
|
||||
- monatliche Aktualisierung der ASN-Daten
|
||||
- keine externen Requests während der Anfrageverarbeitung
|
||||
|
||||
---
|
||||
|
||||
## Einschränkungen
|
||||
|
||||
- Die Erkennung ist **heuristisch**
|
||||
- Es gibt **keine Garantie**, dass jede Anfrage aus einem Hochschulnetz erkannt wird
|
||||
- Die Information darf **nicht als Authentifizierungsmerkmal** verwendet werden
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Dieses Projekt ermöglicht eine **performante, datenschutzfreundliche Erkennung von Hochschulnetzen**, um **Research-Angebote kontextabhängig bereitzustellen**, ohne Nutzer zu identifizieren oder externe Dienste zur Laufzeit zu kontaktieren.
|
||||
27
asn-updater/.gitignore
vendored
Normal file
27
asn-updater/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
.DS_Store
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
19
asn-updater/Dockerfile
Normal file
19
asn-updater/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.12-alpine
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata curl tar && update-ca-certificates
|
||||
WORKDIR /app
|
||||
|
||||
COPY update.py /app/update.py
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
COPY healthcheck.sh /app/healthcheck.sh
|
||||
|
||||
RUN pip install --no-cache-dir requests==2.32.3 \
|
||||
&& chmod +x /app/entrypoint.sh /app/healthcheck.sh
|
||||
|
||||
ENV OUT_DIR=/data
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD /app/healthcheck.sh
|
||||
18
asn-updater/LICENSE
Normal file
18
asn-updater/LICENSE
Normal file
@@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 mindboost
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
102
asn-updater/README.md
Normal file
102
asn-updater/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# education-flagger
|
||||
|
||||
Forschung und Bildung sind die wichtigste Investition in die Zukunft, und der Zugang zu ihren Netzwerken verdient besondere Unterstützung.
|
||||
|
||||
Dieses Repo stellt einen **minimalen Microservice** bereit, um **Hochschul- und Forschungsnetzwerke (NRENs)** anhand der **Autonomous System Number (ASN)** zu erkennen. Ziel ist es, **Zugriff oder bevorzugte Behandlung** für Nutzer aus Research- und Education-Netzen zu ermöglichen, ohne personenbezogene Daten zu verarbeiten.
|
||||
|
||||
Das System dient ausschließlich der **Netzwerk-Klassifikation** und **ersetzt keine Authentifizierung**.
|
||||
|
||||
## Ziel
|
||||
|
||||
- Erkennen, ob eine Anfrage aus einem **Hochschul- oder Forschungsnetz** stammt
|
||||
- Bereitstellung eines **Header-Hinweises** für nachgelagerte Services
|
||||
- Grundlage für Entscheidungen wie:
|
||||
- kostenfreie Research-Features
|
||||
- angepasste UI-Hinweise
|
||||
- alternative Rate-Limits
|
||||
|
||||
## Funktionsweise (Kurzfassung)
|
||||
|
||||
```
|
||||
Client
|
||||
-> Traefik
|
||||
-> ForwardAuth
|
||||
-> ASN Detection Service
|
||||
-> Header wird ergänzt
|
||||
```
|
||||
|
||||
1. Die Client-IP wird ermittelt
|
||||
2. Die zugehörige ASN wird lokal nachgeschlagen
|
||||
3. Die ASN wird mit einer NREN-ASN-Liste verglichen
|
||||
4. Das Ergebnis wird als HTTP-Header zurückgegeben
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- **GeoLite2 ASN (MaxMind)**
|
||||
- kostenlos
|
||||
- lokal
|
||||
- monatliche Aktualisierung
|
||||
|
||||
- **NREN-ASN-Liste**
|
||||
- abgeleitet aus PeeringDB
|
||||
- Kategorie: `Research and Education`
|
||||
- monatliche Aktualisierung
|
||||
|
||||
## Bereitgestellte Header
|
||||
|
||||
| Header | Beschreibung |
|
||||
|------|-------------|
|
||||
| `X-ASN` | ASN der Client-IP |
|
||||
| `X-ASN-ORG` | Organisation (optional) |
|
||||
| `X-NREN` | `1` wenn ASN zu einem Hochschul-/Forschungsnetz gehört, sonst `0` |
|
||||
|
||||
## Integration
|
||||
|
||||
Der Service wird als **Traefik ForwardAuth Middleware** eingebunden.
|
||||
Die Header werden über `authResponseHeaders` an die eigentliche Anwendung weitergereicht.
|
||||
|
||||
Der Service ist **nicht öffentlich exponiert** und kommuniziert ausschließlich über das interne Docker-Netzwerk.
|
||||
|
||||
Die dafür vorgesehenen Labels sind:
|
||||
|
||||
# Middleware Definition (ForwardAuth -> asn-header)
|
||||
- "traefik.http.middlewares.asn-enrich.forwardauth.address=http://asn-header:8080/auth"
|
||||
- "traefik.http.middlewares.asn-enrich.forwardauth.trustForwardHeader=true"
|
||||
- "traefik.http.middlewares.asn-enrich.forwardauth.authResponseHeaders=X-ASN,X-ASN-ORG,X-NREN"
|
||||
|
||||
# Middleware am Router aktivieren
|
||||
- "traefik.http.routers.web.middlewares=asn-enrich@docker"
|
||||
|
||||
Bitte füge diese zu dem Service hinzu, bei welchem man die gewünschten Header möchte.
|
||||
|
||||
## Run/Deploy (kurz)
|
||||
|
||||
1. `example.env` kopieren und als `.env` befüllen (mindestens `MAXMIND_LICENSE_KEY`).
|
||||
2. Den Updater-Container starten und `OUT_DIR` als Volume mounten (z. B. `/data`).
|
||||
3. Den ASN-Detection-Service so starten, dass er **denselben** `OUT_DIR` liest.
|
||||
4. Traefik ForwardAuth aktivieren und `authResponseHeaders` durchreichen.
|
||||
5. Nach dem ersten Update sollten `GeoLite2-ASN.mmdb` und `nren_asns.txt` im `OUT_DIR` liegen.
|
||||
|
||||
## example.env (kurz erklärt)
|
||||
|
||||
- `MAXMIND_LICENSE_KEY`: notwendig für den GeoLite2 Download.
|
||||
- `PDB_API_KEY`: optional, reduziert PeeringDB Rate-Limits.
|
||||
- `OUT_DIR`: gemeinsamer Datenpfad zwischen Updater und Detection-Service.
|
||||
- `PDB_BASE`, `PDB_INFO_TYPE`, `PDB_LIMIT`: PeeringDB Filter.
|
||||
- `HTTP_TIMEOUT`: Timeout pro HTTP-Request.
|
||||
- `INTERVAL_SECONDS`: Update-Intervall (Standard 30 Tage).
|
||||
|
||||
## Update-Strategie
|
||||
|
||||
- monatliche Aktualisierung der ASN-Daten
|
||||
- keine externen Requests während der Anfrageverarbeitung
|
||||
|
||||
## Einschränkungen
|
||||
|
||||
- Die Erkennung ist **heuristisch**
|
||||
- Es gibt **keine Garantie**, dass jede Anfrage aus einem Hochschulnetz erkannt wird
|
||||
- Die Information darf **nicht als Authentifizierungsmerkmal** verwendet werden
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Dieses Projekt ermöglicht eine **performante, datenschutzfreundliche Erkennung von Hochschulnetzen**, um **Research-Angebote kontextabhängig bereitzustellen**, ohne Nutzer zu identifizieren oder externe Dienste zur Laufzeit zu kontaktieren.
|
||||
13
asn-updater/entrypoint.sh
Normal file
13
asn-updater/entrypoint.sh
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
INTERVAL_SECONDS="${INTERVAL_SECONDS:-2592000}"
|
||||
echo "[start] updater interval=${INTERVAL_SECONDS}s out_dir=${OUT_DIR:-/data}"
|
||||
|
||||
while true; do
|
||||
echo "[run] update now"
|
||||
python /app/update.py
|
||||
echo "[sleep] ${INTERVAL_SECONDS}s"
|
||||
sleep "${INTERVAL_SECONDS}"
|
||||
done
|
||||
|
||||
19
asn-updater/example.env
Normal file
19
asn-updater/example.env
Normal file
@@ -0,0 +1,19 @@
|
||||
# Required
|
||||
MAXMIND_LICENSE_KEY=
|
||||
|
||||
# Optional (helps with rate limits)
|
||||
PDB_API_KEY=
|
||||
|
||||
# Output data location shared with the detection service
|
||||
OUT_DIR=/data
|
||||
|
||||
# PeeringDB settings
|
||||
PDB_BASE=https://www.peeringdb.com
|
||||
PDB_INFO_TYPE=Educational/Research
|
||||
PDB_LIMIT=250
|
||||
|
||||
# HTTP settings
|
||||
HTTP_TIMEOUT=30
|
||||
|
||||
# Update interval (seconds, default 30 days)
|
||||
INTERVAL_SECONDS=2592000
|
||||
49
asn-updater/healthcheck.sh
Normal file
49
asn-updater/healthcheck.sh
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
OUT_DIR="${OUT_DIR:-/data}"
|
||||
PDB_BASE="${PDB_BASE:-https://www.peeringdb.com}"
|
||||
INFO_TYPE="${PDB_INFO_TYPE:-Educational/Research}"
|
||||
|
||||
if [ -z "${MAXMIND_LICENSE_KEY:-}" ]; then
|
||||
echo "[health] MAXMIND_LICENSE_KEY missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "${OUT_DIR}" ]; then
|
||||
echo "[health] OUT_DIR missing: ${OUT_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -s "${OUT_DIR}/GeoLite2-ASN.mmdb" ]; then
|
||||
echo "[health] GeoLite2-ASN.mmdb missing in ${OUT_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -s "${OUT_DIR}/nren_asns.txt" ]; then
|
||||
echo "[health] nren_asns.txt missing in ${OUT_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mm_url="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz"
|
||||
mm_code="$(curl -fsS -o /dev/null -w "%{http_code}" "${mm_url}" || true)"
|
||||
if [ "${mm_code}" != "200" ]; then
|
||||
echo "[health] MaxMind download not accessible (status ${mm_code})" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pdb_code="000"
|
||||
pdb_url="${PDB_BASE}/api/net"
|
||||
pdb_args="--get --data-urlencode info_type=${INFO_TYPE} --data-urlencode limit=1 --data-urlencode skip=0 --data-urlencode fields=asn,status,info_type"
|
||||
if [ -n "${PDB_API_KEY:-}" ]; then
|
||||
pdb_code="$(curl -fsS -o /dev/null -w "%{http_code}" -H "Accept: application/json" -H "Authorization: Api-Key ${PDB_API_KEY}" ${pdb_args} "${pdb_url}" || true)"
|
||||
else
|
||||
pdb_code="$(curl -fsS -o /dev/null -w "%{http_code}" -H "Accept: application/json" ${pdb_args} "${pdb_url}" || true)"
|
||||
fi
|
||||
|
||||
if [ "${pdb_code}" != "200" ] && [ "${pdb_code}" != "429" ]; then
|
||||
echo "[health] PeeringDB not accessible (status ${pdb_code})" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
107
asn-updater/update.py
Normal file
107
asn-updater/update.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import os, time, json, tarfile, tempfile, shutil
|
||||
import requests
|
||||
|
||||
OUT_DIR = os.getenv("OUT_DIR", "/data")
|
||||
LICENSE_KEY = os.getenv("MAXMIND_LICENSE_KEY", "").strip()
|
||||
PDB_API_KEY = os.getenv("PDB_API_KEY", "").strip()
|
||||
PDB_BASE = os.getenv("PDB_BASE", "https://www.peeringdb.com")
|
||||
INFO_TYPE = os.getenv("PDB_INFO_TYPE", "Educational/Research")
|
||||
TIMEOUT = int(os.getenv("HTTP_TIMEOUT", "30"))
|
||||
LIMIT = int(os.getenv("PDB_LIMIT", "250"))
|
||||
|
||||
def atomic_replace(src_path: str, dst_path: str) -> None:
|
||||
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
||||
tmp = dst_path + ".tmp"
|
||||
shutil.copyfile(src_path, tmp)
|
||||
os.replace(tmp, dst_path)
|
||||
|
||||
def download_maxmind_mmdb() -> None:
|
||||
if not LICENSE_KEY:
|
||||
raise RuntimeError("MAXMIND_LICENSE_KEY missing")
|
||||
|
||||
# Offizieller GeoLite2 Download-Mechanismus per license_key + edition_id
|
||||
url = (
|
||||
"https://download.maxmind.com/app/geoip_download"
|
||||
f"?edition_id=GeoLite2-ASN&license_key={LICENSE_KEY}&suffix=tar.gz"
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tgz = os.path.join(td, "GeoLite2-ASN.tar.gz")
|
||||
r = requests.get(url, timeout=TIMEOUT)
|
||||
r.raise_for_status()
|
||||
with open(tgz, "wb") as f:
|
||||
f.write(r.content)
|
||||
|
||||
mmdb_found = None
|
||||
with tarfile.open(tgz, "r:gz") as tar:
|
||||
for member in tar.getmembers():
|
||||
if member.name.endswith("GeoLite2-ASN.mmdb"):
|
||||
tar.extract(member, path=td)
|
||||
mmdb_found = os.path.join(td, member.name)
|
||||
break
|
||||
|
||||
if not mmdb_found or not os.path.exists(mmdb_found):
|
||||
raise RuntimeError("GeoLite2-ASN.mmdb not found in archive")
|
||||
|
||||
atomic_replace(mmdb_found, os.path.join(OUT_DIR, "GeoLite2-ASN.mmdb"))
|
||||
|
||||
def pdb_headers():
|
||||
if not PDB_API_KEY:
|
||||
return {"Accept": "application/json"}
|
||||
# PeeringDB API Key (optional)
|
||||
return {"Accept": "application/json", "Authorization": f"Api-Key {PDB_API_KEY}"}
|
||||
|
||||
def fetch_pdb_page(skip: int):
|
||||
url = f"{PDB_BASE}/api/net"
|
||||
params = {
|
||||
"info_type": INFO_TYPE,
|
||||
"limit": LIMIT,
|
||||
"skip": skip,
|
||||
"fields": "asn,status,info_type",
|
||||
}
|
||||
r = requests.get(url, params=params, headers=pdb_headers(), timeout=TIMEOUT)
|
||||
r.raise_for_status()
|
||||
j = r.json()
|
||||
return j.get("data", [])
|
||||
|
||||
def update_nren_asns() -> None:
|
||||
asns = set()
|
||||
skip = 0
|
||||
while True:
|
||||
data = fetch_pdb_page(skip)
|
||||
for obj in data:
|
||||
if obj.get("status") != "ok":
|
||||
continue
|
||||
asn = obj.get("asn")
|
||||
if isinstance(asn, int) and asn > 0:
|
||||
asns.add(asn)
|
||||
if len(data) < LIMIT:
|
||||
break
|
||||
skip += LIMIT
|
||||
time.sleep(1.1) # sehr konservativ
|
||||
|
||||
out_txt = os.path.join(OUT_DIR, "nren_asns.txt")
|
||||
with tempfile.NamedTemporaryFile("w", delete=False) as f:
|
||||
for a in sorted(asns):
|
||||
f.write(f"{a}\n")
|
||||
tmp_path = f.name
|
||||
os.replace(tmp_path, out_txt)
|
||||
|
||||
def write_meta():
|
||||
meta = {
|
||||
"updated_at_unix": int(time.time()),
|
||||
"info_type": INFO_TYPE,
|
||||
"pdb_base": PDB_BASE,
|
||||
}
|
||||
with open(os.path.join(OUT_DIR, "metadata.json"), "w") as f:
|
||||
json.dump(meta, f, indent=2)
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
download_maxmind_mmdb()
|
||||
update_nren_asns()
|
||||
write_meta()
|
||||
print("[ok] updated mmdb + nren_asns")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
36
docker-compose.yml
Normal file
36
docker-compose.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
services:
|
||||
asn-header:
|
||||
build: .
|
||||
container_name: asn-header
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
MMDB_PATH: /data/GeoLite2-ASN.mmdb
|
||||
ASN_LIST_PATH: /data/nren_asns.txt
|
||||
ADDR: ":8080"
|
||||
volumes:
|
||||
- asn_data:/data:ro
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
asn-updater:
|
||||
build: ./asn-updater
|
||||
container_name: asn-updater
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
OUT_DIR: /data
|
||||
PDB_INFO_TYPE: "Research and Education"
|
||||
INTERVAL_SECONDS: "${UPDATE_INTERVAL_SECONDS}"
|
||||
volumes:
|
||||
- asn_data:/data
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
name: ${PROXY_NETWORK}
|
||||
|
||||
volumes:
|
||||
asn_data:
|
||||
6
go.mod
Normal file
6
go.mod
Normal file
@@ -0,0 +1,6 @@
|
||||
module asn-header-service
|
||||
|
||||
go 1.22
|
||||
|
||||
require github.com/oschwald/maxminddb-golang v1.13.1
|
||||
|
||||
158
main.go
Normal file
158
main.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
)
|
||||
|
||||
type asnRecord struct {
|
||||
ASN uint `maxminddb:"autonomous_system_number"`
|
||||
Org string `maxminddb:"autonomous_system_organization"`
|
||||
}
|
||||
|
||||
type server struct {
|
||||
db *maxminddb.Reader
|
||||
nrenASNs map[uint]struct{}
|
||||
ready atomic.Bool
|
||||
versionTag string
|
||||
}
|
||||
|
||||
func loadASNSet(path string) (map[uint]struct{}, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
set := make(map[uint]struct{}, 4096)
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
line := strings.TrimSpace(sc.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
v, err := strconv.ParseUint(line, 10, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
set[uint(v)] = struct{}{}
|
||||
}
|
||||
return set, sc.Err()
|
||||
}
|
||||
|
||||
func firstForwardedFor(r *http.Request) string {
|
||||
xff := r.Header.Get("X-Forwarded-For")
|
||||
if xff == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(xff, ",")
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
|
||||
func remoteIP(r *http.Request) string {
|
||||
// Prefer XFF (because Traefik is proxy)
|
||||
ip := firstForwardedFor(r)
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err == nil {
|
||||
return host
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
func (s *server) authHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.ready.Load() {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
ipStr := remoteIP(r)
|
||||
parsed := net.ParseIP(ipStr)
|
||||
if parsed == nil {
|
||||
// Always 200: we enrich, not block
|
||||
w.Header().Set("X-NREN", "0")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
var rec asnRecord
|
||||
if err := s.db.Lookup(parsed, &rec); err != nil || rec.ASN == 0 {
|
||||
w.Header().Set("X-NREN", "0")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("X-ASN", strconv.FormatUint(uint64(rec.ASN), 10))
|
||||
if rec.Org != "" {
|
||||
// optional: keep it short; some org strings can be long
|
||||
w.Header().Set("X-ASN-ORG", rec.Org)
|
||||
}
|
||||
|
||||
_, ok := s.nrenASNs[rec.ASN]
|
||||
if ok {
|
||||
w.Header().Set("X-NREN", "1")
|
||||
} else {
|
||||
w.Header().Set("X-NREN", "0")
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("X-Service", s.versionTag)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func main() {
|
||||
mmdbPath := getenv("MMDB_PATH", "/data/GeoLite2-ASN.mmdb")
|
||||
asnListPath := getenv("ASN_LIST_PATH", "/data/nren_asns.txt")
|
||||
addr := getenv("ADDR", ":8080")
|
||||
version := getenv("VERSION_TAG", "asn-header-service")
|
||||
|
||||
db, err := maxminddb.Open(mmdbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open mmdb: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
set, err := loadASNSet(asnListPath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load asn list: %v", err)
|
||||
}
|
||||
|
||||
s := &server{db: db, nrenASNs: set, versionTag: version}
|
||||
s.ready.Store(true)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/auth", s.authHandler)
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 2 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("listening on %s (asn_count=%d)", addr, len(set))
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
|
||||
func getenv(k, def string) string {
|
||||
v := strings.TrimSpace(os.Getenv(k))
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user