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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 ''
|
||||
);
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user