wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
125
service/firewall/interception/nfq/conntrack.go
Normal file
125
service/firewall/interception/nfq/conntrack.go
Normal file
@@ -0,0 +1,125 @@
|
||||
//go:build linux
|
||||
|
||||
package nfq
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
ct "github.com/florianl/go-conntrack"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
)
|
||||
|
||||
var nfct *ct.Nfct // Conntrack handler. NFCT: Network Filter Connection Tracking.
|
||||
|
||||
// InitNFCT initializes the network filter conntrack library.
|
||||
func InitNFCT() error {
|
||||
var err error
|
||||
nfct, err = ct.Open(&ct.Config{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TeardownNFCT deinitializes the network filter conntrack library.
|
||||
func TeardownNFCT() {
|
||||
if nfct != nil {
|
||||
_ = nfct.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAllMarkedConnection deletes all marked entries from the conntrack table.
|
||||
func DeleteAllMarkedConnection() error {
|
||||
if nfct == nil {
|
||||
return fmt.Errorf("nfq: nfct not initialized")
|
||||
}
|
||||
|
||||
// Delete all ipv4 marked connections
|
||||
deleted := deleteMarkedConnections(nfct, ct.IPv4)
|
||||
|
||||
if netenv.IPv6Enabled() {
|
||||
// Delete all ipv6 marked connections
|
||||
deleted += deleteMarkedConnections(nfct, ct.IPv6)
|
||||
}
|
||||
|
||||
log.Infof("nfq: deleted %d conntrack entries to reset permanent connection verdicts", deleted)
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteMarkedConnections(nfct *ct.Nfct, f ct.Family) (deleted int) {
|
||||
// initialize variables
|
||||
permanentFlags := []uint32{MarkAcceptAlways, MarkBlockAlways, MarkDropAlways, MarkRerouteNS, MarkRerouteSPN}
|
||||
filter := ct.FilterAttr{}
|
||||
filter.MarkMask = []byte{0xFF, 0xFF, 0xFF, 0xFF}
|
||||
filter.Mark = []byte{0x00, 0x00, 0x00, 0x00} // 4 zeros starting value
|
||||
|
||||
numberOfErrors := 0
|
||||
var deleteError error = nil
|
||||
// Get all connections from the specified family (ipv4 or ipv6)
|
||||
for _, mark := range permanentFlags {
|
||||
binary.BigEndian.PutUint32(filter.Mark, mark) // Little endian is in reverse not sure why. BigEndian makes it in correct order.
|
||||
currentConnections, err := nfct.Query(ct.Conntrack, f, filter)
|
||||
if err != nil {
|
||||
log.Warningf("nfq: error on conntrack query: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, connection := range currentConnections {
|
||||
deleteError = nfct.Delete(ct.Conntrack, ct.IPv4, connection)
|
||||
if err != nil {
|
||||
numberOfErrors++
|
||||
} else {
|
||||
deleted++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if numberOfErrors > 0 {
|
||||
log.Warningf("nfq: failed to delete %d conntrack entries last error is: %s", numberOfErrors, deleteError)
|
||||
}
|
||||
return deleted
|
||||
}
|
||||
|
||||
// DeleteMarkedConnection removes a specific connection from the conntrack table.
|
||||
func DeleteMarkedConnection(conn *network.Connection) error {
|
||||
if nfct == nil {
|
||||
return fmt.Errorf("nfq: nfct not initialized")
|
||||
}
|
||||
|
||||
con := ct.Con{
|
||||
Origin: &ct.IPTuple{
|
||||
Src: &conn.LocalIP,
|
||||
Dst: &conn.Entity.IP,
|
||||
Proto: &ct.ProtoTuple{
|
||||
Number: &conn.Entity.Protocol,
|
||||
SrcPort: &conn.LocalPort,
|
||||
DstPort: &conn.Entity.Port,
|
||||
},
|
||||
},
|
||||
}
|
||||
connections, err := nfct.Get(ct.Conntrack, ct.IPv4, con)
|
||||
if err != nil {
|
||||
return fmt.Errorf("nfq: failed to find entry for connection %s: %w", conn.String(), err)
|
||||
}
|
||||
|
||||
if len(connections) > 1 {
|
||||
log.Warningf("nfq: multiple entries found for single connection: %s -> %d", conn.String(), len(connections))
|
||||
}
|
||||
|
||||
for _, connection := range connections {
|
||||
deleteErr := nfct.Delete(ct.Conntrack, ct.IPv4, connection)
|
||||
if err == nil {
|
||||
err = deleteErr
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Warningf("nfq: error while deleting conntrack entries for connection %s: %s", conn.String(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
241
service/firewall/interception/nfq/nfq.go
Normal file
241
service/firewall/interception/nfq/nfq.go
Normal file
@@ -0,0 +1,241 @@
|
||||
//go:build linux
|
||||
|
||||
// Package nfq contains a nfqueue library experiment.
|
||||
package nfq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/florianl/go-nfqueue"
|
||||
"github.com/tevino/abool"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
pmpacket "github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
)
|
||||
|
||||
// Queue wraps a nfqueue.
|
||||
type Queue struct {
|
||||
id uint16
|
||||
afFamily uint8
|
||||
nf atomic.Value
|
||||
packets chan pmpacket.Packet
|
||||
cancelSocketCallback context.CancelFunc
|
||||
restart chan struct{}
|
||||
|
||||
pendingVerdicts uint64
|
||||
verdictCompleted chan struct{}
|
||||
}
|
||||
|
||||
func (q *Queue) getNfq() *nfqueue.Nfqueue {
|
||||
return q.nf.Load().(*nfqueue.Nfqueue) //nolint:forcetypeassert // TODO: Check.
|
||||
}
|
||||
|
||||
// New opens a new nfQueue.
|
||||
func New(qid uint16, v6 bool) (*Queue, error) { //nolint:gocognit
|
||||
afFamily := unix.AF_INET
|
||||
if v6 {
|
||||
afFamily = unix.AF_INET6
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
q := &Queue{
|
||||
id: qid,
|
||||
afFamily: uint8(afFamily),
|
||||
nf: atomic.Value{},
|
||||
restart: make(chan struct{}, 1),
|
||||
packets: make(chan pmpacket.Packet, 1000),
|
||||
cancelSocketCallback: cancel,
|
||||
verdictCompleted: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
// Do not retry if the first one fails immediately as it
|
||||
// might point to a deeper integration error that's not fixable
|
||||
// with retrying ...
|
||||
if err := q.open(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
Wait:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-q.restart:
|
||||
runtime.Gosched()
|
||||
}
|
||||
|
||||
for {
|
||||
err := q.open(ctx)
|
||||
if err == nil {
|
||||
continue Wait
|
||||
}
|
||||
|
||||
// Wait 100 ms and then try again ...
|
||||
log.Errorf("Failed to open nfqueue: %s", err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return q, nil
|
||||
}
|
||||
|
||||
// open opens a new netlink socket and creates a new nfqueue.
|
||||
// Upon success, the new nfqueue is atomically stored in Queue.nf.
|
||||
// Users must use Queue.getNfq to access it. open does not care about
|
||||
// any other value or queue that might be stored in Queue.nf at
|
||||
// the time open is called.
|
||||
func (q *Queue) open(ctx context.Context) error {
|
||||
cfg := &nfqueue.Config{
|
||||
NfQueue: q.id,
|
||||
MaxPacketLen: 1600, // mtu is normally around 1500, make sure to capture it.
|
||||
MaxQueueLen: 0xffff,
|
||||
AfFamily: q.afFamily,
|
||||
Copymode: nfqueue.NfQnlCopyPacket,
|
||||
ReadTimeout: 1000 * time.Millisecond,
|
||||
WriteTimeout: 1000 * time.Millisecond,
|
||||
}
|
||||
|
||||
nf, err := nfqueue.Open(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := nf.RegisterWithErrorFunc(ctx, q.packetHandler(ctx), q.handleError); err != nil {
|
||||
_ = nf.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
q.nf.Store(nf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *Queue) handleError(e error) int {
|
||||
// embedded interface is required to work-around some
|
||||
// dep-vendoring weirdness
|
||||
if opError, ok := e.(interface { //nolint:errorlint // TODO: Check if we can remove workaround.
|
||||
Timeout() bool
|
||||
Temporary() bool
|
||||
}); ok {
|
||||
if opError.Timeout() || opError.Temporary() {
|
||||
c := atomic.LoadUint64(&q.pendingVerdicts)
|
||||
if c > 0 {
|
||||
log.Tracef("nfqueue: waiting for %d pending verdicts", c)
|
||||
|
||||
for atomic.LoadUint64(&q.pendingVerdicts) > 0 { // must NOT use c here
|
||||
<-q.verdictCompleted
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the queue was already closed. Unfortunately, the exposed error
|
||||
// variable is in an internal stdlib package. Therefore, check for the error
|
||||
// string instead. :(
|
||||
// Official error variable is defined here:
|
||||
// https://github.com/golang/go/blob/0e85fd7561de869add933801c531bf25dee9561c/src/internal/poll/fd.go#L24
|
||||
if !strings.HasSuffix(e.Error(), "use of closed file") {
|
||||
log.Errorf("nfqueue: encountered error while receiving packets: %s\n", e.Error())
|
||||
}
|
||||
|
||||
// Close the existing socket
|
||||
if nf := q.getNfq(); nf != nil {
|
||||
// Call Close() on the Con directly, as nf.Close() calls waitgroup.Wait(), which then may deadlock.
|
||||
_ = nf.Con.Close()
|
||||
}
|
||||
|
||||
// Trigger a restart of the queue
|
||||
q.restart <- struct{}{}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
func (q *Queue) packetHandler(ctx context.Context) func(nfqueue.Attribute) int {
|
||||
return func(attrs nfqueue.Attribute) int {
|
||||
if attrs.PacketID == nil {
|
||||
// we need a packet id to set a verdict,
|
||||
// if we don't get an ID there's hardly anything
|
||||
// we can do.
|
||||
return 0
|
||||
}
|
||||
|
||||
pkt := &packet{
|
||||
pktID: *attrs.PacketID,
|
||||
queue: q,
|
||||
verdictSet: make(chan struct{}),
|
||||
verdictPending: abool.New(),
|
||||
}
|
||||
pkt.Info().PID = process.UndefinedProcessID
|
||||
pkt.Info().SeenAt = time.Now()
|
||||
|
||||
if attrs.Payload == nil {
|
||||
// There is not payload.
|
||||
log.Warningf("nfqueue: packet #%d has no payload", pkt.pktID)
|
||||
return 0
|
||||
}
|
||||
|
||||
if err := pmpacket.Parse(*attrs.Payload, &pkt.Base); err != nil {
|
||||
log.Warningf("nfqueue: failed to parse payload: %s", err)
|
||||
_ = pkt.Drop()
|
||||
return 0
|
||||
}
|
||||
|
||||
select {
|
||||
case q.packets <- pkt:
|
||||
// DEBUG:
|
||||
// log.Tracef("nfqueue: queued packet %s (%s -> %s) after %s", pkt.ID(), pkt.Info().Src, pkt.Info().Dst, time.Since(pkt.Info().SeenAt))
|
||||
case <-ctx.Done():
|
||||
return 0
|
||||
case <-time.After(time.Second):
|
||||
log.Warningf("nfqueue: failed to queue packet (%s since it was handed over by the kernel)", time.Since(pkt.Info().SeenAt))
|
||||
}
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-pkt.verdictSet:
|
||||
|
||||
case <-time.After(20 * time.Second):
|
||||
log.Warningf("nfqueue: no verdict set for packet %s (%s -> %s) after %s, dropping", pkt.ID(), pkt.Info().Src, pkt.Info().Dst, time.Since(pkt.Info().SeenAt))
|
||||
if err := pkt.Drop(); err != nil {
|
||||
log.Warningf("nfqueue: failed to apply default-drop to unveridcted packet %s (%s -> %s)", pkt.ID(), pkt.Info().Src, pkt.Info().Dst)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return 0 // continue calling this fn
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy destroys the queue. Any error encountered is logged.
|
||||
func (q *Queue) Destroy() {
|
||||
if q == nil {
|
||||
return
|
||||
}
|
||||
|
||||
q.cancelSocketCallback()
|
||||
|
||||
if nf := q.getNfq(); nf != nil {
|
||||
if err := nf.Close(); err != nil {
|
||||
log.Errorf("nfqueue: failed to close queue %d: %s", q.id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PacketChannel returns the packet channel.
|
||||
func (q *Queue) PacketChannel() <-chan pmpacket.Packet {
|
||||
return q.packets
|
||||
}
|
||||
178
service/firewall/interception/nfq/packet.go
Normal file
178
service/firewall/interception/nfq/packet.go
Normal file
@@ -0,0 +1,178 @@
|
||||
//go:build linux
|
||||
|
||||
package nfq
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/florianl/go-nfqueue"
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
pmpacket "github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
// Firewalling marks used by the Portmaster.
|
||||
// See TODO on packet.mark() on their relevance
|
||||
// and a possibility to remove most IPtables rules.
|
||||
const (
|
||||
MarkAccept = 1700
|
||||
MarkBlock = 1701
|
||||
MarkDrop = 1702
|
||||
MarkAcceptAlways = 1710
|
||||
MarkBlockAlways = 1711
|
||||
MarkDropAlways = 1712
|
||||
MarkRerouteNS = 1799
|
||||
MarkRerouteSPN = 1717
|
||||
)
|
||||
|
||||
func markToString(mark int) string {
|
||||
switch mark {
|
||||
case MarkAccept:
|
||||
return "Accept"
|
||||
case MarkBlock:
|
||||
return "Block"
|
||||
case MarkDrop:
|
||||
return "Drop"
|
||||
case MarkAcceptAlways:
|
||||
return "AcceptAlways"
|
||||
case MarkBlockAlways:
|
||||
return "BlockAlways"
|
||||
case MarkDropAlways:
|
||||
return "DropAlways"
|
||||
case MarkRerouteNS:
|
||||
return "RerouteNS"
|
||||
case MarkRerouteSPN:
|
||||
return "RerouteSPN"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// packet implements the packet.Packet interface.
|
||||
type packet struct {
|
||||
pmpacket.Base
|
||||
pktID uint32
|
||||
queue *Queue
|
||||
verdictSet chan struct{}
|
||||
verdictPending *abool.AtomicBool
|
||||
}
|
||||
|
||||
func (pkt *packet) ID() string {
|
||||
return fmt.Sprintf("pkt:%d qid:%d", pkt.pktID, pkt.queue.id)
|
||||
}
|
||||
|
||||
// LoadPacketData does nothing on Linux, as data is always fully parsed.
|
||||
func (pkt *packet) LoadPacketData() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(ppacher): revisit the following behavior:
|
||||
//
|
||||
// The legacy implementation of nfqueue (and the interception) module
|
||||
// always accept a packet but may mark it so that a subsequent rule in
|
||||
// the C17 chain drops, rejects or modifies it.
|
||||
//
|
||||
// For drop/return we could use the actual nfQueue verdicts Drop and Stop.
|
||||
// Re-routing to local NS or SPN can be done by modifying the packet here
|
||||
// and using SetVerdictModPacket and reject can be implemented using a simple
|
||||
// raw-socket.
|
||||
func (pkt *packet) mark(mark int) (err error) {
|
||||
if pkt.verdictPending.SetToIf(false, true) {
|
||||
defer close(pkt.verdictSet)
|
||||
return pkt.setMark(mark)
|
||||
}
|
||||
|
||||
return errors.New("verdict already set")
|
||||
}
|
||||
|
||||
func (pkt *packet) setMark(mark int) error {
|
||||
atomic.AddUint64(&pkt.queue.pendingVerdicts, 1)
|
||||
|
||||
defer func() {
|
||||
atomic.AddUint64(&pkt.queue.pendingVerdicts, ^uint64(0))
|
||||
select {
|
||||
case pkt.queue.verdictCompleted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
if err := pkt.queue.getNfq().SetVerdictWithMark(pkt.pktID, nfqueue.NfAccept, mark); err != nil {
|
||||
// embedded interface is required to work-around some
|
||||
// dep-vendoring weirdness
|
||||
if opErr, ok := err.(interface { //nolint:errorlint // TODO: Check if we can remove workaround.
|
||||
Timeout() bool
|
||||
Temporary() bool
|
||||
}); ok {
|
||||
if opErr.Timeout() || opErr.Temporary() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Tracer(pkt.Ctx()).Errorf("nfqueue: failed to set verdict %s for %s (%s -> %s): %s", markToString(mark), pkt.ID(), pkt.Info().Src, pkt.Info().Dst, err)
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// DEBUG:
|
||||
// log.Tracer(pkt.Ctx()).Tracef(
|
||||
// "nfqueue: marking packet %s (%s -> %s) on queue %d with %s after %s",
|
||||
// pkt.ID(), pkt.Info().Src, pkt.Info().Dst, pkt.queue.id,
|
||||
// markToString(mark), time.Since(pkt.Info().SeenAt),
|
||||
// )
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pkt *packet) Accept() error {
|
||||
return pkt.mark(MarkAccept)
|
||||
}
|
||||
|
||||
func (pkt *packet) Block() error {
|
||||
if pkt.Info().Protocol == pmpacket.ICMP {
|
||||
// ICMP packets attributed to a blocked connection are always allowed, as
|
||||
// rejection ICMP packets will have the same mark as the blocked
|
||||
// connection. This is why we need to drop blocked ICMP packets instead.
|
||||
return pkt.mark(MarkDrop)
|
||||
}
|
||||
return pkt.mark(MarkBlock)
|
||||
}
|
||||
|
||||
func (pkt *packet) Drop() error {
|
||||
return pkt.mark(MarkDrop)
|
||||
}
|
||||
|
||||
func (pkt *packet) PermanentAccept() error {
|
||||
// If the packet is localhost only, do not permanently accept the outgoing
|
||||
// packet, as the packet mark will be copied to the connection mark, which
|
||||
// will stick and it will bypass the incoming queue.
|
||||
if !pkt.Info().Inbound && pkt.Info().Dst.IsLoopback() {
|
||||
return pkt.Accept()
|
||||
}
|
||||
|
||||
return pkt.mark(MarkAcceptAlways)
|
||||
}
|
||||
|
||||
func (pkt *packet) PermanentBlock() error {
|
||||
if pkt.Info().Protocol == pmpacket.ICMP || pkt.Info().Protocol == pmpacket.ICMPv6 {
|
||||
// ICMP packets attributed to a blocked connection are always allowed, as
|
||||
// rejection ICMP packets will have the same mark as the blocked
|
||||
// connection. This is why we need to drop blocked ICMP packets instead.
|
||||
return pkt.mark(MarkDropAlways)
|
||||
}
|
||||
return pkt.mark(MarkBlockAlways)
|
||||
}
|
||||
|
||||
func (pkt *packet) PermanentDrop() error {
|
||||
return pkt.mark(MarkDropAlways)
|
||||
}
|
||||
|
||||
func (pkt *packet) RerouteToNameserver() error {
|
||||
return pkt.mark(MarkRerouteNS)
|
||||
}
|
||||
|
||||
func (pkt *packet) RerouteToTunnel() error {
|
||||
return pkt.mark(MarkRerouteSPN)
|
||||
}
|
||||
Reference in New Issue
Block a user