// Package api holds the JSON shapes of Housecarl's REST API, shared by the // server and the Go client (and so the CLI). package api import ( "encoding/json" "time" "housecarl/internal/model" "housecarl/internal/validate" "housecarl/internal/version" ) // Prefix starts every API path. const Prefix = "/api/v1" // ErrorBody is every error response. type ErrorBody struct { Error ErrorInfo `json:"error"` } // ErrorInfo describes an error. type ErrorInfo struct { Code string `json:"code"` Message string `json:"message"` Problems []validate.Problem `json:"problems,omitempty"` // Result is the preview of a refused change. Result json.RawMessage `json:"result,omitempty"` // Op is the changeset operation (its seq) that couldn't be applied. Op int `json:"op,omitempty"` } // Whoami describes the caller. type Whoami struct { User *model.User `json:"user"` Admin bool `json:"admin"` Method string `json:"method"` Permissions []string `json:"permissions"` // TokenID is the token the caller authenticated with (0: none). TokenID int64 `json:"token_id,omitempty"` CSRFToken string `json:"csrf_token,omitempty"` Build version.Build `json:"build"` // Mode is enforcing, or shadow (docs/shadow.md). Mode string `json:"mode"` } // Modes: housecarld's ruleset decides, or (shadow mode, the // configuration file's shadow.enabled) it only shows what it would do, // beside another firewall. const ( ModeEnforcing = "enforcing" ModeShadow = "shadow" ) // Login is the login request. type Login struct { Username string `json:"username"` Password string `json:"password"` } // PasswordChange changes the caller's password. type PasswordChange struct { Current string `json:"current"` Password string `json:"password"` } // MutationResponse answers a configuration change. type MutationResponse struct { Applied bool `json:"applied"` DryRun bool `json:"dry_run,omitempty"` HistoryID int64 `json:"history_id,omitempty"` Changes []model.Change `json:"changes"` Warnings []validate.Problem `json:"warnings,omitempty"` // Object is the created or changed object. Object json.RawMessage `json:"object,omitempty"` // ConfirmBy is set when the change is on probation: unless confirmed // (POST /confirm) by then, it is rolled back. ConfirmBy *time.Time `json:"confirm_by,omitempty"` // ConfirmToken lets the client that made the change confirm it by // itself (POST /confirm {"token"}): from the same address, over a new // connection, which proves it can still reach the API. The answer // closes its connection, so the next request opens a new one. ConfirmToken string `json:"confirm_token,omitempty"` // Diff is the ruleset's unified diff, for a dry run. Diff string `json:"diff,omitempty"` // Staged: the change was added to a changeset (Changeset), not // applied. Staged bool `json:"staged,omitempty"` Changeset *model.Changeset `json:"changeset,omitempty"` } // ChangesetInput creates a changeset, or changes its description. type ChangesetInput struct { Description string `json:"description"` } // ChangesetPreview is what applying a changeset would do: its changes, // the problems, nftables' check and the ruleset's diff. type ChangesetPreview struct { // Valid: it can be applied as it is. Valid bool `json:"valid"` Changes []model.Change `json:"changes"` Warnings []validate.Problem `json:"warnings,omitempty"` // Error says why it can't be applied (Op: which operation, if one). Error *ErrorInfo `json:"error,omitempty"` Diff string `json:"diff,omitempty"` } // HostInterface is one of the server's network interfaces, with the zone // it is in. type HostInterface struct { Name string `json:"name"` Addresses []string `json:"addresses"` Up bool `json:"up"` Zone string `json:"zone,omitempty"` } // Counter is packets and bytes since the table was created. type Counter struct { Packets uint64 `json:"packets"` Bytes uint64 `json:"bytes"` } // Counters are the hits of every object, by mark: "r12" (rule 12), "f3" // (port forward 3; "f3-6" its IPv6 pinhole, "f3-hairpin"), "s4", "d5", // "b-established" (a built-in), "p-input" (what reached a policy). type Counters struct { Since time.Time `json:"since"` Counters map[string]Counter `json:"counters"` } // Element is a dynamic set's entry. type Element struct { Address string `json:"address"` // Timeout in seconds: on input, how long it stays (0: the set's // default); on output, how long it has left. Timeout int `json:"timeout,omitempty"` } // Probation is the changes waiting to be confirmed. type Probation struct { // Pending history entries, oldest first (empty: nothing pending). Pending []int64 `json:"pending"` // Takeovers is how many takeovers (deleted tables) are pending. Takeovers int `json:"takeovers,omitempty"` Deadline time.Time `json:"deadline,omitempty"` // Seconds left before they are rolled back. SecondsLeft int `json:"seconds_left,omitempty"` } // LogEntry is a packet a rule or a policy logged. type LogEntry struct { Time time.Time `json:"time"` Prefix string `json:"prefix"` In string `json:"in,omitempty"` Out string `json:"out,omitempty"` Family string `json:"family"` Protocol string `json:"protocol"` Source string `json:"source"` Dest string `json:"destination"` SrcPort int `json:"source_port,omitempty"` DestPort int `json:"destination_port,omitempty"` ICMPType string `json:"icmp_type,omitempty"` Length int `json:"length"` TCPFlags string `json:"tcp_flags,omitempty"` RuleMark string `json:"mark,omitempty"` Verdict string `json:"verdict,omitempty"` // Shadow: logged in shadow mode, where a drop or reject is what would // have been done; the packet went on. Shadow bool `json:"shadow,omitempty"` } // Status is the dashboard's overview. type Status struct { Build version.Build `json:"build"` Started time.Time `json:"started"` Database string `json:"database,omitempty"` // Mode is enforcing or shadow; ShadowPosition (shadow mode) after or // before: where Housecarl's filter chains run beside the other // firewall's. Mode string `json:"mode"` ShadowPosition string `json:"shadow_position,omitempty"` Applied bool `json:"applied"` ApplyError string `json:"apply_error,omitempty"` // Unconfigured: the configuration is empty, and no ruleset was // applied yet. Unconfigured bool `json:"unconfigured,omitempty"` Problems []validate.Problem `json:"problems"` Probation Probation `json:"probation"` Interfaces []HostInterface `json:"interfaces"` Counts map[string]int `json:"counts"` // Other tables in the ruleset (not Housecarl's), "inet main". OtherTables []string `json:"other_tables"` } // MoveInput moves a rule (or NAT rule) before or after another, or to the // start or end of its chain; Chain moves a rule to another chain. type MoveInput struct { Before int64 `json:"before,omitempty"` After int64 `json:"after,omitempty"` To string `json:"to,omitempty"` // start, end Chain string `json:"chain,omitempty"` } // ImportInput is an nftables ruleset to import: nft -j's JSON, and // optionally the ruleset file's text, whose defines name the sets. type ImportInput struct { JSON json.RawMessage `json:"json"` // Text is `nft -a list ruleset`: rules Housecarl can't express are // kept from it as raw rules. Text string `json:"text,omitempty"` Defines string `json:"defines,omitempty"` // Table limits the import to one table ("inet main"; default: every // table but Housecarl's). Table string `json:"table,omitempty"` // Replace replaces Housecarl's configuration; without it, the import // is refused when there is one. Replace bool `json:"replace,omitempty"` // Zones names the zones by interface ({"enp2s0": "lan"}); interfaces // given the same name share a zone. Zones map[string]string `json:"zones,omitempty"` } // ImportResult reports an import. type ImportResult struct { MutationResponse // State is the imported configuration (also on a dry run). State *model.State `json:"state"` // Notes say how things were mapped; Raw lists what became raw rules. Notes []string `json:"notes"` Raw []string `json:"raw"` } // TakeoverInput deletes the tables Housecarl replaces ("inet main"). type TakeoverInput struct { Tables []string `json:"tables"` } // TokenInput creates a token. type TokenInput struct { Name string `json:"name"` // Grants ("log:read", "sets:write:guest") or a role restrict the token; none: all of // the user's permissions. Grants []string `json:"grants,omitempty"` Role string `json:"role,omitempty"` ExpiresIn string `json:"expires_in,omitempty"` // 90d, 720h ExpiresAt *time.Time `json:"expires_at,omitempty"` } // TokenRotation gives a token a new secret. Grace keeps the old secret // working for a while (up to 7d). The token gets its lifetime again, // counted from now, unless ExpiresIn or NoExpiry says otherwise. type TokenRotation struct { Grace string `json:"grace,omitempty"` ExpiresIn string `json:"expires_in,omitempty"` NoExpiry bool `json:"no_expiry,omitempty"` } // Token is a token; Secret is only set when it is created. type Token struct { *model.Token GrantStrings []string `json:"grant_strings"` Secret string `json:"secret,omitempty"` } // UserInput creates or changes a user. type UserInput struct { Username *string `json:"username,omitempty"` Password *string `json:"password,omitempty"` Admin *bool `json:"admin,omitempty"` Grants *[]string `json:"grants,omitempty"` Role *string `json:"role,omitempty"` Disabled *bool `json:"disabled,omitempty"` Comment *string `json:"comment,omitempty"` } // User is a user with grants written out. type User struct { *model.User GrantStrings []string `json:"grant_strings"` } // CertStep is one step of the preflight check. type CertStep struct { Name string `json:"name"` Status string `json:"status"` // ok, fail, warn, skip Detail string `json:"detail"` // Queries are the DNS questions asked, as dig commands. Queries []string `json:"queries,omitempty"` } // CertCheck is the result of a preflight check: can Housecarl publish its // challenge through Jarl, and does the internet see it? type CertCheck struct { Time time.Time `json:"time"` Domain string `json:"domain"` OK bool `json:"ok"` Steps []CertStep `json:"steps"` } // CertStatus describes the certificate, for the API and the dashboard. type CertStatus struct { Domain string `json:"domain"` Directory string `json:"directory"` Staging bool `json:"staging"` State string `json:"state"` // none, ok, working, error NotBefore *time.Time `json:"not_before,omitempty"` NotAfter *time.Time `json:"not_after,omitempty"` Issuer string `json:"issuer,omitempty"` // IssuedBy is the directory the current certificate came from (it may // differ from Directory until the next renewal). IssuedBy string `json:"issued_by,omitempty"` RenewAt *time.Time `json:"renew_at,omitempty"` LastError string `json:"last_error,omitempty"` LastAttempt *time.Time `json:"last_attempt,omitempty"` NextAttempt *time.Time `json:"next_attempt,omitempty"` Check *CertCheck `json:"check,omitempty"` } // CertificateResponse is GET /certificate. type CertificateResponse struct { // Configured is false when tls.acme is off. Configured bool `json:"configured"` // Source is acme, file or none. Source string `json:"source"` Status *CertStatus `json:"status,omitempty"` } // QuotaInfo is a rule's quota and what it used. type QuotaInfo struct { RuleID int64 `json:"rule_id"` Rule string `json:"rule"` Quota model.Quota `json:"quota"` // Used is a total quota's bytes; Addresses a per-address quota's, // each ("192.168.1.20", or "2001:db8:1::/64"). Used int64 `json:"used"` Addresses []QuotaAddress `json:"addresses,omitempty"` LastReset *time.Time `json:"last_reset,omitempty"` NextReset *time.Time `json:"next_reset,omitempty"` } // QuotaAddress is one address's (or prefix's) use of a per-address quota. type QuotaAddress struct { Address string `json:"address"` Used int64 `json:"used"` } // ConfirmInput confirms the changes on probation. With Token (a change's // confirm_token), it is the client that made the change confirming it by // itself; without, a person's confirmation. type ConfirmInput struct { Token string `json:"token,omitempty"` } // Connection is a connection the kernel tracks (conntrack), as it was // first seen: Source:SourcePort → Destination:DestinationPort. NAT shows // as DNATTo (where it was really sent: a port forward) and SNATTo (the // source it went out with: masquerading). type Connection struct { ID uint32 `json:"id,omitempty"` Family string `json:"family"` Protocol string `json:"protocol"` Source string `json:"source"` SourcePort int `json:"source_port,omitempty"` Destination string `json:"destination"` DestinationPort int `json:"destination_port,omitempty"` ICMPID int `json:"icmp_id,omitempty"` ICMPType int `json:"icmp_type,omitempty"` ICMPCode int `json:"icmp_code,omitempty"` DNATTo string `json:"dnat_to,omitempty"` DNATPort int `json:"dnat_port,omitempty"` SNATTo string `json:"snat_to,omitempty"` // State is TCP's (established, time-wait, …); Assured: traffic went // both ways. State string `json:"state,omitempty"` Assured bool `json:"assured,omitempty"` // Timeout is the seconds left until it is forgotten unless more // traffic comes. Timeout int `json:"timeout"` Mark uint32 `json:"mark,omitempty"` // Packets and Bytes, both ways, when the kernel counts them // (net.netfilter.nf_conntrack_acct). Packets uint64 `json:"packets,omitempty"` Bytes uint64 `json:"bytes,omitempty"` } // NamesInput asks for the names of addresses (POST /names). type NamesInput struct { Addresses []string `json:"addresses"` } // Names are the names found for the inside addresses asked about, by // address; those without one (or still being looked up) are left out. // Enabled is false when names are off (names.enabled). type Names struct { Enabled bool `json:"enabled"` Names map[string]string `json:"names"` } // CloseConnections closes connections: these, or every one Search finds // (as GET /connections?q= does). type CloseConnections struct { Connections []Connection `json:"connections,omitempty"` Search string `json:"search,omitempty"` } // TraceInput says what to trace: packets from or to Address (an address // or prefix), on Port (either way), of Protocol, for Seconds (0: 60). type TraceInput struct { Address string `json:"address,omitempty"` Port int `json:"port,omitempty"` Protocol string `json:"protocol,omitempty"` Seconds int `json:"seconds,omitempty"` } // TraceStep is one rule or policy a traced packet met, in any table. type TraceStep struct { Table string `json:"table"` // "inet housecarl", "ip nat"… Chain string `json:"chain"` Kind string `json:"kind"` // rule or policy // Rule is the rule as nftables writes it; Mark its Housecarl mark // ("r12", "b-established"), if it is one of Housecarl's. Rule string `json:"rule,omitempty"` Mark string `json:"mark,omitempty"` Verdict string `json:"verdict"` // accept, drop, continue, jump X… // Label names Housecarl's object ("rule input #12 (ssh)"). Label string `json:"label,omitempty"` } // TracePacket is one traced packet: its headers as it arrived, every // step, and its fate. type TracePacket struct { ID string `json:"id"` Time time.Time `json:"time"` In string `json:"in,omitempty"` Out string `json:"out,omitempty"` Protocol string `json:"protocol,omitempty"` Source string `json:"source,omitempty"` SourcePort int `json:"source_port,omitempty"` Destination string `json:"destination,omitempty"` DestinationPort int `json:"destination_port,omitempty"` TCPFlags string `json:"tcp_flags,omitempty"` ICMPType string `json:"icmp_type,omitempty"` Steps []TraceStep `json:"steps"` // Verdict is accepted or dropped; DecidedBy the step that decided. Verdict string `json:"verdict"` DecidedBy TraceStep `json:"decided_by"` // WouldDrop, in shadow mode, is the step of Housecarl's where it // would have dropped (or rejected) the packet, had it been enforcing. WouldDrop *TraceStep `json:"would_drop,omitempty"` } // TraceStatus is the trace running (or the last one). type TraceStatus struct { Running bool `json:"running"` Input TraceInput `json:"input"` Started time.Time `json:"started"` Ends time.Time `json:"ends"` Packets []TracePacket `json:"packets"` Error string `json:"error,omitempty"` } // Matrix is what the rules do to new connections between zones: // zone → zone (the forward chain), zone → the firewall (input) and the // firewall → zone (output). "firewall" stands for the machine itself. type Matrix struct { Zones []string `json:"zones"` Cells []MatrixCell `json:"cells"` } // MatrixCell is one pair: what happens to new connections from From to // To that no exception covers (Verdict, decided by DecidedBy), and the // exceptions: rules and built-ins that decide for some of them (some // addresses, services, times…), in the order they are checked. type MatrixCell struct { From string `json:"from"` To string `json:"to"` Chain string `json:"chain"` Verdict string `json:"verdict"` // allowed or blocked DecidedBy string `json:"decided_by"` RuleID int64 `json:"rule_id,omitempty"` // the deciding rule Exceptions []MatrixException `json:"exceptions"` // Summary is the pair at a glance: allowed or blocked when every new // connection gets the same, partly when some are let through and // some not. Summary string `json:"summary"` // Allowed and Blocked count the exceptions that let part of the // traffic through, and that block part of it. Allowed int `json:"allowed"` Blocked int `json:"blocked"` // Open sums up what the allowing exceptions let through, shortest // first in their order: "ssh", "dns", "5 port forwards". Open []string `json:"open"` // SameZone: from a zone to itself. Note says what of that traffic // passes the firewall at all. SameZone bool `json:"same_zone,omitempty"` Note string `json:"note,omitempty"` // Hits are the deciding rule's (or policy's) packets, all of its // traffic since housecarld started (nil: no counter). Hits *uint64 `json:"hits,omitempty"` // Marks are the counters behind Hits. Marks []string `json:"-"` } // MatrixException is a rule (or built-in) that decides for part of a // pair's traffic. type MatrixException struct { RuleID int64 `json:"rule_id,omitempty"` // 0: a built-in, or port forwards Chain string `json:"chain"` Label string `json:"label"` // Verdict is allowed, blocked, logged (and goes on), or unknown (a raw // rule Housecarl can't read). Verdict string `json:"verdict"` // What it covers: "from trusted · ssh", "Monday 08:00–17:00". What string `json:"what"` // Short is What in a word or two, for the overview: its services, // or else its first condition. Short string `json:"short"` // Hits are its packets, all of its traffic since housecarld started // (a rule may apply to several pairs; nil: no counter). Hits *uint64 `json:"hits,omitempty"` // Marks are the counters behind Hits ("r12", "b-ping", "f3"). Marks []string `json:"-"` }