- 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
93 lines
2.2 KiB
Go
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]
|
|
}
|