ProjekteFRITZ!Box-Verlauf
FRITZ!Box-Verlauf
Die FRITZ!Box behält nur wenige hundert Protokollzeilen und kennt in ihrer Geräteliste nur den Zustand „jetzt“. Ein Collector in Go holt beides regelmäßig ab und schreibt jedes Verbinden und Trennen dauerhaft weg. Die Oberfläche zeigt dann über Monate, wann welches Gerät im Netz war.
Privat, nur im eigenen VPN erreichbar. Der Quellcode ist nicht öffentlich.

Eckdaten
Stand
- Betrieb
- seit 19.10.2026 auf dem eigenen Server, die Box hängt per WireGuard daran
- Tests
- 108 Go-Tests, mit Untertests 246, dazu 212 pgTAP-Prüfungen der Datenbank, 92 Vitest-Fälle und 10 Playwright-Tests mit axe
- Zugang
- ein Besitzer, Anmeldung nur auf Einladung, keine Registrierung
Das Gerät ist die Einheit, nicht die Zeile
Aus Protokollzeilen und Geräteliste werden Ereignisse, aus Ereignissen Sitzungen mit Beginn, Ende und Dauer. Erkannt wird ein Gerät an seiner MAC-Adresse. Handys wechseln oft auf eine neue, private Adresse, deshalb lassen sich Adressen von Hand zu einem Gerät zusammenführen und wieder lösen.
Für ausgewählte Geräte kommt eine Mail, wenn sie sich verbinden oder trennen, mit mindestens 15 Minuten Abstand. Unbekannte Geräte lösen keine Mail aus.

Eine Lücke ist kein „offline“
Erreicht der Collector die Box eine Zeit lang nicht, weiß niemand, was in dieser Zeit passiert ist. Die Oberfläche zeigt das als Lücke und nicht als Zeit, in der alle Geräte weg waren.
Nach einer Lücke gleicht der Collector Protokoll und Geräteliste ab, damit Ereignisse zwischen zwei Abfragen nicht verloren gehen. Die Rohereignisse werden nur angehängt, nie geändert. Sitzungen lassen sich daraus jederzeit neu berechnen.
Im Lasttest mit 4,6 Millionen Ereignissen brauchte die erste Abfrage nach einem Ausfall anfangs 333 Sekunden. Nach dem Umbau von Lückenlauf und Indizes sind es 0,18 Sekunden, mit kaltem Cache bis zu 10.

Vom Protokoll in die Datenbank
Die Box schreibt ihre Zeilen auf Deutsch und ohne Zeitzone. In der Nacht der Zeitumstellung gibt es die Stunde von 2 bis 3 Uhr zweimal. Der Parser löst das über den Zeitpunkt der Abfrage auf: Neuere Zeilen stehen oben, also kann keine ältere Zeile später liegen als die darüber.
Jede Zeile bekommt einen Schlüssel aus Meldungsnummer, Text und Zeitpunkt. Holt der Collector dieselbe Zeile noch einmal, verwirft die Datenbank sie. Lesen darf nur, wer als Besitzer eingetragen ist. Das prüft Postgres selbst über Row Level Security.
Auszüge aus dem echten Code, gekürzt.
Echter Code aus dem FRITZ!Box-Verlauf
collector/internal/logparse/logparse.go
func Resolve(lines []model.LogLine, loc *time.Location, observedAt time.Time) { ceiling := observedAt.Add(jitter) for i := range lines { if lines[i].At.IsZero() { if wall, ok := wallTime(lines[i].Raw); ok { lines[i].At = localTime(wall, loc, ceiling) } } if at := lines[i].At; !at.IsZero() && at.Add(jitter).Before(ceiling) { ceiling = at.Add(jitter) } }}
collector/internal/logparse/logparse.go
func dedupeKey(id int, text string, at time.Time) string { sum := sha256.Sum256(fmt.Appendf(nil, "%d|%s|%s", id, text, at.UTC().Truncate(time.Second).Format(time.RFC3339))) return hex.EncodeToString(sum[:])}
supabase/migrations/20261002000300_collector.sql
… format('hostlist:%s:%s:%s', hosts.mac, case when hosts.active then 'connected' else 'disconnected' end, extract(epoch from p_observed_at)) from hosts where hosts.active <> hosts.was_open on conflict (dedupe_key) do nothing
supabase/migrations/20261002000500_access.sql
create policy "owner reads events" on public.events for select to authenticated using ((select public.is_owner()));create policy "owner reads sessions" on public.sessions for select to authenticated using ((select public.is_owner()));
Betrieb
Fünf Container auf dem eigenen Server: Datenbank, Anmeldung, REST-Schnittstelle, Oberfläche und Collector. Datenbank und Schnittstellen hängen in internen Netzen ohne Ausgang, nur der Collector darf zur Box und zum Mailserver.
- Collector spricht TR-064 mit Digest-Anmeldung, nur mit der Standardbibliothek von Go
- Box über WireGuard angebunden, Oberfläche nur aus dem VPN erreichbar
- Alle Container außer der Datenbank mit schreibgeschütztem Dateisystem, alle mit Speicher- und Prozessgrenzen und nur den nötigen Linux-Capabilities
- Collector-Image auf Basis von distroless, Caddy selbst gebaut
- tägliche Sicherung der Datenbank, mit age verschlüsselt, dazu ein Skript, das die Wiederherstellung prüft
- Kettentest von der nachgebauten Box über Collector und Datenbank bis zur Oberfläche und zur Mail
Technik
- Collector
- Go 1.27, einzige direkte Abhängigkeit pgx
- Daten
- PostgreSQL 17 mit GoTrue und PostgREST aus dem Supabase-Stack, Logik als SQL-Funktionen, getestet mit pgTAP
- Oberfläche
- React 19, Vite, TanStack Query und Router, React Aria
- Betrieb
- Docker Compose hinter Caddy, WireGuard zur Box, Mails über SMTP
- CI
- go test -race, golangci-lint, govulncheck, pgTAP, Vitest, Playwright, Trivy und Suche nach Geheimnissen
Zugang
- Adresse nur im eigenen VPN
- Quellcode privat
Das Werkzeug läuft im eigenen Heimnetz und zeigt echte Geräte. Deshalb gibt es keine Demo. Die Screenshots oben stammen aus der Entwicklung mit erfundenen Geräten.