Commit d50aa4c9 authored by jan.koester's avatar jan.koester
Browse files

test

parent 3508aac1
Loading
Loading
Loading
Loading
+1 −0
Changes for CMakeLists.txt: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ set(PROXYPLUS_CORE_SOURCES
    src/security/DnsTxtResolver.cpp
    src/security/DkimAdapter.cpp
    src/security/DmarcAdapter.cpp
    src/security/SpfAdapter.cpp
    src/tls/CertificateManager.cpp
    src/lb/LoadBalancer.cpp
)
+30 −14
Changes for docs/ARCHITECTURE.md: 30 added lines, 14 removed lines.
Original line number Diff line number Diff line
@@ -144,16 +144,32 @@ DKIM-Signatur-Header vor dem restlichen Body eingefügt wird (siehe §6c).
  `ROLES.md`) und die Admin-GUI. Trägt die MAIL-FROM-Domain einer Nachricht einen passenden,
  aktivierten Schlüssel, signiert `DataEvent` sie vor der Weiterleitung — außer sie trägt
  bereits eine `DKIM-Signature` (kein Doppel-Signieren beim reinen Durchreichen).
- **Eingehend (Verifizieren) — nur beratend.** Trägt eine durchgereichte Nachricht bereits eine
  `DKIM-Signature`, verifiziert `DataEvent` sie (`DkimAdapter::verify()` holt den Public Key
  selbst per DNS, braucht keine Konfiguration) und wertet das Ergebnis per `DmarcAdapter`
  gegen die `_dmarc.<domain>`-Policy aus. Das Ergebnis wird als Standard-`Authentication-Results`-
  Header (RFC 8601) angehängt, **nicht** zum Zurückweisen/Quarantänisieren verwendet: ProxyPlus
  ist an dieser Stelle ein Relay-Hop, kein finaler Zustellagent, und die Auswertung hat zwei
  bekannte Lücken (kein SPF — `spfPass` ist immer `false`, siehe §8; `fromDomain` wird aus der
  Umschlag-MAIL-FROM-Adresse angenähert statt aus einem geparsten RFC5322.From-Header). Eine
  spätere Durchsetzung (Reject/Quarantäne nach Policy) bräuchte zuerst eine echte
  SPF-Implementierung und RFC5322.From-Parsing, um keine legitime Post fälschlich abzulehnen.
- **Eingehend (Verifizieren) — nur beratend.** Für jede Nachricht prüft `DataEvent` zuerst SPF
  (`SpfAdapter::check(domain, clientIp)`, eigene RFC-7208-Implementierung, siehe unten) gegen
  die MAIL-FROM-Domain (oder die HELO-Domain bei leerem MAIL FROM, RFC 7208 §2.4). Trägt die
  Nachricht bereits eine `DKIM-Signature`, verifiziert `DataEvent` sie zusätzlich
  (`DkimAdapter::verify()` holt den Public Key selbst per DNS, braucht keine Konfiguration).
  Beide Ergebnisse gehen in `DmarcAdapter::evaluate()` gegen die `_dmarc.<domain>`-Policy ein.
  Alles zusammen wird als Standard-`Authentication-Results`-Header (RFC 8601) angehängt,
  **nicht** zum Zurückweisen/Quarantänisieren verwendet: ProxyPlus ist an dieser Stelle ein
  Relay-Hop, kein finaler Zustellagent, und die DMARC-Auswertung hat weiterhin eine bekannte
  Lücke (`fromDomain` wird aus der Umschlag-MAIL-FROM-Adresse angenähert statt aus einem
  geparsten RFC5322.From-Header, siehe §8). Eine spätere Durchsetzung (Reject/Quarantäne nach
  Policy) bräuchte zuerst RFC5322.From-Parsing, um keine legitime Post fälschlich abzulehnen.

## 6d. SPF (2026-08-07)

