Fix location estimation via ICMP traceroute

This commit is contained in:
Daniel
2021-09-29 15:42:52 +02:00
parent 9ff9d7d4e1
commit 70dbfa7bd3
4 changed files with 77 additions and 46 deletions

View File

@@ -196,6 +196,17 @@ func fastTrackedPermit(pkt packet.Packet) (handled bool) {
return true return true
} }
// Submit to ICMP listener.
submitted := netenv.SubmitPacketToICMPListener(pkt)
if submitted {
// If the packet was submitted to the listener, we must not do a
// permanent accept, because then we won't see any future packets of that
// connection and thus cannot continue to submit them.
log.Debugf("filter: fast-track tracing ICMP/v6: %s", pkt)
_ = pkt.Accept()
return true
}
// Handle echo request and replies regularly. // Handle echo request and replies regularly.
// Other ICMP packets are considered system business. // Other ICMP packets are considered system business.
icmpLayers := pkt.Layers().LayerClass(layers.LayerClassIPControl) icmpLayers := pkt.Layers().LayerClass(layers.LayerClassIPControl)
@@ -214,20 +225,8 @@ func fastTrackedPermit(pkt packet.Packet) (handled bool) {
} }
} }
// Premit all ICMP/v6 packets that are not echo requests or replies. // Permit all ICMP/v6 packets that are not echo requests or replies.
log.Debugf("filter: fast-track accepting ICMP/v6: %s", pkt) log.Debugf("filter: fast-track accepting ICMP/v6: %s", pkt)
// Submit to ICMP listener.
submitted := netenv.SubmitPacketToICMPListener(pkt)
// If the packet was submitted to the listener, we must not do a
// permanent accept, because then we won't see any future packets of that
// connection and thus cannot continue to submit them.
if submitted {
_ = pkt.Accept()
} else {
_ = pkt.PermanentAccept()
}
return true return true
case packet.UDP, packet.TCP: case packet.UDP, packet.TCP:

View File

@@ -50,5 +50,18 @@ func registerAPIEndpoints() error {
return err 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()
},
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 return nil
} }

View File

