Files
tomswall/internal/shorewall/convert.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

1446 lines
32 KiB
Go

package shorewall
import (
"fmt"
"strconv"
"strings"
"git.unkin.net/unkin/tomswall/internal/config"
)
// Convert reads a shorewall or shorewall6 config directory and returns a tomswall Config.
// Auto-detects IPv6 mode when shorewall6.conf is present.
func Convert(dir string) (*config.Config, error) {
ipv6 := IsIPv6Dir(dir)
return convertDir(dir, ipv6)
}
func convertDir(dir string, ipv6 bool) (*config.Config, error) {
cfg := &config.Config{
Zones: make(map[string]config.Zone),
PortGroups: make(map[string]config.PortGroup),
}
params, _ := ParseParams(dir + "/params")
if params == nil {
params = make(map[string]string)
}
if err := convertConf(dir, cfg, params, ipv6); err != nil {
confName := "shorewall.conf"
if ipv6 {
confName = "shorewall6.conf"
}
return nil, fmt.Errorf("%s: %w", confName, err)
}
if err := convertZones(dir, cfg, params); err != nil {
return nil, fmt.Errorf("zones: %w", err)
}
// $FW is a shorewall built-in that refers to the firewall zone
if _, ok := params["FW"]; !ok {
for name, z := range cfg.Zones {
if z.Type == config.ZoneFirewall {
params["FW"] = name
break
}
}
}
if len(params) > 0 {
cfg.Vars = params
}
// Shared config files (identical format for shorewall and shorewall6)
converters := []struct {
name string
fn func(string, *config.Config, map[string]string) error
}{
{"interfaces", convertInterfaces},
{"hosts", convertHosts},
{"policy", convertPolicy},
{"rules", convertRules},
{"snat", convertSNAT},
{"nat", convertNAT},
{"netmap", convertNetmap},
{"providers", convertProviders},
{"conntrack", convertConntrack},
{"blrules", convertBlrules},
{"tunnels", convertTunnels},
{"rtrules", convertRtrules},
{"stoppedrules", convertStoppedRules},
{"mangle", convertMangle},
{"accounting", convertAccounting},
{"maclist", convertMaclist},
{"routes", convertRoutes},
{"tcdevices", convertTCDevices},
{"tcclasses", convertTCClasses},
{"tcinterfaces", convertTCInterfaces},
{"tcpri", convertTCPri},
{"secmarks", convertSecmarks},
}
for _, c := range converters {
if err := c.fn(dir, cfg, params); err != nil {
return nil, fmt.Errorf("%s: %w", c.name, err)
}
}
// Family-specific converters
if ipv6 {
if err := convertProxyNDP(dir, cfg, params); err != nil {
return nil, fmt.Errorf("proxyndp: %w", err)
}
} else {
if err := convertProxyARP(dir, cfg, params); err != nil {
return nil, fmt.Errorf("proxyarp: %w", err)
}
}
return cfg, nil
}
func subst(s string, params map[string]string) string {
if !strings.Contains(s, "$") {
return s
}
return config.SubstituteVars(s, params)
}
func convertConf(dir string, cfg *config.Config, params map[string]string, ipv6 bool) error {
confFile := dir + "/shorewall.conf"
if ipv6 {
confFile = dir + "/shorewall6.conf"
}
conf, err := ParseConf(confFile)
if err != nil {
return err
}
if conf == nil {
return nil
}
cfg.Settings.TableName = "tomswall"
if ipv6 {
cfg.Settings.AddressFamily = config.FamilyIP6
} else {
cfg.Settings.AddressFamily = config.FamilyIP
}
if v, ok := conf["LOG_LEVEL"]; ok && v != "" {
cfg.Settings.LogLevel = strings.ToLower(v)
} else {
cfg.Settings.LogLevel = "info"
}
if v, ok := conf["IP_FORWARDING"]; ok {
cfg.Settings.IPForwarding = v == "Yes" || v == "On" || v == "on" || v == "Keep"
}
if v, ok := conf["IMPLICIT_CONTINUE"]; ok {
cfg.Settings.ImplicitContinue = v == "Yes"
}
return nil
}
func convertZones(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/zones")
if err != nil {
return err
}
for _, row := range rows {
name := subst(field(row, 0), params)
typeStr := subst(field(row, 1), params)
var parents []string
if idx := strings.IndexByte(name, ':'); idx > 0 {
parentStr := name[idx+1:]
name = name[:idx]
for _, p := range strings.Split(parentStr, ",") {
p = strings.TrimSpace(p)
if p != "" {
parents = append(parents, p)
}
}
}
zone := config.Zone{
Type: convertZoneType(typeStr),
}
if len(parents) > 0 {
zone.Parents = parents
}
opts := subst(field(row, 2), params)
if !isDash(opts) {
zone.Options = splitOptions(opts)
}
inOpts := subst(field(row, 3), params)
if !isDash(inOpts) {
zone.InOptions = splitOptions(inOpts)
}
outOpts := subst(field(row, 4), params)
if !isDash(outOpts) {
zone.OutOptions = splitOptions(outOpts)
}
cfg.Zones[name] = zone
}
return nil
}
func convertZoneType(t string) config.ZoneType {
switch strings.ToLower(strings.TrimSpace(t)) {
case "firewall", "fw":
return config.ZoneFirewall
case "ipv4", "ip", "ipv6":
return config.ZoneIP
case "ipsec", "ipsec4", "ipsec6":
return config.ZoneIPSec
case "bport", "bport4", "bport6":
return config.ZoneBPort
case "loopback":
return config.ZoneLoopback
case "local":
return config.ZoneLocal
default:
return config.ZoneIP
}
}
func splitOptions(s string) []string {
var opts []string
for _, o := range strings.Split(s, ",") {
o = strings.TrimSpace(o)
if o != "" {
opts = append(opts, o)
}
}
return opts
}
func convertInterfaces(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/interfaces")
if err != nil {
return err
}
for _, row := range rows {
zone := subst(field(row, 0), params)
iface := subst(field(row, 1), params)
intf := config.Interface{
Zone: zone,
Interface: iface,
}
optsStr := subst(field(row, 2), params)
if !isDash(optsStr) {
intf.Options = parseInterfaceOptions(optsStr)
}
cfg.Interfaces = append(cfg.Interfaces, intf)
}
return nil
}
func parseInterfaceOptions(s string) config.InterfaceOptions {
var opts config.InterfaceOptions
for _, o := range strings.Split(s, ",") {
o = strings.TrimSpace(o)
if o == "" {
continue
}
key, val := splitKV(o)
switch key {
case "dhcp":
opts.DHCP = true
case "tcpflags":
b := true
opts.TCPFlags = &b
case "nosmurfs":
opts.NoSmurfs = true
case "routeback":
b := true
opts.RouteBack = &b
case "bridge":
opts.Bridge = true
case "destonly":
opts.DestOnly = true
case "optional":
opts.Optional = true
case "required":
opts.Required = true
case "physical":
opts.Physical = val
case "routefilter":
if val != "" {
n, _ := strconv.Atoi(val)
opts.RouteFilter = &n
} else {
n := 1
opts.RouteFilter = &n
}
case "logmartians":
b := true
opts.LogMartians = &b
case "arp_filter":
b := true
opts.ArpFilter = &b
case "arp_ignore":
if val != "" {
n, _ := strconv.Atoi(val)
opts.ArpIgnore = &n
}
case "proxyarp":
b := true
opts.ProxyArp = &b
case "sourceroute":
b := true
opts.SourceRoute = &b
case "upnp":
opts.Upnp = true
case "wait":
if val != "" {
n, _ := strconv.Atoi(val)
opts.Wait = n
} else {
opts.Wait = 1
}
case "mss":
if val != "" {
n, _ := strconv.Atoi(val)
opts.MSS = n
}
case "nets":
if val != "" {
for _, n := range strings.Split(val, ",") {
n = strings.TrimSpace(n)
if n != "" {
opts.Nets = append(opts.Nets, n)
}
}
}
case "unmanaged":
opts.Unmanaged = true
case "accept_ra":
if val != "" {
n, _ := strconv.Atoi(val)
opts.AcceptRA = &n
} else {
n := 1
opts.AcceptRA = &n
}
}
}
return opts
}
func splitHelperChain(s string) (string, string) {
idx := strings.LastIndexByte(s, ':')
if idx < 0 {
return s, ""
}
suffix := s[idx+1:]
suffixLower := strings.ToLower(suffix)
switch suffixLower {
case "p":
return s[:idx], "prerouting"
case "o":
return s[:idx], "output"
case "po", "op":
return s[:idx], "both"
}
return s, ""
}
func splitKV(s string) (string, string) {
idx := strings.IndexByte(s, '=')
if idx < 0 {
return strings.ToLower(s), ""
}
return strings.ToLower(s[:idx]), s[idx+1:]
}
func convertHosts(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/hosts")
if err != nil {
return err
}
for _, row := range rows {
zone := subst(field(row, 0), params)
hostDef := subst(field(row, 1), params)
iface, addrs := splitHostDef(hostDef)
host := config.Host{
Zone: zone,
Interface: iface,
Addresses: addrs,
}
optsStr := subst(field(row, 2), params)
if !isDash(optsStr) {
host.Options = parseHostOptions(optsStr)
}
cfg.Hosts = append(cfg.Hosts, host)
}
return nil
}
func splitHostDef(s string) (string, []string) {
idx := strings.IndexByte(s, ':')
if idx < 0 {
return s, nil
}
iface := s[:idx]
addrPart := s[idx+1:]
var addrs []string
for _, a := range strings.Split(addrPart, ",") {
a = strings.TrimSpace(a)
if a != "" {
addrs = append(addrs, a)
}
}
return iface, addrs
}
func parseHostOptions(s string) config.HostOptions {
var opts config.HostOptions
for _, o := range strings.Split(s, ",") {
o = strings.TrimSpace(o)
switch strings.ToLower(o) {
case "broadcast":
opts.Broadcast = true
case "destonly":
opts.DestOnly = true
case "ipsec":
opts.IPSec = true
case "nosmurfs":
opts.NoSmurfs = true
case "routeback":
opts.RouteBack = true
case "tcpflags":
opts.TCPFlags = true
}
key, val := splitKV(o)
if key == "mss" && val != "" {
n, _ := strconv.Atoi(val)
opts.MSS = n
}
}
return opts
}
func convertPolicy(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/policy")
if err != nil {
return err
}
for _, row := range rows {
source := subst(field(row, 0), params)
dest := subst(field(row, 1), params)
action := subst(field(row, 2), params)
logLevel := subst(field(row, 3), params)
pol := config.Policy{
Source: strings.ToLower(source),
Dest: strings.ToLower(dest),
Action: config.PolicyAction(strings.ToLower(action)),
}
if !isDash(logLevel) {
pol.Log = logLevel
}
burstLimit := subst(field(row, 4), params)
if !isDash(burstLimit) {
pol.RateLimit = burstLimit
}
connLimit := subst(field(row, 5), params)
if !isDash(connLimit) {
pol.ConnLimit = connLimit
}
cfg.Policy = append(cfg.Policy, pol)
}
return nil
}
func convertRules(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/rules")
if err != nil {
return err
}
currentSection := ""
for _, row := range rows {
if row[0] == "?SECTION" {
currentSection = strings.ToLower(field(row, 1))
continue
}
actionStr := subst(field(row, 0), params)
source := subst(field(row, 1), params)
dest := subst(field(row, 2), params)
proto := subst(field(row, 3), params)
action, logLevel := splitActionLog(actionStr)
rule := config.Rule{
Action: config.RuleAction(strings.ToLower(action)),
Source: source,
Dest: dest,
}
if currentSection != "" && currentSection != "all" {
rule.Section = config.RuleSection(currentSection)
}
if logLevel != "" {
rule.Log = logLevel
}
if !isDash(proto) {
rule.Proto = strings.ToLower(proto)
}
dport := subst(field(row, 4), params)
if !isDash(dport) {
rule.DPort = parsePortSpec(dport)
}
sport := subst(field(row, 5), params)
if !isDash(sport) {
rule.SPort = parsePortSpec(sport)
}
origdest := subst(field(row, 6), params)
if !isDash(origdest) {
rule.OrigDest = origdest
}
rate := subst(field(row, 7), params)
if !isDash(rate) {
rule.RateLimit = rate
}
user := subst(field(row, 8), params)
if !isDash(user) {
rule.User = user
}
mark := subst(field(row, 9), params)
if !isDash(mark) {
rule.Mark = mark
}
connLimit := subst(field(row, 10), params)
if !isDash(connLimit) {
rule.ConnLimit = connLimit
}
helper := subst(field(row, 14), params)
if !isDash(helper) {
rule.Helper = helper
}
cfg.Rules = append(cfg.Rules, rule)
}
return nil
}
func splitActionLog(s string) (action, logLevel string) {
idx := strings.IndexByte(s, ':')
if idx < 0 {
return s, ""
}
return s[:idx], s[idx+1:]
}
func parsePortSpec(s string) config.PortSpec {
var ports config.PortSpec
for _, p := range strings.Split(s, ",") {
p = strings.TrimSpace(p)
if p != "" {
ports = append(ports, p)
}
}
return ports
}
func convertSNAT(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/snat")
if err != nil {
return err
}
for _, row := range rows {
actionStr := subst(field(row, 0), params)
action, logLevel := splitActionLog(actionStr)
snat := config.SNATRule{
Action: config.SNATAction(strings.ToLower(action)),
}
if logLevel != "" {
snat.Log = logLevel
}
source := subst(field(row, 1), params)
if !isDash(source) {
snat.Source = source
}
dest := subst(field(row, 2), params)
if !isDash(dest) {
snat.Dest = dest
}
address := subst(field(row, 3), params)
if !isDash(address) {
snat.Address = address
}
proto := subst(field(row, 4), params)
if !isDash(proto) {
snat.Proto = strings.ToLower(proto)
}
dport := subst(field(row, 5), params)
if !isDash(dport) {
snat.DPort = parsePortSpec(dport)
}
sport := subst(field(row, 6), params)
if !isDash(sport) {
snat.SPort = parsePortSpec(sport)
}
mark := subst(field(row, 7), params)
if !isDash(mark) {
snat.Mark = mark
}
cfg.SNAT = append(cfg.SNAT, snat)
}
return nil
}
func convertNAT(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/nat")
if err != nil {
return err
}
for _, row := range rows {
nat := config.StaticNAT{
External: subst(field(row, 0), params),
Interface: subst(field(row, 1), params),
Internal: subst(field(row, 2), params),
}
allIfaces := subst(field(row, 3), params)
if strings.EqualFold(allIfaces, "yes") {
nat.AllInterfaces = true
}
local := subst(field(row, 4), params)
if strings.EqualFold(local, "yes") {
nat.Local = true
}
cfg.StaticNAT = append(cfg.StaticNAT, nat)
}
return nil
}
func convertNetmap(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/netmap")
if err != nil {
return err
}
for _, row := range rows {
nm := config.Netmap{
Type: config.NetmapType(strings.ToLower(subst(field(row, 0), params))),
Net1: subst(field(row, 1), params),
Interface: subst(field(row, 2), params),
Net2: subst(field(row, 3), params),
}
net3 := subst(field(row, 4), params)
if !isDash(net3) {
nm.Net3 = net3
}
proto := subst(field(row, 5), params)
if !isDash(proto) {
nm.Proto = strings.ToLower(proto)
}
dport := subst(field(row, 6), params)
if !isDash(dport) {
nm.DPort = parsePortSpec(dport)
}
sport := subst(field(row, 7), params)
if !isDash(sport) {
nm.SPort = parsePortSpec(sport)
}
cfg.Netmap = append(cfg.Netmap, nm)
}
return nil
}
func convertProviders(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/providers")
if err != nil {
return err
}
for _, row := range rows {
name := subst(field(row, 0), params)
numberStr := subst(field(row, 1), params)
number, _ := strconv.Atoi(numberStr)
prov := config.Provider{
Name: name,
Number: number,
}
markStr := subst(field(row, 2), params)
if !isDash(markStr) {
mark, _ := strconv.ParseInt(markStr, 0, 64)
prov.Mark = int(mark)
}
dup := subst(field(row, 3), params)
if !isDash(dup) {
prov.Duplicate = dup
}
iface := subst(field(row, 4), params)
if !isDash(iface) {
prov.Interface = iface
}
gw := subst(field(row, 5), params)
if !isDash(gw) {
prov.Gateway = gw
}
optsStr := subst(field(row, 6), params)
if !isDash(optsStr) {
prov.Options = parseProviderOptions(optsStr)
}
copyStr := subst(field(row, 7), params)
if !isDash(copyStr) {
for _, c := range strings.Split(copyStr, ",") {
c = strings.TrimSpace(c)
if c != "" {
prov.Copy = append(prov.Copy, c)
}
}
}
cfg.Providers = append(cfg.Providers, prov)
}
return nil
}
func parseProviderOptions(s string) config.ProviderOptions {
var opts config.ProviderOptions
for _, o := range strings.Split(s, ",") {
o = strings.TrimSpace(o)
key, val := splitKV(o)
switch key {
case "track":
opts.Track = true
case "balance":
if val != "" {
n, _ := strconv.Atoi(val)
opts.Balance = n
} else {
opts.Balance = 1
}
case "load":
if val != "" {
f, _ := strconv.ParseFloat(val, 64)
opts.Load = f
}
case "loose":
opts.Loose = true
case "fallback":
if val != "" {
n, _ := strconv.Atoi(val)
opts.Fallback = n
} else {
opts.Fallback = 1
}
case "primary":
opts.Primary = true
case "src":
opts.Src = val
case "mtu":
if val != "" {
n, _ := strconv.Atoi(val)
opts.MTU = n
}
case "tproxy":
opts.TProxy = true
case "optional":
opts.Optional = true
case "persistent":
opts.Persistent = true
}
}
return opts
}
func convertConntrack(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/conntrack")
if err != nil {
return err
}
for _, row := range rows {
actionStr := subst(field(row, 0), params)
ct := config.ConntrackRule{}
actionLower := strings.ToLower(actionStr)
if strings.HasPrefix(actionLower, "ct:helper:") {
ct.Action = config.ConntrackHelper
helper := actionStr[10:]
helper, chain := splitHelperChain(helper)
ct.Helper = helper
if chain != "" {
ct.Chain = config.ConntrackChain(chain)
}
} else if strings.HasPrefix(actionLower, "ct:") {
ct.Action = config.ConntrackHelper
helper := actionStr[3:]
helper, chain := splitHelperChain(helper)
ct.Helper = helper
if chain != "" {
ct.Chain = config.ConntrackChain(chain)
}
} else {
ct.Action = config.ConntrackAction(actionLower)
}
source := subst(field(row, 1), params)
if !isDash(source) {
ct.Source = source
}
dest := subst(field(row, 2), params)
if !isDash(dest) {
ct.Dest = dest
}
proto := subst(field(row, 3), params)
if !isDash(proto) {
ct.Proto = strings.ToLower(proto)
}
dport := subst(field(row, 4), params)
if !isDash(dport) {
ct.DPort = parsePortSpec(dport)
}
sport := subst(field(row, 5), params)
if !isDash(sport) {
ct.SPort = parsePortSpec(sport)
}
user := subst(field(row, 6), params)
if !isDash(user) {
ct.User = user
}
cfg.Conntrack = append(cfg.Conntrack, ct)
}
return nil
}
func convertBlrules(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/blrules")
if err != nil {
return err
}
for _, row := range rows {
actionStr := subst(field(row, 0), params)
action, logLevel := splitActionLog(actionStr)
rule := config.BlruleRule{
Action: config.BlruleAction(strings.ToLower(action)),
Source: subst(field(row, 1), params),
Dest: subst(field(row, 2), params),
}
if logLevel != "" {
rule.Log = logLevel
}
proto := subst(field(row, 3), params)
if !isDash(proto) {
rule.Proto = strings.ToLower(proto)
}
dport := subst(field(row, 4), params)
if !isDash(dport) {
rule.DPort = parsePortSpec(dport)
}
sport := subst(field(row, 5), params)
if !isDash(sport) {
rule.SPort = parsePortSpec(sport)
}
cfg.Blrules = append(cfg.Blrules, rule)
}
return nil
}
func convertTunnels(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/tunnels")
if err != nil {
return err
}
for _, row := range rows {
typeStr := subst(field(row, 0), params)
zone := subst(field(row, 1), params)
gwStr := subst(field(row, 2), params)
var gateways []string
for _, gw := range strings.Split(gwStr, ",") {
gw = strings.TrimSpace(gw)
if gw != "" {
gateways = append(gateways, gw)
}
}
tunnel := config.Tunnel{
Type: typeStr,
Zone: zone,
Gateways: gateways,
}
gwZones := subst(field(row, 3), params)
if !isDash(gwZones) {
for _, gz := range strings.Split(gwZones, ",") {
gz = strings.TrimSpace(gz)
if gz != "" {
tunnel.GatewayZones = append(tunnel.GatewayZones, gz)
}
}
}
cfg.Tunnels = append(cfg.Tunnels, tunnel)
}
return nil
}
func convertRtrules(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/rtrules")
if err != nil {
return err
}
for _, row := range rows {
source := subst(field(row, 0), params)
dest := subst(field(row, 1), params)
provider := subst(field(row, 2), params)
priorityStr := subst(field(row, 3), params)
priority, _ := strconv.Atoi(strings.TrimSuffix(priorityStr, "!"))
rule := config.RoutingRule{
Provider: provider,
Priority: priority,
}
if !isDash(source) {
rule.Source = source
}
if !isDash(dest) {
rule.Dest = dest
}
if strings.HasSuffix(priorityStr, "!") {
rule.Persistent = true
}
mark := subst(field(row, 4), params)
if !isDash(mark) {
rule.Mark = mark
}
cfg.RoutingRules = append(cfg.RoutingRules, rule)
}
return nil
}
func convertStoppedRules(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/stoppedrules")
if err != nil {
return err
}
for _, row := range rows {
action := subst(field(row, 0), params)
rule := config.StoppedRule{
Action: config.StoppedAction(strings.ToLower(action)),
}
source := subst(field(row, 1), params)
if !isDash(source) {
rule.Source = source
}
dest := subst(field(row, 2), params)
if !isDash(dest) {
rule.Dest = dest
}
proto := subst(field(row, 3), params)
if !isDash(proto) {
rule.Proto = strings.ToLower(proto)
}
dport := subst(field(row, 4), params)
if !isDash(dport) {
rule.DPort = parsePortSpec(dport)
}
sport := subst(field(row, 5), params)
if !isDash(sport) {
rule.SPort = parsePortSpec(sport)
}
cfg.StoppedRules = append(cfg.StoppedRules, rule)
}
return nil
}
func convertMangle(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/mangle")
if err != nil {
return err
}
for _, row := range rows {
actionStr := subst(field(row, 0), params)
rule := config.MangleRule{}
actionParts := strings.SplitN(actionStr, "(", 2)
base := actionParts[0]
var chain string
if idx := strings.LastIndexByte(base, ':'); idx > 0 {
chain = strings.ToLower(base[idx+1:])
base = base[:idx]
}
markVal := ""
if len(actionParts) > 1 {
markVal = strings.TrimSuffix(actionParts[1], ")")
}
rule.Action = config.MangleAction(strings.ToLower(base))
if chain != "" {
rule.Chain = config.MangleChain(chain)
}
if markVal != "" {
rule.MarkValue = markVal
}
source := subst(field(row, 1), params)
if !isDash(source) {
rule.Source = source
}
dest := subst(field(row, 2), params)
if !isDash(dest) {
rule.Dest = dest
}
proto := subst(field(row, 3), params)
if !isDash(proto) {
rule.Proto = strings.ToLower(proto)
}
dport := subst(field(row, 4), params)
if !isDash(dport) {
rule.DPort = parsePortSpec(dport)
}
sport := subst(field(row, 5), params)
if !isDash(sport) {
rule.SPort = parsePortSpec(sport)
}
user := subst(field(row, 6), params)
if !isDash(user) {
rule.User = user
}
test := subst(field(row, 7), params)
if !isDash(test) {
rule.Mark = test
}
length := subst(field(row, 8), params)
if !isDash(length) {
rule.Length = length
}
tos := subst(field(row, 9), params)
if !isDash(tos) {
rule.TOS = tos
}
helper := subst(field(row, 11), params)
if !isDash(helper) {
rule.Helper = helper
}
prob := subst(field(row, 12), params)
if !isDash(prob) {
f, _ := strconv.ParseFloat(prob, 64)
rule.Probability = f
}
dscp := subst(field(row, 13), params)
if !isDash(dscp) {
rule.DSCP = dscp
}
cfg.Mangle = append(cfg.Mangle, rule)
}
return nil
}
func convertAccounting(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/accounting")
if err != nil {
return err
}
currentSection := ""
for _, row := range rows {
if row[0] == "?SECTION" {
currentSection = strings.ToLower(field(row, 1))
continue
}
actionStr := subst(field(row, 0), params)
rule := config.AccountingRule{
Action: config.AccountingAction(strings.ToLower(actionStr)),
}
if currentSection != "" {
rule.Section = config.AccountingSection(currentSection)
}
chain := subst(field(row, 1), params)
if !isDash(chain) {
rule.Chain = chain
}
source := subst(field(row, 2), params)
if !isDash(source) {
rule.Source = source
}
dest := subst(field(row, 3), params)
if !isDash(dest) {
rule.Dest = dest
}
proto := subst(field(row, 4), params)
if !isDash(proto) {
rule.Proto = strings.ToLower(proto)
}
dport := subst(field(row, 5), params)
if !isDash(dport) {
rule.DPort = parsePortSpec(dport)
}
sport := subst(field(row, 6), params)
if !isDash(sport) {
rule.SPort = parsePortSpec(sport)
}
mark := subst(field(row, 8), params)
if !isDash(mark) {
rule.Mark = mark
}
cfg.Accounting = append(cfg.Accounting, rule)
}
return nil
}
func convertMaclist(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/maclist")
if err != nil {
return err
}
for _, row := range rows {
actionStr := subst(field(row, 0), params)
action, logLevel := splitActionLog(actionStr)
entry := config.MaclistEntry{
Action: config.MaclistAction(strings.ToLower(action)),
Interface: subst(field(row, 1), params),
MAC: subst(field(row, 2), params),
}
if logLevel != "" {
entry.Log = logLevel
}
addrs := subst(field(row, 3), params)
if !isDash(addrs) {
for _, a := range strings.Split(addrs, ",") {
a = strings.TrimSpace(a)
if a != "" {
entry.Addresses = append(entry.Addresses, a)
}
}
}
cfg.Maclist = append(cfg.Maclist, entry)
}
return nil
}
func convertProxyARP(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/proxyarp")
if err != nil {
return err
}
for _, row := range rows {
entry := config.ProxyARP{
Address: subst(field(row, 0), params),
Interface: subst(field(row, 1), params),
External: subst(field(row, 2), params),
}
haveRoute := subst(field(row, 3), params)
if strings.EqualFold(haveRoute, "yes") {
entry.HaveRoute = true
}
persistent := subst(field(row, 4), params)
if strings.EqualFold(persistent, "yes") {
entry.Persistent = true
}
cfg.ProxyARP = append(cfg.ProxyARP, entry)
}
return nil
}
func convertRoutes(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/routes")
if err != nil {
return err
}
for _, row := range rows {
route := config.StaticRoute{
Provider: subst(field(row, 0), params),
Dest: subst(field(row, 1), params),
}
gw := subst(field(row, 2), params)
if !isDash(gw) {
route.Gateway = gw
}
dev := subst(field(row, 3), params)
if !isDash(dev) {
route.Device = dev
}
cfg.Routes = append(cfg.Routes, route)
}
return nil
}
func convertTCDevices(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/tcdevices")
if err != nil {
return err
}
for _, row := range rows {
dev := config.TCDevice{
Interface: subst(field(row, 0), params),
InBandwidth: subst(field(row, 1), params),
OutBandwidth: subst(field(row, 2), params),
}
if isDash(dev.InBandwidth) {
dev.InBandwidth = ""
}
opts := subst(field(row, 3), params)
if !isDash(opts) {
dev.Options = parseTCDeviceOptions(opts)
}
cfg.TCDevices = append(cfg.TCDevices, dev)
}
return nil
}
func parseTCDeviceOptions(s string) config.TCDeviceOptions {
var opts config.TCDeviceOptions
for _, o := range strings.Split(s, ",") {
o = strings.TrimSpace(o)
switch strings.ToLower(o) {
case "classify":
opts.Classify = true
case "htb":
opts.HTB = true
case "hfsc":
opts.HFSC = true
default:
key, val := splitKV(o)
if key == "linklayer" {
opts.Linklayer = val
}
}
}
return opts
}
func convertTCClasses(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/tcclasses")
if err != nil {
return err
}
for _, row := range rows {
cls := config.TCClass{
Interface: subst(field(row, 0), params),
Rate: subst(field(row, 2), params),
}
markStr := subst(field(row, 1), params)
if !isDash(markStr) {
mark, _ := strconv.Atoi(markStr)
cls.Mark = mark
}
ceil := subst(field(row, 3), params)
if !isDash(ceil) {
cls.Ceil = ceil
}
priStr := subst(field(row, 4), params)
if !isDash(priStr) {
pri, _ := strconv.Atoi(priStr)
cls.Priority = pri
}
opts := subst(field(row, 5), params)
if !isDash(opts) {
cls.Options = parseTCClassOptions(opts)
}
cfg.TCClasses = append(cfg.TCClasses, cls)
}
return nil
}
func parseTCClassOptions(s string) config.TCClassOptions {
var opts config.TCClassOptions
for _, o := range strings.Split(s, ",") {
switch strings.ToLower(strings.TrimSpace(o)) {
case "default":
opts.Default = true
case "tcp-ack":
opts.TCPAck = true
case "pfifo":
opts.Pfifo = true
}
}
return opts
}
func convertTCInterfaces(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/tcinterfaces")
if err != nil {
return err
}
for _, row := range rows {
tci := config.TCInterface{
Interface: subst(field(row, 0), params),
}
typeStr := subst(field(row, 1), params)
if !isDash(typeStr) {
tci.Type = strings.ToLower(typeStr)
}
inBw := subst(field(row, 2), params)
if !isDash(inBw) {
tci.InBandwidth = inBw
}
outBw := subst(field(row, 3), params)
if !isDash(outBw) {
tci.OutBandwidth = outBw
}
cfg.TCInterfaces = append(cfg.TCInterfaces, tci)
}
return nil
}
func convertTCPri(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/tcpri")
if err != nil {
return err
}
for _, row := range rows {
bandStr := subst(field(row, 0), params)
band, _ := strconv.Atoi(bandStr)
tp := config.TCPriority{
Band: band,
}
proto := subst(field(row, 1), params)
if !isDash(proto) {
tp.Proto = strings.ToLower(proto)
}
dport := subst(field(row, 2), params)
if !isDash(dport) {
tp.DPort = parsePortSpec(dport)
}
sport := subst(field(row, 3), params)
if !isDash(sport) {
tp.SPort = parsePortSpec(sport)
}
addr := subst(field(row, 4), params)
if !isDash(addr) {
tp.Address = addr
}
iface := subst(field(row, 5), params)
if !isDash(iface) {
tp.Interface = iface
}
helper := subst(field(row, 6), params)
if !isDash(helper) {
tp.Helper = helper
}
cfg.TCPriorities = append(cfg.TCPriorities, tp)
}
return nil
}
func convertSecmarks(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/secmarks")
if err != nil {
return err
}
for _, row := range rows {
sm := config.SecmarkRule{
Secmark: subst(field(row, 0), params),
Chain: subst(field(row, 1), params),
}
source := subst(field(row, 2), params)
if !isDash(source) {
sm.Source = source
}
dest := subst(field(row, 3), params)
if !isDash(dest) {
sm.Dest = dest
}
proto := subst(field(row, 4), params)
if !isDash(proto) {
sm.Proto = strings.ToLower(proto)
}
dport := subst(field(row, 5), params)
if !isDash(dport) {
sm.DPort = parsePortSpec(dport)
}
sport := subst(field(row, 6), params)
if !isDash(sport) {
sm.SPort = parsePortSpec(sport)
}
cfg.Secmarks = append(cfg.Secmarks, sm)
}
return nil
}
func convertProxyNDP(dir string, cfg *config.Config, params map[string]string) error {
rows, err := ParseFile(dir + "/proxyndp")
if err != nil {
return err
}
for _, row := range rows {
entry := config.ProxyNDP{
Address: subst(field(row, 0), params),
Interface: subst(field(row, 1), params),
External: subst(field(row, 2), params),
}
haveRoute := subst(field(row, 3), params)
if strings.EqualFold(haveRoute, "yes") {
entry.HaveRoute = true
}
persistent := subst(field(row, 4), params)
if strings.EqualFold(persistent, "yes") {
entry.Persistent = true
}
cfg.ProxyNDP = append(cfg.ProxyNDP, entry)
}
return nil
}