Move network/environment to netenv

This commit is contained in:
Daniel
2020-04-02 17:08:02 +02:00
parent 1a3f9a75da
commit dc32e72b3a
21 changed files with 34 additions and 68 deletions

View File

@@ -1,46 +0,0 @@
package environment
import (
"net"
"strings"
"github.com/safing/portmaster/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 := net.InterfaceAddrs()
if err != nil {
return nil, nil, err
}
for _, addr := range addrs {
ip := net.ParseIP(strings.Split(addr.String(), "/")[0])
if ip != nil {
if ip4 := ip.To4(); ip4 != nil {
ipv4 = append(ipv4, ip4)
} else {
ipv6 = append(ipv6, 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.IPIsGlobal(ip4) {
ipv4 = append(ipv4, ip4)
}
}
for _, ip6 := range allv6 {
if netutils.IPIsGlobal(ip6) {
ipv6 = append(ipv6, ip6)
}
}
return
}

View File

@@ -1,27 +0,0 @@
package environment
import (
"fmt"
"testing"
)
func TestGetAssignedAddresses(t *testing.T) {
ipv4, ipv6, err := GetAssignedAddresses()
fmt.Printf("all v4: %v", ipv4)
fmt.Printf("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) {
ipv4, ipv6, err := GetAssignedGlobalAddresses()
fmt.Printf("all global v4: %v", ipv4)
fmt.Printf("all global v6: %v", ipv6)
if err != nil {
t.Fatalf("failed to get addresses: %s", err)
}
}

View File

@@ -1,216 +0,0 @@
// +build !server
package environment
import (
"errors"
"fmt"
"net"
"sync"
"github.com/godbus/dbus"
)
var (
dbusConn *dbus.Conn
dbusConnLock sync.Mutex
)
func getNameserversFromDbus() ([]Nameserver, error) {
// cmdline tool for exploring: gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager
var nameservers []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, 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, 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 {
ip4ConfigVariant, err := getNetworkManagerProperty(dbusConn, activeConnection, "org.freedesktop.NetworkManager.Connection.Active.Ip4Config")
if err != nil {
return nil, err
}
ip4Config, ok := ip4ConfigVariant.Value().(dbus.ObjectPath)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.Connection.Active.Ip4Config", activeConnection)
}
nameserverIP4sVariant, err := getNetworkManagerProperty(dbusConn, ip4Config, "org.freedesktop.NetworkManager.IP4Config.Nameservers")
if err != nil {
return nil, err
}
nameserverIP4s, ok := nameserverIP4sVariant.Value().([]uint32)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP4Config.Nameservers", ip4Config)
}
nameserverDomainsVariant, err := getNetworkManagerProperty(dbusConn, ip4Config, "org.freedesktop.NetworkManager.IP4Config.Domains")
if err != nil {
return nil, err
}
nameserverDomains, ok := nameserverDomainsVariant.Value().([]string)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP4Config.Domains", ip4Config)
}
nameserverSearchesVariant, err := getNetworkManagerProperty(dbusConn, ip4Config, "org.freedesktop.NetworkManager.IP4Config.Searches")
if err != nil {
return nil, err
}
nameserverSearches, ok := nameserverSearchesVariant.Value().([]string)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP4Config.Searches", ip4Config)
}
for _, ip := range nameserverIP4s {
a := uint8(ip / 16777216)
b := uint8((ip % 16777216) / 65536)
c := uint8((ip % 65536) / 256)
d := uint8(ip % 256)
nameservers = append(nameservers, Nameserver{
IP: net.IPv4(d, c, b, a),
Search: append(nameserverDomains, nameserverSearches...),
})
}
ip6ConfigVariant, err := getNetworkManagerProperty(dbusConn, activeConnection, "org.freedesktop.NetworkManager.Connection.Active.Ip6Config")
if err != nil {
return nil, err
}
ip6Config, ok := ip6ConfigVariant.Value().(dbus.ObjectPath)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.Connection.Active.Ip6Config", activeConnection)
}
nameserverIP6sVariant, err := getNetworkManagerProperty(dbusConn, ip6Config, "org.freedesktop.NetworkManager.IP6Config.Nameservers")
if err != nil {
return nil, err
}
nameserverIP6s, ok := nameserverIP6sVariant.Value().([][]byte)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP6Config.Nameservers", ip6Config)
}
nameserverDomainsVariant, err = getNetworkManagerProperty(dbusConn, ip6Config, "org.freedesktop.NetworkManager.IP6Config.Domains")
if err != nil {
return nil, err
}
nameserverDomains, ok = nameserverDomainsVariant.Value().([]string)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP6Config.Domains", ip6Config)
}
nameserverSearchesVariant, err = getNetworkManagerProperty(dbusConn, ip6Config, "org.freedesktop.NetworkManager.IP6Config.Searches")
if err != nil {
return nil, err
}
nameserverSearches, ok = nameserverSearchesVariant.Value().([]string)
if !ok {
return nil, fmt.Errorf("dbus: could not assert type of %s:org.freedesktop.NetworkManager.IP6Config.Searches", ip6Config)
}
for _, ip := range nameserverIP6s {
if len(ip) != 16 {
return nil, fmt.Errorf("dbus: query returned IPv6 address (%s) with invalid length", ip)
}
nameservers = append(nameservers, Nameserver{
IP: net.IP(ip),
Search: append(nameserverDomains, nameserverSearches...),
})
}
}
return nameservers, 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

