proftpd-mod_geoip2/0000755000175000001440000000000013720461205013572 5ustar hilleusersproftpd-mod_geoip2/mod_geoip2.c0000644000175000001440000011331413720460146015770 0ustar hilleusers/* * ProFTPD: mod_geoip2 -- a module for looking up country/city/etc for clients * Copyright (c) 2019 TJ Saunders * * 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., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. * * As a special exemption, TJ Saunders and other respective copyright holders * give permission to link this program with OpenSSL, and distribute the * resulting executable, without including the source code for OpenSSL in the * source distribution. * * This is mod_geoip2, contrib software for proftpd 1.3.x and above. * For more information contact TJ Saunders . * * --- DO NOT DELETE BELOW THIS LINE ---- * $Libraries: -lmaxminddb$ */ #include "conf.h" #include "privs.h" /* A lot of ideas for this module were liberally borrowed from the mod_geoip * module for Apache. */ #define MOD_GEOIP2_VERSION "mod_geoip2/0.1" /* Make sure the version of proftpd is as necessary. */ #if PROFTPD_VERSION_NUMBER < 0x0001030402 # error "ProFTPD 1.3.4rc2 or later required" #endif #include module geoip2_module; static int geoip2_engine = FALSE; static int geoip2_logfd = -1; static pool *geoip2_pool = NULL; static array_header *geoip2_mmdbs = NULL; /* The types of data that GeoIP can provide, and that we care about. */ static const char *geoip_city = NULL; static const char *geoip_postal_code = NULL; static const char *geoip_latitude = NULL; static const char *geoip_longitude = NULL; static const char *geoip_org = NULL; static const char *geoip_country_code2 = NULL; static const char *geoip_country_name = NULL; static const char *geoip_region_code = NULL; static const char *geoip_region_name = NULL; static const char *geoip_continent_code = NULL; static const char *geoip_asn = NULL; static const char *geoip_timezone = NULL; /* Names of supported GeoIP values */ #define GEOIP_FILTER_KEY_COUNTRY_CODE 100 #define GEOIP_FILTER_KEY_COUNTRY_NAME 101 #define GEOIP_FILTER_KEY_REGION_CODE 102 #define GEOIP_FILTER_KEY_REGION_NAME 103 #define GEOIP_FILTER_KEY_CONTINENT 104 #define GEOIP_FILTER_KEY_ORGANIZATION 105 #define GEOIP_FILTER_KEY_CITY 106 #define GEOIP_FILTER_KEY_POSTAL_CODE 107 #define GEOIP_FILTER_KEY_LATITUDE 108 #define GEOIP_FILTER_KEY_LONGITUDE 109 #define GEOIP_FILTER_KEY_ASN 110 #define GEOIP_FILTER_KEY_TIMEZONE 111 struct geoip_filter_key { const char *filter_name; int filter_id; }; static struct geoip_filter_key geoip_filter_keys[] = { { "CountryCode", GEOIP_FILTER_KEY_COUNTRY_CODE }, { "CountryName", GEOIP_FILTER_KEY_COUNTRY_NAME }, { "RegionCode", GEOIP_FILTER_KEY_REGION_CODE }, { "RegionName", GEOIP_FILTER_KEY_REGION_NAME }, { "Continent", GEOIP_FILTER_KEY_CONTINENT }, { "Organization", GEOIP_FILTER_KEY_ORGANIZATION }, { "City", GEOIP_FILTER_KEY_CITY }, { "PostalCode", GEOIP_FILTER_KEY_POSTAL_CODE }, { "Latitude", GEOIP_FILTER_KEY_LATITUDE }, { "Longitude", GEOIP_FILTER_KEY_LONGITUDE }, { "ASN", GEOIP_FILTER_KEY_ASN }, { "Timezone", GEOIP_FILTER_KEY_TIMEZONE }, { NULL, -1 } }; #if PR_USE_REGEX /* GeoIP filter */ struct geoip_filter { int filter_id; const char *filter_pattern; pr_regex_t *filter_re; }; #endif /* PR_USE_REGEX */ /* GeoIP policies */ typedef enum { GEOIP_POLICY_ALLOW_DENY, GEOIP_POLICY_DENY_ALLOW } geoip_policy_e; static geoip_policy_e geoip_policy = GEOIP_POLICY_ALLOW_DENY; static const char *trace_channel = "geoip2"; static const char *get_geoip_filter_name(int); static const char *get_geoip_filter_value(int); static int get_filter_id(const char *filter_name) { register unsigned int i; int filter_id = -1; for (i = 0; geoip_filter_keys[i].filter_name != NULL; i++) { if (strcasecmp(filter_name, geoip_filter_keys[i].filter_name) == 0) { filter_id = geoip_filter_keys[i].filter_id; break; } } return filter_id; } #if PR_USE_REGEX static int get_filter(pool *p, const char *pattern, pr_regex_t **pre) { int res; *pre = pr_regexp_alloc(&geoip2_module); res = pr_regexp_compile(*pre, pattern, REG_EXTENDED|REG_NOSUB|REG_ICASE); if (res != 0) { char errstr[256]; memset(errstr, '\0', sizeof(errstr)); pr_regexp_error(res, *pre, errstr, sizeof(errstr)-1); pr_regexp_free(&geoip2_module, *pre); *pre = NULL; pr_log_pri(PR_LOG_DEBUG, MOD_GEOIP2_VERSION ": pattern '%s' failed regex compilation: %s", pattern, errstr); errno = EINVAL; return -1; } return res; } static struct geoip_filter *make_filter(pool *p, const char *filter_name, const char *pattern) { struct geoip_filter *filter; int filter_id; pr_regex_t *pre = NULL; filter_id = get_filter_id(filter_name); if (filter_id < 0) { pr_log_debug(DEBUG0, MOD_GEOIP2_VERSION ": unknown GeoIP filter name '%s'", filter_name); return NULL; } if (get_filter(p, pattern, &pre) < 0) { return NULL; } filter = pcalloc(p, sizeof(struct geoip_filter)); filter->filter_id = filter_id; filter->filter_pattern = pstrdup(p, pattern); filter->filter_re = pre; return filter; } static array_header *get_sql_filters(pool *p, const char *query_name) { register unsigned int i; cmdtable *sql_cmdtab = NULL; cmd_rec *sql_cmd = NULL; modret_t *sql_res = NULL; array_header *sql_data = NULL; const char **values = NULL; array_header *sql_filters = NULL; sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL, NULL); if (sql_cmdtab == NULL) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "unable to execute SQLNamedQuery '%s': mod_sql not loaded", query_name); errno = EPERM; return NULL; } sql_cmd = pr_cmd_alloc(p, 2, "sql_lookup", query_name); sql_res = pr_module_call(sql_cmdtab->m, sql_cmdtab->handler, sql_cmd); if (sql_res == NULL || MODRET_ISERROR(sql_res)) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "error processing SQLNamedQuery '%s'; check mod_sql logs for details", query_name); errno = EPERM; return NULL; } sql_data = sql_res->data; pr_trace_msg(trace_channel, 9, "SQLNamedQuery '%s' returned item count %d", query_name, sql_data->nelts); if (sql_data->nelts == 0) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "SQLNamedQuery '%s' returned no values", query_name); errno = ENOENT; return NULL; } if (sql_data->nelts % 2 == 1) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "SQLNamedQuery '%s' returned odd number of values (%d), " "expected even number", query_name, sql_data->nelts); errno = EINVAL; return NULL; } values = sql_data->elts; sql_filters = make_array(p, 0, sizeof(struct geoip_filter)); for (i = 0; i < sql_data->nelts; i += 2) { const char *filter_name, *pattern = NULL; struct geoip_filter *filter; filter_name = values[i]; pattern = values[i+1]; filter = make_filter(p, filter_name, pattern); if (filter == NULL) { pr_trace_msg(trace_channel, 3, "unable to use '%s %s' as filter: %s", filter_name, pattern, strerror(errno)); continue; } *((struct geoip_filter **) push_array(sql_filters)) = filter; } return sql_filters; } #endif /* PR_USE_REGEX */ static void resolve_deferred_patterns(pool *p, const char *directive) { #if PR_USE_REGEX config_rec *c; c = find_config(main_server->conf, CONF_PARAM, directive, FALSE); while (c != NULL) { register unsigned int i; array_header *deferred_filters, *filters; pr_signals_handle(); filters = c->argv[0]; deferred_filters = c->argv[1]; for (i = 0; i < deferred_filters->nelts; i++) { const char *query_name; array_header *sql_filters; query_name = ((const char **) deferred_filters->elts)[i]; sql_filters = get_sql_filters(p, query_name); if (sql_filters == NULL) { continue; } array_cat(filters, sql_filters); } c = find_config_next(c, c->next, CONF_PARAM, directive, FALSE); } #endif /* PR_USE_REGEX */ } static void resolve_deferred_filters(pool *p) { resolve_deferred_patterns(p, "GeoIPAllowFilter"); resolve_deferred_patterns(p, "GeoIPDenyFilter"); } static int check_geoip_filters(geoip_policy_e policy) { int allow_conn = 0, matched_allow_filter = -1, matched_deny_filter = -1; #if PR_USE_REGEX config_rec *c; c = find_config(main_server->conf, CONF_PARAM, "GeoIPAllowFilter", FALSE); while (c != NULL) { register unsigned int i; int matched = TRUE; array_header *filters; pr_signals_handle(); if (matched_allow_filter == -1) { matched_allow_filter = FALSE; } filters = c->argv[0]; for (i = 0; i < filters->nelts; i++) { int filter_id, res; struct geoip_filter *filter; pr_regex_t *filter_re; const char *filter_name, *filter_pattern, *filter_value; filter = ((struct geoip_filter **) filters->elts)[i]; filter_id = filter->filter_id; filter_pattern = filter->filter_pattern; filter_re = filter->filter_re; filter_value = get_geoip_filter_value(filter_id); if (filter_value == NULL) { matched = FALSE; break; } filter_name = get_geoip_filter_name(filter_id); res = pr_regexp_exec(filter_re, filter_value, 0, NULL, 0, 0, 0); pr_trace_msg(trace_channel, 12, "%s filter value %s %s GeoIPAllowFilter pattern '%s'", filter_name, filter_value, res == 0 ? "matched" : "did not match", filter_pattern); if (res == 0) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "%s filter value '%s' matched GeoIPAllowFilter pattern '%s'", filter_name, filter_value, filter_pattern); } else { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "%s filter value '%s' did not match GeoIPAllowFilter pattern '%s'", filter_name, filter_value, filter_pattern); matched = FALSE; break; } } if (matched == TRUE) { matched_allow_filter = TRUE; break; } c = find_config_next(c, c->next, CONF_PARAM, "GeoIPAllowFilter", FALSE); } c = find_config(main_server->conf, CONF_PARAM, "GeoIPDenyFilter", FALSE); while (c != NULL) { register unsigned int i; int matched = TRUE; array_header *filters; pr_signals_handle(); if (matched_deny_filter == -1) { matched_deny_filter = FALSE; } filters = c->argv[0]; for (i = 0; i < filters->nelts; i++) { int filter_id, res; struct geoip_filter *filter; pr_regex_t *filter_re; const char *filter_name, *filter_pattern, *filter_value; filter = ((struct geoip_filter **) filters->elts)[i]; filter_id = filter->filter_id; filter_pattern = filter->filter_pattern; filter_re = filter->filter_re; filter_value = get_geoip_filter_value(filter_id); if (filter_value == NULL) { matched = FALSE; break; } filter_name = get_geoip_filter_name(filter_id); res = pr_regexp_exec(filter_re, filter_value, 0, NULL, 0, 0, 0); pr_trace_msg(trace_channel, 12, "%s filter value %s %s GeoIPDenyFilter pattern '%s'", filter_name, filter_value, res == 0 ? "matched" : "did not match", filter_pattern); if (res == 0) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "%s filter value '%s' matched GeoIPDenyFilter pattern '%s'", filter_name, filter_value, filter_pattern); } else { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "%s filter value '%s' did not match GeoIPDenyFilter pattern '%s'", filter_name, filter_value, filter_pattern); matched = FALSE; break; } } if (matched == TRUE) { matched_deny_filter = TRUE; break; } c = find_config_next(c, c->next, CONF_PARAM, "GeoIPDenyFilter", FALSE); } #endif /* !HAVE_REGEX_H or !HAVE_REGCOMP */ switch (policy) { case GEOIP_POLICY_ALLOW_DENY: if (matched_deny_filter == TRUE && matched_allow_filter != TRUE) { /* If we explicitly matched any deny filters AND have NOT explicitly * matched any allow filters, the connection is rejected, otherwise, * it is allowed. */ (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "client matched GeoIPDenyFilter, rejecting connection"); allow_conn = -1; } else { pr_trace_msg(trace_channel, 9, "allowing client connection (policy 'allow,deny')"); } break; case GEOIP_POLICY_DENY_ALLOW: if (matched_allow_filter == FALSE) { /* If we have not explicitly matched any allow filters, then * reject the connection. */ (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "client did not match any GeoIPAllowFilters, rejecting connection"); allow_conn = -1; } else { pr_trace_msg(trace_channel, 9, "allowing client connection (policy 'deny,allow')"); } break; } return allow_conn; } static const char *get_geoip_filter_name(int filter_id) { register unsigned int i; for (i = 0; geoip_filter_keys[i].filter_name != NULL; i++) { if (geoip_filter_keys[i].filter_id == filter_id) { return geoip_filter_keys[i].filter_name; } } errno = ENOENT; return NULL; } static const char *get_geoip_filter_value(int filter_id) { switch (filter_id) { case GEOIP_FILTER_KEY_COUNTRY_CODE: if (geoip_country_code2 != NULL) { return geoip_country_code2; } break; case GEOIP_FILTER_KEY_COUNTRY_NAME: if (geoip_country_name != NULL) { return geoip_country_name; } break; case GEOIP_FILTER_KEY_REGION_NAME: if (geoip_region_name != NULL) { return geoip_region_name; } break; case GEOIP_FILTER_KEY_CONTINENT: if (geoip_continent_code != NULL) { return geoip_continent_code; } break; case GEOIP_FILTER_KEY_ORGANIZATION: if (geoip_org != NULL) { return geoip_org; } break; case GEOIP_FILTER_KEY_CITY: if (geoip_city != NULL) { return geoip_city; } break; case GEOIP_FILTER_KEY_POSTAL_CODE: if (geoip_postal_code != NULL) { return geoip_postal_code; } break; case GEOIP_FILTER_KEY_LATITUDE: if (geoip_latitude != NULL) { return geoip_latitude; } break; case GEOIP_FILTER_KEY_LONGITUDE: if (geoip_longitude != NULL) { return geoip_longitude; } break; case GEOIP_FILTER_KEY_ASN: if (geoip_asn != NULL) { return geoip_asn; } break; case GEOIP_FILTER_KEY_TIMEZONE: if (geoip_timezone != NULL) { return geoip_timezone; } break; } errno = ENOENT; return NULL; } static void get_geoip_tables(void) { config_rec *c; c = find_config(main_server->conf, CONF_PARAM, "GeoIPTable", FALSE); while (c != NULL) { MMDB_s *mmdb = NULL; const char *path; uint32_t flags; int res, xerrno = 0; pr_signals_handle(); path = c->argv[0]; flags = *((uint32_t *) c->argv[1]); mmdb = pcalloc(geoip2_pool, sizeof(MMDB_s)); PRIVS_ROOT res = MMDB_open(path, flags, mmdb); xerrno = errno; PRIVS_RELINQUISH if (res == MMDB_SUCCESS) { char build_date[64]; time_t build_epoch; *((MMDB_s **) push_array(geoip2_mmdbs)) = mmdb; build_epoch = mmdb->metadata.build_epoch; strftime(build_date, sizeof(build_date), "%F %T UTC", gmtime(&build_epoch)); pr_trace_msg(trace_channel, 15, "loaded GeoIP table '%s': %s (IP version = IPv%d, format version = " "%d.%d, built = %s)", path, mmdb->metadata.database_type, mmdb->metadata.ip_version, mmdb->metadata.binary_format_major_version, mmdb->metadata.binary_format_minor_version, build_date); } else { if (res != MMDB_IO_ERROR) { pr_log_pri(PR_LOG_WARNING, MOD_GEOIP2_VERSION ": warning: unable to open/use GeoIPTable '%s': %s", path, MMDB_strerror(res)); } else { pr_log_pri(PR_LOG_WARNING, MOD_GEOIP2_VERSION ": warning: unable to open/use GeoIPTable '%s': %s (%s)", path, MMDB_strerror(res), strerror(xerrno)); } } c = find_config_next(c, c->next, CONF_PARAM, "GeoIPTable", FALSE); } } static void remove_geoip_tables(void) { register unsigned int i; MMDB_s **mmdbs; if (geoip2_mmdbs == NULL || geoip2_mmdbs->nelts == 0) { return; } mmdbs = geoip2_mmdbs->elts; for (i = 0; i < geoip2_mmdbs->nelts; i++) { if (mmdbs[i] != NULL) { MMDB_close(mmdbs[i]); mmdbs[i] = NULL; } } } static const char *get_geoip_data_text(pool *p, MMDB_lookup_result_s *lookup, const char **lookup_path, int filter_id) { int res, xerrno = 0; const char *text = NULL; MMDB_entry_data_s entry_data; res = MMDB_aget_value(&(lookup->entry), &entry_data, lookup_path); xerrno = errno; if (res != MMDB_SUCCESS) { const char *lookup_name; lookup_name = get_geoip_filter_name(filter_id); switch (res) { case MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR: /* Ignored. */ errno = ENOENT; break; case MMDB_IO_ERROR: pr_trace_msg(trace_channel, 3, "error getting data for %s: %s (%s)", lookup_name, MMDB_strerror(res), strerror(xerrno)); errno = xerrno; break; default: pr_trace_msg(trace_channel, 3, "error getting data for %s: %s", lookup_name, MMDB_strerror(res)); errno = EPERM; break; } return NULL; } if (!entry_data.has_data) { errno = ENOENT; return NULL; } switch (entry_data.type) { case MMDB_DATA_TYPE_UTF8_STRING: text = pstrndup(p, entry_data.utf8_string, entry_data.data_size); break; case MMDB_DATA_TYPE_UINT32: { char buf[64]; memset(buf, '\0', sizeof(buf)); pr_snprintf(buf, sizeof(buf)-1, "%lu", (unsigned long) entry_data.uint32); text = pstrdup(p, buf); break; } case MMDB_DATA_TYPE_DOUBLE: { char buf[64]; memset(buf, '\0', sizeof(buf)); pr_snprintf(buf, sizeof(buf)-1, "%f", entry_data.double_value); text = pstrdup(p, buf); break; } default: pr_trace_msg(trace_channel, 3, "unknown/unsupported entry data type (%lu), ignoring", (unsigned long) entry_data.type); errno = EINVAL; return NULL; } return text; } static void get_geoip_data(void) { register unsigned int i; const char *ip_addr, *text; const char *lookup_path[5] = { NULL, NULL, NULL, NULL, NULL }; MMDB_s **mmdbs; ip_addr = pr_netaddr_get_ipstr(session.c->remote_addr); mmdbs = geoip2_mmdbs->elts; for (i = 0; i < geoip2_mmdbs->nelts; i++) { MMDB_s *mmdb; MMDB_lookup_result_s lookup; int gai_error = 0, mmdb_error = 0; pr_signals_handle(); if (mmdbs[i] == NULL) { continue; } mmdb = mmdbs[i]; lookup = MMDB_lookup_string(mmdb, ip_addr, &gai_error, &mmdb_error); if (mmdb_error != MMDB_SUCCESS) { pr_trace_msg(trace_channel, 2, "error looking up '%s' in GeoIPTable '%s': %s", ip_addr, mmdb->filename, MMDB_strerror(mmdb_error)); continue; } if (!lookup.found_entry) { pr_trace_msg(trace_channel, 2, "no entry found for '%s' in GeoIPTable '%s'", ip_addr, mmdb->filename); continue; } /* XXX This cries out to be done in a table-driven fashion. */ lookup_path[0] = "country"; lookup_path[1] = "iso_code"; lookup_path[2] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_COUNTRY_CODE); if (text != NULL) { geoip_country_code2 = text; } /* "country" already set as first element above; no need to duplicate * it again. */ lookup_path[1] = "names"; lookup_path[2] = "en"; lookup_path[3] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_COUNTRY_NAME); if (text != NULL) { geoip_country_name = text; } lookup_path[0] = "continent"; lookup_path[1] = "code"; lookup_path[2] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_CONTINENT); if (text != NULL) { geoip_continent_code = text; } lookup_path[0] = "subdivisions"; lookup_path[1] = "0"; lookup_path[2] = "iso_code"; lookup_path[3] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_REGION_CODE); if (text != NULL) { geoip_region_code = text; } /* "subdivisions" already set as first element above; no need to duplicate * it again. */ lookup_path[1] = "0"; lookup_path[2] = "names"; lookup_path[3] = "en"; lookup_path[4] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_REGION_NAME); if (text != NULL) { geoip_region_name = text; } lookup_path[0] = "city"; lookup_path[1] = "names"; lookup_path[2] = "en"; lookup_path[3] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_CITY); if (text != NULL) { geoip_city = text; } lookup_path[0] = "postal"; lookup_path[1] = "code"; lookup_path[2] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_POSTAL_CODE); if (text != NULL) { geoip_postal_code = text; } lookup_path[0] = "locations"; lookup_path[1] = "latitude"; lookup_path[2] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_LATITUDE); if (text != NULL) { geoip_latitude = text; } /* "locations" already set as first element above; no need to duplicate * it again. */ lookup_path[1] = "longitude"; lookup_path[2] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_LONGITUDE); if (text != NULL) { geoip_longitude = text; } /* "locations" already set as first element above; no need to duplicate * it again. */ lookup_path[1] = "time_zone"; lookup_path[2] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_TIMEZONE); if (text != NULL) { geoip_longitude = text; } lookup_path[0] = "autonomous_system_number"; lookup_path[1] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_ASN); if (text != NULL) { geoip_asn = text; } lookup_path[0] = "autonomous_system_organization"; lookup_path[1] = NULL; text = get_geoip_data_text(geoip2_pool, &lookup, lookup_path, GEOIP_FILTER_KEY_ORGANIZATION); if (text != NULL) { geoip_org = text; } } } static void get_geoip_info(void) { const char *ip_addr; get_geoip_data(); ip_addr = pr_netaddr_get_ipstr(session.c->remote_addr); if (geoip_country_code2 != NULL) { pr_trace_msg(trace_channel, 8, "%s: 2-Letter country code: %s", ip_addr, geoip_country_code2); } if (geoip_country_name != NULL) { pr_trace_msg(trace_channel, 8, "%s: Country name: %s", ip_addr, geoip_country_name); } if (geoip_region_code != NULL) { pr_trace_msg(trace_channel, 8, "%s: Region code: %s", ip_addr, geoip_region_code); } if (geoip_region_name != NULL) { pr_trace_msg(trace_channel, 8, "%s: Region name: %s", ip_addr, geoip_region_name); } if (geoip_timezone != NULL) { pr_trace_msg(trace_channel, 8, "%s: Timezone: %s", ip_addr, geoip_timezone); } if (geoip_continent_code != NULL) { pr_trace_msg(trace_channel, 8, "%s: Continent code: %s", ip_addr, geoip_continent_code); } if (geoip_org != NULL) { pr_trace_msg(trace_channel, 8, "%s: Organization: %s", ip_addr, geoip_org); } if (geoip_city != NULL) { pr_trace_msg(trace_channel, 8, "%s: City: %s", ip_addr, geoip_city); } if (geoip_postal_code != NULL) { pr_trace_msg(trace_channel, 8, "%s: Postal code: %s", ip_addr, geoip_postal_code); } if (geoip_latitude != NULL) { pr_trace_msg(trace_channel, 8, "%s: Latitude: %s", ip_addr, geoip_latitude); } if (geoip_longitude != NULL) { pr_trace_msg(trace_channel, 8, "%s: Longitude: %s", ip_addr, geoip_longitude); } if (geoip_asn != NULL) { pr_trace_msg(trace_channel, 8, "%s: ASN: %s", ip_addr, geoip_asn); } } static void set_geoip_value(const char *key, const char *value) { int res; res = pr_env_set(session.pool, key, value); if (res < 0) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "error setting %s environment variable: %s", key, strerror(errno)); } res = pr_table_add_dup(session.notes, pstrdup(session.pool, key), (char *) value, 0); if (res < 0) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "error adding %s session note: %s", key, strerror(errno)); } } static void set_geoip_values(void) { if (geoip_country_code2 != NULL) { set_geoip_value("GEOIP_COUNTRY_CODE", geoip_country_code2); } if (geoip_country_name != NULL) { set_geoip_value("GEOIP_COUNTRY_NAME", geoip_country_name); } if (geoip_region_code != NULL) { set_geoip_value("GEOIP_REGION", geoip_region_code); } if (geoip_region_name != NULL) { set_geoip_value("GEOIP_REGION_NAME", geoip_region_name); } if (geoip_continent_code != NULL) { set_geoip_value("GEOIP_CONTINENT_CODE", geoip_continent_code); } if (geoip_org != NULL) { set_geoip_value("GEOIP_ORGANIZATION", geoip_org); } if (geoip_city != NULL) { set_geoip_value("GEOIP_CITY", geoip_city); } if (geoip_postal_code != NULL) { set_geoip_value("GEOIP_POSTAL_CODE", geoip_postal_code); } if (geoip_latitude != NULL) { set_geoip_value("GEOIP_LATITUDE", geoip_latitude); } if (geoip_longitude != NULL) { set_geoip_value("GEOIP_LONGITUDE", geoip_longitude); } if (geoip_asn != NULL) { set_geoip_value("GEOIP_ASN", geoip_asn); } if (geoip_timezone != NULL) { set_geoip_value("GEOIP_TIMEZONE", geoip_timezone); } } /* Configuration handlers */ /* usage: * GeoIPAllowFilter key1 regex1 [key2 regex2 ...] * sql:/... * GeoIPDenyFilter key1 regex1 [key2 regex2 ...] * sql:/... */ MODRET set_geoipfilter(cmd_rec *cmd) { #if PR_USE_REGEX config_rec *c; array_header *deferred_patterns, *filters; CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); if (cmd->argc == 1) { CONF_ERROR(cmd, "wrong number of parameters"); } /* IFF the first parameter starts with "sql:/", then we expect ONLY one * parameter. If not, then we expect an even number of parameters. */ if (strncmp(cmd->argv[1], "sql:/", 5) == 0) { if (cmd->argc > 2) { CONF_ERROR(cmd, "wrong number of parameters"); } } else { if ((cmd->argc-1) % 2 != 0) { CONF_ERROR(cmd, "wrong number of parameters"); } } c = add_config_param(cmd->argv[0], 2, NULL, NULL); filters = make_array(c->pool, 0, sizeof(struct geoip_filter *)); deferred_patterns = make_array(c->pool, 0, sizeof(char *)); if (cmd->argc == 2) { const char *pattern; pattern = cmd->argv[1]; /* Advance past the "sql:/" prefix. */ *((char **) push_array(deferred_patterns)) = pstrdup(c->pool, pattern + 5); } else { register unsigned int i; for (i = 1; i < cmd->argc; i += 2) { const char *filter_name, *pattern = NULL; struct geoip_filter *filter; filter_name = cmd->argv[i]; pattern = cmd->argv[i+1]; filter = make_filter(c->pool, filter_name, pattern); if (filter == NULL) { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use '", filter_name, " ", pattern, "' as filter: ", strerror(errno), NULL)); } *((struct geoip_filter **) push_array(filters)) = filter; } } c->argv[0] = filters; c->argv[1] = deferred_patterns; return PR_HANDLED(cmd); #else /* no regular expression support at the moment */ CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "The ", cmd->argv[0], " directive cannot be used on this system, as you do not have POSIX " "compliant regex support", NULL)); #endif } /* usage: GeoIPEngine on|off */ MODRET set_geoipengine(cmd_rec *cmd) { int engine = -1; config_rec *c = NULL; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); engine = get_boolean(cmd, 1); if (engine == -1) { CONF_ERROR(cmd, "expected Boolean parameter"); } c = add_config_param(cmd->argv[0], 1, NULL); c->argv[0] = pcalloc(c->pool, sizeof(int)); *((int *) c->argv[0]) = engine; return PR_HANDLED(cmd); } /* usage: GeoIPLog path|"none" */ MODRET set_geoiplog(cmd_rec *cmd) { CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); (void) add_config_param_str(cmd->argv[0], 1, cmd->argv[1]); return PR_HANDLED(cmd); } /* usage: GeoIPPolicy "allow,deny"|"deny,allow" */ MODRET set_geoippolicy(cmd_rec *cmd) { geoip_policy_e policy; config_rec *c; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); if (strcasecmp(cmd->argv[1], "allow,deny") == 0) { policy = GEOIP_POLICY_ALLOW_DENY; } else if (strcasecmp(cmd->argv[1], "deny,allow") == 0) { policy = GEOIP_POLICY_DENY_ALLOW; } else { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": '", cmd->argv[1], "' is not one of the approved GeoIPPolicy settings", NULL)); } c = add_config_param(cmd->argv[0], 1, NULL); c->argv[0] = pcalloc(c->pool, sizeof(geoip_policy_e)); *((geoip_policy_e *) c->argv[0]) = policy; return PR_HANDLED(cmd); } /* usage: GeoIPTable path [flags] */ MODRET set_geoiptable(cmd_rec *cmd) { config_rec *c; uint32_t flags = 0; char *path; CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); if (cmd->argc < 2) { CONF_ERROR(cmd, "wrong number of parameters"); } path = cmd->argv[1]; if (cmd->argc > 2) { register unsigned int i; for (i = 2; i < cmd->argc; i++) { /* Most of these are ignored, for backward compatibility with the * mod_geoip flags. */ if (strcasecmp(cmd->argv[i], "Standard") == 0) { /* Ignored. */ } else if (strcasecmp(cmd->argv[i], "MemoryCache") == 0) { /* Ignored. */ } else if (strcasecmp(cmd->argv[i], "MMapCache") == 0) { flags |= MMDB_MODE_MMAP; } else if (strcasecmp(cmd->argv[i], "IndexCache") == 0) { /* Ignored. */ } else if (strcasecmp(cmd->argv[i], "CheckCache") == 0) { /* Ignored. */ } else if (strcasecmp(cmd->argv[i], "UTF8") == 0) { /* Ignored. */ } else { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unknown GeoIPTable flag '", cmd->argv[i], "'", NULL)); } } } c = add_config_param(cmd->argv[0], 2, NULL, NULL); c->argv[0] = pstrdup(c->pool, path); c->argv[1] = palloc(c->pool, sizeof(uint32_t)); *((uint32_t *) c->argv[1]) = flags; return PR_HANDLED(cmd); } /* Command handlers */ MODRET geoip2_post_pass(cmd_rec *cmd) { int res; if (geoip2_engine == FALSE) { return PR_DECLINED(cmd); } /* Scan for any deferred GeoIP filters and resolve them. */ resolve_deferred_filters(cmd->tmp_pool); /* Modules such as mod_ifsession may have added new filters; check the * filters again. */ res = check_geoip_filters(geoip_policy); if (res < 0) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "connection from %s denied due to GeoIP filter/policy", pr_netaddr_get_ipstr(session.c->remote_addr)); pr_log_pri(PR_LOG_NOTICE, MOD_GEOIP2_VERSION ": Connection denied to %s due to GeoIP filter/policy", pr_netaddr_get_ipstr(session.c->remote_addr)); pr_event_generate("mod_geoip.connection-denied", NULL); pr_session_disconnect(&geoip2_module, PR_SESS_DISCONNECT_CONFIG_ACL, "GeoIP Filters"); } return PR_DECLINED(cmd); } /* Event handlers */ #if defined(PR_SHARED_MODULE) static void geoip2_mod_unload_ev(const void *event_data, void *user_data) { if (strcmp("mod_geoip2.c", (const char *) event_data) == 0) { remove_geoip_tables(); destroy_pool(geoip2_pool); /* Unregister ourselves from all events. */ pr_event_unregister(&geoip2_module, NULL, NULL); } } #endif /* PR_SHARED_MODULE */ static void geoip2_postparse_ev(const void *event_data, void *user_data) { pr_log_debug(DEBUG8, MOD_GEOIP2_VERSION ": loading static GeoIP tables"); get_geoip_tables(); } static void geoip2_restart_ev(const void *event_data, void *user_data) { remove_geoip_tables(); destroy_pool(geoip2_pool); geoip2_pool = make_sub_pool(permanent_pool); pr_pool_tag(geoip2_pool, MOD_GEOIP2_VERSION); geoip2_mmdbs = make_array(geoip2_pool, 0, sizeof(MMDB_s *)); } /* Initialization functions */ static int geoip2_init(void) { /* Make sure that mod_geoip is NOT loaded. If it is, error out. There * can be only one. (Make sure the docs note this, too.) */ if (pr_module_exists("mod_geoip.c") == TRUE) { pr_log_pri(PR_LOG_NOTICE, MOD_GEOIP2_VERSION ": mod_geoip and mod_geoip2 cannot be used at the same time"); errno = EPERM; return -1; } geoip2_pool = make_sub_pool(permanent_pool); pr_pool_tag(geoip2_pool, MOD_GEOIP2_VERSION); geoip2_mmdbs = make_array(geoip2_pool, 0, sizeof(MMDB_s *)); #if defined(PR_SHARED_MODULE) pr_event_register(&geoip2_module, "core.module-unload", geoip2_mod_unload_ev, NULL); #endif /* PR_SHARED_MODULE */ pr_event_register(&geoip2_module, "core.postparse", geoip2_postparse_ev, NULL); pr_event_register(&geoip2_module, "core.restart", geoip2_restart_ev, NULL); pr_log_debug(DEBUG2, MOD_GEOIP2_VERSION ": using libmaxmindb-%s", MMDB_lib_version()); return 0; } static int geoip2_sess_init(void) { config_rec *c; int res; pool *tmp_pool; c = find_config(main_server->conf, CONF_PARAM, "GeoIPEngine", FALSE); if (c != NULL) { geoip2_engine = *((int *) c->argv[0]); } if (geoip2_engine == FALSE) { return 0; } c = find_config(main_server->conf, CONF_PARAM, "GeoIPLog", FALSE); if (c != NULL) { char *path; path = c->argv[0]; if (strcasecmp(path, "none") != 0) { int xerrno; pr_signals_block(); PRIVS_ROOT res = pr_log_openfile(path, &geoip2_logfd, PR_LOG_SYSTEM_MODE); xerrno = errno; PRIVS_RELINQUISH pr_signals_unblock(); if (res < 0) { if (res == -1) { pr_log_pri(PR_LOG_NOTICE, MOD_GEOIP2_VERSION ": notice: unable to open GeoIPLog '%s': %s", path, strerror(xerrno)); } else if (res == PR_LOG_WRITABLE_DIR) { pr_log_pri(PR_LOG_WARNING, MOD_GEOIP2_VERSION ": notice: unable to open GeoIPLog '%s': parent directory is " "world-writable", path); } else if (res == PR_LOG_SYMLINK) { pr_log_pri(PR_LOG_WARNING, MOD_GEOIP2_VERSION ": notice: unable to open GeoIPLog '%s': cannot log to a symlink", path); } } } } tmp_pool = make_sub_pool(geoip2_pool); pr_pool_tag(tmp_pool, "GeoIP Session Pool"); if (geoip2_mmdbs->nelts == 0) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "no usable GeoIPTable files found, skipping GeoIP lookups"); (void) close(geoip2_logfd); destroy_pool(tmp_pool); return 0; } get_geoip_info(); c = find_config(main_server->conf, CONF_PARAM, "GeoIPPolicy", FALSE); if (c != NULL) { geoip_policy = *((geoip_policy_e *) c->argv[0]); } switch (geoip_policy) { case GEOIP_POLICY_ALLOW_DENY: pr_trace_msg(trace_channel, 8, "using policy of allowing connections unless rejected by " "GeoIPDenyFilters"); break; case GEOIP_POLICY_DENY_ALLOW: pr_trace_msg(trace_channel, 8, "using policy of rejecting connections unless allowed by " "GeoIPAllowFilters"); break; } res = check_geoip_filters(geoip_policy); if (res < 0) { (void) pr_log_writefile(geoip2_logfd, MOD_GEOIP2_VERSION, "connection from %s denied due to GeoIP filter/policy", pr_netaddr_get_ipstr(session.c->remote_addr)); pr_log_pri(PR_LOG_NOTICE, MOD_GEOIP2_VERSION ": Connection denied to %s due to GeoIP filter/policy", pr_netaddr_get_ipstr(session.c->remote_addr)); pr_event_generate("mod_geoip.connection-denied", NULL); /* XXX send_geoip_mesg(tmp_pool, mesg) */ destroy_pool(tmp_pool); errno = EACCES; return -1; } set_geoip_values(); destroy_pool(tmp_pool); return 0; } /* Module API tables */ static conftable geoip2_conftab[] = { { "GeoIPAllowFilter", set_geoipfilter, NULL }, { "GeoIPDenyFilter", set_geoipfilter, NULL }, { "GeoIPEngine", set_geoipengine, NULL }, { "GeoIPLog", set_geoiplog, NULL }, { "GeoIPPolicy", set_geoippolicy, NULL }, { "GeoIPTable", set_geoiptable, NULL }, { NULL } }; static cmdtable geoip2_cmdtab[] = { { POST_CMD, C_PASS, G_NONE, geoip2_post_pass, FALSE, FALSE }, { 0, NULL }, }; module geoip2_module = { NULL, NULL, /* Module API version 2.0 */ 0x20, /* Module name */ "geoip2", /* Module configuration handler table */ geoip2_conftab, /* Module command handler table */ geoip2_cmdtab, /* Module authentication handler table */ NULL, /* Module initialization function */ geoip2_init, /* Session initialization function */ geoip2_sess_init, /* Module version */ MOD_GEOIP2_VERSION }; proftpd-mod_geoip2/mod_geoip2.html0000644000175000001440000004010713720460146016511 0ustar hilleusers ProFTPD module mod_geoip2

