Compare commits

...

11 Commits

Author SHA1 Message Date
benvin 25d0f178f9 Merge pull request 'ci: add buildkit_config CA trust for artifactapi push' (#24) from benvin/buildx-ca-config into main
Reviewed-on: #24
2026-08-15 18:46:23 +10:00
unkin-agent 7d47bf2148 ci: add buildkit_config CA trust for artifactapi push
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
2026-08-15 18:31:51 +10:00
benvin 3d57692cf3 Merge pull request 'ci: use CA-baked plugin-docker-buildx image for artifactapi push' (#23) from benvin/buildx-ca-plugin-image into main
Reviewed-on: #23
2026-08-15 18:20:13 +10:00
unkin-agent a922f2f958 ci: use CA-baked plugin-docker-buildx image for artifactapi push
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
2026-08-15 18:04:32 +10:00
benvin 6b36a33f2f Merge pull request 'ci: push images to artifactapi registry instead of gitea' (#22) from benvin/push-artifactapi into main
Reviewed-on: #22
2026-07-30 20:56:03 +10:00
unkinben df2a4ac3b8 ci: push images to artifactapi registry instead of gitea
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
Hard switch of the docker push target from the Gitea registry to the
artifactapi local docker registry (docker-internal); the Gitea VM and its
registry are being retired. Drops the droneci/DRONECI_PASSWORD creds since
artifactapi accepts unauthenticated in-cluster pushes.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-30 00:34:59 +10:00
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
14 changed files with 1403 additions and 8 deletions
+2 -2
View File
@@ -3,9 +3,9 @@ when:
steps:
- name: docker-build
image: woodpeckerci/plugin-docker-buildx
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest
settings:
repo: git.unkin.net/unkin/tomswallapi
repo: artifactapi.k8s.syd1.au.unkin.net/docker-internal/tomswallapi
dry_run: true
backend_options:
kubernetes:
+6 -6
View File
@@ -4,15 +4,15 @@ when:
steps:
- name: docker-tomswallapi
image: woodpeckerci/plugin-docker-buildx
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/plugin-docker-buildx:latest
settings:
registry: git.unkin.net
repo: git.unkin.net/unkin/tomswallapi
registry: artifactapi.k8s.syd1.au.unkin.net
repo: artifactapi.k8s.syd1.au.unkin.net/docker-internal/tomswallapi
buildkit_config: |
[registry."artifactapi.k8s.syd1.au.unkin.net"]
ca = ["/etc/docker/certs.d/artifactapi.k8s.syd1.au.unkin.net/ca.crt"]
build_args:
VERSION: ${CI_COMMIT_TAG}
username: droneci
password:
from_secret: DRONECI_PASSWORD
tags:
- ${CI_COMMIT_TAG}
- latest
+57
View File
@@ -45,6 +45,17 @@ type Input struct {
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.
@@ -72,6 +83,17 @@ type RenderedConfig struct {
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
@@ -220,6 +242,8 @@ 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
}
@@ -480,5 +504,38 @@ func Compile(ctx context.Context, s *store.Store, device string) (*RenderedConfi
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,
})
}
}
+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,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"`
}
+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")))
}
+2
View File
@@ -63,6 +63,8 @@ func (s *Server) mountResources(r chi.Router) {
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)
}
+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)
}