8d9a76c751
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.
173 lines
3.9 KiB
Go
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()
|
|
}
|