/
location.go
218 lines (181 loc) Β· 6.61 KB
/
location.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package geoip
import (
"encoding/binary"
"net"
"strings"
"github.com/umahmood/haversine"
"github.com/safing/portbase/utils"
)
const (
earthCircumferenceInKm = 40100 // earth circumference in km
defaultLocationAccuracy = 100
)
// Location holds information regarding the geographical and network location of an IP address.
// TODO: We are currently re-using the Continent-Code for the region. Update this and and all dependencies.
type Location struct {
Continent struct {
Code string `maxminddb:"code"`
} `maxminddb:"continent"`
Country struct {
ISOCode string `maxminddb:"iso_code"`
} `maxminddb:"country"`
Coordinates Coordinates `maxminddb:"location"`
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
IsAnycast bool `maxminddb:"is_anycast"`
IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"`
}
// Coordinates holds geographic coordinates and their estimated accuracy.
type Coordinates struct {
AccuracyRadius uint16 `maxminddb:"accuracy_radius"`
Latitude float64 `maxminddb:"latitude"`
Longitude float64 `maxminddb:"longitude"`
}
/*
Location Estimation
Distance Value
- 0: Other side of the Internet.
- 100: Very near, up to same network / datacenter.
Weighting Goal
- Exposure to different networks shall be limited as much as possible.
- A single network should not see a connection over a large distance.
- Latency should be low.
Weighting Intentions
- Being on the same continent is better than being in the same AS.
- Being in the same country is better than having low coordinate distance.
- Coordinate distance is only a tie breaker, as accuracy varies heavily.
- Same AS with lower coordinate distance beats being on the same continent.
Weighting Configuration
*/
const (
weightCountryMatch = 10
weightRegionMatch = 10
weightRegionalNeighborMatch = 10
weightASNMatch = 10
weightASOrgMatch = 10
weightCoordinateDistance = 50
)
/*
About the Accuracy Radius
- Range: 1-1000
- Seen values (estimation): 1,5,10,20,50,100,200,500,1000
- The default seems to be 100.
Cxamples
- 1.1.1/24 has 1000: Anycast
- 8.8.0/19 has 1000: Anycast
- 8.8.52/22 has 1: City of Westfield
Conclusion
- Ignore or penalize high accuracy radius.
*/
// EstimateNetworkProximity aims to calculate the distance between two network locations. Returns a proximity value between 0 (far away) and 100 (nearby).
func (l *Location) EstimateNetworkProximity(to *Location) (proximity float32) {
switch {
case l.Country.ISOCode != "" && l.Country.ISOCode == to.Country.ISOCode:
proximity += weightCountryMatch + weightRegionMatch + weightRegionalNeighborMatch
case l.Continent.Code != "" && l.Continent.Code == to.Continent.Code:
// FYI: This is the region code!
proximity += weightRegionMatch + weightRegionalNeighborMatch
case l.IsRegionalNeighbor(to):
proximity += weightRegionalNeighborMatch
}
switch {
case l.AutonomousSystemNumber == to.AutonomousSystemNumber &&
l.AutonomousSystemNumber != 0:
// Rely more on the ASN data, as it is more accurate than the ASOrg data,
// especially when combining location data from multiple sources.
proximity += weightASNMatch + weightASOrgMatch
case l.AutonomousSystemOrganization == to.AutonomousSystemOrganization &&
l.AutonomousSystemNumber != 0 && // Check if an ASN is set. If the ASOrg is known, the ASN must be too.
!ASOrgUnknown(l.AutonomousSystemOrganization): // Check if the ASOrg name is valid.
proximity += weightASOrgMatch
}
// Check coordinates and adjust accuracy value.
accuracy := l.Coordinates.AccuracyRadius
switch {
case l.Coordinates.Latitude == 0 && l.Coordinates.Longitude == 0:
fallthrough
case to.Coordinates.Latitude == 0 && to.Coordinates.Longitude == 0:
// If we don't have any coordinates, return.
return proximity
case to.Coordinates.AccuracyRadius > accuracy:
// If the destination accuracy is worse, use that one.
accuracy = to.Coordinates.AccuracyRadius
}
// Apply the default location accuracy if there is none.
if accuracy == 0 {
accuracy = defaultLocationAccuracy
}
// Calculate coordinate distance in kilometers.
fromCoords := haversine.Coord{Lat: l.Coordinates.Latitude, Lon: l.Coordinates.Longitude}
toCoords := haversine.Coord{Lat: to.Coordinates.Latitude, Lon: to.Coordinates.Longitude}
_, km := haversine.Distance(fromCoords, toCoords)
if km <= 100 && accuracy <= 100 {
// Give the full value for highly accurate coordinates within 100km.
proximity += weightCoordinateDistance
} else {
// Else, take a percentage.
proximityInPercent := (earthCircumferenceInKm - km) / earthCircumferenceInKm
// Apply penalty for locations with low accuracy (targeting accuracy radius >100).
// Take away at most 50% of the weight through inaccuracy.
accuracyModifier := 1 - float64(accuracy)/2000
// Add proximiy weight.
proximity += float32(
weightCoordinateDistance * // Maxmimum weight for this data point.
proximityInPercent * // Range: 0-1
accuracyModifier, // Range: 0.5-1
)
}
return proximity
}
// PrimitiveNetworkProximity calculates the numerical distance between two IP addresses. Returns a proximity value between 0 (far away) and 100 (nearby).
func PrimitiveNetworkProximity(from net.IP, to net.IP, ipVersion uint8) int {
var diff float64
switch ipVersion {
case 4:
// TODO: use ip.To4() and :4
a := binary.BigEndian.Uint32(from[12:])
b := binary.BigEndian.Uint32(to[12:])
if a > b {
diff = float64(a - b)
} else {
diff = float64(b - a)
}
case 6:
a := binary.BigEndian.Uint64(from[:8])
b := binary.BigEndian.Uint64(to[:8])
if a > b {
diff = float64(a - b)
} else {
diff = float64(b - a)
}
default:
return 0
}
switch ipVersion {
case 4:
diff /= 256
return int((1 - diff/16777216) * 100)
case 6:
return int((1 - diff/18446744073709552000) * 100)
default:
return 0
}
}
var unknownASOrgNames = []string{
"", // Expected default for unknown.
"not routed", // Observed as "Not routed" in data set.
"unknown", // Observed as "UNKNOWN" in online data set.
"nil", // Programmatic unknown value.
"null", // Programmatic unknown value.
"undef", // Programmatic unknown value.
"undefined", // Programmatic unknown value.
}
// ASOrgUnknown return whether the given AS Org string actually is meant to
// mean that the AS Org is unknown.
func ASOrgUnknown(asOrg string) bool {
return utils.StringInSlice(
unknownASOrgNames,
strings.ToLower(asOrg),
)
}