diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 153b23a..e19ba00 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -45,6 +45,13 @@ type Input struct { ProxyNDP []model.ProxyEntry ArpRules []model.ArpRule Maclist []model.MaclistEntry + Mangle []model.MangleRule + Accounting []model.AccountingRule + TCDevices []model.TCDevice + TCClasses []model.TCClass + TCFilters []model.TCFilter + TCInterfaces []model.TCInterface + TCPriorities []model.TCPriority } // RenderedConfig is the per-device output served to the agent. @@ -72,6 +79,13 @@ type RenderedConfig struct { ProxyNDP []RenderedProxy `yaml:"proxy_ndp,omitempty" json:"proxy_ndp,omitempty"` ArpRules []RenderedArpRule `yaml:"arp_rules,omitempty" json:"arp_rules,omitempty"` Maclist []RenderedMaclist `yaml:"maclist,omitempty" json:"maclist,omitempty"` + Mangle []RenderedMangle `yaml:"mangle,omitempty" json:"mangle,omitempty"` + Accounting []RenderedAccounting `yaml:"accounting,omitempty" json:"accounting,omitempty"` + TCDevices []RenderedTCDevice `yaml:"tc_devices,omitempty" json:"tc_devices,omitempty"` + TCClasses []RenderedTCClass `yaml:"tc_classes,omitempty" json:"tc_classes,omitempty"` + TCFilters []RenderedTCFilter `yaml:"tc_filters,omitempty" json:"tc_filters,omitempty"` + TCInterfaces []RenderedTCInterface `yaml:"tc_interfaces,omitempty" json:"tc_interfaces,omitempty"` + TCPriorities []RenderedTCPriority `yaml:"tc_priorities,omitempty" json:"tc_priorities,omitempty"` } // RenderedSNAT is a resolved SNAT/masquerade rule: source addresses masqueraded @@ -220,6 +234,7 @@ func Render(in Input) (*RenderedConfig, error) { // Per-device long-tail sections owned by this device. renderPerDevice(in, out) renderPerDeviceL2(in, out) + renderTraffic(in, out) return out, nil } @@ -480,5 +495,26 @@ func Compile(ctx context.Context, s *store.Store, device string) (*RenderedConfi if in.Maclist, err = s.ListMaclist(ctx); err != nil { return nil, err } + if in.Mangle, err = s.ListMangle(ctx); err != nil { + return nil, err + } + if in.Accounting, err = s.ListAccounting(ctx); err != nil { + return nil, err + } + if in.TCDevices, err = s.ListTCDevices(ctx); err != nil { + return nil, err + } + if in.TCClasses, err = s.ListTCClasses(ctx); err != nil { + return nil, err + } + if in.TCFilters, err = s.ListTCFilters(ctx); err != nil { + return nil, err + } + if in.TCInterfaces, err = s.ListTCInterfaces(ctx); err != nil { + return nil, err + } + if in.TCPriorities, err = s.ListTCPriorities(ctx); err != nil { + return nil, err + } return Render(in) } diff --git a/internal/compiler/traffic.go b/internal/compiler/traffic.go new file mode 100644 index 0000000..04d1ea5 --- /dev/null +++ b/internal/compiler/traffic.go @@ -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, + }) + } +} diff --git a/internal/database/migrations/0008_traffic_control.sql b/internal/database/migrations/0008_traffic_control.sql new file mode 100644 index 0000000..c74ac50 --- /dev/null +++ b/internal/database/migrations/0008_traffic_control.sql @@ -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 '' +); diff --git a/internal/model/traffic.go b/internal/model/traffic.go new file mode 100644 index 0000000..9a259be --- /dev/null +++ b/internal/model/traffic.go @@ -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"` +} diff --git a/internal/server/resources.go b/internal/server/resources.go index 8384baf..748b4ff 100644 --- a/internal/server/resources.go +++ b/internal/server/resources.go @@ -63,6 +63,7 @@ func (s *Server) mountResources(r chi.Router) { s.mountLongtail(r) s.mountPerDevice(r) s.mountPerDeviceL2(r) + s.mountTraffic(r) } // respondOne writes a single resource, mapping ErrNotFound to 404. diff --git a/internal/server/traffic.go b/internal/server/traffic.go new file mode 100644 index 0000000..f142860 --- /dev/null +++ b/internal/server/traffic.go @@ -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)) +} diff --git a/internal/store/traffic.go b/internal/store/traffic.go new file mode 100644 index 0000000..afdf892 --- /dev/null +++ b/internal/store/traffic.go @@ -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) +}