Files
tomswallapi/internal/store/perdevice_l2.go
T
benvin d9d192757b
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Add per-device L2/misc long-tail: tunnels/stopped_rules/proxy_arp/proxy_ndp/arp_rules/maclist
Storage + CRUD (migration 0007, model, id-keyed store, REST handlers) + compiler
rendering, each owned by a device. proxy_arp/proxy_ndp share the ProxyEntry
shape via table-parameterized store helpers.
2026-07-26 15:50:01 +10:00

321 lines
10 KiB
Go

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)
}