Compare commits
18 Commits
b350c8d198
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 73ddcf651e | |||
| c46b4ab5c5 | |||
| 4693b7093c | |||
| 6b600f8c8d | |||
| d54325c685 | |||
| d9d192757b | |||
| 22e0d07227 | |||
| 721f4c1af3 | |||
| bd97b13ff7 | |||
| af7117faae | |||
| 2d1b317e0a | |||
| ef9a71bf0a | |||
| 86876687a7 | |||
| 92213090fe | |||
| dc3b2ecdb9 | |||
| 335c61383a | |||
| a5957d0e98 | |||
| 9dbeb62414 |
+8
-11
@@ -54,18 +54,15 @@ func main() {
|
||||
slog.Warn("TOMSWALLAPI_AGENT_TOKEN is not set; agent config endpoint is disabled")
|
||||
}
|
||||
|
||||
// Start the central ASN expander when a key is configured. Without one, asn
|
||||
// address groups simply stay unexpanded (their sets render empty and inert).
|
||||
if cfg.IPLocateAPIKey != "" {
|
||||
refresher := &asnexpand.Refresher{
|
||||
Store: store.New(db.Pool),
|
||||
Expander: asnexpand.NewIPLocate(cfg.IPLocateAPIKey),
|
||||
}
|
||||
go refresher.Run(ctx)
|
||||
slog.Info("started ASN expander")
|
||||
} else {
|
||||
slog.Warn("TOMSWALLAPI_IPLOCATE_API_KEY is not set; asn address groups will not be expanded")
|
||||
// Start the central ASN expander, which reads the iplocate ip-to-asn database
|
||||
// (proxied via artifactapi) and expands asn address groups into prefixes.
|
||||
expander := asnexpand.NewIPLocateDB(cfg.IPLocateDBURL)
|
||||
refresher := &asnexpand.Refresher{
|
||||
Store: store.New(db.Pool),
|
||||
Expander: expander,
|
||||
}
|
||||
go refresher.Run(ctx)
|
||||
slog.Info("started ASN expander", "source", "iplocate-db", "url", expander.URL)
|
||||
|
||||
srv := server.New(server.Options{
|
||||
DB: db,
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package asnexpand
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IPLocate expands ASNs via the iplocate.io IP-intelligence API.
|
||||
//
|
||||
// NOTE: iplocate's documented ASN data type is returned as part of an IP lookup
|
||||
// (it carries the single `route` for that IP), so the exact ASN->prefixes
|
||||
// endpoint depends on the account/plan. This client is deliberately tolerant of
|
||||
// response shape and endpoint-configurable so it can be pointed at the correct
|
||||
// endpoint once the API key and plan are confirmed. It expects a JSON body
|
||||
// containing a list of prefixes under any of: "prefixes", "routes", "cidrs".
|
||||
type IPLocate struct {
|
||||
// BaseURL is the API root, default https://iplocate.io/api. The ASN path is
|
||||
// BaseURL + "/asn/" + asn.
|
||||
BaseURL string
|
||||
// APIKey is sent as the ?apikey= query parameter (iplocate's scheme).
|
||||
APIKey string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
// NewIPLocate builds a client with sane defaults.
|
||||
func NewIPLocate(apiKey string) *IPLocate {
|
||||
return &IPLocate{
|
||||
BaseURL: "https://iplocate.io/api",
|
||||
APIKey: apiKey,
|
||||
HTTP: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// asnResponse tolerates the several field names an ASN-prefix payload might use.
|
||||
type asnResponse struct {
|
||||
Prefixes []string `json:"prefixes"`
|
||||
Routes []string `json:"routes"`
|
||||
CIDRs []string `json:"cidrs"`
|
||||
}
|
||||
|
||||
func (r asnResponse) list() []string {
|
||||
switch {
|
||||
case len(r.Prefixes) > 0:
|
||||
return r.Prefixes
|
||||
case len(r.Routes) > 0:
|
||||
return r.Routes
|
||||
default:
|
||||
return r.CIDRs
|
||||
}
|
||||
}
|
||||
|
||||
// Prefixes implements Expander.
|
||||
func (c *IPLocate) Prefixes(ctx context.Context, asn string) ([]string, error) {
|
||||
if c.APIKey == "" {
|
||||
return nil, fmt.Errorf("iplocate: no API key configured")
|
||||
}
|
||||
asn = strings.TrimPrefix(strings.ToUpper(strings.TrimSpace(asn)), "AS")
|
||||
|
||||
u, err := url.Parse(fmt.Sprintf("%s/asn/AS%s", strings.TrimRight(c.BaseURL, "/"), asn))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("apikey", c.APIKey)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("iplocate: AS%s: status %d: %s", asn, resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
var parsed asnResponse
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return nil, fmt.Errorf("iplocate: AS%s: decoding response: %w", asn, err)
|
||||
}
|
||||
return parsed.list(), nil
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package asnexpand
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultIPLocateDBURL is the iplocate ip-to-asn CSV database, proxied through
|
||||
// the artifactapi github remote. Remote proxies are served at
|
||||
// /api/v1/remote/<name>/<path>. The files are Git-LFS, so the /raw/ path
|
||||
// redirects to the LFS media host — artifactapi follows that redirect.
|
||||
const DefaultIPLocateDBURL = "https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/github/iplocate/ip-address-databases/raw/main/ip-to-asn/ip-to-asn.csv.zip"
|
||||
|
||||
// IPLocateDB expands ASNs by reading iplocate's ip-to-asn CSV (CIDR,asn,...). It
|
||||
// downloads and indexes the whole database once, then serves every ASN group
|
||||
// from the in-memory index, rebuilding when the index goes stale. This is far
|
||||
// cheaper than a per-ASN API call and needs no API key.
|
||||
type IPLocateDB struct {
|
||||
URL string
|
||||
TTL time.Duration
|
||||
HTTP *http.Client
|
||||
|
||||
mu sync.Mutex
|
||||
index map[string][]string
|
||||
loadedAt time.Time
|
||||
}
|
||||
|
||||
// NewIPLocateDB builds a DB-backed expander. An empty url uses the default.
|
||||
func NewIPLocateDB(url string) *IPLocateDB {
|
||||
if url == "" {
|
||||
url = DefaultIPLocateDBURL
|
||||
}
|
||||
return &IPLocateDB{
|
||||
URL: url,
|
||||
TTL: 24 * time.Hour,
|
||||
HTTP: &http.Client{Timeout: 5 * time.Minute},
|
||||
}
|
||||
}
|
||||
|
||||
// Prefixes implements Expander: it returns the CIDRs announced by the ASN.
|
||||
func (d *IPLocateDB) Prefixes(ctx context.Context, asn string) ([]string, error) {
|
||||
if err := d.ensureIndex(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return d.index[normalizeASN(asn)], nil
|
||||
}
|
||||
|
||||
// ensureIndex (re)builds the index when it is missing or stale. On a refresh
|
||||
// failure it keeps the prior index (fail-safe) and only errors when there is
|
||||
// nothing cached yet.
|
||||
func (d *IPLocateDB) ensureIndex(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
fresh := d.index != nil && time.Since(d.loadedAt) < d.TTL
|
||||
hadIndex := d.index != nil
|
||||
d.mu.Unlock()
|
||||
if fresh {
|
||||
return nil
|
||||
}
|
||||
|
||||
idx, err := d.download(ctx)
|
||||
if err != nil {
|
||||
if hadIndex {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.index = idx
|
||||
d.loadedAt = time.Now()
|
||||
d.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *IPLocateDB) download(ctx context.Context) (map[string][]string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, d.URL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := d.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("iplocate db: status %d", resp.StatusCode)
|
||||
}
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return indexFromZip(data)
|
||||
}
|
||||
|
||||
// indexFromZip finds the single .csv in the archive and indexes it.
|
||||
func indexFromZip(data []byte) (map[string][]string, error) {
|
||||
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iplocate db: opening zip: %w", err)
|
||||
}
|
||||
for _, f := range zr.File {
|
||||
if !strings.HasSuffix(f.Name, ".csv") {
|
||||
continue
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
return buildIndex(rc)
|
||||
}
|
||||
return nil, fmt.Errorf("iplocate db: no .csv entry in archive")
|
||||
}
|
||||
|
||||
// buildIndex reads the ip-to-asn CSV (header: network,asn,country_code,name,org,
|
||||
// domain) and returns a map of ASN -> announced CIDRs.
|
||||
func buildIndex(r io.Reader) (map[string][]string, error) {
|
||||
cr := csv.NewReader(bufio.NewReaderSize(r, 1<<20))
|
||||
cr.ReuseRecord = true
|
||||
cr.FieldsPerRecord = -1 // tolerate quoted commas (org names)
|
||||
|
||||
header, err := cr.Read()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iplocate db: reading header: %w", err)
|
||||
}
|
||||
netIdx := columnIndex(header, "network")
|
||||
asnIdx := columnIndex(header, "asn")
|
||||
if netIdx < 0 || asnIdx < 0 {
|
||||
return nil, fmt.Errorf("iplocate db: header missing network/asn columns: %v", header)
|
||||
}
|
||||
|
||||
index := map[string][]string{}
|
||||
for {
|
||||
rec, err := cr.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iplocate db: reading row: %w", err)
|
||||
}
|
||||
if netIdx >= len(rec) || asnIdx >= len(rec) {
|
||||
continue
|
||||
}
|
||||
// Records are reused across Read calls, so clone the retained fields.
|
||||
asn := strings.Clone(rec[asnIdx])
|
||||
index[asn] = append(index[asn], strings.Clone(rec[netIdx]))
|
||||
}
|
||||
return index, nil
|
||||
}
|
||||
|
||||
func columnIndex(header []string, name string) int {
|
||||
for i, h := range header {
|
||||
if strings.EqualFold(strings.TrimSpace(h), name) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// normalizeASN strips an optional AS prefix so "AS13335" and "13335" both match
|
||||
// the bare numbers in the database.
|
||||
func normalizeASN(asn string) string {
|
||||
return strings.TrimPrefix(strings.ToUpper(strings.TrimSpace(asn)), "AS")
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package asnexpand
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const sampleCSV = `network,asn,country_code,name,org,domain
|
||||
1.0.0.0/24,13335,US,CLOUDFLARENET,"Cloudflare, Inc.",cloudflare.com
|
||||
1.1.1.0/24,13335,US,CLOUDFLARENET,"Cloudflare, Inc.",cloudflare.com
|
||||
1.0.4.0/24,38803,AU,GTELECOM-AS-AP,Gtelecom Pty Ltd,gtelecom.com.au
|
||||
104.16.0.0/13,13335,US,CLOUDFLARENET,"Cloudflare, Inc.",cloudflare.com
|
||||
`
|
||||
|
||||
func TestBuildIndex(t *testing.T) {
|
||||
idx, err := buildIndex(strings.NewReader(sampleCSV))
|
||||
if err != nil {
|
||||
t.Fatalf("buildIndex: %v", err)
|
||||
}
|
||||
cf := idx["13335"]
|
||||
sort.Strings(cf)
|
||||
want := []string{"1.0.0.0/24", "1.1.1.0/24", "104.16.0.0/13"}
|
||||
sort.Strings(want)
|
||||
if strings.Join(cf, ",") != strings.Join(want, ",") {
|
||||
t.Errorf("AS13335 prefixes = %v, want %v", cf, want)
|
||||
}
|
||||
if got := idx["38803"]; len(got) != 1 || got[0] != "1.0.4.0/24" {
|
||||
t.Errorf("AS38803 prefixes = %v", got)
|
||||
}
|
||||
// The quoted org field with an embedded comma must not shift columns.
|
||||
if len(idx) != 2 {
|
||||
t.Errorf("expected 2 distinct ASNs, got %d", len(idx))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildIndexMissingColumns(t *testing.T) {
|
||||
if _, err := buildIndex(strings.NewReader("foo,bar\n1,2\n")); err == nil {
|
||||
t.Fatal("expected error when network/asn columns are absent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexFromZip(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
w, _ := zw.Create("ip-to-asn-20260721.csv")
|
||||
_, _ = w.Write([]byte(sampleCSV))
|
||||
if err := zw.Close(); err != nil {
|
||||
t.Fatalf("zip close: %v", err)
|
||||
}
|
||||
|
||||
idx, err := indexFromZip(buf.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("indexFromZip: %v", err)
|
||||
}
|
||||
if len(idx["13335"]) != 3 {
|
||||
t.Errorf("expected 3 cloudflare prefixes from zip, got %d", len(idx["13335"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexFromZipNoCSV(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
w, _ := zw.Create("readme.txt")
|
||||
_, _ = w.Write([]byte("no csv here"))
|
||||
_ = zw.Close()
|
||||
if _, err := indexFromZip(buf.Bytes()); err == nil {
|
||||
t.Fatal("expected error when archive has no .csv")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeASN(t *testing.T) {
|
||||
for in, want := range map[string]string{
|
||||
"13335": "13335",
|
||||
"AS13335": "13335",
|
||||
"as13335": "13335",
|
||||
" 13335 ": "13335",
|
||||
} {
|
||||
if got := normalizeASN(in); got != want {
|
||||
t.Errorf("normalizeASN(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+247
-20
@@ -11,6 +11,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
@@ -21,30 +22,106 @@ import (
|
||||
// Input is the fully-resolved model needed to render one device. Keeping Render
|
||||
// pure (no store access) makes it unit-testable without a database.
|
||||
type Input struct {
|
||||
Generation int64
|
||||
Settings model.Settings
|
||||
Device model.Device
|
||||
Fabric *model.Fabric
|
||||
Zones map[string]model.Zone
|
||||
Groups map[string]model.AddressGroup
|
||||
PortGroups map[string]model.PortGroup
|
||||
Rules []model.Rule
|
||||
Policies []model.Policy
|
||||
Bindings []model.Binding
|
||||
Generation int64
|
||||
Settings model.Settings
|
||||
Device model.Device
|
||||
Fabric *model.Fabric
|
||||
Zones map[string]model.Zone
|
||||
Groups map[string]model.AddressGroup
|
||||
PortGroups map[string]model.PortGroup
|
||||
Rules []model.Rule
|
||||
Policies []model.Policy
|
||||
Bindings []model.Binding
|
||||
SNAT []model.SNATRule
|
||||
Netmap []model.NetmapRule
|
||||
NAT []model.NATRule
|
||||
Hosts []model.Host
|
||||
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.
|
||||
type RenderedConfig struct {
|
||||
Generation int64 `yaml:"generation" json:"generation"`
|
||||
Device string `yaml:"device" json:"device"`
|
||||
Class model.DeviceClass `yaml:"class" json:"class"`
|
||||
Enforcing bool `yaml:"enforcing" json:"enforcing"`
|
||||
Settings RenderedSettings `yaml:"settings" json:"settings"`
|
||||
Resolver []string `yaml:"resolver,omitempty" json:"resolver,omitempty"`
|
||||
Bindings map[string][]string `yaml:"bindings,omitempty" json:"bindings,omitempty"` // zone -> interfaces
|
||||
Sets []RenderedSet `yaml:"sets,omitempty" json:"sets,omitempty"`
|
||||
Rules []RenderedRule `yaml:"rules,omitempty" json:"rules,omitempty"`
|
||||
Policies []model.Policy `yaml:"policies,omitempty" json:"policies,omitempty"`
|
||||
Generation int64 `yaml:"generation" json:"generation"`
|
||||
Device string `yaml:"device" json:"device"`
|
||||
Class model.DeviceClass `yaml:"class" json:"class"`
|
||||
Enforcing bool `yaml:"enforcing" json:"enforcing"`
|
||||
Settings RenderedSettings `yaml:"settings" json:"settings"`
|
||||
Resolver []string `yaml:"resolver,omitempty" json:"resolver,omitempty"`
|
||||
Bindings map[string][]string `yaml:"bindings,omitempty" json:"bindings,omitempty"` // zone -> interfaces
|
||||
Sets []RenderedSet `yaml:"sets,omitempty" json:"sets,omitempty"`
|
||||
Rules []RenderedRule `yaml:"rules,omitempty" json:"rules,omitempty"`
|
||||
Policies []model.Policy `yaml:"policies,omitempty" json:"policies,omitempty"`
|
||||
SNAT []RenderedSNAT `yaml:"snat,omitempty" json:"snat,omitempty"`
|
||||
Netmap []RenderedNetmap `yaml:"netmap,omitempty" json:"netmap,omitempty"`
|
||||
NAT []RenderedNAT `yaml:"nat,omitempty" json:"nat,omitempty"`
|
||||
Hosts []RenderedHost `yaml:"hosts,omitempty" json:"hosts,omitempty"`
|
||||
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
|
||||
// (or SNATed to Address) as they leave via the resolved egress interfaces.
|
||||
type RenderedSNAT struct {
|
||||
Action string `yaml:"action" json:"action"`
|
||||
Source []string `yaml:"source,omitempty" json:"source,omitempty"` // source CIDRs
|
||||
Egress []string `yaml:"egress" json:"egress"` // egress interface names
|
||||
Address string `yaml:"address,omitempty" json:"address,omitempty"`
|
||||
Probability *float64 `yaml:"probability,omitempty" json:"probability,omitempty"`
|
||||
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// RenderedNetmap is a resolved network-to-network mapping on one interface.
|
||||
type RenderedNetmap struct {
|
||||
Type string `yaml:"type" json:"type"`
|
||||
FromNet string `yaml:"from_net" json:"from_net"`
|
||||
ToNet string `yaml:"to_net" json:"to_net"`
|
||||
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
|
||||
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// RenderedNAT is a resolved one-to-one static NAT on one interface.
|
||||
type RenderedNAT struct {
|
||||
External string `yaml:"external" json:"external"`
|
||||
Internal string `yaml:"internal" json:"internal"`
|
||||
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
|
||||
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// RenderedSettings is the effective settings after per-device overrides.
|
||||
@@ -131,6 +208,10 @@ func Render(in Input) (*RenderedConfig, error) {
|
||||
|
||||
usedSets := map[string]model.AddressGroup{}
|
||||
|
||||
// Every enforcing device carries every applicable rule: the interface-agnostic
|
||||
// address-matched form is correct under ECMP precisely because it does not
|
||||
// depend on which device is on the path (over-approximation is safe). Reported
|
||||
// FIBs are stored for observability/validation, not to limit rules.
|
||||
if out.Enforcing {
|
||||
for _, rule := range in.Rules {
|
||||
rr, err := renderRule(in, rule, usedSets)
|
||||
@@ -151,9 +232,83 @@ func Render(in Input) (*RenderedConfig, error) {
|
||||
for _, n := range names {
|
||||
out.Sets = append(out.Sets, renderSet(usedSets[n]))
|
||||
}
|
||||
|
||||
// NAT tier: resolve the global NAT intents against this device's bindings.
|
||||
// These are binding-scoped, independent of the forward-rule enforce flag.
|
||||
out.SNAT = renderSNATRules(in, out.Bindings)
|
||||
out.Netmap = renderNetmapRules(in, out.Bindings)
|
||||
out.NAT = renderNATRules(in)
|
||||
|
||||
// Per-device long-tail sections owned by this device.
|
||||
renderPerDevice(in, out)
|
||||
renderPerDeviceL2(in, out)
|
||||
renderTraffic(in, out)
|
||||
renderGlobal2(in, out)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// renderSNATRules resolves masquerade/SNAT intents that egress via this device.
|
||||
// A SNAT lands here only if the device binds the egress zone (and, when the
|
||||
// source is a zone, that zone too) — which auto-scopes masquerade to edges.
|
||||
func renderSNATRules(in Input, bindings map[string][]string) []RenderedSNAT {
|
||||
var out []RenderedSNAT
|
||||
for _, s := range in.SNAT {
|
||||
egress := bindings[s.Egress]
|
||||
if len(egress) == 0 {
|
||||
continue // device is not an egress for this SNAT
|
||||
}
|
||||
var source []string
|
||||
if z, ok := in.Zones[s.Source]; ok {
|
||||
if _, bound := bindings[s.Source]; !bound {
|
||||
continue // device does not attach the source zone
|
||||
}
|
||||
source = z.Subnets
|
||||
} else {
|
||||
source = []string{s.Source} // literal CIDR
|
||||
}
|
||||
out = append(out, RenderedSNAT{
|
||||
Action: s.Action, Source: source, Egress: egress,
|
||||
Address: s.Address, Probability: s.Probability, Comment: s.Comment,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// renderNetmapRules resolves netmaps anchored (device:zone or device:interface)
|
||||
// at this device.
|
||||
func renderNetmapRules(in Input, bindings map[string][]string) []RenderedNetmap {
|
||||
var out []RenderedNetmap
|
||||
for _, n := range in.Netmap {
|
||||
dev, sel, ok := strings.Cut(n.Anchor, ":")
|
||||
if !ok || dev != in.Device.Name {
|
||||
continue
|
||||
}
|
||||
iface := sel
|
||||
if ifaces, bound := bindings[sel]; bound && len(ifaces) > 0 {
|
||||
iface = ifaces[0] // anchor named a zone: use its bound interface
|
||||
}
|
||||
out = append(out, RenderedNetmap{
|
||||
Type: n.Type, FromNet: n.FromNet, ToNet: n.ToNet, Interface: iface, Comment: n.Comment,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// renderNATRules resolves 1:1 static NATs bound to this device.
|
||||
func renderNATRules(in Input) []RenderedNAT {
|
||||
var out []RenderedNAT
|
||||
for _, n := range in.NAT {
|
||||
if n.Device != in.Device.Name {
|
||||
continue
|
||||
}
|
||||
out = append(out, RenderedNAT{
|
||||
External: n.External, Internal: n.Internal, Interface: n.Interface, Comment: n.Comment,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func renderSettings(in Input) RenderedSettings {
|
||||
s := RenderedSettings{
|
||||
AddressFamily: in.Settings.AddressFamily,
|
||||
@@ -310,5 +465,77 @@ func Compile(ctx context.Context, s *store.Store, device string) (*RenderedConfi
|
||||
if in.Bindings, err = s.ListBindings(ctx, device); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.SNAT, err = s.ListSNAT(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.Netmap, err = s.ListNetmap(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.NAT, err = s.ListNAT(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.Hosts, err = s.ListHosts(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.Providers, err = s.ListProviders(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.Routes, err = s.ListRoutes(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -116,6 +116,86 @@ func TestRenderUnknownGroupIsError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportedFIBDoesNotLimitRules(t *testing.T) {
|
||||
// A router with a narrow FIB must still carry every applicable rule: reported
|
||||
// reachability is observability data, not a rule filter (over-approximation is
|
||||
// safe and intended under ECMP).
|
||||
in := Input{
|
||||
Fabric: &model.Fabric{Name: "core", EnforceOnRouters: true},
|
||||
Device: model.Device{Name: "rt1", Class: model.ClassRouter, Fabric: "core",
|
||||
ReachablePrefixes: []string{"192.168.0.0/16"}},
|
||||
Zones: map[string]model.Zone{
|
||||
"zone-a": {Name: "zone-a", Subnets: []string{"10.1.0.0/24"}},
|
||||
"zone-b": {Name: "zone-b", Subnets: []string{"10.4.0.0/24"}},
|
||||
},
|
||||
Rules: []model.Rule{
|
||||
{ID: 1, Action: "accept", Source: []string{"zone-a"}, Dest: []string{"zone-b"}, Proto: "tcp", Ports: []string{"22"}},
|
||||
},
|
||||
}
|
||||
cfg, err := Render(in)
|
||||
if err != nil {
|
||||
t.Fatalf("Render: %v", err)
|
||||
}
|
||||
if len(cfg.Rules) != 1 {
|
||||
t.Errorf("reported FIB must not limit rules, got %d", len(cfg.Rules))
|
||||
}
|
||||
}
|
||||
|
||||
func natInput() Input {
|
||||
return Input{
|
||||
Zones: map[string]model.Zone{
|
||||
"loc": {Name: "loc", Subnets: []string{"10.1.0.0/24"}},
|
||||
"net": {Name: "net"},
|
||||
},
|
||||
SNAT: []model.SNATRule{{ID: 1, Action: "masquerade", Source: "loc", Egress: "net"}},
|
||||
Netmap: []model.NetmapRule{{ID: 1, Type: "dnat", FromNet: "10.0.0.0/24", ToNet: "192.168.1.0/24", Anchor: "fw-a:net"}},
|
||||
NAT: []model.NATRule{{ID: 1, Device: "fw-a", External: "203.0.113.10", Internal: "10.1.0.10", Interface: "eth0"}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderNATScopesToBindings(t *testing.T) {
|
||||
// fw-a binds both loc and net (an edge) → masquerade + its netmap + its nat.
|
||||
edge := natInput()
|
||||
edge.Device = model.Device{Name: "fw-a", Class: model.ClassFirewall}
|
||||
edge.Bindings = []model.Binding{
|
||||
{Device: "fw-a", Zone: "loc", Interfaces: []string{"eth1"}},
|
||||
{Device: "fw-a", Zone: "net", Interfaces: []string{"eth0"}},
|
||||
}
|
||||
cfg, err := Render(edge)
|
||||
if err != nil {
|
||||
t.Fatalf("Render: %v", err)
|
||||
}
|
||||
if len(cfg.SNAT) != 1 || cfg.SNAT[0].Action != "masquerade" ||
|
||||
len(cfg.SNAT[0].Source) != 1 || cfg.SNAT[0].Source[0] != "10.1.0.0/24" ||
|
||||
len(cfg.SNAT[0].Egress) != 1 || cfg.SNAT[0].Egress[0] != "eth0" {
|
||||
t.Errorf("edge masquerade not rendered correctly: %+v", cfg.SNAT)
|
||||
}
|
||||
if len(cfg.Netmap) != 1 || cfg.Netmap[0].Interface != "eth0" {
|
||||
t.Errorf("netmap anchor not resolved to eth0: %+v", cfg.Netmap)
|
||||
}
|
||||
if len(cfg.NAT) != 1 || cfg.NAT[0].External != "203.0.113.10" {
|
||||
t.Errorf("1:1 nat not rendered on its device: %+v", cfg.NAT)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderNATSkipsNonEgressAndOtherDevices(t *testing.T) {
|
||||
// rt1 binds only net (not loc): masquerade requires both, so it's skipped;
|
||||
// the netmap/nat are anchored/bound to fw-a, so they don't render here either.
|
||||
interior := natInput()
|
||||
interior.Device = model.Device{Name: "rt1", Class: model.ClassRouter}
|
||||
interior.Bindings = []model.Binding{{Device: "rt1", Zone: "net", Interfaces: []string{"eth0"}}}
|
||||
cfg, err := Render(interior)
|
||||
if err != nil {
|
||||
t.Fatalf("Render: %v", err)
|
||||
}
|
||||
if len(cfg.SNAT) != 0 {
|
||||
t.Errorf("masquerade should not render without the source-zone binding: %+v", cfg.SNAT)
|
||||
}
|
||||
if len(cfg.Netmap) != 0 || len(cfg.NAT) != 0 {
|
||||
t.Errorf("netmap/nat must not render on a device they aren't bound to: %+v %+v", cfg.Netmap, cfg.NAT)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveResolverPrefersDevice(t *testing.T) {
|
||||
in := baseInput()
|
||||
in.Device = model.Device{Name: "fw-a", Class: model.ClassFirewall, Resolver: []string{"10.9.9.9"}}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package compiler
|
||||
|
||||
// Per-device long-tail sections rendered into a device's config. The compiler
|
||||
// filters each global list to the entries owned by the device.
|
||||
|
||||
type RenderedHost struct {
|
||||
Zone string `yaml:"zone" json:"zone"`
|
||||
Interface string `yaml:"interface" json:"interface"`
|
||||
Addresses []string `yaml:"addresses,omitempty" json:"addresses,omitempty"`
|
||||
Exclusions []string `yaml:"exclusions,omitempty" json:"exclusions,omitempty"`
|
||||
Dynamic bool `yaml:"dynamic,omitempty" json:"dynamic,omitempty"`
|
||||
}
|
||||
|
||||
type RenderedProvider struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Number int `yaml:"number" json:"number"`
|
||||
Mark int `yaml:"mark,omitempty" json:"mark,omitempty"`
|
||||
Duplicate string `yaml:"duplicate,omitempty" json:"duplicate,omitempty"`
|
||||
Interface string `yaml:"interface" json:"interface"`
|
||||
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
|
||||
Copy []string `yaml:"copy,omitempty" json:"copy,omitempty"`
|
||||
}
|
||||
|
||||
type RenderedRoute struct {
|
||||
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
|
||||
Dest string `yaml:"dest" json:"dest"`
|
||||
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
|
||||
Oif string `yaml:"oif,omitempty" json:"oif,omitempty"`
|
||||
Persistent bool `yaml:"persistent,omitempty" json:"persistent,omitempty"`
|
||||
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type RenderedRoutingRule struct {
|
||||
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
||||
Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
|
||||
Provider string `yaml:"provider" json:"provider"`
|
||||
Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`
|
||||
Persistent bool `yaml:"persistent,omitempty" json:"persistent,omitempty"`
|
||||
Mark string `yaml:"mark,omitempty" json:"mark,omitempty"`
|
||||
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// renderPerDevice projects the per-device long-tail sections owned by this device.
|
||||
func renderPerDevice(in Input, out *RenderedConfig) {
|
||||
dev := in.Device.Name
|
||||
for _, h := range in.Hosts {
|
||||
if h.Device != dev {
|
||||
continue
|
||||
}
|
||||
out.Hosts = append(out.Hosts, RenderedHost{
|
||||
Zone: h.Zone, Interface: h.Interface, Addresses: h.Addresses,
|
||||
Exclusions: h.Exclusions, Dynamic: h.Dynamic,
|
||||
})
|
||||
}
|
||||
for _, p := range in.Providers {
|
||||
if p.Device != dev {
|
||||
continue
|
||||
}
|
||||
out.Providers = append(out.Providers, RenderedProvider{
|
||||
Name: p.Name, Number: p.Number, Mark: p.Mark, Duplicate: p.Duplicate,
|
||||
Interface: p.Interface, Gateway: p.Gateway, Copy: p.Copy,
|
||||
})
|
||||
}
|
||||
for _, r := range in.Routes {
|
||||
if r.Device != dev {
|
||||
continue
|
||||
}
|
||||
out.Routes = append(out.Routes, RenderedRoute{
|
||||
Provider: r.Provider, Dest: r.Dest, Gateway: r.Gateway,
|
||||
Oif: r.Oif, Persistent: r.Persistent, Comment: r.Comment,
|
||||
})
|
||||
}
|
||||
for _, r := range in.RoutingRules {
|
||||
if r.Device != dev {
|
||||
continue
|
||||
}
|
||||
out.RoutingRules = append(out.RoutingRules, RenderedRoutingRule{
|
||||
Source: r.Source, Dest: r.Dest, Provider: r.Provider, Priority: r.Priority,
|
||||
Persistent: r.Persistent, Mark: r.Mark, Comment: r.Comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package compiler
|
||||
|
||||
import "git.unkin.net/unkin/tomswallapi/internal/model"
|
||||
|
||||
// Per-device L2/misc long-tail sections rendered into a device's config.
|
||||
|
||||
type RenderedTunnel struct {
|
||||
Type string `yaml:"type" json:"type"`
|
||||
Zone string `yaml:"zone" json:"zone"`
|
||||
Gateways []string `yaml:"gateways,omitempty" json:"gateways,omitempty"`
|
||||
GatewayZones []string `yaml:"gateway_zones,omitempty" json:"gateway_zones,omitempty"`
|
||||
Port int `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type RenderedStoppedRule struct {
|
||||
Action string `yaml:"action" json:"action"`
|
||||
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
||||
Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
|
||||
Proto string `yaml:"proto,omitempty" json:"proto,omitempty"`
|
||||
DPort []string `yaml:"dport,omitempty" json:"dport,omitempty"`
|
||||
SPort []string `yaml:"sport,omitempty" json:"sport,omitempty"`
|
||||
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type RenderedProxy struct {
|
||||
Address string `yaml:"address" json:"address"`
|
||||
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
|
||||
External string `yaml:"external" json:"external"`
|
||||
HaveRoute bool `yaml:"haveroute,omitempty" json:"haveroute,omitempty"`
|
||||
Persistent bool `yaml:"persistent,omitempty" json:"persistent,omitempty"`
|
||||
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type RenderedArpRule struct {
|
||||
Action string `yaml:"action" json:"action"`
|
||||
ActionAddress string `yaml:"action_address,omitempty" json:"action_address,omitempty"`
|
||||
ActionMAC string `yaml:"action_mac,omitempty" json:"action_mac,omitempty"`
|
||||
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
||||
Dest string `yaml:"dest,omitempty" json:"dest,omitempty"`
|
||||
Opcode int `yaml:"opcode,omitempty" json:"opcode,omitempty"`
|
||||
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type RenderedMaclist struct {
|
||||
Action string `yaml:"action" json:"action"`
|
||||
Interface string `yaml:"interface" json:"interface"`
|
||||
MAC string `yaml:"mac,omitempty" json:"mac,omitempty"`
|
||||
Addresses []string `yaml:"addresses,omitempty" json:"addresses,omitempty"`
|
||||
Log string `yaml:"log,omitempty" json:"log,omitempty"`
|
||||
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
func renderPerDeviceL2(in Input, out *RenderedConfig) {
|
||||
dev := in.Device.Name
|
||||
for _, t := range in.Tunnels {
|
||||
if t.Device != dev {
|
||||
continue
|
||||
}
|
||||
out.Tunnels = append(out.Tunnels, RenderedTunnel{
|
||||
Type: t.Type, Zone: t.Zone, Gateways: t.Gateways,
|
||||
GatewayZones: t.GatewayZones, Port: t.Port, Comment: t.Comment,
|
||||
})
|
||||
}
|
||||
for _, r := range in.StoppedRules {
|
||||
if r.Device != dev {
|
||||
continue
|
||||
}
|
||||
out.StoppedRules = append(out.StoppedRules, RenderedStoppedRule{
|
||||
Action: r.Action, Source: r.Source, Dest: r.Dest, Proto: r.Proto,
|
||||
DPort: r.DPort, SPort: r.SPort, Comment: r.Comment,
|
||||
})
|
||||
}
|
||||
for _, p := range in.ProxyARP {
|
||||
if p.Device != dev {
|
||||
continue
|
||||
}
|
||||
out.ProxyARP = append(out.ProxyARP, renderProxy(p))
|
||||
}
|
||||
for _, p := range in.ProxyNDP {
|
||||
if p.Device != dev {
|
||||
continue
|
||||
}
|
||||
out.ProxyNDP = append(out.ProxyNDP, renderProxy(p))
|
||||
}
|
||||
for _, a := range in.ArpRules {
|
||||
if a.Device != dev {
|
||||
continue
|
||||
}
|
||||
out.ArpRules = append(out.ArpRules, RenderedArpRule{
|
||||
Action: a.Action, ActionAddress: a.ActionAddress, ActionMAC: a.ActionMAC,
|
||||
Source: a.Source, Dest: a.Dest, Opcode: a.Opcode, Comment: a.Comment,
|
||||
})
|
||||
}
|
||||
for _, m := range in.Maclist {
|
||||
if m.Device != dev {
|
||||
continue
|
||||
}
|
||||
out.Maclist = append(out.Maclist, RenderedMaclist{
|
||||
Action: m.Action, Interface: m.Interface, MAC: m.MAC,
|
||||
Addresses: m.Addresses, Log: m.Log, Comment: m.Comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func renderProxy(p model.ProxyEntry) RenderedProxy {
|
||||
return RenderedProxy{
|
||||
Address: p.Address, Interface: p.Interface, External: p.External,
|
||||
HaveRoute: p.HaveRoute, Persistent: p.Persistent, Comment: p.Comment,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,12 @@ type Config struct {
|
||||
// AgentToken guards the per-device config endpoint (used by tomswall agents).
|
||||
AgentToken string
|
||||
|
||||
// IPLocateAPIKey is used to expand ASN address groups into prefixes.
|
||||
// IPLocateAPIKey is used to expand ASN address groups into prefixes via the
|
||||
// iplocate API (legacy path). The DB expander below is preferred.
|
||||
IPLocateAPIKey string
|
||||
// IPLocateDBURL points at the iplocate ip-to-asn CSV database (proxied via
|
||||
// artifactapi). Empty uses the built-in default.
|
||||
IPLocateDBURL string
|
||||
}
|
||||
|
||||
// Load reads configuration from the environment, applying defaults.
|
||||
@@ -40,6 +44,7 @@ func Load() (*Config, error) {
|
||||
WriteToken: os.Getenv("TOMSWALLAPI_WRITE_TOKEN"),
|
||||
AgentToken: os.Getenv("TOMSWALLAPI_AGENT_TOKEN"),
|
||||
IPLocateAPIKey: os.Getenv("TOMSWALLAPI_IPLOCATE_API_KEY"),
|
||||
IPLocateDBURL: os.Getenv("TOMSWALLAPI_IPLOCATE_DB_URL"),
|
||||
}
|
||||
if c.DBName == "" {
|
||||
return nil, fmt.Errorf("TOMSWALLAPI_DB_NAME must not be empty")
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Per-device reachability, reported by the agent from its FIB. The compiler uses
|
||||
-- it to scope which routers actually need to enforce an intent: a router only
|
||||
-- carries a rule if it can route to both the source and destination networks.
|
||||
-- When absent, the compiler safely over-approximates (enforces everywhere).
|
||||
ALTER TABLE devices
|
||||
ADD COLUMN reachable_prefixes JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN routes_reported_at TIMESTAMPTZ;
|
||||
@@ -0,0 +1,32 @@
|
||||
-- Global-compiled long-tail sections: blrules (blacklist/whitelist, processed
|
||||
-- before normal rules) and conntrack (connection-tracking control). The policies
|
||||
-- table already exists (migration 0001); this migration only adds the two new
|
||||
-- tables — policy CRUD is wired on the existing table.
|
||||
|
||||
CREATE TABLE blrules (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
priority INT NOT NULL DEFAULT 0,
|
||||
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,
|
||||
log TEXT NOT NULL DEFAULT '',
|
||||
comment TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE conntrack (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
priority INT NOT NULL DEFAULT 0,
|
||||
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,
|
||||
chain TEXT NOT NULL DEFAULT '',
|
||||
helper TEXT NOT NULL DEFAULT '',
|
||||
"user" TEXT NOT NULL DEFAULT '',
|
||||
comment TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Per-device long-tail sections: hosts, providers (multi-ISP), static routes,
|
||||
-- and routing rules (rtrules). Each is owned by a device and renders into that
|
||||
-- device's config. (Nested option structs on host/provider are deferred.)
|
||||
|
||||
CREATE TABLE hosts (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
|
||||
zone TEXT NOT NULL,
|
||||
interface TEXT NOT NULL,
|
||||
addresses JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
exclusions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
dynamic BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
CREATE TABLE providers (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
number INT NOT NULL,
|
||||
mark INT NOT NULL DEFAULT 0,
|
||||
duplicate TEXT NOT NULL DEFAULT '',
|
||||
interface TEXT NOT NULL,
|
||||
gateway TEXT NOT NULL DEFAULT '',
|
||||
copy JSONB NOT NULL DEFAULT '[]'::jsonb
|
||||
);
|
||||
|
||||
CREATE TABLE routes (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
|
||||
provider TEXT NOT NULL DEFAULT '',
|
||||
dest TEXT NOT NULL,
|
||||
gateway TEXT NOT NULL DEFAULT '',
|
||||
oif TEXT NOT NULL DEFAULT '', -- egress interface (tomswall route "device")
|
||||
persistent BOOLEAN NOT NULL DEFAULT false,
|
||||
comment TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE routing_rules (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
device TEXT NOT NULL REFERENCES devices(name) ON DELETE CASCADE,
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
dest TEXT NOT NULL DEFAULT '',
|
||||
provider TEXT NOT NULL,
|
||||
priority INT NOT NULL DEFAULT 0,
|
||||
persistent BOOLEAN NOT NULL DEFAULT false,
|
||||
mark TEXT NOT NULL DEFAULT '',
|
||||
comment TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
@@ -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 ''
|
||||
);
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
// BlruleRule is a blacklist/whitelist rule, processed before normal rules.
|
||||
type BlruleRule struct {
|
||||
ID int64 `json:"id"`
|
||||
Priority int `json:"priority"`
|
||||
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"`
|
||||
Log string `json:"log,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// ConntrackRule controls connection tracking (notrack, helper assignment).
|
||||
type ConntrackRule struct {
|
||||
ID int64 `json:"id"`
|
||||
Priority int `json:"priority"`
|
||||
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"`
|
||||
Chain string `json:"chain,omitempty"`
|
||||
Helper string `json:"helper,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
@@ -99,6 +99,10 @@ type Device struct {
|
||||
Fabric string `json:"fabric,omitempty"`
|
||||
Resolver []string `json:"resolver,omitempty"`
|
||||
Settings map[string]string `json:"settings,omitempty"`
|
||||
|
||||
// ReachablePrefixes is the device's FIB as last reported by its agent
|
||||
// (server-managed). The compiler uses it to scope router enforcement.
|
||||
ReachablePrefixes []string `json:"reachable_prefixes,omitempty"`
|
||||
}
|
||||
|
||||
// Binding maps a global zone to one device's local interface(s).
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package model
|
||||
|
||||
// Per-device long-tail sections. Each is owned by a device (the fleet member it
|
||||
// renders on) and maps to the corresponding tomswall config section.
|
||||
|
||||
// Host constrains a zone to specific addresses on a device's interface.
|
||||
type Host struct {
|
||||
ID int64 `json:"id"`
|
||||
Device string `json:"device"`
|
||||
Zone string `json:"zone"`
|
||||
Interface string `json:"interface"`
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
Exclusions []string `json:"exclusions,omitempty"`
|
||||
Dynamic bool `json:"dynamic,omitempty"`
|
||||
}
|
||||
|
||||
// Provider is a multi-ISP routing provider on a device.
|
||||
type Provider struct {
|
||||
ID int64 `json:"id"`
|
||||
Device string `json:"device"`
|
||||
Name string `json:"name"`
|
||||
Number int `json:"number"`
|
||||
Mark int `json:"mark,omitempty"`
|
||||
Duplicate string `json:"duplicate,omitempty"`
|
||||
Interface string `json:"interface"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
Copy []string `json:"copy,omitempty"`
|
||||
}
|
||||
|
||||
// Route is a static route on a device. Oif is the egress interface (tomswall's
|
||||
// route "device" field, renamed to avoid colliding with the fleet device).
|
||||
type Route struct {
|
||||
ID int64 `json:"id"`
|
||||
Device string `json:"device"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Dest string `json:"dest"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
Oif string `json:"oif,omitempty"`
|
||||
Persistent bool `json:"persistent,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// RoutingRule directs traffic to a provider's routing table on a device.
|
||||
type RoutingRule struct {
|
||||
ID int64 `json:"id"`
|
||||
Device string `json:"device"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Dest string `json:"dest,omitempty"`
|
||||
Provider string `json:"provider"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Persistent bool `json:"persistent,omitempty"`
|
||||
Mark string `json:"mark,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package model
|
||||
|
||||
// Per-device L2/misc long-tail sections.
|
||||
|
||||
type Tunnel struct {
|
||||
ID int64 `json:"id"`
|
||||
Device string `json:"device"`
|
||||
Type string `json:"type"`
|
||||
Zone string `json:"zone"`
|
||||
Gateways []string `json:"gateways,omitempty"`
|
||||
GatewayZones []string `json:"gateway_zones,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type StoppedRule struct {
|
||||
ID int64 `json:"id"`
|
||||
Device string `json:"device"`
|
||||
Action string `json:"action"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Dest string `json:"dest,omitempty"`
|
||||
Proto string `json:"proto,omitempty"`
|
||||
DPort []string `json:"dport,omitempty"`
|
||||
SPort []string `json:"sport,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// ProxyEntry backs both proxy_arp and proxy_ndp (identical shape).
|
||||
type ProxyEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
Device string `json:"device"`
|
||||
Address string `json:"address"`
|
||||
Interface string `json:"interface,omitempty"`
|
||||
External string `json:"external"`
|
||||
HaveRoute bool `json:"haveroute,omitempty"`
|
||||
Persistent bool `json:"persistent,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type ArpRule struct {
|
||||
ID int64 `json:"id"`
|
||||
Device string `json:"device"`
|
||||
Action string `json:"action"`
|
||||
ActionAddress string `json:"action_address,omitempty"`
|
||||
ActionMAC string `json:"action_mac,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Dest string `json:"dest,omitempty"`
|
||||
Opcode int `json:"opcode,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type MaclistEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
Device string `json:"device"`
|
||||
Action string `json:"action"`
|
||||
Interface string `json:"interface"`
|
||||
MAC string `json:"mac,omitempty"`
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
Log string `json:"log,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,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"`
|
||||
}
|
||||
@@ -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")))
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.unkin.net/unkin/tomswallapi/internal/model"
|
||||
)
|
||||
|
||||
// mountLongtail wires the global-compiled long-tail resources: policies (default
|
||||
// posture), blrules (blacklist/whitelist), and conntrack.
|
||||
func (s *Server) mountLongtail(r chi.Router) {
|
||||
r.Route("/policies", func(r chi.Router) {
|
||||
r.Get("/", s.listPolicies)
|
||||
r.Post("/", s.createPolicy)
|
||||
r.Get("/{id}", s.getPolicy)
|
||||
r.Delete("/{id}", s.deletePolicy)
|
||||
})
|
||||
r.Route("/blrules", func(r chi.Router) {
|
||||
r.Get("/", s.listBlrules)
|
||||
r.Post("/", s.createBlrule)
|
||||
r.Get("/{id}", s.getBlrule)
|
||||
r.Delete("/{id}", s.deleteBlrule)
|
||||
})
|
||||
r.Route("/conntrack", func(r chi.Router) {
|
||||
r.Get("/", s.listConntrack)
|
||||
r.Post("/", s.createConntrack)
|
||||
r.Get("/{id}", s.getConntrack)
|
||||
r.Delete("/{id}", s.deleteConntrack)
|
||||
})
|
||||
}
|
||||
|
||||
// ---- Policies --------------------------------------------------------------
|
||||
|
||||
func (s *Server) listPolicies(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.store.ListPolicies(r.Context())
|
||||
respondList(w, list, err)
|
||||
}
|
||||
|
||||
func (s *Server) createPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
var v model.Policy
|
||||
if !decode(w, r, &v) {
|
||||
return
|
||||
}
|
||||
if v.Action == "" {
|
||||
writeError(w, http.StatusBadRequest, "action is required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreatePolicy(r.Context(), v)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
v.ID = id
|
||||
writeJSON(w, http.StatusCreated, v)
|
||||
}
|
||||
|
||||
func (s *Server) getPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, err := s.store.GetPolicy(r.Context(), id)
|
||||
respondOne(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) deletePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondDelete(w, s.store.DeletePolicy(r.Context(), id))
|
||||
}
|
||||
|
||||
// ---- Blrules ---------------------------------------------------------------
|
||||
|
||||
func (s *Server) listBlrules(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.store.ListBlrules(r.Context())
|
||||
respondList(w, list, err)
|
||||
}
|
||||
|
||||
func (s *Server) createBlrule(w http.ResponseWriter, r *http.Request) {
|
||||
var v model.BlruleRule
|
||||
if !decode(w, r, &v) {
|
||||
return
|
||||
}
|
||||
if v.Action == "" {
|
||||
writeError(w, http.StatusBadRequest, "action is required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreateBlrule(r.Context(), v)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
v.ID = id
|
||||
writeJSON(w, http.StatusCreated, v)
|
||||
}
|
||||
|
||||
func (s *Server) getBlrule(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, err := s.store.GetBlrule(r.Context(), id)
|
||||
respondOne(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) deleteBlrule(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondDelete(w, s.store.DeleteBlrule(r.Context(), id))
|
||||
}
|
||||
|
||||
// ---- Conntrack -------------------------------------------------------------
|
||||
|
||||
func (s *Server) listConntrack(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.store.ListConntrack(r.Context())
|
||||
respondList(w, list, err)
|
||||
}
|
||||
|
||||
func (s *Server) createConntrack(w http.ResponseWriter, r *http.Request) {
|
||||
var v model.ConntrackRule
|
||||
if !decode(w, r, &v) {
|
||||
return
|
||||
}
|
||||
if v.Action == "" {
|
||||
writeError(w, http.StatusBadRequest, "action is required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreateConntrack(r.Context(), v)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
v.ID = id
|
||||
writeJSON(w, http.StatusCreated, v)
|
||||
}
|
||||
|
||||
func (s *Server) getConntrack(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, err := s.store.GetConntrack(r.Context(), id)
|
||||
respondOne(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) deleteConntrack(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondDelete(w, s.store.DeleteConntrack(r.Context(), id))
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.unkin.net/unkin/tomswallapi/internal/model"
|
||||
)
|
||||
|
||||
// mountPerDevice wires the per-device long-tail sections: hosts, providers,
|
||||
// routes, and routing-rules.
|
||||
func (s *Server) mountPerDevice(r chi.Router) {
|
||||
r.Route("/hosts", func(r chi.Router) {
|
||||
r.Get("/", s.listHosts)
|
||||
r.Post("/", s.createHost)
|
||||
r.Get("/{id}", s.getHost)
|
||||
r.Delete("/{id}", s.deleteHost)
|
||||
})
|
||||
r.Route("/providers", func(r chi.Router) {
|
||||
r.Get("/", s.listProviders)
|
||||
r.Post("/", s.createProvider)
|
||||
r.Get("/{id}", s.getProvider)
|
||||
r.Delete("/{id}", s.deleteProvider)
|
||||
})
|
||||
r.Route("/routes", func(r chi.Router) {
|
||||
r.Get("/", s.listRoutes)
|
||||
r.Post("/", s.createRoute)
|
||||
r.Get("/{id}", s.getRoute)
|
||||
r.Delete("/{id}", s.deleteRoute)
|
||||
})
|
||||
r.Route("/routing-rules", func(r chi.Router) {
|
||||
r.Get("/", s.listRoutingRules)
|
||||
r.Post("/", s.createRoutingRule)
|
||||
r.Get("/{id}", s.getRoutingRule)
|
||||
r.Delete("/{id}", s.deleteRoutingRule)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) listHosts(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.store.ListHosts(r.Context())
|
||||
respondList(w, list, err)
|
||||
}
|
||||
|
||||
func (s *Server) createHost(w http.ResponseWriter, r *http.Request) {
|
||||
var v model.Host
|
||||
if !decode(w, r, &v) {
|
||||
return
|
||||
}
|
||||
if v.Device == "" || v.Zone == "" || v.Interface == "" {
|
||||
writeError(w, http.StatusBadRequest, "device, zone, and interface are required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreateHost(r.Context(), v)
|
||||
if err == nil {
|
||||
v.ID = id
|
||||
}
|
||||
respondCreated(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) getHost(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, err := s.store.GetHost(r.Context(), id)
|
||||
respondOne(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) deleteHost(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondDelete(w, s.store.DeleteHost(r.Context(), id))
|
||||
}
|
||||
|
||||
func (s *Server) listProviders(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.store.ListProviders(r.Context())
|
||||
respondList(w, list, err)
|
||||
}
|
||||
|
||||
func (s *Server) createProvider(w http.ResponseWriter, r *http.Request) {
|
||||
var v model.Provider
|
||||
if !decode(w, r, &v) {
|
||||
return
|
||||
}
|
||||
if v.Device == "" || v.Name == "" || v.Interface == "" {
|
||||
writeError(w, http.StatusBadRequest, "device, name, and interface are required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreateProvider(r.Context(), v)
|
||||
if err == nil {
|
||||
v.ID = id
|
||||
}
|
||||
respondCreated(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) getProvider(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, err := s.store.GetProvider(r.Context(), id)
|
||||
respondOne(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) deleteProvider(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondDelete(w, s.store.DeleteProvider(r.Context(), id))
|
||||
}
|
||||
|
||||
func (s *Server) listRoutes(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.store.ListRoutes(r.Context())
|
||||
respondList(w, list, err)
|
||||
}
|
||||
|
||||
func (s *Server) createRoute(w http.ResponseWriter, r *http.Request) {
|
||||
var v model.Route
|
||||
if !decode(w, r, &v) {
|
||||
return
|
||||
}
|
||||
if v.Device == "" || v.Dest == "" {
|
||||
writeError(w, http.StatusBadRequest, "device and dest are required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreateRoute(r.Context(), v)
|
||||
if err == nil {
|
||||
v.ID = id
|
||||
}
|
||||
respondCreated(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) getRoute(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, err := s.store.GetRoute(r.Context(), id)
|
||||
respondOne(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) deleteRoute(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondDelete(w, s.store.DeleteRoute(r.Context(), id))
|
||||
}
|
||||
|
||||
func (s *Server) listRoutingRules(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.store.ListRoutingRules(r.Context())
|
||||
respondList(w, list, err)
|
||||
}
|
||||
|
||||
func (s *Server) createRoutingRule(w http.ResponseWriter, r *http.Request) {
|
||||
var v model.RoutingRule
|
||||
if !decode(w, r, &v) {
|
||||
return
|
||||
}
|
||||
if v.Device == "" || v.Provider == "" {
|
||||
writeError(w, http.StatusBadRequest, "device and provider are required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreateRoutingRule(r.Context(), v)
|
||||
if err == nil {
|
||||
v.ID = id
|
||||
}
|
||||
respondCreated(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) getRoutingRule(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
v, err := s.store.GetRoutingRule(r.Context(), id)
|
||||
respondOne(w, v, err)
|
||||
}
|
||||
|
||||
func (s *Server) deleteRoutingRule(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := idParam(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondDelete(w, s.store.DeleteRoutingRule(r.Context(), id))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -60,6 +60,11 @@ func (s *Server) mountResources(r chi.Router) {
|
||||
r.Delete("/{id}", s.deleteRule)
|
||||
})
|
||||
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.
|
||||
@@ -75,6 +80,15 @@ func respondOne(w http.ResponseWriter, v any, err error) {
|
||||
writeJSON(w, http.StatusOK, v)
|
||||
}
|
||||
|
||||
// respondCreated writes a 201 with the created resource, or 500 on error.
|
||||
func respondCreated(w http.ResponseWriter, v any, err error) {
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, v)
|
||||
}
|
||||
|
||||
// respondDelete maps a delete result to 204/404/500.
|
||||
func respondDelete(w http.ResponseWriter, err error) {
|
||||
if err != nil {
|
||||
@@ -350,6 +364,24 @@ func (s *Server) handleDeviceConfig(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeviceRoutes(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Prefixes []string `json:"prefixes"`
|
||||
}
|
||||
if !decode(w, r, &body) {
|
||||
return
|
||||
}
|
||||
if err := s.store.UpdateDeviceRoutes(r.Context(), chi.URLParam(r, "name"), body.Prefixes); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
writeError(w, http.StatusNotFound, "device not found")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeviceStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Generation int64 `json:"generation"`
|
||||
|
||||
@@ -94,6 +94,7 @@ func (s *Server) routes() http.Handler {
|
||||
r.Use(s.requireToken(s.agentToken))
|
||||
r.Get("/devices/{name}/config", s.handleDeviceConfig)
|
||||
r.Post("/devices/{name}/status", s.handleDeviceStatus)
|
||||
r.Post("/devices/{name}/routes", s.handleDeviceRoutes)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"git.unkin.net/unkin/tomswallapi/internal/model"
|
||||
)
|
||||
|
||||
// ---- Policies (table exists from 0001; ListPolicies is in store.go) ---------
|
||||
|
||||
func (s *Store) GetPolicy(ctx context.Context, id int64) (model.Policy, error) {
|
||||
var p model.Policy
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT id, priority, source, dest, action, log FROM policies WHERE id = $1`, id,
|
||||
).Scan(&p.ID, &p.Priority, &p.Source, &p.Dest, &p.Action, &p.Log)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return p, ErrNotFound
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (s *Store) CreatePolicy(ctx context.Context, p model.Policy) (int64, error) {
|
||||
var id int64
|
||||
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO policies (priority, source, dest, action, log)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING id`,
|
||||
p.Priority, p.Source, p.Dest, p.Action, p.Log,
|
||||
).Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
return bump(ctx, tx)
|
||||
})
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) DeletePolicy(ctx context.Context, id int64) error {
|
||||
return s.deleteOne(ctx, `DELETE FROM policies WHERE id = $1`, id)
|
||||
}
|
||||
|
||||
// ---- Blrules ---------------------------------------------------------------
|
||||
|
||||
func (s *Store) ListBlrules(ctx context.Context) ([]model.BlruleRule, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, priority, action, source, dest, proto, dport, sport, log, comment
|
||||
FROM blrules ORDER BY priority, id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []model.BlruleRule
|
||||
for rows.Next() {
|
||||
r, err := scanBlrule(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetBlrule(ctx context.Context, id int64) (model.BlruleRule, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, priority, action, source, dest, proto, dport, sport, log, comment
|
||||
FROM blrules WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return model.BlruleRule{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return model.BlruleRule{}, ErrNotFound
|
||||
}
|
||||
return scanBlrule(rows)
|
||||
}
|
||||
|
||||
func scanBlrule(rows pgx.Rows) (model.BlruleRule, error) {
|
||||
var r model.BlruleRule
|
||||
var dport, sport []byte
|
||||
if err := rows.Scan(&r.ID, &r.Priority, &r.Action, &r.Source, &r.Dest, &r.Proto, &dport, &sport, &r.Log, &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) CreateBlrule(ctx context.Context, r model.BlruleRule) (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 blrules (priority, action, source, dest, proto, dport, sport, log, comment)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`,
|
||||
r.Priority, r.Action, r.Source, r.Dest, r.Proto, dport, sport, r.Log, r.Comment,
|
||||
).Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
return bump(ctx, tx)
|
||||
})
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteBlrule(ctx context.Context, id int64) error {
|
||||
return s.deleteOne(ctx, `DELETE FROM blrules WHERE id = $1`, id)
|
||||
}
|
||||
|
||||
// ---- Conntrack -------------------------------------------------------------
|
||||
|
||||
func (s *Store) ListConntrack(ctx context.Context) ([]model.ConntrackRule, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, priority, action, source, dest, proto, dport, sport, chain, helper, "user", comment
|
||||
FROM conntrack ORDER BY priority, id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []model.ConntrackRule
|
||||
for rows.Next() {
|
||||
r, err := scanConntrack(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetConntrack(ctx context.Context, id int64) (model.ConntrackRule, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, priority, action, source, dest, proto, dport, sport, chain, helper, "user", comment
|
||||
FROM conntrack WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return model.ConntrackRule{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return model.ConntrackRule{}, ErrNotFound
|
||||
}
|
||||
return scanConntrack(rows)
|
||||
}
|
||||
|
||||
func scanConntrack(rows pgx.Rows) (model.ConntrackRule, error) {
|
||||
var r model.ConntrackRule
|
||||
var dport, sport []byte
|
||||
if err := rows.Scan(&r.ID, &r.Priority, &r.Action, &r.Source, &r.Dest, &r.Proto, &dport, &sport, &r.Chain, &r.Helper, &r.User, &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) CreateConntrack(ctx context.Context, r model.ConntrackRule) (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 conntrack (priority, action, source, dest, proto, dport, sport, chain, helper, "user", comment)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id`,
|
||||
r.Priority, r.Action, r.Source, r.Dest, r.Proto, dport, sport, r.Chain, r.Helper, r.User, r.Comment,
|
||||
).Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
return bump(ctx, tx)
|
||||
})
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteConntrack(ctx context.Context, id int64) error {
|
||||
return s.deleteOne(ctx, `DELETE FROM conntrack WHERE id = $1`, id)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"git.unkin.net/unkin/tomswallapi/internal/model"
|
||||
)
|
||||
|
||||
// ---- Hosts -----------------------------------------------------------------
|
||||
|
||||
func (s *Store) ListHosts(ctx context.Context) ([]model.Host, error) {
|
||||
rows, err := s.pool.Query(ctx,
|
||||
`SELECT id, device, zone, interface, addresses, exclusions, dynamic FROM hosts ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []model.Host
|
||||
for rows.Next() {
|
||||
h, err := scanHost(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, h)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetHost(ctx context.Context, id int64) (model.Host, error) {
|
||||
rows, err := s.pool.Query(ctx,
|
||||
`SELECT id, device, zone, interface, addresses, exclusions, dynamic FROM hosts WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return model.Host{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return model.Host{}, ErrNotFound
|
||||
}
|
||||
return scanHost(rows)
|
||||
}
|
||||
|
||||
func scanHost(rows pgx.Rows) (model.Host, error) {
|
||||
var h model.Host
|
||||
var addrs, excl []byte
|
||||
if err := rows.Scan(&h.ID, &h.Device, &h.Zone, &h.Interface, &addrs, &excl, &h.Dynamic); err != nil {
|
||||
return h, err
|
||||
}
|
||||
if err := unmarshalStrings(addrs, &h.Addresses); err != nil {
|
||||
return h, err
|
||||
}
|
||||
return h, unmarshalStrings(excl, &h.Exclusions)
|
||||
}
|
||||
|
||||
func (s *Store) CreateHost(ctx context.Context, h model.Host) (int64, error) {
|
||||
addrs, _ := jsonb(h.Addresses)
|
||||
excl, _ := jsonb(h.Exclusions)
|
||||
var id int64
|
||||
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO hosts (device, zone, interface, addresses, exclusions, dynamic)
|
||||
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
||||
h.Device, h.Zone, h.Interface, addrs, excl, h.Dynamic).Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
return bump(ctx, tx)
|
||||
})
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteHost(ctx context.Context, id int64) error {
|
||||
return s.deleteOne(ctx, `DELETE FROM hosts WHERE id = $1`, id)
|
||||
}
|
||||
|
||||
// ---- Providers -------------------------------------------------------------
|
||||
|
||||
func (s *Store) ListProviders(ctx context.Context) ([]model.Provider, error) {
|
||||
rows, err := s.pool.Query(ctx,
|
||||
`SELECT id, device, name, number, mark, duplicate, interface, gateway, copy FROM providers ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []model.Provider
|
||||
for rows.Next() {
|
||||
p, err := scanProvider(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetProvider(ctx context.Context, id int64) (model.Provider, error) {
|
||||
rows, err := s.pool.Query(ctx,
|
||||
`SELECT id, device, name, number, mark, duplicate, interface, gateway, copy FROM providers WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return model.Provider{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return model.Provider{}, ErrNotFound
|
||||
}
|
||||
return scanProvider(rows)
|
||||
}
|
||||
|
||||
func scanProvider(rows pgx.Rows) (model.Provider, error) {
|
||||
var p model.Provider
|
||||
var cp []byte
|
||||
if err := rows.Scan(&p.ID, &p.Device, &p.Name, &p.Number, &p.Mark, &p.Duplicate, &p.Interface, &p.Gateway, &cp); err != nil {
|
||||
return p, err
|
||||
}
|
||||
return p, unmarshalStrings(cp, &p.Copy)
|
||||
}
|
||||
|
||||
func (s *Store) CreateProvider(ctx context.Context, p model.Provider) (int64, error) {
|
||||
cp, _ := jsonb(p.Copy)
|
||||
var id int64
|
||||
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO providers (device, name, number, mark, duplicate, interface, gateway, copy)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
|
||||
p.Device, p.Name, p.Number, p.Mark, p.Duplicate, p.Interface, p.Gateway, cp).Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
return bump(ctx, tx)
|
||||
})
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteProvider(ctx context.Context, id int64) error {
|
||||
return s.deleteOne(ctx, `DELETE FROM providers WHERE id = $1`, id)
|
||||
}
|
||||
|
||||
// ---- Routes ----------------------------------------------------------------
|
||||
|
||||
func (s *Store) ListRoutes(ctx context.Context) ([]model.Route, error) {
|
||||
rows, err := s.pool.Query(ctx,
|
||||
`SELECT id, device, provider, dest, gateway, oif, persistent, comment FROM routes ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []model.Route
|
||||
for rows.Next() {
|
||||
var r model.Route
|
||||
if err := rows.Scan(&r.ID, &r.Device, &r.Provider, &r.Dest, &r.Gateway, &r.Oif, &r.Persistent, &r.Comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetRoute(ctx context.Context, id int64) (model.Route, error) {
|
||||
var r model.Route
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT id, device, provider, dest, gateway, oif, persistent, comment FROM routes WHERE id = $1`, id,
|
||||
).Scan(&r.ID, &r.Device, &r.Provider, &r.Dest, &r.Gateway, &r.Oif, &r.Persistent, &r.Comment)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return r, ErrNotFound
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
func (s *Store) CreateRoute(ctx context.Context, r model.Route) (int64, error) {
|
||||
var id int64
|
||||
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO routes (device, provider, dest, gateway, oif, persistent, comment)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
|
||||
r.Device, r.Provider, r.Dest, r.Gateway, r.Oif, r.Persistent, r.Comment).Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
return bump(ctx, tx)
|
||||
})
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteRoute(ctx context.Context, id int64) error {
|
||||
return s.deleteOne(ctx, `DELETE FROM routes WHERE id = $1`, id)
|
||||
}
|
||||
|
||||
// ---- Routing rules ---------------------------------------------------------
|
||||
|
||||
func (s *Store) ListRoutingRules(ctx context.Context) ([]model.RoutingRule, error) {
|
||||
rows, err := s.pool.Query(ctx,
|
||||
`SELECT id, device, source, dest, provider, priority, persistent, mark, comment FROM routing_rules ORDER BY priority, id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []model.RoutingRule
|
||||
for rows.Next() {
|
||||
var r model.RoutingRule
|
||||
if err := rows.Scan(&r.ID, &r.Device, &r.Source, &r.Dest, &r.Provider, &r.Priority, &r.Persistent, &r.Mark, &r.Comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetRoutingRule(ctx context.Context, id int64) (model.RoutingRule, error) {
|
||||
var r model.RoutingRule
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT id, device, source, dest, provider, priority, persistent, mark, comment FROM routing_rules WHERE id = $1`, id,
|
||||
).Scan(&r.ID, &r.Device, &r.Source, &r.Dest, &r.Provider, &r.Priority, &r.Persistent, &r.Mark, &r.Comment)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return r, ErrNotFound
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
func (s *Store) CreateRoutingRule(ctx context.Context, r model.RoutingRule) (int64, error) {
|
||||
var id int64
|
||||
err := pgx.BeginFunc(ctx, s.pool, func(tx pgx.Tx) error {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO routing_rules (device, source, dest, provider, priority, persistent, mark, comment)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
|
||||
r.Device, r.Source, r.Dest, r.Provider, r.Priority, r.Persistent, r.Mark, r.Comment).Scan(&id); err != nil {
|
||||
return err
|
||||
}
|
||||
return bump(ctx, tx)
|
||||
})
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteRoutingRule(ctx context.Context, id int64) error {
|
||||
return s.deleteOne(ctx, `DELETE FROM routing_rules WHERE id = $1`, id)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+31
-3
@@ -46,6 +46,11 @@ func jsonb(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// unmarshalStrings decodes a JSONB string array into dst.
|
||||
func unmarshalStrings(data []byte, dst *[]string) error {
|
||||
return json.Unmarshal(data, dst)
|
||||
}
|
||||
|
||||
// ---- Fabrics ---------------------------------------------------------------
|
||||
|
||||
func (s *Store) ListFabrics(ctx context.Context) ([]model.Fabric, error) {
|
||||
@@ -290,10 +295,11 @@ func (s *Store) RecordDeviceStatus(ctx context.Context, name string, generation
|
||||
|
||||
func (s *Store) GetDevice(ctx context.Context, name string) (model.Device, error) {
|
||||
var d model.Device
|
||||
var resolver, settings []byte
|
||||
var resolver, settings, reachable []byte
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT name, class, COALESCE(fabric, ''), resolver, settings FROM devices WHERE name = $1`, name,
|
||||
).Scan(&d.Name, &d.Class, &d.Fabric, &resolver, &settings)
|
||||
`SELECT name, class, COALESCE(fabric, ''), resolver, settings, reachable_prefixes
|
||||
FROM devices WHERE name = $1`, name,
|
||||
).Scan(&d.Name, &d.Class, &d.Fabric, &resolver, &settings, &reachable)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return d, ErrNotFound
|
||||
}
|
||||
@@ -303,9 +309,31 @@ func (s *Store) GetDevice(ctx context.Context, name string) (model.Device, error
|
||||
if err := json.Unmarshal(resolver, &d.Resolver); err != nil {
|
||||
return d, err
|
||||
}
|
||||
if err := json.Unmarshal(reachable, &d.ReachablePrefixes); err != nil {
|
||||
return d, err
|
||||
}
|
||||
return d, json.Unmarshal(settings, &d.Settings)
|
||||
}
|
||||
|
||||
// UpdateDeviceRoutes stores the reachable prefixes an agent reports from its FIB.
|
||||
// This is scoping data, not config — it does not bump the config generation.
|
||||
func (s *Store) UpdateDeviceRoutes(ctx context.Context, name string, prefixes []string) error {
|
||||
reachable, err := jsonb(prefixes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag, err := s.pool.Exec(ctx,
|
||||
`UPDATE devices SET reachable_prefixes = $2, routes_reported_at = now() WHERE name = $1`,
|
||||
name, reachable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- Settings, portgroups, policies ----------------------------------------
|
||||
|
||||
func (s *Store) GetSettings(ctx context.Context) (model.Settings, error) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user