wip: migrate to mono-repo. SPN has already been moved to spn/

This commit is contained in:
Patrick Pacher
2024-03-15 11:55:13 +01:00
parent b30fd00ccf
commit 8579430db9
577 changed files with 35981 additions and 818 deletions

View 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
View 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
View 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
}

View 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
}

View 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
View 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
}

View 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
}

View 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

View 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
}

View 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)
}

View 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)
}

View 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
}

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)
}

View 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
View 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)
}

View 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")
}

View 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)
}

View 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
View 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)
}

View 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)
}

View 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
View 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

View 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(&notifications.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(&notifications.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")
}
}

View 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())
}

View 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()
}

View 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()
}