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:
1 parent
3daa368233
commit
f521fa8668
35 files changed
+10392
-1
No files matched your search
@@ -0,0 +1,265 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user