mirror of
https://github.com/safing/portmaster.git
synced 2026-06-19 20:44:35 +00:00
493 lines
17 KiB
Go
493 lines
17 KiB
Go
package interception
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/coreos/go-iptables/iptables"
|
|
"github.com/hashicorp/go-multierror"
|
|
|
|
"github.com/safing/portmaster/base/log"
|
|
"github.com/safing/portmaster/service/firewall/interception/nfq"
|
|
"github.com/safing/portmaster/service/mgr"
|
|
"github.com/safing/portmaster/service/netenv"
|
|
"github.com/safing/portmaster/service/network/packet"
|
|
)
|
|
|
|
var (
|
|
v4chains []string
|
|
v4rules []string
|
|
v4once []string
|
|
|
|
v6chains []string
|
|
v6rules []string
|
|
v6once []string
|
|
|
|
out4Queue nfQueue
|
|
in4Queue nfQueue
|
|
out6Queue nfQueue
|
|
in6Queue nfQueue
|
|
|
|
isRunning atomic.Bool
|
|
shutdownSignal = make(chan struct{})
|
|
)
|
|
|
|
// nfQueue encapsulates nfQueue providers.
|
|
type nfQueue interface {
|
|
PacketChannel() <-chan packet.Packet
|
|
Destroy()
|
|
}
|
|
|
|
func init() {
|
|
v4chains = []string{
|
|
"mangle PORTMASTER-INGEST-OUTPUT",
|
|
"mangle PORTMASTER-INGEST-INPUT",
|
|
"filter PORTMASTER-FILTER",
|
|
"nat PORTMASTER-REDIRECT",
|
|
}
|
|
|
|
v4rules = []string{
|
|
// stenya: Preserve original packet marks for permanently allowed connections (connmark 1710/AcceptAlways)
|
|
// to ensure compatibility with other tools that also rely on packet marks.
|
|
// This rule is placed before `CONNMARK --restore-mark` to prevent overwriting the original mark.
|
|
// (Example: WireGuard/wg-quick relies on packet marks; changing them would break its routing).
|
|
"mangle PORTMASTER-INGEST-OUTPUT -m mark ! --mark 0 -m connmark --mark 1710 -j RETURN",
|
|
"mangle PORTMASTER-INGEST-OUTPUT -m mark ! --mark 0 -m connmark --mark 1709 -j RETURN",
|
|
"mangle PORTMASTER-INGEST-OUTPUT -j CONNMARK --restore-mark",
|
|
"mangle PORTMASTER-INGEST-OUTPUT -m mark --mark 0 -j NFQUEUE --queue-num 17040 --queue-bypass",
|
|
|
|
// stenya: Preserve original packet marks, similar to the OUTPUT chain (not sure if this is really needed for INPUT).
|
|
"mangle PORTMASTER-INGEST-INPUT -m mark ! --mark 0 -m connmark --mark 1710 -j RETURN",
|
|
"mangle PORTMASTER-INGEST-INPUT -m mark ! --mark 0 -m connmark --mark 1709 -j RETURN",
|
|
"mangle PORTMASTER-INGEST-INPUT -j CONNMARK --restore-mark",
|
|
"mangle PORTMASTER-INGEST-INPUT -m mark --mark 0 -j NFQUEUE --queue-num 17140 --queue-bypass",
|
|
|
|
"filter PORTMASTER-FILTER -m mark --mark 0 -j DROP",
|
|
// stenya: Preserve original packet marks.
|
|
"filter PORTMASTER-FILTER -m connmark --mark 1710 -j RETURN",
|
|
"filter PORTMASTER-FILTER -m connmark --mark 1709 -j ACCEPT",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1700 -j RETURN",
|
|
// Accepting ICMP packets with mark 1701 is required for rejecting to work,
|
|
// as the rejection ICMP packet will have the same mark. Blocked ICMP
|
|
// packets will always result in a drop within the Portmaster.
|
|
"filter PORTMASTER-FILTER -m mark --mark 1701 -p icmp -j RETURN",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1701 -j REJECT --reject-with icmp-admin-prohibited",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1702 -j DROP",
|
|
"filter PORTMASTER-FILTER -j CONNMARK --save-mark",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1710 -j RETURN",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1709 -j ACCEPT",
|
|
// Accepting ICMP packets with mark 1711 is required for rejecting to work,
|
|
// as the rejection ICMP packet will have the same mark. Blocked ICMP
|
|
// packets will always result in a drop within the Portmaster.
|
|
"filter PORTMASTER-FILTER -m mark --mark 1711 -p icmp -j RETURN",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1711 -j REJECT --reject-with icmp-admin-prohibited",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1712 -j DROP",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1717 -j RETURN", // informational (non-functional) RETURN verdicts at the end of the chain
|
|
"filter PORTMASTER-FILTER -m mark --mark 1719 -j RETURN", // informational (non-functional) RETURN verdicts at the end of the chain
|
|
|
|
"nat PORTMASTER-REDIRECT -m mark --mark 1799 -p udp -j DNAT --to 127.0.0.17:53",
|
|
"nat PORTMASTER-REDIRECT -m mark --mark 1717 -p tcp -j DNAT --to 127.0.0.17:717",
|
|
"nat PORTMASTER-REDIRECT -m mark --mark 1717 -p udp -j DNAT --to 127.0.0.17:717",
|
|
"nat PORTMASTER-REDIRECT -m mark --mark 1719 -p tcp -j DNAT --to 127.0.0.17:719",
|
|
"nat PORTMASTER-REDIRECT -m mark --mark 1719 -p udp -j DNAT --to 127.0.0.17:719",
|
|
}
|
|
|
|
v4once = []string{
|
|
"mangle OUTPUT -j PORTMASTER-INGEST-OUTPUT",
|
|
"mangle INPUT -j PORTMASTER-INGEST-INPUT",
|
|
"filter OUTPUT -j PORTMASTER-FILTER",
|
|
"filter INPUT -j PORTMASTER-FILTER",
|
|
"nat OUTPUT -j PORTMASTER-REDIRECT",
|
|
}
|
|
|
|
v6chains = []string{
|
|
"mangle PORTMASTER-INGEST-OUTPUT",
|
|
"mangle PORTMASTER-INGEST-INPUT",
|
|
"filter PORTMASTER-FILTER",
|
|
"nat PORTMASTER-REDIRECT",
|
|
}
|
|
|
|
v6rules = []string{
|
|
"mangle PORTMASTER-INGEST-OUTPUT -m mark ! --mark 0 -m connmark --mark 1710 -j RETURN",
|
|
"mangle PORTMASTER-INGEST-OUTPUT -m mark ! --mark 0 -m connmark --mark 1709 -j RETURN",
|
|
"mangle PORTMASTER-INGEST-OUTPUT -j CONNMARK --restore-mark",
|
|
"mangle PORTMASTER-INGEST-OUTPUT -m mark --mark 0 -j NFQUEUE --queue-num 17060 --queue-bypass",
|
|
|
|
"mangle PORTMASTER-INGEST-INPUT -m mark ! --mark 0 -m connmark --mark 1710 -j RETURN",
|
|
"mangle PORTMASTER-INGEST-INPUT -m mark ! --mark 0 -m connmark --mark 1709 -j RETURN",
|
|
"mangle PORTMASTER-INGEST-INPUT -j CONNMARK --restore-mark",
|
|
"mangle PORTMASTER-INGEST-INPUT -m mark --mark 0 -j NFQUEUE --queue-num 17160 --queue-bypass",
|
|
|
|
"filter PORTMASTER-FILTER -m mark --mark 0 -j DROP",
|
|
"filter PORTMASTER-FILTER -m connmark --mark 1710 -j RETURN",
|
|
"filter PORTMASTER-FILTER -m connmark --mark 1709 -j ACCEPT",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1700 -j RETURN",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1701 -p icmpv6 -j RETURN",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1701 -j REJECT --reject-with icmp6-adm-prohibited",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1702 -j DROP",
|
|
"filter PORTMASTER-FILTER -j CONNMARK --save-mark",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1710 -j RETURN",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1709 -j ACCEPT",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1711 -p icmpv6 -j RETURN",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1711 -j REJECT --reject-with icmp6-adm-prohibited",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1712 -j DROP",
|
|
"filter PORTMASTER-FILTER -m mark --mark 1717 -j RETURN", // informational (non-functional) RETURN verdicts at the end of the chain
|
|
"filter PORTMASTER-FILTER -m mark --mark 1719 -j RETURN", // informational (non-functional) RETURN verdicts at the end of the chain
|
|
|
|
"nat PORTMASTER-REDIRECT -m mark --mark 1799 -p udp -j DNAT --to [::1]:53",
|
|
"nat PORTMASTER-REDIRECT -m mark --mark 1717 -p tcp -j DNAT --to [::1]:717",
|
|
"nat PORTMASTER-REDIRECT -m mark --mark 1717 -p udp -j DNAT --to [::1]:717",
|
|
"nat PORTMASTER-REDIRECT -m mark --mark 1719 -p tcp -j DNAT --to [::1]:719",
|
|
"nat PORTMASTER-REDIRECT -m mark --mark 1719 -p udp -j DNAT --to [::1]:719",
|
|
}
|
|
|
|
v6once = []string{
|
|
"mangle OUTPUT -j PORTMASTER-INGEST-OUTPUT",
|
|
"mangle INPUT -j PORTMASTER-INGEST-INPUT",
|
|
"filter OUTPUT -j PORTMASTER-FILTER",
|
|
"filter INPUT -j PORTMASTER-FILTER",
|
|
"nat OUTPUT -j PORTMASTER-REDIRECT",
|
|
}
|
|
|
|
// Reverse because we'd like to insert in a loop
|
|
_ = sort.Reverse(sort.StringSlice(v4once)) // silence vet (sort is used just like in the docs)
|
|
_ = sort.Reverse(sort.StringSlice(v6once)) // silence vet (sort is used just like in the docs)
|
|
}
|
|
|
|
func activateNfqueueFirewall() error {
|
|
if err := activateIPTables(iptables.ProtocolIPv4, v4rules, v4once, v4chains); err != nil {
|
|
return err
|
|
}
|
|
|
|
if netenv.IPv6Enabled() {
|
|
if err := activateIPTables(iptables.ProtocolIPv6, v6rules, v6once, v6chains); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := nfq.InitNFCT(); err != nil {
|
|
return err
|
|
}
|
|
// Remove stale conntrack entries carrying Portmaster marks.
|
|
// This is required to prevent conflicts with existing entries if Portmaster was not cleanly stopped,
|
|
// and to ensure a clean state on firewall activation.
|
|
_ = nfq.DeleteAllMarkedConnection()
|
|
|
|
// Force re-evaluation of connections that bypassed Portmaster while it was
|
|
// stopped or paused. Without this, DNAT rules (SPN, Split-Tunneling) would
|
|
// never apply to already-established connections, as the nat table is only
|
|
// traversed for new connections.
|
|
//
|
|
// NOTE: This will disconnect all existing non-loopback connections with mark=0!
|
|
//
|
|
// TODO: normally, this is only necessary when DNAT-based routing features are active (e.g. SPN, Split-Tunneling)
|
|
_ = nfq.DeleteUnmarkedConnections()
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeactivateNfqueueFirewall drops portmaster related IP tables rules.
|
|
// Any errors encountered accumulated into a *multierror.Error.
|
|
func DeactivateNfqueueFirewall() error {
|
|
// IPv4
|
|
var result *multierror.Error
|
|
if err := deactivateIPTables(iptables.ProtocolIPv4, v4once, v4chains); err != nil {
|
|
result = multierror.Append(result, err)
|
|
}
|
|
|
|
// IPv6
|
|
if netenv.IPv6Enabled() {
|
|
if err := deactivateIPTables(iptables.ProtocolIPv6, v6once, v6chains); err != nil {
|
|
result = multierror.Append(result, err)
|
|
}
|
|
}
|
|
|
|
_ = nfq.DeleteAllMarkedConnection()
|
|
nfq.TeardownNFCT()
|
|
|
|
return result.ErrorOrNil()
|
|
}
|
|
|
|
func activateIPTables(protocol iptables.Protocol, rules, once, chains []string) error {
|
|
tbls, err := iptables.NewWithProtocol(protocol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, chain := range chains {
|
|
splittedRule := strings.Split(chain, " ")
|
|
if err = tbls.ClearChain(splittedRule[0], splittedRule[1]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, rule := range rules {
|
|
splittedRule := strings.Split(rule, " ")
|
|
if err = tbls.Append(splittedRule[0], splittedRule[1], splittedRule[2:]...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, rule := range once {
|
|
splittedRule := strings.Split(rule, " ")
|
|
|
|
err := tbls.InsertUnique(splittedRule[0], splittedRule[1], 1, splittedRule[2:]...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ensureJumpRulesAtTop ensures that all "once" rules (the jump rules
|
|
// into Portmaster chains) are at the first position in their respective chains
|
|
// for both IPv4 and IPv6. It returns the list of rules that were out of position
|
|
// and had to be reinserted.
|
|
func ensureJumpRulesAtTop() (reinsertedRules []string, err error) {
|
|
reinsertedRules, err = reinsertDisplacedRules(iptables.ProtocolIPv4, v4once)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if netenv.IPv6Enabled() {
|
|
v6ReinsertedRules, err := reinsertDisplacedRules(iptables.ProtocolIPv6, v6once)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reinsertedRules = append(reinsertedRules, v6ReinsertedRules...)
|
|
}
|
|
|
|
return reinsertedRules, nil
|
|
}
|
|
|
|
// reinsertDisplacedRules checks each rule in once and, if it is not already
|
|
// at position 1 of its chain, moves it there. To avoid a window where packets
|
|
// can bypass the firewall, a temporary placeholder rule is inserted first,
|
|
// then the original rule is deleted and reinserted at position 1, and finally
|
|
// the placeholder is removed. Returns the subset of rules that required moving.
|
|
// Required rules format example: "filter OUTPUT -j PORTMASTER-FILTER"
|
|
func reinsertDisplacedRules(protocol iptables.Protocol, once []string) (reinsertedRules []string, err error) {
|
|
tbls, err := iptables.NewWithProtocol(protocol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var rulesToUpdate []string
|
|
for _, onceRule := range once {
|
|
splittedRule := strings.Split(onceRule, " ")
|
|
table := splittedRule[0]
|
|
chain := splittedRule[1]
|
|
// get the first rule of the chain
|
|
firstRule, err := tbls.ListById(table, chain, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// check if the first rule of the chain is the portmaster rule
|
|
pmChainName := splittedRule[len(splittedRule)-1]
|
|
if !strings.HasSuffix(firstRule, pmChainName) {
|
|
rulesToUpdate = append(rulesToUpdate, onceRule)
|
|
}
|
|
}
|
|
|
|
comment := []string{"-m", "comment", "--comment", `TEMPORARY_RULE`}
|
|
for _, rule := range rulesToUpdate {
|
|
splittedRule := strings.Split(rule, " ")
|
|
table := splittedRule[0] // "filter"
|
|
chain := splittedRule[1] // "OUTPUT"
|
|
ruleSpec := splittedRule[2:] // "-j PORTMASTER-FILTER"
|
|
|
|
tmpRuleSpec := append(append([]string{}, comment...), ruleSpec...) // "-m comment --comment "TEMPORARY_RULE" -j PORTMASTER-FILTER"
|
|
|
|
// Insert the temporary rule on the first position
|
|
err = tbls.Insert(table, chain, 1, tmpRuleSpec...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to insert temporary rule '%s' into chain '%s' in table '%s': %w", tmpRuleSpec, chain, table, err)
|
|
}
|
|
// delete the original rule and re-insert it on the first position
|
|
err = tbls.Delete(table, chain, ruleSpec...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to delete original rule '%s' from chain '%s' in table '%s': %w", ruleSpec, chain, table, err)
|
|
}
|
|
err = tbls.Insert(table, chain, 1, ruleSpec...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to re-insert original rule '%s' into chain '%s' in table '%s': %w", ruleSpec, chain, table, err)
|
|
}
|
|
// delete the temporary rule
|
|
err = tbls.Delete(table, chain, tmpRuleSpec...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to delete temporary rule '%s' from chain '%s' in table '%s': %w", tmpRuleSpec, chain, table, err)
|
|
}
|
|
}
|
|
|
|
return rulesToUpdate, nil
|
|
}
|
|
|
|
func deactivateIPTables(protocol iptables.Protocol, rules, chains []string) error {
|
|
tbls, err := iptables.NewWithProtocol(protocol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var multierr *multierror.Error
|
|
|
|
for _, rule := range rules {
|
|
splittedRule := strings.Split(rule, " ")
|
|
ok, err := tbls.Exists(splittedRule[0], splittedRule[1], splittedRule[2:]...)
|
|
if err != nil {
|
|
multierr = multierror.Append(multierr, err)
|
|
}
|
|
if ok {
|
|
if err = tbls.Delete(splittedRule[0], splittedRule[1], splittedRule[2:]...); err != nil {
|
|
multierr = multierror.Append(multierr, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, chain := range chains {
|
|
splittedRule := strings.Split(chain, " ")
|
|
if err = tbls.ClearChain(splittedRule[0], splittedRule[1]); err != nil {
|
|
multierr = multierror.Append(multierr, err)
|
|
}
|
|
if err = tbls.DeleteChain(splittedRule[0], splittedRule[1]); err != nil {
|
|
multierr = multierror.Append(multierr, err)
|
|
}
|
|
}
|
|
|
|
return multierr.ErrorOrNil()
|
|
}
|
|
|
|
// StartNfqueueInterception starts the nfqueue interception.
|
|
func StartNfqueueInterception(packets chan<- packet.Packet) (err error) {
|
|
if !isRunning.CompareAndSwap(false, true) {
|
|
return nil // already running
|
|
}
|
|
|
|
// Reset shutdown signal
|
|
shutdownSignal = make(chan struct{})
|
|
|
|
err = activateNfqueueFirewall()
|
|
if err != nil {
|
|
return fmt.Errorf("could not initialize nfqueue: %w", err)
|
|
}
|
|
|
|
out4Queue, err = nfq.New(17040, false)
|
|
if err != nil {
|
|
return fmt.Errorf("nfqueue(IPv4, out): %w", err)
|
|
}
|
|
in4Queue, err = nfq.New(17140, false)
|
|
if err != nil {
|
|
return fmt.Errorf("nfqueue(IPv4, in): %w", err)
|
|
}
|
|
|
|
if netenv.IPv6Enabled() {
|
|
out6Queue, err = nfq.New(17060, true)
|
|
if err != nil {
|
|
return fmt.Errorf("nfqueue(IPv6, out): %w", err)
|
|
}
|
|
in6Queue, err = nfq.New(17160, true)
|
|
if err != nil {
|
|
return fmt.Errorf("nfqueue(IPv6, in): %w", err)
|
|
}
|
|
} else {
|
|
log.Warningf("interception: no IPv6 stack detected, disabling IPv6 network integration")
|
|
out6Queue = &disabledNfQueue{}
|
|
in6Queue = &disabledNfQueue{}
|
|
}
|
|
|
|
module.mgr.Go("nfqueue packet handler", func(_ *mgr.WorkerCtx) error {
|
|
return handleInterception(packets)
|
|
})
|
|
|
|
// Safety check: ensure Portmaster's iptables jump rules remain at the top of their chains.
|
|
// During system boot, other services may insert their own iptables rules, potentially
|
|
// displacing Portmaster's rules and causing traffic to bypass the firewall.
|
|
// The check runs a few times after startup with increasing delays to cover this window.
|
|
// A continuous periodic check is intentionally avoided - it would not react immediately
|
|
// to rule changes anyway, and the overhead is not justified after the boot stage.
|
|
// TODO: consider a more reactive approach using netlink to detect iptables changes in real time.
|
|
module.mgr.Go("iptables rule order maintenance (startup)", func(w *mgr.WorkerCtx) error {
|
|
for _, d := range []time.Duration{5 * time.Second, 10 * time.Second, 30 * time.Second} {
|
|
select {
|
|
case <-time.After(d):
|
|
case <-w.Done():
|
|
return nil
|
|
case <-shutdownSignal:
|
|
return nil
|
|
}
|
|
if updatedRules, err := ensureJumpRulesAtTop(); err != nil {
|
|
log.Errorf("interception: failed to ensure iptables jump rules at top: %v", err)
|
|
} else if len(updatedRules) > 0 {
|
|
log.Warningf("interception: the following iptables rules were found out of position and have been reinserted at the top of their chains: %v", updatedRules)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// StopNfqueueInterception stops the nfqueue interception.
|
|
func StopNfqueueInterception() error {
|
|
if !isRunning.CompareAndSwap(true, false) {
|
|
return nil // not running
|
|
}
|
|
|
|
// Signal shutdown to packet handler
|
|
defer close(shutdownSignal)
|
|
|
|
if out4Queue != nil {
|
|
out4Queue.Destroy()
|
|
}
|
|
if in4Queue != nil {
|
|
in4Queue.Destroy()
|
|
}
|
|
if out6Queue != nil {
|
|
out6Queue.Destroy()
|
|
}
|
|
if in6Queue != nil {
|
|
in6Queue.Destroy()
|
|
}
|
|
|
|
err := DeactivateNfqueueFirewall()
|
|
if err != nil {
|
|
return fmt.Errorf("interception: error while deactivating nfqueue: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func handleInterception(packets chan<- packet.Packet) error {
|
|
for {
|
|
var pkt packet.Packet
|
|
select {
|
|
case <-shutdownSignal:
|
|
return nil
|
|
case pkt = <-out4Queue.PacketChannel():
|
|
pkt.SetOutbound()
|
|
case pkt = <-in4Queue.PacketChannel():
|
|
pkt.SetInbound()
|
|
case pkt = <-out6Queue.PacketChannel():
|
|
pkt.SetOutbound()
|
|
case pkt = <-in6Queue.PacketChannel():
|
|
pkt.SetInbound()
|
|
}
|
|
|
|
select {
|
|
case packets <- pkt:
|
|
case <-shutdownSignal:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
type disabledNfQueue struct{}
|
|
|
|
func (dnfq *disabledNfQueue) PacketChannel() <-chan packet.Packet {
|
|
return nil
|
|
}
|
|
|
|
func (dnfq *disabledNfQueue) Destroy() {}
|