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