diff --git a/apps/security/Eduroam Analyzer/.env.example b/apps/security/Eduroam Analyzer/.env.example new file mode 100644 index 0000000..d1221fb --- /dev/null +++ b/apps/security/Eduroam Analyzer/.env.example @@ -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 diff --git a/apps/security/Eduroam Analyzer/.gitignore b/apps/security/Eduroam Analyzer/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/apps/security/Eduroam Analyzer/.gitignore @@ -0,0 +1 @@ +.env diff --git a/apps/security/Eduroam Analyzer/Dockerfile b/apps/security/Eduroam Analyzer/Dockerfile new file mode 100644 index 0000000..b239585 --- /dev/null +++ b/apps/security/Eduroam Analyzer/Dockerfile @@ -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"] + diff --git a/apps/security/Eduroam Analyzer/README.md b/apps/security/Eduroam Analyzer/README.md new file mode 100644 index 0000000..5112f55 --- /dev/null +++ b/apps/security/Eduroam Analyzer/README.md @@ -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. diff --git a/apps/security/Eduroam Analyzer/README_technical.md b/apps/security/Eduroam Analyzer/README_technical.md new file mode 100644 index 0000000..5112f55 --- /dev/null +++ b/apps/security/Eduroam Analyzer/README_technical.md @@ -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. diff --git a/apps/security/Eduroam Analyzer/asn-updater b/apps/security/Eduroam Analyzer/asn-updater index 5870ab9..d36a1e7 160000 --- a/apps/security/Eduroam Analyzer/asn-updater +++ b/apps/security/Eduroam Analyzer/asn-updater @@ -1 +1 @@ -Subproject commit 5870ab952f609193bc01e9aaa04b5c874583001e +Subproject commit d36a1e765521aebd6dcb69e795f0f25d75b64c99 diff --git a/apps/security/Eduroam Analyzer/docker-compose.yml b/apps/security/Eduroam Analyzer/docker-compose.yml new file mode 100644 index 0000000..cdd6463 --- /dev/null +++ b/apps/security/Eduroam Analyzer/docker-compose.yml @@ -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: diff --git a/apps/security/Eduroam Analyzer/go.mod b/apps/security/Eduroam Analyzer/go.mod new file mode 100644 index 0000000..815e898 --- /dev/null +++ b/apps/security/Eduroam Analyzer/go.mod @@ -0,0 +1,6 @@ +module asn-header-service + +go 1.22 + +require github.com/oschwald/maxminddb-golang v1.13.1 + diff --git a/apps/security/Eduroam Analyzer/main.go b/apps/security/Eduroam Analyzer/main.go new file mode 100644 index 0000000..bc7485d --- /dev/null +++ b/apps/security/Eduroam Analyzer/main.go @@ -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 +} +