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.
1446 lines
32 KiB
Go
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
|
|
}
|