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
};