Files
tomswall/internal/shorewall/parser.go
T
unkinben 8d9a76c751 Add comprehensive nftables compiler with shorewall feature parity
Rewrites the compiler from ~440 to ~1700 lines covering all major shorewall
firewall features: loopback, conntrack fast-path, anti-spoof, DHCP, intra-zone,
blacklist/whitelist, conntrack notrack, tunnels (13 types), rules with sections,
DNAT/redirect, SNAT/masquerade, static NAT, policies with zone exclusions,
MSS clamping, rate limiting, connection limiting, negated addresses, ICMP type
matching, TCP RST reject, user/UID matching, mark match/set, NFQUEUE, NONAT,
and policy-level rate/conn limiting.

Adds full config types for all shorewall subsystems (mangle, accounting, maclist,
netmap, providers, tunnels, conntrack, blrules, proxyarp/ndp, routes, tc, secmarks),
shorewall migration tooling, expanded CLI commands, expression-level diff engine,
and 49 unit tests.
2026-07-01 23:56:44 +10:00

173 lines
3.9 KiB
Go

package shorewall
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
)
// ParseFile reads a shorewall columnar config file and returns rows of fields.
// Handles comments (#), blank lines, line continuation (\), and ?COMMENT directives.
func ParseFile(path string) ([][]string, error) {
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("opening %s: %w", path, err)
}
defer f.Close()
var rows [][]string
var continuation string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if strings.HasSuffix(line, "\\") {
continuation += strings.TrimSuffix(line, "\\") + " "
continue
}
if continuation != "" {
line = continuation + line
continuation = ""
}
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "SECTION") || strings.HasPrefix(line, "?SECTION") {
raw := strings.TrimPrefix(line, "?")
rows = append(rows, []string{"?SECTION", strings.TrimSpace(strings.TrimPrefix(raw, "SECTION"))})
continue
}
if strings.HasPrefix(line, "?") {
continue
}
fields := splitFields(line)
if len(fields) > 0 {
rows = append(rows, fields)
}
}
return rows, scanner.Err()
}
// splitFields splits a shorewall config line into fields.
// Fields are whitespace-separated, but supports the { key=value ... } alternate syntax.
func splitFields(line string) []string {
var fields []string
for _, f := range strings.Fields(line) {
if f == "#" {
break
}
if strings.HasPrefix(f, "#") {
break
}
fields = append(fields, f)
}
return fields
}
// ParseConf reads a shorewall.conf (key=value) file into a map.
func ParseConf(path string) (map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
conf := make(map[string]string)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
idx := strings.IndexByte(line, '=')
if idx < 0 {
continue
}
key := strings.TrimSpace(line[:idx])
val := strings.TrimSpace(line[idx+1:])
val = strings.Trim(val, "\"'")
if key != "" {
conf[key] = val
}
}
return conf, scanner.Err()
}
// ParseParams reads a shorewall params file and extracts variable assignments.
// This is simplified — it handles VAR=value lines but not full shell evaluation.
func ParseParams(path string) (map[string]string, error) {
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
defer f.Close()
params := make(map[string]string)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
idx := strings.IndexByte(line, '=')
if idx < 0 {
continue
}
key := strings.TrimSpace(line[:idx])
val := strings.TrimSpace(line[idx+1:])
val = strings.Trim(val, "\"'")
if key != "" && !strings.ContainsAny(key, " \t$(){}") {
params[key] = val
}
}
return params, scanner.Err()
}
// field returns the nth field (0-based) from a row, or "-" if missing.
func field(row []string, n int) string {
if n >= len(row) {
return "-"
}
return row[n]
}
// isDash returns true if the field is empty or a dash.
func isDash(s string) bool {
return s == "" || s == "-"
}
// DirExists checks if a shorewall or shorewall6 config directory looks valid.
func DirExists(dir string) bool {
for _, name := range []string{"zones", "shorewall.conf", "shorewall6.conf"} {
info, err := os.Stat(filepath.Join(dir, name))
if err == nil && !info.IsDir() {
return true
}
}
return false
}
// IsIPv6Dir returns true if the directory contains a shorewall6 config.
func IsIPv6Dir(dir string) bool {
info, err := os.Stat(filepath.Join(dir, "shorewall6.conf"))
return err == nil && !info.IsDir()
}