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 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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. }
  56. ),
  57. }
  58. local function set_from_rcvd(task, rcvd)
  59. local rcvd_ip = rcvd.real_ip
  60. if not (rcvd_ip and rcvd_ip:is_valid()) then
  61. rspamd_logger.errx(task, 'no IP in header: %s', rcvd)
  62. return
  63. end
  64. task:set_from_ip(rcvd_ip)
  65. if rcvd.from_hostname then
  66. task:set_hostname(rcvd.from_hostname)
  67. task:set_helo(rcvd.from_hostname) -- use fake value for HELO
  68. else
  69. rspamd_logger.warnx(task, "couldn't get hostname from headers")
  70. local ipstr = string.format('[%s]', rcvd_ip)
  71. task:set_hostname(ipstr) -- returns nil from task:get_hostname()
  72. task:set_helo(ipstr)
  73. end
  74. return true
  75. end
  76. local strategies = {}
  77. strategies.authenticated = function(rule)
  78. local user_map
  79. if rule.user_map then
  80. user_map = lua_maps.map_add_from_ucl(rule.user_map, 'set', 'external relay usernames')
  81. if not user_map then
  82. rspamd_logger.errx(rspamd_config, "couldn't add map %s; won't register symbol %s",
  83. rule.user_map, rule.symbol)
  84. return
  85. end
  86. end
  87. return function(task)
  88. local user = task:get_user()
  89. if not user then
  90. lua_util.debugm(N, task, 'sender is unauthenticated')
  91. return
  92. end
  93. if user_map then
  94. if not user_map:get_key(user) then
  95. lua_util.debugm(N, task, 'sender (%s) is not in user_map', user)
  96. return
  97. end
  98. end
  99. local rcvd_hdrs = task:get_received_headers()
  100. -- Try find end of authentication chain
  101. for _, rcvd in ipairs(rcvd_hdrs) do
  102. if not rcvd.flags.authenticated then
  103. -- Found unauthenticated hop, use this header
  104. return set_from_rcvd(task, rcvd)
  105. end
  106. end
  107. rspamd_logger.errx(task, 'found nothing useful in Received headers')
  108. end
  109. end
  110. strategies.count = function(rule)
  111. return function(task)
  112. local rcvd_hdrs = task:get_received_headers()
  113. -- Reduce count by 1 if artificial header is present
  114. local hdr_count
  115. if ((rcvd_hdrs[1] or E).flags or E).artificial then
  116. hdr_count = rule.count - 1
  117. else
  118. hdr_count = rule.count
  119. end
  120. local rcvd = rcvd_hdrs[hdr_count]
  121. if not rcvd then
  122. rspamd_logger.errx(task, 'found no received header #%s', hdr_count)
  123. return
  124. end
  125. return set_from_rcvd(task, rcvd)
  126. end
  127. end
  128. strategies.hostname_map = function(rule)
  129. local hostname_map = lua_maps.map_add_from_ucl(rule.hostname_map, 'map', 'external relay hostnames')
  130. if not hostname_map then
  131. rspamd_logger.errx(rspamd_config, "couldn't add map %s; won't register symbol %s",
  132. rule.hostname_map, rule.symbol)
  133. return
  134. end
  135. return function(task)
  136. local from_hn = task:get_hostname()
  137. if not from_hn then
  138. lua_util.debugm(N, task, 'sending hostname is missing')
  139. return
  140. end
  141. local rcvd_hdrs = task:get_received_headers()
  142. -- Try find sending hostname in Received headers
  143. for _, rcvd in ipairs(rcvd_hdrs) do
  144. if rcvd.by_hostname == from_hn and rcvd.real_ip then
  145. if not hostname_map:get_key(rcvd.from_hostname) then
  146. -- Remote hostname is not another relay, use this header
  147. return set_from_rcvd(task, rcvd)
  148. else
  149. -- Keep checking with new hostname
  150. from_hn = rcvd.from_hostname
  151. end
  152. end
  153. end
  154. rspamd_logger.errx(task, 'found nothing useful in Received headers')
  155. end
  156. end
  157. strategies['local'] = function(rule)
  158. return function(task)
  159. local from_ip = task:get_from_ip()
  160. if not from_ip then
  161. lua_util.debugm(N, task, 'sending IP is missing')
  162. return
  163. end
  164. if not from_ip:is_local() then
  165. lua_util.debugm(N, task, 'sending IP (%s) is non-local', from_ip)
  166. return
  167. end
  168. local rcvd_hdrs = task:get_received_headers()
  169. local num_rcvd = #rcvd_hdrs
  170. -- Try find first non-local IP in Received headers
  171. for i, rcvd in ipairs(rcvd_hdrs) do
  172. if rcvd.real_ip then
  173. local rcvd_ip = rcvd.real_ip
  174. if rcvd_ip and rcvd_ip:is_valid() and (not rcvd_ip:is_local() or i == num_rcvd) then
  175. return set_from_rcvd(task, rcvd)
  176. end
  177. end
  178. end
  179. rspamd_logger.errx(task, 'found nothing useful in Received headers')
  180. end
  181. end
  182. local opts = rspamd_config:get_all_opt(N)
  183. if opts then
  184. settings = lua_util.override_defaults(settings, opts)
  185. local ok, schema_err = config_schema:transform(settings)
  186. if not ok then
  187. rspamd_logger.errx(rspamd_config, 'config schema error: %s', schema_err)
  188. lua_util.disable_module(N, "config")
  189. return
  190. end
  191. for k, rule in pairs(settings.rules) do
  192. if not rule.symbol then
  193. rule.symbol = k
  194. end
  195. local cb = strategies[rule.strategy](rule)
  196. if cb then
  197. rspamd_config:register_symbol({
  198. name = rule.symbol,
  199. type = 'prefilter',
  200. priority = rule.priority or lua_util.symbols_priorities.top + 1,
  201. group = N,
  202. callback = cb,
  203. })
  204. end
  205. end
  206. end