mirror of
https://github.com/safing/portmaster
synced 2025-09-01 10:09:11 +00:00
Fix location estimation on Windows
Also, switch to a better Portal Test IP.
This commit is contained in:
parent
633bb34288
commit
b9f011fa37
7 changed files with 206 additions and 64 deletions
|
@ -1,113 +1,162 @@
|
||||||
package netenv
|
package netenv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portmaster/network/netutils"
|
"golang.org/x/net/ipv4"
|
||||||
|
|
||||||
"golang.org/x/net/icmp"
|
"golang.org/x/net/icmp"
|
||||||
"golang.org/x/net/ipv4"
|
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/rng"
|
||||||
|
"github.com/safing/portmaster/network/netutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Create IPv6 version of GetApproximateInternetLocation
|
var (
|
||||||
|
locationTestingIPv4 = "1.1.1.1"
|
||||||
|
locationTestingIPv4Addr *net.IPAddr
|
||||||
|
)
|
||||||
|
|
||||||
// GetApproximateInternetLocation returns the IP-address of the nearest ping-answering internet node
|
func prepLocation() (err error) {
|
||||||
//nolint:gocognit // TODO
|
locationTestingIPv4Addr, err = net.ResolveIPAddr("ip", locationTestingIPv4)
|
||||||
func GetApproximateInternetLocation() (net.IP, error) {
|
return err
|
||||||
// TODO: first check if we have a public IP
|
}
|
||||||
// net.InterfaceAddrs()
|
|
||||||
|
|
||||||
// Traceroute example
|
// GetApproximateInternetLocation returns the nearest detectable IP address. If one or more global IP addresses are configured, one of them is returned. Currently only support IPv4. Else, the IP address of the nearest ping-answering internet node is returned.
|
||||||
|
func GetApproximateInternetLocation() (net.IP, error) { //nolint:gocognit
|
||||||
|
// TODO: Create IPv6 version of GetApproximateInternetLocation
|
||||||
|
|
||||||
dst := net.IPAddr{
|
// First check if we have an assigned IPv6 address. Return that if available.
|
||||||
IP: net.IPv4(1, 1, 1, 1),
|
globalIPv4, _, err := GetAssignedGlobalAddresses()
|
||||||
}
|
|
||||||
|
|
||||||
c, err := net.ListenPacket("ip4:icmp", "0.0.0.0") // ICMP for IPv4
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
log.Warningf("netenv: location approximation: failed to get assigned global addresses: %s", err)
|
||||||
|
} else if len(globalIPv4) > 0 {
|
||||||
|
return globalIPv4[0], nil
|
||||||
}
|
}
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
p := ipv4.NewPacketConn(c)
|
// Create OS specific ICMP Listener.
|
||||||
err = p.SetControlMessage(ipv4.FlagTTL|ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface, true)
|
conn, err := newICMPListener(locationTestingIPv4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to listen: %s", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
v4Conn := ipv4.NewPacketConn(conn)
|
||||||
|
|
||||||
|
// Generate a random ID for the ICMP packets.
|
||||||
|
msgID, err := rng.Number(0xFFFF) // uint16
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate ID: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wm := icmp.Message{
|
// Create ICMP message body
|
||||||
Type: ipv4.ICMPTypeEcho, Code: 0,
|
pingMessage := icmp.Message{
|
||||||
|
Type: ipv4.ICMPTypeEcho,
|
||||||
|
Code: 0,
|
||||||
Body: &icmp.Echo{
|
Body: &icmp.Echo{
|
||||||
ID: os.Getpid() & 0xffff,
|
ID: int(msgID),
|
||||||
Data: []byte{0},
|
Seq: 0, // increased before marshal
|
||||||
|
Data: []byte{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rb := make([]byte, 1500)
|
recvBuffer := make([]byte, 1500)
|
||||||
|
maxHops := 4 // add one for every reply that is not global
|
||||||
|
|
||||||
next:
|
next:
|
||||||
for i := 1; i <= 64; i++ { // up to 64 hops
|
for i := 1; i <= maxHops; i++ {
|
||||||
repeat:
|
repeat:
|
||||||
for j := 1; j <= 5; j++ {
|
for j := 1; j <= 2; j++ { // Try every hop twice.
|
||||||
wm.Body.(*icmp.Echo).Seq = i
|
// Increase sequence number.
|
||||||
|
pingMessage.Body.(*icmp.Echo).Seq++
|
||||||
|
|
||||||
wb, err := wm.Marshal(nil)
|
// Make packet data.
|
||||||
|
pingPacket, err := pingMessage.Marshal(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.SetTTL(i)
|
// Set TTL on IP packet.
|
||||||
|
err = v4Conn.SetTTL(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = p.WriteTo(wb, nil, &dst)
|
// Send ICMP packet.
|
||||||
if err != nil {
|
if _, err := conn.WriteTo(pingPacket, locationTestingIPv4Addr); err != nil {
|
||||||
|
if neterr, ok := err.(*net.OpError); ok {
|
||||||
|
if neterr.Err == syscall.ENOBUFS {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
|
// Listen for replies to the ICMP packet.
|
||||||
if err != nil {
|
listen:
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// n, cm, peer, err := p.ReadFrom(rb)
|
|
||||||
// readping:
|
|
||||||
for {
|
for {
|
||||||
|
// Set read timeout.
|
||||||
|
err = conn.SetReadDeadline(
|
||||||
|
time.Now().Add(
|
||||||
|
time.Duration(i*2+30) * time.Millisecond,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
n, _, peer, err := p.ReadFrom(rb)
|
// Read next packet.
|
||||||
|
n, src, err := conn.ReadFrom(recvBuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||||
|
// Continue with next packet if we timeout
|
||||||
continue repeat
|
continue repeat
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rm, err := icmp.ParseMessage(1, rb[:n])
|
// Parse remote IP address.
|
||||||
if err != nil {
|
addr, ok := src.(*net.IPAddr)
|
||||||
log.Fatal(err)
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to parse IP: %s", src.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
switch rm.Type {
|
// Continue if we receive a packet from ourself. This is specific to Windows.
|
||||||
case ipv4.ICMPTypeTimeExceeded:
|
if me, err := IsMyIP(addr.IP); err == nil && me {
|
||||||
ip := net.ParseIP(peer.String())
|
log.Tracef("netenv: location approximation: ignoring own message from %s", src)
|
||||||
if ip == nil {
|
continue listen
|
||||||
return nil, fmt.Errorf("failed to parse IP: %s", peer.String())
|
}
|
||||||
}
|
|
||||||
if !netutils.IPIsLAN(ip) {
|
// If we received something from a global IP address, we have succeeded and can return immediately.
|
||||||
return ip, nil
|
if netutils.IPIsGlobal(addr.IP) {
|
||||||
}
|
return addr.IP, nil
|
||||||
continue next
|
}
|
||||||
case ipv4.ICMPTypeEchoReply:
|
|
||||||
|
// For everey non-global reply received, increase the maximum hops to try.
|
||||||
|
maxHops++
|
||||||
|
|
||||||
|
// Parse the ICMP message.
|
||||||
|
icmpReply, err := icmp.ParseMessage(1, recvBuffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("netenv: location approximation: failed to parse ICMP message: %s", err)
|
||||||
|
continue listen
|
||||||
|
}
|
||||||
|
|
||||||
|
// React based on message type.
|
||||||
|
switch icmpReply.Type {
|
||||||
|
case ipv4.ICMPTypeTimeExceeded, ipv4.ICMPTypeEchoReply:
|
||||||
|
log.Tracef("netenv: location approximation: receveived %q from %s", icmpReply.Type, addr.IP)
|
||||||
continue next
|
continue next
|
||||||
|
case ipv4.ICMPTypeDestinationUnreachable:
|
||||||
|
return nil, fmt.Errorf("destination unreachable")
|
||||||
default:
|
default:
|
||||||
// log.Tracef("unknown ICMP message: %+v\n", rm)
|
log.Tracef("netenv: location approximation: unexpected ICMP reply: received %q from %s", icmpReply.Type, addr.IP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
return nil, errors.New("no usable response to any icmp message")
|
||||||
}
|
}
|
||||||
|
|
9
netenv/location_default.go
Normal file
9
netenv/location_default.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//+build !windows
|
||||||
|
|
||||||
|
package netenv
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
func newICMPListener(_ string) (net.PacketConn, error) {
|
||||||
|
return net.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||||
|
}
|
|
@ -1,13 +1,29 @@
|
||||||
// +build root
|
|
||||||
|
|
||||||
package netenv
|
package netenv
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"flag"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
privileged bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&privileged, "privileged", false, "run tests that require root/admin privileges")
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetApproximateInternetLocation(t *testing.T) {
|
func TestGetApproximateInternetLocation(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
if !privileged {
|
||||||
|
t.Skip("skipping privileged test, active with -privileged argument")
|
||||||
|
}
|
||||||
|
|
||||||
ip, err := GetApproximateInternetLocation()
|
ip, err := GetApproximateInternetLocation()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("GetApproximateInternetLocation failed: %s", err)
|
t.Fatalf("GetApproximateInternetLocation failed: %s", err)
|
||||||
}
|
}
|
||||||
t.Logf("GetApproximateInternetLocation: %s", ip.String())
|
t.Logf("GetApproximateInternetLocation: %s", ip.String())
|
||||||
}
|
}
|
||||||
|
|
61
netenv/location_windows.go
Normal file
61
netenv/location_windows.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package netenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -21,7 +21,11 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func prep() error {
|
func prep() error {
|
||||||
return prepOnlineStatus()
|
if err := prepOnlineStatus(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() error {
|
func start() error {
|
||||||
|
|
|
@ -35,7 +35,7 @@ const (
|
||||||
|
|
||||||
// Online Status and Resolver
|
// Online Status and Resolver
|
||||||
var (
|
var (
|
||||||
PortalTestIP = net.IPv4(255, 255, 255, 254)
|
PortalTestIP = net.IPv4(192, 0, 2, 1)
|
||||||
PortalTestURL = fmt.Sprintf("http://%s/", PortalTestIP)
|
PortalTestURL = fmt.Sprintf("http://%s/", PortalTestIP)
|
||||||
|
|
||||||
DNSTestDomain = "one.one.one.one."
|
DNSTestDomain = "one.one.one.one."
|
||||||
|
|
|
@ -75,7 +75,10 @@ func cleanConnections() (activePIDs map[int]struct{}) {
|
||||||
if !exists {
|
if !exists {
|
||||||
// Step 2: mark end
|
// Step 2: mark end
|
||||||
conn.Ended = nowUnix
|
conn.Ended = nowUnix
|
||||||
conn.Save()
|
if conn.KeyIsSet() {
|
||||||
|
// Be absolutely sure that we have a key set here, else conn.Save() will deadlock.
|
||||||
|
conn.Save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case conn.Ended < deleteOlderThan:
|
case conn.Ended < deleteOlderThan:
|
||||||
// Step 3: delete
|
// Step 3: delete
|
||||||
|
|
Loading…
Add table
Reference in a new issue