Merge pull request 'Long-tail batch 3 (API): per-device L2/misc (tunnels/stopped_rules/proxy_arp/proxy_ndp/arp_rules/maclist)' (#11) from benvin/longtail-l2 into main

Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
2026-07-26 16:23:35 +10:00
7 changed files with 828 additions and 0 deletions
+31
View File
@@ -39,6 +39,12 @@ type Input struct {
Providers []model.Provider
Routes []model.Route
RoutingRules []model.RoutingRule
Tunnels []model.Tunnel
StoppedRules []model.StoppedRule
ProxyARP []model.ProxyEntry
ProxyNDP []model.ProxyEntry
ArpRules []model.ArpRule
Maclist []model.MaclistEntry
}
// RenderedConfig is the per-device output served to the agent.
@@ -60,6 +66,12 @@ type RenderedConfig struct {
Providers []RenderedProvider `yaml:"providers,omitempty" json:"providers,omitempty"`
Routes []RenderedRoute `yaml:"routes,omitempty" json:"routes,omitempty"`
RoutingRules []RenderedRoutingRule `yaml:"routing_rules,omitempty" json:"routing_rules,omitempty"`
Tunnels []RenderedTunnel `yaml:"tunnels,omitempty" json:"tunnels,omitempty"`
StoppedRules []RenderedStoppedRule `yaml:"stopped_rules,omitempty" json:"stopped_rules,omitempty"`
ProxyARP []RenderedProxy `yaml:"proxy_arp,omitempty" json:"proxy_arp,omitempty"`
ProxyNDP []RenderedProxy `yaml:"proxy_ndp,omitempty" json:"proxy_ndp,omitempty"`
ArpRules []RenderedArpRule `yaml:"arp_rules,omitempty" json:"arp_rules,omitempty"`
Maclist []RenderedMaclist `yaml:"maclist,omitempty" json:"maclist,omitempty"`
}
// RenderedSNAT is a resolved SNAT/masquerade rule: source addresses masqueraded
@@ -207,6 +219,7 @@ func Render(in Input) (*RenderedConfig, error) {
// Per-device long-tail sections owned by this device.
renderPerDevice(in, out)
renderPerDeviceL2(in, out)
return out, nil
}
@@ -449,5 +462,23 @@ func Compile(ctx context.Context, s *store.Store, device string) (*RenderedConfi
if in.RoutingRules, err = s.ListRoutingRules(ctx); err != nil {
return nil, err
}
if in.Tunnels, err = s.ListTunnels(ctx); err != nil {
return nil, err
}
if in.StoppedRules, err = s.ListStoppedRules(ctx); err != nil {
return nil, err
}
if in.ProxyARP, err = s.ListProxyARP(ctx); err != nil {
return nil, err
}
if in.ProxyNDP, err = s.ListProxyNDP(ctx); err != nil {
return nil, err
}
if in.ArpRules, err = s.ListArpRules(ctx); err != nil {
return nil, err
}
if in.Maclist, err = s.ListMaclist(ctx); err != nil {
return nil, err
}
return Render(in)
}
+111
View File
@@ -0,0 +1,111 @@
package compiler
import "git.unkin.net/unkin/tomswallapi/internal/model"
// Per-device L2/misc long-tail sections rendered into a device's config.
type RenderedTunnel struct {
Type string `yaml:"type" json:"type"`
Zone string `yaml:"zone" json:"zone"`
Gateways []string `yaml:"gateways,omitempty" json:"gateways,omitempty"`
GatewayZones []string `yaml:"gateway_zones,omitempty" json:"gateway_zones,omitempty"`
Port int `yaml:"port,omitempty" json:"port,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedStoppedRule 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"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedProxy struct {
Address string `yaml:"address" json:"address"`
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
External string `yaml:"external" json:"external"`
HaveRoute bool `yaml:"haveroute,omitempty" json:"haveroute,omitempty"`
Persistent bool `yaml:"persistent,omitempty" json:"persistent,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedArpRule struct {
Action string `yaml:"action" json:"action"`
ActionAddress string `yaml:"action_address,omitempty" json:"action_address,omitempty"`
ActionMAC string `yaml:"action_mac,omitempty" json:"action_mac,omitempty"`
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
Opcode int `yaml:"opcode,omitempty" json:"opcode,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedMaclist struct {
Action string `yaml:"action" json:"action"`
Interface string `yaml:"interface" json:"interface"`
MAC string `yaml:"mac,omitempty" json:"mac,omitempty"`
Addresses []string `yaml:"addresses,omitempty" json:"addresses,omitempty"`
Log string `yaml:"log,omitempty" json:"log,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
func renderPerDeviceL2(in Input, out *RenderedConfig) {
dev := in.Device.Name
for _, t := range in.Tunnels {
if t.Device != dev {
continue
}
out.Tunnels = append(out.Tunnels, RenderedTunnel{
Type: t.Type, Zone: t.Zone, Gateways: t.Gateways,
GatewayZones: t.GatewayZones, Port: t.Port, Comment: t.Comment,
})
}
for _, r := range in.StoppedRules {
if r.Device != dev {
continue
}
out.StoppedRules = append(out.StoppedRules, RenderedStoppedRule{
Action: r.Action, Source: r.Source, Dest: r.Dest, Proto: r.Proto,
DPort: r.DPort, SPort: r.SPort, Comment: r.Comment,
})
}
for _, p := range in.ProxyARP {
if p.Device != dev {
continue
}
out.ProxyARP = append(out.ProxyARP, renderProxy(p))
}
for _, p := range in.ProxyNDP {
if p.Device != dev {
continue
}
out.ProxyNDP = append(out.ProxyNDP, renderProxy(p))
}
for _, a := range in.ArpRules {
if a.Device != dev {
continue
}
out.ArpRules = append(out.ArpRules, RenderedArpRule{
Action: a.Action, ActionAddress: a.ActionAddress, ActionMAC: a.ActionMAC,
Source: a.Source, Dest: a.Dest, Opcode: a.Opcode, Comment: a.Comment,
})
}
for _, m := range in.Maclist {
if m.Device != dev {
continue
}
out.Maclist = append(out.Maclist, RenderedMaclist{
Action: m.Action, Interface: m.Interface, MAC: m.MAC,
Addresses: m.Addresses, Log: m.Log, Comment: m.Comment,
})
}
}
func renderProxy(p model.ProxyEntry) RenderedProxy {
return RenderedProxy{
Address: p.Address, Interface: p.Interface, External: p.External,
HaveRoute: p.HaveRoute, Persistent: p.Persistent, Comment: p.Comment,
}
}
@@ -0,0 +1,70 @@
-- Per-device L2/misc long-tail sections: tunnels, stopped_rules, proxy_arp,
-- proxy_ndp, arp_rules, maclist. Each is owned by a device.
CREATE TABLE tunnels (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
type TEXT NOT NULL,
zone TEXT NOT NULL,
gateways JSONB NOT NULL DEFAULT '[]'::jsonb,
gateway_zones JSONB NOT NULL DEFAULT '[]'::jsonb,
port INT NOT NULL DEFAULT 0,
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE stopped_rules (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
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,
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE proxy_arp (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
address TEXT NOT NULL,
interface TEXT NOT NULL DEFAULT '',
external TEXT NOT NULL,
haveroute BOOLEAN NOT NULL DEFAULT false,
persistent BOOLEAN NOT NULL DEFAULT false,
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE proxy_ndp (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
address TEXT NOT NULL,
interface TEXT NOT NULL DEFAULT '',
external TEXT NOT NULL,
haveroute BOOLEAN NOT NULL DEFAULT false,
persistent BOOLEAN NOT NULL DEFAULT false,
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE arp_rules (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
action TEXT NOT NULL,
action_address TEXT NOT NULL DEFAULT '',
action_mac TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT '',
dest TEXT NOT NULL DEFAULT '',
opcode INT NOT NULL DEFAULT 0,
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE maclist (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
action TEXT NOT NULL,
interface TEXT NOT NULL,
mac TEXT NOT NULL DEFAULT '',
addresses JSONB NOT NULL DEFAULT '[]'::jsonb,
log TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT ''
);
+61
View File
@@ -0,0 +1,61 @@
package model
// Per-device L2/misc long-tail sections.
type Tunnel struct {
ID int64 `json:"id"`
Device string `json:"device"`
Type string `json:"type"`
Zone string `json:"zone"`
Gateways []string `json:"gateways,omitempty"`
GatewayZones []string `json:"gateway_zones,omitempty"`
Port int `json:"port,omitempty"`
Comment string `json:"comment,omitempty"`
}
type StoppedRule struct {
ID int64 `json:"id"`
Device string `json:"device"`
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"`
Comment string `json:"comment,omitempty"`
}
// ProxyEntry backs both proxy_arp and proxy_ndp (identical shape).
type ProxyEntry struct {
ID int64 `json:"id"`
Device string `json:"device"`
Address string `json:"address"`
Interface string `json:"interface,omitempty"`
External string `json:"external"`
HaveRoute bool `json:"haveroute,omitempty"`
Persistent bool `json:"persistent,omitempty"`
Comment string `json:"comment,omitempty"`
}
type ArpRule struct {
ID int64 `json:"id"`
Device string `json:"device"`
Action string `json:"action"`
ActionAddress string `json:"action_address,omitempty"`
ActionMAC string `json:"action_mac,omitempty"`
Source string `json:"source,omitempty"`
Dest string `json:"dest,omitempty"`
Opcode int `json:"opcode,omitempty"`
Comment string `json:"comment,omitempty"`
}
type MaclistEntry struct {
ID int64 `json:"id"`
Device string `json:"device"`
Action string `json:"action"`
Interface string `json:"interface"`
MAC string `json:"mac,omitempty"`
Addresses []string `json:"addresses,omitempty"`
Log string `json:"log,omitempty"`
Comment string `json:"comment,omitempty"`
}
+234
View File
@@ -0,0 +1,234 @@
package server
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
"git.unkin.net/unkin/tomswallapi/internal/model"
)
// mountPerDeviceL2 wires the per-device L2/misc sections: tunnels, stopped_rules,
// proxy_arp, proxy_ndp, arp_rules, maclist.
func (s *Server) mountPerDeviceL2(r chi.Router) {
idCRUD(r, "/tunnels", s.listTunnels, s.createTunnel, s.getTunnel, s.deleteTunnel)
idCRUD(r, "/stopped-rules", s.listStoppedRules, s.createStoppedRule, s.getStoppedRule, s.deleteStoppedRule)
idCRUD(r, "/proxy-arp", s.listProxyARP, s.createProxyARP, s.getProxyARP, s.deleteProxyARP)
idCRUD(r, "/proxy-ndp", s.listProxyNDP, s.createProxyNDP, s.getProxyNDP, s.deleteProxyNDP)
idCRUD(r, "/arp-rules", s.listArpRules, s.createArpRule, s.getArpRule, s.deleteArpRule)
idCRUD(r, "/maclist", s.listMaclist, s.createMaclist, s.getMaclist, s.deleteMaclist)
}
// idCRUD wires the four standard id-keyed handlers for a collection.
func idCRUD(r chi.Router, path string, list, create, get, del http.HandlerFunc) {
r.Route(path, func(r chi.Router) {
r.Get("/", list)
r.Post("/", create)
r.Get("/{id}", get)
r.Delete("/{id}", del)
})
}
func (s *Server) listTunnels(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListTunnels(r.Context())
respondList(w, list, err)
}
func (s *Server) createTunnel(w http.ResponseWriter, r *http.Request) {
var v model.Tunnel
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Type == "" || v.Zone == "" {
writeError(w, http.StatusBadRequest, "device, type, and zone are required")
return
}
id, err := s.store.CreateTunnel(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getTunnel(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetTunnel(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteTunnel(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteTunnel(r.Context(), id))
}
func (s *Server) listStoppedRules(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListStoppedRules(r.Context())
respondList(w, list, err)
}
func (s *Server) createStoppedRule(w http.ResponseWriter, r *http.Request) {
var v model.StoppedRule
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Action == "" {
writeError(w, http.StatusBadRequest, "device and action are required")
return
}
id, err := s.store.CreateStoppedRule(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getStoppedRule(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetStoppedRule(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteStoppedRule(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteStoppedRule(r.Context(), id))
}
func (s *Server) listProxyARP(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListProxyARP(r.Context())
respondList(w, list, err)
}
func (s *Server) createProxyARP(w http.ResponseWriter, r *http.Request) {
s.createProxy(w, r, s.store.CreateProxyARP)
}
func (s *Server) getProxyARP(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetProxyARP(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteProxyARP(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteProxyARP(r.Context(), id))
}
func (s *Server) listProxyNDP(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListProxyNDP(r.Context())
respondList(w, list, err)
}
func (s *Server) createProxyNDP(w http.ResponseWriter, r *http.Request) {
s.createProxy(w, r, s.store.CreateProxyNDP)
}
func (s *Server) getProxyNDP(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetProxyNDP(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteProxyNDP(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteProxyNDP(r.Context(), id))
}
// createProxy is shared by proxy_arp/ndp (identical shape).
func (s *Server) createProxy(w http.ResponseWriter, r *http.Request, create func(context.Context, model.ProxyEntry) (int64, error)) {
var v model.ProxyEntry
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Address == "" || v.External == "" {
writeError(w, http.StatusBadRequest, "device, address, and external are required")
return
}
id, err := create(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) listArpRules(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListArpRules(r.Context())
respondList(w, list, err)
}
func (s *Server) createArpRule(w http.ResponseWriter, r *http.Request) {
var v model.ArpRule
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Action == "" {
writeError(w, http.StatusBadRequest, "device and action are required")
return
}
id, err := s.store.CreateArpRule(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getArpRule(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetArpRule(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteArpRule(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteArpRule(r.Context(), id))
}
func (s *Server) listMaclist(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListMaclist(r.Context())
respondList(w, list, err)
}
func (s *Server) createMaclist(w http.ResponseWriter, r *http.Request) {
var v model.MaclistEntry
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Action == "" || v.Interface == "" {
writeError(w, http.StatusBadRequest, "device, action, and interface are required")
return
}
id, err := s.store.CreateMaclist(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getMaclist(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetMaclist(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteMaclist(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteMaclist(r.Context(), id))
}
+1
View File
@@ -62,6 +62,7 @@ func (s *Server) mountResources(r chi.Router) {
s.mountNAT(r)
s.mountLongtail(r)
s.mountPerDevice(r)
s.mountPerDeviceL2(r)
}
// respondOne writes a single resource, mapping ErrNotFound to 404.
+320
View File
@@ -0,0 +1,320 @@
package store
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"git.unkin.net/unkin/tomswallapi/internal/model"
)
// ---- Tunnels ---------------------------------------------------------------
func (s *Store) ListTunnels(ctx context.Context) ([]model.Tunnel, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, type, zone, gateways, gateway_zones, port, comment FROM tunnels ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.Tunnel
for rows.Next() {
t, err := scanTunnel(rows)
if err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (s *Store) GetTunnel(ctx context.Context, id int64) (model.Tunnel, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, type, zone, gateways, gateway_zones, port, comment FROM tunnels WHERE id = $1`, id)
if err != nil {
return model.Tunnel{}, err
}
defer rows.Close()
if !rows.Next() {
return model.Tunnel{}, ErrNotFound
}
return scanTunnel(rows)
}
func scanTunnel(rows pgx.Rows) (model.Tunnel, error) {
var t model.Tunnel
var gw, gz []byte
if err := rows.Scan(&t.ID, &t.Device, &t.Type, &t.Zone, &gw, &gz, &t.Port, &t.Comment); err != nil {
return t, err
}
if err := unmarshalStrings(gw, &t.Gateways); err != nil {
return t, err
}
return t, unmarshalStrings(gz, &t.GatewayZones)
}
func (s *Store) CreateTunnel(ctx context.Context, t model.Tunnel) (int64, error) {
gw, _ := jsonb(t.Gateways)
gz, _ := jsonb(t.GatewayZones)
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO tunnels (device, type, zone, gateways, gateway_zones, port, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
t.Device, t.Type, t.Zone, gw, gz, t.Port, t.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteTunnel(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM tunnels WHERE id = $1`, id)
}
// ---- Stopped rules ---------------------------------------------------------
func (s *Store) ListStoppedRules(ctx context.Context) ([]model.StoppedRule, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, action, source, dest, proto, dport, sport, comment FROM stopped_rules ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.StoppedRule
for rows.Next() {
r, err := scanStopped(rows)
if err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
func (s *Store) GetStoppedRule(ctx context.Context, id int64) (model.StoppedRule, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, action, source, dest, proto, dport, sport, comment FROM stopped_rules WHERE id = $1`, id)
if err != nil {
return model.StoppedRule{}, err
}
defer rows.Close()
if !rows.Next() {
return model.StoppedRule{}, ErrNotFound
}
return scanStopped(rows)
}
func scanStopped(rows pgx.Rows) (model.StoppedRule, error) {
var r model.StoppedRule
var dport, sport []byte
if err := rows.Scan(&r.ID, &r.Device, &r.Action, &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) CreateStoppedRule(ctx context.Context, r model.StoppedRule) (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 stopped_rules (device, action, source, dest, proto, dport, sport, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
r.Device, r.Action, 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) DeleteStoppedRule(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM stopped_rules WHERE id = $1`, id)
}
// ---- Proxy ARP / NDP (identical shape, table-parameterized) ----------------
func (s *Store) listProxy(ctx context.Context, table string) ([]model.ProxyEntry, error) {
q := fmt.Sprintf(`SELECT id, device, address, interface, external, haveroute, persistent, comment FROM %s ORDER BY id`, table)
rows, err := s.pool.Query(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.ProxyEntry
for rows.Next() {
var p model.ProxyEntry
if err := rows.Scan(&p.ID, &p.Device, &p.Address, &p.Interface, &p.External, &p.HaveRoute, &p.Persistent, &p.Comment); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
func (s *Store) getProxy(ctx context.Context, table string, id int64) (model.ProxyEntry, error) {
q := fmt.Sprintf(`SELECT id, device, address, interface, external, haveroute, persistent, comment FROM %s WHERE id = $1`, table)
var p model.ProxyEntry
err := s.pool.QueryRow(ctx, q, id).Scan(&p.ID, &p.Device, &p.Address, &p.Interface, &p.External, &p.HaveRoute, &p.Persistent, &p.Comment)
if errors.Is(err, pgx.ErrNoRows) {
return p, ErrNotFound
}
return p, err
}
func (s *Store) createProxy(ctx context.Context, table string, p model.ProxyEntry) (int64, error) {
q := fmt.Sprintf(`INSERT INTO %s (device, address, interface, external, haveroute, persistent, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, table)
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, q, p.Device, p.Address, p.Interface, p.External, p.HaveRoute, p.Persistent, p.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) ListProxyARP(ctx context.Context) ([]model.ProxyEntry, error) {
return s.listProxy(ctx, "proxy_arp")
}
func (s *Store) GetProxyARP(ctx context.Context, id int64) (model.ProxyEntry, error) {
return s.getProxy(ctx, "proxy_arp", id)
}
func (s *Store) CreateProxyARP(ctx context.Context, p model.ProxyEntry) (int64, error) {
return s.createProxy(ctx, "proxy_arp", p)
}
func (s *Store) DeleteProxyARP(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM proxy_arp WHERE id = $1`, id)
}
func (s *Store) ListProxyNDP(ctx context.Context) ([]model.ProxyEntry, error) {
return s.listProxy(ctx, "proxy_ndp")
}
func (s *Store) GetProxyNDP(ctx context.Context, id int64) (model.ProxyEntry, error) {
return s.getProxy(ctx, "proxy_ndp", id)
}
func (s *Store) CreateProxyNDP(ctx context.Context, p model.ProxyEntry) (int64, error) {
return s.createProxy(ctx, "proxy_ndp", p)
}
func (s *Store) DeleteProxyNDP(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM proxy_ndp WHERE id = $1`, id)
}
// ---- ARP rules -------------------------------------------------------------
func (s *Store) ListArpRules(ctx context.Context) ([]model.ArpRule, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, action, action_address, action_mac, source, dest, opcode, comment FROM arp_rules ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.ArpRule
for rows.Next() {
var r model.ArpRule
if err := rows.Scan(&r.ID, &r.Device, &r.Action, &r.ActionAddress, &r.ActionMAC, &r.Source, &r.Dest, &r.Opcode, &r.Comment); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
func (s *Store) GetArpRule(ctx context.Context, id int64) (model.ArpRule, error) {
var r model.ArpRule
err := s.pool.QueryRow(ctx,
`SELECT id, device, action, action_address, action_mac, source, dest, opcode, comment FROM arp_rules WHERE id = $1`, id,
).Scan(&r.ID, &r.Device, &r.Action, &r.ActionAddress, &r.ActionMAC, &r.Source, &r.Dest, &r.Opcode, &r.Comment)
if errors.Is(err, pgx.ErrNoRows) {
return r, ErrNotFound
}
return r, err
}
func (s *Store) CreateArpRule(ctx context.Context, r model.ArpRule) (int64, error) {
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO arp_rules (device, action, action_address, action_mac, source, dest, opcode, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
r.Device, r.Action, r.ActionAddress, r.ActionMAC, r.Source, r.Dest, r.Opcode, r.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteArpRule(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM arp_rules WHERE id = $1`, id)
}
// ---- Maclist ---------------------------------------------------------------
func (s *Store) ListMaclist(ctx context.Context) ([]model.MaclistEntry, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, action, interface, mac, addresses, log, comment FROM maclist ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.MaclistEntry
for rows.Next() {
m, err := scanMaclist(rows)
if err != nil {
return nil, err
}
out = append(out, m)
}
return out, rows.Err()
}
func (s *Store) GetMaclist(ctx context.Context, id int64) (model.MaclistEntry, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, action, interface, mac, addresses, log, comment FROM maclist WHERE id = $1`, id)
if err != nil {
return model.MaclistEntry{}, err
}
defer rows.Close()
if !rows.Next() {
return model.MaclistEntry{}, ErrNotFound
}
return scanMaclist(rows)
}
func scanMaclist(rows pgx.Rows) (model.MaclistEntry, error) {
var m model.MaclistEntry
var addrs []byte
if err := rows.Scan(&m.ID, &m.Device, &m.Action, &m.Interface, &m.MAC, &addrs, &m.Log, &m.Comment); err != nil {
return m, err
}
return m, unmarshalStrings(addrs, &m.Addresses)
}
func (s *Store) CreateMaclist(ctx context.Context, m model.MaclistEntry) (int64, error) {
addrs, _ := jsonb(m.Addresses)
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO maclist (device, action, interface, mac, addresses, log, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
m.Device, m.Action, m.Interface, m.MAC, addrs, m.Log, m.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteMaclist(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM maclist WHERE id = $1`, id)
}