`SpfAdapter` (`src/security/SpfAdapter.{h,cpp}`) ist eine eigene RFC-7208-Implementierung auf
Basis derselben hand-geschriebenen DNS-Abfrage (`DnsTxtResolver`, um A/AAAA/MX-Lookups
erweitert) wie DKIM/DMARC — keine externe SPF-Bibliothek. Braucht **keine** eigene
Konfiguration (rein DNS-getrieben wie DMARC), daher keine neue ConfigStore-Tabelle/API/GUI.
Unterstützt `all`/`ip4`/`ip6`/`a`/`mx` (inkl. dual-cidr-length) sowie `include` (rekursiv) und
den `redirect`-Modifier, mit dem von der RFC vorgeschriebenen 10-DNS-Lookup-Budget (§4.6.4).
Bekannte Lücken: keine Makro-Expansion (`%{...}`) irgendwo — ein Domain-Spec mit `%` wird als
nicht auswertbar übersprungen statt falsch interpretiert; `exists`/`ptr`-Mechanismen werden
syntaktisch erkannt (kein PermError), zählen aber nie als Treffer (`exists` bräuchte
Makro-Expansion, `ptr` ist von der RFC selbst als deprecated markiert, §5.5); der `exp=`
Modifier wird geparst und ignoriert (rein kosmetischer Erklärtext).

## 6a. Let's Encrypt / ZeroSSL / ACME (TLS-Zertifikatsbeschaffung)

@@ -306,10 +322,10 @@ proxyplus/
  Durchsetzung). Bekannte Lücken gegenüber dem vollen RFC: kein Fallback auf die
  Organisationsdomain bei DMARC (RFC 7489 §6.6.3), relaxed-Alignment approximiert einen
  Public-Suffix-List-Abgleich nur über einen Suffix-Vergleich, `pct=` wird geparst aber nicht
  gesampelt (Policy wird immer vollständig angewendet), **keine SPF-Implementierung** —
  `spfPass` wird von `DataEvent` deshalb immer als `false` übergeben, SPF-Alignment kann also
  nie zum DMARC-Pass beitragen (nur DKIM-Alignment kann), und `fromDomain` ist die
  Umschlag-MAIL-FROM-Domain, kein geparster RFC5322.From-Header.
  gesampelt (Policy wird immer vollständig angewendet), und `fromDomain` ist weiterhin die
  Umschlag-MAIL-FROM-Domain, kein geparster RFC5322.From-Header. `spfPass` kommt seit
  2026-08-07 von einer echten SPF-Implementierung (`SpfAdapter`, §6d) statt fest `false` zu
  sein -- Details/Lücken dort.
- Gruppen-Anlage (`addGroup`) fehlt aktuell im `authdb::client`-API (nur `listGroups()` lesend,
  `GPOadd` schreibend) — `scripts/authdb-bootstrap.sh` dokumentiert das explizit als
  authdb-seitige Lücke statt sie zu umgehen.