@@ -1,12 +0,0 @@
// +build !linux
package environment
func getNameserversFromDbus() ([]Nameserver, error) {
var nameservers []Nameserver
return nameservers, nil
}
func getConnectivityStateFromDbus() (uint8, error) {
return StatusUnknown, nil
}

View File

@@ -1,25 +0,0 @@
package environment
import (
"os"
"testing"
)
func TestDbus(t *testing.T) {
if _, err := os.Stat("/var/run/dbus/system_bus_socket"); os.IsNotExist(err) {
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)
}

View File

@@ -1,21 +0,0 @@
package environment
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

@@ -1,41 +0,0 @@
package environment
import (
"net"
"sync"
"time"
)
// 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:
// windows: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365917
// this info might already be included in the interfaces api provided by golang!
const (
gatewaysRecheck = 2 * time.Second
nameserversRecheck = 2 * time.Second
)
var (
// interfaces = make(map[*net.IP]net.Flags)
// interfacesLock sync.Mutex
// interfacesExpires = time.Now()
gateways = make([]*net.IP, 0)
gatewaysLock sync.Mutex
gatewaysExpires = time.Now()
nameservers = make([]Nameserver, 0)
nameserversLock sync.Mutex
nameserversExpires = time.Now()
)
// Nameserver describes a system assigned namserver.
type Nameserver struct {
IP net.IP
Search []string
}

View File

@@ -1,27 +0,0 @@
package environment
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

