This analysis explores Tor Exit Nodes - the current list of exit addresses outputted by TorDNSEL.
Tor Check, a scheduled administrative program running in the Tor network, produces the list of known exits and corresponding exit IP addresses. The record above shows an single element of the exit node list (snapshot) written on December 28, 2010 at 15:21:44 UTC. This entry means that the relay (a.k.a Exit Node) with fingerprint 63BA… published a descriptor at 07:35:55 that was contained in a version 2 network status on 08:10:11 and uses two different IP addresses for exiting. The first address 91.102.152.236 was found in a test performed at 07:10:30. When looking at the corresponding server descriptor, one finds that this is also the IP address on which the relay accepts connections from inside the Tor network. A second test performed at 10:35:30 reveals that the relay also uses IP address 91.102.152.227 for exiting.
# Read a snap shot of pre-processed exit nodes
TorExitNodes <- read_rds("./data/TorExitNodes_2017-05-16_04-33-35_UTC.rds")Representative snapshot of Tor exit nodes.
See Snapshot PDF for ingest detail.
# Geo-code Exit Node IP Addresses with freegeoipnet API.
n <- nrow(TorExitNodes)
fillx <- rep(NA_character_, n)
geo_df <- tibble(
countrycode = fillx,
statecode = fillx,
lat = fillx,
long = fillx,
city = fillx,
zip = fillx,
IPreq = fillx,
IPret = fillx,
id = fillx )
for (i in 1:n) {
APIcall <- paste("freegeoip.net/csv/", TorExitNodes$ExitIPAddr[i], sep = "")
Sys.sleep(0.01)
con <- curl(APIcall)
Sys.sleep(0.01)
IPgeo <- readLines(con, warn = FALSE)
Sys.sleep(0.01)
close(con)
Sys.sleep(0.01)
rm(con)
res <- str_split(IPgeo, ",")
print(i)
geo_df$countrycode[i] <- as.character(res[[1]][2])
geo_df$statecode[i] <- as.character(res[[1]][4])
geo_df$lat[i] <- as.character(res[[1]][9])
geo_df$long[i] <- as.character(res[[1]][10])
geo_df$city[i] <- as.character(res[[1]][6])
geo_df$zip[i] <- as.character(res[[1]][7])
geo_df$IPret[i] <- as.character(res[[1]][1])
geo_df$IPreq[i] <- as.character(TorExitNodes$ExitIPAddr[i])
geo_df$id[i] <- as.character(TorExitNodes$id[i]) }
write_rds(geo_df, "./data/geo_df.rds")
rm(geo_df, n, fillx, res, APIcall, i, IPgeo)All Tor exit nodes & exit IP addresses, arranged in expandable clusters.
Click on location marker (blue dots) below for exit address detail.
df <- read_rds("./data/geo_df.rds") %>%
mutate(lat = as.numeric(lat), long = as.numeric(long)) %>%
select(-id) %>%
bind_cols(TorExitNodes)
leaflet() %>%
addProviderTiles(providers$Esri.WorldStreetMap) %>%
addCircleMarkers(lng = df$long,
lat = df$lat,
radius <- 2.5,
fill = TRUE,
fillOpacity = 0.1,
color = "blue",
popup = paste("ExitAddrIP:", df$ExitIPAddr,
", Country:", df$countrycode,
", Province/State:", df$statecode,
", City:", df$city,
", Postal Code:", df$zip,
", Longitude:", df$long,
", Lattitude:", df$lat,
", ExitNode:", df$ExitNode,
", ExitAddrDate:", df$ExitAddrDate,
", id:", df$id, sep = " ")) %>%
# Zoom to Level 1 button
addEasyButton(easyButton(
icon="fa-globe", title="Zoom to Level 1",
onClick=JS("function(btn, map){ map.setZoom(1); }"))) %>%
# Min Map
addMiniMap(
minimized = TRUE,
toggleDisplay = TRUE,
width = 100, height = 100) %>%
# Cluster Markers
addMarkers(data = df,
clusterOptions = markerClusterOptions(),
clusterId = "id") %>%
# Freeze/unfreeze clusters button
addEasyButton(easyButton(
states = list(
easyButtonState(
stateName="frozen-markers",
icon="ion-toggle-filled",
title="UnFreeze Clusters",
onClick = JS("
function(btn, map) {
var clusterManager =
map.layerManager.getLayer('cluster', 'id');
clusterManager.unfreeze();
btn.state('unfrozen-markers');
}")),
easyButtonState(
stateName="unfrozen-markers",
icon="ion-toggle",
title="Freeze Clusters",
onClick = JS("
function(btn, map) {
var clusterManager =
map.layerManager.getLayer('cluster', 'id');
clusterManager.freezeAtZoom();
btn.state('frozen-markers'); }"))
# # locate me UNRELIABLE
# addEasyButton(easyButton(
# icon="fa-crosshairs", title="Locate Me",
# onClick=JS("function(btn, map){ map.locate({setView: true}); }")))
)))Each node record contains one or more address. Each address contains an IP address and a datetime.
df <- TorExitNodes %>%
group_by(id, ExitNode ) %>%
summarize(n = n()) %>%
arrange(desc(n)) %>%
filter(n > 1) %>%
rename(number_of_addresses = n) %>%
rename(fingerprint = ExitNode)
kable(df)| id | fingerprint | number_of_addresses |
|---|---|---|
| 536 | 8ED43EC3683D7E261BB8FEA4EA8122952968CF8E | 9 |
| 63 | 10353360DD0265289463BA5E3C91209A71977863 | 2 |
| 99 | 1987567DE8ED6EFB81E3289A7639B5D05CB042E8 | 2 |
| 361 | 5EC13C778A9EE85A054B7BEB98C8B19BA9F75B55 | 2 |
| 837 | D5D6DBED4BEB90DB089AC1E57EA3A13B9B8AA769 | 2 |
# find the id of the exit node with the maximum # of exit addresses
idmax <- df %>%
filter(n == max(n)) %>%
select(id) %>%
as.integer
# detail exit addess data for node with maximum # of exit addresses
df <- TorExitNodes %>%
filter(id == idmax) %>%
rename(fingerprint = ExitNode) %>%
rename(address = ExitIPAddr) %>%
rename(date = ExitAddrDate) %>%
select(id, fingerprint, address, date)
kable(df)| id | fingerprint | address | date |
|---|---|---|---|
| 536 | 8ED43EC3683D7E261BB8FEA4EA8122952968CF8E | 192.36.27.7 | 2017-05-15 16:04:22 |
| 536 | 8ED43EC3683D7E261BB8FEA4EA8122952968CF8E | 85.248.227.165 | 2017-05-15 16:04:29 |
| 536 | 8ED43EC3683D7E261BB8FEA4EA8122952968CF8E | 78.109.23.1 | 2017-05-15 16:04:29 |
| 536 | 8ED43EC3683D7E261BB8FEA4EA8122952968CF8E | 65.19.167.132 | 2017-05-15 16:04:38 |
| 536 | 8ED43EC3683D7E261BB8FEA4EA8122952968CF8E | 192.36.27.4 | 2017-05-15 16:04:45 |
| 536 | 8ED43EC3683D7E261BB8FEA4EA8122952968CF8E | 93.115.94.204 | 2017-05-15 16:04:53 |
| 536 | 8ED43EC3683D7E261BB8FEA4EA8122952968CF8E | 155.4.230.97 | 2017-05-15 16:04:53 |
| 536 | 8ED43EC3683D7E261BB8FEA4EA8122952968CF8E | 95.128.43.164 | 2017-05-15 16:05:09 |
| 536 | 8ED43EC3683D7E261BB8FEA4EA8122952968CF8E | 171.25.193.78 | 2017-05-15 16:05:10 |
Click on location marker (red dots) below for exit address detail.
df <- read_rds("./data/geo_df.rds") %>%
mutate(lat = as.numeric(lat), long = as.numeric(long)) %>%
select(-id) %>%
bind_cols(TorExitNodes) %>%
filter(id == idmax)
leaflet() %>%
addProviderTiles(providers$Esri.WorldStreetMap) %>%
addCircleMarkers(lng = df$long,
lat = df$lat,
radius <- 2.5,
fill = TRUE,
fillOpacity = 0.9,
color = "red",
popup = paste("ExitAddrIP:", df$ExitIPAddr,
", Country:", df$countrycode,
", Province/State:", df$statecode,
", City:", df$city,
", Postal Code:", df$zip,
", Longitude:", df$long,
", Lattitude:", df$lat,
", ExitNode:", df$ExitNode,
", ExitAddrDate:", df$ExitAddrDate,
", id:", df$id, sep = " ")) %>%
addMiniMap(
minimized = TRUE,
toggleDisplay = TRUE,
width = 100, height = 100) %>%
addEasyButton(easyButton(
icon="fa-globe", title="Zoom to Level 1",
onClick=JS("function(btn, map){ map.setZoom(1); }"))) Each Exit Node/Address pair has a Published datetime.
TorExitNodes %>%
ggplot(aes(Published)) +
geom_freqpoly() +
theme_economist()Each Exit Node/Address pair has a LastStatus datetime.
TorExitNodes %>%
ggplot(aes(LastStatus)) +
geom_freqpoly() +
theme_economist()In addition to an IP address, Each Exit Node/Address pair has one or more Address datetime.
TorExitNodes %>%
ggplot(aes(ExitAddrDate)) +
geom_freqpoly() +
theme_economist()