Files
Hammerhead/agent.go
T
VeillaxandClaude Sonnet 5.5 f521fa8668 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>
2026-10-26 02:50:03 -06:00

266 lines
6.2 KiB
Go

package hammerhead
import (
"crypto/subtle"
"errors"
"net/http"
"strconv"
"time"
"git.veillax.com/Veillax/reef/model"
)
// Endpoints used by blacktip and bamboo. They authenticate with the edge/site token.
func (s *Server) edgeByToken(r *http.Request) *model.Edge {
t := bearer(r)
if t == "" {
return nil
}
for _, e := range s.st.Edges {
if subtle.ConstantTimeCompare([]byte(e.Token), []byte(t)) == 1 {
return e
}
}
return nil
}
func (s *Server) siteByToken(r *http.Request) *model.Site {
t := bearer(r)
if t == "" {
return nil
}
for _, e := range s.st.Sites {
if !e.Local && subtle.ConstantTimeCompare([]byte(e.Token), []byte(t)) == 1 {
return e
}
}
return nil
}
func (s *Server) edgeConfig(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
e := s.edgeByToken(r)
s.mu.Unlock()
if e == nil {
fail(w, 401, errors.New("bad edge token"))
return
}
have, _ := strconv.ParseInt(r.URL.Query().Get("v"), 10, 64)
if !s.wait(r, have) {
s.mu.Lock()
if e := s.edgeByToken(r); e != nil {
e.LastSeen = time.Now()
}
s.mu.Unlock()
w.WriteHeader(http.StatusNoContent)
return
}
s.mu.Lock()
defer s.mu.Unlock()
if e = s.edgeByToken(r); e == nil {
fail(w, 401, errors.New("bad edge token"))
return
}
e.LastSeen = time.Now()
cfg := model.EdgeConfig{
Version: s.st.Version, EdgeID: e.ID, AuthKey: e.AuthKey, Dashboard: s.st.Settings.DashboardURL,
Mappings: []model.Mapping{}, Peers: []model.WGPeer{}, Local: []string{}, TunnelIP: e.TunnelIP,
AcmeEmail: s.st.Settings.AcmeEmail, AcmeStages: s.st.Settings.AcmeStaging,
}
for _, m := range s.st.Mappings {
if m.EdgeID == e.ID && m.Enabled {
cfg.Mappings = append(cfg.Mappings, *m)
}
}
cfg.MailEgress = []model.MailPolicy{}
for _, x := range s.st.MailEgress {
if x.EdgeID == e.ID && x.Enabled {
cfg.MailEgress = append(cfg.MailEgress, model.MailPolicy{ID: x.ID, SiteID: x.SiteID, SourceIP: x.SourceIP, HeloName: x.HeloName,
Smarthost: x.Smarthost, RequireTLS: x.RequireTLS, RatePerMinute: x.RatePerMinute, DKIM: s.signersLocked(x.OrgID)})
}
}
for _, st := range s.st.Sites {
switch {
case st.Local:
if st.EdgeID == e.ID {
cfg.Local = append(cfg.Local, st.ID)
}
case st.PublicKey != "" && st.TunnelIP != "" && s.edgeAllowedFor(e, st.OrgID):
cfg.Peers = append(cfg.Peers, model.WGPeer{SiteID: st.ID, PublicKey: st.PublicKey, IP: st.TunnelIP})
}
}
writeJSON(w, 200, cfg)
}
func (s *Server) edgeReport(w http.ResponseWriter, r *http.Request) {
var in model.EdgeReport
if !decode(w, r, &in) {
return
}
s.mu.Lock()
defer s.mu.Unlock()
e := s.edgeByToken(r)
if e == nil {
fail(w, 401, errors.New("bad edge token"))
return
}
e.LastSeen = time.Now()
e.IPs = in.IPs
for _, x := range s.st.MailEgress {
if x.EdgeID == e.ID {
if chk, found := in.MailChecks[x.ID]; found {
s.mailChecks[x.ID] = chk
}
}
}
for _, m := range s.st.Mappings {
if m.EdgeID != e.ID {
continue
}
if st, found := in.Status[m.ID]; found {
s.status[m.ID] = st
} else {
delete(s.status, m.ID)
}
if d, found := in.Stats[m.ID]; found {
t := s.st.Totals[m.ID]
if t == nil {
t = &Total{}
s.st.Totals[m.ID] = t
}
t.Conns += d.Conns
t.BytesIn += d.BytesIn
t.BytesOut += d.BytesOut
s.db.addTraffic(m.ID, d)
}
}
for _, st := range s.st.Sites {
if st.Local && st.EdgeID == e.ID {
s.containers[st.ID] = in.Containers
}
}
if e.PublicKey != in.PublicKey {
e.PublicKey = in.PublicKey
s.bump() // sites need the edge's new key
} else {
s.save()
}
ok(w)
}
func (s *Server) edgeLogs(w http.ResponseWriter, r *http.Request) {
var in []model.LogEntry
if !decode(w, r, &in) {
return
}
s.mu.Lock()
e := s.edgeByToken(r)
if e == nil {
s.mu.Unlock()
fail(w, 401, errors.New("bad edge token"))
return
}
out := make([]model.LogEntry, 0, len(in))
for _, l := range in {
if m := s.findMapping(l.Mapping); m != nil && m.EdgeID == e.ID {
l.OrgID = m.OrgID
} else if x := s.findMailEgress(l.Mapping); x != nil && x.EdgeID == e.ID {
l.OrgID = x.OrgID
} else {
continue
}
out = append(out, l)
}
s.mu.Unlock()
s.logs.addAccess(out)
ok(w)
}
func (s *Server) siteConfig(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
st := s.siteByToken(r)
s.mu.Unlock()
if st == nil {
fail(w, 401, errors.New("bad site token"))
return
}
have, _ := strconv.ParseInt(r.URL.Query().Get("v"), 10, 64)
if !s.wait(r, have) {
s.mu.Lock()
if st := s.siteByToken(r); st != nil {
st.LastSeen = time.Now()
}
s.mu.Unlock()
w.WriteHeader(http.StatusNoContent)
return
}
s.mu.Lock()
defer s.mu.Unlock()
if st = s.siteByToken(r); st == nil {
fail(w, 401, errors.New("bad site token"))
return
}
st.LastSeen = time.Now()
cfg := model.SiteConfig{Version: s.st.Version, SiteID: st.ID, TunnelIP: st.TunnelIP, Edges: []model.EdgeRef{}, MailEgress: []model.MailSpec{}}
seen := map[string]bool{}
addEdge := func(id string) {
if seen[id] {
return
}
seen[id] = true
if e := s.findEdge(id); e != nil {
cfg.Edges = append(cfg.Edges, model.EdgeRef{ID: e.ID, Name: e.Name, Endpoint: e.TunnelAddr, PublicKey: e.PublicKey, IP: e.TunnelIP})
}
}
for _, m := range s.st.Mappings {
for _, t := range m.Targets {
if t.SiteID == st.ID {
addEdge(m.EdgeID)
}
}
}
for _, x := range s.st.MailEgress {
if x.SiteID == st.ID && x.Enabled {
addEdge(x.EdgeID)
cfg.MailEgress = append(cfg.MailEgress, model.MailSpec{ID: x.ID, EdgeID: x.EdgeID, Listen: x.Listen, Username: x.Username, Password: x.Password})
}
}
writeJSON(w, 200, cfg)
}
func (s *Server) siteReport(w http.ResponseWriter, r *http.Request) {
var in model.SiteReport
if !decode(w, r, &in) {
return
}
s.mu.Lock()
defer s.mu.Unlock()
st := s.siteByToken(r)
if st == nil {
fail(w, 401, errors.New("bad site token"))
return
}
st.LastSeen = time.Now()
if in.Connected == nil {
in.Connected = []string{}
}
st.Connected = in.Connected
s.containers[st.ID] = in.Containers
for _, x := range s.st.MailEgress {
if x.SiteID != st.ID {
continue
}
if msg, known := in.MailEgress[x.ID]; known {
s.mailStatus[x.ID] = msg
} else {
delete(s.mailStatus, x.ID)
}
}
if in.PublicKey != "" && st.PublicKey != in.PublicKey {
st.PublicKey = in.PublicKey
s.bump() // every edge this site talks to needs the new key
}
ok(w)
}