safing-portmaster/netenv/location.go
2021-04-03 16:03:00 +02:00

393 lines
9.5 KiB
Go

package netenv
import (
"errors"
"net"
"sync"
"syscall"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"github.com/google/gopacket/layers"
"github.com/safing/portbase/log"
"github.com/safing/portbase/rng"
"github.com/safing/portmaster/intel/geoip"
"github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet"
)
var (
locationTestingIPv4 = "1.1.1.1"
locationTestingIPv4Addr *net.IPAddr
locations = &DeviceLocations{
All: make(map[string]*DeviceLocation),
}
locationsLock sync.Mutex
gettingLocationsLock sync.Mutex
locationNetworkChangedFlag = GetNetworkChangedFlag()
)
func prepLocation() (err error) {
locationTestingIPv4Addr, err = net.ResolveIPAddr("ip", locationTestingIPv4)
return err
}
type DeviceLocations struct {
Best *DeviceLocation
All map[string]*DeviceLocation
}
func copyDeviceLocations() *DeviceLocations {
locationsLock.Lock()
defer locationsLock.Unlock()
// Create a copy of the locations, but not the entries.
cp := *locations
cp.All = make(map[string]*DeviceLocation, len(locations.All))
for k, v := range locations.All {
cp.All[k] = v
}
return &cp
}
// DeviceLocation represents a single IP and metadata. It must not be changed
// once created.
type DeviceLocation struct {
IP net.IP
Continent string
Country string
ASN uint
ASOrg string
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 accuracy is better.
return true
case dl.ASN != 0 && other.ASN == 0:
// Having an ASN is better than having none.
return true
case dl.Country == "" && other.Country != "":
// Having a Country is better than having none.
return true
}
return false
}
type DeviceLocationSource string
const (
SourceInterface DeviceLocationSource = "interface"
SourcePeer DeviceLocationSource = "peer"
SourceUPNP DeviceLocationSource = "upnp"
SourceTraceroute DeviceLocationSource = "traceroute"
SourceOther DeviceLocationSource = "other"
)
func (dls DeviceLocationSource) Accuracy() int {
switch dls {
case SourceInterface:
return 5
case SourcePeer:
return 4
case SourceUPNP:
return 3
case SourceTraceroute:
return 2
case SourceOther:
return 1
default:
return 0
}
}
func SetInternetLocation(ip net.IP, source DeviceLocationSource) (ok bool) {
// Check if IP is global.
if netutils.GetIPScope(ip) != netutils.Global {
return false
}
// Create new location.
loc := &DeviceLocation{
IP: ip,
Source: source,
SourceAccuracy: source.Accuracy(),
}
// 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)
} else {
loc.Continent = geoLoc.Continent.Code
loc.Country = geoLoc.Country.ISOCode
loc.ASN = geoLoc.AutonomousSystemNumber
loc.ASOrg = geoLoc.AutonomousSystemOrganization
}
locationsLock.Lock()
defer locationsLock.Unlock()
// Add to locations, if better.
key := loc.IP.String()
existing, ok := locations.All[key]
if ok && existing.IsMoreAccurateThan(loc) {
// Existing entry is more accurate, abort adding.
// Return true, because the IP address is already part of the locations.
return true
}
locations.All[key] = loc
// Find best location.
best := loc
for _, dl := range locations.All {
if dl.IsMoreAccurateThan(best) {
best = dl
}
}
locations.Best = best
return true
}
// DEPRECATED: Please use GetInternetLocation instead.
func GetApproximateInternetLocation() (net.IP, error) {
loc, ok := GetInternetLocation()
if !ok {
return nil, errors.New("no location data available")
}
return loc.Best.IP, nil
}
func GetInternetLocation() (deviceLocations *DeviceLocations, ok bool) {
gettingLocationsLock.Lock()
defer gettingLocationsLock.Unlock()
// Check if the network changed, if not, return cache.
if !locationNetworkChangedFlag.IsSet() {
return copyDeviceLocations(), true
}
locationNetworkChangedFlag.Refresh()
// Check different sources, return on first success.
switch {
case getLocationFromInterfaces():
case getLocationFromTraceroute():
default:
return nil, false
}
// Return gathered locations.
cp := copyDeviceLocations()
return cp, cp.Best != nil
}
func getLocationFromInterfaces() (ok bool) {
globalIPv4, globalIPv6, err := GetAssignedGlobalAddresses()
if err != nil {
log.Warningf("netenv: location: failed to get assigned global addresses: %s", err)
return false
}
for _, ip := range globalIPv4 {
if SetInternetLocation(ip, SourceInterface) {
ok = true
}
}
for _, ip := range globalIPv6 {
if SetInternetLocation(ip, SourceInterface) {
ok = true
}
}
return ok
}
// 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 addres.
return false
}
*/
func getLocationFromTraceroute() (ok bool) {
// Create connection.
conn, err := net.ListenPacket("ip4:icmp", "")
if err != nil {
log.Warningf("netenv: location: failed to open icmp conn: %s", err)
return false
}
v4Conn := ipv4.NewPacketConn(conn)
// Generate a random ID for the ICMP packets.
generatedID, err := rng.Number(0xFFFF) // uint16
if err != nil {
log.Warningf("netenv: location: failed to generate icmp msg ID: %s", err)
return false
}
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()
defer doneWithListeningToICMP()
nextHop:
for i := 1; i <= maxHops; i++ {
minSeq := msgSeq + 1
repeat:
for j := 1; j <= 2; j++ { // Try every hop twice.
// Increase sequence number.
msgSeq++
pingMessage.Body.(*icmp.Echo).Seq = msgSeq
// Make packet data.
pingPacket, err := pingMessage.Marshal(nil)
if err != nil {
log.Warningf("netenv: location: failed to build icmp packet: %s", err)
return false
}
// Set TTL on IP packet.
err = v4Conn.SetTTL(i)
if err != nil {
log.Warningf("netenv: location: failed to set icmp packet TTL: %s", err)
return false
}
// Send ICMP packet.
if _, err := conn.WriteTo(pingPacket, locationTestingIPv4Addr); err != nil {
if neterr, ok := err.(*net.OpError); ok {
if neterr.Err == syscall.ENOBUFS {
continue
}
}
log.Warningf("netenv: location: failed to send icmp packet: %s", err)
return false
}
// Listen for replies of the ICMP packet.
listen:
for {
remoteIP, icmpPacket, ok := recvICMP(i, icmpPacketsViaFirewall)
if !ok {
continue repeat
}
// 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 false
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 != int(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 false
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 {
return SetInternetLocation(remoteIP, SourceTraceroute)
}
// Otherwise, continue.
continue nextHop
}
}
}
}
// We did not receive anything actionable.
return false
}
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*10+50) * time.Millisecond):
return nil, nil, false
}
}
}