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
}
// 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.
// Other ICMP packets are considered system business.
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)
// 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
case packet.UDP, packet.TCP:

View File

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

View File

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

View File

@@ -21,6 +21,12 @@ import (
)
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"
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) 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.
if netutils.GetIPScope(ip) != netutils.Global {
return false
return nil, false
}
// Create new location.
@@ -188,29 +194,32 @@ func SetInternetLocation(ip net.IP, source DeviceLocationSource) (ok bool) {
loc.Location = geoLoc
}
addLocation(loc)
return loc, true
}
func addLocation(dl *DeviceLocation) {
locationsLock.Lock()
defer locationsLock.Unlock()
// Add to locations, if better.
var exists bool
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
if loc.IsMoreAccurateThan(existing) {
if dl.IsMoreAccurateThan(existing) {
// Replace
locations.All[i] = loc
locations.All[i] = dl
break
}
}
}
if !exists {
locations.All = append(locations.All, loc)
locations.All = append(locations.All, dl)
}
// Sort locations.
sort.Sort(sortLocationsByAccuracy(locations.All))
return true
}
// 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.
conn, err := net.ListenPacket("ip4:icmp", "")
if err != nil {
log.Warningf("netenv: location: failed to open icmp conn: %s", err)
return false
return nil, fmt.Errorf("failed to open icmp conn: %s", err)
}
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
return nil, fmt.Errorf("failed to generate icmp msg ID: %s", err)
}
msgID := int(generatedID)
var msgSeq int
@@ -323,7 +330,7 @@ func getLocationFromTraceroute() (v4ok bool) {
maxHops := 4 // add one for every reply that is not global
// Get additional listener for ICMP messages via the firewall.
icmpPacketsViaFirewall, doneWithListeningToICMP := ListenToICMP()
icmpPacketsViaFirewall, doneWithListeningToICMP := ListenToICMP(locationTestingIPv4Addr.IP)
defer doneWithListeningToICMP()
nextHop:
@@ -339,15 +346,13 @@ nextHop:
// Make packet data.
pingPacket, err := pingMessage.Marshal(nil)
if err != nil {
log.Warningf("netenv: location: failed to build icmp packet: %s", err)
return false
return nil, fmt.Errorf("failed to build icmp packet: %s", err)
}
// 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
return nil, fmt.Errorf("failed to set icmp packet TTL: %s", err)
}
// Send ICMP packet.
@@ -357,8 +362,7 @@ nextHop:
continue
}
}
log.Warningf("netenv: location: failed to send icmp packet: %s", err)
return false
return nil, fmt.Errorf("failed to send icmp packet: %s", err)
}
// 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.
// 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,
layers.ICMPv4TypeTimeExceeded:
// Continue processing.
@@ -413,13 +417,17 @@ nextHop:
switch icmpPacket.TypeCode.Type() {
case layers.ICMPv4TypeDestinationUnreachable:
// We have received a valid destination unreachable response, abort.
return false
return nil, errors.New("destination unreachable")
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)
dl, ok := SetInternetLocation(remoteIP, SourceTraceroute)
if !ok {
return nil, errors.New("invalid IP address")
}
return dl, nil
}
// Otherwise, continue.
@@ -430,7 +438,7 @@ nextHop:
}
// 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) (
@@ -455,7 +463,7 @@ func recvICMP(currentHop int, icmpPacketsViaFirewall chan packet.Packet) (
}
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
}
}