Jul 2007
ip_conntrack_max Threshold Script
Iptables is great, easy to setup and generally worry free once it is all
configured. Except of course if one forgets to keep an eye on the state
machine. ip_conntrack does just what is says; tracks
current connections (with a time lag of course). There is a variable in
/proc/sys/net/ipv4 labelled ip_conntrack_max
which also means what it says... when ip_conntrack_max is
hit (or even close to being hit): bad things can happen. Solution:
write a script to keep an eye on ip_conntrack. Note this only helps with
kernel 2.4. In the 2.6 kernel a variety of netfilter parameters can be
changed [3].
Of course there is always
the first draft and refinements to follow.
When ip_conntrack is getting close to ip_conntrack_max
iptables starts dropping valid packets.
Dropped packets can be
a minor annoyance in some configurations or an all out disaster in others.
To top it off, systems retransmitting simply exacerbated the problem even
further [ 1 ].
One possible solution is to monitor the status of connections. For instance, periodically getting the number of current connections:
wc -l /proc/net/ip_conntrack | mail -s "Current IP Connections" \ someadmin@somemail.net
Of course periodic mails are heavy handed. What would make more sense is once a reasonable threshold is calculated; check to make sure the threshold is not approached.
In the example a simple Perl script is used. Before getting into the actual checks a look at interrupt handling and some helper functions.
#!/usr/bin/env perl
use strict;
$SIG{'INT' } = 'interrupt'; $SIG{'QUIT'} = 'interrupt';
$SIG{'HUP' } = 'interrupt'; $SIG{'TRAP'} = 'interrupt';
$SIG{'ABRT'} = 'interrupt'; $SIG{'STOP'} = 'interrupt';
#------------------------------------------------------------------------------
# Simple interrupt handling
#------------------------------------------------------------------------------
sub interrupt {
my($sig) = @_;
die $sig;
die;
}
Pretty standard stuff, the shebang, load up the strict
pragma then a simple (and no frills) routine to handle certain
interrupts.
There will be a need to load several files within the script, following the
rule of never do anything more than once unless you have to
a
short routine to load up a file and return the contents in an array
to the caller:
#------------------------------------------------------------------------------
# Generic file loader
#------------------------------------------------------------------------------
sub load_file {
my ($file) = shift;
my @flist;
open(FILE, $file) or die "Unable to open logfile $file: $!\n";
@flist = <FILE>;
close FILE;
return(@flist);
}
How much ram can iptables take before you need more? Tough question and there
is no precise answer. No doubt about it, iptables and the connection tracking
will take RAM. 70% is used - remember; physical
memory not virtual memory. Each connection requires about 1/16384 of RAM
[2].
In the following sub routine physical memory
is read from /proc/meminfo and the result * .7 * .015 is returned:
#------------------------------------------------------------------------------
# Return max mem we are willing to use
#------------------------------------------------------------------------------
sub get_mem_max {
my @kmeminfo = load_file("/proc/meminfo");
my $ram_total = @kmeminfo[3];
$ram_total =~ s{MemTotal:}{};
$ram_total =~ s{kB}{};
$ram_total =~ s{MB}{};
return (.7 * $ram_total) * .06;
}
In the check itself there are several steps to take, read in the current maximum, read in the current connections, use .6 (60%) as the ip_conntrack threshold, make sure there is room in RAM (remembering there is some fudge factor) and either:
#------------------------------------------------------------------------------
# Check the current connections situation...
#------------------------------------------------------------------------------
sub check_ip_conntrack {
my $ip_conntrack_max = `cat /proc/sys/net/ipv4/ip_conntrack_max`;
my $ip_conntrack_cur = `wc -l /proc/net/ip_conntrack`;
my $hi_water_mark = ($ip_conntrack_max * .6);
my $ip_conntrack_hard_limit = get_mem_max();
if ($ip_conntrack_cur >= $hi_water_mark) {
my $new_value = ($hi_water_mark * 2);
if (($new_value * 65000) >= $ip_conntrack_hard_limit) {
print "Error! IP CONNTRACK HAS REACHED THE 70% of RAM HIMARK!\n";
exit 0;
} else {
system("echo $new_value > /proc/sys/net/ipv4/ip_conntrack_max");
}
}
}
Following is the script; draft1:
#!/usr/bin/env perl
#
# Script ----------------------------------------------------------------------
# $Id: $
#
# Description - Take care of ip_conntrack entries.
#
# Created
# Author: Jay Fink
# Date: 2007/05/02
#
# Last Modified
# $Author: $
# $Date: $
# $State: $
#
#------------------------------------------------------------------------------
use strict;
# Interrupts to trap
$SIG{'INT' } = 'interrupt'; $SIG{'QUIT'} = 'interrupt';
$SIG{'HUP' } = 'interrupt'; $SIG{'TRAP'} = 'interrupt';
$SIG{'ABRT'} = 'interrupt'; $SIG{'STOP'} = 'interrupt';
#------------------------------------------------------------------------------
# Simple interrupt handling
#------------------------------------------------------------------------------
sub interrupt {
my($sig) = @_;
die $sig;
die;
}
#------------------------------------------------------------------------------
# Check the current connections situation...
#------------------------------------------------------------------------------
sub check_ip_conntrack {
my $ip_conntrack_max = `cat /proc/sys/net/ipv4/ip_conntrack_max`;
my $ip_conntrack_cur = `wc -l /proc/net/ip_conntrack`;
my $hi_water_mark = ($ip_conntrack_max * .6);
my $ip_conntrack_hard_limit = get_mem_max();
if ($ip_conntrack_cur >= $hi_water_mark) {
my $new_value = ($hi_water_mark * 2);
if (($new_value * 65000) >= $ip_conntrack_hard_limit) {
print "Error! IP CONNTRACK HAS REACHED THE 70% of RAM HIMARK!\n";
exit 0;
} else {
system("echo $new_value > /proc/sys/net/ipv4/ip_conntrack_max");
}
}
}
#------------------------------------------------------------------------------
# Calculate how much memory we can gobble up for conntrack
#------------------------------------------------------------------------------
sub get_mem_max {
my @kmeminfo = load_file("/proc/meminfo");
my $ram_total = @kmeminfo[3];
$ram_total =~ s{MemTotal:}{};
$ram_total =~ s{kB}{};
$ram_total =~ s{MB}{};
return (.7 * $ram_total)*.06;
}
#------------------------------------------------------------------------------
# Generic file loader
#------------------------------------------------------------------------------
sub load_file {
my ($file) = shift;
my @flist;
open(FILE, $file) or die "Unable to open logfile $file: $!\n";
@flist = <FILE>;
close FILE;
return(@flist);
}
# No input required - just run
check_ip_conntrack();
The first draft of the script does the job, however, it is lacking:
system() like calls are used
to do operations Perl 5 can do by itself.Assuming draft 1 is in production for stop gap - it is now time to go back and revise the script. Precision is easy to fix, just use longer expressions :
#------------------------------------------------------------------------------
# Calculate how much memory we can gobble up for conntrack
#------------------------------------------------------------------------------
sub get_mem_max {
my @kmeminfo = load_file("/proc/meminfo");
my $ram_total = @kmeminfo[3];
$ram_total =~ s{MemTotal:}{};
$ram_total =~ s{kB}{};
$ram_total =~ s{MB}{};
return ((.70000 * $ram_total) * (1/16384))
}
Next, fixup some variable names - the script is for ip_conntrack; no reason to state the fact over and over:
#------------------------------------------------------------------------------
# Check the maximum ip conntracks and adjust
#------------------------------------------------------------------------------
sub checkmax {
my $max = `cat /proc/sys/net/ipv4/ip_conntrack_max`;
my $cur = `wc -l /proc/net/ip_conntrack`;
my $hi_water_mark = ($max * .6);
my $hard_limit = get_mem_max();
if ($cur >= $hi_water_mark) {
my $new_value = ($hi_water_mark * 2);
if (($new_value * (1/16384)) >= $hard_limit) {
print "Error! IP CONNTRACK HAS REACHED THE 70% of RAM HIMARK!\n";
exit 0;
} else {
system("echo $new_value > /proc/sys/net/ipv4/ip_conntrack_max");
}
}
}
Note the calculation using the bucket numbers was fixed as well. Now it is time to look at internalizing the two callouts:
...
my $max = `cat /proc/sys/net/ipv4/ip_conntrack_max`;
my $cur = `wc -l /proc/net/ip_conntrack`;
...
There are many ways to do the above in Perl. What is shown
below is just one way, For the line count, just use the
load_file() routine from another routine (just in case it is
needed again in the future):
sub count {
my $fp = shift;
my @fp_contents = load_file($fp);
return (scalar(@fp_contents));
}
Next up, use load_file again - just take out what
we need instead of calling cat:
#------------------------------------------------------------------------------
# Check the maximum ip conntracks and adjust
#------------------------------------------------------------------------------
sub checkmax {
my @max_file = load_file("/proc/sys/net/ipv4/ip_conntrack_max");
my $max = $load_file[0];
chomp($max) # get rid of the end of line
my $cur = count("/proc/net/ip_conntrack");
...
Done. There are probably better ways to do it - but the goal is accomplished for the time being. A few obvious problems nixed and it is back in production.
The example was done in Perl. It could just has easily have been done in Bash or Python. Doesn't matter, what does matter is when a quick script has to be written the first draft will often do the job just don't forget to go back and improve it.
~src/linux/net/netfilter/nf_conntrack_core.c.netfilter tree for it's
paramters.(based on last 2 months log reports)