Add support for the new kext

This commit is contained in:
Vladimir Stoilov 2024-01-20 14:50:52 +02:00
parent e9940d77a0
commit 1f2f0e5213
10 changed files with 765 additions and 3 deletions

View file

@ -5,7 +5,7 @@ import (
"fmt"
"time"
"github.com/safing/portmaster/firewall/interception/windowskext"
"github.com/safing/portmaster/firewall/interception/windowskext2"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/updates"
@ -35,8 +35,29 @@ func startInterception(packets chan packet.Packet) error {
})
// Start bandwidth stats monitor.
module.StartServiceWorker("kext bandwidth stats monitor", 0, func(ctx context.Context) error {
return windowskext.BandwidthStatsWorker(ctx, 1*time.Second, BandwidthUpdates)
// module.StartServiceWorker("kext bandwidth stats monitor", 0, func(ctx context.Context) error {
// return windowskext.BandwidthStatsWorker(ctx, 1*time.Second, BandwidthUpdates)
// })
// Start kext logging. The worker will periodically send request to the kext to send logs.
module.StartServiceWorker("kext log request worker", 0, func(ctx context.Context) error {
timer := time.NewTimer(time.Second)
for {
select {
case <-timer.C:
{
err := windowskext.SendLogRequest()
if err != nil {
return err
}
}
case <-ctx.Done():
{
return nil
}
}
}
})
return nil

View file

@ -0,0 +1,132 @@
//go:build windows
// +build windows
package windowskext
// This file contains example code how to read bandwidth stats from the kext. Its not ment to be used in production.
import (
"context"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/packet"
)
type Rxtxdata struct {
rx uint64
tx uint64
}
type Key struct {
localIP [4]uint32
remoteIP [4]uint32
localPort uint16
remotePort uint16
ipv6 bool
protocol uint8
}
var m = make(map[Key]Rxtxdata)
func BandwidthStatsWorker(ctx context.Context, collectInterval time.Duration, bandwidthUpdates chan *packet.BandwidthUpdate) error {
// Setup ticker.
ticker := time.NewTicker(collectInterval)
defer ticker.Stop()
// Collect bandwidth at every tick.
for {
select {
case <-ticker.C:
err := reportBandwidth(ctx, bandwidthUpdates)
if err != nil {
return err
}
case <-ctx.Done():
return nil
}
}
}
func reportBandwidth(ctx context.Context, bandwidthUpdates chan *packet.BandwidthUpdate) error {
stats, err := GetConnectionsStats()
if err != nil {
return err
}
// Report all statistics.
for i, stat := range stats {
connID := packet.CreateConnectionID(
packet.IPProtocol(stat.protocol),
convertArrayToIP(stat.localIP, stat.ipV6 == 1), stat.localPort,
convertArrayToIP(stat.remoteIP, stat.ipV6 == 1), stat.remotePort,
false,
)
update := &packet.BandwidthUpdate{
ConnID: connID,
BytesReceived: stat.receivedBytes,
BytesSent: stat.transmittedBytes,
Method: packet.Additive,
}
select {
case bandwidthUpdates <- update:
case <-ctx.Done():
return nil
default:
log.Warningf("kext: bandwidth update queue is full, skipping rest of batch (%d entries)", len(stats)-i)
return nil
}
}
return nil
}
func StartBandwithConsoleLogger() {
go func() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
conns, err := GetConnectionsStats()
if err != nil {
continue
}
for _, conn := range conns {
if conn.receivedBytes == 0 && conn.transmittedBytes == 0 {
continue
}
key := Key{
localIP: conn.localIP,
remoteIP: conn.remoteIP,
localPort: conn.localPort,
remotePort: conn.remotePort,
ipv6: conn.ipV6 == 1,
protocol: conn.protocol,
}
// First we get a "copy" of the entry
if entry, ok := m[key]; ok {
// Then we modify the copy
entry.rx += conn.receivedBytes
entry.tx += conn.transmittedBytes
// Then we reassign map entry
m[key] = entry
} else {
m[key] = Rxtxdata{
rx: conn.receivedBytes,
tx: conn.transmittedBytes,
}
}
}
log.Debug("----------------------------------")
for key, value := range m {
log.Debugf(
"Conn: %d %s:%d %s:%d rx:%d tx:%d", key.protocol,
convertArrayToIP(key.localIP, key.ipv6), key.localPort,
convertArrayToIP(key.remoteIP, key.ipv6), key.remotePort,
value.rx, value.tx,
)
}
}
}()
}

View file

@ -0,0 +1,4 @@
// +build windows
// Package windowskext provides network interception capabilities on windows via the Portmaster Kernel Extension.
package windowskext

