You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Copyright (c) 2019, Denis Paavilainen <denpa@denpa.pro>
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. ]]--
  14. --[[[
  15. -- @module p0f
  16. -- This module contains p0f access functions
  17. --]]
  18. local tcp = require "rspamd_tcp"
  19. local rspamd_util = require "rspamd_util"
  20. local rspamd_logger = require "rspamd_logger"
  21. local lua_redis = require "lua_redis"
  22. local lua_util = require "lua_util"
  23. local common = require "lua_scanners/common"
  24. -- SEE: https://github.com/p0f/p0f/blob/v3.06b/docs/README#L317
  25. local S = {
  26. BAD_QUERY = 0x0,
  27. OK = 0x10,
  28. NO_MATCH = 0x20
  29. }
  30. local N = 'p0f'
  31. local function p0f_check(task, ip, rule)
  32. local function ip2bin(addr)
  33. addr = addr:to_table()
  34. for k, v in ipairs(addr) do
  35. addr[k] = rspamd_util.pack('B', v)
  36. end
  37. return table.concat(addr)
  38. end
  39. local function trim(...)
  40. local vars = { ... }
  41. for k, v in ipairs(vars) do
  42. -- skip numbers, trim only strings
  43. if tonumber(vars[k]) == nil then
  44. vars[k] = string.gsub(v, '[^%w-_\\.\\(\\) ]', '')
  45. end
  46. end
  47. return lua_util.unpack(vars)
  48. end
  49. local function parse_p0f_response(data)
  50. --[[
  51. p0f_api_response[232]: magic, status, first_seen, last_seen, total_conn,
  52. uptime_min, up_mod_days, last_nat, last_chg, distance, bad_sw, os_match_q,
  53. os_name, os_flavor, http_name, http_flavor, link_type, language
  54. ]]--
  55. data = tostring(data)
  56. -- API response must be 232 bytes long
  57. if #data ~= 232 then
  58. rspamd_logger.errx(task, 'malformed response from p0f on %s, %s bytes',
  59. rule.socket, #data)
  60. common.yield_result(task, rule, 'Malformed Response: ' .. rule.socket,
  61. 0.0, 'fail')
  62. return
  63. end
  64. local _, status, _, _, _, uptime_min, _, _, _, distance, _, _, os_name,
  65. os_flavor, _, _, link_type, _ = trim(rspamd_util.unpack(
  66. 'I4I4I4I4I4I4I4I4I4hbbc32c32c32c32c32c32', data))
  67. if status ~= S.OK then
  68. if status == S.BAD_QUERY then
  69. rspamd_logger.errx(task, 'malformed p0f query on %s', rule.socket)
  70. common.yield_result(task, rule, 'Malformed Query: ' .. rule.socket,
  71. 0.0, 'fail')
  72. end
  73. return
  74. end
  75. local os_string = #os_name == 0 and 'unknown' or os_name .. ' ' .. os_flavor
  76. task:get_mempool():set_variable('os_fingerprint', os_string, link_type,
  77. uptime_min, distance)
  78. if link_type and #link_type > 0 then
  79. common.yield_result(task, rule, {
  80. os_string,
  81. 'link=' .. link_type,
  82. 'distance=' .. distance },
  83. 0.0)
  84. else
  85. common.yield_result(task, rule, {
  86. os_string,
  87. 'link=unknown',
  88. 'distance=' .. distance },
  89. 0.0)
  90. end
  91. return data
  92. end
  93. local function make_p0f_request()
  94. local function check_p0f_cb(err, data)
  95. local function redis_set_cb(redis_set_err)
  96. if redis_set_err then
  97. rspamd_logger.errx(task, 'redis received an error: %s', redis_set_err)
  98. end
  99. end
  100. if err then
  101. rspamd_logger.errx(task, 'p0f received an error: %s', err)
  102. common.yield_result(task, rule, 'Error getting result: ' .. err,
  103. 0.0, 'fail')
  104. return
  105. end
  106. data = parse_p0f_response(data)
  107. if rule.redis_params and data then
  108. local key = rule.prefix .. ip:to_string()
  109. local ret = lua_redis.redis_make_request(task,
  110. rule.redis_params,
  111. key,
  112. true,
  113. redis_set_cb,
  114. 'SETEX',
  115. { key, tostring(rule.expire), data }
  116. )
  117. if not ret then
  118. rspamd_logger.warnx(task, 'error connecting to redis')
  119. end
  120. end
  121. end
  122. local query = rspamd_util.pack('I4 I1 c16', 0x50304601,
  123. ip:get_version(), ip2bin(ip))
  124. tcp.request({
  125. host = rule.socket,
  126. callback = check_p0f_cb,
  127. data = { query },
  128. task = task,
  129. timeout = rule.timeout
  130. })
  131. end
  132. local function redis_get_cb(err, data)
  133. if err or type(data) ~= 'string' then
  134. make_p0f_request()
  135. else
  136. parse_p0f_response(data)
  137. end
  138. end
  139. local ret = nil
  140. if rule.redis_params then
  141. local key = rule.prefix .. ip:to_string()
  142. ret = lua_redis.redis_make_request(task,
  143. rule.redis_params,
  144. key,
  145. false,
  146. redis_get_cb,
  147. 'GET',
  148. { key }
  149. )
  150. end
  151. if not ret then
  152. make_p0f_request() -- fallback to directly querying p0f
  153. end
  154. end
  155. local function p0f_config(opts)
  156. local p0f_conf = {
  157. name = N,
  158. timeout = 5,
  159. symbol = 'P0F',
  160. symbol_fail = 'P0F_FAIL',
  161. patterns = {},
  162. expire = 7200,
  163. prefix = 'p0f',
  164. detection_category = 'fingerprint',
  165. message = '${SCANNER}: fingerprint matched: "${VIRUS}"'
  166. }
  167. p0f_conf = lua_util.override_defaults(p0f_conf, opts)
  168. p0f_conf.patterns = common.create_regex_table(p0f_conf.patterns)
  169. if not p0f_conf.log_prefix then
  170. p0f_conf.log_prefix = p0f_conf.name
  171. end
  172. if not p0f_conf.socket then
  173. rspamd_logger.errx(rspamd_config, 'no servers defined')
  174. return nil
  175. end
  176. return p0f_conf
  177. end
  178. return {
  179. type = { N, 'fingerprint', 'scanner' },
  180. description = 'passive OS fingerprinter',
  181. configure = p0f_config,
  182. check = p0f_check,
  183. name = N
  184. }