// Package model holds the types shared by hammerhead, blacktip and bamboo. package model import ( "strings" "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 } // IsRedirect reports whether the mapping answers with a redirect instead of forwarding to a target. func (m Mapping) IsRedirect() bool { return m.RedirectTo != "" } // RedirectLocation is where a request for requestURI (path and query) is sent. With KeepPath the // request's path and query follow the destination, so example.com/a?b=1 goes to to/a?b=1. func (m Mapping) RedirectLocation(requestURI string) string { if !m.RedirectKeepPath { return m.RedirectTo } if requestURI == "" || requestURI[0] != '/' { requestURI = "/" + requestURI } return strings.TrimRight(m.RedirectTo, "/") + requestURI } // 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"` // Version is the release the program reports (e.g. v0.1.5). AutoUpdate says whether it may update // itself; only then will it act on an update request (UpdateID). UpdateMsg is the outcome of the last requested update. Version string `json:"version,omitempty"` AutoUpdate bool `json:"autoUpdate,omitempty"` UpdateID string `json:"updateId,omitempty"` // set while an update requested by an administrator is pending UpdateMsg string `json:"updateMsg,omitempty"` } // 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 Tags []string `json:"tags,omitempty"` // labels for grouping and filtering in the dashboard 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 // Version is the release the program reports (e.g. v0.1.5). AutoUpdate says whether it may update // itself; only then will it act on an update request (UpdateID). UpdateMsg is the outcome of the last requested update. Version string `json:"version,omitempty"` AutoUpdate bool `json:"autoUpdate,omitempty"` UpdateID string `json:"updateId,omitempty"` // set while an update requested by an administrator is pending UpdateMsg string `json:"updateMsg,omitempty"` } // 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"` Tags []string `json:"tags,omitempty"` // labels for grouping and filtering in the dashboard // 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"` // A web mapping with RedirectTo set sends visitors there instead of to a target (so it has no // targets, login or health checks). It answers 301 when RedirectPermanent, otherwise 302. RedirectTo string `json:"redirectTo,omitempty"` RedirectPermanent bool `json:"redirectPermanent,omitempty"` RedirectKeepPath bool `json:"redirectKeepPath,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 ._domainkey.. 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"` UpdateID string `json:"updateId,omitempty"` // an administrator asked this edge to update itself now; echoed back in its report 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"` } // DiscoveredService is a listening service an agent found on its own network (local sockets or an // opt-in scan of configured subnets). Hammerhead offers these as suggested targets; nothing is // proxied until an administrator picks one. type DiscoveredService struct { Host string `json:"host"` // address to dial, as seen from the agent Port int `json:"port"` Proto string `json:"proto"` // "tcp" Service string `json:"service,omitempty"` // best guess: http, https, ssh, smtp, ftp, mysql, ... (empty = unknown) Detail string `json:"detail,omitempty"` // banner or Server header, trimmed Source string `json:"source"` // "local" (listening socket on the agent's host) or "scan" } // 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 Version string `json:"version,omitempty"` AutoUpdate bool `json:"autoUpdate,omitempty"` UpdateMsg string `json:"updateMsg,omitempty"` // outcome of the update asked for by UpdateID UpdateID string `json:"updateId,omitempty"` // progress of that update, for the dashboard's progress bar and log UpdateStage string `json:"updateStage,omitempty"` UpdatePct int `json:"updatePct,omitempty"` UpdateLog []string `json:"updateLog,omitempty"` } // 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"` UpdateID string `json:"updateId,omitempty"` // an administrator asked this agent to update itself now; echoed back in its report 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"` Discovered []DiscoveredService `json:"discovered,omitempty"` Version string `json:"version,omitempty"` AutoUpdate bool `json:"autoUpdate,omitempty"` UpdateMsg string `json:"updateMsg,omitempty"` UpdateID string `json:"updateId,omitempty"` // progress of that update, for the dashboard's progress bar and log UpdateStage string `json:"updateStage,omitempty"` UpdatePct int `json:"updatePct,omitempty"` UpdateLog []string `json:"updateLog,omitempty"` } // 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:] }