summaryrefslogtreecommitdiffstats
path: root/scripts/tor-consensus-checker.nse
blob: e8f57370db51fbcd804ae52438c13307b4124e57 (plain)
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
local http = require "http"
local ipOps = require "ipOps"
local stdnse = require "stdnse"
local string = require "string"
local nmap = require "nmap"

description = [[
Checks if a target is a known Tor node.

The script works by querying the Tor directory authorities. Initially,
the script stores all IPs of Tor nodes in a lookup table to reduce the
number of requests and make lookups quicker.
]]

---
-- @usage
-- nmap --script=tor-consensus-checker <host>
--
-- @output
-- Host script results:
-- | tor-consensus-checker:
-- |_  127.0.0.1 is a Tor node
---

author = "Jiayi Ye"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"external", "safe"}

-- from Tor 0.2.9 auth_dirs.inc
local dir_authorities = {
  { ip = "128.31.0.39", port = 9131},
  { ip = "86.59.21.38", port = 80 },
  { ip = "45.66.33.45", port = 80 },
  { ip = "66.111.2.131", port = 9030 },
  { ip = "131.188.40.189", port = 80 },
  { ip = "193.23.244.244", port = 80 },
  { ip = "171.25.193.9", port = 443 },
  { ip = "154.35.175.225", port = 80 },
  { ip = "199.58.81.140", port = 80 },
  { ip = "204.13.164.118", port = 80 },
}

hostrule = function(host)
  if nmap.registry.tornode and not(nmap.registry.tornode.connect) then
    return false
  end
  return not ipOps.isPrivate(host.ip)
end

function get_consensus(server)
  local response = http.get(server.ip, server.port, "/tor/status-vote/current/consensus",
    {
      -- consensus files were 2.3 MiB as of February 2020
      -- https://metrics.torproject.org/collector/recent/relay-descriptors/consensuses/
      no_cache = true,
      max_body_size=3*1024*1024
    })

  if not response.status then
    stdnse.print_debug(2, "failed to connect to " .. server.ip)
  elseif response.status ~= 200  then
    stdnse.print_debug(2, "%s http error %s", server.ip, response.status)
  else
    stdnse.print_debug(2, "consensus retrieved from %s", server.ip)
    return response.body
  end

  -- no valid server found
  return nil
end

function script_init()
  -- Data and flags shared between threads.
  -- @name tornode
  -- @class table
  -- @field cache     A table for cached tor nodes
  -- @field connect   A flag which prevents threads from looking up when failed to connnect to directory authorities
  nmap.registry.tornode = {}
  nmap.registry.tornode.cache = {}

  local isConnected = false
  local regexp = "r [%S]+ [%S]+ [%S]+ [%d-]+ [%d:]+ ([%d.]+) ([%d]+) [%d]*"
  for _, server in ipairs(dir_authorities) do
    local consensus = get_consensus(server)
    if consensus then
      -- parse the consensus
      for line in string.gmatch(consensus,"[^\n]+") do
        local _, _, ip, port = string.find(line,regexp)
        if ip then
          isConnected = true
          nmap.registry.tornode.cache[ip] = true
        end
      end
    end
    if isConnected then
      break
    end
  end
  if not(isConnected) then
    stdnse.verbose1("failed to connect to directory authorities")
  end
  nmap.registry.tornode.connect = isConnected
end

function check_tornode_cache(ip)
  if not next( nmap.registry.tornode.cache ) then return false end
  if type( ip ) ~= "string" or ip == "" then return false end
  return nmap.registry.tornode.cache[ip]
end

action = function(host)
  local mutex = nmap.mutex("tornode")
  mutex "lock"
  --initialize nmap.registry.tornode
  if not nmap.registry.tornode then
    script_init()
  end
  mutex "done"

  if not(nmap.registry.tornode.connect) then
    if nmap.verbosity() > 2 then
      return "Couln't connect to Tor dir authorities"
    else
      return nil
    end
  end

  local output_tab = stdnse.output_table()
  if check_tornode_cache(host.ip) then
    output_tab.tor_nodes = host.ip
    return output_tab, host.ip .. " is a Tor node"
  else
    return output_tab, host.ip .. " not found in Tor consensus"
  end
end