Commit 404bdbfd authored by Patrick McHardy's avatar Patrick McHardy Committed by David S. Miller
Browse files

[NETFILTER]: recent match: replace by rewritten version



Replace the unmaintainable ipt_recent match by a rewritten version that
should be fully compatible.
Signed-off-by: default avatarPatrick McHardy <kaber@trash.net>
Signed-off-by: default avatarDavid S. Miller <davem@davemloft.net>
parent f3389805
/* Kernel module to check if the source address has been seen recently. */
/* Copyright 2002-2003, Stephen Frost, 2.5.x port by laforge@netfilter.org */
/* Author: Stephen Frost <sfrost@snowman.net> */
/* Project Page: http://snowman.net/projects/ipt_recent/ */
/* This software is distributed under the terms of the GPL, Version 2 */
/* This copyright does not cover user programs that use kernel services
* by normal system calls. */
#include <linux/module.h>
#include <linux/skbuff.h>
/*
* Copyright (c) 2006 Patrick McHardy <kaber@trash.net>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This is a replacement of the old ipt_recent module, which carried the
* following copyright notice:
*
* Author: Stephen Frost <sfrost@snowman.net>
* Copyright 2002-2003, Stephen Frost, 2.5.x port by laforge@netfilter.org
*/
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/proc_fs.h>
#include <linux/spinlock.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
#include <linux/seq_file.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/ip.h>
#include <linux/vmalloc.h>
#include <linux/moduleparam.h>
#include <linux/list.h>
#include <linux/random.h>
#include <linux/jhash.h>
#include <linux/bitops.h>
#include <linux/skbuff.h>
#include <linux/inet.h>
#include <linux/netfilter_ipv4/ip_tables.h>
#include <linux/netfilter_ipv4/ipt_recent.h>
#undef DEBUG
#define HASH_LOG 9
MODULE_AUTHOR("Patrick McHardy <kaber@trash.net>");
MODULE_DESCRIPTION("IP tables recently seen matching module");
MODULE_LICENSE("GPL");
/* Defaults, these can be overridden on the module command-line. */
static unsigned int ip_list_tot = 100;
static unsigned int ip_pkt_list_tot = 20;
static unsigned int ip_list_hash_size = 0;
static unsigned int ip_list_perms = 0644;
#ifdef DEBUG
static int debug = 1;
#endif
static char version[] =
KERN_INFO RECENT_NAME " " RECENT_VER ": Stephen Frost <sfrost@snowman.net>. http://snowman.net/projects/ipt_recent/\n";
MODULE_AUTHOR("Stephen Frost <sfrost@snowman.net>");
MODULE_DESCRIPTION("IP tables recently seen matching module " RECENT_VER);
MODULE_LICENSE("GPL");
module_param(ip_list_tot, uint, 0400);
module_param(ip_pkt_list_tot, uint, 0400);
module_param(ip_list_hash_size, uint, 0400);
module_param(ip_list_perms, uint, 0400);
#ifdef DEBUG
module_param(debug, bool, 0600);
MODULE_PARM_DESC(debug,"enable debugging output");
#endif
MODULE_PARM_DESC(ip_list_tot,"number of IPs to remember per list");
MODULE_PARM_DESC(ip_pkt_list_tot,"number of packets per IP to remember");
MODULE_PARM_DESC(ip_list_hash_size,"size of hash table used to look up IPs");
MODULE_PARM_DESC(ip_list_perms,"permissions on /proc/net/ipt_recent/* files");
/* Structure of our list of recently seen addresses. */
struct recent_ip_list {
u_int32_t addr;
u_int8_t ttl;
unsigned long last_seen;
unsigned long *last_pkts;
u_int32_t oldest_pkt;
u_int32_t hash_entry;
u_int32_t time_pos;
};
struct time_info_list {
u_int32_t position;
u_int32_t time;
MODULE_PARM_DESC(ip_list_tot, "number of IPs to remember per list");
MODULE_PARM_DESC(ip_pkt_list_tot, "number of packets per IP to remember (max. 255)");
MODULE_PARM_DESC(ip_list_hash_size, "size of hash table used to look up IPs");
MODULE_PARM_DESC(ip_list_perms, "permissions on /proc/net/ipt_recent/* files");
struct recent_entry {
struct list_head list;
struct list_head lru_list;
u_int32_t addr;
u_int8_t ttl;
u_int8_t index;
u_int16_t nstamps;
unsigned long stamps[0];
};
/* Structure of our linked list of tables of recent lists. */
struct recent_ip_tables {
char name[IPT_RECENT_NAME_LEN];
int count;
int time_pos;
struct recent_ip_list *table;
struct recent_ip_tables *next;
spinlock_t list_lock;
int *hash_table;
struct time_info_list *time_info;
struct recent_table {
struct list_head list;
char name[IPT_RECENT_NAME_LEN];
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *status_proc;
#endif /* CONFIG_PROC_FS */
struct proc_dir_entry *proc;
#endif
unsigned int refcnt;
unsigned int entries;
struct list_head lru_list;
struct list_head iphash[0];
};
/* Our current list of addresses we have recently seen.
* Only added to on a --set, and only updated on --set || --update
*/
static struct recent_ip_tables *r_tables = NULL;
/* We protect r_list with this spinlock so two processors are not modifying
* the list at the same time.
*/
static LIST_HEAD(tables);
static DEFINE_SPINLOCK(recent_lock);
#ifdef CONFIG_PROC_FS
/* Our /proc/net/ipt_recent entry */
static struct proc_dir_entry *proc_net_ipt_recent = NULL;
#endif
/* Function declaration for later. */
static int
match(const struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
const struct xt_match *match,
const void *matchinfo,
int offset,
unsigned int protoff,
int *hotdrop);
/* Function to hash a given address into the hash table of table_size size */
static int hash_func(unsigned int addr, int table_size)
{
int result = 0;
unsigned int value = addr;
do { result ^= value; } while((value >>= HASH_LOG));
#ifdef DEBUG
if(debug) printk(KERN_INFO RECENT_NAME ": %d = hash_func(%u,%d)\n",
result & (table_size - 1),
addr,
table_size);
static struct proc_dir_entry *proc_dir;
static struct file_operations recent_fops;
#endif
return(result & (table_size - 1));
}
static u_int32_t hash_rnd;
static int hash_rnd_initted;
#ifdef CONFIG_PROC_FS
/* This is the function which produces the output for our /proc output
* interface which lists each IP address, the last seen time and the
* other recent times the address was seen.
*/
static int ip_recent_get_info(char *buffer, char **start, off_t offset, int length, int *eof, void *data)
static unsigned int recent_entry_hash(u_int32_t addr)
{
int len = 0, count, last_len = 0, pkt_count;
off_t pos = 0;
off_t begin = 0;
struct recent_ip_tables *curr_table;
curr_table = (struct recent_ip_tables*) data;
spin_lock_bh(&curr_table->list_lock);
for(count = 0; count < ip_list_tot; count++) {
if(!curr_table->table[count].addr) continue;
last_len = len;
len += sprintf(buffer+len,"src=%u.%u.%u.%u ",NIPQUAD(curr_table->table[count].addr));
len += sprintf(buffer+len,"ttl: %u ",curr_table->table[count].ttl);
len += sprintf(buffer+len,"last_seen: %lu ",curr_table->table[count].last_seen);
len += sprintf(buffer+len,"oldest_pkt: %u ",curr_table->table[count].oldest_pkt);
len += sprintf(buffer+len,"last_pkts: %lu",curr_table->table[count].last_pkts[0]);
for(pkt_count = 1; pkt_count < ip_pkt_list_tot; pkt_count++) {
if(!curr_table->table[count].last_pkts[pkt_count]) break;
len += sprintf(buffer+len,", %lu",curr_table->table[count].last_pkts[pkt_count]);
}
len += sprintf(buffer+len,"\n");
pos = begin + len;
if(pos < offset) { len = 0; begin = pos; }
if(pos > offset + length) { len = last_len; break; }
if (!hash_rnd_initted) {
get_random_bytes(&hash_rnd, 4);
hash_rnd_initted = 1;
}
*start = buffer + (offset - begin);
len -= (offset - begin);
if(len > length) len = length;
spin_unlock_bh(&curr_table->list_lock);
return len;
return jhash_1word(addr, hash_rnd) & (ip_list_hash_size - 1);
}
/* ip_recent_ctrl provides an interface for users to modify the table
* directly. This allows adding entries, removing entries, and
* flushing the entire table.
* This is done by opening up the appropriate table for writing and
* sending one of:
* xx.xx.xx.xx -- Add entry to table with current time
* +xx.xx.xx.xx -- Add entry to table with current time
* -xx.xx.xx.xx -- Remove entry from table
* clear -- Flush table, remove all entries
*/
static int ip_recent_ctrl(struct file *file, const char __user *input, unsigned long size, void *data)
static struct recent_entry *
recent_entry_lookup(const struct recent_table *table, u_int32_t addr, u_int8_t ttl)
{
static const u_int32_t max[4] = { 0xffffffff, 0xffffff, 0xffff, 0xff };
u_int32_t val;
int base, used = 0;
char c, *cp;
union iaddr {
uint8_t bytes[4];
uint32_t word;
} res;
uint8_t *pp = res.bytes;
int digit;
char buffer[20];
int len, check_set = 0, count;
u_int32_t addr = 0;
struct sk_buff *skb;
struct ipt_recent_info *info;
struct recent_ip_tables *curr_table;
curr_table = (struct recent_ip_tables*) data;
if(size > 20) len = 20; else len = size;
if(copy_from_user(buffer,input,len)) return -EFAULT;
if(len < 20) buffer[len] = '\0';
#ifdef DEBUG
if(debug) printk(KERN_INFO RECENT_NAME ": ip_recent_ctrl len: %d, input: `%.20s'\n",len,buffer);
#endif
struct recent_entry *e;
unsigned int h;
h = recent_entry_hash(addr);
list_for_each_entry(e, &table->iphash[h], list)
if (e->addr == addr && (ttl == e->ttl || !ttl || !e->ttl))
return e;
return NULL;
}
cp = buffer;
while(isspace(*cp)) { cp++; used++; if(used >= len-5) return used; }
static void recent_entry_remove(struct recent_table *t, struct recent_entry *e)
{
list_del(&e->list);
list_del(&e->lru_list);
kfree(e);
t->entries--;
}
/* Check if we are asked to flush the entire table */
if(!memcmp(cp,"clear",5)) {
used += 5;
spin_lock_bh(&curr_table->list_lock);
curr_table->time_pos = 0;
for(count = 0; count < ip_list_hash_size; count++) {
curr_table->hash_table[count] = -1;
}
for(count = 0; count < ip_list_tot; count++) {
curr_table->table[count].last_seen = 0;
curr_table->table[count].addr = 0;
curr_table->table[count].ttl = 0;
memset(curr_table->table[count].last_pkts,0,ip_pkt_list_tot*sizeof(unsigned long));
curr_table->table[count].oldest_pkt = 0;
curr_table->table[count].time_pos = 0;
curr_table->time_info[count].position = count;
curr_table->time_info[count].time = 0;
}
spin_unlock_bh(&curr_table->list_lock);
return used;
}
static struct recent_entry *
recent_entry_init(struct recent_table *t, u_int32_t addr, u_int8_t ttl)
{
struct recent_entry *e;
check_set = IPT_RECENT_SET;
switch(*cp) {
case '+': check_set = IPT_RECENT_SET; cp++; used++; break;
case '-': check_set = IPT_RECENT_REMOVE; cp++; used++; break;
default: if(!isdigit(*cp)) return (used+1); break;
if (t->entries >= ip_list_tot) {
e = list_entry(t->lru_list.next, struct recent_entry, lru_list);
recent_entry_remove(t, e);
}
e = kmalloc(sizeof(*e) + sizeof(e->stamps[0]) * ip_pkt_list_tot,
GFP_ATOMIC);
if (e == NULL)
return NULL;
e->addr = addr;
e->ttl = ttl;
e->stamps[0] = jiffies;
e->nstamps = 1;
e->index = 1;
list_add_tail(&e->list, &t->iphash[recent_entry_hash(addr)]);
list_add_tail(&e->lru_list, &t->lru_list);
t->entries++;
return e;
}
#ifdef DEBUG
if(debug) printk(KERN_INFO RECENT_NAME ": ip_recent_ctrl cp: `%c', check_set: %d\n",*cp,check_set);
#endif
/* Get addr (effectively inet_aton()) */
/* Shamelessly stolen from libc, a function in the kernel for doing
* this would, of course, be greatly preferred, but our options appear
* to be rather limited, so we will just do it ourselves here.
*/
res.word = 0;
c = *cp;
for(;;) {
if(!isdigit(c)) return used;
val = 0; base = 10; digit = 0;
if(c == '0') {
c = *++cp;
if(c == 'x' || c == 'X') base = 16, c = *++cp;
else { base = 8; digit = 1; }
}
for(;;) {
if(isascii(c) && isdigit(c)) {
if(base == 8 && (c == '8' || c == '0')) return used;
val = (val * base) + (c - '0');
c = *++cp;
digit = 1;
} else if(base == 16 && isascii(c) && isxdigit(c)) {
val = (val << 4) | (c + 10 - (islower(c) ? 'a' : 'A'));
c = *++cp;
digit = 1;
} else break;
}
if(c == '.') {
if(pp > res.bytes + 2 || val > 0xff) return used;
*pp++ = val;
c = *++cp;
} else break;
}
used = cp - buffer;
if(c != '\0' && (!isascii(c) || !isspace(c))) return used;
if(c == '\n') used++;
if(!digit) return used;
static void recent_entry_update(struct recent_table *t, struct recent_entry *e)
{
e->stamps[e->index++] = jiffies;
if (e->index > e->nstamps)
e->nstamps = e->index;
e->index %= ip_pkt_list_tot;
list_move_tail(&e->lru_list, &t->lru_list);
}
if(val > max[pp - res.bytes]) return used;
addr = res.word | htonl(val);
static struct recent_table *recent_table_lookup(const char *name)
{
struct recent_table *t;
if(!addr && check_set == IPT_RECENT_SET) return used;
list_for_each_entry(t, &tables, list)
if (!strcmp(t->name, name))
return t;
return NULL;
}
#ifdef DEBUG
if(debug) printk(KERN_INFO RECENT_NAME ": ip_recent_ctrl c: %c, addr: %u used: %d\n",c,addr,used);
#endif
static void recent_table_flush(struct recent_table *t)
{
struct recent_entry *e, *next;
unsigned int i;
/* Set up and just call match */
info = kmalloc(sizeof(struct ipt_recent_info),GFP_KERNEL);
if(!info) { return -ENOMEM; }
info->seconds = 0;
info->hit_count = 0;
info->check_set = check_set;
info->invert = 0;
info->side = IPT_RECENT_SOURCE;
strncpy(info->name,curr_table->name,IPT_RECENT_NAME_LEN);
info->name[IPT_RECENT_NAME_LEN-1] = '\0';
skb = kmalloc(sizeof(struct sk_buff),GFP_KERNEL);
if (!skb) {
used = -ENOMEM;
goto out_free_info;
}
skb->nh.iph = kmalloc(sizeof(struct iphdr),GFP_KERNEL);
if (!skb->nh.iph) {
used = -ENOMEM;
goto out_free_skb;
for (i = 0; i < ip_list_hash_size; i++) {
list_for_each_entry_safe(e, next, &t->iphash[i], list)
recent_entry_remove(t, e);
}
skb->nh.iph->saddr = addr;
skb->nh.iph->daddr = 0;
/* Clear ttl since we have no way of knowing it */
skb->nh.iph->ttl = 0;
match(skb,NULL,NULL,NULL,info,0,0,NULL);
kfree(skb->nh.iph);
out_free_skb:
kfree(skb);
out_free_info:
kfree(info);
#ifdef DEBUG
if(debug) printk(KERN_INFO RECENT_NAME ": Leaving ip_recent_ctrl addr: %u used: %d\n",addr,used);
#endif
return used;
}
#endif /* CONFIG_PROC_FS */
/* 'match' is our primary function, called by the kernel whenever a rule is
* hit with our module as an option to it.
* What this function does depends on what was specifically asked of it by
* the user:
* --set -- Add or update last seen time of the source address of the packet
* -- matchinfo->check_set == IPT_RECENT_SET
* --rcheck -- Just check if the source address is in the list
* -- matchinfo->check_set == IPT_RECENT_CHECK
* --update -- If the source address is in the list, update last_seen
* -- matchinfo->check_set == IPT_RECENT_UPDATE
* --remove -- If the source address is in the list, remove it
* -- matchinfo->check_set == IPT_RECENT_REMOVE
* --seconds -- Option to --rcheck/--update, only match if last_seen within seconds
* -- matchinfo->seconds
* --hitcount -- Option to --rcheck/--update, only match if seen hitcount times
* -- matchinfo->hit_count
* --seconds and --hitcount can be combined
*/
static int
match(const struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
const struct xt_match *match,
const void *matchinfo,
int offset,
unsigned int protoff,
int *hotdrop)
ipt_recent_match(const struct sk_buff *skb,
const struct net_device *in, const struct net_device *out,
const struct xt_match *match, const void *matchinfo,
int offset, unsigned int protoff, int *hotdrop)
{
int pkt_count, hits_found, ans;
unsigned long now;
const struct ipt_recent_info *info = matchinfo;
u_int32_t addr = 0, time_temp;
u_int8_t ttl = skb->nh.iph->ttl;
int *hash_table;
int orig_hash_result, hash_result, temp, location = 0, time_loc, end_collision_chain = -1;
struct time_info_list *time_info;
struct recent_ip_tables *curr_table;
struct recent_ip_tables *last_table;
struct recent_ip_list *r_list;
#ifdef DEBUG
if(debug) printk(KERN_INFO RECENT_NAME ": match() called\n");
#endif
/* Default is false ^ info->invert */
ans = info->invert;
struct recent_table *t;
struct recent_entry *e;
u_int32_t addr;
u_int8_t ttl;
int ret = info->invert;
#ifdef DEBUG
if(debug) printk(KERN_INFO RECENT_NAME ": match(): name = '%s'\n",info->name);
#endif
if (info->side == IPT_RECENT_DEST)
addr = skb->nh.iph->daddr;
else
addr = skb->nh.iph->saddr;
/* if out != NULL then routing has been done and TTL changed.
* We change it back here internally for match what came in before routing. */
if(out) ttl++;
ttl = skb->nh.iph->ttl;
/* use TTL as seen before forwarding */
if (out && !skb->sk)
ttl++;
/* Find the right table */
spin_lock_bh(&recent_lock);
curr_table = r_tables;
while( (last_table = curr_table) && strncmp(info->name,curr_table->name,IPT_RECENT_NAME_LEN) && (curr_table = curr_table->next) );
#ifdef DEBUG
if(debug) printk(KERN_INFO RECENT_NAME ": match(): table found('%s')\n",info->name);
#endif
spin_unlock_bh(&recent_lock);
/* Table with this name not found, match impossible */
if(!curr_table) { return ans; }
/* Make sure no one is changing the list while we work with it */
spin_lock_bh(&curr_table->list_lock);
r_list = curr_table->table;
if(info->side == IPT_RECENT_DEST) addr = skb->nh.iph->daddr; else addr = skb->nh.iph->saddr;
if(!addr) {
#ifdef DEBUG
if(debug) printk(KERN_INFO RECENT_NAME ": match() address (%u) invalid, leaving.\n",addr);
#endif
spin_unlock_bh(&curr_table->list_lock);
return ans;
t = recent_table_lookup(info->name);
e = recent_entry_lookup(t, addr,
info->check_set & IPT_RECENT_TTL ? ttl : 0);
if (e == NULL) {
if (!(info->check_set & IPT_RECENT_SET))
goto out;
e = recent_entry_init(t, addr, ttl);
if (e == NULL)
*hotdrop = 1;
ret ^= 1;
goto out;
}
#ifdef DEBUG
if(debug) printk(KERN_INFO RECENT_NAME ": match(): checking table, addr: %u, ttl: %u, orig_ttl: %u\n",addr,ttl,skb->nh.iph->ttl);
#endif
/* Get jiffies now in case they changed while we were waiting for a lock */
now = jiffies;
hash_table = curr_table->hash_table;
time_info = curr_table->time_info;
orig_hash_result = hash_result = hash_func(addr,ip_list_hash_size);
/* Hash entry at this result used */
/* Check for TTL match if requested. If TTL is zero then a match would never
* happen, so match regardless of existing TTL in that case. Zero means the
* entry was added via the /proc interface anyway, so we will just use the
* first TTL we get for that IP address. */
if(info->check_set & IPT_RECENT_TTL) {
while(hash_table[hash_result] != -1 && !(r_list[hash_table[hash_result]].addr == addr &&
(!r_list[hash_table[hash_result]].ttl || r_list[hash_table[hash_result]].ttl == ttl))) {
/* Collision in hash table */
hash_result = (hash_result + 1) % ip_list_hash_size;
}
} else {
while(hash_table[hash_result] != -1 && r_list[hash_table[hash_result]].addr != addr) {
/* Collision in hash table */
hash_result = (hash_result + 1) % ip_list_hash_size;
}
}
if(hash_table[hash_result] == -1 && !(info->check_set & IPT_RECENT_SET)) {
/* IP not in list and not asked to SET */
spin_unlock_bh(&curr_table->list_lock);
return ans;
}