From db9bb2778d3685a9361394c694c649316dc867e4 Mon Sep 17 00:00:00 2001 From: Jan Engelhardt Date: Tue, 29 Sep 2009 14:00:59 +0200 Subject: [PATCH] pknock: import pknock trunk@463 --- extensions/Kbuild | 1 + extensions/Mbuild | 1 + extensions/libxt_pknock.c | 351 +++++++++++ extensions/xt_pknock.c | 1241 +++++++++++++++++++++++++++++++++++++ extensions/xt_pknock.h | 94 +++ mconfig | 1 + 6 files changed, 1689 insertions(+) create mode 100644 extensions/libxt_pknock.c create mode 100644 extensions/xt_pknock.c create mode 100644 extensions/xt_pknock.h diff --git a/extensions/Kbuild b/extensions/Kbuild index ab3dfdd..b86a1dc 100644 --- a/extensions/Kbuild +++ b/extensions/Kbuild @@ -26,6 +26,7 @@ obj-${build_ipset} += ipset/ obj-${build_ipv4options} += xt_ipv4options.o obj-${build_length2} += xt_length2.o obj-${build_lscan} += xt_lscan.o +obj-${build_pknock} += xt_pknock.o obj-${build_psd} += xt_psd.o obj-${build_quota2} += xt_quota2.o diff --git a/extensions/Mbuild b/extensions/Mbuild index 65ae18f..bc0e1d0 100644 --- a/extensions/Mbuild +++ b/extensions/Mbuild @@ -21,5 +21,6 @@ obj-${build_ipset} += ipset/ obj-${build_ipv4options} += libxt_ipv4options.so obj-${build_length2} += libxt_length2.so obj-${build_lscan} += libxt_lscan.so +obj-${build_pknock} += libxt_pknock.so obj-${build_psd} += libxt_psd.so obj-${build_quota2} += libxt_quota2.so diff --git a/extensions/libxt_pknock.c b/extensions/libxt_pknock.c new file mode 100644 index 0000000..581e5bb --- /dev/null +++ b/extensions/libxt_pknock.c @@ -0,0 +1,351 @@ +/* + * Shared library add-on to iptables to add Port Knocking and SPA matching + * support. + * + * (C) 2006-2009 J. Federico Hernandez + * (C) 2006 Luis Floreani + * + * $Id$ + * + * This program is released under the terms of GNU GPL version 2. + */ +#include +#include +#include +#include + +#include +#include +#include +//#include +#include "xt_pknock.h" + +static const struct option pknock_opts[] = { + /* .name, .has_arg, .flag, .val */ + { "knockports", 1, 0, 'k' }, + { "t", 1, 0, 't' }, + { "time", 1, 0, 't' }, + { "name", 1, 0, 'n' }, + { "opensecret", 1, 0, 'a' }, + { "closesecret",1, 0, 'z' }, + { "strict", 0, 0, 'x' }, + { "checkip", 0, 0, 'c' }, + { "chkip", 0, 0, 'c' }, + { .name = NULL } +}; + +/* Function which prints out usage message. */ +static void pknock_help(void) +{ + printf("pknock match options:\n" + " --knockports port[,port,port,...] " + "Matches destination port(s).\n" + " --time seconds\n" + " --t ... " + "Time between port match.\n" + " --secure " + "hmac must be in the packets.\n" + " --strict " + "Knocks sequence must be exact.\n" + " --name rule_name " + "Rule name.\n" + " --checkip " + "Matches if the source ip is in the list.\n" + " --chkip\n"); +} + +static unsigned int +parse_ports(const char *portstring, uint16_t *ports, const char *proto) +{ + char *buffer, *cp, *next; + unsigned int i; + + buffer = strdup(portstring); + if (!buffer) xtables_error(OTHER_PROBLEM, "strdup failed"); + + for (cp=buffer, i=0; cp && idata; + + switch (c) { + case 'k': /* --knockports */ + if (*flags & IPT_PKNOCK_KNOCKPORT) + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot use --knockports twice.\n"); + + xtables_check_inverse(argv[optind-1], &invert, &optind, 0); + proto = check_proto(pnum, invflags); + + info->ports_count = parse_ports(optarg, info->port, proto); + info->option |= IPT_PKNOCK_KNOCKPORT; + *flags |= IPT_PKNOCK_KNOCKPORT; +#if DEBUG + printf("ports_count: %d\n", info->ports_count); +#endif + break; + + case 't': /* --time */ + if (*flags & IPT_PKNOCK_TIME) + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot use --time twice.\n"); + + xtables_check_inverse(argv[optind-1], &invert, &optind, 0); + + info->max_time = atoi(optarg); + info->option |= IPT_PKNOCK_TIME; + *flags |= IPT_PKNOCK_TIME; + break; + + case 'n': /* --name */ + if (*flags & IPT_PKNOCK_NAME) + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot use --name twice.\n"); + + xtables_check_inverse(argv[optind-1], &invert, &optind, 0); + + memset(info->rule_name, 0, IPT_PKNOCK_MAX_BUF_LEN + 1); + strncpy(info->rule_name, optarg, IPT_PKNOCK_MAX_BUF_LEN); + + info->rule_name_len = strlen(info->rule_name); + info->option |= IPT_PKNOCK_NAME; + *flags |= IPT_PKNOCK_NAME; +#if DEBUG + printf("info->rule_name: %s\n", info->rule_name); +#endif + break; + + case 'a': /* --opensecret */ + if (*flags & IPT_PKNOCK_OPENSECRET) + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot use --opensecret twice.\n"); + + xtables_check_inverse(argv[optind-1], &invert, &optind, 0); + + memset(info->open_secret, 0, IPT_PKNOCK_MAX_PASSWD_LEN + 1); + strncpy(info->open_secret, optarg, IPT_PKNOCK_MAX_PASSWD_LEN); + + info->open_secret_len = strlen(info->open_secret); + info->option |= IPT_PKNOCK_OPENSECRET; + *flags |= IPT_PKNOCK_OPENSECRET; + break; + + case 'z': /* --closesecret */ + if (*flags & IPT_PKNOCK_CLOSESECRET) + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot use --closesecret twice.\n"); + + xtables_check_inverse(argv[optind-1], &invert, &optind, 0); + + memset(info->close_secret, 0, IPT_PKNOCK_MAX_PASSWD_LEN + 1); + strncpy(info->close_secret, optarg, IPT_PKNOCK_MAX_PASSWD_LEN); + + info->close_secret_len = strlen(info->close_secret); + info->option |= IPT_PKNOCK_CLOSESECRET; + *flags |= IPT_PKNOCK_CLOSESECRET; + break; + + case 'c': /* --checkip */ + if (*flags & IPT_PKNOCK_CHECKIP) + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot use --checkip twice.\n"); + + xtables_check_inverse(argv[optind-1], &invert, &optind, 0); + + info->option |= IPT_PKNOCK_CHECKIP; + *flags |= IPT_PKNOCK_CHECKIP; + break; + + case 'x': /* --strict */ + if (*flags & IPT_PKNOCK_STRICT) + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot use --strict twice.\n"); + + xtables_check_inverse(argv[optind-1], &invert, &optind, 0); + + info->option |= IPT_PKNOCK_STRICT; + *flags |= IPT_PKNOCK_STRICT; + break; + + default: + return 0; + } + + if (invert) + xtables_error(PARAMETER_PROBLEM, PKNOCK "does not support invert."); + + return 1; +} + +static int pknock_parse(int c, char **argv, int invert, unsigned int *flags, + const void *e, struct xt_entry_match **match) +{ + const struct ipt_entry *entry = e; + return __pknock_parse(c, argv, invert, flags, match, + entry->ip.proto, entry->ip.invflags); +} + +/* Final check. */ +static void pknock_check(unsigned int flags) +{ + if (!flags) + xtables_error(PARAMETER_PROBLEM, PKNOCK "expection an option.\n"); + + if (!(flags & IPT_PKNOCK_NAME)) + xtables_error(PARAMETER_PROBLEM, PKNOCK + "--name option is required.\n"); + + if (flags & IPT_PKNOCK_KNOCKPORT) { + if (flags & IPT_PKNOCK_CHECKIP) { + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot specify --knockports with --checkip.\n"); + } + if ((flags & IPT_PKNOCK_OPENSECRET) + && !(flags & IPT_PKNOCK_CLOSESECRET)) + { + xtables_error(PARAMETER_PROBLEM, PKNOCK + "--opensecret must go with --closesecret.\n"); + } + if ((flags & IPT_PKNOCK_CLOSESECRET) + && !(flags & IPT_PKNOCK_OPENSECRET)) + { + xtables_error(PARAMETER_PROBLEM, PKNOCK + "--closesecret must go with --opensecret.\n"); + } + } + + if (flags & IPT_PKNOCK_CHECKIP) { + if (flags & IPT_PKNOCK_KNOCKPORT) { + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot specify --checkip with --knockports.\n"); + } + if ((flags & IPT_PKNOCK_OPENSECRET) + || (flags & IPT_PKNOCK_CLOSESECRET)) + { + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot specify --opensecret and" + " --closesecret with --checkip.\n"); + } + if (flags & IPT_PKNOCK_TIME) { + xtables_error(PARAMETER_PROBLEM, PKNOCK + "cannot specify --time with --checkip.\n"); + } + } +} + +/* Prints out the matchinfo. */ +static void pknock_print(const void *ip, + const struct xt_entry_match *match, int numeric) +{ + const struct ipt_pknock *info; + int i; + + info = (const struct ipt_pknock *)match->data; + + printf("pknock "); + if (info->option & IPT_PKNOCK_KNOCKPORT) { + printf("knockports "); + for (i=0; iports_count; i++) + printf("%s%d", i ? "," : "", info->port[i]); + printf(" "); + } + if (info->option & IPT_PKNOCK_TIME) + printf("time %ld ", info->max_time); + if (info->option & IPT_PKNOCK_NAME) + printf("name %s ", info->rule_name); + if (info->option & IPT_PKNOCK_OPENSECRET) + printf("opensecret "); + if (info->option & IPT_PKNOCK_CLOSESECRET) + printf("closesecret "); +} + +/* Saves the union ipt_matchinfo in parsable form to stdout. */ +static void pknock_save(const void *ip, const struct xt_entry_match *match) +{ + int i; + const struct ipt_pknock *info = (const struct ipt_pknock *)match->data; + + if (info->option & IPT_PKNOCK_KNOCKPORT) { + printf("--knockports "); + for (i=0; iports_count; i++) + printf("%s%d", i ? "," : "", info->port[i]); + printf(" "); + } + if (info->option & IPT_PKNOCK_TIME) + printf("--time %ld ", info->max_time); + if (info->option & IPT_PKNOCK_NAME) + printf("--name %s ", info->rule_name); + if (info->option & IPT_PKNOCK_OPENSECRET) + printf("--opensecret "); + if (info->option & IPT_PKNOCK_CLOSESECRET) + printf("--closesecret "); + if (info->option & IPT_PKNOCK_STRICT) + printf("--strict "); + if (info->option & IPT_PKNOCK_CHECKIP) + printf("--checkip "); +} + +static struct xtables_match pknock_match = { + .name = "pknock", + .version = XTABLES_VERSION, + .family = AF_INET, + .size = XT_ALIGN(sizeof (struct ipt_pknock)), + .userspacesize = XT_ALIGN(sizeof (struct ipt_pknock)), + .help = pknock_help, + .parse = pknock_parse, + .final_check = pknock_check, + .print = pknock_print, + .save = pknock_save, + .extra_opts = pknock_opts +}; + +void _init(void) +{ + xtables_register_match(&pknock_match); +} diff --git a/extensions/xt_pknock.c b/extensions/xt_pknock.c new file mode 100644 index 0000000..da4c170 --- /dev/null +++ b/extensions/xt_pknock.c @@ -0,0 +1,1241 @@ +/* + * Kernel module to implement Port Knocking and SPA matching support. + * + * (C) 2006-2009 J. Federico Hernandez Scarso + * (C) 2006 Luis A. Floreani + * + * $Id$ + * + * This program is released under the terms of GNU GPL version 2. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +//#include +#include "xt_pknock.h" + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("J. Federico Hernandez Scarso, Luis A. Floreani"); +MODULE_DESCRIPTION("netfilter match for Port Knocking and SPA"); + +enum { + GC_EXPIRATION_TIME = 65000, /* in msecs */ + DEFAULT_RULE_HASH_SIZE = 8, + DEFAULT_PEER_HASH_SIZE = 16, +}; + +#define hashtable_for_each_safe(pos, n, head, size, i) \ + for ((i) = 0; (i) < (size); (i)++) \ + list_for_each_safe((pos), (n), (&head[(i)])) + +#if DEBUG + #define DEBUGP(msg, peer) printk(KERN_INFO PKNOCK \ + "(S) peer: %u.%u.%u.%u - %s.\n", \ + NIPQUAD((peer)->ip), msg) + #define duprintf(format, args...) printk(format, ## args); +#else + #define DEBUGP(msg, peer) + #define duprintf(format, args...) +#endif + +static uint32_t ipt_pknock_hash_rnd; + +static unsigned int rule_hashsize = DEFAULT_RULE_HASH_SIZE; +static unsigned int peer_hashsize = DEFAULT_PEER_HASH_SIZE; +static unsigned int ipt_pknock_gc_expir_time = GC_EXPIRATION_TIME; +static int nl_multicast_group = -1; + +static struct list_head *rule_hashtable = NULL; +static struct proc_dir_entry *pde = NULL; + +static DEFINE_SPINLOCK(list_lock); + +static struct ipt_pknock_crypto crypto = { + .algo = "hmac(sha256)", + .tfm = NULL, + .size = 0 +}; + +module_param(rule_hashsize, int, S_IRUGO); +module_param(peer_hashsize, int, S_IRUGO); +module_param(ipt_pknock_gc_expir_time, int, S_IRUGO); +module_param(nl_multicast_group, int, S_IRUGO); + +/** + * Calculates a value from 0 to max from a hash of the arguments. + * + * @key + * @len: length + * @initval + * @max + * @return: a 32 bits index + */ +static uint32_t +pknock_hash(const void *key, uint32_t len, uint32_t initval, uint32_t max) +{ + return jhash(key, len, initval) % max; +} + +/** + * @return: the epoch minute + */ +static int +get_epoch_minute(void) +{ + struct timespec t = CURRENT_TIME; + return (int)(t.tv_sec/60); +} + +/** + * Alloc a hashtable with n buckets. + * + * @size + * @return: hashtable + */ +static struct list_head * +alloc_hashtable(int size) +{ + struct list_head *hash = NULL; + unsigned int i; + + if ((hash = kmalloc(sizeof(*hash) * size, GFP_ATOMIC)) == NULL) { + printk(KERN_ERR PKNOCK + "kmalloc() error in alloc_hashtable() function.\n"); + return NULL; + } + + for (i = 0; i < size; i++) + INIT_LIST_HEAD(&hash[i]); + + return hash; +} + +/** + * This function converts the status from integer to string. + * + * @status + * @return: status + */ +static inline const char * +status_itoa(enum status status) +{ + switch (status) { + case ST_INIT: return "INIT"; + case ST_MATCHING: return "MATCHING"; + case ST_ALLOWED: return "ALLOWED"; + default: return "UNKNOWN"; + } +} + +/** + * @s + * @pos + * @return: private value used by the iterator + */ +static void * +pknock_seq_start(struct seq_file *s, loff_t *pos) +{ + struct proc_dir_entry *pde = s->private; + struct ipt_pknock_rule *rule = pde->data; + + spin_lock_bh(&list_lock); + + if (*pos >= peer_hashsize) + return NULL; + + return rule->peer_head + *pos; +} + +/** + * @s + * @v + * @pos + * @return: next value for the iterator + */ +static void * +pknock_seq_next(struct seq_file *s, void *v, loff_t *pos) +{ + struct proc_dir_entry *pde = s->private; + struct ipt_pknock_rule *rule = pde->data; + + (*pos)++; + if (*pos >= peer_hashsize) + return NULL; + + return rule->peer_head + *pos; +} + +/** + * @s + * @v + */ +static void +pknock_seq_stop(struct seq_file *s, void *v) +{ + spin_unlock_bh(&list_lock); +} + +/** + * @s + * @v + * @return: 0 if OK + */ +static int +pknock_seq_show(struct seq_file *s, void *v) +{ + struct list_head *pos = NULL, *n = NULL; + struct peer *peer = NULL; + unsigned long expir_time = 0; + uint32_t ip; + + struct list_head *peer_head = (struct list_head *)v; + + struct proc_dir_entry *pde = s->private; + struct ipt_pknock_rule *rule = pde->data; + + list_for_each_safe(pos, n, peer_head) { + peer = list_entry(pos, struct peer, head); + ip = htonl(peer->ip); + expir_time = time_before(jiffies/HZ, + peer->timestamp + rule->max_time) + ? ((peer->timestamp + rule->max_time)-(jiffies/HZ)) : 0; + + seq_printf(s, "src=%u.%u.%u.%u ", NIPQUAD(ip)); + seq_printf(s, "proto=%s ", (peer->proto == IPPROTO_TCP) ? + "TCP" : "UDP"); + seq_printf(s, "status=%s ", status_itoa(peer->status)); + seq_printf(s, "expir_time=%ld ", expir_time); + seq_printf(s, "next_port_id=%d ", peer->id_port_knocked-1); + seq_printf(s, "\n"); + } + + return 0; +} + +static struct seq_operations pknock_seq_ops = { + .start = pknock_seq_start, + .next = pknock_seq_next, + .stop = pknock_seq_stop, + .show = pknock_seq_show +}; + +/** + * @inode + * @file + */ +static int +pknock_proc_open(struct inode *inode, struct file *file) +{ + int ret = seq_open(file, &pknock_seq_ops); + if (!ret) { + struct seq_file *sf = file->private_data; + sf->private = PDE(inode); + } + return ret; +} + +static struct file_operations pknock_proc_ops = { + .owner = THIS_MODULE, + .open = pknock_proc_open, + .read = seq_read, + .llseek = seq_lseek, + .release = seq_release +}; + +/** + * It updates the rule timer to execute garbage collector. + * + * @rule + */ +static inline void +update_rule_timer(struct ipt_pknock_rule *rule) +{ + if (timer_pending(&rule->timer)) + del_timer(&rule->timer); + + rule->timer.expires = jiffies + msecs_to_jiffies(ipt_pknock_gc_expir_time); + add_timer(&rule->timer); +} + +/** + * @peer + * @max_time + * @return: 1 time exceeded, 0 still valid + */ +static inline bool +is_time_exceeded(struct peer *peer, int max_time) +{ + return peer && time_after(jiffies/HZ, peer->timestamp + max_time); +} + +/** + * @peer + * @return: 1 has logged, 0 otherwise + */ +static inline bool +has_logged_during_this_minute(const struct peer *peer) +{ + return peer && (peer->login_min == get_epoch_minute()); +} + +/** + * Garbage collector. It removes the old entries after timer has expired. + * + * @r: rule + */ +static void +peer_gc(unsigned long r) +{ + int i; + struct ipt_pknock_rule *rule = (struct ipt_pknock_rule *)r; + struct peer *peer = NULL; + struct list_head *pos = NULL, *n = NULL; + + hashtable_for_each_safe(pos, n, rule->peer_head, peer_hashsize, i) { + peer = list_entry(pos, struct peer, head); + + if (!has_logged_during_this_minute(peer) && + is_time_exceeded(peer, rule->max_time)) + { + DEBUGP("DESTROYED", peer); + list_del(pos); + kfree(peer); + } + } +} + +/** + * Compares length and name equality for the rules. + * + * @info + * @rule + * @return: 0 equals, 1 otherwise + */ +static inline int +rulecmp(const struct ipt_pknock *info, const struct ipt_pknock_rule *rule) +{ + if (info->rule_name_len != rule->rule_name_len) + return 1; + if (strncmp(info->rule_name, rule->rule_name, info->rule_name_len) != 0) + return 1; + return 0; +} + +/** + * Search the rule and returns a pointer if it exists. + * + * @info + * @return: rule or NULL + */ +static inline struct ipt_pknock_rule * +search_rule(const struct ipt_pknock *info) +{ + struct ipt_pknock_rule *rule = NULL; + struct list_head *pos = NULL, *n = NULL; + int hash = pknock_hash(info->rule_name, info->rule_name_len, + ipt_pknock_hash_rnd, rule_hashsize); + + list_for_each_safe(pos, n, &rule_hashtable[hash]) { + rule = list_entry(pos, struct ipt_pknock_rule, head); + if (rulecmp(info, rule) == 0) + return rule; + } + return NULL; +} + +/** + * It adds a rule to list only if it doesn't exist. + * + * @info + * @return: 1 success, 0 failure + */ +static bool +add_rule(struct ipt_pknock *info) +{ + struct ipt_pknock_rule *rule = NULL; + struct list_head *pos = NULL, *n = NULL; + int hash = pknock_hash(info->rule_name, info->rule_name_len, + ipt_pknock_hash_rnd, rule_hashsize); + + list_for_each_safe(pos, n, &rule_hashtable[hash]) { + rule = list_entry(pos, struct ipt_pknock_rule, head); + + if (rulecmp(info, rule) == 0) { + rule->ref_count++; +#if DEBUG + if (info->option & IPT_PKNOCK_CHECKIP) { + printk(KERN_DEBUG PKNOCK "add_rule() (AC)" + " rule found: %s - " + "ref_count: %d\n", + rule->rule_name, + rule->ref_count); + } +#endif + return true; + } + } + + if ((rule = kmalloc(sizeof (*rule), GFP_ATOMIC)) == NULL) { + printk(KERN_ERR PKNOCK "kmalloc() error in add_rule().\n"); + return false; + } + + INIT_LIST_HEAD(&rule->head); + + memset(rule->rule_name, 0, IPT_PKNOCK_MAX_BUF_LEN + 1); + strncpy(rule->rule_name, info->rule_name, info->rule_name_len); + rule->rule_name_len = info->rule_name_len; + + rule->ref_count = 1; + rule->max_time = info->max_time; + + if (!(rule->peer_head = alloc_hashtable(peer_hashsize))) { + printk(KERN_ERR PKNOCK "alloc_hashtable() error in add_rule().\n"); + return false; + } + + init_timer(&rule->timer); + rule->timer.function = peer_gc; + rule->timer.data = (unsigned long)rule; + + rule->status_proc = create_proc_entry(info->rule_name, 0, pde); + if (!rule->status_proc) { + printk(KERN_ERR PKNOCK "create_proc_entry() error in add_rule()" + " function.\n"); + kfree(rule); + return false; + } + + rule->status_proc->proc_fops = &pknock_proc_ops; + rule->status_proc->data = rule; + + list_add(&rule->head, &rule_hashtable[hash]); +#if DEBUG + printk(KERN_INFO PKNOCK "(A) rule_name: %s - created.\n", rule->rule_name); +#endif + return true; +} + +/** + * It removes a rule only if it exists. + * + * @info + */ +static void +remove_rule(struct ipt_pknock *info) +{ + struct ipt_pknock_rule *rule = NULL; + struct list_head *pos = NULL, *n = NULL; + struct peer *peer = NULL; + int i; +#if DEBUG + int found = 0; +#endif + int hash = pknock_hash(info->rule_name, info->rule_name_len, + ipt_pknock_hash_rnd, rule_hashsize); + + if (list_empty(&rule_hashtable[hash])) return; + + list_for_each_safe(pos, n, &rule_hashtable[hash]) { + rule = list_entry(pos, struct ipt_pknock_rule, head); + + if (rulecmp(info, rule) == 0) { +#if DEBUG + found = 1; +#endif + rule->ref_count--; + break; + } + } +#if DEBUG + if (!found) { + printk(KERN_INFO PKNOCK "(N) rule not found: %s.\n", info->rule_name); + return; + } +#endif + if (rule && rule->ref_count == 0) { + hashtable_for_each_safe(pos, n, rule->peer_head, peer_hashsize, i) { + peer = list_entry(pos, struct peer, head); + + if (peer != NULL) { + DEBUGP("DELETED", peer); + list_del(pos); + kfree(peer); + } + } + + if (rule->status_proc) + remove_proc_entry(info->rule_name, pde); +#if DEBUG + printk(KERN_INFO PKNOCK "(D) rule deleted: %s.\n", rule->rule_name); +#endif + if (timer_pending(&rule->timer)) + del_timer(&rule->timer); + + list_del(&rule->head); + kfree(rule->peer_head); + kfree(rule); + } +} + +/** + * If peer status exist in the list it returns peer status, if not it returns NULL. + * + * @rule + * @ip + * @return: peer or NULL + */ +static inline struct peer * +get_peer(struct ipt_pknock_rule *rule, uint32_t ip) +{ + struct peer *peer = NULL; + struct list_head *pos = NULL, *n = NULL; + int hash; + + ip = ntohl(ip); + + hash = pknock_hash(&ip, sizeof(ip), ipt_pknock_hash_rnd, peer_hashsize); + + list_for_each_safe(pos, n, &rule->peer_head[hash]) { + peer = list_entry(pos, struct peer, head); + if (peer->ip == ip) return peer; + } + return NULL; +} + +/** + * Reset the knock sequence status of the peer. + * + * @peer + */ +static inline void +reset_knock_status(struct peer *peer) +{ + peer->id_port_knocked = 1; + peer->status = ST_INIT; +} + +/** + * It creates a new peer matching status. + * + * @rule + * @ip + * @proto + * @return: peer or NULL + */ +static inline struct peer * +new_peer(uint32_t ip, uint8_t proto) +{ + struct peer *peer = NULL; + + if ((peer = kmalloc(sizeof (*peer), GFP_ATOMIC)) == NULL) { + printk(KERN_ERR PKNOCK "kmalloc() error in new_peer().\n"); + return NULL; + } + + INIT_LIST_HEAD(&peer->head); + peer->ip = ntohl(ip); + peer->proto = proto; + peer->timestamp = jiffies/HZ; + peer->login_min = 0; + reset_knock_status(peer); + + return peer; +} + +/** + * It adds a new peer matching status to the list. + * + * @peer + * @rule + */ +static inline void +add_peer(struct peer *peer, struct ipt_pknock_rule *rule) +{ + int hash = pknock_hash(&peer->ip, sizeof(peer->ip), + ipt_pknock_hash_rnd, peer_hashsize); + list_add(&peer->head, &rule->peer_head[hash]); +} + +/** + * It removes a peer matching status. + * + * @peer + */ +static inline void +remove_peer(struct peer *peer) +{ + list_del(&peer->head); + if (peer) kfree(peer); +} + +/** + * @peer + * @info + * @port + * @return: 1 success, 0 failure + */ +static inline bool +is_first_knock(const struct peer *peer, const struct ipt_pknock *info, + uint16_t port) +{ + return (peer == NULL && info->port[0] == port) ? 1 : 0; +} + +/** + * @peer + * @info + * @port + * @return: 1 success, 0 failure + */ +static inline bool +is_wrong_knock(const struct peer *peer, const struct ipt_pknock *info, + uint16_t port) +{ + return peer && (info->port[peer->id_port_knocked-1] != port); +} + +/** + * @peer + * @info + * @return: 1 success, 0 failure + */ +static inline bool +is_last_knock(const struct peer *peer, const struct ipt_pknock *info) +{ + return peer && (peer->id_port_knocked-1 == info->ports_count); +} + +/** + * @peer + * @return: 1 success, 0 failure + */ +static inline bool +is_allowed(const struct peer *peer) +{ + return peer && (peer->status == ST_ALLOWED); +} + +/** + * Sends a message to user space through netlink sockets. + * + * @info + * @peer + * @return: 1 success, 0 otherwise + */ +static bool +msg_to_userspace_nl(const struct ipt_pknock *info, + const struct peer *peer, int multicast_group) +{ + struct cn_msg *m; + struct ipt_pknock_nl_msg msg; + + m = kmalloc(sizeof(*m) + sizeof(msg), GFP_ATOMIC); + if (!m) { + printk(KERN_ERR PKNOCK "kmalloc() error in " + "msg_to_userspace_nl().\n"); + return false; + } + + memset(m, 0, sizeof(*m) + sizeof(msg)); + m->seq = 0; + m->len = sizeof(msg); + + msg.peer_ip = peer->ip; + scnprintf(msg.rule_name, info->rule_name_len + 1, info->rule_name); + + memcpy(m + 1, (char *)&msg, m->len); + + cn_netlink_send(m, multicast_group, GFP_ATOMIC); + + kfree(m); + return true; +} + +/** + * Transforms a sequence of characters to hexadecimal. + * + * @out: the hexadecimal result + * @crypt: the original sequence + * @size + */ +static void +crypt_to_hex(char *out, char *crypt, int size) +{ + int i; + for (i=0; i < size; i++) { + unsigned char c = crypt[i]; + *out++ = '0' + ((c&0xf0)>>4) + (c>=0xa0)*('a'-'9'-1); + *out++ = '0' + (c&0x0f) + ((c&0x0f)>=0x0a)*('a'-'9'-1); + } +} + +/** + * Checks that the payload has the hmac(secret+ipsrc+epoch_min). + * + * @secret + * @secret_len + * @ipsrc + * @payload + * @payload_len + * @return: 1 success, 0 failure + */ +static int +has_secret(unsigned char *secret, int secret_len, uint32_t ipsrc, + unsigned char *payload, int payload_len) +{ + struct scatterlist sg[2]; + char result[64]; // 64 bytes * 8 = 512 bits + char *hexresult = NULL; + int hexa_size; + int ret = 0; + int epoch_min; + + if (payload_len == 0) + return 0; + + /* + * hexa: 4bits + * ascii: 8bits + * hexa = ascii * 2 + */ + hexa_size = crypto.size * 2; + + /* + 1 cause we MUST add NULL in the payload */ + if (payload_len != hexa_size + 1) + return 0; + + hexresult = kmalloc(sizeof(char) * hexa_size, GFP_ATOMIC); + if (hexresult == NULL) { + printk(KERN_ERR PKNOCK "kmalloc() error in has_secret().\n"); + return 0; + } + + memset(result, 0, 64); + memset(hexresult, 0, (sizeof(char) * hexa_size)); + + epoch_min = get_epoch_minute(); + + sg_set_buf(&sg[0], &ipsrc, sizeof(ipsrc)); + sg_set_buf(&sg[1], &epoch_min, sizeof(epoch_min)); + + ret = crypto_hash_setkey(crypto.tfm, secret, secret_len); + if (ret) { + printk("crypto_hash_setkey() failed ret=%d\n", ret); + return ret; + } + + /* + * The third parameter is the number of bytes INSIDE the sg! + * 4 bytes IP (32 bits) + + * 4 bytes int epoch_min (32 bits) + */ + ret = crypto_hash_digest(&crypto.desc, sg, 8, result); + if (ret) { + printk("crypto_hash_digest() failed ret=%d\n", ret); + return ret; + } + + crypt_to_hex(hexresult, result, crypto.size); + + if (memcmp(hexresult, payload, hexa_size) != 0) { +#if DEBUG + printk(KERN_INFO PKNOCK "secret match failed\n"); +#endif + goto out; + } + + ret = 1; + +out: + if (hexresult != NULL) kfree(hexresult); + return ret; +} + +/** + * If the peer pass the security policy. + * + * @peer + * @info + * @payload + * @payload_len + * @return: 1 if pass security, 0 otherwise + */ +static bool +pass_security(struct peer *peer, const struct ipt_pknock *info, + unsigned char *payload, int payload_len) +{ + if (is_allowed(peer)) + return true; + + /* The peer can't log more than once during the same minute. */ + if (has_logged_during_this_minute(peer)) { + DEBUGP("BLOCKED", peer); + return false; + } + /* Check for OPEN secret */ + if (!has_secret((unsigned char *)info->open_secret, + (int)info->open_secret_len, htonl(peer->ip), + payload, payload_len)) + { + return false; + } + return true; +} + +/** + * It updates the peer matching status. + * + * @peer + * @info + * @rule + * @hdr + * @return: 1 if allowed, 0 otherwise + */ +static bool +update_peer(struct peer *peer, const struct ipt_pknock *info, + struct ipt_pknock_rule *rule, + const struct transport_data *hdr) +{ + unsigned long time; + + if (is_wrong_knock(peer, info, hdr->port)) { + DEBUGP("DIDN'T MATCH", peer); + /* Peer must start the sequence from scratch. */ + if (info->option & IPT_PKNOCK_STRICT) + reset_knock_status(peer); + + return false; + } + + /* If security is needed. */ + if (info->option & IPT_PKNOCK_OPENSECRET ) { + if (hdr->proto != IPPROTO_UDP) + return false; + + if (!pass_security(peer, info, hdr->payload, hdr->payload_len)) { + return false; + } + } + + /* Just update the timer when there is a state change. */ + update_rule_timer(rule); + + peer->id_port_knocked++; + + if (is_last_knock(peer, info)) { + peer->status = ST_ALLOWED; + + DEBUGP("ALLOWED", peer); + + if (nl_multicast_group > 0) + msg_to_userspace_nl(info, peer, nl_multicast_group); + + peer->login_min = get_epoch_minute(); + return true; + } + + /* Controls the max matching time between ports. */ + if (info->option & IPT_PKNOCK_TIME) { + time = jiffies/HZ; + + if (is_time_exceeded(peer, info->max_time)) { +#if DEBUG + DEBUGP("TIME EXCEEDED", peer); + DEBUGP("DESTROYED", peer); + printk(KERN_INFO PKNOCK "max_time: %ld - time: %ld\n", + peer->timestamp + info->max_time, + time); +#endif + remove_peer(peer); + return false; + } + peer->timestamp = time; + } + DEBUGP("MATCHING", peer); + peer->status = ST_MATCHING; + return false; +} + +/** + * Make the peer no more ALLOWED sending a payload with a special secret for + * closure. + * + * @peer + * @info + * @payload + * @payload_len + * @return: 1 if close knock, 0 otherwise + */ +static inline bool +is_close_knock(const struct peer *peer, const struct ipt_pknock *info, + unsigned char *payload, int payload_len) +{ + /* Check for CLOSE secret. */ + if (has_secret((unsigned char *)info->close_secret, + (int)info->close_secret_len, htonl(peer->ip), + payload, payload_len)) + { + DEBUGP("RESET", peer); + return true; + } + return false; +} + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,23) +static bool +#else +static int +#endif +match(const struct sk_buff *skb, +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,28) + const struct xt_match_param *par +#else + const struct net_device *in, + const struct net_device *out, +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,17) + const struct xt_match *match, +#endif + const void *matchinfo, + int offset, +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,16) + unsigned int protoff, +#endif + bool *hotdrop +#endif +) +{ +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,28) + const struct ipt_pknock *info = par->matchinfo; +#else + const struct ipt_pknock *info = matchinfo; +#endif + struct ipt_pknock_rule *rule = NULL; + struct peer *peer = NULL; +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,22) + struct iphdr *iph = ip_hdr(skb); +#else + struct iphdr *iph = skb->nh.iph; +#endif + int hdr_len = 0; + __be16 _ports[2], *pptr = NULL; + struct transport_data hdr = {0, 0, 0, NULL}; +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,23) + bool ret = false; +#else + int ret = 0; +#endif + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,28) + pptr = skb_header_pointer(skb, par->thoff, sizeof _ports, &_ports); +#else + pptr = skb_header_pointer(skb, protoff, sizeof _ports, &_ports); +#endif + + if (pptr == NULL) { + /* We've been asked to examine this packet, and we + * can't. Hence, no choice but to drop. + */ + duprintf("Dropping evil offset=0 tinygram.\n"); +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,23) +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,28) + *par->hotdrop = true; +#else + *hotdrop = true; +#endif + return false; +#else + *hotdrop = 1; + return 0; +#endif + } + + hdr.port = ntohs(pptr[1]); + + switch ((hdr.proto = iph->protocol)) { + case IPPROTO_TCP: + break; + + case IPPROTO_UDP: + hdr_len = (iph->ihl * 4) + sizeof(struct udphdr); + break; + + default: + printk(KERN_INFO PKNOCK + "IP payload protocol is neither tcp nor udp.\n"); + return false; + } + + spin_lock_bh(&list_lock); + + /* Searches a rule from the list depending on info structure options. */ + if ((rule = search_rule(info)) == NULL) { + printk(KERN_INFO PKNOCK "The rule %s doesn't exist.\n", + info->rule_name); + goto out; + } + + /* Gives the peer matching status added to rule depending on ip src. */ + peer = get_peer(rule, iph->saddr); + + if (info->option & IPT_PKNOCK_CHECKIP) { + ret = is_allowed(peer); + goto out; + } + + if (iph->protocol == IPPROTO_UDP) { + hdr.payload = (void *)iph + hdr_len; + hdr.payload_len = skb->len - hdr_len; + } + + /* Sets, updates, removes or checks the peer matching status. */ + if (info->option & IPT_PKNOCK_KNOCKPORT) { + if ((ret = is_allowed(peer))) { + if (info->option & IPT_PKNOCK_CLOSESECRET && + iph->protocol == IPPROTO_UDP) + { + if (is_close_knock(peer, info, hdr.payload, hdr.payload_len)) + { + reset_knock_status(peer); +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,23) + ret = false; +#else + ret = 0; +#endif + } + } + goto out; + } + + if (is_first_knock(peer, info, hdr.port)) { + peer = new_peer(iph->saddr, iph->protocol); + add_peer(peer, rule); + } + + if (peer == NULL) goto out; + + update_peer(peer, info, rule, &hdr); + } + +out: +#if DEBUG + if (ret) DEBUGP("PASS OK", peer); +#endif + spin_unlock_bh(&list_lock); + return ret; +} + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,23) +#define RETURN_ERR(err) do { printk(KERN_ERR PKNOCK err); return false; } while (0) +#else +#define RETURN_ERR(err) do { printk(KERN_ERR PKNOCK err); return 0; } while (0) +#endif + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,23) +static bool +#else +static int +#endif +checkentry( +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,28) + const struct xt_mtchk_param *par +#else + const char *tablename, +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,16) + const void *ip, +#else + const struct ipt_ip *ip, +#endif +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,17) + const struct xt_match *match, +#endif + void *matchinfo, +#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,17) + unsigned int matchsize, +#endif + unsigned int hook_mask +#endif +) +{ +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,28) + struct ipt_pknock *info = par->matchinfo; +#else + struct ipt_pknock *info = matchinfo; +#endif + + /* Singleton. */ + if (!rule_hashtable) { + if (!(rule_hashtable = alloc_hashtable(rule_hashsize))) + RETURN_ERR("alloc_hashtable() error in checkentry()\n"); + + get_random_bytes(&ipt_pknock_hash_rnd, sizeof (ipt_pknock_hash_rnd)); + } + + if (!add_rule(info)) + RETURN_ERR("add_rule() error in checkentry() function.\n"); + + if (!(info->option & IPT_PKNOCK_NAME)) + RETURN_ERR("You must specify --name option.\n"); + + if ((info->option & IPT_PKNOCK_OPENSECRET) && (info->ports_count != 1)) + RETURN_ERR("--opensecret must have just one knock port\n"); + + if (info->option & IPT_PKNOCK_KNOCKPORT) { + if (info->option & IPT_PKNOCK_CHECKIP) { + RETURN_ERR("Can't specify --knockports with --checkip.\n"); + } + if ((info->option & IPT_PKNOCK_OPENSECRET) && + !(info->option & IPT_PKNOCK_CLOSESECRET)) + { + RETURN_ERR("--opensecret must go with --closesecret.\n"); + } + if ((info->option & IPT_PKNOCK_CLOSESECRET) && + !(info->option & IPT_PKNOCK_OPENSECRET)) + { + RETURN_ERR("--closesecret must go with --opensecret.\n"); + } + } + + if (info->option & IPT_PKNOCK_CHECKIP) { + if (info->option & IPT_PKNOCK_KNOCKPORT) + { + RETURN_ERR("Can't specify --checkip with --knockports.\n"); + } + if ((info->option & IPT_PKNOCK_OPENSECRET) || + (info->option & IPT_PKNOCK_CLOSESECRET)) + { + RETURN_ERR("Can't specify --opensecret and --closesecret" + " with --checkip.\n"); + } + if (info->option & IPT_PKNOCK_TIME) + RETURN_ERR("Can't specify --time with --checkip.\n"); + } + + if (info->option & IPT_PKNOCK_OPENSECRET) { + if (info->open_secret_len == info->close_secret_len) { + if (memcmp(info->open_secret, info->close_secret, + info->open_secret_len) == 0) + { + RETURN_ERR("opensecret & closesecret cannot be equal.\n"); + } + } + } + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,23) + return true; +#else + return 1; +#endif +} + +static void +destroy( +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,28) + const struct xt_mtdtor_param *par +#else +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,17) + const struct xt_match *match, void *matchinfo +#else + void *matchinfo, unsigned int matchsize +#endif +#endif +) +{ +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,28) + struct ipt_pknock *info = par->matchinfo; +#else + struct ipt_pknock *info = matchinfo; +#endif + /* Removes a rule only if it exits and ref_count is equal to 0. */ + remove_rule(info); +} + +static struct xt_match ipt_pknock_match __read_mostly = { + .name = "pknock", +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,21) + .family = NFPROTO_IPV4, +#endif +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,17) + .matchsize = sizeof (struct ipt_pknock), +#endif + .match = match, + .checkentry = checkentry, + .destroy = destroy, + .me = THIS_MODULE +}; + +static int __init ipt_pknock_init(void) +{ + printk(KERN_INFO PKNOCK "register.\n"); + + if (request_module(crypto.algo) < 0) { + printk(KERN_ERR PKNOCK "request_module('%s') error.\n", + crypto.algo); + return -1; + } + + crypto.tfm = crypto_alloc_hash(crypto.algo, 0, CRYPTO_ALG_ASYNC); + + if (crypto.tfm == NULL) { + printk(KERN_ERR PKNOCK "failed to load transform for %s\n", + crypto.algo); + return -1; + } + + crypto.size = crypto_hash_digestsize(crypto.tfm); + crypto.desc.tfm = crypto.tfm; + crypto.desc.flags = 0; + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,24) + if (!(pde = proc_mkdir("ipt_pknock", init_net.proc_net))) { +#else + if (!(pde = proc_mkdir("ipt_pknock", proc_net))) { +#endif + printk(KERN_ERR PKNOCK "proc_mkdir() error in _init().\n"); + return -1; + } +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,21) + return xt_register_match(&ipt_pknock_match); +#else + return ipt_register_match(&ipt_pknock_match); +#endif +} + +static void __exit ipt_pknock_fini(void) +{ + printk(KERN_INFO PKNOCK "unregister.\n"); +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,24) + remove_proc_entry("ipt_pknock", init_net.proc_net); +#else + remove_proc_entry("ipt_pknock", proc_net); +#endif +#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,21) + xt_unregister_match(&ipt_pknock_match); +#else + ipt_unregister_match(&ipt_pknock_match); +#endif + kfree(rule_hashtable); + + if (crypto.tfm != NULL) crypto_free_hash(crypto.tfm); +} + +module_init(ipt_pknock_init); +module_exit(ipt_pknock_fini); diff --git a/extensions/xt_pknock.h b/extensions/xt_pknock.h new file mode 100644 index 0000000..3d0f467 --- /dev/null +++ b/extensions/xt_pknock.h @@ -0,0 +1,94 @@ +/* + * Kernel module to implement Port Knocking and SPA matching support. + * + * (C) 2006-2008 J. Federico Hernandez + * (C) 2006 Luis Floreani + * + * $Id$ + * + * This program is released under the terms of GNU GPL version 2. + */ +#ifndef _IPT_PKNOCK_H +#define _IPT_PKNOCK_H + +#define PKNOCK "ipt_pknock: " + +#define IPT_PKNOCK_KNOCKPORT 0x01 +#define IPT_PKNOCK_TIME 0x02 +#define IPT_PKNOCK_NAME 0x04 +#define IPT_PKNOCK_STRICT 0x08 +#define IPT_PKNOCK_CHECKIP 0x10 +#define IPT_PKNOCK_OPENSECRET 0x20 +#define IPT_PKNOCK_CLOSESECRET 0x40 + +#define IPT_PKNOCK_MAX_PORTS 15 +#define IPT_PKNOCK_MAX_BUF_LEN 32 +#define IPT_PKNOCK_MAX_PASSWD_LEN 32 + +#define DEBUG 1 + +struct ipt_pknock { + char rule_name[IPT_PKNOCK_MAX_BUF_LEN + 1]; + int rule_name_len; + char open_secret[IPT_PKNOCK_MAX_PASSWD_LEN + 1]; + int open_secret_len; + char close_secret[IPT_PKNOCK_MAX_PASSWD_LEN + 1]; + int close_secret_len; + uint8_t ports_count; /* number of ports */ + uint16_t port[IPT_PKNOCK_MAX_PORTS]; /* port[,port,port,...] */ + unsigned long max_time; /* max matching time between ports */ + uint8_t option; /* --time, --knock-port, ... */ +}; + +struct ipt_pknock_nl_msg { + char rule_name[IPT_PKNOCK_MAX_BUF_LEN + 1]; + uint32_t peer_ip; +}; + +enum status {ST_INIT=1, ST_MATCHING, ST_ALLOWED}; + +#ifdef __KERNEL__ +#include +#include + +struct peer { + struct list_head head; + uint32_t ip; + uint8_t proto; + uint32_t id_port_knocked; + enum status status; + unsigned long timestamp; + int login_min; /* the login epoch minute */ +}; + +#include + +struct ipt_pknock_rule { + struct list_head head; + char rule_name[IPT_PKNOCK_MAX_BUF_LEN + 1]; + int rule_name_len; + unsigned int ref_count; + struct timer_list timer; /* garbage collector timer */ + struct list_head *peer_head; + struct proc_dir_entry *status_proc; + unsigned long max_time; /* max matching time between ports */ +}; + +#include + +struct ipt_pknock_crypto { + char *algo; + struct crypto_hash *tfm; + int size; + struct hash_desc desc; +}; + +struct transport_data { + uint8_t proto; + uint16_t port; /* destination port */ + int payload_len; + unsigned char *payload; +}; + +#endif /* __KERNEL__ */ +#endif /* _IPT_PKNOCK_H */ diff --git a/mconfig b/mconfig index cbeaa69..b10731b 100644 --- a/mconfig +++ b/mconfig @@ -21,5 +21,6 @@ build_ipset=m build_ipv4options=m build_length2=m build_lscan=m +build_pknock=m build_psd=m build_quota2=m