package server import ( "fmt" "net/http" "strings" "housecarl/internal/api" "housecarl/internal/auth" "housecarl/internal/engine" "housecarl/internal/importer" "housecarl/internal/model" ) // importNft makes an nftables ruleset Housecarl's configuration // (internal/importer); ?dry_run=1 shows the result without applying it. func (s *Server) importNft(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := require(p, auth.ResConfig, true); err != nil { return err } if err := notStaged(r, "an import"); err != nil { return err } var in api.ImportInput if err := decodeJSON(r, &in); err != nil { return err } res, err := importer.Import(importer.Input{JSON: in.JSON, Text: in.Text, Defines: in.Defines, Table: in.Table, Zones: in.Zones}) if err != nil { return badRequest("%v", err) } cur := s.eng.State() if !in.Replace && hasConfig(cur) { return conflict("Housecarl already has a configuration; import with replace (--replace) to replace it") } // The importer numbers objects from 1; give them IDs of their own. if err := s.renumber(r, res.State); err != nil { return err } opts := engine.Options{DryRun: queryBool(r, "dry_run"), Via: via(r, p)} er, err := s.eng.Replace(r.Context(), p, "import from nftables ("+tableName(in.Table)+")", opts, res.State) out := api.ImportResult{State: res.State, Notes: res.Notes, Raw: res.Raw} if out.Notes == nil { out.Notes = []string{} } if out.Raw == nil { out.Raw = []string{} } if err != nil { s.writeErr(w, r, err, out) return nil } s.afterChange(r.Context(), p, er) if er.Applied { // Until it is taken over, the old table still applies too: the // status warns (takeover_pending). s.store.SetMeta(r.Context(), importedTableKey, res.Table) } out.MutationResponse = api.MutationResponse{Applied: er.Applied, DryRun: er.DryRun, HistoryID: er.HistoryID, Changes: er.Changes, Warnings: er.Warnings} if out.Changes == nil { out.Changes = []model.Change{} } if pr := s.probationStatus(); er.Applied && len(pr.Pending) > 0 { out.ConfirmBy = &pr.Deadline } out.ConfirmToken = s.issueCheck(w, r, p, er) writeJSON(w, http.StatusOK, out) return nil } func tableName(t string) string { if t == "" { return "the ruleset" } return t } func hasConfig(st *model.State) bool { for _, k := range model.Kinds { if len(st.Objects(k)) > 0 { return true } } return false } // renumber gives every object of an imported state a fresh ID. func (s *Server) renumber(r *http.Request, st *model.State) error { n := 0 for _, k := range model.Kinds { n += len(st.Objects(k)) } if n == 0 { return nil } ids, err := s.store.AllocateIDs(r.Context(), n) if err != nil { return err } i := 0 for _, k := range model.Kinds { for _, o := range st.Objects(k) { setID(o, ids[i]) i++ } } return nil } // takeover deletes the tables Housecarl replaces (the ruleset imported). // Over the network it is on probation: unless confirmed, the tables come // back. func (s *Server) takeover(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireAdmin(p); err != nil { return err } if err := notStaged(r, "a takeover"); err != nil { return err } var in api.TakeoverInput if err := decodeJSON(r, &in); err != nil { return err } if len(in.Tables) == 0 { return badRequest("name the tables to delete (\"inet main\")") } var done []string for _, t := range in.Tables { fam, name, ok := strings.Cut(strings.TrimSpace(t), " ") if !ok || name == "" { return badRequest("%q: write a table as FAMILY NAME (\"inet main\")", t) } if fam == "inet" && name == "housecarl" { return badRequest("that is Housecarl's own table") } script, err := s.fw.TableText(r.Context(), fam, name) if err != nil { return notFound("there is no table %s %s", fam, name) } if queryBool(r, "dry_run") { done = append(done, t) continue } if err := s.fw.DeleteTable(r.Context(), fam, name); err != nil { return fmt.Errorf("deleting table %s: %w", t, err) } s.startProbation(r.Context(), p, 0, script) s.eng.Audit(r.Context(), p, "takeover", t, "success", "") done = append(done, t) } msg := "deleted " + strings.Join(done, ", ") if queryBool(r, "dry_run") { msg = "would delete " + strings.Join(done, ", ") } else if pr := s.probationStatus(); pr.Takeovers > 0 { msg += fmt.Sprintf("; confirm with 'hc confirm' within %ds, or they come back", pr.SecondsLeft) } answer := map[string]any{"deleted": done, "message": msg} if !queryBool(r, "dry_run") && len(done) > 0 { if tok := s.issueCheck(w, r, p, nil); tok != "" { answer["confirm_token"] = tok } } writeJSON(w, http.StatusOK, answer) return nil }