From c5e3e0ee70a5c39c4deaec210dc5b8fad57a014a Mon Sep 17 00:00:00 2001 From: James Shubin Date: Sun, 10 Mar 2024 16:55:13 -0400 Subject: [PATCH] engine: resources: Add firewalld resource This is a simple firewalld resource to make the seamless opening of firewall ports in standalone laptop (and other) environments easy. --- engine/resources/firewalld.go | 651 ++++++++++++++++++++++++++++++++++ examples/lang/firewalld0.mcl | 9 + go.mod | 14 +- go.sum | 16 + 4 files changed, 684 insertions(+), 6 deletions(-) create mode 100644 engine/resources/firewalld.go create mode 100644 examples/lang/firewalld0.mcl diff --git a/engine/resources/firewalld.go b/engine/resources/firewalld.go new file mode 100644 index 00000000..117d3758 --- /dev/null +++ b/engine/resources/firewalld.go @@ -0,0 +1,651 @@ +// Mgmt +// Copyright (C) 2013-2024+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this program, or any covered work, by linking or combining it +// with embedded mcl code and modules (and that the embedded mcl code and +// modules which link with this program, contain a copy of their source code in +// the authoritative form) containing parts covered by the terms of any other +// license, the licensors of this program grant you additional permission to +// convey the resulting work. Furthermore, the licensors of this program grant +// the original author, James Shubin, additional permission to update this +// additional permission if he deems it necessary to achieve the goals of this +// additional permission. + +package resources + +import ( + "context" + "fmt" + "os/user" + "strconv" + "strings" + "sync" + + "github.com/purpleidea/mgmt/engine" + "github.com/purpleidea/mgmt/engine/traits" + "github.com/purpleidea/mgmt/lang/funcs/vars" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util" + "github.com/purpleidea/mgmt/util/errwrap" + + "github.com/godbus/dbus/v5" + "github.com/google/nftables" +) + +func init() { + // TODO: Should we split this into firewalld:service and firewalld:port? + engine.RegisterResource(KindFirewalld, func() engine.Res { return &FirewalldRes{} }) + + // const.res.firewalld.state.exists = "exists" + // const.res.firewalld.state.absent = "absent" + vars.RegisterResourceParams(KindFirewalld, map[string]map[string]func() interfaces.Var{ + ParamFileState: { + FirewalldStateExists: func() interfaces.Var { + return &types.StrValue{ + V: FirewalldStateExists, + } + }, + FirewalldStateAbsent: func() interfaces.Var { + return &types.StrValue{ + V: FirewalldStateAbsent, + } + }, + // TODO: consider removing this field entirely + "undefined": func() interfaces.Var { + return &types.StrValue{ + V: FirewalldStateUndefined, // empty string + } + }, + }, + }) +} + +const ( + // KindFirewalld is the kind string used to identify this resource. + KindFirewalld = "firewalld" + + // ParamFirewalldState is the name of the state field parameter. + ParamFirewalldState = "state" + + // FirewalldStateExists is the string that represents that the service, + // or port, or other setting should be present. + FirewalldStateExists = "exists" + + // FirewalldStateAbsent is the string that represents that the service, + // or port, or other setting should not exist. + FirewalldStateAbsent = "absent" + + // FirewalldStateUndefined means the file state has not been specified. + // TODO: consider moving to *string and express this state as a nil. + FirewalldStateUndefined = "" + + // ErrInvalidZone is a firewalld error from dbus. + ErrInvalidZone = engine.Error("invalid zone") + + // ErrInvalidPort is a firewalld error from dbus. + ErrInvalidPort = engine.Error("invalid port") + + // ErrInvalidService is a firewalld error from dbus. + ErrInvalidService = engine.Error("invalid service") + + // ErrInvalidProtocol is a firewalld error from dbus. + ErrInvalidProtocol = engine.Error("invalid protocol") + + // ErrInvalidCommand is a firewalld error from dbus. + ErrInvalidCommand = engine.Error("invalid command") + + // ErrMissingProtocol is a firewalld error from dbus. + ErrMissingProtocol = engine.Error("missing protocol") + + // ErrAlreadyEnabled is a firewalld error from dbus. + ErrAlreadyEnabled = engine.Error("already enabled") + + // ErrNotEnabled is a firewalld error from dbus. + ErrNotEnabled = engine.Error("not enabled") + + firewalld1Path = dbus.ObjectPath("/org/fedoraproject/FirewallD1") + firewalld1Iface = "org.fedoraproject.FirewallD1" +) + +// FirewalldRes is a simple resource to interact with the firewalld service. It +// is not a replacement for a modern, robust tool like `shorewall`, but it has +// its uses such as for small, desktop use cases. The API of this resource might +// change to either add new features, split this into multiple resources, or to +// optimize the execution if it turns out to be too expensive to run large +// amounts of these as-is. The name variable currently has no useful purpose. +// Keep in mind that this resource requires root permissions to be able change +// the firewall settings and to monitor for changes. The change detection uses +// the nftables monitor facility. +type FirewalldRes struct { + traits.Base // add the base methods without re-implementation + + // XXX: add traits.Reversible and make this undo itself on removal + + init *engine.Init + + // Zone is the name of the zone to manage. If unspecified, we will + // attempt to get the default zone automatically. In this situation, it + // is possible that this default changes over time if it is acted upon + // by external tools that use firewalld. + Zone string `lang:"zone" yaml:"zone"` + + // State is the desired state. + State string `lang:"state" yaml:"state"` + + // Services are the list of services to manage to the desired state. + // These are single lower case strings like `dhcp`, and `tftp`. + Services []string `lang:"services" yaml:"services"` + + // Ports are the list of port/protocol combinations to manage to the + // desired state. These are strings of port number (slash) protocol like + // `4280/tcp` and `38/udp`. + Ports []string `lang:"ports" yaml:"ports"` + + // TODO: add a boolean setting for persistence (across reboots) + // TODO: this is the equivalent of `firewall-cmd --permanent` + //Permanent bool `lang:"permanent" yaml:"permanent"` + + zone string // cached name of the zone we're managing + call callFunc + wg *sync.WaitGroup +} + +// Default returns some sensible defaults for this resource. +func (obj *FirewalldRes) Default() engine.Res { + return &FirewalldRes{} +} + +// Validate if the params passed in are valid data. +func (obj *FirewalldRes) Validate() error { + // Check that we're running as root or with enough capabilities. + // XXX: check that we have enough capabilities if we're not root + currentUser, err := user.Current() + if err != nil { + return errwrap.Wrapf(err, "error looking up current user") + } + if currentUser.Uid != "0" { + // TODO: Technically we only need perms to watch state and to + // change things, so without this we could just error entirely + // if this were only used as a change detection tool. + return fmt.Errorf("running as root is required for firewalld") + } + + if obj.State != "" { + if obj.State != FirewalldStateExists && obj.State != FirewalldStateAbsent { + return fmt.Errorf("invalid state: %s", obj.State) + } + } + + for _, x := range obj.Services { + // TODO: we could check services from a list + if x == "" { + return fmt.Errorf("service is empty") + } + } + + for _, x := range obj.Ports { + split := strings.Split(x, "/") + if len(split) != 2 { + return fmt.Errorf("port/protocol was invalid: %s", x) + } + + if num, err := strconv.Atoi(split[0]); err != nil { + return err + } else if num <= 0 { + return fmt.Errorf("invalid number: %d", num) + } + + // TODO: we could check protocols from a list + if split[1] == "" { + return fmt.Errorf("protocol is empty") + } + } + + return nil +} + +// Init runs some startup code for this resource. +func (obj *FirewalldRes) Init(init *engine.Init) error { + obj.init = init // save for later + + obj.wg = &sync.WaitGroup{} + + return nil +} + +// Cleanup is run by the engine to clean up after the resource is done. +func (obj *FirewalldRes) Cleanup() error { + return nil +} + +// Watch is the primary listener for this resource and it outputs events. This +// could have been implemented with an exec of `/usr/sbin/nft --json monitor`, +// but the wrapped polkit and difficulty getting permissions using capabilities +// and without using full root, was challenging. This is cleaner too. +func (obj *FirewalldRes) Watch(ctx context.Context) error { + defer obj.wg.Wait() + + // `sudo setcap CAP_NET_ADMIN=+eip mgmt` seems to let us avoid root + opts := []nftables.ConnOption{} + conn, err := nftables.New(opts...) // (*nftables.Conn, error) + if err != nil { + return err + } + + // XXX: filter out events we don't care about using WithMonitorObject + //opt := nftables.WithMonitorObject(???) + mopts := []nftables.MonitorOption{} + //mopts = append(mopts, opt) + monitor := nftables.NewMonitor(mopts...) // *nftables.Monitor + defer monitor.Close() + events, err := conn.AddMonitor(monitor) + if err != nil { + return err + } + + obj.init.Running() // when started, notify engine that we're running + + var send = false // send event? + for { + select { + case event, ok := <-events: // &nftables.MonitorEvent + if !ok { + return nil + } + if event.Error != nil { + return errwrap.Wrapf(err, "monitor err") + } + if obj.init.Debug { + //obj.init.Logf("event: %#v", event) + obj.init.Logf("event type: %v", event.Type) + //obj.init.Logf("event data: %+v", event.Data) + } + + send = true + + case <-ctx.Done(): // closed by the engine to signal shutdown + return nil + } + + // do all our event sending all together to avoid duplicate msgs + if send { + send = false + obj.init.Event() // notify engine of an event (this can block) + } + } +} + +// CheckApply checks the resource state and applies the resource if the bool +// input is true. It returns error info and if the state check passed or not. +func (obj *FirewalldRes) CheckApply(ctx context.Context, apply bool) (bool, error) { + + conn, err := util.SystemBusPrivateUsable() + if err != nil { + return false, errwrap.Wrapf(err, "failed to connect to the private system bus") + } + defer conn.Close() + + firewalldObject := conn.Object(firewalld1Iface, firewalld1Path) // TODO: can we reuse this? + var flags dbus.Flags // none set + obj.call = func(ctx context.Context, method string, args ...interface{}) *dbus.Call { + fullMethod := firewalld1Iface + method // eg: ".getDefaultZone" + return firewalldObject.CallWithContext(ctx, fullMethod, flags, args...) + } + + // NOTE: The default zone might change over time by external users... + obj.zone = obj.Zone // default to this + if obj.Zone == "" { + if err := obj.call(ctx, ".getDefaultZone").Store(&obj.zone); err != nil { + return false, err + } + if obj.zone == "" { + return false, fmt.Errorf("unexpected empty zone") + } + obj.init.Logf("zone: %s\n", obj.zone) + } + + checkOK := true + + // This ordering doesn't currently matter, but might change if we find + // any sort of relevant relationship. + for _, x := range obj.Services { + if c, err := obj.serviceCheckApply(ctx, apply, x); err != nil { + return false, err + } else if !c { + checkOK = false + } + } + + for _, x := range obj.Ports { + if c, err := obj.portCheckApply(ctx, apply, x); err != nil { + return false, err + } else if !c { + checkOK = false + } + } + + return checkOK, nil +} + +// does the equivalent of: `firewall-cmd --zone= --list-services` and: +// `firewall-cmd --zone= --add-service=` and: `firewall-cmd +// --zone= --remove-service=`. +func (obj *FirewalldRes) serviceCheckApply(ctx context.Context, apply bool, service string) (bool, error) { + // .zone.getServices(s: zone) -> as + var services []string + args := []interface{}{obj.zone} + if err := obj.call(ctx, ".zone.getServices", args...).Store(&services); err != nil { + if parseError(err) == ErrInvalidZone { + obj.init.Logf("did the zone change?") // two managers! + } + return false, err + } + if obj.init.Debug { + obj.init.Logf("services: %+v", services) + } + + found := util.StrInList(service, services) + exists := obj.State != FirewalldStateAbsent // other states mean "exists" + + if exists && found || !exists && !found { + return true, nil + } + + if !apply { + return false, nil + } + + if !found { + // add it + // .zone.addService(s: zone, s: service, i: timeout) -> s + timeout := 0 // TODO: what should this be? + var output string + addArgs := []interface{}{obj.zone, service, timeout} + if err := obj.call(ctx, ".zone.addService", addArgs...).Store(&output); err != nil && parseError(err) != ErrAlreadyEnabled { + return false, err + } + obj.init.Logf("service added: %s", service) + if output != obj.zone { + // programming error + return false, fmt.Errorf("dbus API inconsistency") + } + + return false, nil + } + + // remove it + // .zone.removeService(s: zone, s: service) -> s + //timeout := 0 + var output string + removeArgs := []interface{}{obj.zone, service} + if err := obj.call(ctx, ".zone.removeService", removeArgs...).Store(&output); err != nil && parseError(err) != ErrNotEnabled { + return false, err + } + obj.init.Logf("service removed: %s", service) + if output != obj.zone { + // programming error + return false, fmt.Errorf("dbus API inconsistency") + } + + return false, nil +} + +// does the equivalent of: `firewall-cmd --zone= --list-ports` and: +// `firewall-cmd --zone= --add-port=4280/tcp` and: `firewall-cmd +// --zone= --remove-port=4280/tcp`. +func (obj *FirewalldRes) portCheckApply(ctx context.Context, apply bool, pp string) (bool, error) { + split := strings.Split(pp, "/") + if len(split) != 2 { // should already be checked in Validate + return false, fmt.Errorf("port/protocol was invalid: %s", pp) + } + port, err := strconv.Atoi(split[0]) + if err != nil { + return false, err + } else if port <= 0 { + return false, fmt.Errorf("invalid number: %d", port) + } + protocol := split[1] + + // .zone.getPorts(s: zone) -> aas + var ports [][]string + args := []interface{}{obj.zone} + if err := obj.call(ctx, ".zone.getPorts", args...).Store(&ports); err != nil { + if parseError(err) == ErrInvalidZone { + obj.init.Logf("did the zone change?") // two managers! + } + return false, err + } + if obj.init.Debug { + obj.init.Logf("ports: %+v", ports) + } + + found := false + for i, x := range ports { + // eg: ["1025-65535", "udp"] + if len(x) != 2 { + return false, fmt.Errorf("unexpected ports length (%d), got: %v", len(x), x) + } + + // x[0] is range or single value, eg: "1025-65535" OR "42" + // x[1] is proto like "tcp" or "udp" + if obj.init.Debug { + obj.init.Logf("rule %d: %s/%s", i, x[0], x[1]) + } + + if protocol != x[1] { + continue // not found (not us) + } + + // does the port number match? + split := strings.Split(x[0], "-") // split the range + if len(split) != 1 && len(split) != 2 { + return false, fmt.Errorf("unexpected ports format (%d), got: %v", len(split), split) + } + if len(split) == 1 { // standalone single value + if num, err := strconv.Atoi(split[0]); err != nil { + return false, err // programming error + } else if num != port { + continue // not found + } + found = true // yay! + break + } + //if len(split) == 2 + lhs, err := strconv.Atoi(split[0]) + if err != nil { + return false, err // programming error + } + rhs, err := strconv.Atoi(split[1]) + if err != nil { + return false, err // programming error + } + + if lhs <= port && port <= rhs { // ranges are inclusive on both boundss + found = true // yay! + break + } + } + + exists := obj.State != FirewalldStateAbsent // other states mean "exists" + + if exists && found || !exists && !found { + return true, nil + } + + if !apply { + return false, nil + } + + if !found { + // add it + // .zone.addPort(s: zone, s: port, s: protocol, i: timeout) -> s + timeout := 0 // TODO: what should this be? + var output string + addArgs := []interface{}{obj.zone, strconv.Itoa(port), protocol, timeout} + if err := obj.call(ctx, ".zone.addPort", addArgs...).Store(&output); err != nil && parseError(err) != ErrAlreadyEnabled { + return false, err + } + obj.init.Logf("port added: %s", pp) + if output != obj.zone { + // programming error + return false, fmt.Errorf("dbus API inconsistency") + } + + return false, nil + } + + // remove it + // .zone.removePort(s: zone, s: port, s: protocol) -> s + //timeout := 0 + var output string + removeArgs := []interface{}{obj.zone, strconv.Itoa(port), protocol} + if err := obj.call(ctx, ".zone.removePort", removeArgs...).Store(&output); err != nil && parseError(err) != ErrNotEnabled { + return false, err + } + obj.init.Logf("port removed: %s", pp) + if output != obj.zone { + // programming error + return false, fmt.Errorf("dbus API inconsistency") + } + + return false, nil +} + +// Cmp compares two resources and returns an error if they are not equivalent. +func (obj *FirewalldRes) Cmp(r engine.Res) error { + // we can only compare FirewalldRes to others of the same resource kind + res, ok := r.(*FirewalldRes) + if !ok { + return fmt.Errorf("not a %s", obj.Kind()) + } + + if obj.Zone != res.Zone { + return fmt.Errorf("the Zone differs") + } + if obj.State != res.State { + return fmt.Errorf("the State differs") + } + + if len(obj.Services) != len(res.Services) { + return fmt.Errorf("the Services differ") + } + for i, x := range obj.Services { + if x != res.Services[i] { + return fmt.Errorf("the service at index %d differs", i) + } + } + + if len(obj.Ports) != len(res.Ports) { + return fmt.Errorf("the Ports differ") + } + for i, x := range obj.Ports { + if x != res.Ports[i] { + return fmt.Errorf("the port/protocol at index %d differs", i) + } + } + + // TODO: consider adding this setting + //if obj.Permanent != res.Permanent { + // return fmt.Errorf("the Permanent differs") + //} + + return nil +} + +// UnmarshalYAML is the custom unmarshal handler for this struct. It is +// primarily useful for setting the defaults. +func (obj *FirewalldRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes FirewalldRes // indirection to avoid infinite recursion + + def := obj.Default() // get the default + res, ok := def.(*FirewalldRes) // put in the right format + if !ok { + return fmt.Errorf("could not convert to FirewalldRes") + } + raw := rawRes(*res) // convert; the defaults go here + + if err := unmarshal(&raw); err != nil { + return err + } + + *obj = FirewalldRes(raw) // restore from indirection with type conversion! + return nil +} + +// callFunc is a helper func for simplifying the making of dbus calls. +type callFunc func(ctx context.Context, method string, args ...interface{}) *dbus.Call + +// parseError converts a returned error into one of our error constants if it +// matches. If it doesn't match, then it passes the data through. This is a +// useful alternative mechanism to inline string matching on the returned error. +func parseError(err error) error { + if err == nil { + return nil // passthrough + } + dbusError, ok := err.(dbus.Error) + if !ok { + return err // passthrough + } + + if s := firewalld1Iface + ".Exception"; dbusError.Name != s { + return err // passthrough + } + + // eg: + // dbus.Error{ + // Name:"org.fedoraproject.FirewallD1", + // Body:[]interface {}{ + // "NOT_ENABLED: '4280:tcp' not in 'FedoraWorkstation'", + // }, + //} + if len(dbusError.Body) != 1 { // TODO: does this happen? + return err // passthrough + } + + body, ok := (dbusError.Body[0]).(string) + if !ok { + return err // passthrough + } + + // eg: "NOT_ENABLED: '4280:tcp' not in 'FedoraWorkstation'" + sep := ": " // the first colon space after the error name + split := strings.Split(body, sep) + //other := strings.Join(split[1:], sep) // remaining data (detailed reason) + switch split[0] { + case "INVALID_ZONE": + return ErrInvalidZone + case "INVALID_PORT": + return ErrInvalidPort + case "INVALID_SERVICE": + return ErrInvalidService + case "INVALID_PROTOCOL": + return ErrInvalidProtocol + case "INVALID_COMMAND": + return ErrInvalidCommand + case "MISSING_PROTOCOL": + return ErrMissingProtocol + case "ALREADY_ENABLED": // we tried to add it but it was already there + return ErrAlreadyEnabled + case "NOT_ENABLED": // we tried to remove it but it was already gone + return ErrNotEnabled + } + + return err // passthrough +} diff --git a/examples/lang/firewalld0.mcl b/examples/lang/firewalld0.mcl new file mode 100644 index 00000000..b57588e3 --- /dev/null +++ b/examples/lang/firewalld0.mcl @@ -0,0 +1,9 @@ +firewalld "misc" { # name is irrelevant + services => [ + "dhcp", + "tftp", + ], + ports => ["4280/tcp",], + + state => $const.res.firewalld.state.absent, +} diff --git a/go.mod b/go.mod index 3aec20e9..469d3ea2 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,8 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.10 go.etcd.io/etcd/client/v3 v3.5.10 go.etcd.io/etcd/server/v3 v3.5.10 - golang.org/x/crypto v0.16.0 - golang.org/x/sys v0.15.0 + golang.org/x/crypto v0.21.0 + golang.org/x/sys v0.18.0 golang.org/x/time v0.5.0 gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/yaml.v2 v2.4.0 @@ -65,7 +65,8 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/nftables v0.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/mux v1.7.2 // indirect github.com/gorilla/websocket v1.5.0 // indirect @@ -92,9 +93,10 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/packet v1.0.0 // indirect github.com/mdlayher/raw v0.1.0 // indirect - github.com/mdlayher/socket v0.2.3 // indirect + github.com/mdlayher/socket v0.5.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -148,8 +150,8 @@ require ( go.uber.org/zap v1.23.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sync v0.5.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sync v0.6.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.16.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect diff --git a/go.sum b/go.sum index 39ff925d..eb5c596c 100644 --- a/go.sum +++ b/go.sum @@ -317,11 +317,15 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8 github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8= +github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -554,6 +558,8 @@ github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/packet v1.0.0 h1:InhZJbdShQYt6XV2GPj5XHxChzOfhJJOMbvnGAmOfQ8= github.com/mdlayher/packet v1.0.0/go.mod h1:eE7/ctqDhoiRhQ44ko5JZU2zxB88g+JH/6jmnjzPjOU= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= @@ -564,6 +570,8 @@ github.com/mdlayher/raw v0.1.0/go.mod h1:yXnxvs6c0XoF/aK52/H5PjsVHmWBCFfZUfoh/Y5 github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E= github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM= github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= +github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= +github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -979,6 +987,8 @@ golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1084,6 +1094,8 @@ golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1114,6 +1126,8 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpi golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1205,6 +1219,8 @@ golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=