Skip to content

Commit

Permalink
Merge pull request #139 from safing/fix/location-estimation
Browse files Browse the repository at this point in the history
Fix location estimation on Windows
  • Loading branch information
dhaavi authored Aug 19, 2020
2 parents 1bac9e7 + b9f011f commit 1e056ae
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 61 deletions.
157 changes: 103 additions & 54 deletions netenv/location.go
Original file line number Diff line number Diff line change
@@ -1,113 +1,162 @@
package netenv

import (
"errors"
"fmt"
"log"
"net"
"os"
"syscall"
"time"

"github.com/safing/portmaster/network/netutils"
"golang.org/x/net/ipv4"

"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
//nolint:gocognit // TODO
func GetApproximateInternetLocation() (net.IP, error) {
// TODO: first check if we have a public IP
// net.InterfaceAddrs()
func prepLocation() (err error) {
locationTestingIPv4Addr, err = net.ResolveIPAddr("ip", locationTestingIPv4)
return err
}

// 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{
IP: net.IPv4(1, 1, 1, 1),
// First check if we have an assigned IPv6 address. Return that if available.
globalIPv4, _, err := GetAssignedGlobalAddresses()
if err != nil {
log.Warningf("netenv: location approximation: failed to get assigned global addresses: %s", err)
} else if len(globalIPv4) > 0 {
return globalIPv4[0], nil
}

c, err := net.ListenPacket("ip4:icmp", "0.0.0.0") // ICMP for IPv4
// Create OS specific ICMP Listener.
conn, err := newICMPListener(locationTestingIPv4)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to listen: %s", err)
}
defer c.Close()
defer conn.Close()
v4Conn := ipv4.NewPacketConn(conn)

p := ipv4.NewPacketConn(c)
err = p.SetControlMessage(ipv4.FlagTTL|ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface, true)
// Generate a random ID for the ICMP packets.
msgID, err := rng.Number(0xFFFF) // uint16
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to generate ID: %s", err)
}

wm := icmp.Message{
Type: ipv4.ICMPTypeEcho, Code: 0,
// Create ICMP message body
pingMessage := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Data: []byte{0},
ID: int(msgID),
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:
for i := 1; i <= 64; i++ { // up to 64 hops
for i := 1; i <= maxHops; i++ {
repeat:
for j := 1; j <= 5; j++ {
wm.Body.(*icmp.Echo).Seq = i
for j := 1; j <= 2; j++ { // Try every hop twice.
// Increase sequence number.
pingMessage.Body.(*icmp.Echo).Seq++

wb, err := wm.Marshal(nil)
// Make packet data.
pingPacket, err := pingMessage.Marshal(nil)
if err != nil {
return nil, err
}

err = p.SetTTL(i)
// Set TTL on IP packet.
err = v4Conn.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 {
// Send ICMP packet.
if _, err := conn.WriteTo(pingPacket, locationTestingIPv4Addr); err != nil {
if neterr, ok := err.(*net.OpError); ok {
if neterr.Err == syscall.ENOBUFS {
continue
}
}
return nil, err
}

// n, cm, peer, err := p.ReadFrom(rb)
// readping:
// Listen for replies to the ICMP packet.
listen:
for {
// Set read timeout.
err = conn.SetReadDeadline(
time.Now().Add(
time.Duration(i*2+30) * time.Millisecond,
),
)
if err != nil {
return nil, err
}

n, _, peer, err := p.ReadFrom(rb)
// Read next packet.
n, src, err := conn.ReadFrom(recvBuffer)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
// Continue with next packet if we timeout
continue repeat
}
return nil, err
}

rm, err := icmp.ParseMessage(1, rb[:n])
// Parse remote IP address.
addr, ok := src.(*net.IPAddr)
if !ok {
return nil, fmt.Errorf("failed to parse IP: %s", src.String())
}

// Continue if we receive a packet from ourself. This is specific to Windows.
if me, err := IsMyIP(addr.IP); err == nil && me {
log.Tracef("netenv: location approximation: ignoring own message from %s", src)
continue listen
}

// If we received something from a global IP address, we have succeeded and can return immediately.
if netutils.IPIsGlobal(addr.IP) {
return addr.IP, nil
}

// 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.Fatal(err)
log.Warningf("netenv: location approximation: failed to parse ICMP message: %s", err)
continue listen
}

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:
// React based on message type.
switch icmpReply.Type {
case ipv4.ICMPTypeTimeExceeded, ipv4.ICMPTypeEchoReply:
log.Tracef("netenv: location approximation: receveived %q from %s", icmpReply.Type, addr.IP)
continue next
case ipv4.ICMPTypeDestinationUnreachable:
return nil, fmt.Errorf("destination unreachable")
default:
// log.Tracef("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 changes: 9 additions & 0 deletions netenv/location_default.go
Original file line number Diff line number Diff line change
@@ -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")
}
24 changes: 20 additions & 4 deletions netenv/location_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
// +build root

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) {
if testing.Short() {
t.Skip()
}
if !privileged {
t.Skip("skipping privileged test, active with -privileged argument")
}

ip, err := GetApproximateInternetLocation()
if err != nil {
t.Errorf("GetApproximateInternetLocation failed: %s", err)
t.Fatalf("GetApproximateInternetLocation failed: %s", err)
}
t.Logf("GetApproximateInternetLocation: %s", ip.String())
}
61 changes: 61 additions & 0 deletions netenv/location_windows.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 5 additions & 1 deletion netenv/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ func init() {
}

func prep() error {
return prepOnlineStatus()
if err := prepOnlineStatus(); err != nil {
return err
}

return prepLocation()
}

func start() error {
Expand Down
2 changes: 1 addition & 1 deletion netenv/online-status.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const (

// Online Status and Resolver
var (
PortalTestIP = net.IPv4(255, 255, 255, 254)
PortalTestIP = net.IPv4(192, 0, 2, 1)
PortalTestURL = fmt.Sprintf("http://%s/", PortalTestIP)

DNSTestDomain = "one.one.one.one."
Expand Down
5 changes: 4 additions & 1 deletion network/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ func cleanConnections() (activePIDs map[int]struct{}) {
if !exists {
// Step 2: mark end
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:
// Step 3: delete
Expand Down

0 comments on commit 1e056ae

Please sign in to comment.