Introduce TCPUDP as the protocol analog of FamilyAny and DirAny: a merged value spanning both transports, distinct from ProtocolAny (which matches every IP protocol and carries no port). Backends whose native syntax holds both transports in one row (nftables, ufw, apf) store and read it as one rule; the rest fan it out with expandProtocols. Removing one transport of a merged row splits it via splitMergedRow, which composes the family and protocol splits so an nftables row merged on both axes leaves a correct, non-overlapping remainder. NAT rejects TCPUDP with ErrUnsupportedNAT. Remove read-side merging. GetRules now reports the firewall's actual rows and never synthesizes a FamilyAny, TCPUDP, or DirAny rule by pairing up separately-stored ones, so mergeFamilies, mergeDirections and their helpers are gone and mergedInsertIndex becomes logicalInsertIndex. Rules are instead compared by coverage: the new exported Rule.Covers / Rule.CoveredBy (and the NATRule pair) expand a rule across family, transport and direction and decide containment cell by cell, which is what lets Sync stay a no-op against its own output whichever representation a backend chose. Extract the systemd/SysV service helpers out of the iptables backend into services.go so every Linux backend shares one implementation, and document the multi-state rule model and the coverage helpers in the README.
266 lines
8.6 KiB
Go
266 lines
8.6 KiB
Go
//go:build linux
|
|
|
|
package firewall
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
dbus "github.com/coreos/go-systemd/dbus"
|
|
)
|
|
|
|
// systemdActive reports whether systemd is the running init (/run/systemd/system
|
|
// exists), the same marker systemctl uses to decide it can reach the manager.
|
|
func systemdActive() bool {
|
|
_, err := os.Stat("/run/systemd/system")
|
|
return err == nil
|
|
}
|
|
|
|
// commandExists reports whether name resolves on PATH.
|
|
func commandExists(name string) bool {
|
|
_, err := exec.LookPath(name)
|
|
return err == nil
|
|
}
|
|
|
|
// systemdUnitEnabled reports whether systemd reports name's unit as set to
|
|
// start at boot. "enabled", "enabled-runtime", and "generated" all count;
|
|
// "generated" is how systemd wraps a SysV init.d script that was registered to
|
|
// start. Returns false when systemd is not running or the unit is absent.
|
|
func systemdUnitEnabled(ctx context.Context, name string) bool {
|
|
conn, err := dbus.NewWithContext(ctx)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer conn.Close()
|
|
prop, err := conn.GetUnitPropertyContext(ctx, name+".service", "UnitFileState")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
switch prop.Value.Value() {
|
|
case "enabled", "enabled-runtime", "generated":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// systemdUnitState returns name's unit-file state (e.g. "enabled", "disabled",
|
|
// "static") and whether its unit file is installed. It lists all unit files and
|
|
// filters by name rather than calling ListUnitFilesByPatterns, which older
|
|
// systemd (CentOS 7's v219) does not export over D-Bus. Used to tell
|
|
// installed-but-disabled units (which can be enabled) from absent ones (which
|
|
// cannot be). Returns "", false when systemd is not running.
|
|
func systemdUnitState(ctx context.Context, name string) (state string, present bool) {
|
|
conn, err := dbus.NewWithContext(ctx)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
defer conn.Close()
|
|
files, err := conn.ListUnitFilesContext(ctx)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
target := name + ".service"
|
|
for _, uf := range files {
|
|
if filepath.Base(uf.Path) == target {
|
|
return uf.Type, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// chkconfigOn reports whether `chkconfig --list name` shows any runlevel on,
|
|
// the RHEL-family enablement signal. Returns false when chkconfig is absent.
|
|
func chkconfigOn(ctx context.Context, name string) bool {
|
|
results, err := runCommand(ctx, "chkconfig", "--list", name)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, line := range results {
|
|
fields := strings.Fields(line)
|
|
if len(fields) == 0 || fields[0] != name {
|
|
continue
|
|
}
|
|
for _, f := range fields[1:] {
|
|
if _, status, found := strings.Cut(f, ":"); found && status == "on" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// rcSymlinksOn reports whether an S*name start symlink exists in any rcN.d
|
|
// tree, the enablement signal for both update-rc.d (Debian/Ubuntu, /etc/rcN.d)
|
|
// and Slackware (/etc/rc.d/rcN.d).
|
|
func rcSymlinksOn(name string) bool {
|
|
for _, rl := range []string{"2", "3", "4", "5"} {
|
|
for _, dir := range []string{"/etc/rc" + rl + ".d", "/etc/rc.d/rc" + rl + ".d"} {
|
|
if matches, _ := filepath.Glob(filepath.Join(dir, "S*"+name)); len(matches) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// openrcOn reports whether name is linked into an OpenRC runlevel directory
|
|
// (default or boot), the enablement signal created by `rc-update add` on Gentoo.
|
|
func openrcOn(name string) bool {
|
|
for _, rl := range []string{"default", "boot"} {
|
|
if _, err := os.Lstat(filepath.Join("/etc/runlevels", rl, name)); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// rcLocalOn reports whether name is invoked from an rc.local file, apf's
|
|
// last-resort enablement when neither systemd nor an init.d registration
|
|
// applies. Commented lines are ignored.
|
|
func rcLocalOn(name string) bool {
|
|
for _, p := range []string{"/etc/rc.local", "/etc/rc.d/rc.local"} {
|
|
data, err := os.ReadFile(p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
if strings.Contains(line, name) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// sysvServiceEnabled reports whether name is enabled under any SysV-family init
|
|
// system, the fallback when systemd is absent or does not report the service
|
|
// enabled. It checks chkconfig, Debian/Slackware rcN.d start symlinks, OpenRC
|
|
// runlevels, and rc.local in turn; the first to report it on wins.
|
|
func sysvServiceEnabled(ctx context.Context, name string) bool {
|
|
if chkconfigOn(ctx, name) {
|
|
return true
|
|
}
|
|
if rcSymlinksOn(name) {
|
|
return true
|
|
}
|
|
if openrcOn(name) {
|
|
return true
|
|
}
|
|
return rcLocalOn(name)
|
|
}
|
|
|
|
// serviceInstalled reports whether name's unit or init.d script is installed,
|
|
// regardless of whether it is enabled. Used to tell a merely-disabled service
|
|
// (which can be enabled) from an absent one (which cannot be).
|
|
func serviceInstalled(ctx context.Context, name string) bool {
|
|
if systemdActive() {
|
|
_, present := systemdUnitState(ctx, name)
|
|
return present
|
|
}
|
|
for _, dir := range []string{"/etc/init.d", "/etc/rc.d/init.d"} {
|
|
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// serviceEnabled reports whether name is enabled to start, checking systemd
|
|
// first and then every SysV-family init mechanism the supported firewalls
|
|
// install under: chkconfig (RHEL), update-rc.d (Debian/Ubuntu, via /etc/rcN.d
|
|
// symlinks), rc-update (Gentoo/OpenRC, via /etc/runlevels), Slackware rc.d
|
|
// symlinks, and rc.local (apf's last resort). name is the service base name
|
|
// without a ".service" suffix (e.g. "csf", "apf", "netfilter-persistent"); any
|
|
// one mechanism reporting it on counts as enabled.
|
|
func serviceEnabled(ctx context.Context, name string) bool {
|
|
if systemdUnitEnabled(ctx, name) {
|
|
return true
|
|
}
|
|
return sysvServiceEnabled(ctx, name)
|
|
}
|
|
|
|
// enableService enables name to start at boot under whatever init system is
|
|
// active, mirroring how the firewalls' installers register themselves. It is a
|
|
// no-op (returns nil) when the service is not installed or already enabled. On
|
|
// systemd it runs `systemctl enable` (preceded by daemon-reload so a freshly
|
|
// installed unit is picked up); otherwise it uses chkconfig, update-rc.d, or
|
|
// rc-update, whichever is present.
|
|
func enableService(ctx context.Context, name string) error {
|
|
if systemdActive() {
|
|
state, present := systemdUnitState(ctx, name)
|
|
if !present {
|
|
return nil
|
|
}
|
|
switch state {
|
|
case "enabled", "enabled-runtime", "static", "generated":
|
|
// static units have no install info and are pulled in by other
|
|
// units, so there is nothing to enable.
|
|
return nil
|
|
}
|
|
if _, err := runCommand(ctx, "systemctl", "daemon-reload"); err != nil {
|
|
return fmt.Errorf("failed to reload systemd for %s: %s", name, err)
|
|
}
|
|
if _, err := runCommand(ctx, "systemctl", "enable", name+".service"); err != nil {
|
|
return fmt.Errorf("failed to enable %s: %s", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
if !serviceInstalled(ctx, name) {
|
|
return nil
|
|
}
|
|
switch {
|
|
case commandExists("chkconfig"):
|
|
if _, err := runCommand(ctx, "chkconfig", "--add", name); err != nil {
|
|
return fmt.Errorf("failed to enable %s: %s", name, err)
|
|
}
|
|
if _, err := runCommand(ctx, "chkconfig", name, "on"); err != nil {
|
|
return fmt.Errorf("failed to enable %s: %s", name, err)
|
|
}
|
|
case commandExists("update-rc.d"):
|
|
if _, err := runCommand(ctx, "update-rc.d", name, "defaults"); err != nil {
|
|
return fmt.Errorf("failed to enable %s: %s", name, err)
|
|
}
|
|
case commandExists("rc-update"):
|
|
if _, err := runCommand(ctx, "rc-update", "add", name, "default"); err != nil {
|
|
return fmt.Errorf("failed to enable %s: %s", name, err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("no supported init system found to enable %s", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// restartService restarts name under whatever init system is active. On systemd
|
|
// it runs `systemctl restart`; otherwise it prefers the `service` wrapper, then
|
|
// OpenRC's `rc-service`, then the init.d script directly.
|
|
func restartService(ctx context.Context, name string) error {
|
|
if systemdActive() {
|
|
if _, err := runCommand(ctx, "systemctl", "restart", name+".service"); err != nil {
|
|
return fmt.Errorf("failed to restart %s: %s", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
switch {
|
|
case commandExists("service"):
|
|
if _, err := runCommand(ctx, "service", name, "restart"); err != nil {
|
|
return fmt.Errorf("failed to restart %s: %s", name, err)
|
|
}
|
|
case commandExists("rc-service"):
|
|
if _, err := runCommand(ctx, "rc-service", name, "restart"); err != nil {
|
|
return fmt.Errorf("failed to restart %s: %s", name, err)
|
|
}
|
|
default:
|
|
if _, err := runCommand(ctx, filepath.Join("/etc/init.d", name), "restart"); err != nil {
|
|
return fmt.Errorf("failed to restart %s: %s", name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|