Introduction

This analysis explores Tor Exit Nodes - the current list of exit addresses outputted by TorDNSEL.

Sample Record

Record Description

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")

Exit Nodes

Snapshot

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)

Clusters

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}); }")))
      )))

Exit Address

Each node record contains one or more address. Each address contains an IP address and a datetime.

IP Groups

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

Single Group

# 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

Single Group Map

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); }")))  

Appendix

Published Date

Each Exit Node/Address pair has a Published datetime.

TorExitNodes %>% 
  ggplot(aes(Published)) +
  geom_freqpoly() +
  theme_economist()

LastStatus Date

Each Exit Node/Address pair has a LastStatus datetime.

TorExitNodes %>% 
  ggplot(aes(LastStatus)) +
  geom_freqpoly() +
  theme_economist()

Exit Address Date

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()