+15 −5
Changes for src/security/SpfAdapter.cpp: 15 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -161,19 +161,29 @@ AMxSpec parseAMxTail(const std::string &tail, const std::string &currentDomain)
        return spec;
    }

    // Dual-cidr-length: ["/" ip4-cidr-length] ["//" ip6-cidr-length]. The two forms share a
    // leading '/', so "//64" (v6 prefix only) must be told apart from "/24" (v4 prefix,
    // optionally followed by its own "//64") before parsing digits out of either.
    if (!s.empty()) {
        size_t doubleSlash = s.find("//");
        if (s.compare(0, 2, "//") == 0) {
            if (!parsePrefixLen(s.substr(2), 128, spec.v6Prefix)) {
                spec.malformed = true;
                return spec;
            }
        } else if (s[0] == '/') {
            size_t doubleSlash = s.find("//", 1);
            std::string v4part = doubleSlash == std::string::npos ? s.substr(1) : s.substr(1, doubleSlash - 1);
        if (!v4part.empty() && !parsePrefixLen(v4part, 32, spec.v4Prefix)) {
            if (v4part.empty() || !parsePrefixLen(v4part, 32, spec.v4Prefix)) {
                spec.malformed = true;
                return spec;
            }
        if (doubleSlash != std::string::npos) {
            std::string v6part = s.substr(doubleSlash + 2);
            if (!parsePrefixLen(v6part, 128, spec.v6Prefix)) {
            if (doubleSlash != std::string::npos &&
                !parsePrefixLen(s.substr(doubleSlash + 2), 128, spec.v6Prefix)) {
                spec.malformed = true;
                return spec;
            }
        } else {
            spec.malformed = true;
        }
    }
    return spec;
+33 −19
Changes for src/smtp/SmtpProxyServer.cpp: 33 added lines, 19 removed lines.
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@

#include "../security/DkimAdapter.h"
#include "../security/DmarcAdapter.h"
#include "../security/SpfAdapter.h"

namespace proxyplus::smtpproxy {

@@ -30,6 +31,18 @@ std::string dmarcResultTag(const security::DmarcResult &r) {
    }
}

std::string spfResultTag(security::SpfResult r) {
    switch (r) {
        case security::SpfResult::Pass: return "pass";
        case security::SpfResult::Fail: return "fail";
        case security::SpfResult::SoftFail: return "softfail";
        case security::SpfResult::Neutral: return "neutral";
        case security::SpfResult::TempError: return "temperror";
        case security::SpfResult::PermError: return "permerror";
        default: return "none";
    }
}

} // namespace

SmtpProxyController::SmtpProxyController(std::vector<netplus::socket *> serverSockets,
@@ -173,31 +186,32 @@ bool SmtpProxyController::DataEvent(netplus::con &curcon, const libsmtppp::SmtpE
        }
    }

    // Inbound DKIM/DMARC evaluation: advisory only, tagged as a standard RFC 8601
    // Inbound SPF/DKIM/DMARC evaluation: advisory only, tagged as a standard RFC 8601
    // Authentication-Results header rather than used to reject/quarantine -- ProxyPlus is a
    // relay hop, not a final delivery agent, and this evaluation has two known gaps: no SPF
    // implementation exists yet (spfPass is always false, see docs/ARCHITECTURE.md §8), and
    // relay hop, not a final delivery agent, and the DMARC piece still has one known gap:
    // fromDomain is approximated from the envelope MAIL FROM rather than a parsed
    // RFC5322.From header. Also decides whether we still need to sign the message ourselves
    // below (skipped if it already carries a signature, to avoid double-signing).
    std::string authResultsHeader;
    bool alreadySigned = false;
    {
    // RFC5322.From header (see docs/ARCHITECTURE.md §8). Also decides whether we still need to
    // sign the message ourselves below (skipped if it already carries a signature, to avoid
    // double-signing).
    std::string spfCheckDomain = state.mailFrom.empty() ? state.heloName : domainOf(state.mailFrom);
    if (spfCheckDomain.empty()) spfCheckDomain = state.heloName;
    security::SpfResult spfResult = security::SpfAdapter{}.check(spfCheckDomain, state.sourceIp);
    bool spfPass = spfResult == security::SpfResult::Pass;

    security::DkimAdapter verifier("", "", "");
    auto dkimResult = verifier.verify(rawMessage);
        alreadySigned = dkimResult.present;
        if (dkimResult.present) {
    bool alreadySigned = dkimResult.present;

    security::DmarcAdapter dmarc;
            auto dmarcResult = dmarc.evaluate(domainOf(state.mailFrom), /*spfPass=*/false,
                                               dkimResult.valid, dkimResult.signingDomain);
            authResultsHeader = "Authentication-Results: " + _listenerLabel +
                                 "; dkim=" + (dkimResult.valid ? "pass" : "fail") +
                                 (dkimResult.signingDomain.empty()
                                      ? ""
                                      : " header.d=" + dkimResult.signingDomain) +
    auto dmarcResult = dmarc.evaluate(spfCheckDomain, spfPass, dkimResult.valid,
                                       dkimResult.signingDomain);

    std::string authResultsHeader =
        "Authentication-Results: " + _listenerLabel + "; spf=" + spfResultTag(spfResult) +
        " smtp.mailfrom=" + spfCheckDomain + "; dkim=" +
        (dkimResult.present ? (dkimResult.valid ? "pass" : "fail") : "none") +
        (dkimResult.signingDomain.empty() ? "" : " header.d=" + dkimResult.signingDomain) +
        "; dmarc=" + dmarcResultTag(dmarcResult) + "\r\n";
        }
    }

    // One smarthost per message, chosen from the first recipient's relay rule -- a message
    // with recipients spanning multiple relay rules with different smarthosts is a known