From b029c56bec5bec750dea86e51307a344eff329d0 Mon Sep 17 00:00:00 2001 From: "Costa Tsaousis (ktsaou)" Date: Sat, 14 Nov 2015 04:23:56 +0200 Subject: [PATCH] added rule option *connlog* to only log the first packet of connections; refactored *connlimit* to support all possible options; added *hashlimit* with all its options; most actions now accept the keywork *with* which also supports *with connlimit* and *with hashlimit* --- doc/firehol/firehol-actions.5.md | 85 ++++ doc/firehol/firehol-params.5.md | 117 ++++- sbin/firehol.in | 715 ++++++++++++++++++++++--------- 3 files changed, 704 insertions(+), 213 deletions(-) diff --git a/doc/firehol/firehol-actions.5.md b/doc/firehol/firehol-actions.5.md index 4687b62..79be6ad 100644 --- a/doc/firehol/firehol-actions.5.md +++ b/doc/firehol/firehol-actions.5.md @@ -19,6 +19,10 @@ extra-manpage: firehol-tarpit.5 accept +accept with hashlimit *name* upto|above *amount/period* [burst *amount*] [mode *{srcip|srcport|dstip|dstport},...*] [srcmask *prefix*] [dstmask *prefix*] [htable-size *buckets*] [htable-max *entries*] [htable-expire *msec*] [htable-gcinterval *msec*] + +accept with connlimit upto|above *limit* [mask *mask*] [saddr|daddr] + accept with limit *requests/period burst* [overflow *action*] accept with recent *name* *seconds* *hits* @@ -63,6 +67,87 @@ For example, to allow SMTP requests and their replies to flow: server smtp accept +## accept with hashlimit *name* upto|above *amount/period* [burst *amount*] [mode *{srcip|srcport|dstip|dstport},...*] [srcmask *prefix*] [dstmask *prefix*] [htable-size *buckets*] [htable-max *entries*] [htable-expire *msec*] [htable-gcinterval *msec*] + +`hashlimit` hashlimit uses hash buckets to express a rate limiting match +(like the limit match) for a group of connections using a single iptables +rule. Grouping can be done per-hostgroup (source and/or destination address) +and/or per-port. + +*name* +The name for the /proc/net/ipt_hashlimit/*name* entry. + +`upto` *amount[/second|/minute|/hour|/day]* +Match if the rate is below or equal to amount/quantum. It is specified either +as a number, with an optional time quantum suffix (the default is 3/hour). + +`above` *amount[/second|/minute|/hour|/day]* +Match if the rate is above amount/quantum. + +`burst` *amount* +Maximum initial number of packets to match: this number gets recharged by one +every time the limit specified above is not reached, up to this number; the +default is 5. This option should be used with caution - if the entry expires, +the burst value is reset too. + +`mode` *{srcip|srcport|dstip|dstport},...* +A comma-separated list of objects to take into consideration. If no `mode` option +is given, *srcip,dstport* is assumed. + +`srcmask` *prefix* +When --hashlimit-mode srcip is used, all source addresses encountered will be +grouped according to the given prefix length and the so-created subnet will be +subject to hashlimit. prefix must be between (inclusive) 0 and 32. +Note that `srcmask` *0* is basically doing the same thing as not specifying +srcip for `mode`, but is technically more expensive. + +`dstmask` *prefix* +Like `srcmask`, but for destination addresses. + +`htable-size` *buckets* +The number of buckets of the hash table + +`htable-max` *entries* +Maximum entries in the hash. + +`htable-expire` *msec* +After how many milliseconds do hash entries expire. + +`htable-gcinterval` *msec* +How many milliseconds between garbage collection intervals. + +Examples: + +Allow up to 5 connections per second per client to SMTP server: + +~~~~ +server smtp accept with hashlimit smtplimit upto 5/s +~~~~ + +You can monitor it using the file /proc/net/ipt_hashlimit/smtplimit + +## accept with connlimit upto|above *limit* [mask *mask*] [saddr|daddr] + +`accept with connlimit` matches on the number of connections per IP. + +*saddr* matches on source IP. +*daddr* matches on destination IP. +*mask* groups IPs with the *mask* given +*upto* matches when the number of connections is up to the given *limit* +*above* matches when the number of connections above to the given *limit* + +The number of connections counted are system wide, not service specific. +For example for *saddr*, you cannot connlimit 2 connections for SSH and +4 for SMTP. If you connlimit 2 connections for SSH, then the first 2 +connections of a client can be SSH. If a client has already 2 connections +to another service, the client will not be able to connect to SSH. + +So, `connlimit` can safely be used: + + - with *daddr* to limit the connections a server can accept + - with *saddr* to limit the total connections per client to all services. + + ## accept with limit *requests/period burst* [overflow *action*] `accept with limit` allows the traffic, with new connections limited diff --git a/doc/firehol/firehol-params.5.md b/doc/firehol/firehol-params.5.md index df038c0..6504530 100644 --- a/doc/firehol/firehol-params.5.md +++ b/doc/firehol/firehol-params.5.md @@ -87,6 +87,8 @@ gid [not] *group* _Logging_ +connlog "log text" + log "log text" [level *loglevel*] loglimit "log text" [level *loglevel*] @@ -103,8 +105,9 @@ ipset [not] name flags [no-counters] [bytes-lt|bytes-eq|bytes-gt|bytes-not-eq *n limit *limit* *burst* -connlimit *limit* *mask* +connlimit upto|above *limit* [mask *mask*] [saddr|daddr] +hashlimit *name* upto|above *amount/period* [burst *amount*] [mode *{srcip|srcport|dstip|dstport},...*] [srcmask *prefix*] [dstmask *prefix*] [htable-size *buckets*] [htable-max *entries*] [htable-expire *msec*] [htable-gcinterval *msec*] # DESCRIPTION @@ -350,6 +353,10 @@ Use `gid` to match the operating system group sending the traffic. The # LOGGING +## connlog + +Use `connlog` to log only the first packet of a connection. + ## log, loglimit Use `log` or `loglimit` to log matching packets to syslog. Unlike @@ -390,6 +397,114 @@ implicit port. `limit` requires the arguments *frequency* and *burst* and will limit the matching of traffic in both directions. +## connlimit + +`connlimit` matches on the number of connections per IP. + +*saddr* matches on source IP. +*daddr* matches on destination IP. +*mask* groups IPs with the *mask* given +*upto* matches when the number of connections is up to the given *limit* +*above* matches when the number of connections above to the given *limit* + +The number of connections counted are system wide, not service specific. +For example for *saddr*, you cannot connlimit 2 connections for SSH and +4 for SMTP. If you connlimit 2 connections for SSH, then the first 2 +connections of a client can be SSH. If a client has already 2 connections +to another service, the client will not be able to connect to SSH. + +So, `connlimit` can safely be used: + + - with *daddr* to limit the connections a server can accept + - with *saddr* to limit the total connections per client to all services. + +## hashlimit + +`hashlimit` hashlimit uses hash buckets to express a rate limiting match +(like the limit match) for a group of connections using a single iptables +rule. Grouping can be done per-hostgroup (source and/or destination address) +and/or per-port. It gives you the ability to express "N packets per time +quantum per group" or "N bytes per seconds" (see below for some examples). + +A hash limit type (`upto`, `above`) and *name* are required. + +*name* +The name for the /proc/net/ipt_hashlimit/*name* entry. + +`upto` *amount[/second|/minute|/hour|/day]* +Match if the rate is below or equal to amount/quantum. It is specified either +as a number, with an optional time quantum suffix (the default is 3/hour), +or as amountb/second (number of bytes per second). + +`above` *amount[/second|/minute|/hour|/day]* +Match if the rate is above amount/quantum. + +`burst` *amount* +Maximum initial number of packets to match: this number gets recharged by one +every time the limit specified above is not reached, up to this number; the +default is 5. When byte-based rate matching is requested, this option specifies +the amount of bytes that can exceed the given rate. This option should be used +with caution - if the entry expires, the burst value is reset too. + +`mode` *{srcip|srcport|dstip|dstport},...* +A comma-separated list of objects to take into consideration. If no `mode` option +is given, *srcip,dstport* is assumed. + +`srcmask` *prefix* +When --hashlimit-mode srcip is used, all source addresses encountered will be +grouped according to the given prefix length and the so-created subnet will be +subject to hashlimit. prefix must be between (inclusive) 0 and 32. +Note that `srcmask` *0* is basically doing the same thing as not specifying +srcip for `mode`, but is technically more expensive. + +`dstmask` *prefix* +Like `srcmask`, but for destination addresses. + +`htable-size` *buckets* +The number of buckets of the hash table + +`htable-max` *entries* +Maximum entries in the hash. + +`htable-expire` *msec* +After how many milliseconds do hash entries expire. + +`htable-gcinterval` *msec* +How many milliseconds between garbage collection intervals. + +Examples: + +matching on source host: "1000 packets per second for every host in 192.168.0.0/16" + +~~~~ +src 192.168.0.0/16 hashlimit mylimit mode srcip upto 1000/sec +~~~~ + +matching on source port: "100 packets per second for every service of 192.168.1.1" + +~~~~ +src 192.168.1.1 hashlimit mylimit mode srcport upto 100/sec +~~~~ + +matching on subnet: "10000 packets per minute for every /28 subnet (groups of 8 addresses) in 10.0.0.0/8" + +~~~~ +src 10.0.0.8 hashlimit mylimit mask 28 upto 10000/min +~~~~ + +matching bytes per second: "flows exceeding 512kbyte/s" + +~~~~ +hashlimit mylimit mode srcip,dstip,srcport,dstport above 512kb/s +~~~~ + +matching bytes per second: "hosts that exceed 512kbyte/s, but permit up to 1Megabytes without matching" + +~~~~ +hashlimit mylimit mode dstip above 512kb/s burst 1mb +~~~~ + + # SEE ALSO * [firehol(1)][] - FireHOL program diff --git a/sbin/firehol.in b/sbin/firehol.in index 014218b..d1b7eea 100755 --- a/sbin/firehol.in +++ b/sbin/firehol.in @@ -6817,6 +6817,52 @@ close_all_groups() { } +# ------------------------------------------------------------------------------ +# AUTO ACTIONS +# + +FIREHOL_AUTO_ACTION_COUNTER=0 +FIREHOL_AUTO_ACTION_LAST= +declare -A FIREHOL_AUTO_ACTIONS=() + +create_auto_state_new_action() { + local table="${1}" action="${2}" + shift 2 + local name="${action} ${*}" chain + + # if it already exists, return + if [ ! -z "${FIREHOL_AUTO_ACTIONS[${name}]}" ] + then + FIREHOL_AUTO_ACTION_LAST="${FIREHOL_AUTO_ACTIONS[${name}]}" + return 0 + fi + + FIREHOL_AUTO_ACTION_COUNTER=$[FIREHOL_AUTO_ACTION_COUNTER + 1] + + if [ ${#name} -gt 28 ] + then + chain="$(echo "${name:0:24}_ID${FIREHOL_AUTO_ACTION_COUNTER}" | $TR_CMD " /\-.[a-z]" "____[A-Z]")" + else + chain="$(echo "${name}" | $TR_CMD " /\-.[a-z]" "____[A-Z]")" + fi + + + # register it, to avoid creating it again + # echo >&2 "NEW AUTO ACTION: ${chain}" + FIREHOL_AUTO_ACTIONS[${name}]="${chain}" + FIREHOL_AUTO_ACTION_LAST="${chain}" + + # the chain does not exist. create it. + # echo >&2 "CREATING CHAIN: ${chain}" + create_chain ${table} "${FIREHOL_AUTO_ACTION_LAST}" || return 1 + + # echo >&2 "ADDING NOT-NEW MATCH: ${chain}" + rule table ${table} chain "${FIREHOL_AUTO_ACTION_LAST}" state not NEW action "${action}" + # echo >&2 "ADDING NEW MATCH: ${chain}" + rule table ${table} chain "${FIREHOL_AUTO_ACTION_LAST}" "${@}" action "${action}" + + return 0 +} # ------------------------------------------------------------------------------ # rule - the heart of FireHOL - iptables commands generation @@ -6831,6 +6877,9 @@ close_all_groups() { # rule_action_param() should only be used within rule() - no other place +FIREHOL_CONNLOG_CHAIN_COUNT=0 +declare -A FIREHOL_CONNLOG_TARGETS=() + declare -A SMART_REJECT_CREATED=() FIREHOL_ACCEPT_CHAIN_COUNT=0 rule_action_param() { @@ -6841,8 +6890,9 @@ rule_action_param() { statenot="${3}" \ state="${4}" \ table="${5}" \ + connlog="${6}" \ count=0 val= - shift 5 + shift 6 local -a action_param=() # All arguments until the separator are the parameters of the action @@ -6862,176 +6912,12 @@ rule_action_param() { return 1 fi - # Do the rule + # prepare the actions case "${action}" in NONE) return 0 ;; - ACCEPT) - # do we have any options for this accept? - if [ ! -z "${action_param[0]}" ] - then - # find the options we have - case "${action_param[0]}" in - "limit") - # limit NEW connections to the specified rate - local freq="${action_param[1]}" \ - burst="${action_param[2]}" \ - overflow="REJECT" - - # if we have a custom overflow action, parse it. - test "${action_param[3]}" = "overflow" && overflow="`echo "${action_param[4]}" | $TR_CMD "a-z" "A-Z"`" - - # unset the action_param, so that if this rule does not include NEW connections, - # we will not append anything to the generated iptables statements. - action_param=() - - # find is this rule matches NEW connections - local has_new=`echo "${state}" | $GREP_CMD -i NEW` - local do_accept_limit=0 - if [ -z "${statenot}" ] - then - test ! -z "${has_new}" && do_accept_limit=1 - else - test -z "${has_new}" && do_accept_limit=1 - fi - - # we have a match for NEW connections. - # redirect the traffic to a new chain, which will control - # the NEW connections while allowing all the other traffic - # to pass. - if [ "${do_accept_limit}" = "1" ] - then - local accept_limit_chain="`echo "ACC LIM ${freq} ${burst} ${overflow}" | $TR_CMD " /." "___"`" - - # does the chain we need already exist? - #if [ ! -f "${FIREHOL_CHAINS_DIR}/${accept_limit_chain}.${iptables_cmd}" ] - if [ -z "${FIREHOL_CHAINS[${accept_limit_chain}.${iptables_cmd}]}" ] - then - # the chain does not exist. create it. - $iptables_cmd -t ${table} -N "${accept_limit_chain}" - FIREHOL_CHAINS[${accept_limit_chain}.${iptables_cmd}]="1" - #$TOUCH_CMD "${FIREHOL_CHAINS_DIR}/${accept_limit_chain}.${iptables_cmd}" - - # first, if the traffic is not a NEW connection, allow it. - # doing this first will speed up normal traffic. - $iptables_cmd -t ${table} -A "${accept_limit_chain}" -m conntrack ! --ctstate NEW -j ACCEPT - - # accept NEW connections within the given limits. - $iptables_cmd -t ${table} -A "${accept_limit_chain}" -m limit --limit "${freq}" --limit-burst "${burst}" -j ACCEPT - - # log the overflow NEW connections reaching this step within the new chain - prepare_iptables_log_arg "LIMIT OVERFLOW" || return 1 - $iptables_cmd -t ${table} -A "${accept_limit_chain}" -m limit --limit "${FIREHOL_LOG_FREQUENCY}" --limit-burst "${FIREHOL_LOG_BURST}" -j ${FIREHOL_LOG_MODE} ${FIREHOL_LOG_OPTIONS} "${FIREHOL_LOG_IPTABLES_ARG[@]}" - - # if the overflow is to be rejected is tcp, reject it with TCP-RESET - if [ "${overflow}" = "REJECT" ] - then - $iptables_cmd -t ${table} -A "${accept_limit_chain}" -p tcp -j REJECT --reject-with tcp-reset - fi - - # do the specified action on the overflow - $iptables_cmd -t ${table} -A "${accept_limit_chain}" -j ${overflow} - fi - - # send the rule to be generated to this chain - action=${accept_limit_chain} - fi - ;; - - "recent") - # limit NEW connections to the specified rate - local name="${action_param[1]}" \ - seconds="${action_param[2]}" \ - hits="${action_param[3]}" - - # unset the action_param, so that if this rule does not include NEW connections, - # we will not append anything to the generated iptables statements. - action_param=() - - # find is this rule matches NEW connections - local has_new=`echo "${state}" | $GREP_CMD -i NEW` - local do_accept_recent=0 - if [ -z "${statenot}" ] - then - test ! -z "${has_new}" && do_accept_recent=1 - else - test -z "${has_new}" && do_accept_recent=1 - fi - - # we have a match for NEW connections. - # redirect the traffic to a new chain, which will control - # the NEW connections while allowing all the other traffic - # to pass. - if [ "${do_accept_recent}" = "1" ] - then - local accept_recent_chain="`echo "ACC REC $name $seconds $hits" | $TR_CMD " /." "___"`" - - # does the chain we need already exist? - #if [ ! -f "${FIREHOL_CHAINS_DIR}/${accept_recent_chain}.${iptables_cmd}" ] - if [ -z "${FIREHOL_CHAINS[${accept_recent_chain}.${iptables_cmd}]}" ] - then - # the chain does not exist. create it. - $iptables_cmd -t ${table} -N "${accept_recent_chain}" - FIREHOL_CHAINS[${accept_recent_chain}.${iptables_cmd}]="1" - #$TOUCH_CMD "${FIREHOL_CHAINS_DIR}/${accept_recent_chain}.${iptables_cmd}" - - # first, if the traffic is not a NEW connection, allow it. - # doing this first will speed up normal traffic. - $iptables_cmd -t ${table} -A "${accept_recent_chain}" -m conntrack ! --ctstate NEW -j ACCEPT - - # accept NEW connections within the given limits. - $iptables_cmd -t ${table} -A "${accept_recent_chain}" -m recent --set --name "${name}" - - local t1= t2= - test ! -z $seconds && t1="--seconds ${seconds}" - test ! -z $hits && t2="--hitcount ${hits}" - - $iptables_cmd -t ${table} -A "${accept_recent_chain}" -m recent --update ${t1} ${t2} --name "${name}" -j RETURN - $iptables_cmd -t ${table} -A "${accept_recent_chain}" -j ACCEPT - fi - - # send the rule to be generated to this chain - action=${accept_recent_chain} - fi - ;; - - 'knock') - # the name of the knock - local name="knock_${action_param[1]}" - - # unset the action_param, so that if this rule does not include NEW connections, - # we will not append anything to the generated iptables statements. - action_param=() - - # does the knock chain exists? - #if [ ! -f "${FIREHOL_CHAINS_DIR}/${name}.${iptables_cmd}" ] - if [ -z "${FIREHOL_CHAINS[${name}.${iptables_cmd}]}" ] - then - # the chain does not exist. create it. - $iptables_cmd -t ${table} -N "${name}" - FIREHOL_CHAINS[${name}.${iptables_cmd}]="1" - #$TOUCH_CMD "${FIREHOL_CHAINS_DIR}/${name}.${iptables_cmd}" - - $iptables_cmd -A "${name}" -m conntrack --ctstate ESTABLISHED -j ACCEPT - - # knockd (http://www.zeroflux.org/knock/) - # will create more rules inside this chain to match NEW packets. - fi - - # send the rule to be generated to this knock chain - action=${name} - ;; - - *) - error "Internal error. Cannot understand action ${action} with parameter '${action_param[0]}'." - return 1 - ;; - esac - fi - ;; - SMART_REJECT) local key="${iptables_cmd}.${table}" key=${key// /_}; key=${key//-/_}; key=${key//\//_} @@ -7044,7 +6930,211 @@ rule_action_param() { fi ;; esac + + # do we have a connlog? + # if yes, create a new chain for the log + if [ ! -z "${connlog}" -a ! "${action}" = "RETURN" ] + then + local connlog_name="${action} ${connlog}" connlog_chain= + if [ -z "${FIREHOL_CONNLOG_TARGETS[${connlog_name}]}" ] + then + FIREHOL_CONNLOG_CHAIN_COUNT=$[FIREHOL_CONNLOG_CHAIN_COUNT + 1] + connlog_chain="${action:0:23}_CL${FIREHOL_CONNLOG_CHAIN_COUNT}" + FIREHOL_CONNLOG_TARGETS[${connlog_name}]="${connlog_chain}" + + create_chain ${table} ${connlog_chain} + rule table ${table} chain ${connlog_chain} state NEW action LOG "${connlog}" + else + connlog_chain="${FIREHOL_CONNLOG_TARGETS[${connlog_name}]}" + fi + rule table ${table} chain ${connlog_chain} action ${action} + action="${connlog_chain}" + fi + # do we have any options? + if [ ! -z "${action_param[0]}" ] + then + # find the options we have + case "${action_param[0]}" in + + connlimit|hashlimit) + # find is this rule matches NEW connections + local do_jump=0 + if [ -z "${state}" ] + then + do_jump=1 + else + local has_new=$(echo "${state}" | $GREP_CMD -i NEW) + if [ -z "${statenot}" ] + then + test ! -z "${has_new}" && do_jump=1 + else + test -z "${has_new}" && do_jump=1 + fi + fi + + if [ ${do_jump} -eq 1 ] + then + create_auto_state_new_action ${table} ${action} "${action_param[@]}" + action_param=() + action="${FIREHOL_AUTO_ACTION_LAST}" + else + action_param=() + fi + ;; + + "limit") + # limit NEW connections to the specified rate + local freq="${action_param[1]}" \ + burst="${action_param[2]}" \ + overflow="REJECT" + + # if we have a custom overflow action, parse it. + test "${action_param[3]}" = "overflow" && overflow="`echo "${action_param[4]}" | $TR_CMD "a-z" "A-Z"`" + + # unset the action_param, so that if this rule does not include NEW connections, + # we will not append anything to the generated iptables statements. + action_param=() + + # find is this rule matches NEW connections + local has_new=`echo "${state}" | $GREP_CMD -i NEW` + local do_accept_limit=0 + if [ -z "${statenot}" ] + then + test ! -z "${has_new}" && do_accept_limit=1 + else + test -z "${has_new}" && do_accept_limit=1 + fi + + # we have a match for NEW connections. + # redirect the traffic to a new chain, which will control + # the NEW connections while allowing all the other traffic + # to pass. + if [ "${do_accept_limit}" = "1" ] + then + local accept_limit_chain="`echo "ACC LIM ${freq} ${burst} ${overflow}" | $TR_CMD " /." "___"`" + + # does the chain we need already exist? + #if [ ! -f "${FIREHOL_CHAINS_DIR}/${accept_limit_chain}.${iptables_cmd}" ] + if [ -z "${FIREHOL_CHAINS[${accept_limit_chain}.${iptables_cmd}]}" ] + then + # the chain does not exist. create it. + $iptables_cmd -t ${table} -N "${accept_limit_chain}" + FIREHOL_CHAINS[${accept_limit_chain}.${iptables_cmd}]="1" + #$TOUCH_CMD "${FIREHOL_CHAINS_DIR}/${accept_limit_chain}.${iptables_cmd}" + + # first, if the traffic is not a NEW connection, allow it. + # doing this first will speed up normal traffic. + $iptables_cmd -t ${table} -A "${accept_limit_chain}" -m conntrack ! --ctstate NEW -j ${action} + + # accept NEW connections within the given limits. + $iptables_cmd -t ${table} -A "${accept_limit_chain}" -m limit --limit "${freq}" --limit-burst "${burst}" -j ${action} + + # log the overflow NEW connections reaching this step within the new chain + prepare_iptables_log_arg "LIMIT OVERFLOW" || return 1 + $iptables_cmd -t ${table} -A "${accept_limit_chain}" -m limit --limit "${FIREHOL_LOG_FREQUENCY}" --limit-burst "${FIREHOL_LOG_BURST}" -j ${FIREHOL_LOG_MODE} ${FIREHOL_LOG_OPTIONS} "${FIREHOL_LOG_IPTABLES_ARG[@]}" + + # if the overflow is to be rejected is tcp, reject it with TCP-RESET + if [ "${overflow}" = "REJECT" ] + then + $iptables_cmd -t ${table} -A "${accept_limit_chain}" -p tcp -j REJECT --reject-with tcp-reset + fi + + # do the specified action on the overflow + $iptables_cmd -t ${table} -A "${accept_limit_chain}" -j ${overflow} + fi + + # send the rule to be generated to this chain + action=${accept_limit_chain} + fi + ;; + + "recent") + # limit NEW connections to the specified rate + local name="${action_param[1]}" \ + seconds="${action_param[2]}" \ + hits="${action_param[3]}" + + # unset the action_param, so that if this rule does not include NEW connections, + # we will not append anything to the generated iptables statements. + action_param=() + + # find is this rule matches NEW connections + local has_new=`echo "${state}" | $GREP_CMD -i NEW` + local do_accept_recent=0 + if [ -z "${statenot}" ] + then + test ! -z "${has_new}" && do_accept_recent=1 + else + test -z "${has_new}" && do_accept_recent=1 + fi + + # we have a match for NEW connections. + # redirect the traffic to a new chain, which will control + # the NEW connections while allowing all the other traffic + # to pass. + if [ "${do_accept_recent}" = "1" ] + then + local accept_recent_chain="`echo "ACC REC $name $seconds $hits" | $TR_CMD " /." "___"`" + + # does the chain we need already exist? + #if [ ! -f "${FIREHOL_CHAINS_DIR}/${accept_recent_chain}.${iptables_cmd}" ] + if [ -z "${FIREHOL_CHAINS[${accept_recent_chain}.${iptables_cmd}]}" ] + then + # the chain does not exist. create it. + $iptables_cmd -t ${table} -N "${accept_recent_chain}" + FIREHOL_CHAINS[${accept_recent_chain}.${iptables_cmd}]="1" + #$TOUCH_CMD "${FIREHOL_CHAINS_DIR}/${accept_recent_chain}.${iptables_cmd}" + + # first, if the traffic is not a NEW connection, allow it. + # doing this first will speed up normal traffic. + $iptables_cmd -t ${table} -A "${accept_recent_chain}" -m conntrack ! --ctstate NEW -j ${action} + + # accept NEW connections within the given limits. + $iptables_cmd -t ${table} -A "${accept_recent_chain}" -m recent --set --name "${name}" + + local t1= t2= + test ! -z $seconds && t1="--seconds ${seconds}" + test ! -z $hits && t2="--hitcount ${hits}" + + $iptables_cmd -t ${table} -A "${accept_recent_chain}" -m recent --update ${t1} ${t2} --name "${name}" -j RETURN + $iptables_cmd -t ${table} -A "${accept_recent_chain}" -j ${action} + fi + + # send the rule to be generated to this chain + action=${accept_recent_chain} + fi + ;; + + 'knock') + # the name of the knock + local name="knock_${action_param[1]}" + + # unset the action_param, so that if this rule does not include NEW connections, + # we will not append anything to the generated iptables statements. + action_param=() + + # does the knock chain exists? + #if [ ! -f "${FIREHOL_CHAINS_DIR}/${name}.${iptables_cmd}" ] + if [ -z "${FIREHOL_CHAINS[${name}.${iptables_cmd}]}" ] + then + # the chain does not exist. create it. + $iptables_cmd -t ${table} -N "${name}" + FIREHOL_CHAINS[${name}.${iptables_cmd}]="1" + #$TOUCH_CMD "${FIREHOL_CHAINS_DIR}/${name}.${iptables_cmd}" + + $iptables_cmd -A "${name}" -m conntrack --ctstate ESTABLISHED -j ${action} + + # knockd (http://www.zeroflux.org/knock/) + # will create more rules inside this chain to match NEW packets. + fi + + # send the rule to be generated to this knock chain + action=${name} + ;; + esac + fi + $iptables_cmd "${@}" -j "${action}" "${action_param[@]}" } @@ -7107,11 +7197,16 @@ rule() { tos=(any) tosnot= \ log= logtxt= loglevel= \ limit= burst= connlimit= connlimit_mask= \ + hashlimit= \ + hashlimit_name= hashlimit_type= hashlimit_burst= \ + hashlimit_mode= hashlimit_srcmask= hashlimit_dstmask= \ + hashlimit_htable_size= hashlimit_htable_max= \ + hashlimit_htable_expire= hashlimit_htable_gcinterval= \ action= state= statenot= \ failed=0 reverse=0 \ swi=0 swo=0 \ custom= \ - accounting= \ + accounting= connlog= \ ipsetnot= ipsetname= ipsetflags= ipsetopts= \ inout= x= param= not= helper=() helpernot= opt_args= @@ -7456,6 +7551,12 @@ rule() { shift ;; + connlog) + test ${softwarnings} -eq 1 -a ! -z "${not}" && softwarning "Cannot negate a connlog. 'not' ignored." + connlog="${1}" + shift + ;; + log) test ${softwarnings} -eq 1 -a ! -z "${not}" && softwarning "Cannot negate a log. 'not' ignored." if [ ${nolog} -eq 0 ] @@ -7503,9 +7604,125 @@ rule() { connlimit|iplimit) test ${softwarnings} -eq 1 -a ! -z "${not}" && softwarning "Cannot negate a connlimit. 'not' ignored." test ${softwarnings} -eq 1 -a ! -z "${connlimit}" && softwarning "Overwriting param: connlimit '${connlimit}' becomes '${1}'" - connlimit="${1}" - connlimit_mask="${2}" - shift 2 + connlimit_type="upto" + connlimit_addr="saddr" + connlimit_mask=32 + while [ ! -z "${1}" ] + do + if [ "${1}" = "upto" ] + then + connlimit_type="upto" + connlimit="${2}" + shift 2 + continue + elif [ "${1}" = "above" ] + then + connlimit_type="above" + connlimit="${2}" + shift 2 + continue + elif [ "${1}" = "mask" ] + then + connlimit_mask="${2}" + shift 2 + continue + elif [ "${1}" = "saddr" ] + then + connlimit_addr="saddr" + shift + continue + elif [ "${1}" = "daddr" ] + then + connlimit_addr="daddr" + shift + continue + else + break + fi + done + if [ -z "${connlimit}" ] + then + error "connlimit is not set. Use: connlimit upto|above X [mask M] [saddr|daddr]" + return 1 + fi + ;; + + hashlimit) + test ${softwarnings} -eq 1 -a ! -z "${not}" && softwarning "Cannot negate a hashlimit. 'not' ignored." + test ${softwarnings} -eq 1 -a ! -z "${hashlimit}" && softwarning "Overwriting param: hashlimit '${hashlimit}' becomes '${1}'" + hashlimit_name="${1}"; shift + hashlimit_type="upto" + hashlimit_burst= + hashlimit_mode="srcip,dstport" + hashlimit_srcmask= + hashlimit_dstmask= + hashlimit_htable_size= + hashlimit_htable_max= + hashlimit_htable_expire= + hashlimit_htable_gcinterval= + while [ ! -z "${1}" ] + do + if [ "${1}" = "upto" ] + then + hashlimit_type="upto" + hashlimit="${2}" + shift 2 + continue + elif [ "${1}" = "above" ] + then + hashlimit_type="above" + hashlimit="${2}" + shift 2 + continue + elif [ "${1}" = "mode" ] + then + hashlimit_mode="${2}" + shift 2 + continue + elif [ "${1}" = "burst" ] + then + hashlimit_burst="${2}" + shift 2 + continue + elif [ "${1}" = "srcmask" ] + then + hashlimit_srcmask="${2}" + shift 2 + continue + elif [ "${1}" = "dstmask" ] + then + hashlimit_dstmask="${2}" + shift 2 + continue + elif [ "${1}" = "htable_size" -o "${1}" = "htable-size" ] + then + hashlimit_htable_size="${2}" + shift 2 + continue + elif [ "${1}" = "htable_max" -o "${1}" = "htable-max" ] + then + hashlimit_htable_max="${2}" + shift 2 + continue + elif [ "${1}" = "htable_expire" -o "${1}" = "htable-expire" ] + then + hashlimit_htable_expire="${2}" + shift 2 + continue + elif [ "${1}" = "htable_gcinterval" -o "${1}" = "htable-gcinterval" ] + then + hashlimit_htable_gcinterval="${2}" + shift 2 + continue + else + break + fi + done + if [ -z "${hashlimit}" ] + then + error "hashlimit is not set. Use: hashlimit name upto|above amount/unit [burst amount] [mode {srcip|srcport|dstip|dstport},...] [srcmask prefix] [dstmask prefix] [htable-size buckets] [htable-max entries] [htable-expire msec] [htable-gcinterval msec]" + return 1 + fi ;; acct|accounting) @@ -7602,6 +7819,7 @@ rule() { RETURN) action="RETURN" ;; NONE) action="NONE" ;; TARPIT) action="TARPIT" ;; + ACCEPT) action="ACCEPT" ;; LOG) action="${FIREHOL_LOG_MODE}" @@ -7656,42 +7874,6 @@ rule() { fi ;; - ACCEPT) action="ACCEPT" - if [ "${1}" = "with" ] - then - shift - - case "${1}" in - limit|LIMIT) - action_param=("limit" "${2}" "${3}") - shift 3 - - if [ "${1}" = "overflow" ] - then - action_param[3]="overflow" - action_param[4]="${2}" - shift 2 - fi - ;; - - recent|RECENT) - action_param=("recent" "${2}" "${3}" "${4}") - shift 4 - ;; - - knock|KNOCK) - action_param=("knock" "${2}") - shift 2 - ;; - - *) - error "Cannot understand action's '${action}' directive '${1}'" - return 1 - ;; - esac - fi - ;; - REJECT) action="REJECT" if [ "${1}" = "with" -o "${1}" = "--reject-with" ] then @@ -8033,6 +8215,88 @@ rule() { local action_is_chain=$? ;; esac + + if [ "${1}" = "with" ] + then + if [ ${#action_param} -ne 0 ] + then + error "action ${action} does not accept 'with' parameters." + return 1 + fi + + shift + case "${1}" in + hashlimit) + action_param=("${1}" "${2}") + shift 2 + while [ ! -z "${1}" ] + do + case "${1}" in + upto|above|mode|burst|srcmask|dstmask|htable_size|htable-size|htable_max|htable-max|htable_expire|htable-expire|htable_gcinterval|htable-gcinterval) + action_param=("${action_param[@]}" "${1}" "${2}") + shift 2 + continue + ;; + *) + break + ;; + esac + done + ;; + + connlimit) + action_param=("${1}") + shift + while [ ! -z "${1}" ] + do + case "${1}" in + upto|above|mask) + action_param=("${action_param[@]}" "${1}" "${2}") + shift 2 + continue + ;; + + saddr|daddr) + action_param=("${action_param[@]}" "${1}") + shift + continue + ;; + + *) + break + ;; + esac + done + ;; + + limit|LIMIT) + action_param=("limit" "${2}" "${3}") + shift 3 + + if [ "${1}" = "overflow" ] + then + action_param[3]="overflow" + action_param[4]="${2}" + shift 2 + fi + ;; + + recent|RECENT) + action_param=("recent" "${2}" "${3}" "${4}") + shift 4 + ;; + + knock|KNOCK) + action_param=("knock" "${2}") + shift 2 + ;; + + *) + error "Cannot understand action's '${action}' directive '${1}'" + return 1 + ;; + esac + fi ;; *) @@ -8277,6 +8541,7 @@ rule() { # # * limit # * connlimit + # * hashlimit # * ipset # * helpers # * custom rules @@ -8305,10 +8570,32 @@ rule() { protected_args=(${custom}) # limit - [ ! -z "${limit}" ] && protected_args=("${protected_args[@]}" "-m" "limit" "--limit" "${limit}" "--limit-burst" "${burst}") + [ ! -z "${limit}" ] && protected_args=("${protected_args[@]}" "-m" "limit" \ + "--limit" "${limit}" \ + "--limit-burst" "${burst}") # connlimit - [ ! -z "${connlimit}" ] && protected_args=("${protected_args[@]}" "-m" "connlimit" "--connlimit-above" "${connlimit}" "--connlimit-mask" "${connlimit_mask}") + [ ! -z "${connlimit}" ] && protected_args=("${protected_args[@]}" "-m" "connlimit" \ + "--connlimit-${connlimit_type}" "${connlimit}" \ + "--connlimit-mask" "${connlimit_mask}" \ + "--connlimit-${connlimit_addr}") + + # hashlimit + if [ ! -z "${hashlimit}" ] + then + protected_args=("${protected_args[@]}" "-m" "hashlimit" \ + "--hashlimit-name" "${hashlimit_name}" \ + "--hashlimit-${hashlimit_type}" "${hashlimit}" \ + "--hashlimit-mode" "${hashlimit_mode}") + + [ ! -z "${hashlimit_burst}" ] && protected_args=("${protected_args[@]}" "--hashlimit-burst" "${hashlimit_burst}") + [ ! -z "${hashlimit_srcmask}" ] && protected_args=("${protected_args[@]}" "--hashlimit-srcmask" "${hashlimit_srcmask}") + [ ! -z "${hashlimit_dstmask}" ] && protected_args=("${protected_args[@]}" "--hashlimit-dstmask" "${hashlimit_dstmask}") + [ ! -z "${hashlimit_htable_size}" ] && protected_args=("${protected_args[@]}" "--hashlimit-htable-size" "${hashlimit_htable_size}") + [ ! -z "${hashlimit_htable_max}" ] && protected_args=("${protected_args[@]}" "--hashlimit_htable_max" "${hashlimit_htable_max}") + [ ! -z "${hashlimit_htable_expire}" ] && protected_args=("${protected_args[@]}" "--hashlimit_htable_expire" "${hashlimit_htable_expire}") + [ ! -z "${hashlimit_htable_gcinterval}" ] && protected_args=("${protected_args[@]}" "--hashlimit_htable_gcinterval" "${hashlimit_htable_gcinterval}") + fi # ipset [ ! -z "${ipsetname}" ] && protected_args=("${protected_args[@]}" "-m" "set" ${ipsetnot} "--match-set" "${ipsetname}" "${ipsetflags}" ${ipsetopts}) @@ -8890,7 +9177,7 @@ rule() { # the original action of the rule # we cannot add positive_args or protected_args here - if we do, the logging and accounting will be broken - rule_action_param ${iptables} "${negative_action}" "" "" "${table}" "${action_param[@]}" -- -t ${table} -A "${negative_chain}" ${pr} "${protected_args[@]}" || failed=$[failed + 1] + rule_action_param ${iptables} "${negative_action}" "" "" "${table}" "${connlog}" "${action_param[@]}" -- -t ${table} -A "${negative_chain}" ${pr} "${protected_args[@]}" || failed=$[failed + 1] done done @@ -8961,7 +9248,7 @@ rule() { # the original action of the rule # we cannot add positive_args or protected_args here - if we do, the logging and accounting will be broken - rule_action_param ${iptables} "${logging_action}" "" "" "${table}" "${action_param[@]}" -- -t ${table} -A "${logging_chain}" ${pr} "${protected_args[@]}" || failed=$[failed + 1] + rule_action_param ${iptables} "${logging_action}" "" "" "${table}" "${connlog}" "${action_param[@]}" -- -t ${table} -A "${logging_chain}" ${pr} "${protected_args[@]}" || failed=$[failed + 1] done done @@ -9028,7 +9315,7 @@ rule() { [ ! -z "${pr}" ] && pr="-p ${pr}" # the original action of the rule - rule_action_param ${iptables} "${protected_action}" "" "" "${table}" "${action_param[@]}" -- -t ${table} -A "${protected_chain}" ${pr} "${protected_args[@]}" || failed=$[failed + 1] + rule_action_param ${iptables} "${protected_action}" "" "" "${table}" "${connlog}" "${action_param[@]}" -- -t ${table} -A "${protected_chain}" ${pr} "${protected_args[@]}" || failed=$[failed + 1] done done @@ -9375,6 +9662,7 @@ rule() { "${addrtype_arg[@]}" "${stp_arg[@]}" "${dtp_arg[@]}" "${state_arg[@]}" "${mark_arg[@]}" "${dscp_arg[@]}" \ "${protected_args[@]}" "${positive_args[@]}") + # if we append rules if [ -z "${positive_rule_number}" ] then if [ "$logrule" = "limit" ]; then ${iptables} -t ${table} ${attachement} "${basecmd[@]}" -m limit --limit "${FIREHOL_LOG_FREQUENCY}" --limit-burst "${FIREHOL_LOG_BURST}" -j ${FIREHOL_LOG_MODE} ${FIREHOL_LOG_OPTIONS} "${logopts_arg[@]}" || failed=$[failed + 1]; fi @@ -9383,9 +9671,12 @@ rule() { fi # do it! - rule_action_param ${iptables} "${action}" "${statenot}" "${state}" "${table}" "${action_param[@]}" -- -t ${table} ${attachement} "${basecmd[@]}" || failed=$[failed + 1] + rule_action_param ${iptables} "${action}" "${statenot}" "${state}" "${table}" "${connlog}" "${action_param[@]}" -- -t ${table} ${attachement} "${basecmd[@]}" || failed=$[failed + 1] (( FIREHOL_RULE_POSITIVE_STATEMENTS_GENERATED += 1 )) + # if we insert rules + # we insert these at the same position we inserted the actual rule + # so it will be inserted before the actual rule if [ ! -z "${positive_rule_number}" ] then if [ "$logrule" = "limit" ]; then ${iptables} -t ${table} ${attachement} "${basecmd[@]}" -m limit --limit "${FIREHOL_LOG_FREQUENCY}" --limit-burst "${FIREHOL_LOG_BURST}" -j ${FIREHOL_LOG_MODE} ${FIREHOL_LOG_OPTIONS} "${logopts_arg[@]}" || failed=$[failed + 1]; fi