Add the Blacktip edge
The edge: per-IP TCP/UDP/HTTP/HTTPS/TLS listeners with port ranges and PROXY protocol, WireGuard tunnel endpoint, logins in front of web ports (built-in proxy or Traefik), health checks and failover, and mail egress with DKIM signing and MTA-STS. Split out of the original single repository; depends on Reef for the shared types. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
995ef9a6aa
commit
9448a532e9
17 files changed
+3492
-1
No files matched your search
+107
@@ -0,0 +1,107 @@
|
||||
package blacktip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseSTS(t *testing.T) {
|
||||
good := "version: STSv1\r\nmode: enforce\r\nmx: mail.example.com\r\nmx: *.mail.example.com\r\nmax_age: 86400\r\n"
|
||||
p, err := parseSTS(good)
|
||||
if err != nil || p.Mode != "enforce" || len(p.MX) != 2 || p.MaxAge != 24*time.Hour {
|
||||
t.Fatalf("parse: %+v %v", p, err)
|
||||
}
|
||||
if p, _ := parseSTS(strings.ReplaceAll(good, "86400", "999999999999")); p == nil || p.MaxAge != 31557600*time.Second {
|
||||
t.Errorf("max_age must be capped at a year: %+v", p)
|
||||
}
|
||||
if p, err := parseSTS("version: STSv1\nmode: none\nmax_age: 60\n"); err != nil || p.Mode != "none" {
|
||||
t.Errorf("mode none needs no mx: %+v %v", p, err)
|
||||
}
|
||||
for name, bad := range map[string]string{
|
||||
"wrong version": "version: STSv2\nmode: enforce\nmx: a.b\nmax_age: 1\n",
|
||||
"unknown mode": "version: STSv1\nmode: maybe\nmx: a.b\nmax_age: 1\n",
|
||||
"no mx": "version: STSv1\nmode: enforce\nmax_age: 1\n",
|
||||
"no max_age": "version: STSv1\nmode: enforce\nmx: a.b\n",
|
||||
"bad max_age": "version: STSv1\nmode: enforce\nmx: a.b\nmax_age: soon\n",
|
||||
"empty": "",
|
||||
} {
|
||||
if _, err := parseSTS(bad); err == nil {
|
||||
t.Errorf("accepted a policy with %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSTSMXMatching(t *testing.T) {
|
||||
p := &STSPolicy{MX: []string{"mail.example.com", "*.mx.example.com"}}
|
||||
for host, want := range map[string]bool{
|
||||
"mail.example.com": true,
|
||||
"MAIL.Example.com.": true, // case and a trailing dot don't matter
|
||||
"a.mx.example.com": true,
|
||||
"a.b.mx.example.com": false, // a wildcard is exactly one label
|
||||
"mx.example.com": false, // and never zero labels
|
||||
"evil-mail.example.com": false,
|
||||
"mail.example.com.evil": false,
|
||||
"": false,
|
||||
} {
|
||||
if got := p.Allows(host); got != want {
|
||||
t.Errorf("Allows(%q) = %v, want %v", host, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchSTSHandlesTheHTTPSExchange(t *testing.T) {
|
||||
var gotHost, gotPath string
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotHost, gotPath = r.Host, r.URL.Path
|
||||
switch r.Header.Get("X-Case") {
|
||||
case "redirect":
|
||||
http.Redirect(w, r, "https://elsewhere.invalid/", http.StatusFound)
|
||||
case "missing":
|
||||
http.NotFound(w, r)
|
||||
default:
|
||||
w.Write([]byte("version: STSv1\nmode: testing\nmx: mx.example.test\nmax_age: 600\n"))
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
// a client that sends every request to the test server regardless of the name
|
||||
client := func(c string) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &roundTripper{c: c, rt: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DialContext: func(ctx context.Context, n, a string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, n, strings.TrimPrefix(srv.URL, "https://"))
|
||||
}}},
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
|
||||
}
|
||||
}
|
||||
p, err := fetchSTS(client(""), "example.test")
|
||||
if err != nil || p.Mode != "testing" || !p.Allows("mx.example.test") {
|
||||
t.Fatalf("fetch: %+v %v", p, err)
|
||||
}
|
||||
if gotHost != "mta-sts.example.test" || gotPath != "/.well-known/mta-sts.txt" {
|
||||
t.Errorf("fetched %s%s, want mta-sts.<domain>/.well-known/mta-sts.txt", gotHost, gotPath)
|
||||
}
|
||||
if _, err := fetchSTS(client("missing"), "example.test"); err == nil {
|
||||
t.Error("a 404 policy must be an error")
|
||||
}
|
||||
if _, err := fetchSTS(client("redirect"), "example.test"); err == nil {
|
||||
t.Error("a redirect must not be followed or accepted")
|
||||
}
|
||||
}
|
||||
|
||||
type roundTripper struct {
|
||||
c string
|
||||
rt http.RoundTripper
|
||||
}
|
||||
|
||||
func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req = req.Clone(req.Context())
|
||||
req.Header.Set("X-Case", r.c)
|
||||
return r.rt.RoundTrip(req)
|
||||
}
|
||||
Reference in new issue
Block a user