package contract // What the web front keeps on the bus about the names it serves, and about the names it was asked for and does not // serve (issue 774). The proxy writes it, on change, in its states `routes` and `unrouted`, announced readable; the // module's tools, and any module that reads them, read it from there, so the question "is this site reachable, and // is its certificate good" is answered from the bus rather than with curl. // // A key is `.`: the machine whose proxy answers, then the name as asked, lower-cased. A node's name holds // no dot, so the first dot splits them. import ( "net" "regexp" "strings" "time" ) // The states route-proxy keeps, by their local names (the buckets are route-proxy_). const ( // RoutesState holds one key per served name: where it forwards, its certificate, the last answer it got. RoutesState = "routes" // UnroutedState holds one key per name the proxy answered "no route for" with, and when it was last asked. UnroutedState = "unrouted" // BoundState is the provider helper's bucket of consumer entries (mesh-sdk go/provider). BoundState = "bound" ) // The words a route's reach and a certificate's authority are said in. const ( ReachPublic = "public" ReachInternal = "internal" ) // The words a certificate's outcome is said in. const ( // Issued: the authority issued it while this proxy ran, at RenewedAt. Issued = "issued" // Cached: this proxy served it from what it kept, and saw no issuance of it since it started. Cached = "cached" // Failed: the last attempt to have one failed, Error says why. Failed = "failed" ) // Target is one rule a name is served by: what a request matching it reaches, or what it is answered with. type Target struct { // Path is the path prefix the rule is scoped to; empty is every path. Path string `json:"path,omitempty"` // Module and Node are the consumer that contributed the route and the machine it runs on. Module string `json:"module,omitempty"` Node string `json:"node,omitempty"` // Target is where the request is forwarded, scheme://address:port; empty for a rule that answers itself. Target string `json:"target,omitempty"` Deny bool `json:"deny,omitempty"` Redirect string `json:"redirect,omitempty"` // Auth says the rule asks for credentials; Sealed says why it cannot check them, and so refuses everything. Auth bool `json:"auth,omitempty"` Sealed string `json:"sealed,omitempty"` } // Certificate is what this proxy knows of a name's certificate. type Certificate struct { // Authority is which issuer certifies the name: public (the ACME directory the module names) or internal (the // mesh's own). Authority string `json:"authority"` Issuer string `json:"issuer,omitempty"` NotAfter *time.Time `json:"not_after,omitempty"` // RenewedAt is when an authority last issued it to this proxy. RenewedAt *time.Time `json:"renewed_at,omitempty"` // Outcome is issued, cached or failed; empty until a handshake or an issuance said anything. Outcome string `json:"outcome,omitempty"` Error string `json:"error,omitempty"` At *time.Time `json:"at,omitempty"` } // Answer is the last answer the proxy got from upstream for a name: the status it carried back, or the error that // stood in for one (a 502). type Answer struct { Status int `json:"status"` At time.Time `json:"at"` Error string `json:"error,omitempty"` Path string `json:"path,omitempty"` } // Route is one served name, the value of a key of RoutesState. type Route struct { Name string `json:"name"` // Proxy is the machine whose proxy serves it. Proxy string `json:"proxy"` // Reach is public (a route's own name) or internal (only its internal-name, served inside the private network). Reach string `json:"reach"` Targets []Target `json:"targets"` Certificate Certificate `json:"certificate"` LastAnswer *Answer `json:"last_answer,omitempty"` } // Unrouted is one name the proxy answered "no route for", the value of a key of UnroutedState. type Unrouted struct { Name string `json:"name"` Proxy string `json:"proxy"` // AskedAt is when the name was last asked for. Who asked is not kept here: the journal keeps it, for the // machine's intrusion prevention. AskedAt time.Time `json:"asked_at"` // Times is how often it was asked since the proxy last started. Times int `json:"times"` // InternalOnly says the name is served, but only inside the private network, and was asked from outside. InternalOnly bool `json:"internal_only,omitempty"` // Handshake says it was asked in a TLS handshake (the name a client sent), not in an HTTP request. Handshake bool `json:"handshake,omitempty"` } // OtherName is the name of the one key per proxy that counts the refusals not kept by name: names under no domain // the proxy serves (what a scan sends), and names past the limit of new names a minute. Its key is // `._other`; no host name begins with an underscore. const OtherName = "_other" // Other is the value of a proxy's OtherName key. type Other struct { Proxy string `json:"proxy"` // Minute is the last whole minute counted, and LastMinute how many refusals it held. Minute time.Time `json:"minute"` LastMinute int `json:"last_minute"` // SinceStart is every refusal not kept by name since the proxy started. SinceStart int `json:"since_start"` } // Registrable is the domain a name is registered under, as its last two labels; "" for an address or a single // label. Two labels is wrong for a public suffix of two (co.uk); it only decides which refused names are kept by name. func Registrable(name string) string { if net.ParseIP(strings.Trim(name, "[]")) != nil { return "" } labels := strings.Split(name, ".") if len(labels) < 2 { return "" } return strings.Join(labels[len(labels)-2:], ".") } var keyable = regexp.MustCompile(`^[a-z0-9_]([a-z0-9_.-]{0,251}[a-z0-9_])?$`) // Keyable says whether a name can be a key's name: what a host name or an address is. A name asked for in a request // is whatever a client sent; one no key could hold is no site anybody runs. func Keyable(name string) bool { return keyable.MatchString(name) && !strings.Contains(name, "..") } // Key is the key of name served or asked at node's proxy. func Key(node, name string) string { return node + "." + name } // Split is a key's node and name. func Split(key string) (node, name string) { node, name, _ = strings.Cut(key, ".") return node, name }