@@ -1,205 +0,0 @@
package environment
import (
"bufio"
"encoding/hex"
"net"
"os"
"strings"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/netutils"
)
// Gateways returns the currently active gateways
func Gateways() []*net.IP {
// locking
gatewaysLock.Lock()
defer gatewaysLock.Unlock()
// cache
if gatewaysExpires.After(time.Now()) {
return gateways
}
// update cache expiry when finished
defer func() {
gatewaysExpires = time.Now().Add(gatewaysRecheck)
}()
// logic
newGateways := 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 newGateways
}
defer 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])
newGateways = append(newGateways, &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 newGateways
}
defer 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)
newGateways = append(newGateways, &gate)
}
}
return newGateways
}
// Nameservers returns the currently active nameservers
func Nameservers() []Nameserver {
// locking
nameserversLock.Lock()
defer nameserversLock.Unlock()
// cache
if nameserversExpires.After(time.Now()) {
return nameservers
}
// update cache expiry when finished
defer func() {
nameserversExpires = time.Now().Add(nameserversRecheck)
}()
// 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 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

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

View File

@@ -1,11 +0,0 @@
package environment
import "net"
func Nameservers() []Nameserver {
return nil
}
func Gateways() []*net.IP {
return nil
}

View File

@@ -1,113 +0,0 @@
package environment
import (
"fmt"
"log"
"net"
"os"
"time"
"github.com/safing/portmaster/network/netutils"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
// TODO: Create IPv6 version of GetApproximateInternetLocation
// GetApproximateInternetLocation returns the IP-address of the nearest ping-answering internet node
//nolint:gocognit // TODO
func GetApproximateInternetLocation() (net.IP, error) {
// TODO: first check if we have a public IP
// net.InterfaceAddrs()
// Traceroute example
dst := net.IPAddr{
IP: net.IPv4(1, 1, 1, 1),
}
c, err := net.ListenPacket("ip4:icmp", "0.0.0.0") // ICMP for IPv4
if err != nil {
return nil, err
}
defer c.Close()
p := ipv4.NewPacketConn(c)
err = p.SetControlMessage(ipv4.FlagTTL|ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface, true)
if err != nil {
return nil, err
}
wm := icmp.Message{
Type: ipv4.ICMPTypeEcho, Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Data: []byte{0},
},
}
rb := make([]byte, 1500)
next:
for i := 1; i <= 64; i++ { // up to 64 hops
repeat:
for j := 1; j <= 5; j++ {
wm.Body.(*icmp.Echo).Seq = i
wb, err := wm.Marshal(nil)
if err != nil {
return nil, err
}
err = p.SetTTL(i)
if err != nil {
return nil, err
}
_, err = p.WriteTo(wb, nil, &dst)
if err != nil {
return nil, err
}
err = p.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
if err != nil {
return nil, err
}
// n, cm, peer, err := p.ReadFrom(rb)
// readping:
for {
n, _, peer, err := p.ReadFrom(rb)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
continue repeat
}
return nil, err
}
rm, err := icmp.ParseMessage(1, rb[:n])
if err != nil {
log.Fatal(err)
}
switch rm.Type {
case ipv4.ICMPTypeTimeExceeded:
ip := net.ParseIP(peer.String())
if ip == nil {
return nil, fmt.Errorf("failed to parse IP: %s", peer.String())
}
if !netutils.IPIsLAN(ip) {
return ip, nil
}
continue next
case ipv4.ICMPTypeEchoReply:
continue next
default:
// log.Tracef("unknown ICMP message: %+v\n", rm)
}
}
}
}
return nil, nil
}

View File

@@ -1,13 +0,0 @@
// +build root
package environment
import "testing"
func TestGetApproximateInternetLocation(t *testing.T) {
ip, err := GetApproximateInternetLocation()
if err != nil {
t.Errorf("GetApproximateInternetLocation failed: %s", err)
}
t.Logf("GetApproximateInternetLocation: %s", ip.String())
}

View File

@@ -1,44 +0,0 @@
package environment
import (
"errors"
"github.com/safing/portbase/modules"
)
const (
networkChangedEvent = "network changed"
onlineStatusChangedEvent = "online status changed"
)
var (
module *modules.Module
)
// InitSubModule initializes module specific things with the given module. Intended to be used as part of the "network" module.
func InitSubModule(m *modules.Module) {
module = m
module.RegisterEvent(networkChangedEvent)
module.RegisterEvent(onlineStatusChangedEvent)
}
// StartSubModule starts module specific things with the given module. Intended to be used as part of the "network" module.
func StartSubModule() error {
if module == nil {
return errors.New("not initialized")
}
module.StartServiceWorker(
"monitor network changes",
0,
monitorNetworkChanges,
)
module.StartServiceWorker(
"monitor online status",
0,
monitorOnlineStatus,
)
return nil
}

View File

@@ -1,35 +0,0 @@
package environment
import (
"os"
"testing"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/core"
)
func TestMain(m *testing.M) {
// setup
tmpDir, err := core.InitForTesting()
if err != nil {
panic(err)
}
// setup package
netModule := modules.Register("network", nil, nil, nil, "core")
InitSubModule(netModule)
err = StartSubModule()
if err != nil {
panic(err)
}
// run tests
rv := m.Run()
// teardown
core.StopTesting()
_ = os.RemoveAll(tmpDir)
// exit with test run return value
os.Exit(rv)
}

View File

