Compare commits

...

7 Commits

Author SHA1 Message Date
benvin 73ddcf651e Merge pull request 'Long-tail batch 5 (API): secmark/var + render blrules/conntrack' (#13) from benvin/longtail-global2 into main
ci/woodpecker/tag/docker Pipeline was successful
Reviewed-on: #13
2026-07-26 16:57:12 +10:00
benvin c46b4ab5c5 Merge pull request 'Long-tail batch 4 (API): traffic control (mangle/accounting/tc_*)' (#12) from benvin/longtail-tc into main
Reviewed-on: #12
2026-07-26 16:56:59 +10:00
benvin 4693b7093c Add secmark/var + render blrules/conntrack/secmark/vars
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
- Add secmarks (id-keyed) and vars (key-keyed) resources: migration 0009, model,
  store CRUD, REST handlers.
- Compiler rendering for the global-compiled tail: blrules, conntrack, secmarks
  render on enforcing devices; vars render on every device. This closes the
  rendering gap left by batch 1 (blrules/conntrack were stored but not rendered).
2026-07-26 16:26:36 +10:00
benvin 6b600f8c8d Add traffic-control long-tail: mangle/accounting/tc_*
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Storage + CRUD (migration 0008, model, id-keyed store, REST handlers) + compiler
rendering for mangle, accounting, and tc_devices/tc_classes/tc_filters/
tc_interfaces/tc_priorities, each owned by a device. (Nested tc option structs
deferred.)
2026-07-26 16:26:14 +10:00
benvin d54325c685 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
2026-07-26 16:23:35 +10:00
benvin d9d192757b Add per-device L2/misc long-tail: tunnels/stopped_rules/proxy_arp/proxy_ndp/arp_rules/maclist
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
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
benvin 22e0d07227 Merge pull request 'Long-tail batch 2 (API): per-device routing (hosts/providers/routes/routing_rules)' (#10) from benvin/longtail-routing into main
Reviewed-on: #10
2026-07-26 15:44:59 +10:00
17 changed files with 2223 additions and 0 deletions
+88
View File
@@ -39,6 +39,23 @@ 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
Mangle []model.MangleRule
Accounting []model.AccountingRule
TCDevices []model.TCDevice
TCClasses []model.TCClass
TCFilters []model.TCFilter
TCInterfaces []model.TCInterface
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.
@@ -60,6 +77,23 @@ 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"`
Mangle []RenderedMangle `yaml:"mangle,omitempty" json:"mangle,omitempty"`
Accounting []RenderedAccounting `yaml:"accounting,omitempty" json:"accounting,omitempty"`
TCDevices []RenderedTCDevice `yaml:"tc_devices,omitempty" json:"tc_devices,omitempty"`
TCClasses []RenderedTCClass `yaml:"tc_classes,omitempty" json:"tc_classes,omitempty"`
TCFilters []RenderedTCFilter `yaml:"tc_filters,omitempty" json:"tc_filters,omitempty"`
TCInterfaces []RenderedTCInterface `yaml:"tc_interfaces,omitempty" json:"tc_interfaces,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
@@ -207,6 +241,9 @@ func Render(in Input) (*RenderedConfig, error) {
// Per-device long-tail sections owned by this device.
renderPerDevice(in, out)
renderPerDeviceL2(in, out)
renderTraffic(in, out)
renderGlobal2(in, out)
return out, nil
}
@@ -449,5 +486,56 @@ 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
}
if in.Mangle, err = s.ListMangle(ctx); err != nil {
return nil, err
}
if in.Accounting, err = s.ListAccounting(ctx); err != nil {
return nil, err
}
if in.TCDevices, err = s.ListTCDevices(ctx); err != nil {
return nil, err
}
if in.TCClasses, err = s.ListTCClasses(ctx); err != nil {
return nil, err
}
if in.TCFilters, err = s.ListTCFilters(ctx); err != nil {
return nil, err
}
if in.TCInterfaces, err = s.ListTCInterfaces(ctx); err != nil {
return nil, err
}
if in.TCPriorities, err = s.ListTCPriorities(ctx); err != nil {
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)
}
+72
View File
@@ -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,
})
}
}
+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,
}
}
+147
View File
@@ -0,0 +1,147 @@
package compiler
// Traffic-control tier rendered into a device's config.
type RenderedMangle struct {
Action string `yaml:"action" json:"action"`
Chain string `yaml:"chain,omitempty" json:"chain,omitempty"`
MarkValue string `yaml:"mark_value,omitempty" json:"mark_value,omitempty"`
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"`
User string `yaml:"user,omitempty" json:"user,omitempty"`
Mark string `yaml:"mark,omitempty" json:"mark,omitempty"`
Length string `yaml:"length,omitempty" json:"length,omitempty"`
TOS string `yaml:"tos,omitempty" json:"tos,omitempty"`
Helper string `yaml:"helper,omitempty" json:"helper,omitempty"`
Probability *float64 `yaml:"probability,omitempty" json:"probability,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedAccounting struct {
Action string `yaml:"action" json:"action"`
Section string `yaml:"section,omitempty" json:"section,omitempty"`
Chain string `yaml:"chain,omitempty" json:"chain,omitempty"`
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"`
Mark string `yaml:"mark,omitempty" json:"mark,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedTCDevice struct {
Interface string `yaml:"interface" json:"interface"`
InBandwidth string `yaml:"in_bandwidth,omitempty" json:"in_bandwidth,omitempty"`
OutBandwidth string `yaml:"out_bandwidth,omitempty" json:"out_bandwidth,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedTCClass struct {
Interface string `yaml:"interface" json:"interface"`
Mark int `yaml:"mark,omitempty" json:"mark,omitempty"`
Rate string `yaml:"rate,omitempty" json:"rate,omitempty"`
Ceil string `yaml:"ceil,omitempty" json:"ceil,omitempty"`
Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedTCFilter struct {
Class string `yaml:"class" json:"class"`
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"`
TOS string `yaml:"tos,omitempty" json:"tos,omitempty"`
Length int `yaml:"length,omitempty" json:"length,omitempty"`
Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedTCInterface struct {
Interface string `yaml:"interface" json:"interface"`
Type string `yaml:"type,omitempty" json:"type,omitempty"`
InBandwidth string `yaml:"in_bandwidth,omitempty" json:"in_bandwidth,omitempty"`
OutBandwidth string `yaml:"out_bandwidth,omitempty" json:"out_bandwidth,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedTCPriority struct {
Band int `yaml:"band" json:"band"`
Proto string `yaml:"proto,omitempty" json:"proto,omitempty"`
DPort []string `yaml:"dport,omitempty" json:"dport,omitempty"`
SPort []string `yaml:"sport,omitempty" json:"sport,omitempty"`
Address string `yaml:"address,omitempty" json:"address,omitempty"`
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
Helper string `yaml:"helper,omitempty" json:"helper,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
func renderTraffic(in Input, out *RenderedConfig) {
dev := in.Device.Name
for _, m := range in.Mangle {
if m.Device != dev {
continue
}
out.Mangle = append(out.Mangle, RenderedMangle{
Action: m.Action, Chain: m.Chain, MarkValue: m.MarkValue, Source: m.Source, Dest: m.Dest,
Proto: m.Proto, DPort: m.DPort, SPort: m.SPort, User: m.User, Mark: m.Mark,
Length: m.Length, TOS: m.TOS, Helper: m.Helper, Probability: m.Probability, Comment: m.Comment,
})
}
for _, a := range in.Accounting {
if a.Device != dev {
continue
}
out.Accounting = append(out.Accounting, RenderedAccounting{
Action: a.Action, Section: a.Section, Chain: a.Chain, Source: a.Source, Dest: a.Dest,
Proto: a.Proto, DPort: a.DPort, SPort: a.SPort, Mark: a.Mark, Comment: a.Comment,
})
}
for _, t := range in.TCDevices {
if t.Device != dev {
continue
}
out.TCDevices = append(out.TCDevices, RenderedTCDevice{
Interface: t.Interface, InBandwidth: t.InBandwidth, OutBandwidth: t.OutBandwidth, Comment: t.Comment,
})
}
for _, t := range in.TCClasses {
if t.Device != dev {
continue
}
out.TCClasses = append(out.TCClasses, RenderedTCClass{
Interface: t.Interface, Mark: t.Mark, Rate: t.Rate, Ceil: t.Ceil, Priority: t.Priority, Comment: t.Comment,
})
}
for _, t := range in.TCFilters {
if t.Device != dev {
continue
}
out.TCFilters = append(out.TCFilters, RenderedTCFilter{
Class: t.Class, Source: t.Source, Dest: t.Dest, Proto: t.Proto, DPort: t.DPort, SPort: t.SPort,
TOS: t.TOS, Length: t.Length, Priority: t.Priority, Comment: t.Comment,
})
}
for _, t := range in.TCInterfaces {
if t.Device != dev {
continue
}
out.TCInterfaces = append(out.TCInterfaces, RenderedTCInterface{
Interface: t.Interface, Type: t.Type, InBandwidth: t.InBandwidth, OutBandwidth: t.OutBandwidth, Comment: t.Comment,
})
}
for _, t := range in.TCPriorities {
if t.Device != dev {
continue
}
out.TCPriorities = append(out.TCPriorities, RenderedTCPriority{
Band: t.Band, Proto: t.Proto, DPort: t.DPort, SPort: t.SPort,
Address: t.Address, Interface: t.Interface, Helper: t.Helper, Comment: t.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,96 @@
-- Traffic-control tier: mangle, accounting, and tc_* (device/class/filter/
-- interface/priority). Each is owned by a device. (Nested tc option structs on
-- tc_device/tc_class are deferred.)
CREATE TABLE mangle (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
action TEXT NOT NULL,
chain TEXT NOT NULL DEFAULT '',
mark_value TEXT NOT NULL DEFAULT '',
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,
"user" TEXT NOT NULL DEFAULT '',
mark TEXT NOT NULL DEFAULT '',
length TEXT NOT NULL DEFAULT '',
tos TEXT NOT NULL DEFAULT '',
helper TEXT NOT NULL DEFAULT '',
probability REAL,
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE accounting (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
action TEXT NOT NULL,
section TEXT NOT NULL DEFAULT '',
chain TEXT NOT NULL DEFAULT '',
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,
mark TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE tc_devices (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
interface TEXT NOT NULL,
in_bandwidth TEXT NOT NULL DEFAULT '',
out_bandwidth TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE tc_classes (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
interface TEXT NOT NULL,
mark INT NOT NULL DEFAULT 0,
rate TEXT NOT NULL DEFAULT '',
ceil TEXT NOT NULL DEFAULT '',
priority INT NOT NULL DEFAULT 0,
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE tc_filters (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
class 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,
tos TEXT NOT NULL DEFAULT '',
length INT NOT NULL DEFAULT 0,
priority INT NOT NULL DEFAULT 0,
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE tc_interfaces (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
interface TEXT NOT NULL,
type TEXT NOT NULL DEFAULT '',
in_bandwidth TEXT NOT NULL DEFAULT '',
out_bandwidth TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT ''
);
CREATE TABLE tc_priorities (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
band INT NOT NULL,
proto TEXT NOT NULL DEFAULT '',
dport JSONB NOT NULL DEFAULT '[]'::jsonb,
sport JSONB NOT NULL DEFAULT '[]'::jsonb,
address TEXT NOT NULL DEFAULT '',
interface TEXT NOT NULL DEFAULT '',
helper TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT ''
);
@@ -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 ''
);
+20
View File
@@ -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"`
}
+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"`
}
+96
View File
@@ -0,0 +1,96 @@
package model
// Traffic-control tier: mangle, accounting, and tc_* sections (per-device).
type MangleRule struct {
ID int64 `json:"id"`
Device string `json:"device"`
Action string `json:"action"`
Chain string `json:"chain,omitempty"`
MarkValue string `json:"mark_value,omitempty"`
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"`
User string `json:"user,omitempty"`
Mark string `json:"mark,omitempty"`
Length string `json:"length,omitempty"`
TOS string `json:"tos,omitempty"`
Helper string `json:"helper,omitempty"`
Probability *float64 `json:"probability,omitempty"`
Comment string `json:"comment,omitempty"`
}
type AccountingRule struct {
ID int64 `json:"id"`
Device string `json:"device"`
Action string `json:"action"`
Section string `json:"section,omitempty"`
Chain string `json:"chain,omitempty"`
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"`
Mark string `json:"mark,omitempty"`
Comment string `json:"comment,omitempty"`
}
type TCDevice struct {
ID int64 `json:"id"`
Device string `json:"device"`
Interface string `json:"interface"`
InBandwidth string `json:"in_bandwidth,omitempty"`
OutBandwidth string `json:"out_bandwidth,omitempty"`
Comment string `json:"comment,omitempty"`
}
type TCClass struct {
ID int64 `json:"id"`
Device string `json:"device"`
Interface string `json:"interface"`
Mark int `json:"mark,omitempty"`
Rate string `json:"rate,omitempty"`
Ceil string `json:"ceil,omitempty"`
Priority int `json:"priority,omitempty"`
Comment string `json:"comment,omitempty"`
}
type TCFilter struct {
ID int64 `json:"id"`
Device string `json:"device"`
Class string `json:"class"`
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"`
TOS string `json:"tos,omitempty"`
Length int `json:"length,omitempty"`
Priority int `json:"priority,omitempty"`
Comment string `json:"comment,omitempty"`
}
type TCInterface struct {
ID int64 `json:"id"`
Device string `json:"device"`
Interface string `json:"interface"`
Type string `json:"type,omitempty"`
InBandwidth string `json:"in_bandwidth,omitempty"`
OutBandwidth string `json:"out_bandwidth,omitempty"`
Comment string `json:"comment,omitempty"`
}
type TCPriority struct {
ID int64 `json:"id"`
Device string `json:"device"`
Band int `json:"band"`
Proto string `json:"proto,omitempty"`
DPort []string `json:"dport,omitempty"`
SPort []string `json:"sport,omitempty"`
Address string `json:"address,omitempty"`
Interface string `json:"interface,omitempty"`
Helper string `json:"helper,omitempty"`
Comment string `json:"comment,omitempty"`
}
+79
View File
@@ -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")))
}
+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))
}
+3
View File
@@ -62,6 +62,9 @@ func (s *Server) mountResources(r chi.Router) {
s.mountNAT(r)
s.mountLongtail(r)
s.mountPerDevice(r)
s.mountPerDeviceL2(r)
s.mountTraffic(r)
s.mountGlobal2(r)
}
// respondOne writes a single resource, mapping ErrNotFound to 404.
+265
View File
@@ -0,0 +1,265 @@
package server
import (
"net/http"
"github.com/go-chi/chi/v5"
"git.unkin.net/unkin/tomswallapi/internal/model"
)
// mountTraffic wires the traffic-control tier: mangle, accounting, tc_*.
func (s *Server) mountTraffic(r chi.Router) {
idCRUD(r, "/mangle", s.listMangle, s.createMangle, s.getMangle, s.deleteMangle)
idCRUD(r, "/accounting", s.listAccounting, s.createAccounting, s.getAccounting, s.deleteAccounting)
idCRUD(r, "/tc-devices", s.listTCDevices, s.createTCDevice, s.getTCDevice, s.deleteTCDevice)
idCRUD(r, "/tc-classes", s.listTCClasses, s.createTCClass, s.getTCClass, s.deleteTCClass)
idCRUD(r, "/tc-filters", s.listTCFilters, s.createTCFilter, s.getTCFilter, s.deleteTCFilter)
idCRUD(r, "/tc-interfaces", s.listTCInterfaces, s.createTCInterface, s.getTCInterface, s.deleteTCInterface)
idCRUD(r, "/tc-priorities", s.listTCPriorities, s.createTCPriority, s.getTCPriority, s.deleteTCPriority)
}
func (s *Server) listMangle(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListMangle(r.Context())
respondList(w, list, err)
}
func (s *Server) createMangle(w http.ResponseWriter, r *http.Request) {
var v model.MangleRule
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.CreateMangle(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getMangle(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetMangle(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteMangle(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteMangle(r.Context(), id))
}
func (s *Server) listAccounting(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListAccounting(r.Context())
respondList(w, list, err)
}
func (s *Server) createAccounting(w http.ResponseWriter, r *http.Request) {
var v model.AccountingRule
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.CreateAccounting(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getAccounting(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetAccounting(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteAccounting(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteAccounting(r.Context(), id))
}
func (s *Server) listTCDevices(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListTCDevices(r.Context())
respondList(w, list, err)
}
func (s *Server) createTCDevice(w http.ResponseWriter, r *http.Request) {
var v model.TCDevice
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Interface == "" {
writeError(w, http.StatusBadRequest, "device and interface are required")
return
}
id, err := s.store.CreateTCDevice(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getTCDevice(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetTCDevice(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteTCDevice(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteTCDevice(r.Context(), id))
}
func (s *Server) listTCClasses(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListTCClasses(r.Context())
respondList(w, list, err)
}
func (s *Server) createTCClass(w http.ResponseWriter, r *http.Request) {
var v model.TCClass
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Interface == "" {
writeError(w, http.StatusBadRequest, "device and interface are required")
return
}
id, err := s.store.CreateTCClass(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getTCClass(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetTCClass(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteTCClass(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteTCClass(r.Context(), id))
}
func (s *Server) listTCFilters(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListTCFilters(r.Context())
respondList(w, list, err)
}
func (s *Server) createTCFilter(w http.ResponseWriter, r *http.Request) {
var v model.TCFilter
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Class == "" {
writeError(w, http.StatusBadRequest, "device and class are required")
return
}
id, err := s.store.CreateTCFilter(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getTCFilter(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetTCFilter(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteTCFilter(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteTCFilter(r.Context(), id))
}
func (s *Server) listTCInterfaces(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListTCInterfaces(r.Context())
respondList(w, list, err)
}
func (s *Server) createTCInterface(w http.ResponseWriter, r *http.Request) {
var v model.TCInterface
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Interface == "" {
writeError(w, http.StatusBadRequest, "device and interface are required")
return
}
id, err := s.store.CreateTCInterface(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getTCInterface(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetTCInterface(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteTCInterface(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteTCInterface(r.Context(), id))
}
func (s *Server) listTCPriorities(w http.ResponseWriter, r *http.Request) {
list, err := s.store.ListTCPriorities(r.Context())
respondList(w, list, err)
}
func (s *Server) createTCPriority(w http.ResponseWriter, r *http.Request) {
var v model.TCPriority
if !decode(w, r, &v) {
return
}
if v.Device == "" || v.Band == 0 {
writeError(w, http.StatusBadRequest, "device and band are required")
return
}
id, err := s.store.CreateTCPriority(r.Context(), v)
if err == nil {
v.ID = id
}
respondCreated(w, v, err)
}
func (s *Server) getTCPriority(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
v, err := s.store.GetTCPriority(r.Context(), id)
respondOne(w, v, err)
}
func (s *Server) deleteTCPriority(w http.ResponseWriter, r *http.Request) {
id, ok := idParam(w, r)
if !ok {
return
}
respondDelete(w, s.store.DeleteTCPriority(r.Context(), id))
}
+118
View File
@@ -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)
}
+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)
}
+424
View File
@@ -0,0 +1,424 @@
package store
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"git.unkin.net/unkin/tomswallapi/internal/model"
)
// ---- Mangle ----------------------------------------------------------------
func (s *Store) ListMangle(ctx context.Context) ([]model.MangleRule, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, device, action, chain, mark_value, source, dest, proto, dport, sport,
"user", mark, length, tos, helper, probability, comment
FROM mangle ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.MangleRule
for rows.Next() {
m, err := scanMangle(rows)
if err != nil {
return nil, err
}
out = append(out, m)
}
return out, rows.Err()
}
func (s *Store) GetMangle(ctx context.Context, id int64) (model.MangleRule, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, device, action, chain, mark_value, source, dest, proto, dport, sport,
"user", mark, length, tos, helper, probability, comment
FROM mangle WHERE id = $1`, id)
if err != nil {
return model.MangleRule{}, err
}
defer rows.Close()
if !rows.Next() {
return model.MangleRule{}, ErrNotFound
}
return scanMangle(rows)
}
func scanMangle(rows pgx.Rows) (model.MangleRule, error) {
var m model.MangleRule
var dport, sport []byte
if err := rows.Scan(&m.ID, &m.Device, &m.Action, &m.Chain, &m.MarkValue, &m.Source, &m.Dest, &m.Proto,
&dport, &sport, &m.User, &m.Mark, &m.Length, &m.TOS, &m.Helper, &m.Probability, &m.Comment); err != nil {
return m, err
}
if err := unmarshalStrings(dport, &m.DPort); err != nil {
return m, err
}
return m, unmarshalStrings(sport, &m.SPort)
}
func (s *Store) CreateMangle(ctx context.Context, m model.MangleRule) (int64, error) {
dport, _ := jsonb(m.DPort)
sport, _ := jsonb(m.SPort)
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO mangle (device, action, chain, mark_value, source, dest, proto, dport, sport,
"user", mark, length, tos, helper, probability, comment)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) RETURNING id`,
m.Device, m.Action, m.Chain, m.MarkValue, m.Source, m.Dest, m.Proto, dport, sport,
m.User, m.Mark, m.Length, m.TOS, m.Helper, m.Probability, m.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteMangle(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM mangle WHERE id = $1`, id)
}
// ---- Accounting ------------------------------------------------------------
func (s *Store) ListAccounting(ctx context.Context) ([]model.AccountingRule, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, action, section, chain, source, dest, proto, dport, sport, mark, comment FROM accounting ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.AccountingRule
for rows.Next() {
a, err := scanAccounting(rows)
if err != nil {
return nil, err
}
out = append(out, a)
}
return out, rows.Err()
}
func (s *Store) GetAccounting(ctx context.Context, id int64) (model.AccountingRule, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, action, section, chain, source, dest, proto, dport, sport, mark, comment FROM accounting WHERE id = $1`, id)
if err != nil {
return model.AccountingRule{}, err
}
defer rows.Close()
if !rows.Next() {
return model.AccountingRule{}, ErrNotFound
}
return scanAccounting(rows)
}
func scanAccounting(rows pgx.Rows) (model.AccountingRule, error) {
var a model.AccountingRule
var dport, sport []byte
if err := rows.Scan(&a.ID, &a.Device, &a.Action, &a.Section, &a.Chain, &a.Source, &a.Dest, &a.Proto, &dport, &sport, &a.Mark, &a.Comment); err != nil {
return a, err
}
if err := unmarshalStrings(dport, &a.DPort); err != nil {
return a, err
}
return a, unmarshalStrings(sport, &a.SPort)
}
func (s *Store) CreateAccounting(ctx context.Context, a model.AccountingRule) (int64, error) {
dport, _ := jsonb(a.DPort)
sport, _ := jsonb(a.SPort)
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO accounting (device, action, section, chain, source, dest, proto, dport, sport, mark, comment)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING id`,
a.Device, a.Action, a.Section, a.Chain, a.Source, a.Dest, a.Proto, dport, sport, a.Mark, a.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteAccounting(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM accounting WHERE id = $1`, id)
}
// ---- TC devices ------------------------------------------------------------
func (s *Store) ListTCDevices(ctx context.Context) ([]model.TCDevice, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, interface, in_bandwidth, out_bandwidth, comment FROM tc_devices ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.TCDevice
for rows.Next() {
var t model.TCDevice
if err := rows.Scan(&t.ID, &t.Device, &t.Interface, &t.InBandwidth, &t.OutBandwidth, &t.Comment); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (s *Store) GetTCDevice(ctx context.Context, id int64) (model.TCDevice, error) {
var t model.TCDevice
err := s.pool.QueryRow(ctx,
`SELECT id, device, interface, in_bandwidth, out_bandwidth, comment FROM tc_devices WHERE id = $1`, id,
).Scan(&t.ID, &t.Device, &t.Interface, &t.InBandwidth, &t.OutBandwidth, &t.Comment)
if errors.Is(err, pgx.ErrNoRows) {
return t, ErrNotFound
}
return t, err
}
func (s *Store) CreateTCDevice(ctx context.Context, t model.TCDevice) (int64, error) {
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO tc_devices (device, interface, in_bandwidth, out_bandwidth, comment)
VALUES ($1,$2,$3,$4,$5) RETURNING id`,
t.Device, t.Interface, t.InBandwidth, t.OutBandwidth, t.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteTCDevice(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM tc_devices WHERE id = $1`, id)
}
// ---- TC classes ------------------------------------------------------------
func (s *Store) ListTCClasses(ctx context.Context) ([]model.TCClass, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, interface, mark, rate, ceil, priority, comment FROM tc_classes ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.TCClass
for rows.Next() {
var t model.TCClass
if err := rows.Scan(&t.ID, &t.Device, &t.Interface, &t.Mark, &t.Rate, &t.Ceil, &t.Priority, &t.Comment); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (s *Store) GetTCClass(ctx context.Context, id int64) (model.TCClass, error) {
var t model.TCClass
err := s.pool.QueryRow(ctx,
`SELECT id, device, interface, mark, rate, ceil, priority, comment FROM tc_classes WHERE id = $1`, id,
).Scan(&t.ID, &t.Device, &t.Interface, &t.Mark, &t.Rate, &t.Ceil, &t.Priority, &t.Comment)
if errors.Is(err, pgx.ErrNoRows) {
return t, ErrNotFound
}
return t, err
}
func (s *Store) CreateTCClass(ctx context.Context, t model.TCClass) (int64, error) {
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO tc_classes (device, interface, mark, rate, ceil, priority, comment)
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id`,
t.Device, t.Interface, t.Mark, t.Rate, t.Ceil, t.Priority, t.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteTCClass(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM tc_classes WHERE id = $1`, id)
}
// ---- TC filters ------------------------------------------------------------
func (s *Store) ListTCFilters(ctx context.Context) ([]model.TCFilter, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, class, source, dest, proto, dport, sport, tos, length, priority, comment FROM tc_filters ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.TCFilter
for rows.Next() {
f, err := scanTCFilter(rows)
if err != nil {
return nil, err
}
out = append(out, f)
}
return out, rows.Err()
}
func (s *Store) GetTCFilter(ctx context.Context, id int64) (model.TCFilter, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, class, source, dest, proto, dport, sport, tos, length, priority, comment FROM tc_filters WHERE id = $1`, id)
if err != nil {
return model.TCFilter{}, err
}
defer rows.Close()
if !rows.Next() {
return model.TCFilter{}, ErrNotFound
}
return scanTCFilter(rows)
}
func scanTCFilter(rows pgx.Rows) (model.TCFilter, error) {
var f model.TCFilter
var dport, sport []byte
if err := rows.Scan(&f.ID, &f.Device, &f.Class, &f.Source, &f.Dest, &f.Proto, &dport, &sport, &f.TOS, &f.Length, &f.Priority, &f.Comment); err != nil {
return f, err
}
if err := unmarshalStrings(dport, &f.DPort); err != nil {
return f, err
}
return f, unmarshalStrings(sport, &f.SPort)
}
func (s *Store) CreateTCFilter(ctx context.Context, f model.TCFilter) (int64, error) {
dport, _ := jsonb(f.DPort)
sport, _ := jsonb(f.SPort)
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO tc_filters (device, class, source, dest, proto, dport, sport, tos, length, priority, comment)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING id`,
f.Device, f.Class, f.Source, f.Dest, f.Proto, dport, sport, f.TOS, f.Length, f.Priority, f.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteTCFilter(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM tc_filters WHERE id = $1`, id)
}
// ---- TC interfaces ---------------------------------------------------------
func (s *Store) ListTCInterfaces(ctx context.Context) ([]model.TCInterface, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, interface, type, in_bandwidth, out_bandwidth, comment FROM tc_interfaces ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.TCInterface
for rows.Next() {
var t model.TCInterface
if err := rows.Scan(&t.ID, &t.Device, &t.Interface, &t.Type, &t.InBandwidth, &t.OutBandwidth, &t.Comment); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (s *Store) GetTCInterface(ctx context.Context, id int64) (model.TCInterface, error) {
var t model.TCInterface
err := s.pool.QueryRow(ctx,
`SELECT id, device, interface, type, in_bandwidth, out_bandwidth, comment FROM tc_interfaces WHERE id = $1`, id,
).Scan(&t.ID, &t.Device, &t.Interface, &t.Type, &t.InBandwidth, &t.OutBandwidth, &t.Comment)
if errors.Is(err, pgx.ErrNoRows) {
return t, ErrNotFound
}
return t, err
}
func (s *Store) CreateTCInterface(ctx context.Context, t model.TCInterface) (int64, error) {
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO tc_interfaces (device, interface, type, in_bandwidth, out_bandwidth, comment)
VALUES ($1,$2,$3,$4,$5,$6) RETURNING id`,
t.Device, t.Interface, t.Type, t.InBandwidth, t.OutBandwidth, t.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteTCInterface(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM tc_interfaces WHERE id = $1`, id)
}
// ---- TC priorities ---------------------------------------------------------
func (s *Store) ListTCPriorities(ctx context.Context) ([]model.TCPriority, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, band, proto, dport, sport, address, interface, helper, comment FROM tc_priorities ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []model.TCPriority
for rows.Next() {
p, err := scanTCPriority(rows)
if err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
func (s *Store) GetTCPriority(ctx context.Context, id int64) (model.TCPriority, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, device, band, proto, dport, sport, address, interface, helper, comment FROM tc_priorities WHERE id = $1`, id)
if err != nil {
return model.TCPriority{}, err
}
defer rows.Close()
if !rows.Next() {
return model.TCPriority{}, ErrNotFound
}
return scanTCPriority(rows)
}
func scanTCPriority(rows pgx.Rows) (model.TCPriority, error) {
var p model.TCPriority
var dport, sport []byte
if err := rows.Scan(&p.ID, &p.Device, &p.Band, &p.Proto, &dport, &sport, &p.Address, &p.Interface, &p.Helper, &p.Comment); err != nil {
return p, err
}
if err := unmarshalStrings(dport, &p.DPort); err != nil {
return p, err
}
return p, unmarshalStrings(sport, &p.SPort)
}
func (s *Store) CreateTCPriority(ctx context.Context, p model.TCPriority) (int64, error) {
dport, _ := jsonb(p.DPort)
sport, _ := jsonb(p.SPort)
var id int64
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
if err := tx.QueryRow(ctx, `
INSERT INTO tc_priorities (device, band, proto, dport, sport, address, interface, helper, comment)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id`,
p.Device, p.Band, p.Proto, dport, sport, p.Address, p.Interface, p.Helper, p.Comment).Scan(&id); err != nil {
return err
}
return bump(ctx, tx)
})
return id, err
}
func (s *Store) DeleteTCPriority(ctx context.Context, id int64) error {
return s.deleteOne(ctx, `DELETE FROM tc_priorities WHERE id = $1`, id)
}