/* * ProFTPD: mod_fw_ipset * * Allows passive ftp connections through stateful firewall by adding the * clients address to an ipset of type ip,port,ip. * * This module can be configured with: * - FwIPsetName4 * - FwIPsetName6 * - FwIPsetTimeout (default 30) * * Make sure to match incoming connections against the ipset. e.g. for iptables: * -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEP * -A INPUT -p tcp -m set --match-set dst,dst,src -j ACCEPT * * Copyright (c) 2017 Manuel Mausz * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. * * $Libraries: -lipset -lcap$ */ #include "conf.h" #include "privs.h" #include #include #include #include #define MOD_FW_IPSET_VERSION "mod_fw_ipset/0.1.1" static const char *trace_channel = "fw_ipset"; static struct ipset_session *ipset; static cap_t capabilities = NULL; static int disable_id_switching = TRUE; static const char *setname4 = NULL; static const char *setname6 = NULL; static uint32_t settimeout = 30; module fw_ipset_module; static int fw_ipset_sess_init(void); static int try_ipset_create(struct ipset_session *session, const char *setname, const char *typename, int family, uint32_t timeout) { pr_trace_msg(trace_channel, 5, "creating ipset: %s with timeout %d", setname, timeout); ipset_session_data_set(session, IPSET_SETNAME, setname); ipset_session_data_set(session, IPSET_OPT_TYPENAME, typename); PRIVS_ROOT const struct ipset_type *type = ipset_type_get(session, IPSET_CMD_CREATE); PRIVS_RELINQUISH if (type == NULL) { pr_log_pri(PR_LOG_ERR, MOD_FW_IPSET_VERSION ": Cannot find ipset type %s: %s", typename, ipset_session_error(session)); return FALSE; } ipset_session_data_set(session, IPSET_OPT_TIMEOUT, &timeout); ipset_session_data_set(session, IPSET_OPT_TYPE, type); ipset_session_data_set(session, IPSET_OPT_FAMILY, &family); PRIVS_ROOT int res = ipset_cmd(session, IPSET_CMD_CREATE, /*lineno*/ 0); PRIVS_RELINQUISH if (res != 0) { pr_log_pri(PR_LOG_ERR, MOD_FW_IPSET_VERSION ": Failed to create ipset %s: %s", setname, ipset_session_error(session)); return FALSE; } return TRUE; } static int try_ipset_cmd(struct ipset_session *session, enum ipset_cmd cmd, const char *setname, const int family, const pr_netaddr_t *local_addr, const pr_netaddr_t *remote_addr) { pr_trace_msg(trace_channel, 5, "adding data connection to ipset %s", setname); ipset_session_data_set(session, IPSET_SETNAME, setname); const struct ipset_type *type = ipset_type_get(session, cmd); if (type == NULL) { pr_log_pri(PR_LOG_ERR, MOD_FW_IPSET_VERSION ": Cannot find ipset %s: %s", setname, ipset_session_error(session)); return FALSE; } ipset_session_data_set(session, IPSET_OPT_FAMILY, &family); ipset_session_data_set(session, IPSET_OPT_IP, pr_netaddr_get_inaddr(local_addr)); ipset_session_data_set(session, IPSET_OPT_IP2, pr_netaddr_get_inaddr(remote_addr)); uint8_t proto = 6; /* TCP */ ipset_session_data_set(session, IPSET_OPT_PROTO, &proto); uint16_t local_port = ntohs(pr_netaddr_get_port(local_addr)); ipset_session_data_set(session, IPSET_OPT_PORT, &local_port); int res = ipset_cmd(session, cmd, /*lineno*/ 0); if (res != 0) { pr_log_pri(PR_LOG_ERR, MOD_FW_IPSET_VERSION ": Failed to add ipset %s: %s", setname, ipset_session_error(session)); return FALSE; } return TRUE; } static int lp_get_cap(void) { if ((capabilities = cap_get_proc()) == NULL) { pr_log_pri(PR_LOG_ERR, MOD_FW_IPSET_VERSION ": fetching capabilities failed: %s", strerror(errno)); return -1; } return 0; } static void lp_free_cap() { if (cap_free(capabilities) < 0) pr_log_pri(PR_LOG_NOTICE, MOD_FW_IPSET_VERSION ": error freeing cap at line %d: %s", __LINE__ - 2, strerror(errno)); capabilities = NULL; } static int lp_modify_cap(cap_flag_value_t flag, cap_value_t cap, cap_flag_t set) { if (cap_set_flag(capabilities, set, 1, &cap, flag) == -1) { pr_log_pri(PR_LOG_ERR, MOD_FW_IPSET_VERSION ": cap_set_flag failed: %s", strerror(errno)); return -1; } if (cap_set_proc(capabilities) == -1) { pr_log_pri(PR_LOG_ERR, MOD_FW_IPSET_VERSION ": cap_set_proc failed: %s", strerror(errno)); return -1; } return 0; } static int cap_net_admin(void) { if (getuid() != PR_ROOT_UID) { pr_trace_msg(trace_channel, 5, "set capability to cap_net_admin+ep"); return (lp_get_cap() != -1 && lp_modify_cap(CAP_SET, CAP_NET_ADMIN, CAP_EFFECTIVE) != -1); } else { disable_id_switching = session.disable_id_switching; session.disable_id_switching = FALSE; PRIVS_ROOT return 0; } } static int no_cap_net_admin(void) { if (capabilities != NULL) { pr_trace_msg(trace_channel, 5, "set capability to cap_net_admin+p again"); int res = lp_modify_cap(CAP_CLEAR, CAP_NET_ADMIN, CAP_EFFECTIVE); (void)lp_free_cap(); return (res != -1); } else { PRIVS_RELINQUISH session.disable_id_switching = disable_id_switching; return 0; } } /* configuration handlers */ MODRET set_ipset_name(cmd_rec *cmd) { CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); add_config_param_str(cmd->argv[0], 1, cmd->argv[1]); return PR_HANDLED(cmd); } MODRET set_ipset_timeout(cmd_rec *cmd) { int timeout = -1; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); if (pr_str_get_duration(cmd->argv[1], &timeout) < 0) { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing timeout value '", cmd->argv[1], "': ", strerror(errno), NULL)); } config_rec *c = add_config_param(cmd->argv[0], 1, NULL); c->argv[0] = pcalloc(c->pool, sizeof(int)); *((int *) c->argv[0]) = timeout; return PR_HANDLED(cmd); } /* event handlers */ static void fw_ipset_data_listen_ev(const void *event_data, void *user_data) { const struct socket_ctx *sc = event_data; if (session.c->remote_addr == NULL) return; int family; const char *setname; switch(pr_netaddr_get_family(sc->addr)) { case AF_INET: family = NFPROTO_IPV4; setname = setname4; break; #ifdef PR_USE_IPV6 case AF_INET6: if (pr_netaddr_use_ipv6()) { family = NFPROTO_IPV6; setname = setname6; break; } #endif default: pr_log_pri(PR_LOG_ERR, MOD_FW_IPSET_VERSION ": Unknown address family"); return; } cap_net_admin(); try_ipset_cmd(ipset, IPSET_CMD_ADD, setname, family, sc->addr, session.c->remote_addr); no_cap_net_admin(); } #if defined(PR_SHARED_MODULE) static void fw_ipset_mod_unload_ev(const void *event_data, void *user_data) { if (strcmp("mod_ipset.c", (const char *)event_data) == 0) pr_event_unregister(&fw_ipset_module, NULL, NULL); } #endif static void fw_ipset_exit_ev(const void *event_data, void *user_data) { ipset_session_fini(ipset); } static void fw_ipset_sess_reinit_ev(const void *event_data, void *user_data) { /* A HOST command changed the main_server pointer, reinitialize ourselves. */ pr_event_unregister(&fw_ipset_module, "core.exit", fw_ipset_exit_ev); pr_event_unregister(&fw_ipset_module, "core.data-listen", fw_ipset_data_listen_ev); pr_event_unregister(&fw_ipset_module, "core.session-reinit", fw_ipset_sess_reinit_ev); ipset_session_fini(ipset); if (fw_ipset_sess_init() < 0) pr_session_disconnect(&fw_ipset_module, PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL); } /* initialization routines */ static int fw_ipset_trace(const char *fmt, ...) { va_list msg; va_start(msg, fmt); int res = pr_trace_vmsg(trace_channel, 5, fmt, msg); va_end(msg); return res; } static int fw_ipset_sess_init(void) { config_rec *c = NULL; c = find_config(main_server->conf, CONF_PARAM, "FwIPsetTimeout", FALSE); if (c != NULL) settimeout = *((uint32_t *)c->argv[0]); setname4 = get_param_ptr(main_server->conf, "FwIPsetName4", FALSE); setname6 = get_param_ptr(main_server->conf, "FwIPsetName6", FALSE); if (setname4 == NULL && setname6 == NULL) return 0; // don't hardfail the client if an error occured. worst case // the client can't transmit files in passive mode if ((ipset = ipset_session_init(fw_ipset_trace)) == NULL) { pr_log_pri(PR_LOG_ERR, MOD_FW_IPSET_VERSION ": Unable to initialize ipset session"); return 0; } /* ignore already created sets */ ipset_envopt_parse(ipset, IPSET_ENV_EXIST, NULL); if (setname4 != NULL) { if (!try_ipset_create(ipset, setname4, "hash:ip,port,ip", NFPROTO_IPV4, settimeout)) return 0; } #ifdef PR_USE_IPV6 if (setname6 != NULL && pr_netaddr_use_ipv6()) { if (!try_ipset_create(ipset, setname6, "hash:ip,port,ip", NFPROTO_IPV6, settimeout)) return 0; } #endif pr_event_register(&fw_ipset_module, "core.data-listen", fw_ipset_data_listen_ev, NULL); pr_event_register(&fw_ipset_module, "core.session-reinit", fw_ipset_sess_reinit_ev, NULL); pr_event_register(&fw_ipset_module, "core.exit", fw_ipset_exit_ev, NULL); return 0; } static int fw_ipset_init(void) { #if defined(PR_SHARED_MODULE) pr_event_register(&fw_ipset_module, "core.module-unload", fw_ipset_mod_unload_ev, NULL); #endif ipset_load_types(); return 0; } /* module api tables */ static conftable fw_ipset_conftab[] = { { "FwIPsetName4", set_ipset_name, NULL }, { "FwIPsetName6", set_ipset_name, NULL }, { "FwIPsetTimeout", set_ipset_timeout, NULL }, { NULL } }; module fw_ipset_module = { /* always NULL */ NULL, NULL, /* module api version 2.0 */ 0x20, /* module name */ "fw_ipset", /* module configuration handler table */ fw_ipset_conftab, /* module command handler table */ NULL, /* module authentication handler table */ NULL, /* module initialization function */ fw_ipset_init, /* module session initialization function */ fw_ipset_sess_init, /* module version */ MOD_FW_IPSET_VERSION };