@@ -1,91 +0,0 @@
package environment
import (
"bytes"
"context"
"crypto/sha1" //nolint:gosec // not used for security
"io"
"net"
"time"
"github.com/safing/portbase/log"
)
var (
networkChangeCheckTrigger = make(chan struct{}, 1)
)
func triggerNetworkChangeCheck() {
select {
case networkChangeCheckTrigger <- struct{}{}:
default:
}
}
func monitorNetworkChanges(ctx context.Context) error {
var lastNetworkChecksum []byte
serviceLoop:
for {
trigger := false
// wait for trigger
if GetOnlineStatus() == StatusOnline {
select {
case <-ctx.Done():
return nil
case <-networkChangeCheckTrigger:
case <-time.After(1 * time.Minute):
trigger = true
}
} else {
select {
case <-ctx.Done():
return nil
case <-networkChangeCheckTrigger:
case <-time.After(1 * time.Second):
trigger = true
}
}
// check network for changes
// create hashsum of current network config
hasher := sha1.New() //nolint:gosec // not used for security
interfaces, err := net.Interfaces()
if err != nil {
log.Warningf("environment: 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("environment: 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()
}
module.TriggerEvent(networkChangedEvent, nil)
}
}
}

View File

@@ -1,19 +0,0 @@
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

@@ -1,353 +0,0 @@
package environment
import (
"context"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/network/netutils"
"github.com/tevino/abool"
)
// 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
const (
HTTPTestURL = "http://detectportal.firefox.com/success.txt"
HTTPExpectedContent = "success"
HTTPSTestURL = "https://one.one.one.one/"
ResolverTestFqdn = "one.one.one.one."
ResolverTestRRType = dns.TypeA
ResolverTestExpectedResponse = "1.1.1.1"
)
var (
parsedHTTPTestURL *url.URL
parsedHTTPSTestURL *url.URL
)
func init() {
var err error
parsedHTTPTestURL, err = url.Parse(HTTPTestURL)
if err != nil {
panic(err)
}
parsedHTTPSTestURL, err = url.Parse(HTTPSTestURL)
if err != nil {
panic(err)
}
}
// IsOnlineStatusTestDomain checks whether the given fqdn is used for testing online status.
func IsOnlineStatusTestDomain(domain string) bool {
switch domain {
case "detectportal.firefox.com.":
return true
case "one.one.one.one.":
return true
}
return false
}
// GetResolverTestingRequestData returns request information that should be used to test DNS resolvers for availability and basic correct behaviour.
func GetResolverTestingRequestData() (fqdn string, rrType uint16, expectedResponse string) {
return ResolverTestFqdn, ResolverTestRRType, ResolverTestExpectedResponse
}
func (os OnlineStatus) String() string {
switch os {
default:
return "Unknown"
case StatusOffline:
return "Offline"
case StatusLimited:
return "Limited"
case StatusPortal:
return "Portal"
case StatusSemiOnline:
return "SemiOnline"
case StatusOnline:
return "Online"
}
}
var (
onlineStatus *int32
onlineStatusQuickCheck = abool.NewBool(false)
onlineStatusInvestigationTrigger = make(chan struct{}, 1)
onlineStatusInvestigationInProgress = abool.NewBool(false)
onlineStatusInvestigationWg sync.WaitGroup
captivePortalURL string
captivePortalLock sync.Mutex
)
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, comment string) {
changed := false
// 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
}
// captive portal
captivePortalLock.Lock()
defer captivePortalLock.Unlock()
if portalURL != captivePortalURL {
captivePortalURL = portalURL
changed = true
}
// trigger event
if changed {
module.TriggerEvent(onlineStatusChangedEvent, nil)
if status == StatusPortal {
log.Infof(`network: setting online status to %s at "%s" (%s)`, status, captivePortalURL, comment)
} else {
log.Infof("network: setting online status to %s (%s)", status, comment)
}
triggerNetworkChangeCheck()
}
}
// GetCaptivePortalURL returns the current captive portal url as a string.
func GetCaptivePortalURL() string {
captivePortalLock.Lock()
defer captivePortalLock.Unlock()
return captivePortalURL
}
// 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()
}
}
func triggerOnlineStatusInvestigation() {
if onlineStatusInvestigationInProgress.SetToIf(false, true) {
onlineStatusInvestigationWg.Add(1)
}
select {
case onlineStatusInvestigationTrigger <- struct{}{}:
default:
}
}
func monitorOnlineStatus(ctx context.Context) error {
for {
// wait for trigger
if GetOnlineStatus() == StatusOnline {
select {
case <-ctx.Done():
return nil
case <-onlineStatusInvestigationTrigger:
case <-time.After(1 * time.Minute):
}
} else {
select {
case <-ctx.Done():
return nil
case <-onlineStatusInvestigationTrigger:
case <-time.After(1 * time.Second):
}
}
// enable waiting
if onlineStatusInvestigationInProgress.SetToIf(false, true) {
onlineStatusInvestigationWg.Add(1)
}
checkOnlineStatus(ctx)
// finished!
onlineStatusInvestigationWg.Done()
onlineStatusInvestigationInProgress.UnSet()
}
}
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
}*/
// 1) check for addresses
ipv4, ipv6, err := GetAssignedAddresses()
if err != nil {
log.Warningf("network: failed to get assigned network addresses: %s", err)
} else {
var lan bool
for _, ip := range ipv4 {
switch netutils.ClassifyIP(ip) {
case netutils.SiteLocal:
lan = true
case netutils.Global:
// we _are_ the Internet ;)
updateOnlineStatus(StatusOnline, "", "global IPv4 interface detected")
return
}
}
for _, ip := range ipv6 {
switch netutils.ClassifyIP(ip) {
case netutils.SiteLocal, netutils.Global:
// IPv6 global addresses are also used in local networks
lan = true
}
}
if !lan {
updateOnlineStatus(StatusOffline, "", "no local or global interfaces detected")
return
}
}
// 2) try a http request
// TODO: find (array of) alternatives to detectportal.firefox.com
// TODO: find something about usage terms of detectportal.firefox.com
client := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
LocalAddr: getLocalAddr("tcp"),
DualStack: true,
}).DialContext,
DisableKeepAlives: true,
DisableCompression: true,
WriteBufferSize: 1024,
ReadBufferSize: 1024,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: 5 * time.Second,
}
request := (&http.Request{
Method: "GET",
URL: parsedHTTPTestURL,
Close: true,
}).WithContext(ctx)
response, err := client.Do(request)
if err != nil {
updateOnlineStatus(StatusLimited, "", "http request failed")
return
}
defer response.Body.Close()
// check location
portalURL, err := response.Location()
if err == nil {
updateOnlineStatus(StatusPortal, portalURL.String(), "http request succeeded with redirect")
return
}
// read the body
data, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Warningf("network: failed to read http body of captive portal testing response: %s", err)
// assume we are online nonetheless
updateOnlineStatus(StatusOnline, "", "http request succeeded, albeit failing later")
return
}
// check body contents
if strings.TrimSpace(string(data)) == HTTPExpectedContent {
updateOnlineStatus(StatusOnline, "", "http request succeeded")
} else {
// something is interfering with the website content
// this might be a weird captive portal, just direct the user there
updateOnlineStatus(StatusPortal, "detectportal.firefox.com", "http request succeeded, response content not as expected")
}
// 3) try a https request
request = (&http.Request{
Method: "HEAD",
URL: parsedHTTPSTestURL,
Close: true,
}).WithContext(ctx)
// only test if we can get the headers
response, err = client.Do(request)
if err != nil {
// if we fail, something is really weird
updateOnlineStatus(StatusSemiOnline, "", "http request failed")
return
}
defer response.Body.Close()
// finally
updateOnlineStatus(StatusOnline, "", "all checks successful")
}

View File

@@ -1,12 +0,0 @@
package environment
import (
"context"
"testing"
)
func TestCheckOnlineStatus(t *testing.T) {
checkOnlineStatus(context.Background())
t.Logf("online status: %s", GetOnlineStatus())
t.Logf("captive portal: %s", GetCaptivePortalURL())
}

View File

@@ -2,7 +2,6 @@ package network
import (
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/network/environment"
)
var (
@@ -10,8 +9,7 @@ var (
)
func init() {
module = modules.Register("network", nil, start, nil, "core")
environment.InitSubModule(module)
module = modules.Register("network", nil, start, nil, "core", "processes")
}
func start() error {
@@ -22,5 +20,5 @@ func start() error {
go cleaner()
return environment.StartSubModule()
return nil
}