Merge pull request 'Add global-tier long-tail resources (policy CRUD, blrules, conntrack)' (#9) from benvin/longtail-global into main
Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
-- Global-compiled long-tail sections: blrules (blacklist/whitelist, processed
|
||||
-- before normal rules) and conntrack (connection-tracking control). The policies
|
||||
-- table already exists (migration 0001); this migration only adds the two new
|
||||
-- tables — policy CRUD is wired on the existing table.
|
||||
|
||||
CREATE TABLE blrules (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
priority INT NOT NULL DEFAULT 0,
|
||||
action TEXT NOT NULL,
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
dest TEXT NOT NULL DEFAULT '',
|
||||
proto TEXT NOT NULL DEFAULT '',
|
||||
dport JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
sport JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
log TEXT NOT NULL DEFAULT '',
|
||||
comment TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE conntrack (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
priority INT NOT NULL DEFAULT 0,
|
||||
action TEXT NOT NULL,
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
dest TEXT NOT NULL DEFAULT '',
|
||||
proto TEXT NOT NULL DEFAULT '',
|
||||
dport JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
sport JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
chain TEXT NOT NULL DEFAULT '',
|
||||
helper TEXT NOT NULL DEFAULT '',
|
||||
"user" TEXT NOT NULL DEFAULT '',
|
||||
comment TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
// BlruleRule is a blacklist/whitelist rule, processed before normal rules.
|
||||
type BlruleRule struct {
|
||||
ID int64 `json:"id"`
|
||||
Priority int `json:"priority"`
|
||||
Action string `json:"action"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Dest string `json:"dest,omitempty"`
|
||||
Proto string `json:"proto,omitempty"`
|
||||
DPort []string `json:"dport,omitempty"`
|
||||
SPort []string `json:"sport,omitempty"`
|
||||
Log string `json:"log,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// ConntrackRule controls connection tracking (notrack, helper assignment).
|
||||
type ConntrackRule struct {
|
||||
ID int64 `json:"id"`
|
||||
Priority int `json:"priority"`
|
||||
Action string `json:"action"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Dest string `json:"dest,omitempty"`
|
||||
Proto string `json:"proto,omitempty"`
|
||||
DPort []string `json:"dport,omitempty"`
|
||||
SPort []string `json:"sport,omitempty"`
|
||||
Chain string `json:"chain,omitempty"`
|
||||
Helper string `json:"helper,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.unkin.net/unkin/tomswallapi/internal/model"
|
||||
)
|
||||
|
||||
// mountLongtail wires the global-compiled long-tail resources: policies (default
|
||||
// posture), blrules (blacklist/whitelist), and conntrack.
|
||||
func (s *Server) mountLongtail(r chi.Router) {
|
||||
r.Route("/policies", func(r chi.Router) {
|
||||
r.Get("/", s.listPolicies)
|
||||
r.Post("/", s.createPolicy)
|
||||
r.Get("/{id}", s.getPolicy)
|
||||
r.Delete("/{id}", s.deletePolicy)
|
||||
})
|
||||
r.Route("/blrules", func(r chi.Router) {
|
||||
r.Get("/", s.listBlrules)
|
||||
r.Post("/", s.createBlrule)
|
||||
r.Get("/{id}", s.getBlrule)
|
||||
r.Delete("/{id}", s.deleteBlrule)
|
||||
})
|
||||
r.Route("/conntrack", func(r chi.Router) {
|
||||
r.Get("/", s.listConntrack)
|
||||
r.Post("/", s.createConntrack)
|
||||
r.Get("/{id}", s.getConntrack)
|
||||
r.Delete("/{id}", s.deleteConntrack)
|
||||
})
|
||||
}
|
||||
|
||||
// ---- Policies --------------------------------------------------------------
|
||||
|
||||
func (s *Server) listPolicies(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.store.ListPolicies(r.Context())
|
||||
respondList(w, list, err)
|
||||
}
|
||||
|
||||
func (s *Server) createPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
var v model.Policy
|
||||
if !decode(w, r, &v) {
|
||||
return
|
||||
}
|
||||
if v.Action == "" {
|
||||
writeError(w, http.StatusBadRequest, "action is required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreatePolicy(r.Context(), v)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
v.ID = id
|
||||
writeJSON(w, http.StatusCreated, v)
|
||||
}
|
||||
|
||||
func (s *Server) getPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, err := s.store.GetPolicy(r.Context(), id)
|
||||
respondOne(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) deletePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondDelete(w, s.store.DeletePolicy(r.Context(), id))
|
||||
}
|
||||
|
||||
// ---- Blrules ---------------------------------------------------------------
|
||||
|
||||
func (s *Server) listBlrules(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.store.ListBlrules(r.Context())
|
||||
respondList(w, list, err)
|
||||
}
|
||||
|
||||
func (s *Server) createBlrule(w http.ResponseWriter, r *http.Request) {
|
||||
var v model.BlruleRule
|
||||
if !decode(w, r, &v) {
|
||||
return
|
||||
}
|
||||
if v.Action == "" {
|
||||
writeError(w, http.StatusBadRequest, "action is required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreateBlrule(r.Context(), v)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
v.ID = id
|
||||
writeJSON(w, http.StatusCreated, v)
|
||||
}
|
||||
|
||||
func (s *Server) getBlrule(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, err := s.store.GetBlrule(r.Context(), id)
|
||||
respondOne(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) deleteBlrule(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondDelete(w, s.store.DeleteBlrule(r.Context(), id))
|
||||
}
|
||||
|
||||
// ---- Conntrack -------------------------------------------------------------
|
||||
|
||||
func (s *Server) listConntrack(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.store.ListConntrack(r.Context())
|
||||
respondList(w, list, err)
|
||||
}
|
||||
|
||||
func (s *Server) createConntrack(w http.ResponseWriter, r *http.Request) {
|
||||
var v model.ConntrackRule
|
||||
if !decode(w, r, &v) {
|
||||
return
|
||||
}
|
||||
if v.Action == "" {
|
||||
writeError(w, http.StatusBadRequest, "action is required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreateConntrack(r.Context(), v)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
v.ID = id
|
||||
writeJSON(w, http.StatusCreated, v)
|
||||
}
|
||||
|
||||
func (s *Server) getConntrack(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, err := s.store.GetConntrack(r.Context(), id)
|
||||
respondOne(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) deleteConntrack(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondDelete(w, s.store.DeleteConntrack(r.Context(), id))
|
||||
}
|
||||
@@ -60,6 +60,7 @@ func (s *Server) mountResources(r chi.Router) {
|
||||
r.Delete("/{id}", s.deleteRule)
|
||||
})
|
||||
s.mountNAT(r)
|
||||
s.mountLongtail(r)
|
||||
}
|
||||
|
||||
// respondOne writes a single resource, mapping ErrNotFound to 404.
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"git.unkin.net/unkin/tomswallapi/internal/model"
|
||||
)
|
||||
|
||||
// ---- Policies (table exists from 0001; ListPolicies is in store.go) ---------
|
||||
|
||||
func (s *Store) GetPolicy(ctx context.Context, id int64) (model.Policy, error) {
|
||||
var p model.Policy
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT id, priority, source, dest, action, log FROM policies WHERE id = $1`, id,
|
||||
).Scan(&p.ID, &p.Priority, &p.Source, &p.Dest, &p.Action, &p.Log)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return p, ErrNotFound
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (s *Store) CreatePolicy(ctx context.Context, p model.Policy) (int64, error) {
|
||||
var id int64
|
||||
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO policies (priority, source, dest, action, log)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING id`,
|
||||
p.Priority, p.Source, p.Dest, p.Action, p.Log,
|
||||
).Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
return bump(ctx, tx)
|
||||
})
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) DeletePolicy(ctx context.Context, id int64) error {
|
||||
return s.deleteOne(ctx, `DELETE FROM policies WHERE id = $1`, id)
|
||||
}
|
||||
|
||||
// ---- Blrules ---------------------------------------------------------------
|
||||
|
||||
func (s *Store) ListBlrules(ctx context.Context) ([]model.BlruleRule, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, priority, action, source, dest, proto, dport, sport, log, comment
|
||||
FROM blrules ORDER BY priority, id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []model.BlruleRule
|
||||
for rows.Next() {
|
||||
r, err := scanBlrule(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetBlrule(ctx context.Context, id int64) (model.BlruleRule, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, priority, action, source, dest, proto, dport, sport, log, comment
|
||||
FROM blrules WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return model.BlruleRule{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return model.BlruleRule{}, ErrNotFound
|
||||
}
|
||||
return scanBlrule(rows)
|
||||
}
|
||||
|
||||
func scanBlrule(rows pgx.Rows) (model.BlruleRule, error) {
|
||||
var r model.BlruleRule
|
||||
var dport, sport []byte
|
||||
if err := rows.Scan(&r.ID, &r.Priority, &r.Action, &r.Source, &r.Dest, &r.Proto, &dport, &sport, &r.Log, &r.Comment); err != nil {
|
||||
return r, err
|
||||
}
|
||||
if err := unmarshalStrings(dport, &r.DPort); err != nil {
|
||||
return r, err
|
||||
}
|
||||
return r, unmarshalStrings(sport, &r.SPort)
|
||||
}
|
||||
|
||||
func (s *Store) CreateBlrule(ctx context.Context, r model.BlruleRule) (int64, error) {
|
||||
dport, _ := jsonb(r.DPort)
|
||||
sport, _ := jsonb(r.SPort)
|
||||
var id int64
|
||||
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO blrules (priority, action, source, dest, proto, dport, sport, log, comment)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`,
|
||||
r.Priority, r.Action, r.Source, r.Dest, r.Proto, dport, sport, r.Log, r.Comment,
|
||||
).Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
return bump(ctx, tx)
|
||||
})
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteBlrule(ctx context.Context, id int64) error {
|
||||
return s.deleteOne(ctx, `DELETE FROM blrules WHERE id = $1`, id)
|
||||
}
|
||||
|
||||
// ---- Conntrack -------------------------------------------------------------
|
||||
|
||||
func (s *Store) ListConntrack(ctx context.Context) ([]model.ConntrackRule, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, priority, action, source, dest, proto, dport, sport, chain, helper, "user", comment
|
||||
FROM conntrack ORDER BY priority, id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []model.ConntrackRule
|
||||
for rows.Next() {
|
||||
r, err := scanConntrack(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetConntrack(ctx context.Context, id int64) (model.ConntrackRule, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, priority, action, source, dest, proto, dport, sport, chain, helper, "user", comment
|
||||
FROM conntrack WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return model.ConntrackRule{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return model.ConntrackRule{}, ErrNotFound
|
||||
}
|
||||
return scanConntrack(rows)
|
||||
}
|
||||
|
||||
func scanConntrack(rows pgx.Rows) (model.ConntrackRule, error) {
|
||||
var r model.ConntrackRule
|
||||
var dport, sport []byte
|
||||
if err := rows.Scan(&r.ID, &r.Priority, &r.Action, &r.Source, &r.Dest, &r.Proto, &dport, &sport, &r.Chain, &r.Helper, &r.User, &r.Comment); err != nil {
|
||||
return r, err
|
||||
}
|
||||
if err := unmarshalStrings(dport, &r.DPort); err != nil {
|
||||
return r, err
|
||||
}
|
||||
return r, unmarshalStrings(sport, &r.SPort)
|
||||
}
|
||||
|
||||
func (s *Store) CreateConntrack(ctx context.Context, r model.ConntrackRule) (int64, error) {
|
||||
dport, _ := jsonb(r.DPort)
|
||||
sport, _ := jsonb(r.SPort)
|
||||
var id int64
|
||||
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO conntrack (priority, action, source, dest, proto, dport, sport, chain, helper, "user", comment)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id`,
|
||||
r.Priority, r.Action, r.Source, r.Dest, r.Proto, dport, sport, r.Chain, r.Helper, r.User, r.Comment,
|
||||
).Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
return bump(ctx, tx)
|
||||
})
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteConntrack(ctx context.Context, id int64) error {
|
||||
return s.deleteOne(ctx, `DELETE FROM conntrack WHERE id = $1`, id)
|
||||
}
|
||||
@@ -46,6 +46,11 @@ func jsonb(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// unmarshalStrings decodes a JSONB string array into dst.
|
||||
func unmarshalStrings(data []byte, dst *[]string) error {
|
||||
return json.Unmarshal(data, dst)
|
||||
}
|
||||
|
||||
// ---- Fabrics ---------------------------------------------------------------
|
||||
|
||||
func (s *Store) ListFabrics(ctx context.Context) ([]model.Fabric, error) {
|
||||
|
||||
Reference in New Issue
Block a user