Long-tail batch 5 (API): secmark/var + render blrules/conntrack #13
@@ -52,6 +52,10 @@ type Input struct {
|
|||||||
TCFilters []model.TCFilter
|
TCFilters []model.TCFilter
|
||||||
TCInterfaces []model.TCInterface
|
TCInterfaces []model.TCInterface
|
||||||
TCPriorities []model.TCPriority
|
TCPriorities []model.TCPriority
|
||||||
|
Blrules []model.BlruleRule
|
||||||
|
Conntrack []model.ConntrackRule
|
||||||
|
Secmarks []model.SecmarkRule
|
||||||
|
Vars []model.Var
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderedConfig is the per-device output served to the agent.
|
// RenderedConfig is the per-device output served to the agent.
|
||||||
@@ -86,6 +90,10 @@ type RenderedConfig struct {
|
|||||||
TCFilters []RenderedTCFilter `yaml:"tc_filters,omitempty" json:"tc_filters,omitempty"`
|
TCFilters []RenderedTCFilter `yaml:"tc_filters,omitempty" json:"tc_filters,omitempty"`
|
||||||
TCInterfaces []RenderedTCInterface `yaml:"tc_interfaces,omitempty" json:"tc_interfaces,omitempty"`
|
TCInterfaces []RenderedTCInterface `yaml:"tc_interfaces,omitempty" json:"tc_interfaces,omitempty"`
|
||||||
TCPriorities []RenderedTCPriority `yaml:"tc_priorities,omitempty" json:"tc_priorities,omitempty"`
|
TCPriorities []RenderedTCPriority `yaml:"tc_priorities,omitempty" json:"tc_priorities,omitempty"`
|
||||||
|
Blrules []RenderedBlrule `yaml:"blrules,omitempty" json:"blrules,omitempty"`
|
||||||
|
Conntrack []RenderedConntrack `yaml:"conntrack,omitempty" json:"conntrack,omitempty"`
|
||||||
|
Secmarks []RenderedSecmark `yaml:"secmarks,omitempty" json:"secmarks,omitempty"`
|
||||||
|
Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderedSNAT is a resolved SNAT/masquerade rule: source addresses masqueraded
|
// RenderedSNAT is a resolved SNAT/masquerade rule: source addresses masqueraded
|
||||||
@@ -235,6 +243,7 @@ func Render(in Input) (*RenderedConfig, error) {
|
|||||||
renderPerDevice(in, out)
|
renderPerDevice(in, out)
|
||||||
renderPerDeviceL2(in, out)
|
renderPerDeviceL2(in, out)
|
||||||
renderTraffic(in, out)
|
renderTraffic(in, out)
|
||||||
|
renderGlobal2(in, out)
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@@ -516,5 +525,17 @@ func Compile(ctx context.Context, s *store.Store, device string) (*RenderedConfi
|
|||||||
if in.TCPriorities, err = s.ListTCPriorities(ctx); err != nil {
|
if in.TCPriorities, err = s.ListTCPriorities(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if in.Blrules, err = s.ListBlrules(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if in.Conntrack, err = s.ListConntrack(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if in.Secmarks, err = s.ListSecmarks(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if in.Vars, err = s.ListVars(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return Render(in)
|
return Render(in)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package compiler
|
||||||
|
|
||||||
|
// Global-compiled long-tail: blrules, conntrack, secmarks (rendered on enforcing
|
||||||
|
// devices), and vars (substitution variables, rendered on every device).
|
||||||
|
|
||||||
|
type RenderedBlrule struct {
|
||||||
|
Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`
|
||||||
|
Action string `yaml:"action" json:"action"`
|
||||||
|
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
||||||
|
Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
|
||||||
|
Proto string `yaml:"proto,omitempty" json:"proto,omitempty"`
|
||||||
|
DPort []string `yaml:"dport,omitempty" json:"dport,omitempty"`
|
||||||
|
SPort []string `yaml:"sport,omitempty" json:"sport,omitempty"`
|
||||||
|
Log string `yaml:"log,omitempty" json:"log,omitempty"`
|
||||||
|
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderedConntrack struct {
|
||||||
|
Action string `yaml:"action" json:"action"`
|
||||||
|
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
||||||
|
Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
|
||||||
|
Proto string `yaml:"proto,omitempty" json:"proto,omitempty"`
|
||||||
|
DPort []string `yaml:"dport,omitempty" json:"dport,omitempty"`
|
||||||
|
SPort []string `yaml:"sport,omitempty" json:"sport,omitempty"`
|
||||||
|
Chain string `yaml:"chain,omitempty" json:"chain,omitempty"`
|
||||||
|
Helper string `yaml:"helper,omitempty" json:"helper,omitempty"`
|
||||||
|
User string `yaml:"user,omitempty" json:"user,omitempty"`
|
||||||
|
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderedSecmark struct {
|
||||||
|
Secmark string `yaml:"secmark" json:"secmark"`
|
||||||
|
Chain string `yaml:"chain" json:"chain"`
|
||||||
|
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
||||||
|
Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
|
||||||
|
Proto string `yaml:"proto,omitempty" json:"proto,omitempty"`
|
||||||
|
DPort []string `yaml:"dport,omitempty" json:"dport,omitempty"`
|
||||||
|
SPort []string `yaml:"sport,omitempty" json:"sport,omitempty"`
|
||||||
|
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderGlobal2 renders the global-compiled sections. blrules/conntrack/secmarks
|
||||||
|
// only apply on enforcing devices; vars are always emitted.
|
||||||
|
func renderGlobal2(in Input, out *RenderedConfig) {
|
||||||
|
if len(in.Vars) > 0 {
|
||||||
|
out.Vars = make(map[string]string, len(in.Vars))
|
||||||
|
for _, v := range in.Vars {
|
||||||
|
out.Vars[v.Key] = v.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !out.Enforcing {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, b := range in.Blrules {
|
||||||
|
out.Blrules = append(out.Blrules, RenderedBlrule{
|
||||||
|
Priority: b.Priority, Action: b.Action, Source: b.Source, Dest: b.Dest, Proto: b.Proto,
|
||||||
|
DPort: b.DPort, SPort: b.SPort, Log: b.Log, Comment: b.Comment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, c := range in.Conntrack {
|
||||||
|
out.Conntrack = append(out.Conntrack, RenderedConntrack{
|
||||||
|
Action: c.Action, Source: c.Source, Dest: c.Dest, Proto: c.Proto, DPort: c.DPort, SPort: c.SPort,
|
||||||
|
Chain: c.Chain, Helper: c.Helper, User: c.User, Comment: c.Comment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, s := range in.Secmarks {
|
||||||
|
out.Secmarks = append(out.Secmarks, RenderedSecmark{
|
||||||
|
Secmark: s.Secmark, Chain: s.Chain, Source: s.Source, Dest: s.Dest, Proto: s.Proto,
|
||||||
|
DPort: s.DPort, SPort: s.SPort, Comment: s.Comment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- Final long-tail sections: secmarks (SELinux security marking, global-compiled)
|
||||||
|
-- and vars (global key-value substitution variables).
|
||||||
|
|
||||||
|
CREATE TABLE secmarks (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
secmark TEXT NOT NULL,
|
||||||
|
chain 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,
|
||||||
|
comment TEXT NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE vars (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// SecmarkRule applies an SELinux security mark (global-compiled).
|
||||||
|
type SecmarkRule struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Secmark string `json:"secmark"`
|
||||||
|
Chain string `json:"chain"`
|
||||||
|
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"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Var is a global key-value substitution variable.
|
||||||
|
type Var struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
"git.unkin.net/unkin/tomswallapi/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mountGlobal2 wires secmarks (id-keyed) and vars (key-keyed).
|
||||||
|
func (s *Server) mountGlobal2(r chi.Router) {
|
||||||
|
idCRUD(r, "/secmarks", s.listSecmarks, s.createSecmark, s.getSecmark, s.deleteSecmark)
|
||||||
|
r.Route("/vars", func(r chi.Router) {
|
||||||
|
r.Get("/", s.listVars)
|
||||||
|
r.Get("/{key}", s.getVar)
|
||||||
|
r.Put("/{key}", s.putVar)
|
||||||
|
r.Delete("/{key}", s.deleteVar)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) listSecmarks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
list, err := s.store.ListSecmarks(r.Context())
|
||||||
|
respondList(w, list, err)
|
||||||
|
}
|
||||||
|
func (s *Server) createSecmark(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var v model.SecmarkRule
|
||||||
|
if !decode(w, r, &v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v.Secmark == "" || v.Chain == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "secmark and chain are required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := s.store.CreateSecmark(r.Context(), v)
|
||||||
|
if err == nil {
|
||||||
|
v.ID = id
|
||||||
|
}
|
||||||
|
respondCreated(w, v, err)
|
||||||
|
}
|
||||||
|
func (s *Server) getSecmark(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, ok := idParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v, err := s.store.GetSecmark(r.Context(), id)
|
||||||
|
respondOne(w, v, err)
|
||||||
|
}
|
||||||
|
func (s *Server) deleteSecmark(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, ok := idParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respondDelete(w, s.store.DeleteSecmark(r.Context(), id))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) listVars(w http.ResponseWriter, r *http.Request) {
|
||||||
|
list, err := s.store.ListVars(r.Context())
|
||||||
|
respondList(w, list, err)
|
||||||
|
}
|
||||||
|
func (s *Server) getVar(w http.ResponseWriter, r *http.Request) {
|
||||||
|
v, err := s.store.GetVar(r.Context(), chi.URLParam(r, "key"))
|
||||||
|
respondOne(w, v, err)
|
||||||
|
}
|
||||||
|
func (s *Server) putVar(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var v model.Var
|
||||||
|
if !decode(w, r, &v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v.Key = chi.URLParam(r, "key")
|
||||||
|
if err := s.store.UpsertVar(r.Context(), v); err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, v)
|
||||||
|
}
|
||||||
|
func (s *Server) deleteVar(w http.ResponseWriter, r *http.Request) {
|
||||||
|
respondDelete(w, s.store.DeleteVar(r.Context(), chi.URLParam(r, "key")))
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@ func (s *Server) mountResources(r chi.Router) {
|
|||||||
s.mountPerDevice(r)
|
s.mountPerDevice(r)
|
||||||
s.mountPerDeviceL2(r)
|
s.mountPerDeviceL2(r)
|
||||||
s.mountTraffic(r)
|
s.mountTraffic(r)
|
||||||
|
s.mountGlobal2(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// respondOne writes a single resource, mapping ErrNotFound to 404.
|
// respondOne writes a single resource, mapping ErrNotFound to 404.
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
||||||
|
"git.unkin.net/unkin/tomswallapi/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Secmarks --------------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *Store) ListSecmarks(ctx context.Context) ([]model.SecmarkRule, error) {
|
||||||
|
rows, err := s.pool.Query(ctx,
|
||||||
|
`SELECT id, secmark, chain, source, dest, proto, dport, sport, comment FROM secmarks ORDER BY id`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []model.SecmarkRule
|
||||||
|
for rows.Next() {
|
||||||
|
r, err := scanSecmark(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetSecmark(ctx context.Context, id int64) (model.SecmarkRule, error) {
|
||||||
|
rows, err := s.pool.Query(ctx,
|
||||||
|
`SELECT id, secmark, chain, source, dest, proto, dport, sport, comment FROM secmarks WHERE id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return model.SecmarkRule{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
if !rows.Next() {
|
||||||
|
return model.SecmarkRule{}, ErrNotFound
|
||||||
|
}
|
||||||
|
return scanSecmark(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanSecmark(rows pgx.Rows) (model.SecmarkRule, error) {
|
||||||
|
var r model.SecmarkRule
|
||||||
|
var dport, sport []byte
|
||||||
|
if err := rows.Scan(&r.ID, &r.Secmark, &r.Chain, &r.Source, &r.Dest, &r.Proto, &dport, &sport, &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) CreateSecmark(ctx context.Context, r model.SecmarkRule) (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 secmarks (secmark, chain, source, dest, proto, dport, sport, comment)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id`,
|
||||||
|
r.Secmark, r.Chain, r.Source, r.Dest, r.Proto, dport, sport, r.Comment).Scan(&id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bump(ctx, tx)
|
||||||
|
})
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteSecmark(ctx context.Context, id int64) error {
|
||||||
|
return s.deleteOne(ctx, `DELETE FROM secmarks WHERE id = $1`, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Vars (key-keyed) ------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *Store) ListVars(ctx context.Context) ([]model.Var, error) {
|
||||||
|
rows, err := s.pool.Query(ctx, `SELECT key, value FROM vars ORDER BY key`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []model.Var
|
||||||
|
for rows.Next() {
|
||||||
|
var v model.Var
|
||||||
|
if err := rows.Scan(&v.Key, &v.Value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetVar(ctx context.Context, key string) (model.Var, error) {
|
||||||
|
var v model.Var
|
||||||
|
err := s.pool.QueryRow(ctx, `SELECT key, value FROM vars WHERE key = $1`, key).Scan(&v.Key, &v.Value)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return v, ErrNotFound
|
||||||
|
}
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpsertVar(ctx context.Context, v model.Var) error {
|
||||||
|
return pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||||
|
if _, err := tx.Exec(ctx,
|
||||||
|
`INSERT INTO vars (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`,
|
||||||
|
v.Key, v.Value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bump(ctx, tx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteVar(ctx context.Context, key string) error {
|
||||||
|
return s.deleteOne(ctx, `DELETE FROM vars WHERE key = $1`, key)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user