1 /*
2 * ProFTPD: mod_dnsbl -- a module for checking DNSBL (DNS Black Lists)
3 * servers before allowing a connection
4 * Copyright (c) 2007-2020 TJ Saunders
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
19 *
20 * As a special exemption, TJ Saunders and other respective copyright holders
21 * give permission to link this program with OpenSSL, and distribute the
22 * resulting executable, without including the source code for OpenSSL in the
23 * source distribution.
24 *
25 * This is mod_dnsbl, contrib software for proftpd 1.3.x and above.
26 * For more information contact TJ Saunders <tj@castaglia.org>.
27 */
28
29 #include "mod_dnsbl.h"
30
31 /* The C_ANY macro is defined in ProFTPD's ftp.h file for "any" FTP command,
32 * and may conflict with the DNS macros. This module does not use ProFTPD's
33 * C_ANY macro, so remove it and avoid the collision.
34 */
35 #undef C_ANY
36
37 #include <arpa/nameser.h>
38 #include <resolv.h>
39
40 #define DNSBL_REASON_MAX_LEN 256
41
42 module dnsbl_module;
43
44 static int dnsbl_engine = FALSE;
45 static int dnsbl_logfd = -1;
46
47 static const char *trace_channel = "dnsbl";
48
49 /* Necessary prototypes. */
50 static int dnsbl_sess_init(void);
51
52 typedef enum {
53 DNSBL_POLICY_ALLOW_DENY,
54 DNSBL_POLICY_DENY_ALLOW
55
56 } dnsbl_policy_e;
57
reverse_ip_addr(pool * p,const char * ip_addr)58 static const char *reverse_ip_addr(pool *p, const char *ip_addr) {
59 char *addr2, *res, *tmp;
60 size_t addrlen;
61
62 if (p == NULL ||
63 ip_addr == NULL) {
64 errno = EINVAL;
65 return NULL;
66 }
67
68 addrlen = strlen(ip_addr) +1;
69
70 res = pcalloc(p, addrlen);
71 addr2 = pstrdup(p, ip_addr);
72
73 tmp = strrchr(addr2, '.');
74 sstrcat(res, tmp+1, addrlen);
75 sstrcat(res, ".", addrlen);
76 *tmp = '\0';
77
78 tmp = strrchr(addr2, '.');
79 sstrcat(res, tmp+1, addrlen);
80 sstrcat(res, ".", addrlen);
81 *tmp = '\0';
82
83 tmp = strrchr(addr2, '.');
84 sstrcat(res, tmp+1, addrlen);
85 sstrcat(res, ".", addrlen);
86 *tmp = '\0';
87
88 sstrcat(res, addr2, addrlen);
89 return res;
90 }
91
get_reversed_addr(pool * p)92 static const char *get_reversed_addr(pool *p) {
93 const char *ipstr = NULL;
94
95 if (pr_netaddr_get_family(session.c->remote_addr) == AF_INET) {
96 ipstr = pr_netaddr_get_ipstr(session.c->remote_addr);
97
98 #ifdef PR_USE_IPV6
99 } else {
100 if (pr_netaddr_use_ipv6() &&
101 pr_netaddr_get_family(session.c->remote_addr) == AF_INET6 &&
102 pr_netaddr_is_v4mappedv6(session.c->remote_addr) == TRUE) {
103 const char *ipv6str = pr_netaddr_get_ipstr(session.c->remote_addr);
104 pr_netaddr_t *tmp = pr_netaddr_alloc(p);
105
106 pr_netaddr_set_family(tmp, AF_INET);
107 pr_netaddr_set_port(tmp, pr_netaddr_get_port(session.c->remote_addr));
108 memcpy(&tmp->na_addr.v4.sin_addr,
109 (((char *) pr_netaddr_get_inaddr(session.c->remote_addr)) + 12),
110 sizeof(struct in_addr));
111
112 ipstr = pr_netaddr_get_ipstr(tmp);
113
114 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
115 "client address '%s' is an IPv4-mapped IPv6 address, treating it as "
116 "IPv4 address '%s'", ipv6str, ipstr);
117
118 } else {
119 return NULL;
120 }
121 #endif /* PR_USE_IPV6 */
122 }
123
124 return reverse_ip_addr(p, ipstr);
125 }
126
lookup_reason(pool * p,const char * name)127 static void lookup_reason(pool *p, const char *name) {
128 int reasonlen;
129 unsigned char reason[NS_PACKETSZ];
130
131 reasonlen = res_query(name, ns_c_in, ns_t_txt, reason, sizeof(reason));
132 if (reasonlen > 0) {
133 ns_msg handle;
134 int rrno;
135
136 /* Now we get the unenviable task of hand-parsing the response record,
137 * trying to get at the actual text message contained within.
138 */
139
140 if (ns_initparse(reason, reasonlen, &handle) < 0) {
141 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
142 "error initialising nameserver response parser: %s", strerror(errno));
143 return;
144 }
145
146 for (rrno = 0; rrno < ns_msg_count(handle, ns_s_an); rrno++) {
147 ns_rr rr;
148
149 if (ns_parserr(&handle, ns_s_an, rrno, &rr) < 0) {
150 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
151 "error parsing resource record %d: %s", rrno, strerror(errno));
152 continue;
153 }
154
155 if (ns_rr_type(rr) == ns_t_txt) {
156 char *reject_reason;
157 size_t len = ns_rr_rdlen(rr);
158
159 reject_reason = pcalloc(p, len+1);
160 memcpy(reject_reason, (unsigned char *) ns_rr_rdata(rr), len);
161
162 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
163 "reason for blacklisting client address: '%s'", reject_reason);
164 }
165 }
166 }
167
168 return;
169 }
170
lookup_addr(pool * p,const char * addr,const char * domain)171 static int lookup_addr(pool *p, const char *addr, const char *domain) {
172 const pr_netaddr_t *reject_addr = NULL;
173 const char *name = pstrcat(p, addr, ".", domain, NULL);
174
175 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
176 "for DNSBLDomain '%s', resolving DNS name '%s'", domain, name);
177
178 reject_addr = pr_netaddr_get_addr(p, name, NULL);
179 if (reject_addr) {
180 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
181 "found record for DNS name '%s', client address has been blacklisted",
182 name);
183
184 /* Check for TXT record for this DNS name, to see if the reason for
185 * blacklisting has been configured.
186 */
187 lookup_reason(p, name);
188 return -1;
189 }
190
191 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
192 "no record returned for DNS name '%s', client address is not blacklisted",
193 name);
194 return 0;
195 }
196
dnsbl_reject_conn(void)197 static int dnsbl_reject_conn(void) {
198 config_rec *c;
199 pool *tmp_pool = NULL;
200 const char *rev_ip_addr = NULL;
201 int reject_conn = FALSE;
202 dnsbl_policy_e policy = DNSBL_POLICY_DENY_ALLOW;
203
204 c = find_config(main_server->conf, CONF_PARAM, "DNSBLPolicy", FALSE);
205 if (c) {
206 policy = *((dnsbl_policy_e *) c->argv[0]);
207 }
208
209 switch (policy) {
210 case DNSBL_POLICY_ALLOW_DENY:
211 pr_trace_msg(trace_channel, 8,
212 "using policy of allowing connections unless listed by DNSBLDomains");
213 reject_conn = FALSE;
214 break;
215
216 case DNSBL_POLICY_DENY_ALLOW:
217 pr_trace_msg(trace_channel, 8,
218 "using policy of rejecting connections unless listed by DNSBLDomains");
219 reject_conn = TRUE;
220 break;
221 }
222
223 tmp_pool = make_sub_pool(permanent_pool);
224 rev_ip_addr = get_reversed_addr(tmp_pool);
225 if (rev_ip_addr == NULL) {
226 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
227 "client address '%s' is an IPv6 address, skipping",
228 pr_netaddr_get_ipstr(session.c->remote_addr));
229 destroy_pool(tmp_pool);
230 return -1;
231 }
232
233 switch (policy) {
234 /* For this policy, the connection will be allowed unless the connecting
235 * client is listed by any of the DNSBLDomain sites.
236 */
237 case DNSBL_POLICY_ALLOW_DENY: {
238 c = find_config(main_server->conf, CONF_PARAM, "DNSBLDomain", FALSE);
239 while (c) {
240 const char *domain;
241
242 pr_signals_handle();
243
244 domain = c->argv[0];
245
246 if (lookup_addr(tmp_pool, rev_ip_addr, domain) < 0) {
247 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
248 "client address '%s' is listed by DNSBLDomain '%s', rejecting "
249 "connection", pr_netaddr_get_ipstr(session.c->remote_addr), domain);
250 reject_conn = TRUE;
251 break;
252 }
253
254 c = find_config_next(c, c->next, CONF_PARAM, "DNSBLDomain", FALSE);
255 }
256
257 break;
258 }
259
260 /* For this policy, the connection will be NOT allowed unless the
261 * connecting client is listed by any of the DNSBLDomain sites.
262 */
263 case DNSBL_POLICY_DENY_ALLOW: {
264 c = find_config(main_server->conf, CONF_PARAM, "DNSBLDomain", FALSE);
265 while (c) {
266 const char *domain;
267
268 pr_signals_handle();
269
270 domain = c->argv[0];
271
272 if (lookup_addr(tmp_pool, rev_ip_addr, domain) < 0) {
273 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
274 "client address '%s' is listed by DNSBLDomain '%s', allowing "
275 "connection", pr_netaddr_get_ipstr(session.c->remote_addr), domain);
276 reject_conn = FALSE;
277 break;
278 }
279
280 c = find_config_next(c, c->next, CONF_PARAM, "DNSBLDomain", FALSE);
281 }
282
283 break;
284 }
285 }
286
287 destroy_pool(tmp_pool);
288
289 if (reject_conn) {
290 return TRUE;
291 }
292
293 return FALSE;
294 }
295
296 /* Configuration handlers
297 */
298
299 /* usage: DNSBLDomain domain */
set_dnsbldomain(cmd_rec * cmd)300 MODRET set_dnsbldomain(cmd_rec *cmd) {
301 char *domain;
302 config_rec *c;
303 CHECK_ARGS(cmd, 1);
304 CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
305
306 domain = cmd->argv[1];
307
308 /* Ignore leading '.' in domain, if present. */
309 if (*domain == '.')
310 domain++;
311
312 c = add_config_param_str(cmd->argv[0], 1, domain);
313 c->flags |= CF_MERGEDOWN_MULTI;
314
315 return PR_HANDLED(cmd);
316 }
317
318 /* usage: DNSBLEngine on|off */
set_dnsblengine(cmd_rec * cmd)319 MODRET set_dnsblengine(cmd_rec *cmd) {
320 int bool;
321 config_rec *c;
322
323 CHECK_ARGS(cmd, 1);
324 CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
325
326 bool = get_boolean(cmd, 1);
327 if (bool == -1)
328 CONF_ERROR(cmd, "expected Boolean parameter");
329
330 c = add_config_param(cmd->argv[0], 1, NULL);
331 c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
332 *((unsigned int *) c->argv[0]) = bool;
333
334 return PR_HANDLED(cmd);
335 }
336
337 /* usage: DNSBLLog path|"none" */
set_dnsbllog(cmd_rec * cmd)338 MODRET set_dnsbllog(cmd_rec *cmd) {
339 CHECK_ARGS(cmd, 1);
340 CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
341
342 if (pr_fs_valid_path(cmd->argv[1]) < 0)
343 CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": ", cmd->argv[1],
344 " is not a valid path", NULL));
345
346 (void) add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
347 return PR_HANDLED(cmd);
348 }
349
350 /* usage: DNSBLPolicy "allow,deny"|"deny,allow" */
set_dnsblpolicy(cmd_rec * cmd)351 MODRET set_dnsblpolicy(cmd_rec *cmd) {
352 dnsbl_policy_e policy = DNSBL_POLICY_ALLOW_DENY;
353 config_rec *c;
354
355 CHECK_ARGS(cmd, 1);
356 CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
357
358 if (strcasecmp(cmd->argv[1], "allow,deny") == 0) {
359 policy = DNSBL_POLICY_ALLOW_DENY;
360
361 } else if (strcasecmp(cmd->argv[1], "deny,allow") == 0) {
362 policy = DNSBL_POLICY_DENY_ALLOW;
363
364 } else {
365 CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": '", cmd->argv[1],
366 "' is not one of the approved DNSBLPolicy settings", NULL));
367 }
368
369 c = add_config_param(cmd->argv[0], 1, NULL);
370 c->argv[0] = pcalloc(c->pool, sizeof(dnsbl_policy_e));
371 *((dnsbl_policy_e *) c->argv[0]) = policy;
372
373 return PR_HANDLED(cmd);
374 }
375
376 /* Event listeners
377 */
378
379 /* Initialization functions
380 */
381
dnsbl_sess_reinit_ev(const void * event_data,void * user_data)382 static void dnsbl_sess_reinit_ev(const void *event_data, void *user_data) {
383 int res;
384
385 /* A HOST command changed the main_server pointer, reinitialize ourselves. */
386
387 pr_event_unregister(&dnsbl_module, "core.session-reinit",
388 dnsbl_sess_reinit_ev);
389
390 (void) close(dnsbl_logfd);
391 dnsbl_logfd = -1;
392
393 res = dnsbl_sess_init();
394 if (res < 0) {
395 pr_session_disconnect(&dnsbl_module,
396 PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
397 }
398 }
399
dnsbl_sess_init(void)400 static int dnsbl_sess_init(void) {
401 config_rec *c;
402
403 pr_event_register(&dnsbl_module, "core.session-reinit", dnsbl_sess_reinit_ev,
404 NULL);
405
406 c = find_config(main_server->conf, CONF_PARAM, "DNSBLEngine", FALSE);
407 if (c &&
408 *((unsigned int *) c->argv[0]) == TRUE) {
409 dnsbl_engine = TRUE;
410
411 } else {
412 return 0;
413 }
414
415 c = find_config(main_server->conf, CONF_PARAM, "DNSBLLog", FALSE);
416 if (c &&
417 strcasecmp(c->argv[0], "none") != 0) {
418 int res, xerrno = 0;
419
420 PRIVS_ROOT
421 res = pr_log_openfile(c->argv[0], &dnsbl_logfd, 0600);
422 xerrno = errno;
423 PRIVS_RELINQUISH
424
425 switch (res) {
426 case -1:
427 pr_log_pri(PR_LOG_NOTICE, MOD_DNSBL_VERSION
428 ": notice: unable to open DNSBLLog '%s': %s", (char *) c->argv[0],
429 strerror(xerrno));
430 break;
431
432 case PR_LOG_WRITABLE_DIR:
433 pr_log_pri(PR_LOG_WARNING, MOD_DNSBL_VERSION
434 ": notice: unable to use DNSBLLog '%s': parent directory is "
435 "world-writable", (char *) c->argv[0]);
436 break;
437
438 case PR_LOG_SYMLINK:
439 pr_log_pri(PR_LOG_WARNING, MOD_DNSBL_VERSION
440 ": notice: unable to use DNSBLLog '%s': cannot log to a symlink",
441 (char *) c->argv[0]);
442 break;
443 }
444 }
445
446 if (dnsbl_reject_conn() == TRUE) {
447 (void) pr_log_writefile(dnsbl_logfd, MOD_DNSBL_VERSION,
448 "client not allowed by DNSBLPolicy, rejecting connection");
449 errno = EACCES;
450 return -1;
451 }
452
453 return 0;
454 }
455
456 /* Module API tables
457 */
458
459 static conftable dnsbl_conftab[] = {
460 { "DNSBLDomain", set_dnsbldomain, NULL },
461 { "DNSBLEngine", set_dnsblengine, NULL },
462 { "DNSBLLog", set_dnsbllog, NULL },
463 { "DNSBLPolicy", set_dnsblpolicy, NULL },
464 { NULL }
465 };
466
467 module dnsbl_module = {
468 NULL, NULL,
469
470 /* Module API version 2.0 */
471 0x20,
472
473 /* Module name */
474 "dnsbl",
475
476 /* Module configuration handler table */
477 dnsbl_conftab,
478
479 /* Module command handler table */
480 NULL,
481
482 /* Module authentication handler table */
483 NULL,
484
485 /* Module initialization function */
486 NULL,
487
488 /* Session initialization function */
489 dnsbl_sess_init,
490
491 /* Module version */
492 MOD_DNSBL_VERSION
493 };
494