# Konzept: Geteiltes Brand-Token-Matching

Status: **Konzept abgenommen (2026-05-22) — im Backlog, Umsetzung später (§10)**
Datum: 2026-05-22
Scope: Layer 1 (DNS-Sinkhole / PacketTunnel) + Mail-Scan (Mo). NICHT Layer 2.

---

## 1. Problem

Glücksspiel-Marken umgehen exakte Blocklisten mit dem **Nummern-/Suffix-Trick**:
dieselbe wiedererkennbare Marke, variierende Zahl/Suffix/TLD.

```
slotoro.bet  →  slotoro88.bet · slotoro2.com · slotoro-casino.net · slotorobet.io
winrolla.com →  winrolla57.com · winrolla-bet.net
```

- **Layer 1** (DNS-Hash-Set, 329k Hashes): matcht nur **exakt**. Hashes erlauben
  kein Substring-/Prefix-Matching — `slotoro88.bet` hasht zu etwas völlig anderem
  als `slotoro.bet`. Architektur-bedingt.
- **Layer 2** (Apple `ManagedSettings`-`WebDomain`): matcht nur exakte Domain +
  Subdomains. Keine Substring-API. **Nicht erweiterbar** — out of scope.
- **Mail-Scan (Mo)**: hat dasselbe Problem (`winrolla → winrolla57`).

Die HaGeZi-Feed-Liste fängt viele Varianten mit **Latenz** — der Feed muss die
neue Variante erst aufnehmen. Brand-Token-Matching schließt genau diese
Latenzlücke und greift sofort bei jeder Variante einer **bekannten** Marke.

---

## 2. Ziel & Scope

**Ziel:** Ein einziges, geteiltes Brand-Token-System — dieselbe kuratierte
Token-Liste + derselbe Matching-Algorithmus für Layer 1 (DNS) und Mail-Scan.

**Im Scope:** Varianten **bekannter** Marken abfangen.
**Nicht im Scope:**
- Brandneue Marken ohne Token (kein Token → kein Match).
- Layer 2 (Apple-API kann es nicht).
- Ersatz für Feed/Blocklist — das System **ergänzt** sie, ersetzt sie nicht.

---

## 3. Kernidee: der Brand-Token

Ein **Brand-Token** = ein distinktiver, kleingeschriebener Marken-Kern, der eine
Glücksspielmarke identifiziert und in legitimen Domains praktisch nicht vorkommt.

```
Gute Tokens:    slotoro · winrolla · spinrollz · wazamba
Schlechte Tokens: bet · win · play · casino · slot · vegas
                  (Wörterbuch-/Generikwörter → False-Positive-Magneten)
```

Regel: **kuratiert, nie automatisch.** Mindestlänge **≥ 5 Zeichen**. Kürzere
Marken bleiben bei der exakten Blocklist.

---

## 4. Der Matching-Algorithmus (das geteilte Stück)

Der entscheidende Teil. **Kein freies Substring** (`host.contains(token)` ist
gefährlich). Stattdessen **Entnummerierung + Segment-Exaktvergleich**:

### Ablauf (pro Host)

```
1. Host in Labels splitten an '.'        slotoro88.bet → ["slotoro88", "bet"]
2. Pro Label: in Segmente splitten an '-'  play-slotoro → ["play", "slotoro"]
3. Pro Segment:
   a. Trailing-Ziffernlauf abschneiden → `core`   slotoro88 → slotoro
   b. core EXAKT in tokenSet?                     → MATCH
   c. sonst: core beginnt mit einem Token UND
      der Rest ∈ genericSuffixSet?                → MATCH
   d. sonst                                       → kein Match
4. Kein Segment matcht → kein Block.
```

`genericSuffixSet` = generische Glücksspiel-Anhängsel, die nur als **Rest nach
einem Token** zählen (nie standalone): `bet bets casino casinos slot slots spin
spins win wins play vegas game games poker sport sports wetten lotto gambling`.

### Warum das robust ist

Es ist **Exaktvergleich auf dem entnummerierten Segment**, kein Substring.
Ein kurzes Token wie `win` würde `winter` NICHT treffen: `winter` → core
`winter` → `winter` ≠ `win` → kein Match. False Positives entstehen nur, wenn
ein legitimes Segment **exakt** ein Token ist — bei distinktiven Tokens ~null.

### Beispiele

| Host | Segment-core | Ergebnis |
|---|---|---|
| `slotoro.bet` | `slotoro` | MATCH (exakt) |
| `slotoro88.bet` | `slotoro88`→`slotoro` | MATCH (entnummeriert) |
| `slotoro2.com` | `slotoro2`→`slotoro` | MATCH |
| `play-slotoro.net` | Segment `slotoro` | MATCH |
| `slotoro-casino.io` | Segment `slotoro` | MATCH |
| `slotorobet.com` | `slotorobet` = `slotoro`+`bet` | MATCH (Token+Suffix) |
| `stat.slotoro88.bet` | Label `slotoro88`→`slotoro` | MATCH |
| `winter.com` | `winter` | kein Match (≠ Token) |
| `slotorox.com` | `slotorox` = `slotoro`+`x`, `x`∉Suffixe | kein Match |