View file

@ -0,0 +1,181 @@
//go:build windows
// +build windows
package windowskext
import (
"context"
"encoding/binary"
"fmt"
"net"
"time"
"unsafe"
"github.com/safing/portmaster/process"
"github.com/tevino/abool"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/packet"
)
const (
// VerdictRequestFlagFastTrackPermitted is set on packets that have been
// already permitted by the kernel extension and the verdict request is only
// informational.
VerdictRequestFlagFastTrackPermitted = 1
// VerdictRequestFlagSocketAuth indicates that the verdict request is for a
// connection that was intercepted on an ALE layer instead of in the network
// stack itself. Thus, no packet data is available.
VerdictRequestFlagSocketAuth = 2
// VerdictRequestFlagExpectSocketAuth indicates that the next verdict
// requests is expected to be an informational socket auth request from
// the ALE layer.
VerdictRequestFlagExpectSocketAuth = 4
)
type ConnectionStat struct {
localIP [4]uint32 //Source Address, only srcIP[0] if IPv4
remoteIP [4]uint32 //Destination Address
localPort uint16 //Source Port
remotePort uint16 //Destination port
receivedBytes uint64 //Number of bytes recived on this connection
transmittedBytes uint64 //Number of bytes transsmited from this connection
ipV6 uint8 //True: IPv6, False: IPv4
protocol uint8 //Protocol (UDP, TCP, ...)
}
type VersionInfo struct {
major uint8
minor uint8
revision uint8
build uint8
}
func (v *VersionInfo) String() string {
return fmt.Sprintf("%d.%d.%d.%d", v.major, v.minor, v.revision, v.build)
}
// Handler transforms received packets to the Packet interface.
func Handler(ctx context.Context, packets chan packet.Packet) {
for {
packetInfo, err := RecvVerdictRequest()
if err != nil {
log.Warningf("failed to get packet from windows kext: %s", err)
return
}
if packetInfo.Connection != nil {
log.Tracef("packet: %+v", packetInfo.Connection)
conn := packetInfo.Connection
// New Packet
new := &Packet{
verdictRequest: conn.Id,
verdictSet: abool.NewBool(false),
}
info := new.Info()
info.Inbound = conn.Direction > 0
info.InTunnel = false
info.Protocol = packet.IPProtocol(conn.Protocol)
info.PID = int(*conn.ProcessId)
info.SeenAt = time.Now()
// Check PID
if info.PID == 0 {
// Windows does not have zero PIDs.
// Set to UndefinedProcessID.
info.PID = process.UndefinedProcessID
}
// Set IP version
if conn.IpV6 {
info.Version = packet.IPv6
} else {
info.Version = packet.IPv4
}
// Set IPs
if info.Inbound {
// Inbound
info.Src = net.IP(conn.RemoteIp)
info.Dst = net.IP(conn.LocalIp)
} else {
// Outbound
info.Src = net.IP(conn.LocalIp)
info.Dst = net.IP(conn.RemoteIp)
}
// Set Ports
if info.Inbound {
// Inbound
info.SrcPort = conn.RemotePort
info.DstPort = conn.LocalPort
} else {
// Outbound
info.SrcPort = conn.LocalPort
info.DstPort = conn.RemotePort
}
packets <- new
}
if packetInfo.LogLines != nil {
for _, line := range *packetInfo.LogLines {
switch line.Severity {
case int(log.DebugLevel):
log.Debugf("kext: %s", line.Line)
case int(log.InfoLevel):
log.Infof("kext: %s", line.Line)
case int(log.WarningLevel):
log.Warningf("kext: %s", line.Line)
case int(log.ErrorLevel):
log.Errorf("kext: %s", line.Line)
case int(log.CriticalLevel):
log.Criticalf("kext: %s", line.Line)
}
}
}
}
}
// convertArrayToIP converts an array of uint32 values to a net.IP address.
func convertArrayToIP(input [4]uint32, ipv6 bool) net.IP {
if !ipv6 {
addressBuf := make([]byte, 4)
binary.BigEndian.PutUint32(addressBuf, input[0])
return net.IP(addressBuf)
}
addressBuf := make([]byte, 16)
for i := 0; i < 4; i++ {
binary.BigEndian.PutUint32(addressBuf[i*4:i*4+4], input[i])
}
return net.IP(addressBuf)
}
func ipAddressToArray(ip net.IP, isIPv6 bool) [4]uint32 {
array := [4]uint32{0}
if isIPv6 {
for i := 0; i < 4; i++ {
binary.BigEndian.PutUint32(asByteArrayWithLength(&array[i], 4), getUInt32Value(&ip[i]))
}
} else {
binary.BigEndian.PutUint32(asByteArrayWithLength(&array[0], 4), getUInt32Value(&ip[0]))
}
return array
}
func asByteArray[T any](obj *T) []byte {
return unsafe.Slice((*byte)(unsafe.Pointer(obj)), unsafe.Sizeof(*obj))
}
func asByteArrayWithLength[T any](obj *T, size uint32) []byte {
return unsafe.Slice((*byte)(unsafe.Pointer(obj)), size)
}
func getUInt32Value[T any](obj *T) uint32 {
return *(*uint32)(unsafe.Pointer(obj))
}

