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.

external_relay.lua 7.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. --[[
  14. external_relay plugin - sets IP/hostname from Received headers
  15. ]]--
  16. if confighelp then
  17. return
  18. end
  19. local lua_maps = require "lua_maps"
  20. local lua_util = require "lua_util"
  21. local rspamd_logger = require "rspamd_logger"
  22. local ts = require("tableshape").types
  23. local E = {}
  24. local N = "external_relay"
  25. local settings = {
  26. rules = {},
  27. }
  28. local config_schema = ts.shape{
  29. enabled = ts.boolean:is_optional(),
  30. rules = ts.map_of(
  31. ts.string, ts.one_of{
  32. ts.shape{
  33. priority = ts.number:is_optional(),
  34. strategy = 'authenticated',
  35. symbol = ts.string:is_optional(),
  36. user_map = lua_maps.map_schema:is_optional(),
  37. },
  38. ts.shape{
  39. count = ts.number,
  40. priority = ts.number:is_optional(),
  41. strategy = 'count',
  42. symbol = ts.string:is_optional(),
  43. },
  44. ts.shape{
  45. priority = ts.number:is_optional(),
  46. strategy = 'local',
  47. symbol = ts.string:is_optional(),
  48. },
  49. ts.shape{
  50. hostname_map = lua_maps.map_schema,
  51. priority = ts.number:is_optional(),
  52. strategy = 'hostname_map',
  53. symbol = ts.string:is_optional(),
  54. },
  55. ts.shape{
  56. ip_map = lua_maps.map_schema,
  57. priority = ts.number:is_optional(),
  58. strategy = 'ip_map',
  59. symbol = ts.string:is_optional(),
  60. },
  61. }
  62. ),
  63. }
  64. local function set_from_rcvd(task, rcvd)
  65. local rcvd_ip = rcvd.real_ip
  66. if not (rcvd_ip and rcvd_ip:is_valid()) then
  67. rspamd_logger.errx(task, 'no IP in header: %s', rcvd)
  68. return
  69. end
  70. task:set_from_ip(rcvd_ip)
  71. if rcvd.from_hostname then
  72. task:set_hostname(rcvd.from_hostname)
  73. task:set_helo(rcvd.from_hostname) -- use fake value for HELO
  74. else
  75. rspamd_logger.warnx(task, "couldn't get hostname from headers")
  76. local ipstr = string.format('[%s]', rcvd_ip)
  77. task:set_hostname(ipstr) -- returns nil from task:get_hostname()
  78. task:set_helo(ipstr)
  79. end
  80. return true
  81. end
  82. local strategies = {}
  83. strategies.authenticated = function(rule)
  84. local user_map
  85. if rule.user_map then
  86. user_map = lua_maps.map_add_from_ucl(rule.user_map, 'set', 'external relay usernames')
  87. if not user_map then
  88. rspamd_logger.errx(rspamd_config, "couldn't add map %s; won't register symbol %s",
  89. rule.user_map, rule.symbol)
  90. return
  91. end
  92. end
  93. return function(task)
  94. local user = task:get_user()
  95. if not user then
  96. lua_util.debugm(N, task, 'sender is unauthenticated')
  97. return
  98. end
  99. if user_map then
  100. if not user_map:get_key(user) then
  101. lua_util.debugm(N, task, 'sender (%s) is not in user_map', user)
  102. return
  103. end
  104. end
  105. local rcvd_hdrs = task:get_received_headers()
  106. -- Try find end of authentication chain
  107. for _, rcvd in ipairs(rcvd_hdrs) do
  108. if not rcvd.flags.authenticated then
  109. -- Found unauthenticated hop, use this header
  110. return set_from_rcvd(task, rcvd)
  111. end
  112. end
  113. rspamd_logger.errx(task, 'found nothing useful in Received headers')
  114. end
  115. end
  116. strategies.count = function(rule)
  117. return function(task)
  118. local rcvd_hdrs = task:get_received_headers()
  119. -- Reduce count by 1 if artificial header is present
  120. local hdr_count
  121. if ((rcvd_hdrs[1] or E).flags or E).artificial then
  122. hdr_count = rule.count - 1
  123. else
  124. hdr_count = rule.count
  125. end
  126. local rcvd = rcvd_hdrs[hdr_count]
  127. if not rcvd then
  128. rspamd_logger.errx(task, 'found no received header #%s', hdr_count)
  129. return
  130. end
  131. return set_from_rcvd(task, rcvd)
  132. end
  133. end
  134. strategies.hostname_map = function(rule)
  135. local hostname_map = lua_maps.map_add_from_ucl(rule.hostname_map, 'map', 'external relay hostnames')
  136. if not hostname_map then
  137. rspamd_logger.errx(rspamd_config, "couldn't add map %s; won't register symbol %s",
  138. rule.hostname_map, rule.symbol)
  139. return
  140. end
  141. return function(task)
  142. local from_hn = task:get_hostname()
  143. if not from_hn then
  144. lua_util.debugm(N, task, 'sending hostname is missing')
  145. return
  146. end
  147. if not hostname_map:get_key(from_hn) then
  148. lua_util.debugm(N, task, 'sender\'s hostname (%s) is not a relay', from_hn)
  149. return
  150. end
  151. local rcvd_hdrs = task:get_received_headers()
  152. -- Try find sending hostname in Received headers
  153. for _, rcvd in ipairs(rcvd_hdrs) do
  154. if rcvd.by_hostname == from_hn and rcvd.real_ip then
  155. if not hostname_map:get_key(rcvd.from_hostname) then
  156. -- Remote hostname is not another relay, use this header
  157. return set_from_rcvd(task, rcvd)
  158. else
  159. -- Keep checking with new hostname
  160. from_hn = rcvd.from_hostname
  161. end
  162. end
  163. end
  164. rspamd_logger.errx(task, 'found nothing useful in Received headers')
  165. end
  166. end
  167. strategies.ip_map = function(rule)
  168. local ip_map = lua_maps.map_add_from_ucl(rule.ip_map, 'radix', 'external relay IPs')
  169. if not ip_map then
  170. rspamd_logger.errx(rspamd_config, "couldn't add map %s; won't register symbol %s",
  171. rule.ip_map, rule.symbol)
  172. return
  173. end
  174. return function(task)
  175. local from_ip = task:get_from_ip()
  176. if not (from_ip and from_ip:is_valid()) then
  177. lua_util.debugm(N, task, 'sender\'s IP is missing')
  178. return
  179. end
  180. if not ip_map:get_key(from_ip) then
  181. lua_util.debugm(N, task, 'sender\'s ip (%s) is not a relay', from_ip)
  182. return
  183. end
  184. local rcvd_hdrs = task:get_received_headers()
  185. local num_rcvd = #rcvd_hdrs
  186. -- Try find sending IP in Received headers
  187. for i, rcvd in ipairs(rcvd_hdrs) do
  188. if rcvd.real_ip then
  189. local rcvd_ip = rcvd.real_ip
  190. if rcvd_ip:is_valid() and (not ip_map:get_key(rcvd_ip) or i == num_rcvd) then
  191. return set_from_rcvd(task, rcvd)
  192. end
  193. end
  194. end
  195. rspamd_logger.errx(task, 'found nothing useful in Received headers')
  196. end
  197. end
  198. strategies['local'] = function(rule)
  199. return function(task)
  200. local from_ip = task:get_from_ip()
  201. if not from_ip then
  202. lua_util.debugm(N, task, 'sending IP is missing')
  203. return
  204. end
  205. if not from_ip:is_local() then
  206. lua_util.debugm(N, task, 'sending IP (%s) is non-local', from_ip)
  207. return
  208. end
  209. local rcvd_hdrs = task:get_received_headers()
  210. local num_rcvd = #rcvd_hdrs
  211. -- Try find first non-local IP in Received headers
  212. for i, rcvd in ipairs(rcvd_hdrs) do
  213. if rcvd.real_ip then
  214. local rcvd_ip = rcvd.real_ip
  215. if rcvd_ip and rcvd_ip:is_valid() and (not rcvd_ip:is_local() or i == num_rcvd) then
  216. return set_from_rcvd(task, rcvd)
  217. end
  218. end
  219. end
  220. rspamd_logger.errx(task, 'found nothing useful in Received headers')
  221. end
  222. end
  223. local opts = rspamd_config:get_all_opt(N)
  224. if opts then
  225. settings = lua_util.override_defaults(settings, opts)
  226. local ok, schema_err = config_schema:transform(settings)
  227. if not ok then
  228. rspamd_logger.errx(rspamd_config, 'config schema error: %s', schema_err)
  229. lua_util.disable_module(N, "config")
  230. return
  231. end
  232. for k, rule in pairs(settings.rules) do
  233. if not rule.symbol then
  234. rule.symbol = k
  235. end
  236. local cb = strategies[rule.strategy](rule)
  237. if cb then
  238. rspamd_config:register_symbol({
  239. name = rule.symbol,
  240. type = 'prefilter',
  241. priority = rule.priority or lua_util.symbols_priorities.top + 1,
  242. group = N,
  243. callback = cb,
  244. })
  245. end
  246. end
  247. end