diff --git a/firewall/interception.go b/firewall/interception.go index 575179be..165af81a 100644 --- a/firewall/interception.go +++ b/firewall/interception.go @@ -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: diff --git a/netenv/api.go b/netenv/api.go index 776eda73..237dee57 100644 --- a/netenv/api.go +++ b/netenv/api.go @@ -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 } diff --git a/netenv/icmp_listener.go b/netenv/icmp_listener.go index 229802b7..7248469c 100644 --- a/netenv/icmp_listener.go +++ b/netenv/icmp_listener.go @@ -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 } diff --git a/netenv/location.go b/netenv/location.go index f2e895b4..87343bbc 100644 --- a/netenv/location.go +++ b/netenv/location.go @@ -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 } }