View file

@ -0,0 +1,156 @@
//go:build windows
// +build windows
package windowskext
import (
"errors"
"fmt"
"unsafe"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network"
"github.com/vlabo/portmaster_windows_rust_kext/kext_interface"
)
// Package errors
var (
ErrKextNotReady = errors.New("the windows kernel extension (driver) is not ready to accept commands")
ErrNoPacketID = errors.New("the packet has no ID, possibly because it was fast-tracked by the kernel extension")
driverPath string
service *kext_interface.KextService
kextFile *kext_interface.KextFile
)
const (
driverName = "PortmasterKext"
)
// Init initializes the DLL and the Kext (Kernel Driver).
func Init(path string) error {
driverPath = path
return nil
}
// Start intercepting.
func Start() error {
// initialize and start driver service
var err error
service, err = kext_interface.CreateKextService(driverName, driverPath)
if err != nil {
return fmt.Errorf("failed to create service: %w", err)
}
// Start service and open file
service.Start(true)
kextFile, err = service.OpenFile(1024)
if err != nil {
return fmt.Errorf("failed to open driver: %w", err)
}
return nil
}
// Stop intercepting.
func Stop() error {
// Prepare kernel for shutdown
err := shutdownRequest()
if err != nil {
log.Warningf("winkext: shutdown request failed: %s", err)
}
// Close the interface to the driver. Driver will continue to run.
kextFile.Close()
// Stop and delete the driver.
service.Stop(true)
service.Delete()
return nil
}
func shutdownRequest() error {
return kext_interface.WriteCommand(kextFile, kext_interface.BuildShutdown())
}
func SendLogRequest() error {
return kext_interface.WriteCommand(kextFile, kext_interface.BuildGetLogs())
}
// RecvVerdictRequest waits for the next verdict request from the kext. If a timeout is reached, both *VerdictRequest and error will be nil.
func RecvVerdictRequest() (*kext_interface.Info, error) {
return kext_interface.ReadInfo(kextFile)
}
// SetVerdict sets the verdict for a packet and/or connection.
func SetVerdict(pkt *Packet, verdict network.Verdict) error {
if verdict == network.VerdictRerouteToNameserver {
redirect := kext_interface.Redirect{Id: pkt.verdictRequest, RemoteAddress: []uint8{127, 0, 0, 1}, RemotePort: 53}
command := kext_interface.BuildRedirect(redirect)
kext_interface.WriteCommand(kextFile, command)
} else if verdict == network.VerdictRerouteToTunnel {
redirect := kext_interface.Redirect{Id: pkt.verdictRequest, RemoteAddress: []uint8{192, 168, 122, 196}, RemotePort: 717}
command := kext_interface.BuildRedirect(redirect)
kext_interface.WriteCommand(kextFile, command)
} else {
verdict := kext_interface.Verdict{Id: pkt.verdictRequest, Verdict: uint8(verdict)}
command := kext_interface.BuildVerdict(verdict)
kext_interface.WriteCommand(kextFile, command)
}
return nil
}
func ClearCache() error {
return kext_interface.WriteCommand(kextFile, kext_interface.BuildClearCache())
}
func UpdateVerdict(conn *network.Connection) error {
redirectAddress := []uint8{}
redirectPort := 0
if conn.Verdict.Active == network.VerdictRerouteToNameserver {
redirectAddress = []uint8{127, 0, 0, 1}
redirectPort = 53
}
if conn.Verdict.Active == network.VerdictRerouteToTunnel {
redirectAddress = []uint8{192, 168, 122, 196}
redirectPort = 717
}
update := kext_interface.Update{
Protocol: conn.Entity.Protocol,
LocalAddress: conn.LocalIP,
LocalPort: conn.LocalPort,
RemoteAddress: conn.Entity.IP,
RemotePort: conn.Entity.Port,
Verdict: uint8(conn.Verdict.Active),
RedirectAddress: redirectAddress,
RedirectPort: uint16(redirectPort),
}
command := kext_interface.BuildUpdate(update)
kext_interface.WriteCommand(kextFile, command)
return nil
}
func GetVersion() (*VersionInfo, error) {
data, err := kext_interface.ReadVersion(kextFile)
if err != nil {
return nil, err
}
version := &VersionInfo{
major: data[0],
minor: data[1],
revision: data[2],
build: data[3],
}
return version, nil
}
var sizeOfConnectionStat = uint32(unsafe.Sizeof(ConnectionStat{}))
func GetConnectionsStats() ([]ConnectionStat, error) {
return nil, nil
}

