Add the shared packages for the sharks stack
model, tunnel, wg, authtoken, netguard, docker and dotenv, split out of the original single repository so Hammerhead, Blacktip and Bamboo can live in their own repos and import them. README extended; LICENSE unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c7c2ae8f8d
commit
2259580adf
16 files changed
+1359
-1
No files matched your search
+327
@@ -0,0 +1,327 @@
|
||||
// Package model holds the types shared by hammerhead, blacktip and bamboo.
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
ProtoTCP = "tcp" // raw TCP on ip:port
|
||||
ProtoUDP = "udp" // raw UDP on ip:port
|
||||
ProtoHTTP = "http" // reverse proxy, routed by Host header (many hostnames per ip:port)
|
||||
ProtoHTTPS = "https" // reverse proxy with automatic Let's Encrypt certificates
|
||||
ProtoTLS = "tls" // TLS passthrough, routed by SNI (many hostnames per ip:port)
|
||||
)
|
||||
|
||||
// LastPort is the last public port a mapping occupies (Port itself unless it is a range).
|
||||
func (m Mapping) LastPort() int {
|
||||
if m.PortEnd > m.Port {
|
||||
return m.PortEnd
|
||||
}
|
||||
return m.Port
|
||||
}
|
||||
|
||||
// HostBased reports whether mappings of this protocol are routed by hostname.
|
||||
func HostBased(proto string) bool {
|
||||
return proto == ProtoHTTP || proto == ProtoHTTPS || proto == ProtoTLS
|
||||
}
|
||||
|
||||
// Web reports whether the protocol is served by the HTTP reverse proxy.
|
||||
func Web(proto string) bool { return proto == ProtoHTTP || proto == ProtoHTTPS }
|
||||
|
||||
// Edge is a blacktip node: a public server that owns one or more IP addresses.
|
||||
type Edge struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token,omitempty"`
|
||||
AuthKey string `json:"authKey,omitempty"` // signs resource-auth tokens; never sent to browsers
|
||||
TunnelAddr string `json:"tunnelAddr"` // public host:port (UDP) of the edge's WireGuard endpoint
|
||||
TunnelIP string `json:"tunnelIp"` // the edge's address inside the tunnel network
|
||||
PublicKey string `json:"publicKey"` // WireGuard public key, reported by the edge
|
||||
OrgIDs []string `json:"orgIds"` // orgs allowed to use this edge; empty = all
|
||||
IPs []string `json:"ips"` // addresses the edge reported it can bind
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
}
|
||||
|
||||
// Site is a bamboo agent: a private network that holds the real services.
|
||||
type Site struct {
|
||||
ID string `json:"id"`
|
||||
OrgID string `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Connected []string `json:"connected"` // edge IDs the agent currently has tunnels to
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
// Local sites are created automatically with each edge. Mappings that use
|
||||
// one are dialed directly from the edge machine, so no bamboo is needed.
|
||||
Local bool `json:"local"`
|
||||
EdgeID string `json:"edgeId,omitempty"` // the edge a local site belongs to
|
||||
|
||||
TunnelIP string `json:"tunnelIp,omitempty"` // the site's address inside the tunnel network
|
||||
PublicKey string `json:"publicKey,omitempty"` // WireGuard public key, reported by the agent
|
||||
}
|
||||
|
||||
// Domain is a base domain an org owns. Hostnames of web/tls mappings must sit under one.
|
||||
type Domain struct {
|
||||
ID string `json:"id"`
|
||||
OrgID string `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
EdgeID string `json:"edgeId"`
|
||||
IP string `json:"ip"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
// Target is one backend a mapping forwards to.
|
||||
type Target struct {
|
||||
SiteID string `json:"siteId"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
// Auth is the access policy in front of a web mapping. Secrets (password, PIN)
|
||||
// live only in hammerhead; these flags say which methods are switched on.
|
||||
type Auth struct {
|
||||
SSO bool `json:"sso"` // sign in with a hammerhead account
|
||||
Password bool `json:"password"` // shared password
|
||||
Pin bool `json:"pin"` // 4-12 digit PIN
|
||||
Email bool `json:"email"` // one-time code emailed to an allowed address
|
||||
AllowedRoles []string `json:"allowedRoles"`
|
||||
AllowedUsers []string `json:"allowedUsers"`
|
||||
AllowedMails []string `json:"allowedMails"` // addresses allowed for email codes ("*@example.com" ok)
|
||||
SessionHours int `json:"sessionHours"` // 0 = 24
|
||||
}
|
||||
|
||||
// Protected reports whether visitors must authenticate.
|
||||
func (a Auth) Protected() bool { return a.SSO || a.Password || a.Pin || a.Email }
|
||||
|
||||
// Rule is evaluated before authentication, top to bottom.
|
||||
type Rule struct {
|
||||
Action string `json:"action"` // "allow" (skip auth) or "deny"
|
||||
Match string `json:"match"` // "path" (prefix) or "ip" (address or CIDR)
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Mapping opens one public port (or a hostname on a shared port) and sends it to one or more targets.
|
||||
type Mapping struct {
|
||||
ID string `json:"id"`
|
||||
OrgID string `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
Proto string `json:"proto"`
|
||||
EdgeID string `json:"edgeId"`
|
||||
BindIP string `json:"bindIp"` // 0.0.0.0 = every address on the edge
|
||||
Port int `json:"port"`
|
||||
PortEnd int `json:"portEnd,omitempty"` // tcp/udp only: Port..PortEnd are all opened, each sent to the target port shifted by the same offset
|
||||
Hostname string `json:"hostname,omitempty"` // web/tls only; "*.example.com" allowed
|
||||
Targets []Target `json:"targets"` // udp uses only the first
|
||||
Enabled bool `json:"enabled"`
|
||||
// ProxyProtocol ("v1" or "v2") makes the edge start each tcp/tls connection to the
|
||||
// target with a PROXY protocol header carrying the real client address.
|
||||
ProxyProtocol string `json:"proxyProtocol,omitempty"`
|
||||
|
||||
// web (http/https) options
|
||||
Auth Auth `json:"auth"`
|
||||
AuthEpoch int64 `json:"authEpoch"` // bumped to sign every visitor out; set by hammerhead only
|
||||
Rules []Rule `json:"rules"`
|
||||
RedirectHTTP bool `json:"redirectHttp"` // https: also answer on port 80 with a redirect
|
||||
BackendHTTPS bool `json:"backendHttps"` // talk TLS (unverified) to the targets
|
||||
RewriteHost string `json:"rewriteHost"` // Host header sent to the target; "" keeps the original
|
||||
HealthPath string `json:"healthPath"` // http(s): GET this path; "" = TCP connect check
|
||||
HealthSeconds int `json:"healthSeconds"` // 0 = 30; -1 = off
|
||||
}
|
||||
|
||||
// MailEgress lets a mail server at a site send its outgoing mail from an edge's
|
||||
// public IP. Bamboo runs a small SMTP relay on the site's machine; each message
|
||||
// travels through the WireGuard tunnel and Blacktip delivers it straight to the
|
||||
// recipient's mail server (found by MX lookup) from SourceIP, introducing itself
|
||||
// as HeloName. Reverse DNS, SPF and the HELO name can then all agree, which is
|
||||
// what receiving servers check.
|
||||
type MailEgress struct {
|
||||
ID string `json:"id"`
|
||||
OrgID string `json:"orgId"`
|
||||
Name string `json:"name"`
|
||||
SiteID string `json:"siteId"`
|
||||
EdgeID string `json:"edgeId"`
|
||||
Listen string `json:"listen"` // host:port of the SMTP relay on the site's machine
|
||||
SourceIP string `json:"sourceIp"` // edge address mail leaves from (its PTR should match HeloName)
|
||||
HeloName string `json:"heloName"` // name announced to receiving servers, e.g. mail.example.com
|
||||
Smarthost string `json:"smarthost"` // optional host:port: relay through this server instead of looking up MX
|
||||
RequireTLS bool `json:"requireTls"` // refuse to send to servers that don't offer verifiable STARTTLS
|
||||
RatePerMinute int `json:"ratePerMinute"` // messages per minute the edge will send; 0 = 60
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// MailSpec is what a site agent needs to run the SMTP relay.
|
||||
type MailSpec struct {
|
||||
ID string `json:"id"`
|
||||
EdgeID string `json:"edgeId"`
|
||||
Listen string `json:"listen"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// DKIMKey is a signing key for one sending domain. The edge signs outgoing mail
|
||||
// with it; the public half goes in DNS at <selector>._domainkey.<domain>.
|
||||
type DKIMKey struct {
|
||||
ID string `json:"id"`
|
||||
OrgID string `json:"orgId"`
|
||||
Domain string `json:"domain"`
|
||||
Selector string `json:"selector"`
|
||||
PrivateKey string `json:"privateKey,omitempty"` // PEM, never sent to browsers
|
||||
PublicKey string `json:"publicKey"` // base64 DER, the p= value of the TXT record
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
// DKIMSigner is what an edge needs to sign mail for one domain.
|
||||
type DKIMSigner struct {
|
||||
KeyID string `json:"keyId"`
|
||||
Domain string `json:"domain"`
|
||||
Selector string `json:"selector"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
}
|
||||
|
||||
// MailPolicy is what an edge needs to deliver mail for one MailEgress.
|
||||
type MailPolicy struct {
|
||||
ID string `json:"id"`
|
||||
SiteID string `json:"siteId"`
|
||||
SourceIP string `json:"sourceIp"`
|
||||
HeloName string `json:"heloName"`
|
||||
Smarthost string `json:"smarthost"`
|
||||
RequireTLS bool `json:"requireTls"`
|
||||
RatePerMinute int `json:"ratePerMinute"`
|
||||
|
||||
DKIM []DKIMSigner `json:"dkim"` // keys for the org's sending domains
|
||||
}
|
||||
|
||||
// MailCheck is the edge's report on how deliverable mail from a source IP is.
|
||||
type MailCheck struct {
|
||||
CheckedAt time.Time `json:"checkedAt"`
|
||||
PTR []string `json:"ptr"` // reverse DNS names of the source IP
|
||||
PTRMatchesHelo bool `json:"ptrMatchesHelo"` // one of them equals the HELO name
|
||||
HeloResolves bool `json:"heloResolves"` // the HELO name resolves back to the source IP
|
||||
Port25 string `json:"port25"` // "open" or why not
|
||||
DKIM map[string]string `json:"dkim"` // key ID -> "published", "missing" or "mismatch"
|
||||
}
|
||||
|
||||
// ---------- edge <-> hammerhead ----------
|
||||
|
||||
// EdgeConfig is what blacktip long-polls from hammerhead.
|
||||
type EdgeConfig struct {
|
||||
Version int64 `json:"version"`
|
||||
EdgeID string `json:"edgeId"`
|
||||
AuthKey string `json:"authKey"`
|
||||
Dashboard string `json:"dashboard"` // public URL of hammerhead, for login redirects
|
||||
Mappings []Mapping `json:"mappings"`
|
||||
TunnelIP string `json:"tunnelIp"` // the edge's WireGuard address
|
||||
Peers []WGPeer `json:"peers"` // sites the edge may reach through the tunnel
|
||||
Local []string `json:"local"` // site IDs the edge dials directly on its own machine
|
||||
AcmeEmail string `json:"acmeEmail"`
|
||||
AcmeStages bool `json:"acmeStaging"`
|
||||
|
||||
MailEgress []MailPolicy `json:"mailEgress"`
|
||||
}
|
||||
|
||||
// WGPeer is a site as seen from an edge.
|
||||
type WGPeer struct {
|
||||
SiteID string `json:"siteId"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// TargetHealth is the latest check result for one target.
|
||||
type TargetHealth struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// MappingStatus is what an edge reports per mapping.
|
||||
type MappingStatus struct {
|
||||
Error string `json:"error"` // "" = listening
|
||||
Cert string `json:"cert,omitempty"` // https: certificate state
|
||||
Targets []TargetHealth `json:"targets,omitempty"`
|
||||
}
|
||||
|
||||
// Stat is traffic since the last report.
|
||||
type Stat struct {
|
||||
Conns int64 `json:"conns"`
|
||||
BytesIn int64 `json:"bytesIn"` // client -> target
|
||||
BytesOut int64 `json:"bytesOut"` // target -> client
|
||||
}
|
||||
|
||||
// LogEntry is one access-log line.
|
||||
type LogEntry struct {
|
||||
ID int64 `json:"id,omitempty"` // assigned by hammerhead
|
||||
Time time.Time `json:"time"`
|
||||
Mapping string `json:"mapping"`
|
||||
OrgID string `json:"orgId,omitempty"` // filled in by hammerhead
|
||||
Remote string `json:"remote"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Status int `json:"status,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Result string `json:"result,omitempty"` // allowed, denied, auth-required, no-target
|
||||
BytesIn int64 `json:"bytesIn"`
|
||||
BytesOut int64 `json:"bytesOut"`
|
||||
Millis int64 `json:"ms"`
|
||||
}
|
||||
|
||||
// Container is a Docker container seen by an agent.
|
||||
type Container struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
State string `json:"state"`
|
||||
Ports []ContainerPort `json:"ports"`
|
||||
Networks map[string]string `json:"networks"` // network name -> container IP
|
||||
}
|
||||
|
||||
type ContainerPort struct {
|
||||
Private int `json:"private"`
|
||||
Public int `json:"public,omitempty"` // 0 = not published
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// EdgeReport is what blacktip posts back.
|
||||
type EdgeReport struct {
|
||||
IPs []string `json:"ips"`
|
||||
PublicKey string `json:"publicKey"` // the edge's WireGuard public key
|
||||
Status map[string]MappingStatus `json:"status"`
|
||||
Stats map[string]Stat `json:"stats"` // deltas since last report
|
||||
Containers []Container `json:"containers"`
|
||||
MailChecks map[string]MailCheck `json:"mailChecks"` // by mail-egress ID
|
||||
}
|
||||
|
||||
// EdgeRef tells a site where to dial.
|
||||
type EdgeRef struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint"` // host:port (UDP) to send WireGuard packets to
|
||||
PublicKey string `json:"publicKey"`
|
||||
IP string `json:"ip"` // the edge's address inside the tunnel
|
||||
}
|
||||
|
||||
// SiteConfig is what bamboo long-polls from hammerhead.
|
||||
type SiteConfig struct {
|
||||
Version int64 `json:"version"`
|
||||
SiteID string `json:"siteId"`
|
||||
TunnelIP string `json:"tunnelIp"` // this site's WireGuard address
|
||||
Edges []EdgeRef `json:"edges"`
|
||||
|
||||
MailEgress []MailSpec `json:"mailEgress"`
|
||||
}
|
||||
|
||||
// SiteReport is what bamboo posts back.
|
||||
type SiteReport struct {
|
||||
PublicKey string `json:"publicKey"` // the agent's WireGuard public key
|
||||
MailEgress map[string]string `json:"mailEgress"` // egress ID -> "" (listening) or an error
|
||||
Connected []string `json:"connected"`
|
||||
Containers []Container `json:"containers"`
|
||||
}
|
||||
|
||||
// HostMatches reports whether host matches an exact name or a "*.example.com" pattern.
|
||||
func HostMatches(pattern, host string) bool {
|
||||
if pattern == host {
|
||||
return true
|
||||
}
|
||||
return len(pattern) > 2 && pattern[:2] == "*." && len(host) > len(pattern)-1 && host[len(host)-len(pattern)+1:] == pattern[1:]
|
||||
}
|
||||
Reference in new issue
Block a user