Merge pull request #278 from safing/fix/netenv

Fix and improve netenv for Windows and Linux
This commit is contained in:
Daniel 2021-04-03 16:06:35 +02:00 committed by GitHub
commit 4390df8cb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 861 additions and 304 deletions

View file

@ -139,15 +139,37 @@ func fastTrackedPermit(pkt packet.Packet) (handled bool) {
switch meta.Protocol {
case packet.ICMP:
// Submit to ICMP listener.
submitted := netenv.SubmitPacketToICMPListener(pkt)
// Always permit ICMP.
log.Debugf("filter: fast-track accepting ICMP: %s", pkt)
_ = pkt.PermanentAccept()
// If the packet was submitted to the listener, we must not do a
// permanent accept, because then we won't see any future packets of that
// connection and thus cannot continue to submit them.
if submitted {
_ = pkt.Accept()
} else {
_ = pkt.PermanentAccept()
}
return true
case packet.ICMPv6:
// Submit to ICMP listener.
submitted := netenv.SubmitPacketToICMPListener(pkt)
// Always permit ICMPv6.
log.Debugf("filter: fast-track accepting ICMPv6: %s", pkt)
_ = pkt.PermanentAccept()
// If the packet was submitted to the listener, we must not do a
// permanent accept, because then we won't see any future packets of that
// connection and thus cannot continue to submit them.
if submitted {
_ = pkt.Accept()
} else {
_ = pkt.PermanentAccept()
}
return true
case packet.UDP, packet.TCP:

View file

@ -179,11 +179,13 @@ func (q *Queue) packetHandler(ctx context.Context) func(nfqueue.Attribute) int {
verdictPending: abool.New(),
}
if attrs.Payload != nil {
pkt.Payload = *attrs.Payload
if attrs.Payload == nil {
// There is not payload.
log.Warningf("nfqueue: packet #%s has no payload", pkt.pktID)
return 0
}
if err := pmpacket.Parse(pkt.Payload, pkt.Info()); err != nil {
if err := pmpacket.Parse(*attrs.Payload, &pkt.Base); err != nil {
log.Warningf("nfqueue: failed to parse payload: %s", err)
_ = pkt.Drop()
return 0

View file

@ -65,6 +65,11 @@ 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

View file

@ -230,6 +230,7 @@ func GetPayload(packetID uint32, packetSize uint32) ([]byte, error) {
if packetSize < uint32(len(buf)) {
return buf[:packetSize], nil
}
return buf, nil
}

View file

@ -24,7 +24,7 @@ type Packet struct {
}
// GetPayload returns the full raw packet.
func (pkt *Packet) GetPayload() ([]byte, error) {
func (pkt *Packet) LoadPacketData() error {
pkt.lock.Lock()
defer pkt.lock.Unlock()
@ -33,17 +33,21 @@ func (pkt *Packet) GetPayload() ([]byte, error) {
payload, err := GetPayload(pkt.verdictRequest.id, pkt.verdictRequest.packetSize)
if err != nil {
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to load payload %s", err)
log.Errorf("windowskext: failed to load payload %s", err)
return nil, packet.ErrFailedToLoadPayload
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to load payload: %s", err)
return packet.ErrFailedToLoadPayload
}
err = packet.Parse(payload, &pkt.Base)
if err != nil {
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)
return packet.ErrFailedToLoadPayload
}
pkt.Payload = payload
}
if len(pkt.Payload) == 0 {
return nil, packet.ErrFailedToLoadPayload
if len(pkt.Raw()) == 0 {
return packet.ErrFailedToLoadPayload
}
return pkt.Payload, nil
return nil
}
// Accept accepts the packet.

View file

@ -1,14 +1,13 @@
package netenv
import (
"fmt"
"testing"
)
func TestGetAssignedAddresses(t *testing.T) {
ipv4, ipv6, err := GetAssignedAddresses()
fmt.Printf("all v4: %v", ipv4)
fmt.Printf("all v6: %v", ipv6)
t.Logf("all v4: %v", ipv4)
t.Logf("all v6: %v", ipv6)
if err != nil {
t.Fatalf("failed to get addresses: %s", err)
}
@ -19,8 +18,8 @@ func TestGetAssignedAddresses(t *testing.T) {
func TestGetAssignedGlobalAddresses(t *testing.T) {
ipv4, ipv6, err := GetAssignedGlobalAddresses()
fmt.Printf("all global v4: %v", ipv4)
fmt.Printf("all global v6: %v", ipv6)
t.Logf("all global v4: %v", ipv4)
t.Logf("all global v6: %v", ipv6)
if err != nil {
t.Fatalf("failed to get addresses: %s", err)
}

51
netenv/api.go Normal file
View file

@ -0,0 +1,51 @@
package netenv
import (
"errors"
"github.com/safing/portbase/api"
)
func registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{
Path: "network/gateways",
Read: api.PermitUser,
StructFunc: func(ar *api.Request) (i interface{}, err error) {
return Gateways(), nil
},
Name: "Get Default Gateways",
Description: "Returns the current active default gateways of the network.",
}); err != nil {
return err
}
if err := api.RegisterEndpoint(api.Endpoint{
Path: "network/nameservers",
Read: api.PermitUser,
StructFunc: func(ar *api.Request) (i interface{}, err error) {
return Nameservers(), nil
},
Name: "Get System Nameservers",
Description: "Returns the currently configured nameservers on the OS.",
}); err != nil {
return err
}
if err := api.RegisterEndpoint(api.Endpoint{
Path: "network/location",
Read: api.PermitUser,
StructFunc: func(ar *api.Request) (i interface{}, err error) {
locs, ok := GetInternetLocation()
if ok {
return locs, nil
}
return nil, errors.New("no location data available")
},
Name: "Get Approximate Internet Location",
Description: "Returns an approximation of where the device is on the Internet.",
}); err != nil {
return err
}
return nil
}

View file

@ -8,6 +8,8 @@ import (
"net"
"sync"
"github.com/safing/portbase/log"
"github.com/godbus/dbus/v5"
)
@ -19,7 +21,7 @@ var (
func getNameserversFromDbus() ([]Nameserver, error) { //nolint:gocognit // TODO
// cmdline tool for exploring: gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager
var nameservers []Nameserver
var ns []Nameserver
var err error
dbusConnLock.Lock()
@ -34,7 +36,7 @@ func getNameserversFromDbus() ([]Nameserver, error) { //nolint:gocognit // TODO
primaryConnectionVariant, err := getNetworkManagerProperty(dbusConn, dbus.ObjectPath("/org/freedesktop/NetworkManager"), "org.freedesktop.NetworkManager.PrimaryConnection")
if err != nil {
return nil, err
return nil, fmt.Errorf("dbus: failed to access NetworkManager.PrimaryConnection: %s", err)
}
primaryConnection, ok := primaryConnectionVariant.Value().(dbus.ObjectPath)
if !ok {
@ -43,7 +45,7 @@ func getNameserversFromDbus() ([]Nameserver, error) { //nolint:gocognit // TODO
activeConnectionsVariant, err := getNetworkManagerProperty(dbusConn, dbus.ObjectPath("/org/freedesktop/NetworkManager"), "org.freedesktop.NetworkManager.ActiveConnections")
if err != nil {
return nil, err
return nil, fmt.Errorf("dbus: failed to access NetworkManager.ActiveConnections: %s", err)
}
activeConnections, ok := activeConnectionsVariant.Value().([]dbus.ObjectPath)
if !ok {
@ -58,102 +60,107 @@ func getNameserversFromDbus() ([]Nameserver, error) { //nolint:gocognit // TODO
}
for _, activeConnection := range sortedConnections {
ip4ConfigVariant, err := getNetworkManagerProperty(dbusConn, activeConnection, "org.freedesktop.NetworkManager.Connection.Active.Ip4Config")
new, err := dbusGetInterfaceNameservers(dbusConn, activeConnection, 4)
if err != nil {
return nil, err
}
ip4Config, ok := ip4ConfigVariant.Value().(dbus.ObjectPath)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.Connection.Active.Ip4Config", activeConnection)
log.Warningf("failed to get nameserver: %s", err)
} else {
ns = append(ns, new...)
}
nameserverIP4sVariant, err := getNetworkManagerProperty(dbusConn, ip4Config, "org.freedesktop.NetworkManager.IP4Config.Nameservers")
new, err = dbusGetInterfaceNameservers(dbusConn, activeConnection, 6)
if err != nil {
return nil, err
}
nameserverIP4s, ok := nameserverIP4sVariant.Value().([]uint32)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP4Config.Nameservers", ip4Config)
log.Warningf("failed to get nameserver: %s", err)
} else {
ns = append(ns, new...)
}
}
nameserverDomainsVariant, err := getNetworkManagerProperty(dbusConn, ip4Config, "org.freedesktop.NetworkManager.IP4Config.Domains")
if err != nil {
return nil, err
}
nameserverDomains, ok := nameserverDomainsVariant.Value().([]string)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP4Config.Domains", ip4Config)
}
return ns, nil
}
nameserverSearchesVariant, err := getNetworkManagerProperty(dbusConn, ip4Config, "org.freedesktop.NetworkManager.IP4Config.Searches")
if err != nil {
return nil, err
}
nameserverSearches, ok := nameserverSearchesVariant.Value().([]string)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP4Config.Searches", ip4Config)
}
func dbusGetInterfaceNameservers(dbusConn *dbus.Conn, interfaceObject dbus.ObjectPath, ipVersion uint8) ([]Nameserver, error) {
ipConfigPropertyKey := fmt.Sprintf("org.freedesktop.NetworkManager.Connection.Active.Ip%dConfig", ipVersion)
nameserversIPsPropertyKey := fmt.Sprintf("org.freedesktop.NetworkManager.IP%dConfig.Nameservers", ipVersion)
nameserversDomainsPropertyKey := fmt.Sprintf("org.freedesktop.NetworkManager.IP%dConfig.Domains", ipVersion)
nameserversSearchesPropertyKey := fmt.Sprintf("org.freedesktop.NetworkManager.IP%dConfig.Searches", ipVersion)
// Get Interface Configuration.
ipConfigVariant, err := getNetworkManagerProperty(dbusConn, interfaceObject, ipConfigPropertyKey)
if err != nil {
return nil, fmt.Errorf("failed to access %s:%s: %s", interfaceObject, ipConfigPropertyKey, err)
}
ipConfig, ok := ipConfigVariant.Value().(dbus.ObjectPath)
if !ok {
return nil, fmt.Errorf("could not assert type of %s:%s", interfaceObject, ipConfigPropertyKey)
}
// Check if interface is active in the selected IP version
if !ipConfig.IsValid() || ipConfig == "/" {
return nil, nil
}
// Get Nameserver IPs
nameserverIPsVariant, err := getNetworkManagerProperty(dbusConn, ipConfig, nameserversIPsPropertyKey)
if err != nil {
return nil, fmt.Errorf("failed to access %s:%s: %s", ipConfig, nameserversIPsPropertyKey, err)
}
var nameserverIPs []net.IP
switch ipVersion {
case 4:
nameserverIP4s, ok := nameserverIPsVariant.Value().([]uint32)
if !ok {
return nil, fmt.Errorf("could not assert type of %s:%s", ipConfig, nameserversIPsPropertyKey)
}
for _, ip := range nameserverIP4s {
a := uint8(ip / 16777216)
b := uint8((ip % 16777216) / 65536)
c := uint8((ip % 65536) / 256)
d := uint8(ip % 256)
nameservers = append(nameservers, Nameserver{
IP: net.IPv4(d, c, b, a),
Search: append(nameserverDomains, nameserverSearches...),
})
nameserverIPs = append(nameserverIPs, net.IPv4(d, c, b, a))
}
ip6ConfigVariant, err := getNetworkManagerProperty(dbusConn, activeConnection, "org.freedesktop.NetworkManager.Connection.Active.Ip6Config")
if err != nil {
return nil, err
}
ip6Config, ok := ip6ConfigVariant.Value().(dbus.ObjectPath)
case 6:
nameserverIP6s, ok := nameserverIPsVariant.Value().([][]byte)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.Connection.Active.Ip6Config", activeConnection)
return nil, fmt.Errorf("could not assert type of %s:%s", ipConfig, nameserversIPsPropertyKey)
}
nameserverIP6sVariant, err := getNetworkManagerProperty(dbusConn, ip6Config, "org.freedesktop.NetworkManager.IP6Config.Nameservers")
if err != nil {
return nil, err
}
nameserverIP6s, ok := nameserverIP6sVariant.Value().([][]byte)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP6Config.Nameservers", ip6Config)
}
nameserverDomainsVariant, err = getNetworkManagerProperty(dbusConn, ip6Config, "org.freedesktop.NetworkManager.IP6Config.Domains")
if err != nil {
return nil, err
}
nameserverDomains, ok = nameserverDomainsVariant.Value().([]string)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP6Config.Domains", ip6Config)
}
nameserverSearchesVariant, err = getNetworkManagerProperty(dbusConn, ip6Config, "org.freedesktop.NetworkManager.IP6Config.Searches")
if err != nil {
return nil, err
}
nameserverSearches, ok = nameserverSearchesVariant.Value().([]string)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP6Config.Searches", ip6Config)
}
for _, ip := range nameserverIP6s {
if len(ip) != 16 {
return nil, fmt.Errorf("dbus: query returned IPv6 address (%s) with invalid length", ip)
return nil, fmt.Errorf("query returned IPv6 address with invalid length: %q", ip)
}
nameservers = append(nameservers, Nameserver{
IP: net.IP(ip),
Search: append(nameserverDomains, nameserverSearches...),
})
nameserverIPs = append(nameserverIPs, net.IP(ip))
}
}
return nameservers, nil
// Get Nameserver Domains
nameserverDomainsVariant, err := getNetworkManagerProperty(dbusConn, ipConfig, nameserversDomainsPropertyKey)
if err != nil {
return nil, fmt.Errorf("failed to access %s:%s: %s", ipConfig, nameserversDomainsPropertyKey, err)
}
nameserverDomains, ok := nameserverDomainsVariant.Value().([]string)
if !ok {
return nil, fmt.Errorf("could not assert type of %s:%s", ipConfig, nameserversDomainsPropertyKey)
}
// Get Nameserver Searches
nameserverSearchesVariant, err := getNetworkManagerProperty(dbusConn, ipConfig, nameserversSearchesPropertyKey)
if err != nil {
return nil, fmt.Errorf("failed to access %s:%s: %s", ipConfig, nameserversSearchesPropertyKey, err)
}
nameserverSearches, ok := nameserverSearchesVariant.Value().([]string)
if !ok {
return nil, fmt.Errorf("could not assert type of %s:%s", ipConfig, nameserversSearchesPropertyKey)
}
ns := make([]Nameserver, 0, len(nameserverIPs))
searchDomains := append(nameserverDomains, nameserverSearches...)
for _, nameserverIP := range nameserverIPs {
ns = append(ns, Nameserver{
IP: nameserverIP,
Search: searchDomains,
})
}
return ns, nil
}
func getConnectivityStateFromDbus() (OnlineStatus, error) {

View file

@ -7,7 +7,6 @@ import (
"os"
"strings"
"sync"
"time"
"github.com/miekg/dns"
@ -15,35 +14,25 @@ import (
"github.com/safing/portmaster/network/netutils"
)
const (
gatewaysRecheck = 2 * time.Second
nameserversRecheck = 2 * time.Second
)
var (
gateways = make([]net.IP, 0)
gatewaysLock sync.Mutex
gatewaysExpires = time.Now()
gateways = make([]net.IP, 0)
gatewaysLock sync.Mutex
gatewaysNetworkChangedFlag = GetNetworkChangedFlag()
nameservers = make([]Nameserver, 0)
nameserversLock sync.Mutex
nameserversExpires = time.Now()
nameservers = make([]Nameserver, 0)
nameserversLock sync.Mutex
nameserversNetworkChangedFlag = GetNetworkChangedFlag()
)
// Gateways returns the currently active gateways.
func Gateways() []net.IP {
// locking
gatewaysLock.Lock()
defer gatewaysLock.Unlock()
// cache
if gatewaysExpires.After(time.Now()) {
// Check if the network changed, if not, return cache.
if !gatewaysNetworkChangedFlag.IsSet() {
return gateways
}
// update cache expiry when finished
defer func() {
gatewaysExpires = time.Now().Add(gatewaysRecheck)
}()
// logic
gatewaysNetworkChangedFlag.Refresh()
gateways = make([]net.IP, 0)
var decoded []byte
@ -119,17 +108,13 @@ func Gateways() []net.IP {
// Nameservers returns the currently active nameservers.
func Nameservers() []Nameserver {
// locking
nameserversLock.Lock()
defer nameserversLock.Unlock()
// cache
if nameserversExpires.After(time.Now()) {
// Check if the network changed, if not, return cache.
if !nameserversNetworkChangedFlag.IsSet() {
return nameservers
}
// update cache expiry when finished
defer func() {
nameserversExpires = time.Now().Add(nameserversRecheck)
}()
nameserversNetworkChangedFlag.Refresh()
// logic
// TODO: try:

View file

@ -0,0 +1,11 @@
package netenv
import "testing"
func TestLinuxEnvironment(t *testing.T) {
nameserversTest, err := getNameserversFromResolvconf()
if err != nil {
t.Errorf("failed to get namerservers from resolvconf: %s", err)
}
t.Logf("nameservers from resolvconf: %+v", nameserversTest)
}

View file

@ -1,21 +1,11 @@
// +build linux
package netenv
import "testing"
func TestEnvironment(t *testing.T) {
nameserversTest, err := getNameserversFromResolvconf()
if err != nil {
t.Errorf("failed to get namerservers from resolvconf: %s", err)
}
t.Logf("nameservers from resolvconf: %v", nameserversTest)
nameserversTest = Nameservers()
t.Logf("nameservers: %v", nameserversTest)
nameserversTest := Nameservers()
t.Logf("nameservers: %+v", nameserversTest)
gatewaysTest := Gateways()
t.Logf("gateways: %v", gatewaysTest)
t.Logf("gateways: %+v", gatewaysTest)
}

View file

@ -3,93 +3,182 @@ package netenv
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"os/exec"
"strings"
"sync"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/utils/osdetail"
)
const (
nameserversRecheck = 2 * time.Second
)
var (
nameservers = make([]Nameserver, 0)
nameserversLock sync.Mutex
nameserversExpires = time.Now()
)
// Nameservers returns the currently active nameservers.
func Nameservers() []Nameserver {
// locking
nameserversLock.Lock()
defer nameserversLock.Unlock()
// cache
if nameserversExpires.After(time.Now()) {
return nameservers
}
// update cache expiry when finished
defer func() {
nameserversExpires = time.Now().Add(nameserversRecheck)
}()
// reset
nameservers = make([]Nameserver, 0)
// This is a preliminary workaround until we have more time for proper interface using iphlpapi.dll
// TODO: make nice implementation
var output = make(chan []byte, 1)
module.StartWorker("get assigned nameservers", func(ctx context.Context) error {
cmd := exec.CommandContext(ctx, "nslookup", "localhost")
data, err := cmd.CombinedOutput()
if err != nil {
log.Debugf("netenv: failed to get assigned nameserves: %s", err)
output <- nil
} else {
output <- data
}
return nil
})
select {
case data := <-output:
scanner := bufio.NewScanner(bytes.NewReader(data))
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
// check if we found the correct line
if !strings.HasPrefix(scanner.Text(), "Address:") {
continue
}
// split into fields, check if we have enough fields
fields := strings.Fields(scanner.Text())
if len(fields) < 2 {
continue
}
// parse nameserver, return if valid IP found
ns := net.ParseIP(fields[1])
if ns != nil {
nameservers = append(nameservers, Nameserver{
IP: ns,
})
return nameservers
}
}
log.Debug("netenv: could not find assigned nameserver")
return nameservers
case <-time.After(5 * time.Second):
log.Debug("netenv: timed out while getting assigned nameserves")
}
return nameservers
}
// Gateways returns the currently active gateways.
func Gateways() []net.IP {
return nil
defaultIf := getDefaultInterface()
if defaultIf == nil {
return nil
}
// Collect gateways.
var gw []net.IP
if defaultIf.IPv4DefaultGateway != nil {
gw = append(gw, defaultIf.IPv4DefaultGateway)
}
if defaultIf.IPv6DefaultGateway != nil {
gw = append(gw, defaultIf.IPv6DefaultGateway)
}
return gw
}
// Nameservers returns the currently active nameservers.
func Nameservers() []Nameserver {
defaultIf := getDefaultInterface()
if defaultIf == nil {
return nil
}
// Compile search list.
var search []string
if defaultIf.DNSServerConfig != nil {
if defaultIf.DNSServerConfig.Suffix != "" {
search = append(search, defaultIf.DNSServerConfig.Suffix)
}
if len(defaultIf.DNSServerConfig.SuffixSearchList) > 0 {
search = append(search, defaultIf.DNSServerConfig.SuffixSearchList...)
}
}
// Compile nameservers.
var ns []Nameserver
for _, nsIP := range defaultIf.DNSServer {
ns = append(ns, Nameserver{
IP: nsIP,
Search: search,
})
}
return ns
}
const (
defaultInterfaceRecheck = 2 * time.Second
)
var (
defaultInterface *defaultNetInterface
defaultInterfaceLock sync.Mutex
defaultInterfaceNetworkChangedFlag = GetNetworkChangedFlag()
)
type defaultNetInterface struct {
InterfaceIndex string
IPv6Address net.IP
IPv4Address net.IP
IPv6DefaultGateway net.IP
IPv4DefaultGateway net.IP
DNSServer []net.IP
DNSServerConfig *dnsServerConfig
}
type dnsServerConfig struct {
Suffix string
SuffixSearchList []string
}
func getDefaultInterface() *defaultNetInterface {
defaultInterfaceLock.Lock()
defer defaultInterfaceLock.Unlock()
// Check if the network changed, if not, return cache.
if !defaultInterfaceNetworkChangedFlag.IsSet() {
return defaultInterface
}
defaultInterfaceNetworkChangedFlag.Refresh()
// Get interface data from Windows.
interfaceData, err := osdetail.RunPowershellCmd("Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Select-Object -First 1 | Get-NetIPConfiguration | Format-List")
if err != nil {
log.Warningf("netenv: failed to get interface data: %s", err)
return nil
}
// TODO: It would be great to get this as json. Powershell can do this,
// but it just spits out lots of weird data instead of the same strings
// seen in the list.
newIf := &defaultNetInterface{}
// Scan data for needed fields.
scanner := bufio.NewScanner(bytes.NewBufferString(interfaceData))
scanner.Split(bufio.ScanLines)
var segmentKey, segmentValue, previousKey string
for scanner.Scan() {
segments := strings.SplitN(scanner.Text(), " : ", 2)
// Check what the line gives us.
switch len(segments) {
case 2:
// This is a new key and value.
segmentKey = strings.TrimSpace(segments[0])
segmentValue = strings.TrimSpace(segments[1])
previousKey = segmentKey
case 1:
// This is another value for the previous key.
segmentKey = previousKey
segmentValue = strings.TrimSpace(segments[0])
default:
continue
}
// Ignore empty lines.
if segmentValue == "" {
continue
}
// Parse and assign value to struct.
switch segmentKey {
case "InterfaceIndex":
newIf.InterfaceIndex = segmentValue
case "IPv6Address":
newIf.IPv6Address = net.ParseIP(segmentValue)
case "IPv4Address":
newIf.IPv4Address = net.ParseIP(segmentValue)
case "IPv6DefaultGateway":
newIf.IPv6DefaultGateway = net.ParseIP(segmentValue)
case "IPv4DefaultGateway":
newIf.IPv4DefaultGateway = net.ParseIP(segmentValue)
case "DNSServer":
newIP := net.ParseIP(segmentValue)
if newIP != nil {
newIf.DNSServer = append(newIf.DNSServer, newIP)
}
}
}
// Get Search Scopes for this interface.
if newIf.InterfaceIndex != "" {
dnsConfigData, err := osdetail.RunPowershellCmd(fmt.Sprintf(
"Get-DnsClient -InterfaceIndex %s | ConvertTo-Json -Depth 1",
newIf.InterfaceIndex,
))
if err != nil {
log.Warningf("netenv: failed to get dns server config data: %s", err)
} else {
// Parse data into struct.
dnsConfig := &dnsServerConfig{}
err := json.Unmarshal([]byte(dnsConfigData), dnsConfig)
if err != nil {
log.Warningf("netenv: failed to get dns server config data: %s", err)
} else {
newIf.DNSServerConfig = dnsConfig
}
}
} else {
log.Warning("netenv: could not get dns server config data, because default interface index is missing")
}
// Assign new value to cache and return.
defaultInterface = newIf
return defaultInterface
}

View file

@ -0,0 +1,11 @@
package netenv
import "testing"
func TestWindowsEnvironment(t *testing.T) {
defaultIf := getDefaultInterface()
if defaultIf == nil {
t.Error("failed to get default interface")
}
t.Logf("default interface: %+v", defaultIf)
}

100
netenv/icmp_listener.go Normal file
View file

@ -0,0 +1,100 @@
package netenv
import (
"sync"
"github.com/tevino/abool"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/packet"
)
/*
This ICMP listening system is a simple system for components to listen to ICMP
packets via the firewall.
The main use case for this is to receive ICMP packets that are not always
delivered correctly, or need special permissions and or sockets to receive
them. This is the case when doing a traceroute.
In order to keep it simple, the system is only designed to be used by one
"user" at at time. Further calls to ListenToICMP will wait for the previous
operation to complete.
*/
var (
// listenICMPLock locks the ICMP listening system for one user at a time.
listenICMPLock sync.Mutex
// listenICMPEnabled defines whether or not the firewall should submit ICMP
// packets to this interface.
listenICMPEnabled = abool.New()
// listenICMPInput is created for every use of the ICMP listenting system.
listenICMPInput chan packet.Packet
listenICMPInputLock sync.Mutex
)
// ListenToICMP returns a new channel for listenting to icmp packets. Please
// note that any icmp packet will be passed and filtering must be done on
// the side of the caller. The caller must call the returned done function when
// done with the listener.
func ListenToICMP() (packets chan packet.Packet, done func()) {
// Lock for single use.
listenICMPLock.Lock()
// Create new input channel.
listenICMPInputLock.Lock()
listenICMPInput = make(chan packet.Packet, 100)
listenICMPEnabled.Set()
listenICMPInputLock.Unlock()
return listenICMPInput, func() {
// Release for someone else to use.
defer listenICMPLock.Unlock()
// Close input channel.
listenICMPInputLock.Lock()
listenICMPEnabled.UnSet()
close(listenICMPInput)
listenICMPInputLock.Unlock()
}
}
// SubmitPacketToICMPListener checks if an ICMP packet should be submitted to
// the listener. If so, it is submitted right away. The function returns
// whether or not the packet should be submitted, not if it was successful.
func SubmitPacketToICMPListener(pkt packet.Packet) (submitted bool) {
// Hot path.
if !listenICMPEnabled.IsSet() {
return false
}
// Slow path.
submitPacketToICMPListenerSlow(pkt)
return true
}
func submitPacketToICMPListenerSlow(pkt packet.Packet) {
// Make sure the payload is available.
if err := pkt.LoadPacketData(); err != nil {
log.Warningf("netenv: failed to get payload for ICMP listener: %s", err)
return
}
// Send to input channel.
listenICMPInputLock.Lock()
defer listenICMPInputLock.Unlock()
// Check if still enabled.
if !listenICMPEnabled.IsSet() {
return
}
// Send to channel, if possible.
select {
case listenICMPInput <- pkt:
default:
log.Warning("netenv: failed to send packet payload to ICMP listener: channel full")
}
}

View file

@ -2,23 +2,32 @@ package netenv
import (
"errors"
"fmt"
"net"
"sync"
"syscall"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/icmp"
"github.com/google/gopacket/layers"
"github.com/safing/portbase/log"
"github.com/safing/portbase/rng"
"github.com/safing/portmaster/intel/geoip"
"github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet"
)
var (
locationTestingIPv4 = "1.1.1.1"
locationTestingIPv4Addr *net.IPAddr
locations = &DeviceLocations{
All: make(map[string]*DeviceLocation),
}
locationsLock sync.Mutex
gettingLocationsLock sync.Mutex
locationNetworkChangedFlag = GetNetworkChangedFlag()
)
func prepLocation() (err error) {
@ -26,62 +35,250 @@ func prepLocation() (err error) {
return err
}
// GetApproximateInternetLocation returns the nearest detectable IP address. If one or more global IP addresses are configured, one of them is returned. Currently only support IPv4. Else, the IP address of the nearest ping-answering internet node is returned.
func GetApproximateInternetLocation() (net.IP, error) { //nolint:gocognit
// TODO: Create IPv6 version of GetApproximateInternetLocation
type DeviceLocations struct {
Best *DeviceLocation
All map[string]*DeviceLocation
}
// First check if we have an assigned IPv6 address. Return that if available.
globalIPv4, _, err := GetAssignedGlobalAddresses()
if err != nil {
log.Warningf("netenv: location approximation: failed to get assigned global addresses: %s", err)
} else if len(globalIPv4) > 0 {
return globalIPv4[0], nil
func copyDeviceLocations() *DeviceLocations {
locationsLock.Lock()
defer locationsLock.Unlock()
// Create a copy of the locations, but not the entries.
cp := *locations
cp.All = make(map[string]*DeviceLocation, len(locations.All))
for k, v := range locations.All {
cp.All[k] = v
}
// Create OS specific ICMP Listener.
conn, err := newICMPListener(locationTestingIPv4)
if err != nil {
return nil, fmt.Errorf("failed to listen: %s", err)
return &cp
}
// DeviceLocation represents a single IP and metadata. It must not be changed
// once created.
type DeviceLocation struct {
IP net.IP
Continent string
Country string
ASN uint
ASOrg string
Source DeviceLocationSource
SourceAccuracy int
}
// IsMoreAccurateThan checks if the device location is more accurate than the
// given one.
func (dl *DeviceLocation) IsMoreAccurateThan(other *DeviceLocation) bool {
switch {
case dl.SourceAccuracy > other.SourceAccuracy:
// Higher accuracy is better.
return true
case dl.ASN != 0 && other.ASN == 0:
// Having an ASN is better than having none.
return true
case dl.Country == "" && other.Country != "":
// Having a Country is better than having none.
return true
}
return false
}
type DeviceLocationSource string
const (
SourceInterface DeviceLocationSource = "interface"
SourcePeer DeviceLocationSource = "peer"
SourceUPNP DeviceLocationSource = "upnp"
SourceTraceroute DeviceLocationSource = "traceroute"
SourceOther DeviceLocationSource = "other"
)
func (dls DeviceLocationSource) Accuracy() int {
switch dls {
case SourceInterface:
return 5
case SourcePeer:
return 4
case SourceUPNP:
return 3
case SourceTraceroute:
return 2
case SourceOther:
return 1
default:
return 0
}
}
func SetInternetLocation(ip net.IP, source DeviceLocationSource) (ok bool) {
// Check if IP is global.
if netutils.GetIPScope(ip) != netutils.Global {
return false
}
// Create new location.
loc := &DeviceLocation{
IP: ip,
Source: source,
SourceAccuracy: source.Accuracy(),
}
// Get geoip information, but continue if it fails.
geoLoc, err := geoip.GetLocation(ip)
if err != nil {
log.Warningf("netenv: failed to get geolocation data of %s (from %s): %s", ip, source, err)
} else {
loc.Continent = geoLoc.Continent.Code
loc.Country = geoLoc.Country.ISOCode
loc.ASN = geoLoc.AutonomousSystemNumber
loc.ASOrg = geoLoc.AutonomousSystemOrganization
}
locationsLock.Lock()
defer locationsLock.Unlock()
// Add to locations, if better.
key := loc.IP.String()
existing, ok := locations.All[key]
if ok && existing.IsMoreAccurateThan(loc) {
// Existing entry is more accurate, abort adding.
// Return true, because the IP address is already part of the locations.
return true
}
locations.All[key] = loc
// Find best location.
best := loc
for _, dl := range locations.All {
if dl.IsMoreAccurateThan(best) {
best = dl
}
}
locations.Best = best
return true
}
// DEPRECATED: Please use GetInternetLocation instead.
func GetApproximateInternetLocation() (net.IP, error) {
loc, ok := GetInternetLocation()
if !ok {
return nil, errors.New("no location data available")
}
return loc.Best.IP, nil
}
func GetInternetLocation() (deviceLocations *DeviceLocations, ok bool) {
gettingLocationsLock.Lock()
defer gettingLocationsLock.Unlock()
// Check if the network changed, if not, return cache.
if !locationNetworkChangedFlag.IsSet() {
return copyDeviceLocations(), true
}
locationNetworkChangedFlag.Refresh()
// Check different sources, return on first success.
switch {
case getLocationFromInterfaces():
case getLocationFromTraceroute():
default:
return nil, false
}
// Return gathered locations.
cp := copyDeviceLocations()
return cp, cp.Best != nil
}
func getLocationFromInterfaces() (ok bool) {
globalIPv4, globalIPv6, err := GetAssignedGlobalAddresses()
if err != nil {
log.Warningf("netenv: location: failed to get assigned global addresses: %s", err)
return false
}
for _, ip := range globalIPv4 {
if SetInternetLocation(ip, SourceInterface) {
ok = true
}
}
for _, ip := range globalIPv6 {
if SetInternetLocation(ip, SourceInterface) {
ok = true
}
}
return ok
}
// TODO: Check feasibility of getting the external IP via UPnP.
/*
func getLocationFromUPnP() (ok bool) {
// Endoint: urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress
// A first test showed that a router did offer that endpoint, but did not
// return an IP addres.
return false
}
*/
func getLocationFromTraceroute() (ok bool) {
// Create connection.
conn, err := net.ListenPacket("ip4:icmp", "")
if err != nil {
log.Warningf("netenv: location: failed to open icmp conn: %s", err)
return false
}
defer conn.Close()
v4Conn := ipv4.NewPacketConn(conn)
// Generate a random ID for the ICMP packets.
msgID, err := rng.Number(0xFFFF) // uint16
generatedID, err := rng.Number(0xFFFF) // uint16
if err != nil {
return nil, fmt.Errorf("failed to generate ID: %s", err)
log.Warningf("netenv: location: failed to generate icmp msg ID: %s", err)
return false
}
msgID := int(generatedID)
var msgSeq int
// Create ICMP message body
// Create ICMP message body.
pingMessage := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: int(msgID),
Seq: 0, // increased before marshal
ID: msgID,
Seq: msgSeq, // Is increased before marshalling.
Data: []byte{},
},
}
recvBuffer := make([]byte, 1500)
maxHops := 4 // add one for every reply that is not global
next:
// Get additional listener for ICMP messages via the firewall.
icmpPacketsViaFirewall, doneWithListeningToICMP := ListenToICMP()
defer doneWithListeningToICMP()
nextHop:
for i := 1; i <= maxHops; i++ {
minSeq := msgSeq + 1
repeat:
for j := 1; j <= 2; j++ { // Try every hop twice.
// Increase sequence number.
pingMessage.Body.(*icmp.Echo).Seq++
msgSeq++
pingMessage.Body.(*icmp.Echo).Seq = msgSeq
// Make packet data.
pingPacket, err := pingMessage.Marshal(nil)
if err != nil {
return nil, err
log.Warningf("netenv: location: failed to build icmp packet: %s", err)
return false
}
// Set TTL on IP packet.
err = v4Conn.SetTTL(i)
if err != nil {
return nil, err
log.Warningf("netenv: location: failed to set icmp packet TTL: %s", err)
return false
}
// Send ICMP packet.
@ -91,72 +288,106 @@ next:
continue
}
}
return nil, err
log.Warningf("netenv: location: failed to send icmp packet: %s", err)
return false
}
// Listen for replies to the ICMP packet.
// Listen for replies of the ICMP packet.
listen:
for {
// Set read timeout.
err = conn.SetReadDeadline(
time.Now().Add(
time.Duration(i*2+30) * time.Millisecond,
),
)
if err != nil {
return nil, err
}
// Read next packet.
n, src, err := conn.ReadFrom(recvBuffer)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
// Continue with next packet if we timeout
continue repeat
}
return nil, err
}
// Parse remote IP address.
addr, ok := src.(*net.IPAddr)
remoteIP, icmpPacket, ok := recvICMP(i, icmpPacketsViaFirewall)
if !ok {
return nil, fmt.Errorf("failed to parse IP: %s", src.String())
continue repeat
}
// Continue if we receive a packet from ourself. This is specific to Windows.
if me, err := IsMyIP(addr.IP); err == nil && me {
log.Tracef("netenv: location approximation: ignoring own message from %s", src)
// Pre-filter by message type.
switch icmpPacket.TypeCode.Type() {
case layers.ICMPv4TypeEchoReply:
// Check if the ID and sequence match.
if icmpPacket.Id != uint16(msgID) {
continue listen
}
if icmpPacket.Seq < uint16(minSeq) {
continue listen
}
// We received a reply, so we did not trigger a time exceeded response on the way.
// This means we were not able to find the nearest router to us.
return false
case layers.ICMPv4TypeDestinationUnreachable,
layers.ICMPv4TypeTimeExceeded:
// Continue processing.
default:
continue listen
}
// If we received something from a global IP address, we have succeeded and can return immediately.
if netutils.GetIPScope(addr.IP).IsGlobal() {
return addr.IP, nil
// Parse copy of origin icmp packet that triggered the error.
if len(icmpPacket.Payload) != ipv4.HeaderLen+8 {
continue listen
}
// For everey non-global reply received, increase the maximum hops to try.
maxHops++
// Parse the ICMP message.
icmpReply, err := icmp.ParseMessage(1, recvBuffer[:n])
originalMessage, err := icmp.ParseMessage(1, icmpPacket.Payload[ipv4.HeaderLen:])
if err != nil {
log.Warningf("netenv: location approximation: failed to parse ICMP message: %s", err)
continue listen
}
originalEcho, ok := originalMessage.Body.(*icmp.Echo)
if !ok {
continue listen
}
// Check if the ID and sequence match.
if originalEcho.ID != int(msgID) {
continue listen
}
if originalEcho.Seq < minSeq {
continue listen
}
// React based on message type.
switch icmpReply.Type {
case ipv4.ICMPTypeTimeExceeded, ipv4.ICMPTypeEchoReply:
log.Tracef("netenv: location approximation: receveived %q from %s", icmpReply.Type, addr.IP)
continue next
case ipv4.ICMPTypeDestinationUnreachable:
return nil, fmt.Errorf("destination unreachable")
default:
log.Tracef("netenv: location approximation: unexpected ICMP reply: received %q from %s", icmpReply.Type, addr.IP)
switch icmpPacket.TypeCode.Type() {
case layers.ICMPv4TypeDestinationUnreachable:
// We have received a valid destination unreachable response, abort.
return false
case layers.ICMPv4TypeTimeExceeded:
// We have received a valid time exceeded error.
// If message came from a global unicast, us it!
if netutils.GetIPScope(remoteIP) == netutils.Global {
return SetInternetLocation(remoteIP, SourceTraceroute)
}
// Otherwise, continue.
continue nextHop
}
}
}
}
return nil, errors.New("no usable response to any icmp message")
// We did not receive anything actionable.
return false
}
func recvICMP(currentHop int, icmpPacketsViaFirewall chan packet.Packet) (
remoteIP net.IP, imcpPacket *layers.ICMPv4, ok bool) {
for {
select {
case pkt := <-icmpPacketsViaFirewall:
if pkt.IsOutbound() {
continue
}
if pkt.Layers() == nil {
continue
}
icmpLayer := pkt.Layers().Layer(layers.LayerTypeICMPv4)
if icmpLayer == nil {
continue
}
icmp4, ok := icmpLayer.(*layers.ICMPv4)
if !ok {
continue
}
return pkt.Info().RemoteIP(), icmp4, true
case <-time.After(time.Duration(currentHop*10+50) * time.Millisecond):
return nil, nil, false
}
}
}

View file

@ -21,9 +21,9 @@ func TestGetApproximateInternetLocation(t *testing.T) {
t.Skip("skipping privileged test, active with -privileged argument")
}
ip, err := GetApproximateInternetLocation()
loc, err := GetInternetLocation()
if err != nil {
t.Fatalf("GetApproximateInternetLocation failed: %s", err)
}
t.Logf("GetApproximateInternetLocation: %s", ip.String())
t.Logf("GetApproximateInternetLocation: %+v", loc)
}

View file

@ -29,6 +29,10 @@ func prep() error {
}
func start() error {
if err := registerAPIEndpoints(); err != nil {
return err
}
module.StartServiceWorker(
"monitor network changes",
0,

View file

@ -9,12 +9,23 @@ import (
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/utils"
)
var (
networkChangeCheckTrigger = make(chan struct{}, 1)
networkChangeCheckTrigger = make(chan struct{}, 1)
networkChangedBroadcastFlag = utils.NewBroadcastFlag()
)
func GetNetworkChangedFlag() *utils.Flag {
return networkChangedBroadcastFlag.NewFlag()
}
func notifyOfNetworkChange() {
networkChangedBroadcastFlag.NotifyAndReset()
module.TriggerEvent(NetworkChangedEvent, nil)
}
func triggerNetworkChangeCheck() {
select {
case networkChangeCheckTrigger <- struct{}{}:
@ -82,7 +93,7 @@ serviceLoop:
if trigger {
triggerOnlineStatusInvestigation()
}
module.TriggerEvent(NetworkChangedEvent, nil)
notifyOfNetworkChange()
}
}

View file

@ -4,14 +4,18 @@ import (
"context"
"fmt"
"net"
"github.com/google/gopacket"
)
// Base is a base structure for satisfying the Packet interface.
type Base struct {
ctx context.Context
info Info
connID string
Payload []byte
ctx context.Context
info Info
connID string
layers gopacket.Packet
layer3Data []byte
layer5Data []byte
}
// SetCtx sets the packet context.
@ -65,9 +69,24 @@ func (pkt *Base) HasPorts() bool {
return false
}
// GetPayload returns the packet payload. In some cases, this will fetch the payload from the os integration system.
func (pkt *Base) GetPayload() ([]byte, error) {
return pkt.Payload, ErrFailedToLoadPayload
// LoadPacketData loads packet data from the integration, if not yet done.
func (pkt *Base) LoadPacketData() error {
return ErrFailedToLoadPayload
}
// Layers returns the parsed layer data.
func (pkt *Base) Layers() gopacket.Packet {
return pkt.layers
}
// Raw returns the raw Layer 3 Network Data.
func (pkt *Base) Raw() []byte {
return pkt.layer3Data
}
// Payload returns the raw Layer 5 Network Data.
func (pkt *Base) Payload() []byte {
return pkt.layer5Data
}
// GetConnectionID returns the link ID for this packet.
@ -214,9 +233,14 @@ type Packet interface {
SetInbound()
SetOutbound()
HasPorts() bool
GetPayload() ([]byte, error)
GetConnectionID() string
// PAYLOAD
LoadPacketData() error
Layers() gopacket.Packet
Raw() []byte
Payload() []byte
// MATCHING
MatchesAddress(bool, IPProtocol, *net.IPNet, uint16) bool
MatchesIP(bool, *net.IPNet) bool

View file

@ -102,10 +102,11 @@ func checkError(packet gopacket.Packet, _ *Info) error {
}
// Parse parses an IP packet and saves the information in the given packet object.
func Parse(packetData []byte, pktInfo *Info) error {
func Parse(packetData []byte, pktBase *Base) (err error) {
if len(packetData) == 0 {
return errors.New("empty packet")
}
pktBase.layer3Data = packetData
ipVersion := packetData[0] >> 4
var networkLayerType gopacket.LayerType
@ -137,11 +138,15 @@ func Parse(packetData []byte, pktInfo *Info) error {
}
for _, dec := range availableDecoders {
if err := dec(packet, pktInfo); err != nil {
if err := dec(packet, pktBase.Info()); err != nil {
return err
}
}
pktBase.layers = packet
if transport := packet.TransportLayer(); transport != nil {
pktBase.layer5Data = transport.LayerPayload()
}
return nil
}

View file

@ -58,8 +58,13 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
// Check if this is the system resolver.
switch runtime.GOOS {
case "windows":
if (p.Path == `C:\Windows\System32\svchost.exe` || p.Path == `C:\Windows\system32\svchost.exe`) &&
(strings.Contains(p.SpecialDetail, "Dnscache") || strings.Contains(p.CmdLine, "-k NetworkService")) {
// Depending on the OS version System32 may be capitalized or not.
if (p.Path == `C:\Windows\System32\svchost.exe` ||
p.Path == `C:\Windows\system32\svchost.exe`) &&
// This comes from the windows tasklist command and should be pretty consistent.
(strings.Contains(p.SpecialDetail, "Dnscache") ||
// As an alternative in case of failure, we try to match the svchost.exe service parameter.
strings.Contains(p.CmdLine, "-s Dnscache")) {
profileID = profile.SystemResolverProfileID
}
case "linux":