View file

@ -0,0 +1,110 @@
//go:build windows
// +build windows
package windowskext
import (
"sync"
"github.com/tevino/abool"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/packet"
)
// Packet represents an IP packet.
type Packet struct {
packet.Base
verdictRequest uint64
verdictSet *abool.AtomicBool
payloadLoaded bool
lock sync.Mutex
}
// FastTrackedByIntegration returns whether the packet has been fast-track
// accepted by the OS integration.
func (pkt *Packet) FastTrackedByIntegration() bool {
return false
}
// InfoOnly returns whether the packet is informational only and does not
// represent an actual packet.
func (pkt *Packet) InfoOnly() bool {
return false
}
// ExpectInfo returns whether the next packet is expected to be informational only.
func (pkt *Packet) ExpectInfo() bool {
return false
}
// GetPayload returns the full raw packet.
func (pkt *Packet) LoadPacketData() error {
return nil
}
// Accept accepts the packet.
func (pkt *Packet) Accept() error {
if pkt.verdictSet.SetToIf(false, true) {
return SetVerdict(pkt, -network.VerdictAccept)
}
return nil
}
// Block blocks the packet.
func (pkt *Packet) Block() error {
if pkt.verdictSet.SetToIf(false, true) {
return SetVerdict(pkt, -network.VerdictBlock)
}
return nil
}
// Drop drops the packet.
func (pkt *Packet) Drop() error {
if pkt.verdictSet.SetToIf(false, true) {
return SetVerdict(pkt, -network.VerdictDrop)
}
return nil
}
// PermanentAccept permanently accepts connection (and the current packet).
func (pkt *Packet) PermanentAccept() error {
if pkt.verdictSet.SetToIf(false, true) {
return SetVerdict(pkt, network.VerdictAccept)
}
return nil
}
// PermanentBlock permanently blocks connection (and the current packet).
func (pkt *Packet) PermanentBlock() error {
if pkt.verdictSet.SetToIf(false, true) {
return SetVerdict(pkt, network.VerdictBlock)
}
return nil
}
// PermanentDrop permanently drops connection (and the current packet).
func (pkt *Packet) PermanentDrop() error {
if pkt.verdictSet.SetToIf(false, true) {
return SetVerdict(pkt, network.VerdictDrop)
}
return nil
}
// RerouteToNameserver permanently reroutes the connection to the local nameserver (and the current packet).
func (pkt *Packet) RerouteToNameserver() error {
if pkt.verdictSet.SetToIf(false, true) {
return SetVerdict(pkt, network.VerdictRerouteToNameserver)
}
return nil
}
// RerouteToTunnel permanently reroutes the connection to the local tunnel entrypoint (and the current packet).
func (pkt *Packet) RerouteToTunnel() error {
if pkt.verdictSet.SetToIf(false, true) {
return SetVerdict(pkt, network.VerdictRerouteToTunnel)
}
return nil
}

View file