@@ -1,6 +1,7 @@
package netenv package netenv
import ( import (
"net"
"sync" "sync"
"github.com/tevino/abool" "github.com/tevino/abool"
@@ -31,21 +32,23 @@ var (
listenICMPEnabled = abool.New() listenICMPEnabled = abool.New()
// listenICMPInput is created for every use of the ICMP listenting system. // listenICMPInput is created for every use of the ICMP listenting system.
listenICMPInput chan packet.Packet listenICMPInput chan packet.Packet
listenICMPInputLock sync.Mutex listenICMPInputTargetIP net.IP
listenICMPInputLock sync.Mutex
) )
// ListenToICMP returns a new channel for listenting to icmp packets. Please // 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 // 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 // the side of the caller. The caller must call the returned done function when
// done with the listener. // done with the listener.
func ListenToICMP() (packets chan packet.Packet, done func()) { func ListenToICMP(targetIP net.IP) (packets chan packet.Packet, done func()) {
// Lock for single use. // Lock for single use.
listenICMPLock.Lock() listenICMPLock.Lock()
// Create new input channel. // Create new input channel.
listenICMPInputLock.Lock() listenICMPInputLock.Lock()
listenICMPInput = make(chan packet.Packet, 100) listenICMPInput = make(chan packet.Packet, 100)
listenICMPInputTargetIP = targetIP
listenICMPEnabled.Set() listenICMPEnabled.Set()
listenICMPInputLock.Unlock() listenICMPInputLock.Unlock()
@@ -56,6 +59,7 @@ func ListenToICMP() (packets chan packet.Packet, done func()) {
// Close input channel. // Close input channel.
listenICMPInputLock.Lock() listenICMPInputLock.Lock()
listenICMPEnabled.UnSet() listenICMPEnabled.UnSet()
listenICMPInputTargetIP = nil
close(listenICMPInput) close(listenICMPInput)
listenICMPInputLock.Unlock() listenICMPInputLock.Unlock()
} }
@@ -71,15 +75,14 @@ func SubmitPacketToICMPListener(pkt packet.Packet) (submitted bool) {
} }
// Slow path. // Slow path.
submitPacketToICMPListenerSlow(pkt) return submitPacketToICMPListenerSlow(pkt)
return true
} }
func submitPacketToICMPListenerSlow(pkt packet.Packet) { func submitPacketToICMPListenerSlow(pkt packet.Packet) (submitted bool) {
// Make sure the payload is available. // Make sure the payload is available.
if err := pkt.LoadPacketData(); err != nil { if err := pkt.LoadPacketData(); err != nil {
log.Warningf("netenv: failed to get payload for ICMP listener: %s", err) log.Warningf("netenv: failed to get payload for ICMP listener: %s", err)
return return false
} }
// Send to input channel. // Send to input channel.
@@ -88,7 +91,14 @@ func submitPacketToICMPListenerSlow(pkt packet.Packet) {
// Check if still enabled. // Check if still enabled.
if !listenICMPEnabled.IsSet() { if !listenICMPEnabled.IsSet() {
return 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. // Send to channel, if possible.
@@ -97,4 +107,5 @@ func submitPacketToICMPListenerSlow(pkt packet.Packet) {
default: default:
log.Warning("netenv: failed to send packet payload to ICMP listener: channel full") log.Warning("netenv: failed to send packet payload to ICMP listener: channel full")
} }
return true
} }

View File

@@ -21,6 +21,12 @@ import (
) )
var ( 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" locationTestingIPv4 = "1.1.1.1"
locationTestingIPv4Addr *net.IPAddr locationTestingIPv4Addr *net.IPAddr
@@ -162,10 +168,10 @@ 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) 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]) } func (a sortLocationsByAccuracy) Less(i, j int) bool { return a[j].IsMoreAccurateThan(a[i]) }
func SetInternetLocation(ip net.IP, source DeviceLocationSource) (ok bool) { func SetInternetLocation(ip net.IP, source DeviceLocationSource) (dl *DeviceLocation, ok bool) {
// Check if IP is global. // Check if IP is global.
if netutils.GetIPScope(ip) != netutils.Global { if netutils.GetIPScope(ip) != netutils.Global {
return false return nil, false
} }
// Create new location. // Create new location.
@@ -188,29 +194,32 @@ func SetInternetLocation(ip net.IP, source DeviceLocationSource) (ok bool) {
loc.Location = geoLoc loc.Location = geoLoc
} }
addLocation(loc)
return loc, true
}
func addLocation(dl *DeviceLocation) {
locationsLock.Lock() locationsLock.Lock()
defer locationsLock.Unlock() defer locationsLock.Unlock()
// Add to locations, if better. // Add to locations, if better.
var exists bool var exists bool
for i, existing := range locations.All { for i, existing := range locations.All {
if ip.Equal(existing.IP) { if (dl.IP == nil && existing.IP == nil) || dl.IP.Equal(existing.IP) {
exists = true exists = true
if loc.IsMoreAccurateThan(existing) { if dl.IsMoreAccurateThan(existing) {
// Replace // Replace
locations.All[i] = loc locations.All[i] = dl
break break
} }
} }
} }
if !exists { if !exists {
locations.All = append(locations.All, loc) locations.All = append(locations.All, dl)
} }
// Sort locations. // Sort locations.
sort.Sort(sortLocationsByAccuracy(locations.All)) sort.Sort(sortLocationsByAccuracy(locations.All))
return true
} }
// DEPRECATED: Please use GetInternetLocation instead. // DEPRECATED: Please use GetInternetLocation instead.
@@ -292,20 +301,18 @@ func getLocationFromUPnP() (ok bool) {
} }
*/ */
func getLocationFromTraceroute() (v4ok bool) { func getLocationFromTraceroute() (dl *DeviceLocation, err error) {
// Create connection. // Create connection.
conn, err := net.ListenPacket("ip4:icmp", "") conn, err := net.ListenPacket("ip4:icmp", "")
if err != nil { if err != nil {
log.Warningf("netenv: location: failed to open icmp conn: %s", err) return nil, fmt.Errorf("failed to open icmp conn: %s", err)
return false
} }
v4Conn := ipv4.NewPacketConn(conn) v4Conn := ipv4.NewPacketConn(conn)
// Generate a random ID for the ICMP packets. // Generate a random ID for the ICMP packets.
generatedID, err := rng.Number(0xFFFF) // uint16 generatedID, err := rng.Number(0xFFFF) // uint16
if err != nil { if err != nil {
log.Warningf("netenv: location: failed to generate icmp msg ID: %s", err) return nil, fmt.Errorf("failed to generate icmp msg ID: %s", err)
return false
} }
msgID := int(generatedID) msgID := int(generatedID)
var msgSeq int var msgSeq int
@@ -323,7 +330,7 @@ func getLocationFromTraceroute() (v4ok bool) {
maxHops := 4 // add one for every reply that is not global maxHops := 4 // add one for every reply that is not global
// Get additional listener for ICMP messages via the firewall. // Get additional listener for ICMP messages via the firewall.
icmpPacketsViaFirewall, doneWithListeningToICMP := ListenToICMP() icmpPacketsViaFirewall, doneWithListeningToICMP := ListenToICMP(locationTestingIPv4Addr.IP)
defer doneWithListeningToICMP() defer doneWithListeningToICMP()
nextHop: nextHop:
@@ -339,15 +346,13 @@ nextHop:
// Make packet data. // Make packet data.
pingPacket, err := pingMessage.Marshal(nil) pingPacket, err := pingMessage.Marshal(nil)
if err != nil { if err != nil {
log.Warningf("netenv: location: failed to build icmp packet: %s", err) return nil, fmt.Errorf("failed to build icmp packet: %s", err)
return false
} }
// Set TTL on IP packet. // Set TTL on IP packet.
err = v4Conn.SetTTL(i) err = v4Conn.SetTTL(i)
if err != nil { if err != nil {
log.Warningf("netenv: location: failed to set icmp packet TTL: %s", err) return nil, fmt.Errorf("failed to set icmp packet TTL: %s", err)
return false
} }
// Send ICMP packet. // Send ICMP packet.
@@ -357,8 +362,7 @@ nextHop:
continue continue
} }
} }
log.Warningf("netenv: location: failed to send icmp packet: %s", err) return nil, fmt.Errorf("failed to send icmp packet: %s", err)
return false
} }
// Listen for replies of the ICMP packet. // Listen for replies of the ICMP packet.
@@ -381,7 +385,7 @@ nextHop:
} }
// We received a reply, so we did not trigger a time exceeded response on the way. // 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. // This means we were not able to find the nearest router to us.
return false return nil, errors.New("received final echo reply without time exceeded messages")
case layers.ICMPv4TypeDestinationUnreachable, case layers.ICMPv4TypeDestinationUnreachable,
layers.ICMPv4TypeTimeExceeded: layers.ICMPv4TypeTimeExceeded:
// Continue processing. // Continue processing.
@@ -413,13 +417,17 @@ nextHop:
switch icmpPacket.TypeCode.Type() { switch icmpPacket.TypeCode.Type() {
case layers.ICMPv4TypeDestinationUnreachable: case layers.ICMPv4TypeDestinationUnreachable:
// We have received a valid destination unreachable response, abort. // We have received a valid destination unreachable response, abort.
return false return nil, errors.New("destination unreachable")
case layers.ICMPv4TypeTimeExceeded: case layers.ICMPv4TypeTimeExceeded:
// We have received a valid time exceeded error. // We have received a valid time exceeded error.
// If message came from a global unicast, us it! // If message came from a global unicast, us it!
if netutils.GetIPScope(remoteIP) == netutils.Global { if netutils.GetIPScope(remoteIP) == netutils.Global {
return SetInternetLocation(remoteIP, SourceTraceroute) dl, ok := SetInternetLocation(remoteIP, SourceTraceroute)
if !ok {
return nil, errors.New("invalid IP address")
}
return dl, nil
} }
// Otherwise, continue. // Otherwise, continue.
@@ -430,7 +438,7 @@ nextHop:
} }
// We did not receive anything actionable. // We did not receive anything actionable.
return false return nil, errors.New("did not receive any actionable ICMP reply")
} }
func recvICMP(currentHop int, icmpPacketsViaFirewall chan packet.Packet) ( func recvICMP(currentHop int, icmpPacketsViaFirewall chan packet.Packet) (
@@ -455,7 +463,7 @@ func recvICMP(currentHop int, icmpPacketsViaFirewall chan packet.Packet) (
} }
return pkt.Info().RemoteIP(), icmp4, true return pkt.Info().RemoteIP(), icmp4, true
case <-time.After(time.Duration(currentHop*10+50) * time.Millisecond): case <-time.After(time.Duration(currentHop*20+100) * time.Millisecond):
return nil, nil, false return nil, nil, false
} }
} }