Download | Plain Text | Line Numbers
/*
* 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 <name_of_ipset_for_ipv4>
* - FwIPsetName6 <name_of_ipset_for_ipv6>
* - FwIPsetTimeout <ipset_default_timeout> (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 <set_name> 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 <libipset/types.h>
#include <libipset/session.h>
#include <linux/capability.h>
#include <sys/capability.h>
#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
};