ProFTPD module mod_geoip2



The mod_geoip2 module uses the GeoIP library from MaxMind to look up various geographic information for a connecting client:

  https://github.com/maxmind/libmaxminddb
This information can be used to set access controls for the server.

This module is contained in the mod_geoip2.c file for ProFTPD 1.3.x, and is not compiled by default. Installation instructions are discussed here.

The most current version of mod_geoip2 can be found at:

  https://github.com/Castaglia/proftpd-mod_geoip2.git

This product includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com/.

Author

Please contact TJ Saunders <tj at castaglia.org> with any questions, concerns, or suggestions regarding this module.

Directives


GeoIPAllowFilter

Syntax: GeoIPAllowFilter filter1 pattern1 [filter2 pattern2 ...]
Default: none
Context: server config, <VirtualHost>, <Global>
Module: mod_geoip2
Compatibility: 1.3.3rc1 and later

The GeoIPAllowFilter directive is used to configure ACLs based on the geographic data provided by the GeoIP library.

Multiple GeoIPAllowFilter directives in the configuration are supported; if any filter matches the connecting client, the connection will be allowed.

The filter parameter specifies the GeoIP value to which to apply the configured pattern for matching. The possible filter values are:

The pattern parameter is case-insensitive regular expression that will be applied to the specified filter value, if available.