### Verbindlichkeit

Der Algorithmus muss **bit-identisch** in Swift (PacketTunnel) und TS
(Mo-Scan + Backend-Validierung) implementiert sein — analog zu
`domainHash.ts` ↔ `DomainHasher.swift`. Ein gemeinsamer Spec-Testvektor-Satz
(Input-Host → erwartetes Ergebnis) wird in beiden Test-Suites geprüft.

---

## 5. Token-Liste: Kuration & Quelle

- **DB-Tabelle** `brand_tokens` (token, source, addedAt, isActive, note).
- **Kuratiert von ReBreak-Admins** — nie auto-generiert (FP-Risiko).
- **Bootstrap:** die existierende 329k-Blocklist nach gemeinsamen SLD-Präfix-
  Clustern minen → Token-Vorschläge → menschliche Freigabe.
- **Laufende Quelle:** wenn ein User über die Custom-Domain-/Submission-Flows
  eine offensichtliche Variante einreicht, kann der Admin statt nur der Domain
  das **Brand-Token** promoten.
- **Allowlist-Notausgang** (`brand_token_allowlist`): falls ein Token doch
  kollidiert, einzelne legitime Domains explizit ausnehmen. v1 optional, im
  Design vorgesehen.

---

## 6. Verteilung aufs Gerät (Layer 1)

- Neuer Endpoint `GET /api/url-filter/brand-tokens` — JSON/Zeilenliste, ETag-
  gecacht. Spiegelt exakt den `blocklist.bin`-Sync-Mechanismus:
  App lädt → schreibt in App-Group → Darwin-Notification → Extension reloadet.
- Datei klein (~wenige KB bei einigen hundert Tokens).
- **File-Protection `.completeUntilFirstUserAuthentication`** — wie nach dem
  Layer-1-Bugfix (2026-05-22), damit die Extension sie auch bei gesperrtem
  Gerät lesen kann.
- **Privacy-Abwägung (ehrlich):** die Hash-Liste ist gehasht, damit keine
  Klartext-Casino-Domains auf dem Gerät eines Spielers liegen. Brand-Tokens
  sind Klartext-Markenfragmente — Substring-Matching gegen Hashes ist
  unmöglich, Klartext ist also nötig. Es sind aber nur einige hundert
  Markenfragmente, kein browsing-history-verräterischer Datensatz. Datei
  file-protected. **Entscheidung (2026-05-22): Klartext akzeptiert.**
  `hans-mueller`-DSGVO-Gegencheck bei der Umsetzung (kein Blocker fürs Konzept).

---

## 7. Integration

### Layer 1 — `DnsFilter.classify`
Nach dem `hashList.matchesAnySuffix(domain)`-**Miss**, vor `.forward`:

```
if hashList-Miss:
    if brandTokens.matches(domain):   // der Algo aus §4
        return .block
return .forward
```

Neue Klasse `BrandTokenList` (Pendant zu `HashList`) — lädt die Token-Datei,
reloadet via Darwin-Notification. Performance: pro verfehlter Query ein paar
Set-Lookups → Mikrosekunden, NE-Memory-unkritisch.

### Mail (Mo)
Derselbe `brandTokens.matches(...)` auf die Absender-Domain (und optional den
Display-Namen) anwenden. Owner der Integration bleibt Mo; das geteilte Stück
ist die Token-Liste + der §4-Algorithmus.

---

## 8. Was es NICHT tut (ehrliche Grenzen)

- Keine **neuen** Marken (ohne Token kein Match).
- Glued-Varianten ohne Ziffern/Trenner/Generik-Suffix (`slotoroxyz.com`) →
  Miss; bleibt Sache des Feeds.
- Kein Layer-2-Support.
- False Positives werden stark reduziert, nicht zu 100% eliminiert → Allowlist.

---

## 9. Entscheidungen

**Entschieden (2026-05-22):**
1. **Privacy:** Klartext-Brand-Tokens auf dem Gerät akzeptiert, file-protected.
   `hans-mueller`-DSGVO-Gegencheck bei der Umsetzung (kein Blocker fürs Konzept).
2. **Generic-Suffix-Regel (§4c):** drin — v1 enthält Token+Suffix-Matching
   (`slotorobet` = `slotoro`+`bet`).

**Noch offen (bei Umsetzung klären):**
3. **Token-Kuration:** rein Admin, oder community-/submission-gespeist?
4. **Sync-Kanal:** eigener Endpoint, oder in den `blocklist.bin`-Sync bündeln?
5. **Bootstrap-Aufwand:** Mining der 329k-Liste nach Präfix-Clustern — wie viel
   manuelle Freigabe ist realistisch?

---

## 10. Rollout-Phasen

- **Phase 1:** DB-Tabelle + §4-Algo-Spec + Testvektoren + Bootstrap-Mining +
  Admin-Kuration-UI.
- **Phase 2:** Endpoint + Sync + `BrandTokenList` + `DnsFilter`-Einbau (Layer 1).
- **Phase 3:** Mo-Mail-Integration.
- Hinter Feature-Flag, mit FP-Report-Pfad zum Monitoring.
