Add the Hammerhead control plane

Dashboard and API, accounts (organizations, roles, invitations, 2FA, OIDC
single sign-on, API keys, revocable sessions), an encrypted SQLite database,
resource logins, alerts and /metrics, mail egress management with DKIM keys,
and the .env loader. Split out of the original single repository; depends on
Reef for the shared types. The end-to-end tests that run Hammerhead, Blacktip
and Bamboo together live in e2e/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-26 02:50:03 -06:00
1 parent 3daa368233
commit f521fa8668
35 files changed
+10392 -1

No files matched your search

+171
View File
@@ -0,0 +1,171 @@
package hammerhead
import (
"errors"
"net"
"net/http"
"strings"
"git.veillax.com/Veillax/reef/model"
)
const defaultMailListen = "127.0.0.1:2525"
func (s *Server) findMailEgress(id string) *model.MailEgress {
for _, e := range s.st.MailEgress {
if e.ID == id {
return e
}
}
return nil
}
func isLoopbackListen(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return false
}
if strings.EqualFold(host, "localhost") {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
// validateMailEgress checks e against org c and normalises it. Caller holds s.mu.
func (s *Server) validateMailEgress(c *orgCtx, e *model.MailEgress) error {
e.OrgID = c.Org.ID
e.Name = strings.TrimSpace(e.Name)
site := s.findSite(e.SiteID)
if site == nil || site.Local || site.OrgID != c.Org.ID {
return errors.New("choose one of your sites: the mail server's network (the built-in local site already sends from the edge itself)")
}
edge := s.findEdge(e.EdgeID)
if edge == nil || !s.edgeAllowedFor(edge, c.Org.ID) {
return errors.New("choose an edge")
}
e.Listen = strings.TrimSpace(e.Listen)
if e.Listen == "" {
e.Listen = defaultMailListen
}
if !validAddr(e.Listen) {
return errors.New("listen address must look like 127.0.0.1:2525 (an address on the site's machine)")
}
for _, o := range s.st.MailEgress {
if o.ID != e.ID && o.SiteID == e.SiteID && o.Listen == e.Listen {
return errors.New("another mail egress on that site already listens on " + e.Listen)
}
}
ip := net.ParseIP(strings.TrimSpace(e.SourceIP))
if ip == nil || ip.IsUnspecified() {
return errors.New("choose the edge IP address your mail should leave from")
}
e.SourceIP = ip.String()
e.HeloName = strings.ToLower(strings.TrimSpace(e.HeloName))
if !validHostname(e.HeloName) || strings.HasPrefix(e.HeloName, "*.") {
return errors.New("enter the hostname your mail server announces, like mail.example.com (its reverse DNS should match)")
}
e.Smarthost = strings.TrimSpace(e.Smarthost)
if e.Smarthost != "" && !validAddr(e.Smarthost) {
return errors.New("smarthost must look like smtp.example.com:587")
}
if e.RatePerMinute < 0 || e.RatePerMinute > 6000 {
return errors.New("messages per minute must be between 1 and 6000")
}
if e.Username != "" && e.Password == "" {
return errors.New("set a password too, or clear the username")
}
if e.Username == "" && !isLoopbackListen(e.Listen) {
return errors.New("listening beyond this machine without a password would let anything on your network send mail from your IP; set a username and password, or listen on 127.0.0.1")
}
if e.Name == "" {
e.Name = "mail via " + e.HeloName
}
return nil
}
func (s *Server) saveMailEgress(w http.ResponseWriter, r *http.Request, c *orgCtx) {
var in model.MailEgress
if !decode(w, r, &in) {
return
}
s.mu.Lock()
defer s.mu.Unlock()
id := r.PathValue("id")
idx := -1
if id != "" {
for i, e := range s.st.MailEgress {
if e.ID == id && e.OrgID == c.Org.ID {
idx = i
}
}
if idx < 0 {
fail(w, 404, errors.New("no such mail egress"))
return
}
in.ID = id
if in.Password == "" && in.Username != "" && in.Username == s.st.MailEgress[idx].Username {
in.Password = s.st.MailEgress[idx].Password // blank keeps the saved password
}
}
if err := s.validateMailEgress(c, &in); err != nil {
fail(w, 400, err)
return
}
if idx >= 0 {
s.st.MailEgress[idx] = &in
delete(s.mailStatus, id)
} else {
in.ID = newID()
s.st.MailEgress = append(s.st.MailEgress, &in)
}
s.bump()
s.audit(c.Org.ID, c.Actor, "mailegress.save", in.Name)
resp := in // the stored record keeps its password; the response never carries it
resp.Password = ""
writeJSON(w, 200, resp)
}
func (s *Server) deleteMailEgress(w http.ResponseWriter, r *http.Request, c *orgCtx) {
id := r.PathValue("id")
s.mu.Lock()
defer s.mu.Unlock()
e := s.findMailEgress(id)
if e == nil || e.OrgID != c.Org.ID {
fail(w, 404, errors.New("no such mail egress"))
return
}
s.st.MailEgress = filter(s.st.MailEgress, func(x *model.MailEgress) bool { return x.ID != id })
delete(s.mailStatus, id)
delete(s.mailChecks, id)
s.bump()
s.audit(c.Org.ID, c.Actor, "mailegress.delete", e.Name)
ok(w)
}
type mailEgressView struct {
model.MailEgress
PasswordSet bool `json:"passwordSet"`
Status string `json:"status"`
StatusKnown bool `json:"statusKnown"`
Check *model.MailCheck `json:"check,omitempty"`
}
// mailEgressViewsLocked lists an org's mail egress without secrets. Caller holds s.mu.
func (s *Server) mailEgressViewsLocked(orgID string) []mailEgressView {
out := []mailEgressView{}
for _, e := range s.st.MailEgress {
if e.OrgID != orgID {
continue
}
v := mailEgressView{MailEgress: *e, PasswordSet: e.Password != ""}
v.Password = ""
v.Status, v.StatusKnown = s.mailStatus[e.ID]
if chk, ok := s.mailChecks[e.ID]; ok {
c := chk
v.Check = &c
}
out = append(out, v)
}
return out
}