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