// Package location resolves the geographic position of this node via IP // geolocation and applies a privacy-preserving random offset proportional // to the chosen granularity level before publishing the result. package location import ( "encoding/json" "fmt" "math/rand" "net/http" "time" peer "cloud.o-forge.io/core/oc-lib/models/peer" ) // fuzzRadius returns the maximum random offset (in degrees) for each axis // given a granularity level. // // 0 → no location // 1 → continent ±15° lat / ±20° lng // 2 → country ±3° lat / ±4° lng (default) // 3 → region ±0.5° lat / ±0.7° lng // 4 → city ±0.05° lat / ±0.07° lng func fuzzRadius(granularity int) (latR, lngR float64) { switch granularity { case 1: return 15.0, 20.0 case 2: return 3.0, 4.0 case 3: return 0.5, 0.7 case 4: return 0.05, 0.07 default: return 3.0, 4.0 } } // clamp keeps a value inside [min, max]. func clamp(v, min, max float64) float64 { if v < min { return min } if v > max { return max } return v } // ipAPIResponse is the subset of fields returned by ip-api.com/json. type ipAPIResponse struct { Status string `json:"status"` Lat float64 `json:"lat"` Lon float64 `json:"lon"` Country string `json:"country"` Region string `json:"regionName"` City string `json:"city"` } // Geolocate resolves the current public IP location via ip-api.com (free, // no key required for non-commercial use), then fuzzes the result according // to granularity. // // Returns nil if granularity == 0 (opt-out) or if the lookup fails. func Geolocate(granularity int) *peer.PeerLocation { if granularity == 0 { return nil } client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Get("http://ip-api.com/json?fields=status,lat,lon,country,regionName,city") if err != nil { return nil } defer resp.Body.Close() var result ipAPIResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || result.Status != "success" { return nil } latR, lngR := fuzzRadius(granularity) rng := rand.New(rand.NewSource(time.Now().UnixNano())) fuzzedLat := result.Lat + (rng.Float64()*2-1)*latR fuzzedLng := result.Lon + (rng.Float64()*2-1)*lngR fuzzedLat = clamp(fuzzedLat, -85.0, 85.0) fuzzedLng = clamp(fuzzedLng, -180.0, 180.0) fmt.Printf("[location] granularity=%d raw=(%.4f,%.4f) fuzzed=(%.4f,%.4f)\n", granularity, result.Lat, result.Lon, fuzzedLat, fuzzedLng) return &peer.PeerLocation{ Latitude: fuzzedLat, Longitude: fuzzedLng, Granularity: granularity, } }