@ -0,0 +1,154 @@
//go:build windows
// +build windows
package windowskext
import (
"github.com/vlabo/portmaster_windows_rust_kext/kext_interface"
)
func createKextService(driverName string, driverPath string) (*kext_interface.KextService, error) {
return kext_interface.CreateKextService(driverName, driverPath)
}
// func deleteService(manager windows.Handle, service *KextService, driverName []uint16) error {
// // Stop and wait before deleting
// _ = service.stop(true)
// // Try to delete even if stop failed
// err := service.delete()
// if err != nil {
// return fmt.Errorf("failed to delete old service: %s", err)
// }
// // Wait until we can no longer open the old service.
// // Not very efficient but NotifyServiceStatusChange cannot be used with driver service.
// start := time.Now()
// timeLimit := time.Duration(30 * time.Second)
// for {
// handle, err := windows.OpenService(manager, &driverName[0], windows.SERVICE_ALL_ACCESS)
// if err != nil {
// break
// }
// _ = windows.CloseServiceHandle(handle)
// if time.Since(start) > timeLimit {
// return fmt.Errorf("time limit reached")
// }
// time.Sleep(100 * time.Millisecond)
// }
// return nil
// }
// func (s *KextService) isValid() bool {
// return s != nil && s.handle != winInvalidHandleValue && s.handle != 0
// }
// func (s *KextService) isRunning() (bool, error) {
// if !s.isValid() {
// return false, fmt.Errorf("kext service not initialized")
// }
// var status windows.SERVICE_STATUS
// err := windows.QueryServiceStatus(s.handle, &status)
// if err != nil {
// return false, err
// }
// return status.CurrentState == windows.SERVICE_RUNNING, nil
// }
// func waitForServiceStatus(handle windows.Handle, neededStatus uint32, timeLimit time.Duration) (bool, error) {
// var status windows.SERVICE_STATUS
// status.CurrentState = windows.SERVICE_NO_CHANGE
// start := time.Now()
// for status.CurrentState == neededStatus {
// err := windows.QueryServiceStatus(handle, &status)
// if err != nil {
// return false, fmt.Errorf("failed while waiting for service to start: %w", err)
// }
// if time.Since(start) > timeLimit {
// return false, fmt.Errorf("time limit reached")
// }
// // Sleep for 1/10 of the wait hint, recommended time from microsoft
// time.Sleep(time.Duration((status.WaitHint / 10)) * time.Millisecond)
// }
// return true, nil
// }
// func (s *KextService) start(wait bool) error {
// if !s.isValid() {
// return fmt.Errorf("kext service not initialized")
// }
// // Start the service:
// err := windows.StartService(s.handle, 0, nil)
// if err != nil {
// err = windows.GetLastError()
// if err != windows.ERROR_SERVICE_ALREADY_RUNNING {
// // Failed to start service; clean-up:
// var status windows.SERVICE_STATUS
// _ = windows.ControlService(s.handle, windows.SERVICE_CONTROL_STOP, &status)
// _ = windows.DeleteService(s.handle)
// _ = windows.CloseServiceHandle(s.handle)
// s.handle = winInvalidHandleValue
// return err
// }
// }
// // Wait for service to start
// if wait {
// success, err := waitForServiceStatus(s.handle, windows.SERVICE_RUNNING, time.Duration(10*time.Second))
// if err != nil || !success {
// return fmt.Errorf("service did not start: %w", err)
// }
// }
// return nil
// }
// func (s *KextService) stop(wait bool) error {
// if !s.isValid() {
// return fmt.Errorf("kext service not initialized")
// }
// // Stop the service
// var status windows.SERVICE_STATUS
// err := windows.ControlService(s.handle, windows.SERVICE_CONTROL_STOP, &status)
// if err != nil {
// return fmt.Errorf("service failed to stop: %w", err)
// }
// // Wait for service to stop
// if wait {
// success, err := waitForServiceStatus(s.handle, windows.SERVICE_STOPPED, time.Duration(10*time.Second))
// if err != nil || !success {
// return fmt.Errorf("service did not stop: %w", err)
// }
// }
// return nil
// }
// func (s *KextService) delete() error {
// if !s.isValid() {
// return fmt.Errorf("kext service not initialized")
// }
// err := windows.DeleteService(s.handle)
// if err != nil {
// return fmt.Errorf("failed to delete service: %s", err)
// }
// // Service wont be deleted until all handles are closed.
// err = windows.CloseServiceHandle(s.handle)
// if err != nil {
// return fmt.Errorf("failed to close service handle: %s", err)
// }
// s.handle = winInvalidHandleValue
// return nil
// }

1
go.mod
View file

@ -39,6 +39,7 @@ require (
github.com/tevino/abool v1.2.0
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
github.com/vincent-petithory/dataurl v1.0.0
github.com/vlabo/portmaster_windows_rust_kext/kext_interface v0.0.0-20240120091731-1a3450b13959
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e
golang.org/x/net v0.20.0
golang.org/x/sync v0.6.0

2
go.sum
View file

@ -275,6 +275,8 @@ github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OL
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/vlabo/portmaster_windows_rust_kext/kext_interface v0.0.0-20240120091731-1a3450b13959 h1:5j8cHx9n4drternoY4HXomea+4aYJuKMgnA3VhlG5WM=
github.com/vlabo/portmaster_windows_rust_kext/kext_interface v0.0.0-20240120091731-1a3450b13959/go.mod h1:PCv02zl4R2SbmEUDetMKO+kTfvMvsVVZuOzOXRMcHwE=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=

View file

@ -424,6 +424,7 @@ func NewIncompleteConnection(pkt packet.Packet) *Connection {
IPProtocol: info.Protocol,
Started: info.SeenAt.Unix(),
PID: info.PID,
Inbound: info.Inbound,
dataComplete: abool.NewBool(false),
}