Note that as of proftpd-1.3.6rc3 and later, the GeoIPAllowFilter directive can also take a single parameter which specifies a SQL query (via mod_sql's SQLNamedQuery), which will be used to retrieve the filter and pattern values to use.

Examples:

  # Allow clients from Ireland
  GeoIPAllowFilter CountryCode IE

  # Reject clients connecting from North America or South America
  GeoIPDenyFilter Continent (NA|SA)
The following more complex configuration demonstrates what can be done using SQL querires:
  <IfModule mod_sql.c>
    ...
    SQLNamedQuery get-geo-allowed SELECT "filter_name, pattern FROM allowed_user_geo WHERE user_name = '%u'"
    SQLNamedQuery get-geo-denied SELECT "filter_name, pattern FROM denied_user_geo WHERE user_name = '%u'"
  </IfModule>

  <IfModule mod_geoip2.c>
    GeoIPEngine on

    GeoIPAllowFilter sql:/get-geo-allowed
    GeoIPDenyFilter sql:/get-geo-denied
  </IfModule>
The above assumes SQL tables with schema similar to the following (expressed using SQLite syntax):
  CREATE TABLE allowed_user_geo (
    user_name TEXT,
    filter_name TEXT,
    pattern TEXT
  );

  CREATE TABLE denied_user_geo (
    user_name TEXT,
    filter_name TEXT,
    pattern TEXT
  );

  # Note that we create separate indexes, to allow for multiple rows per user
  CREATE INDEX allowed_user_geo_name_idx ON allowed_user_geo (user_name);
  CREATE INDEX denied_user_geo_name_idx ON denied_user_geo (user_name);


GeoIPDenyFilter

Syntax: GeoIPDenyFilter filter1 pattern1 [filter2 pattern2 ...]
Default: none
Context: server config, <VirtualHost>, <Global>
Module: mod_geoip2
Compatibility: 1.3.3rc1 and later

The GeoIPDenyFilter directive is used to configure ACLs based on the geographic data provided by the GeoIP library.

Multiple GeoIPDenyFilter directives in the configuration are supported; if any filter matches the connecting client, the connection will be rejected.

Note that as of proftpd-1.3.6rc3 and later, the GeoIPDenyFilter directive can also take a single parameter which specifies a SQL query (via mod_sql's SQLNamedQuery), which will be used to retrieve the filter and pattern values to use.

See GeoIPAllowFilter for a description of the directive syntax and parameters.


GeoIPEngine

Syntax: GeoIPEngine on|off
Default: off
Context: server config, <VirtualHost>, <Global>
Module: mod_geoip2
Compatibility: 1.3.3rc1 and later

The GeoIPEngine directive enables or disables the module's lookup of geographic information for a connecting client, and subsequent enforcement of any configured ACLs.


GeoIPLog

Syntax: GeoIPLog file|"none"
Default: None
Context: server config, <VirtualHost>, <Global>
Module: mod_geoip2
Compatibility: 1.3.3rc1 and later

The GeoIPLog directive is used to specify a log file for mod_geoip2's reporting on a per-server basis. The file parameter given must be the full path to the file to use for logging.

Note that this path must not be to a world-writable directory and, unless AllowLogSymlinks is explicitly set to on (generally a bad idea), the path must not be a symbolic link.


GeoIPPolicy

Syntax: GeoIPPolicy "allow,deny"|"deny,allow"
Default: GeoIPPolicy allow,deny
Context: server config, <VirtualHost>, <Global>
Module: mod_geoip2
Compatibility: 1.3.5rc1 and later

The GeoIPPolicy directive determines whether the mod_geoip2 module will allow a connection by default or not.

If GeoIPPolicy is configured using "allow,deny" (which is the default setting), then the mod_geoip2 module will allow the connection, unless the connecting client is rejected by any GeoIPDenyFilter rules.

If GeoIPPolicy is configured using "deny,allow", then the mod_geoip2 module will reject any connection, unless the connecting client matches any configured GeoIPAllowFilter rules.


GeoIPTable

Syntax: GeoIPTable path [flags]
Default: None
Context: server config, <VirtualHost>, <Global>
Module: mod_geoip2
Compatibility: 1.3.3rc1 and later

The GeoIPTable directive is used to a GeoIP database file for use by the GeoIP library. The path parameter given must be the full path to the database file.

If no GeoIPTable directive is configured, then mod_geoip2 will not perform any geographical lookups.

Multiple GeoIPTable directives can be used to configure multiple different GeoIP database files for use at the same time.

Note that the flags parameter is currently supported for backward compatibility with mod_geoip, but is currently ignored.

Examples:

  GeoIPTable /path/to/GeoLite2-City.mmdb
  GeoIPTable /path/to/GeoLite2-Country.mmdb
  GeoIPTable /path/to/GeoLite2-ASN.mmdb


Installation

The mod_geoip2 module requires that the MaxMindDB library be installed. For including mod_geoip2 as a statically linked module:
  $ ./configure --with-modules=mod_geoip2
Alternatively, mod_geoip2 could be built as a DSO module:
  $ ./configure --with-shared=mod_geoip2
Then follow the usual steps:
  $ make
  $ make install
You may need to specify the location of the MaxMindDB header and library files in your configure command, e.g.:
  $ ./configure --with-modules=mod_geoip2 \
    --with-includes=/usr/local/maxminddb/include \
    --with-libraries=/usr/local/maxminddb/lib

Alternatively, if your proftpd was compiled with DSO support, you can use the prxs tool to build mod_geoip2 as a shared module:

  $ prxs -c -i -d mod_geoip2.c


Usage

mod_geoip Interactions
The mod_geoip2 module is meant to be, roughly, a drop-in replacement for the legacy mod_geoip module; it implements the same configuration directive, provides the same environment variables, etc. Thus mod_geoip2 will fail to start if it detects that the mod_geoip module is also being used. You must use only one of either mod_geoip or mod_geoip2 in ProFTPD, but not both.

Access Controls
If any GeoIPAllowFilter or GeoIPDenyFilter directives are configured, the mod_geoip2 module applies them against the geographic information retrieved from the GeoIP library. First any GeoIPAllowFilters are checked. If any of these filters matches the connecting client's information, the connection is allowed. Next, any GeoIPDenyFilters are checked. If any of these filters matches the connecting client's information, the connection is closed. Otherwise, the connection is allowed.

This means that if you wanted to reject connections from the US except for connections from California, you might use something like this:

  # Deny all connections from the US
  GeoIPDenyFilter CountryCode US

  # But allow connections from California
  GeoIPAllowFilter RegionCode CA

Environment Variables
The mod_geoip2 module will set the following environment variables whenever a client connects, assuming that the appropriate GeoIP tables have been configured and the values are known for the connecting client:

These values are also available in the session.notes table, under keys of the names above.

Example Configuration

  <IfModule mod_geoip2.c>
    GeoIPEngine on
    GeoIPLog /path/to/ftpd/geoip.log

    GeoIPTable /path/to/GeoLite2-City.mmdb

    # Add your GeoIPAllowFilter/GeoIPDenyFilter rules here
  </IfModule>

Logging
The mod_geoip2 module supports different forms of logging. The main module logging is done via the GeoIPLog directive. For debugging purposes, the module also uses trace logging, via the module-specific log channels:

Thus for trace logging, to aid in debugging, you would use the following in your proftpd.conf:
  TraceLog /path/to/ftpd/trace.log
  Trace geoip2:20
The geographic information retrieved from the GeoIP library for the connecting client is logged using this "geoip2" trace log channel. This trace logging can generate large files; it is intended for debugging use only, and should be removed from any production configuration.

Suggested Future Features
The following lists the features I hope to add to mod_geoip2, according to need, demand, inclination, and time:

Frequently Asked Questions

Question: What is the difference between mod_geoip2 and mod_geoip? Answer: The mod_geoip module uses the "legacy" GeoIP API from MaxMind, which was discontinued in early 2019. This mod_geoip2 module uses the newer MaxMindDB library from MaxMind.

Question: How I can whitelist specific clients from mod_geoip2's checking?
Answer: You should be able to easily do this using
classes and the mod_ifsession module. For example:

  <Class geoip-whitelist>
    From 1.2.3.4
  </Class>

  <IfModule mod_geoip2.c>
    # Add the normal mod_geoip2 directives here except GeoIPEngine
  </IfModule>

  <IfClass geoip-whitelist>
    # Disable mod_geoip2 for the whitelisted clients
    GeoIPEngine off
  </IfClass>

  <IfClass !geoip-whitelist>
    # Enable mod_geoip2 for all non-whitelisted clients
    GeoIPEngine on
  </IfClass>

Question: How I can require that a connection match multiple rules, e.g. both a RegionCode and a CountryCode?
Answer: In a given GeoIPAllowFilter or GeoIPDenyFilter, you can configure a list of filters/patterns. And all of these filters must be matched, in order for that GeoIPAllowFilter or GeoIPDenyFilter to be matched. Thus you can use:

  # Deny all connections, unless they are explicitly allowed
  GeoIPPolicy deny,allow

  # Allow only connections from TX, US
  GeoIPAllowFilter RegionCode TX CountryCode US

Question: Does mod_geoip2 support IPv6?
Answer: Yes.


© Copyright 2019 TJ Saunders
All Rights Reserved

proftpd-mod_geoip2/.travis.yml0000644000175000001440000000116713720460146015713 0ustar hilleuserslanguage: c compiler: - gcc - clang install: - sudo apt-get update -qq # for MaxMinDB support - sudo apt-get install -y libmaxminddb-dev # for test code coverage - sudo apt-get install -y lcov - gem install coveralls-lcov before_script: - cd ${TRAVIS_BUILD_DIR} - lcov --directory . --zerocounters script: - git clone --depth 50 https://github.com/proftpd/proftpd.git - cp mod_geoip2.c proftpd/contrib/ - cd proftpd - ./configure --enable-dso --with-shared=mod_geoip2 - make - make clean - ./configure --with-modules=mod_geoip2 - make # Run `tidy -e -q mod_geoip2.html` for doc validation proftpd-mod_geoip2/.gitignore0000644000175000001440000000012513720460230015555 0ustar hilleusersMakefile config.status autom4te.cache .libs *.sw? *.la *.lo *.log *Tests*.log *.o *~ proftpd-mod_geoip2/.gitattributes0000644000175000001440000000006213720460146016466 0ustar hilleusers*.pl linguist-language=C *.pm linguist-language=C proftpd-mod_geoip2/README.md0000644000175000001440000000126213720460146015055 0ustar hilleusersproftpd-mod_geoip2 ================== Status ------ [![Build Status](https://travis-ci.org/Castaglia/proftpd-mod_geoip2.svg?branch=master)](https://travis-ci.org/Castaglia/proftpd-mod_geoip2) [![License](https://img.shields.io/badge/license-GPL-brightgreen.svg)](https://img.shields.io/badge/license-GPL-brightgreen.svg) Synopsis -------- The `mod_geoip2` module for ProFTPD uses the MaxMind GeoIP library to look up geographic information on connecting clients, and to provide ACLs based on that geographic information. See the [mod_geoip2.html](https://htmlpreview.github.io/?https://github.com/Castaglia/proftpd-mod_geoip2/blob/master/mod_geoip2.html) documentation for more details.