Files
benvin 6b600f8c8d
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Add traffic-control long-tail: mangle/accounting/tc_*
Storage + CRUD (migration 0008, model, id-keyed store, REST handlers) + compiler
rendering for mangle, accounting, and tc_devices/tc_classes/tc_filters/
tc_interfaces/tc_priorities, each owned by a device. (Nested tc option structs
deferred.)
2026-07-26 16:26:14 +10:00

425 lines
14 KiB
Go

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