Files
router/tools/ipadm/store.go
T
mike 8591223aca ipadm: Port-Forwarding hinzufügen (-pa/-pl/-pd) + nftables-Include verdrahten
- ipadm verwaltet jetzt auch WAN->LAN Port-Forwards, referenziert per
  Hostname aus der bestehenden Host-DB (folgt IP-Änderungen automatisch)
- ipadm -u generiert zusätzlich /etc/nftables.d/portforward.conf, validiert
  via 'nft -c -f' und reloadet nftables (Rollback bei ungültiger Config,
  wie beim dnsmasq-Teil)
- /etc/nftables.conf bindet dafür neu /etc/nftables.d/*.conf ein
- Host-Store-Locking-Logik in generischen LineStore[T] extrahiert, von
  Host- und PortForward-Store gemeinsam genutzt
2026-08-18 11:43:29 +02:00

93 lines
2.2 KiB
Go

package main
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
)
// LineStore gives locked read-modify-write access to a flat text file with
// one record per line. Lines starting with '#' and blank lines are ignored.
// parse/format convert between a line and a record of type T.
type LineStore[T any] struct {
path string
file *os.File
header string
parse func(line string) (T, error)
format func(v T) string
}
func openLineStore[T any](path, header string, parse func(string) (T, error), format func(T) string) (*LineStore[T], error) {
if err := os.MkdirAll(dirOf(path), 0755); err != nil {
return nil, fmt.Errorf("kann Verzeichnis für %s nicht anlegen: %w", path, err)
}
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return nil, fmt.Errorf("kann %s nicht öffnen: %w", path, err)
}
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
f.Close()
return nil, fmt.Errorf("kann Lock auf %s nicht setzen: %w", path, err)
}
return &LineStore[T]{path: path, file: f, header: header, parse: parse, format: format}, nil
}
func (s *LineStore[T]) Close() error {
syscall.Flock(int(s.file.Fd()), syscall.LOCK_UN)
return s.file.Close()
}
func (s *LineStore[T]) Load() ([]T, error) {
if _, err := s.file.Seek(0, 0); err != nil {
return nil, err
}
var items []T
sc := bufio.NewScanner(s.file)
lineNo := 0
for sc.Scan() {
lineNo++
line := strings.TrimRight(sc.Text(), "\r\n")
if line == "" || strings.HasPrefix(line, "#") {
continue
}
v, err := s.parse(line)
if err != nil {
return nil, fmt.Errorf("%s:%d: %w", s.path, lineNo, err)
}
items = append(items, v)
}
if err := sc.Err(); err != nil {
return nil, err
}
return items, nil
}
func (s *LineStore[T]) Save(items []T) error {
var b strings.Builder
b.WriteString(s.header)
for _, v := range items {
b.WriteString(s.format(v))
b.WriteString("\n")
}
if err := s.file.Truncate(0); err != nil {
return err
}
if _, err := s.file.Seek(0, 0); err != nil {
return err
}
if _, err := s.file.WriteString(b.String()); err != nil {
return err
}
return s.file.Sync()
}
func dirOf(path string) string {
i := strings.LastIndexByte(path, '/')
if i <= 0 {
return "."
}
return path[:i]
}