wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
30
service/netenv/addresses_test.go
Normal file
30
service/netenv/addresses_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetAssignedAddresses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ipv4, ipv6, err := GetAssignedAddresses()
|
||||
t.Logf("all v4: %v", ipv4)
|
||||
t.Logf("all v6: %v", ipv6)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get addresses: %s", err)
|
||||
}
|
||||
if len(ipv4) == 0 && len(ipv6) == 0 {
|
||||
t.Fatal("GetAssignedAddresses did not return any addresses")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAssignedGlobalAddresses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ipv4, ipv6, err := GetAssignedGlobalAddresses()
|
||||
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)
|
||||
}
|
||||
}
|
||||
177
service/netenv/adresses.go
Normal file
177
service/netenv/adresses.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
)
|
||||
|
||||
// GetAssignedAddresses returns the assigned IPv4 and IPv6 addresses of the host.
|
||||
func GetAssignedAddresses() (ipv4 []net.IP, ipv6 []net.IP, err error) {
|
||||
addrs, err := osGetInterfaceAddrs()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
netAddr, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
log.Warningf("netenv: interface address of unexpected type %T", addr)
|
||||
continue
|
||||
}
|
||||
|
||||
if ip4 := netAddr.IP.To4(); ip4 != nil {
|
||||
ipv4 = append(ipv4, ip4)
|
||||
} else {
|
||||
ipv6 = append(ipv6, netAddr.IP)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetAssignedGlobalAddresses returns the assigned global IPv4 and IPv6 addresses of the host.
|
||||
func GetAssignedGlobalAddresses() (ipv4 []net.IP, ipv6 []net.IP, err error) {
|
||||
allv4, allv6, err := GetAssignedAddresses()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, ip4 := range allv4 {
|
||||
if netutils.GetIPScope(ip4).IsGlobal() {
|
||||
ipv4 = append(ipv4, ip4)
|
||||
}
|
||||
}
|
||||
for _, ip6 := range allv6 {
|
||||
if netutils.GetIPScope(ip6).IsGlobal() {
|
||||
ipv6 = append(ipv6, ip6)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
myNetworks []*net.IPNet
|
||||
myNetworksLock sync.Mutex
|
||||
myNetworksNetworkChangedFlag = GetNetworkChangedFlag()
|
||||
myNetworksRefreshError error //nolint:errname // Not what the linter thinks this is for.
|
||||
myNetworksDontRefreshUntil time.Time
|
||||
)
|
||||
|
||||
// refreshMyNetworks refreshes the networks held in refreshMyNetworks.
|
||||
// The caller must hold myNetworksLock.
|
||||
func refreshMyNetworks() error {
|
||||
// Check if we already refreshed recently.
|
||||
if time.Now().Before(myNetworksDontRefreshUntil) {
|
||||
// Return previous error, if available.
|
||||
if myNetworksRefreshError != nil {
|
||||
return fmt.Errorf("failed to previously refresh interface addresses: %w", myNetworksRefreshError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
myNetworksRefreshError = nil
|
||||
myNetworksDontRefreshUntil = time.Now().Add(1 * time.Second)
|
||||
|
||||
// Refresh assigned networks.
|
||||
interfaceNetworks, err := osGetInterfaceAddrs()
|
||||
if err != nil {
|
||||
// In some cases the system blocks on this call, which piles up to
|
||||
// literally over thousand goroutines wanting to try this again.
|
||||
myNetworksRefreshError = err
|
||||
return fmt.Errorf("failed to refresh interface addresses: %w", err)
|
||||
}
|
||||
myNetworks = make([]*net.IPNet, 0, len(interfaceNetworks))
|
||||
for _, ifNet := range interfaceNetworks {
|
||||
ipNet, ok := ifNet.(*net.IPNet)
|
||||
if !ok {
|
||||
log.Warningf("netenv: interface network of unexpected type %T", ifNet)
|
||||
continue
|
||||
}
|
||||
|
||||
myNetworks = append(myNetworks, ipNet)
|
||||
}
|
||||
|
||||
// Reset changed flag.
|
||||
myNetworksNetworkChangedFlag.Refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsMyIP returns whether the given unicast IP is currently configured on the local host.
|
||||
// Broadcast or multicast addresses will never match, even if valid and in use.
|
||||
// Function is optimized with the assumption that is likely that the IP is mine.
|
||||
func IsMyIP(ip net.IP) (yes bool, err error) {
|
||||
// Check for IPs that don't need extra checks.
|
||||
switch netutils.GetIPScope(ip) { //nolint:exhaustive // Only looking for specific values.
|
||||
case netutils.HostLocal:
|
||||
return true, nil
|
||||
case netutils.LocalMulticast, netutils.GlobalMulticast:
|
||||
return false, nil
|
||||
}
|
||||
|
||||
myNetworksLock.Lock()
|
||||
defer myNetworksLock.Unlock()
|
||||
|
||||
// Check if the network changed.
|
||||
if myNetworksNetworkChangedFlag.IsSet() {
|
||||
err := refreshMyNetworks()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check against assigned IPs.
|
||||
for _, myNet := range myNetworks {
|
||||
if ip.Equal(myNet.IP) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check for other IPs in range and broadcast addresses.
|
||||
// Do this in a second loop, as an IP will match in
|
||||
// most cases and network matching is more expensive.
|
||||
for _, myNet := range myNetworks {
|
||||
if myNet.Contains(ip) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Could not find IP anywhere. Refresh network to be sure.
|
||||
err = refreshMyNetworks()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check against assigned IPs again.
|
||||
for _, myNet := range myNetworks {
|
||||
if ip.Equal(myNet.IP) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// GetLocalNetwork uses the given IP to search for a network configured on the
|
||||
// device and returns it.
|
||||
func GetLocalNetwork(ip net.IP) (myNet *net.IPNet, err error) {
|
||||
myNetworksLock.Lock()
|
||||
defer myNetworksLock.Unlock()
|
||||
|
||||
// Check if the network changed.
|
||||
if myNetworksNetworkChangedFlag.IsSet() {
|
||||
err := refreshMyNetworks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the IP address is in my networks.
|
||||
for _, myNet := range myNetworks {
|
||||
if myNet.Contains(ip) {
|
||||
return myNet, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
67
service/netenv/api.go
Normal file
67
service/netenv/api.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
)
|
||||
|
||||
func registerAPIEndpoints() error {
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "network/gateways",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
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,
|
||||
BelongsTo: module,
|
||||
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,
|
||||
BelongsTo: module,
|
||||
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
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "network/location/traceroute",
|
||||
Read: api.PermitUser,
|
||||
BelongsTo: module,
|
||||
StructFunc: func(ar *api.Request) (i interface{}, err error) {
|
||||
return getLocationFromTraceroute(&DeviceLocations{})
|
||||
},
|
||||
Name: "Get Approximate Internet Location via Traceroute",
|
||||
Description: "Returns an approximation of where the device is on the Internet using a the traceroute technique.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
223
service/netenv/dbus_linux.go
Normal file
223
service/netenv/dbus_linux.go
Normal file
@@ -0,0 +1,223 @@
|
||||
//go:build !server
|
||||
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
var (
|
||||
dbusConn *dbus.Conn
|
||||
dbusConnLock sync.Mutex
|
||||
)
|
||||
|
||||
func getNameserversFromDbus() ([]Nameserver, error) { //nolint:gocognit // TODO
|
||||
// cmdline tool for exploring: gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager
|
||||
|
||||
var ns []Nameserver
|
||||
var err error
|
||||
|
||||
dbusConnLock.Lock()
|
||||
defer dbusConnLock.Unlock()
|
||||
|
||||
if dbusConn == nil {
|
||||
dbusConn, err = dbus.SystemBus()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
primaryConnectionVariant, err := getNetworkManagerProperty(dbusConn, dbus.ObjectPath("/org/freedesktop/NetworkManager"), "org.freedesktop.NetworkManager.PrimaryConnection")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dbus: failed to access NetworkManager.PrimaryConnection: %w", err)
|
||||
}
|
||||
primaryConnection, ok := primaryConnectionVariant.Value().(dbus.ObjectPath)
|
||||
if !ok {
|
||||
return nil, errors.New("dbus: could not assert type of /org/freedesktop/NetworkManager:org.freedesktop.NetworkManager.PrimaryConnection")
|
||||
}
|
||||
|
||||
activeConnectionsVariant, err := getNetworkManagerProperty(dbusConn, dbus.ObjectPath("/org/freedesktop/NetworkManager"), "org.freedesktop.NetworkManager.ActiveConnections")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dbus: failed to access NetworkManager.ActiveConnections: %w", err)
|
||||
}
|
||||
activeConnections, ok := activeConnectionsVariant.Value().([]dbus.ObjectPath)
|
||||
if !ok {
|
||||
return nil, errors.New("dbus: could not assert type of /org/freedesktop/NetworkManager:org.freedesktop.NetworkManager.ActiveConnections")
|
||||
}
|
||||
|
||||
sortedConnections := []dbus.ObjectPath{primaryConnection}
|
||||
for _, activeConnection := range activeConnections {
|
||||
if !objectPathInSlice(activeConnection, sortedConnections) {
|
||||
sortedConnections = append(sortedConnections, activeConnection)
|
||||
}
|
||||
}
|
||||
|
||||
for _, activeConnection := range sortedConnections {
|
||||
newNameservers, err := dbusGetInterfaceNameservers(dbusConn, activeConnection, 4)
|
||||
if err != nil {
|
||||
log.Warningf("failed to get nameserver: %s", err)
|
||||
} else {
|
||||
ns = append(ns, newNameservers...)
|
||||
}
|
||||
|
||||
newNameservers, err = dbusGetInterfaceNameservers(dbusConn, activeConnection, 6)
|
||||
if err != nil {
|
||||
log.Warningf("failed to get nameserver: %s", err)
|
||||
} else {
|
||||
ns = append(ns, newNameservers...)
|
||||
}
|
||||
}
|
||||
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
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: %w", 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: %w", 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)
|
||||
nameserverIPs = append(nameserverIPs, net.IPv4(d, c, b, a))
|
||||
}
|
||||
case 6:
|
||||
nameserverIP6s, ok := nameserverIPsVariant.Value().([][]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not assert type of %s:%s", ipConfig, nameserversIPsPropertyKey)
|
||||
}
|
||||
for _, ip := range nameserverIP6s {
|
||||
if len(ip) != 16 {
|
||||
return nil, fmt.Errorf("query returned IPv6 address with invalid length: %q", ip)
|
||||
}
|
||||
nameserverIPs = append(nameserverIPs, net.IP(ip))
|
||||
}
|
||||
}
|
||||
|
||||
// Get Nameserver Domains
|
||||
nameserverDomainsVariant, err := getNetworkManagerProperty(dbusConn, ipConfig, nameserversDomainsPropertyKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to access %s:%s: %w", 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: %w", 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...) //nolint:gocritic
|
||||
for _, nameserverIP := range nameserverIPs {
|
||||
ns = append(ns, Nameserver{
|
||||
IP: nameserverIP,
|
||||
Search: searchDomains,
|
||||
})
|
||||
}
|
||||
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func getConnectivityStateFromDbus() (OnlineStatus, error) {
|
||||
var err error
|
||||
|
||||
dbusConnLock.Lock()
|
||||
defer dbusConnLock.Unlock()
|
||||
|
||||
if dbusConn == nil {
|
||||
dbusConn, err = dbus.SystemBus()
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
connectivityStateVariant, err := getNetworkManagerProperty(dbusConn, dbus.ObjectPath("/org/freedesktop/NetworkManager"), "org.freedesktop.NetworkManager.Connectivity")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
connectivityState, ok := connectivityStateVariant.Value().(uint32)
|
||||
if !ok {
|
||||
return 0, errors.New("dbus: could not assert type of /org/freedesktop/NetworkManager:org.freedesktop.NetworkManager.Connectivity")
|
||||
}
|
||||
|
||||
// NMConnectivityState
|
||||
// NM_CONNECTIVITY_UNKNOWN = 0 Network connectivity is unknown.
|
||||
// NM_CONNECTIVITY_NONE = 1 The host is not connected to any network.
|
||||
// NM_CONNECTIVITY_PORTAL = 2 The host is behind a captive portal and cannot reach the full Internet.
|
||||
// NM_CONNECTIVITY_LIMITED = 3 The host is connected to a network, but does not appear to be able to reach the full Internet.
|
||||
// NM_CONNECTIVITY_FULL = 4 The host is connected to a network, and appears to be able to reach the full Internet.
|
||||
|
||||
switch connectivityState {
|
||||
case 0:
|
||||
return StatusUnknown, nil
|
||||
case 1:
|
||||
return StatusOffline, nil
|
||||
case 2:
|
||||
return StatusPortal, nil
|
||||
case 3:
|
||||
return StatusLimited, nil
|
||||
case 4:
|
||||
return StatusOnline, nil
|
||||
}
|
||||
|
||||
return StatusUnknown, nil
|
||||
}
|
||||
|
||||
func getNetworkManagerProperty(conn *dbus.Conn, objectPath dbus.ObjectPath, property string) (dbus.Variant, error) {
|
||||
object := conn.Object("org.freedesktop.NetworkManager", objectPath)
|
||||
return object.GetProperty(property)
|
||||
}
|
||||
|
||||
func objectPathInSlice(a dbus.ObjectPath, list []dbus.ObjectPath) bool {
|
||||
for _, b := range list {
|
||||
if string(b) == string(a) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
33
service/netenv/dbus_linux_test.go
Normal file
33
service/netenv/dbus_linux_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDbus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode because it fails in the CI")
|
||||
}
|
||||
|
||||
if _, err := os.Stat("/var/run/dbus/system_bus_socket"); errors.Is(err, fs.ErrNotExist) {
|
||||
t.Logf("skipping dbus tests, as dbus does not seem to be installed: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
nameservers, err := getNameserversFromDbus()
|
||||
if err != nil {
|
||||
t.Errorf("getNameserversFromDbus failed: %s", err)
|
||||
}
|
||||
t.Logf("getNameserversFromDbus: %v", nameservers)
|
||||
|
||||
connectivityState, err := getConnectivityStateFromDbus()
|
||||
if err != nil {
|
||||
t.Errorf("getConnectivityStateFromDbus failed: %s", err)
|
||||
}
|
||||
t.Logf("getConnectivityStateFromDbus: %v", connectivityState)
|
||||
}
|
||||
19
service/netenv/dialing.go
Normal file
19
service/netenv/dialing.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package netenv
|
||||
|
||||
import "net"
|
||||
|
||||
var localAddrFactory func(network string) net.Addr
|
||||
|
||||
// SetLocalAddrFactory supplies the environment package with a function to get permitted local addresses for connections.
|
||||
func SetLocalAddrFactory(laf func(network string) net.Addr) {
|
||||
if localAddrFactory == nil {
|
||||
localAddrFactory = laf
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalAddr(network string) net.Addr {
|
||||
if localAddrFactory != nil {
|
||||
return localAddrFactory(network)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
20
service/netenv/environment.go
Normal file
20
service/netenv/environment.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// TODO: find a good way to identify a network
|
||||
// best options until now:
|
||||
// MAC of gateway
|
||||
// domain parameter of dhcp
|
||||
|
||||
// TODO: get dhcp servers on windows:
|
||||
// doc: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365917
|
||||
// this info might already be included in the interfaces api provided by golang!
|
||||
|
||||
// Nameserver describes a system assigned namserver.
|
||||
type Nameserver struct {
|
||||
IP net.IP
|
||||
Search []string
|
||||
}
|
||||
29
service/netenv/environment_default.go
Normal file
29
service/netenv/environment_default.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//+build !windows,!linux
|
||||
|
||||
package netenv
|
||||
|
||||
import "net"
|
||||
|
||||
func Nameservers() []Nameserver {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Gateways() []net.IP {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: implement using
|
||||
// ifconfig
|
||||
// scutil --nwi
|
||||
// scutil --proxy
|
||||
// networksetup -listallnetworkservices
|
||||
// networksetup -listnetworkserviceorder
|
||||
// networksetup -getdnsservers "Wi-Fi"
|
||||
// networksetup -getsearchdomains <networkservice>
|
||||
// networksetup -getftpproxy <networkservice>
|
||||
// networksetup -getwebproxy <networkservice>
|
||||
// networksetup -getsecurewebproxy <networkservice>
|
||||
// networksetup -getstreamingproxy <networkservice>
|
||||
// networksetup -getgopherproxy <networkservice>
|
||||
// networksetup -getsocksfirewallproxy <networkservice>
|
||||
// route -n get default
|
||||
211
service/netenv/environment_linux.go
Normal file
211
service/netenv/environment_linux.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
)
|
||||
|
||||
var (
|
||||
gateways = make([]net.IP, 0)
|
||||
gatewaysLock sync.Mutex
|
||||
gatewaysNetworkChangedFlag = GetNetworkChangedFlag()
|
||||
|
||||
nameservers = make([]Nameserver, 0)
|
||||
nameserversLock sync.Mutex
|
||||
nameserversNetworkChangedFlag = GetNetworkChangedFlag()
|
||||
)
|
||||
|
||||
// Gateways returns the currently active gateways.
|
||||
func Gateways() []net.IP {
|
||||
gatewaysLock.Lock()
|
||||
defer gatewaysLock.Unlock()
|
||||
// Check if the network changed, if not, return cache.
|
||||
if !gatewaysNetworkChangedFlag.IsSet() {
|
||||
return gateways
|
||||
}
|
||||
gatewaysNetworkChangedFlag.Refresh()
|
||||
|
||||
gateways = make([]net.IP, 0)
|
||||
var decoded []byte
|
||||
|
||||
// open file
|
||||
route, err := os.Open("/proc/net/route")
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not read /proc/net/route: %s", err)
|
||||
return gateways
|
||||
}
|
||||
defer func() {
|
||||
_ = route.Close()
|
||||
}()
|
||||
|
||||
// file scanner
|
||||
scanner := bufio.NewScanner(route)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
// parse
|
||||
for scanner.Scan() {
|
||||
line := strings.SplitN(scanner.Text(), "\t", 4)
|
||||
if len(line) < 4 {
|
||||
continue
|
||||
}
|
||||
if line[1] == "00000000" {
|
||||
decoded, err = hex.DecodeString(line[2])
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not parse gateway %s from /proc/net/route: %s", line[2], err)
|
||||
continue
|
||||
}
|
||||
if len(decoded) != 4 {
|
||||
log.Warningf("environment: decoded gateway %s from /proc/net/route has wrong length", decoded)
|
||||
continue
|
||||
}
|
||||
gate := net.IPv4(decoded[3], decoded[2], decoded[1], decoded[0])
|
||||
gateways = append(gateways, gate)
|
||||
}
|
||||
}
|
||||
|
||||
// open file
|
||||
v6route, err := os.Open("/proc/net/ipv6_route")
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not read /proc/net/ipv6_route: %s", err)
|
||||
return gateways
|
||||
}
|
||||
defer func() {
|
||||
_ = v6route.Close()
|
||||
}()
|
||||
|
||||
// file scanner
|
||||
scanner = bufio.NewScanner(v6route)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
// parse
|
||||
for scanner.Scan() {
|
||||
line := strings.SplitN(scanner.Text(), " ", 6)
|
||||
if len(line) < 6 {
|
||||
continue
|
||||
}
|
||||
if line[0] == "00000000000000000000000000000000" && line[4] != "00000000000000000000000000000000" {
|
||||
decoded, err := hex.DecodeString(line[4])
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not parse gateway %s from /proc/net/ipv6_route: %s", line[2], err)
|
||||
continue
|
||||
}
|
||||
if len(decoded) != 16 {
|
||||
log.Warningf("environment: decoded gateway %s from /proc/net/ipv6_route has wrong length", decoded)
|
||||
continue
|
||||
}
|
||||
gate := net.IP(decoded)
|
||||
gateways = append(gateways, gate)
|
||||
}
|
||||
}
|
||||
|
||||
return gateways
|
||||
}
|
||||
|
||||
// Nameservers returns the currently active nameservers.
|
||||
func Nameservers() []Nameserver {
|
||||
nameserversLock.Lock()
|
||||
defer nameserversLock.Unlock()
|
||||
// Check if the network changed, if not, return cache.
|
||||
if !nameserversNetworkChangedFlag.IsSet() {
|
||||
return nameservers
|
||||
}
|
||||
nameserversNetworkChangedFlag.Refresh()
|
||||
|
||||
// logic
|
||||
// TODO: try:
|
||||
// 1. NetworkManager DBUS
|
||||
// 2. /etc/resolv.conf
|
||||
// 2.1. if /etc/resolv.conf has localhost nameserver, check for dnsmasq config (are there others?)
|
||||
nameservers = make([]Nameserver, 0)
|
||||
|
||||
// get nameservers from DBUS
|
||||
dbusNameservers, err := getNameserversFromDbus()
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not get nameservers from dbus: %s", err)
|
||||
} else {
|
||||
nameservers = addNameservers(nameservers, dbusNameservers)
|
||||
}
|
||||
|
||||
// get nameservers from /etc/resolv.conf
|
||||
resolvconfNameservers, err := getNameserversFromResolvconf()
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not get nameservers from resolvconf: %s", err)
|
||||
} else {
|
||||
nameservers = addNameservers(nameservers, resolvconfNameservers)
|
||||
}
|
||||
|
||||
return nameservers
|
||||
}
|
||||
|
||||
func getNameserversFromResolvconf() ([]Nameserver, error) {
|
||||
// open file
|
||||
resolvconf, err := os.Open("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not read /etc/resolv.conf: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = resolvconf.Close()
|
||||
}()
|
||||
|
||||
// file scanner
|
||||
scanner := bufio.NewScanner(resolvconf)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
var searchDomains []string
|
||||
var servers []net.IP
|
||||
|
||||
// parse
|
||||
for scanner.Scan() {
|
||||
line := strings.SplitN(scanner.Text(), " ", 3)
|
||||
if len(line) < 2 {
|
||||
continue
|
||||
}
|
||||
switch line[0] {
|
||||
case "search":
|
||||
if netutils.IsValidFqdn(dns.Fqdn(line[1])) {
|
||||
searchDomains = append(searchDomains, line[1])
|
||||
}
|
||||
case "nameserver":
|
||||
ip := net.ParseIP(line[1])
|
||||
if ip != nil {
|
||||
servers = append(servers, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// build array
|
||||
nameservers := make([]Nameserver, 0, len(servers))
|
||||
for _, server := range servers {
|
||||
nameservers = append(nameservers, Nameserver{
|
||||
IP: server,
|
||||
Search: searchDomains,
|
||||
})
|
||||
}
|
||||
return nameservers, nil
|
||||
}
|
||||
|
||||
func addNameservers(nameservers, newNameservers []Nameserver) []Nameserver {
|
||||
for _, newNameserver := range newNameservers {
|
||||
found := false
|
||||
for _, nameserver := range nameservers {
|
||||
if nameserver.IP.Equal(newNameserver.IP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
nameservers = append(nameservers, newNameserver)
|
||||
}
|
||||
}
|
||||
return nameservers
|
||||
}
|
||||
13
service/netenv/environment_linux_test.go
Normal file
13
service/netenv/environment_linux_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package netenv
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLinuxEnvironment(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
nameserversTest, err := getNameserversFromResolvconf()
|
||||
if err != nil {
|
||||
t.Errorf("failed to get namerservers from resolvconf: %s", err)
|
||||
}
|
||||
t.Logf("nameservers from resolvconf: %+v", nameserversTest)
|
||||
}
|
||||
13
service/netenv/environment_test.go
Normal file
13
service/netenv/environment_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package netenv
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEnvironment(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
nameserversTest := Nameservers()
|
||||
t.Logf("nameservers: %+v", nameserversTest)
|
||||
|
||||
gatewaysTest := Gateways()
|
||||
t.Logf("gateways: %+v", gatewaysTest)
|
||||
}
|
||||
184
service/netenv/environment_windows.go
Normal file
184
service/netenv/environment_windows.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils/osdetail"
|
||||
)
|
||||
|
||||
// Gateways returns the currently active gateways.
|
||||
func Gateways() []net.IP {
|
||||
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.NewBuffer(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
|
||||
}
|
||||
11
service/netenv/environment_windows_test.go
Normal file
11
service/netenv/environment_windows_test.go
Normal 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)
|
||||
}
|
||||
111
service/netenv/icmp_listener.go
Normal file
111
service/netenv/icmp_listener.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/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 a 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
|
||||
listenICMPInputTargetIP net.IP
|
||||
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(targetIP net.IP) (packets chan packet.Packet, done func()) {
|
||||
// Lock for single use.
|
||||
listenICMPLock.Lock()
|
||||
|
||||
// Create new input channel.
|
||||
listenICMPInputLock.Lock()
|
||||
listenICMPInput = make(chan packet.Packet, 100)
|
||||
listenICMPInputTargetIP = targetIP
|
||||
listenICMPEnabled.Set()
|
||||
listenICMPInputLock.Unlock()
|
||||
|
||||
return listenICMPInput, func() {
|
||||
// Release for someone else to use.
|
||||
defer listenICMPLock.Unlock()
|
||||
|
||||
// Close input channel.
|
||||
listenICMPInputLock.Lock()
|
||||
listenICMPEnabled.UnSet()
|
||||
listenICMPInputTargetIP = nil
|
||||
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.
|
||||
return submitPacketToICMPListenerSlow(pkt)
|
||||
}
|
||||
|
||||
func submitPacketToICMPListenerSlow(pkt packet.Packet) (submitted bool) {
|
||||
// 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 false
|
||||
}
|
||||
|
||||
// Send to input channel.
|
||||
listenICMPInputLock.Lock()
|
||||
defer listenICMPInputLock.Unlock()
|
||||
|
||||
// Check if still enabled.
|
||||
if !listenICMPEnabled.IsSet() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only listen for outbound packets to the target IP.
|
||||
if pkt.IsOutbound() &&
|
||||
listenICMPInputTargetIP != nil &&
|
||||
!pkt.Info().Dst.Equal(listenICMPInputTargetIP) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Send to channel, if possible.
|
||||
select {
|
||||
case listenICMPInput <- pkt:
|
||||
default:
|
||||
log.Warning("netenv: failed to send packet payload to ICMP listener: channel full")
|
||||
}
|
||||
return true
|
||||
}
|
||||
556
service/netenv/location.go
Normal file
556
service/netenv/location.go
Normal file
@@ -0,0 +1,556 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket/layers"
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv4"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/rng"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
var (
|
||||
// locationTestingIPv4 holds the IP address of the server that should be
|
||||
// tracerouted to find the location of the device. The ping will never reach
|
||||
// the destination in most cases.
|
||||
// The selection of this IP requires sensitivity, as the IP address must be
|
||||
// far enough away to produce good results.
|
||||
// At the same time, the IP address should be common and not raise attention.
|
||||
locationTestingIPv4 = "1.1.1.1"
|
||||
locationTestingIPv4Addr *net.IPAddr
|
||||
|
||||
locations = &DeviceLocations{}
|
||||
locationsLock sync.Mutex
|
||||
gettingLocationsLock sync.Mutex
|
||||
locationNetworkChangedFlag = GetNetworkChangedFlag()
|
||||
)
|
||||
|
||||
func prepLocation() (err error) {
|
||||
locationTestingIPv4Addr, err = net.ResolveIPAddr("ip", locationTestingIPv4)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeviceLocations holds multiple device locations.
|
||||
type DeviceLocations struct {
|
||||
All []*DeviceLocation
|
||||
}
|
||||
|
||||
// Best returns the best (most accurate) device location.
|
||||
func (dls *DeviceLocations) Best() *DeviceLocation {
|
||||
if len(dls.All) > 0 {
|
||||
return dls.All[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BestV4 returns the best (most accurate) IPv4 device location.
|
||||
func (dls *DeviceLocations) BestV4() *DeviceLocation {
|
||||
for _, loc := range dls.All {
|
||||
if loc.IPVersion == packet.IPv4 {
|
||||
return loc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BestV6 returns the best (most accurate) IPv6 device location.
|
||||
func (dls *DeviceLocations) BestV6() *DeviceLocation {
|
||||
for _, loc := range dls.All {
|
||||
if loc.IPVersion == packet.IPv6 {
|
||||
return loc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy creates a copy of the locations, but not the individual entries.
|
||||
func (dls *DeviceLocations) Copy() *DeviceLocations {
|
||||
cp := &DeviceLocations{
|
||||
All: make([]*DeviceLocation, len(locations.All)),
|
||||
}
|
||||
copy(cp.All, locations.All)
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
// AddLocation adds a location.
|
||||
func (dls *DeviceLocations) AddLocation(dl *DeviceLocation) {
|
||||
if dls == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add to locations, if better.
|
||||
var exists bool
|
||||
for i, existing := range dls.All {
|
||||
if (dl.IP == nil && existing.IP == nil) || dl.IP.Equal(existing.IP) {
|
||||
exists = true
|
||||
if dl.IsMoreAccurateThan(existing) {
|
||||
// Replace
|
||||
dls.All[i] = dl
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
dls.All = append(dls.All, dl)
|
||||
}
|
||||
|
||||
// Sort locations.
|
||||
sort.Sort(sortLocationsByAccuracy(dls.All))
|
||||
|
||||
log.Debugf("netenv: added new device location to IPv%d scope: %s from %s", dl.IPVersion, dl, dl.Source)
|
||||
}
|
||||
|
||||
// DeviceLocation represents a single IP and metadata. It must not be changed
|
||||
// once created.
|
||||
type DeviceLocation struct {
|
||||
IP net.IP
|
||||
IPVersion packet.IPVersion
|
||||
Location *geoip.Location
|
||||
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 source accuracy is better.
|
||||
return true
|
||||
case dl.IP != nil && other.IP == nil:
|
||||
// Location based on IP is better than without.
|
||||
return true
|
||||
case dl.Location.AutonomousSystemNumber != 0 &&
|
||||
other.Location.AutonomousSystemNumber == 0:
|
||||
// Having an ASN is better than having none.
|
||||
return true
|
||||
case dl.Location.Country.Code != "" &&
|
||||
other.Location.Country.Code == "":
|
||||
// Having a Country is better than having none.
|
||||
return true
|
||||
case (dl.Location.Coordinates.Latitude != 0 ||
|
||||
dl.Location.Coordinates.Longitude != 0) &&
|
||||
other.Location.Coordinates.Latitude == 0 &&
|
||||
other.Location.Coordinates.Longitude == 0:
|
||||
// Having Coordinates is better than having none.
|
||||
return true
|
||||
case dl.Location.Coordinates.AccuracyRadius < other.Location.Coordinates.AccuracyRadius:
|
||||
// Higher geo accuracy is better.
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// LocationOrNil or returns the geoip location, or nil if not present.
|
||||
func (dl *DeviceLocation) LocationOrNil() *geoip.Location {
|
||||
if dl == nil {
|
||||
return nil
|
||||
}
|
||||
return dl.Location
|
||||
}
|
||||
|
||||
func (dl *DeviceLocation) String() string {
|
||||
switch {
|
||||
case dl == nil:
|
||||
return "<none>"
|
||||
case dl.Location == nil:
|
||||
return dl.IP.String()
|
||||
case dl.Source == SourceTimezone:
|
||||
return fmt.Sprintf(
|
||||
"TZ(%.0f/%.0f)",
|
||||
dl.Location.Coordinates.Latitude,
|
||||
dl.Location.Coordinates.Longitude,
|
||||
)
|
||||
default:
|
||||
return fmt.Sprintf(
|
||||
"%s (AS%d in %s - %s)",
|
||||
dl.IP,
|
||||
dl.Location.AutonomousSystemNumber,
|
||||
dl.Location.Country.Name,
|
||||
dl.Location.Country.Code,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// DeviceLocationSource is a location source.
|
||||
type DeviceLocationSource string
|
||||
|
||||
// Location Sources.
|
||||
const (
|
||||
SourceInterface DeviceLocationSource = "interface"
|
||||
SourcePeer DeviceLocationSource = "peer"
|
||||
SourceUPNP DeviceLocationSource = "upnp"
|
||||
SourceTraceroute DeviceLocationSource = "traceroute"
|
||||
SourceTimezone DeviceLocationSource = "timezone"
|
||||
SourceOther DeviceLocationSource = "other"
|
||||
)
|
||||
|
||||
// Accuracy returns the location accuracy of the source.
|
||||
func (dls DeviceLocationSource) Accuracy() int {
|
||||
switch dls {
|
||||
case SourceInterface:
|
||||
return 6
|
||||
case SourcePeer:
|
||||
return 5
|
||||
case SourceUPNP:
|
||||
return 4
|
||||
case SourceTraceroute:
|
||||
return 3
|
||||
case SourceOther:
|
||||
return 2
|
||||
case SourceTimezone:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
type sortLocationsByAccuracy []*DeviceLocation
|
||||
|
||||
func (a sortLocationsByAccuracy) Len() int { return len(a) }
|
||||
func (a sortLocationsByAccuracy) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sortLocationsByAccuracy) Less(i, j int) bool { return !a[j].IsMoreAccurateThan(a[i]) }
|
||||
|
||||
// SetInternetLocation provides the location management system with a possible Internet location.
|
||||
func SetInternetLocation(ip net.IP, source DeviceLocationSource) (dl *DeviceLocation, ok bool) {
|
||||
locationsLock.Lock()
|
||||
defer locationsLock.Unlock()
|
||||
|
||||
return locations.AddIP(ip, source)
|
||||
}
|
||||
|
||||
// AddIP adds a new location based on the given IP.
|
||||
func (dls *DeviceLocations) AddIP(ip net.IP, source DeviceLocationSource) (dl *DeviceLocation, ok bool) {
|
||||
// Check if IP is global.
|
||||
if netutils.GetIPScope(ip) != netutils.Global {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Create new location.
|
||||
loc := &DeviceLocation{
|
||||
IP: ip,
|
||||
Source: source,
|
||||
SourceAccuracy: source.Accuracy(),
|
||||
}
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
loc.IPVersion = packet.IPv4
|
||||
} else {
|
||||
loc.IPVersion = packet.IPv6
|
||||
}
|
||||
|
||||
// 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)
|
||||
return nil, false
|
||||
}
|
||||
// Only use location if there is data for it.
|
||||
if geoLoc.Country.Code == "" {
|
||||
return nil, false
|
||||
}
|
||||
loc.Location = geoLoc
|
||||
|
||||
dls.AddLocation(loc)
|
||||
return loc, true
|
||||
}
|
||||
|
||||
// GetApproximateInternetLocation returns the approximate Internet location.
|
||||
// Deprecated: Please use GetInternetLocation instead.
|
||||
func GetApproximateInternetLocation() (net.IP, error) {
|
||||
loc, ok := GetInternetLocation()
|
||||
if !ok || loc.Best() == nil {
|
||||
return nil, errors.New("no device location data available")
|
||||
}
|
||||
return loc.Best().IP, nil
|
||||
}
|
||||
|
||||
// GetInternetLocation returns the possible device locations.
|
||||
func GetInternetLocation() (deviceLocations *DeviceLocations, ok bool) {
|
||||
gettingLocationsLock.Lock()
|
||||
defer gettingLocationsLock.Unlock()
|
||||
|
||||
// Check if the network changed, if not, return cache.
|
||||
if !locationNetworkChangedFlag.IsSet() {
|
||||
locationsLock.Lock()
|
||||
defer locationsLock.Unlock()
|
||||
return locations.Copy(), true
|
||||
}
|
||||
locationNetworkChangedFlag.Refresh()
|
||||
|
||||
// Create new location list.
|
||||
dls := &DeviceLocations{}
|
||||
log.Debug("netenv: getting new device locations")
|
||||
|
||||
// Check interfaces for global addresses.
|
||||
v4ok, v6ok := getLocationFromInterfaces(dls)
|
||||
|
||||
// Try other methods for missing locations.
|
||||
if !v4ok {
|
||||
_, err := getLocationFromTraceroute(dls)
|
||||
if err != nil {
|
||||
log.Warningf("netenv: failed to get IPv4 device location from traceroute: %s", err)
|
||||
} else {
|
||||
v4ok = true
|
||||
}
|
||||
|
||||
// Get location from timezone as final fallback.
|
||||
if !v4ok {
|
||||
getLocationFromTimezone(dls, packet.IPv4)
|
||||
}
|
||||
}
|
||||
if !v6ok && IPv6Enabled() {
|
||||
// TODO: Find more ways to get IPv6 device location
|
||||
|
||||
// Get location from timezone as final fallback.
|
||||
getLocationFromTimezone(dls, packet.IPv6)
|
||||
}
|
||||
|
||||
// As a last guard, make sure there is at least one location in the list.
|
||||
if len(dls.All) == 0 {
|
||||
getLocationFromTimezone(dls, packet.IPv4)
|
||||
}
|
||||
|
||||
// Set new locations.
|
||||
locationsLock.Lock()
|
||||
defer locationsLock.Unlock()
|
||||
locations = dls
|
||||
|
||||
// Return gathered locations.
|
||||
return locations.Copy(), true
|
||||
}
|
||||
|
||||
func getLocationFromInterfaces(dls *DeviceLocations) (v4ok, v6ok bool) {
|
||||
globalIPv4, globalIPv6, err := GetAssignedGlobalAddresses()
|
||||
if err != nil {
|
||||
log.Warningf("netenv: location: failed to get assigned global addresses: %s", err)
|
||||
return false, false
|
||||
}
|
||||
for _, ip := range globalIPv4 {
|
||||
if _, ok := dls.AddIP(ip, SourceInterface); ok {
|
||||
v4ok = true
|
||||
}
|
||||
}
|
||||
for _, ip := range globalIPv6 {
|
||||
if _, ok := dls.AddIP(ip, SourceInterface); ok {
|
||||
v6ok = true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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 address.
|
||||
return false
|
||||
}
|
||||
*/
|
||||
|
||||
func getLocationFromTraceroute(dls *DeviceLocations) (dl *DeviceLocation, err error) {
|
||||
// Create connection.
|
||||
conn, err := icmp.ListenPacket("ip4:icmp", "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open icmp conn: %w", err)
|
||||
}
|
||||
v4Conn := conn.IPv4PacketConn()
|
||||
|
||||
// Generate a random ID for the ICMP packets.
|
||||
generatedID, err := rng.Number(0xFFFF) // uint16
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate icmp msg ID: %w", err)
|
||||
}
|
||||
msgID := int(generatedID)
|
||||
var msgSeq int
|
||||
|
||||
// Create ICMP message body.
|
||||
pingMessage := icmp.Message{
|
||||
Type: ipv4.ICMPTypeEcho,
|
||||
Code: 0,
|
||||
Body: &icmp.Echo{
|
||||
ID: msgID,
|
||||
Seq: msgSeq, // Is increased before marshalling.
|
||||
Data: []byte{},
|
||||
},
|
||||
}
|
||||
maxHops := 4 // add one for every reply that is not global
|
||||
|
||||
// Get additional listener for ICMP messages via the firewall.
|
||||
icmpPacketsViaFirewall, doneWithListeningToICMP := ListenToICMP(locationTestingIPv4Addr.IP)
|
||||
defer doneWithListeningToICMP()
|
||||
|
||||
nextHop:
|
||||
for i := 1; i <= maxHops; i++ {
|
||||
minSeq := msgSeq + 1
|
||||
|
||||
repeatHop:
|
||||
for j := 1; j <= 2; j++ { // Try every hop twice.
|
||||
// Increase sequence number.
|
||||
msgSeq++
|
||||
pingMessage.Body.(*icmp.Echo).Seq = msgSeq //nolint:forcetypeassert // Can only be *icmp.Echo.
|
||||
|
||||
// Make packet data.
|
||||
pingPacket, err := pingMessage.Marshal(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build icmp packet: %w", err)
|
||||
}
|
||||
|
||||
// Set TTL on IP packet.
|
||||
err = v4Conn.SetTTL(i)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set icmp packet TTL: %w", err)
|
||||
}
|
||||
|
||||
// Send ICMP packet.
|
||||
// Try to send three times, as this can be flaky.
|
||||
sendICMP:
|
||||
for i := 0; i < 3; i++ {
|
||||
_, err = conn.WriteTo(pingPacket, locationTestingIPv4Addr)
|
||||
if err == nil {
|
||||
break sendICMP
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send icmp packet: %w", err)
|
||||
}
|
||||
|
||||
// Listen for replies of the ICMP packet.
|
||||
listen:
|
||||
for {
|
||||
remoteIP, icmpPacket, ok := recvICMP(i, icmpPacketsViaFirewall)
|
||||
if !ok {
|
||||
// Timed out.
|
||||
continue repeatHop
|
||||
}
|
||||
|
||||
// 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 nil, errors.New("received final echo reply without time exceeded messages")
|
||||
case layers.ICMPv4TypeDestinationUnreachable,
|
||||
layers.ICMPv4TypeTimeExceeded:
|
||||
// Continue processing.
|
||||
default:
|
||||
continue listen
|
||||
}
|
||||
|
||||
// Parse copy of origin icmp packet that triggered the error.
|
||||
if len(icmpPacket.Payload) != ipv4.HeaderLen+8 {
|
||||
continue listen
|
||||
}
|
||||
originalMessage, err := icmp.ParseMessage(1, icmpPacket.Payload[ipv4.HeaderLen:])
|
||||
if err != nil {
|
||||
continue listen
|
||||
}
|
||||
originalEcho, ok := originalMessage.Body.(*icmp.Echo)
|
||||
if !ok {
|
||||
continue listen
|
||||
}
|
||||
// Check if the ID and sequence match.
|
||||
if originalEcho.ID != msgID {
|
||||
continue listen
|
||||
}
|
||||
if originalEcho.Seq < minSeq {
|
||||
continue listen
|
||||
}
|
||||
|
||||
// React based on message type.
|
||||
switch icmpPacket.TypeCode.Type() {
|
||||
case layers.ICMPv4TypeDestinationUnreachable:
|
||||
// We have received a valid destination unreachable response, abort.
|
||||
return nil, errors.New("destination unreachable")
|
||||
|
||||
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 {
|
||||
dl, ok := dls.AddIP(remoteIP, SourceTraceroute)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid IP address")
|
||||
}
|
||||
return dl, nil
|
||||
}
|
||||
|
||||
// Add one max hop for every reply that was not global.
|
||||
maxHops++
|
||||
|
||||
// Otherwise, continue.
|
||||
continue nextHop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We did not receive anything actionable.
|
||||
return nil, errors.New("did not receive any actionable ICMP reply")
|
||||
}
|
||||
|
||||
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*20+100) * time.Millisecond):
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getLocationFromTimezone(dls *DeviceLocations, ipVersion packet.IPVersion) {
|
||||
// Create base struct.
|
||||
tzLoc := &DeviceLocation{
|
||||
IPVersion: ipVersion,
|
||||
Location: &geoip.Location{},
|
||||
Source: SourceTimezone,
|
||||
SourceAccuracy: SourceTimezone.Accuracy(),
|
||||
}
|
||||
|
||||
// Calculate longitude based on current timezone.
|
||||
_, offsetSeconds := time.Now().Zone()
|
||||
tzLoc.Location.Coordinates.AccuracyRadius = 1000
|
||||
tzLoc.Location.Coordinates.Latitude = 48
|
||||
tzLoc.Location.Coordinates.Longitude = float64(offsetSeconds) / 43200 * 180
|
||||
|
||||
dls.AddLocation(tzLoc)
|
||||
}
|
||||
9
service/netenv/location_default.go
Normal file
9
service/netenv/location_default.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package netenv
|
||||
|
||||
import "net"
|
||||
|
||||
func newICMPListener(_ string) (net.PacketConn, error) { //nolint:unused,deadcode // TODO: clean with Windows code later.
|
||||
return net.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
}
|
||||
29
service/netenv/location_test.go
Normal file
29
service/netenv/location_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var privileged bool
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&privileged, "privileged", false, "run tests that require root/admin privileges")
|
||||
}
|
||||
|
||||
func TestGetInternetLocation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip()
|
||||
}
|
||||
if !privileged {
|
||||
t.Skip("skipping privileged test, active with -privileged argument")
|
||||
}
|
||||
|
||||
loc, ok := GetInternetLocation()
|
||||
if !ok {
|
||||
t.Fatal("GetApproximateInternetLocation failed")
|
||||
}
|
||||
t.Logf("GetApproximateInternetLocation: %+v", loc)
|
||||
}
|
||||
63
service/netenv/location_windows.go
Normal file
63
service/netenv/location_windows.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Windows specific constants for the WSAIoctl interface.
|
||||
//nolint:golint,stylecheck
|
||||
const (
|
||||
SIO_RCVALL = syscall.IOC_IN | syscall.IOC_VENDOR | 1
|
||||
|
||||
RCVALL_OFF = 0
|
||||
RCVALL_ON = 1
|
||||
RCVALL_SOCKETLEVELONLY = 2
|
||||
RCVALL_IPLEVEL = 3
|
||||
)
|
||||
|
||||
func newICMPListener(address string) (net.PacketConn, error) {
|
||||
// This is an attempt to work around the problem described here:
|
||||
// https://github.com/golang/go/issues/38427
|
||||
|
||||
// First, get the correct local interface address, as SIO_RCVALL can't be set on a 0.0.0.0 listeners.
|
||||
dialedConn, err := net.Dial("ip4:icmp", address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial: %s", err)
|
||||
}
|
||||
localAddr := dialedConn.LocalAddr()
|
||||
dialedConn.Close()
|
||||
|
||||
// Configure the setup routine in order to extract the socket handle.
|
||||
var socketHandle syscall.Handle
|
||||
cfg := net.ListenConfig{
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
return c.Control(func(s uintptr) {
|
||||
socketHandle = syscall.Handle(s)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// Bind to interface.
|
||||
conn, err := cfg.ListenPacket(context.Background(), "ip4:icmp", localAddr.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set socket option to receive all packets, such as ICMP error messages.
|
||||
// This is somewhat dirty, as there is guarantee that socketHandle is still valid.
|
||||
// WARNING: The Windows Firewall might just drop the incoming packets you might want to receive.
|
||||
unused := uint32(0) // Documentation states that this is unused, but WSAIoctl fails without it.
|
||||
flag := uint32(RCVALL_IPLEVEL)
|
||||
size := uint32(unsafe.Sizeof(flag))
|
||||
err = syscall.WSAIoctl(socketHandle, SIO_RCVALL, (*byte)(unsafe.Pointer(&flag)), size, nil, 0, &unused, nil, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set socket to listen to all packests: %s", os.NewSyscallError("WSAIoctl", err))
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
72
service/netenv/main.go
Normal file
72
service/netenv/main.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
)
|
||||
|
||||
// Event Names.
|
||||
const (
|
||||
ModuleName = "netenv"
|
||||
NetworkChangedEvent = "network changed"
|
||||
OnlineStatusChangedEvent = "online status changed"
|
||||
)
|
||||
|
||||
var module *modules.Module
|
||||
|
||||
func init() {
|
||||
module = modules.Register(ModuleName, prep, start, nil)
|
||||
module.RegisterEvent(NetworkChangedEvent, true)
|
||||
module.RegisterEvent(OnlineStatusChangedEvent, true)
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
checkForIPv6Stack()
|
||||
|
||||
if err := registerAPIEndpoints(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := prepOnlineStatus(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return prepLocation()
|
||||
}
|
||||
|
||||
func start() error {
|
||||
module.StartServiceWorker(
|
||||
"monitor network changes",
|
||||
0,
|
||||
monitorNetworkChanges,
|
||||
)
|
||||
|
||||
module.StartServiceWorker(
|
||||
"monitor online status",
|
||||
0,
|
||||
monitorOnlineStatus,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ipv6Enabled = abool.NewBool(true)
|
||||
|
||||
// IPv6Enabled returns whether the device has an active IPv6 stack.
|
||||
// This is only checked once on startup in order to maintain consistency.
|
||||
func IPv6Enabled() bool {
|
||||
return ipv6Enabled.IsSet()
|
||||
}
|
||||
|
||||
func checkForIPv6Stack() {
|
||||
_, v6IPs, err := GetAssignedAddresses()
|
||||
if err != nil {
|
||||
log.Warningf("netenv: failed to get assigned addresses to check for ipv6 stack: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set IPv6 as enabled if any IPv6 addresses are found.
|
||||
ipv6Enabled.SetTo(len(v6IPs) > 0)
|
||||
}
|
||||
11
service/netenv/main_test.go
Normal file
11
service/netenv/main_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portmaster/service/core/pmtesting"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pmtesting.TestMain(m, module)
|
||||
}
|
||||
104
service/netenv/network-change.go
Normal file
104
service/netenv/network-change.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
networkChangeCheckTrigger = make(chan struct{}, 1)
|
||||
networkChangedBroadcastFlag = utils.NewBroadcastFlag()
|
||||
)
|
||||
|
||||
// GetNetworkChangedFlag returns a flag to be notified about a network change.
|
||||
func GetNetworkChangedFlag() *utils.Flag {
|
||||
return networkChangedBroadcastFlag.NewFlag()
|
||||
}
|
||||
|
||||
func notifyOfNetworkChange() {
|
||||
networkChangedBroadcastFlag.NotifyAndReset()
|
||||
module.TriggerEvent(NetworkChangedEvent, nil)
|
||||
}
|
||||
|
||||
// TriggerNetworkChangeCheck triggers a network change check.
|
||||
func TriggerNetworkChangeCheck() {
|
||||
select {
|
||||
case networkChangeCheckTrigger <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func monitorNetworkChanges(ctx context.Context) error {
|
||||
var lastNetworkChecksum []byte
|
||||
|
||||
serviceLoop:
|
||||
for {
|
||||
trigger := false
|
||||
|
||||
var ticker *time.Ticker
|
||||
if Online() {
|
||||
ticker = monitorNetworkChangeOnlineTicker
|
||||
} else {
|
||||
ticker = monitorNetworkChangeOfflineTicker
|
||||
}
|
||||
|
||||
// wait for trigger
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-networkChangeCheckTrigger:
|
||||
// don't fall through because the online change check
|
||||
// triggers the networkChangeCheck this way. If we would set
|
||||
// trigger == true we would trigger the online check again
|
||||
// resulting in a loop of pointless checks.
|
||||
case <-ticker.C:
|
||||
trigger = true
|
||||
}
|
||||
|
||||
// check network for changes
|
||||
// create hashsum of current network config
|
||||
hasher := sha1.New() //nolint:gosec // not used for security
|
||||
interfaces, err := osGetNetworkInterfaces()
|
||||
if err != nil {
|
||||
log.Warningf("netenv: failed to get interfaces: %s", err)
|
||||
continue
|
||||
}
|
||||
for _, iface := range interfaces {
|
||||
_, _ = io.WriteString(hasher, iface.Name)
|
||||
// log.Tracef("adding: %s", iface.Name)
|
||||
_, _ = io.WriteString(hasher, iface.Flags.String())
|
||||
// log.Tracef("adding: %s", iface.Flags.String())
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
log.Warningf("netenv: failed to get addrs from interface %s: %s", iface.Name, err)
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
_, _ = io.WriteString(hasher, addr.String())
|
||||
// log.Tracef("adding: %s", addr.String())
|
||||
}
|
||||
}
|
||||
newChecksum := hasher.Sum(nil)
|
||||
|
||||
// compare checksum with last
|
||||
if !bytes.Equal(lastNetworkChecksum, newChecksum) {
|
||||
if len(lastNetworkChecksum) == 0 {
|
||||
lastNetworkChecksum = newChecksum
|
||||
continue serviceLoop
|
||||
}
|
||||
lastNetworkChecksum = newChecksum
|
||||
|
||||
if trigger {
|
||||
TriggerOnlineStatusInvestigation()
|
||||
}
|
||||
notifyOfNetworkChange()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
19
service/netenv/notes.md
Normal file
19
service/netenv/notes.md
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
Intel:
|
||||
- First ever request: use first resolver as selected
|
||||
- If resolver fails:
|
||||
- stop all requesting
|
||||
- get network status
|
||||
- if failed: do nothing, return offline error
|
||||
- check list front to back, use first resolver that resolves one.one.one.one correctly
|
||||
|
||||
NetEnv:
|
||||
- check for intercepted HTTP Request requests
|
||||
- if fails on:
|
||||
- connection establishment: OFFLINE
|
||||
-
|
||||
- check for intercepted HTTPS Request requests
|
||||
|
||||
|
||||
- check for intercepted DNS requests
|
||||
564
service/netenv/online-status.go
Normal file
564
service/netenv/online-status.go
Normal file
@@ -0,0 +1,564 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/notifications"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
// OnlineStatus represent a state of connectivity to the Internet.
|
||||
type OnlineStatus uint8
|
||||
|
||||
// Online Status Values.
|
||||
const (
|
||||
StatusUnknown OnlineStatus = 0
|
||||
StatusOffline OnlineStatus = 1
|
||||
StatusLimited OnlineStatus = 2 // local network only
|
||||
StatusPortal OnlineStatus = 3 // there seems to be an internet connection, but we are being intercepted, possibly by a captive portal
|
||||
StatusSemiOnline OnlineStatus = 4 // we seem to online, but without full connectivity
|
||||
StatusOnline OnlineStatus = 5
|
||||
)
|
||||
|
||||
// Online Status and Resolver.
|
||||
var (
|
||||
PortalTestIP = net.IPv4(192, 0, 2, 1)
|
||||
PortalTestURL = fmt.Sprintf("http://%s/", PortalTestIP)
|
||||
|
||||
// IP address -> 100.127.247.245 is a special ip used by the android VPN service. Must be ignored during online check.
|
||||
IgnoreIPsInOnlineStatusCheck = []net.IP{net.IPv4(100, 127, 247, 245)}
|
||||
|
||||
DNSTestDomain = "online-check.safing.io."
|
||||
DNSTestExpectedIP = net.IPv4(0, 65, 67, 75) // Ascii: \0ACK
|
||||
DNSTestQueryFunc func(ctx context.Context, fdqn string) (ips []net.IP, ok bool, err error)
|
||||
|
||||
ConnectedToSPN = abool.New()
|
||||
ConnectedToDNS = abool.New()
|
||||
|
||||
// SpecialCaptivePortalDomain is the domain name used to point to the detected captive portal IP
|
||||
// or the captive portal test IP. The default value should be overridden by the resolver package,
|
||||
// which defines the custom internal domain name to use.
|
||||
SpecialCaptivePortalDomain = "captiveportal.invalid."
|
||||
|
||||
// ConnectivityDomains holds all connectivity domains. This slice must not be modified.
|
||||
ConnectivityDomains = []string{
|
||||
SpecialCaptivePortalDomain,
|
||||
|
||||
// Windows
|
||||
"dns.msftncsi.com.", // DNS Check
|
||||
"msftncsi.com.", // Older
|
||||
"www.msftncsi.com.",
|
||||
"microsoftconnecttest.com.", // Newer
|
||||
"www.microsoftconnecttest.com.",
|
||||
"ipv6.microsoftconnecttest.com.",
|
||||
// https://de.wikipedia.org/wiki/Captive_Portal
|
||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/mobilebroadband/captive-portals
|
||||
// TODO: read value from registry: HKLM:\SYSTEM\CurrentControlSet\Services\NlaSvc\Parameters\Internet
|
||||
|
||||
// Apple
|
||||
"captive.apple.com.",
|
||||
// https://de.wikipedia.org/wiki/Captive_Portal
|
||||
|
||||
// Linux
|
||||
"connectivity-check.ubuntu.com.", // Ubuntu
|
||||
"nmcheck.gnome.org.", // Gnome DE
|
||||
"network-test.debian.org.", // Debian
|
||||
"204.pop-os.org.", // Pop OS
|
||||
"conncheck.opensuse.org.", // OpenSUSE
|
||||
"ping.archlinux.org", // Arch
|
||||
// There are probably a lot more domains for all the Linux Distro/DE Variants. Please raise issues and/or submit PRs!
|
||||
// https://github.com/solus-project/budgie-desktop/issues/807
|
||||
// https://www.lguruprasad.in/blog/2015/07/21/enabling-captive-portal-detection-in-gnome-3-14-on-debian-jessie/
|
||||
// TODO: read value from NetworkManager config: /etc/NetworkManager/conf.d/*.conf
|
||||
|
||||
// Android
|
||||
"connectivitycheck.gstatic.com.",
|
||||
// https://de.wikipedia.org/wiki/Captive_Portal
|
||||
|
||||
// Other
|
||||
"neverssl.com.", // Common Community Service
|
||||
"detectportal.firefox.com.", // Firefox
|
||||
}
|
||||
|
||||
parsedPortalTestURL *url.URL
|
||||
)
|
||||
|
||||
func prepOnlineStatus() (err error) {
|
||||
parsedPortalTestURL, err = url.Parse(PortalTestURL)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsConnectivityDomain checks whether the given domain (fqdn) is used for any
|
||||
// connectivity related network connections and should always be resolved using
|
||||
// the network assigned DNS server.
|
||||
func IsConnectivityDomain(domain string) bool {
|
||||
if domain == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, connectivityDomain := range ConnectivityDomains {
|
||||
if domain == connectivityDomain {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check for captive portal domain.
|
||||
captivePortal := GetCaptivePortal()
|
||||
if captivePortal.Domain != "" &&
|
||||
domain == captivePortal.Domain {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (os OnlineStatus) String() string {
|
||||
switch os {
|
||||
case StatusOffline:
|
||||
return "Offline"
|
||||
case StatusLimited:
|
||||
return "Limited"
|
||||
case StatusPortal:
|
||||
return "Portal"
|
||||
case StatusSemiOnline:
|
||||
return "SemiOnline"
|
||||
case StatusOnline:
|
||||
return "Online"
|
||||
case StatusUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
onlineStatus *int32
|
||||
onlineStatusQuickCheck = abool.NewBool(false)
|
||||
|
||||
onlineStatusInvestigationTrigger = make(chan struct{}, 1)
|
||||
onlineStatusInvestigationInProgress = abool.NewBool(false)
|
||||
onlineStatusInvestigationWg sync.WaitGroup
|
||||
onlineStatusNotification *notifications.Notification
|
||||
|
||||
captivePortal = &CaptivePortal{}
|
||||
captivePortalLock sync.Mutex
|
||||
captivePortalNotification *notifications.Notification
|
||||
)
|
||||
|
||||
// CaptivePortal holds information about a detected captive portal.
|
||||
type CaptivePortal struct {
|
||||
URL string
|
||||
Domain string
|
||||
IP net.IP
|
||||
}
|
||||
|
||||
func init() {
|
||||
var onlineStatusValue int32
|
||||
onlineStatus = &onlineStatusValue
|
||||
}
|
||||
|
||||
// Online returns true if online status is either SemiOnline or Online.
|
||||
func Online() bool {
|
||||
return onlineStatusQuickCheck.IsSet()
|
||||
}
|
||||
|
||||
// GetOnlineStatus returns the current online stats.
|
||||
func GetOnlineStatus() OnlineStatus {
|
||||
return OnlineStatus(atomic.LoadInt32(onlineStatus))
|
||||
}
|
||||
|
||||
// CheckAndGetOnlineStatus triggers a new online status check and returns the result.
|
||||
func CheckAndGetOnlineStatus() OnlineStatus {
|
||||
// trigger new investigation
|
||||
TriggerOnlineStatusInvestigation()
|
||||
// wait for completion
|
||||
onlineStatusInvestigationWg.Wait()
|
||||
// return current status
|
||||
return GetOnlineStatus()
|
||||
}
|
||||
|
||||
func updateOnlineStatus(status OnlineStatus, portalURL *url.URL, comment string) {
|
||||
changed := false
|
||||
|
||||
// Update online status.
|
||||
currentStatus := atomic.LoadInt32(onlineStatus)
|
||||
if status != OnlineStatus(currentStatus) && atomic.CompareAndSwapInt32(onlineStatus, currentStatus, int32(status)) {
|
||||
// status changed!
|
||||
onlineStatusQuickCheck.SetTo(
|
||||
status == StatusOnline || status == StatusSemiOnline,
|
||||
)
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Update captive portal.
|
||||
setCaptivePortal(portalURL)
|
||||
|
||||
// Trigger events.
|
||||
if changed {
|
||||
module.TriggerEvent(OnlineStatusChangedEvent, status)
|
||||
if status == StatusPortal {
|
||||
log.Infof(`netenv: setting online status to %s at "%s" (%s)`, status, portalURL, comment)
|
||||
} else {
|
||||
log.Infof("netenv: setting online status to %s (%s)", status, comment)
|
||||
}
|
||||
TriggerNetworkChangeCheck()
|
||||
|
||||
// Notify user.
|
||||
notifyOnlineStatus(status)
|
||||
|
||||
// Trigger update check when coming (semi) online.
|
||||
if Online() {
|
||||
_ = updates.TriggerUpdate(false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func notifyOnlineStatus(status OnlineStatus) {
|
||||
var eventID, title, message string
|
||||
|
||||
// Check if status is worth notifying.
|
||||
switch status { //nolint:exhaustive // Checking for selection only.
|
||||
case StatusOffline:
|
||||
eventID = "netenv:online-status:offline"
|
||||
title = "Device is Offline"
|
||||
message = "Portmaster did not detect any network connectivity."
|
||||
case StatusLimited:
|
||||
eventID = "netenv:online-status:limited"
|
||||
title = "Limited network connectivity."
|
||||
message = "Portmaster did detect local network connectivity, but could not detect connectivity to the Internet."
|
||||
default:
|
||||
// Delete notification, if present.
|
||||
if onlineStatusNotification != nil {
|
||||
onlineStatusNotification.Delete()
|
||||
onlineStatusNotification = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Update notification if not present or online status changed.
|
||||
switch {
|
||||
case onlineStatusNotification == nil:
|
||||
// Continue creating new notification.
|
||||
case onlineStatusNotification.EventID == eventID:
|
||||
// Notification stays the same, stick with the old one.
|
||||
return
|
||||
default:
|
||||
// Delete old notification before triggering updated one.
|
||||
onlineStatusNotification.Delete()
|
||||
}
|
||||
|
||||
// Create update status notification.
|
||||
onlineStatusNotification = notifications.Notify(¬ifications.Notification{
|
||||
EventID: eventID,
|
||||
Type: notifications.Info,
|
||||
Title: title,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func setCaptivePortal(portalURL *url.URL) {
|
||||
captivePortalLock.Lock()
|
||||
defer captivePortalLock.Unlock()
|
||||
|
||||
// Delete captive portal if no url is supplied.
|
||||
if portalURL == nil {
|
||||
captivePortal = &CaptivePortal{}
|
||||
if captivePortalNotification != nil {
|
||||
captivePortalNotification.Delete()
|
||||
captivePortalNotification = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Only set captive portal once per detection.
|
||||
if captivePortal.URL != "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Compile captive portal data.
|
||||
captivePortal = &CaptivePortal{
|
||||
URL: portalURL.String(),
|
||||
}
|
||||
portalIP := net.ParseIP(portalURL.Hostname())
|
||||
if portalIP != nil {
|
||||
captivePortal.IP = portalIP
|
||||
captivePortal.Domain = SpecialCaptivePortalDomain
|
||||
} else {
|
||||
captivePortal.Domain = portalURL.Hostname()
|
||||
}
|
||||
|
||||
// Notify user about portal.
|
||||
captivePortalNotification = notifications.Notify(¬ifications.Notification{
|
||||
EventID: "netenv:captive-portal",
|
||||
Type: notifications.Info,
|
||||
Title: "Captive Portal Detected",
|
||||
Message: "The Portmaster detected a captive portal. You might experience limited network connectivity until the portal is handled.",
|
||||
ShowOnSystem: true,
|
||||
EventData: captivePortal,
|
||||
AvailableActions: []*notifications.Action{
|
||||
{
|
||||
Text: "Open Portal",
|
||||
Type: notifications.ActionTypeOpenURL,
|
||||
Payload: captivePortal.URL,
|
||||
},
|
||||
{
|
||||
ID: "ack",
|
||||
Text: "Ignore",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetCaptivePortal returns the current captive portal. The returned struct must not be edited.
|
||||
func GetCaptivePortal() *CaptivePortal {
|
||||
captivePortalLock.Lock()
|
||||
defer captivePortalLock.Unlock()
|
||||
|
||||
return captivePortal
|
||||
}
|
||||
|
||||
// ReportSuccessfulConnection hints the online status monitoring system that a connection attempt was successful.
|
||||
func ReportSuccessfulConnection() {
|
||||
if !onlineStatusQuickCheck.IsSet() {
|
||||
TriggerOnlineStatusInvestigation()
|
||||
}
|
||||
}
|
||||
|
||||
// ReportFailedConnection hints the online status monitoring system that a connection attempt has failed. This function has extremely low overhead and may be called as much as wanted.
|
||||
func ReportFailedConnection() {
|
||||
if onlineStatusQuickCheck.IsSet() {
|
||||
TriggerOnlineStatusInvestigation()
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerOnlineStatusInvestigation manually triggers the online status check.
|
||||
// It will not trigger it again, if it is already in progress.
|
||||
func TriggerOnlineStatusInvestigation() {
|
||||
if onlineStatusInvestigationInProgress.SetToIf(false, true) {
|
||||
onlineStatusInvestigationWg.Add(1)
|
||||
}
|
||||
|
||||
select {
|
||||
case onlineStatusInvestigationTrigger <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func monitorOnlineStatus(ctx context.Context) error {
|
||||
TriggerOnlineStatusInvestigation()
|
||||
for {
|
||||
// wait for trigger
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-onlineStatusInvestigationTrigger:
|
||||
case <-getDynamicStatusTrigger():
|
||||
}
|
||||
|
||||
// enable waiting
|
||||
if onlineStatusInvestigationInProgress.SetToIf(false, true) {
|
||||
onlineStatusInvestigationWg.Add(1)
|
||||
}
|
||||
|
||||
checkOnlineStatus(ctx)
|
||||
|
||||
// finished!
|
||||
onlineStatusInvestigationWg.Done()
|
||||
onlineStatusInvestigationInProgress.UnSet()
|
||||
}
|
||||
}
|
||||
|
||||
func getDynamicStatusTrigger() <-chan time.Time {
|
||||
switch GetOnlineStatus() {
|
||||
case StatusOffline:
|
||||
// Will also be triggered by network change.
|
||||
return time.After(10 * time.Second)
|
||||
case StatusLimited, StatusPortal:
|
||||
// Change will not be detected otherwise, but impact is minor.
|
||||
return time.After(5 * time.Second)
|
||||
case StatusSemiOnline:
|
||||
// Very small impact.
|
||||
return time.After(60 * time.Second)
|
||||
case StatusOnline:
|
||||
// Don't check until resolver reports problems.
|
||||
return nil
|
||||
case StatusUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
return time.After(5 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
func ipInList(list []net.IP, ip net.IP) bool {
|
||||
for _, ignoreIP := range list {
|
||||
if ignoreIP.Equal(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkOnlineStatus(ctx context.Context) {
|
||||
// TODO: implement more methods
|
||||
/*status, err := getConnectivityStateFromDbus()
|
||||
if err != nil {
|
||||
log.Warningf("environment: could not get connectivity: %s", err)
|
||||
setConnectivity(StatusUnknown)
|
||||
return StatusUnknown
|
||||
}*/
|
||||
|
||||
// 0) check if connected to SPN and/or DNS.
|
||||
|
||||
if ConnectedToSPN.IsSet() {
|
||||
updateOnlineStatus(StatusOnline, nil, "connected to SPN")
|
||||
return
|
||||
}
|
||||
|
||||
if ConnectedToDNS.IsSet() {
|
||||
updateOnlineStatus(StatusOnline, nil, "connected to DNS")
|
||||
return
|
||||
}
|
||||
|
||||
// 1) check for addresses
|
||||
|
||||
ipv4, ipv6, err := GetAssignedAddresses()
|
||||
if err != nil {
|
||||
log.Warningf("netenv: failed to get assigned network addresses: %s", err)
|
||||
} else {
|
||||
var lan bool
|
||||
|
||||
for _, ip := range ipv4 {
|
||||
// Ignore IP if it is in the online check ignore list.
|
||||
if ipInList(IgnoreIPsInOnlineStatusCheck, ip) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch netutils.GetIPScope(ip) { //nolint:exhaustive // Checking to specific values only.
|
||||
case netutils.SiteLocal:
|
||||
lan = true
|
||||
case netutils.Global:
|
||||
// we _are_ the Internet ;)
|
||||
updateOnlineStatus(StatusOnline, nil, "global IPv4 interface detected")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, ip := range ipv6 {
|
||||
// Ignore IP if it is in the online check ignore list.
|
||||
if ipInList(IgnoreIPsInOnlineStatusCheck, ip) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch netutils.GetIPScope(ip) { //nolint:exhaustive // Checking to specific values only.
|
||||
case netutils.SiteLocal, netutils.Global:
|
||||
// IPv6 global addresses are also used in local networks
|
||||
lan = true
|
||||
}
|
||||
}
|
||||
if !lan {
|
||||
updateOnlineStatus(StatusOffline, nil, "no local or global interfaces detected")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 2) try a http request
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
LocalAddr: getLocalAddr("tcp"),
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: dialer.DialContext,
|
||||
DisableKeepAlives: true,
|
||||
DisableCompression: true,
|
||||
WriteBufferSize: 1024,
|
||||
ReadBufferSize: 1024,
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Timeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
request := (&http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: parsedPortalTestURL,
|
||||
Close: true,
|
||||
}).WithContext(ctx)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
var netErr net.Error
|
||||
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
||||
// Timeout is the expected error when there is no portal
|
||||
log.Debugf("netenv: http portal test failed: %s", err)
|
||||
// TODO: discern between errors to detect StatusLimited
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
_ = response.Body.Close()
|
||||
}()
|
||||
// Got a response, something is messing with the request
|
||||
|
||||
// check location
|
||||
portalURL, err := response.Location()
|
||||
if err == nil {
|
||||
updateOnlineStatus(StatusPortal, portalURL, "portal test request succeeded with redirect")
|
||||
return
|
||||
}
|
||||
|
||||
// direct response
|
||||
if response.StatusCode == http.StatusOK {
|
||||
updateOnlineStatus(StatusPortal, &url.URL{
|
||||
Scheme: "http",
|
||||
Host: SpecialCaptivePortalDomain,
|
||||
Path: "/",
|
||||
}, "portal test request succeeded")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("netenv: unexpected http portal test response code: %d", response.StatusCode)
|
||||
// other responses are undefined, continue with next test
|
||||
}
|
||||
|
||||
// 3) resolve a query
|
||||
|
||||
// Check if we can resolve the dns check domain.
|
||||
if DNSTestQueryFunc == nil {
|
||||
updateOnlineStatus(StatusOnline, nil, "all checks passed, dns query check disabled")
|
||||
return
|
||||
}
|
||||
ips, ok, err := DNSTestQueryFunc(ctx, DNSTestDomain)
|
||||
switch {
|
||||
case ok && err != nil:
|
||||
updateOnlineStatus(StatusOnline, nil, fmt.Sprintf(
|
||||
"all checks passed, acceptable result for dns query check: %s",
|
||||
err,
|
||||
))
|
||||
case ok && len(ips) >= 1 && ips[0].Equal(DNSTestExpectedIP):
|
||||
updateOnlineStatus(StatusOnline, nil, "all checks passed")
|
||||
case ok && len(ips) >= 1:
|
||||
log.Warningf("netenv: dns query check response mismatched: got %s", ips[0])
|
||||
updateOnlineStatus(StatusOnline, nil, "all checks passed, dns query check response mismatched")
|
||||
case ok:
|
||||
log.Warningf("netenv: dns query check response mismatched: empty response")
|
||||
updateOnlineStatus(StatusOnline, nil, "all checks passed, dns query check response was empty")
|
||||
default:
|
||||
log.Warningf("netenv: dns query check failed: %s", err)
|
||||
updateOnlineStatus(StatusOffline, nil, "dns query check failed")
|
||||
}
|
||||
}
|
||||
14
service/netenv/online-status_test.go
Normal file
14
service/netenv/online-status_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckOnlineStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkOnlineStatus(context.Background())
|
||||
t.Logf("online status: %s", GetOnlineStatus())
|
||||
t.Logf("captive portal: %+v", GetCaptivePortal())
|
||||
}
|
||||
40
service/netenv/os_android.go
Normal file
40
service/netenv/os_android.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/service-android/go/app_interface"
|
||||
)
|
||||
|
||||
var (
|
||||
monitorNetworkChangeOnlineTicker = time.NewTicker(time.Second)
|
||||
monitorNetworkChangeOfflineTicker = time.NewTicker(time.Second)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Network change event is monitored by the android system.
|
||||
monitorNetworkChangeOnlineTicker.Stop()
|
||||
monitorNetworkChangeOfflineTicker.Stop()
|
||||
}
|
||||
|
||||
func osGetInterfaceAddrs() ([]net.Addr, error) {
|
||||
list, err := app_interface.GetNetworkAddresses()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var netList []net.Addr
|
||||
for _, addr := range list {
|
||||
ipNetAddr, err := addr.ToIPNet()
|
||||
if err == nil {
|
||||
netList = append(netList, ipNetAddr)
|
||||
}
|
||||
}
|
||||
|
||||
return netList, nil
|
||||
}
|
||||
|
||||
func osGetNetworkInterfaces() ([]app_interface.NetworkInterface, error) {
|
||||
return app_interface.GetNetworkInterfaces()
|
||||
}
|
||||
21
service/netenv/os_default.go
Normal file
21
service/netenv/os_default.go
Normal file
@@ -0,0 +1,21 @@
|
||||
//go:build !android
|
||||
|
||||
package netenv
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
monitorNetworkChangeOnlineTicker = time.NewTicker(15 * time.Second)
|
||||
monitorNetworkChangeOfflineTicker = time.NewTicker(time.Second)
|
||||
)
|
||||
|
||||
func osGetInterfaceAddrs() ([]net.Addr, error) {
|
||||
return net.InterfaceAddrs()
|
||||
}
|
||||
|
||||
func osGetNetworkInterfaces() ([]net.Interface, error) {
|
||||
return net.Interfaces()
|
||||
}
|
||||
Reference in New Issue
Block a user