#!/bin/sh # # portblock: iptables temporary portblocking control # # Author: Sun Jiang Dong (initial version) # Philipp Reisner (per-IP filtering) # # License: GNU General Public License (GPL) # # Copyright: (C) 2005 International Business Machines # # OCF parameters are as below: # OCF_RESKEY_protocol # OCF_RESKEY_portno # OCF_RESKEY_action # OCF_RESKEY_ip # OCF_RESKEY_tickle_dir # OCF_RESKEY_sync_script ####################################################################### # Initialization: : ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat} . ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs # Defaults OCF_RESKEY_protocol_default="" OCF_RESKEY_portno_default="" OCF_RESKEY_direction_default="in" OCF_RESKEY_action_default="" OCF_RESKEY_ip_default="0.0.0.0/0" OCF_RESKEY_reset_local_on_unblock_stop_default="false" OCF_RESKEY_tickle_dir_default="" OCF_RESKEY_sync_script_default="" : ${OCF_RESKEY_protocol=${OCF_RESKEY_protocol_default}} : ${OCF_RESKEY_portno=${OCF_RESKEY_portno_default}} : ${OCF_RESKEY_direction=${OCF_RESKEY_direction_default}} : ${OCF_RESKEY_action=${OCF_RESKEY_action_default}} : ${OCF_RESKEY_ip=${OCF_RESKEY_ip_default}} : ${OCF_RESKEY_reset_local_on_unblock_stop=${OCF_RESKEY_reset_local_on_unblock_stop_default}} : ${OCF_RESKEY_tickle_dir=${OCF_RESKEY_tickle_dir_default}} : ${OCF_RESKEY_sync_script=${OCF_RESKEY_sync_script_default}} ####################################################################### CMD=`basename $0` TICKLETCP=$HA_BIN/tickle_tcp usage() { cat <&2 usage: $CMD {start|stop|status|monitor|meta-data|validate-all} $CMD is used to temporarily block ports using iptables. It can be used to blackhole a port before bringing up an IP address, and enable it after a service is started. To do that for samba, the following can be used: crm configure < 1.0 Resource script for portblock. It is used to temporarily block ports using iptables. In addition, it may allow for faster TCP reconnects for clients on failover. Use that if there are long lived TCP connections to an HA service. This feature is enabled by setting the tickle_dir parameter and only in concert with action set to unblock. Note that the tickle ACK function is new as of version 3.0.2 and hasn't yet seen widespread use. Block and unblocks access to TCP and UDP ports The protocol used to be blocked/unblocked. protocol The port number used to be blocked/unblocked. portno The action (block/unblock) to be done on the protocol::portno. action If for some reason the long lived server side TCP sessions won't be cleaned up by a reconfiguration/flush/stop of whatever services this portblock protects, they would linger in the connection table, even after the IP is gone and services have been switched over to another node. An example would be the default NFS kernel server. These "known" connections may seriously confuse and delay a later switchback. Enabling this option will cause this agent to try to get rid of these connections by injecting a temporary iptables rule to TCP-reset outgoing packets from the blocked ports, and additionally tickle them locally, just before it starts to DROP incoming packets on "unblock stop". (try to) reset server TCP sessions when unblock stops The IP address used to be blocked/unblocked. ip The shared or local directory (_must_ be absolute path) which stores the established TCP connections. Tickle directory If the tickle_dir is a local directory, then the TCP connection state file has to be replicated to other nodes in the cluster. It can be csync2 (default), some wrapper of rsync, or whatever. It takes the file name as a single argument. For csync2, set it to "csync2 -xv". Connection state file synchronization script Whether to block incoming or outgoing traffic. Can be either "in", "out", or "both". If "in" is used, the incoming ports are blocked on the INPUT chain. If "out" is used, the outgoing ports are blocked on the OUTPUT chain. If "both" is used, both the incoming and outgoing ports are blocked. Whether to block incoming or outgoing traffic, or both END } # # Because this is the normal usage, we consider "block" # resources to be pseudo-resources -- that is, their status can't # be reliably determined through external means. # This is because we expect an "unblock" resource to come along # and disable us -- but we're still in some sense active... # #active_grep_pat {udp|tcp} portno,portno ip {d|s} # d = look for destination ports # s = look for source ports active_grep_pat() { w="[ ][ ]*" any="0\\.0\\.0\\.0/0" src=$any dst=$3 if [ "$4" = "s" ]; then local src=$3 local dst=$any fi # iptables 1.8.9 briefly broke the output format, returning the # numeric protocol value instead of a string. Support both variants. if [ "$1" = "tcp" ]; then local prot="(tcp|6)" else local prot="(udp|17)" fi echo "^DROP${w}${prot}${w}--${w}${src}${w}${dst}${w}multiport${w}${4}ports${w}${2}$" } #chain_isactive {udp|tcp} portno,portno ip chain chain_isactive() { [ "$4" = "OUTPUT" ] && ds="s" || ds="d" PAT=$(active_grep_pat "$1" "$2" "$3" "$ds") $IPTABLES $wait -n -L "$4" | grep -qE "$PAT" } # netstat -tn and ss -Htn, split on whitespace and colon, # look very similar: # tcp 0 0 10.43.55.1 675 10.43.9.8 2049 ESTABLISHED # ESTAB 0 0 10.43.55.1 675 10.43.9.8 2049 # so we can write one awk script for both get_established_tcp_connections() { local columns if [ -z "$1" ] ; then columns='$4,$5, $6,$7' else # swap local and remote for "tickle_local" columns='$6,$7, $4,$5' fi $ss_or_netstat | awk -F '[:[:space:]]+' ' ( $8 == "ESTABLISHED" || $1 == "ESTAB" ) && $4 == "'$OCF_RESKEY_ip'" \ {printf "%s:%s\t%s:%s\n", '"$columns"'}' } save_tcp_connections() { [ -z "$OCF_RESKEY_tickle_dir" ] && return statefile=$OCF_RESKEY_tickle_dir/$OCF_RESKEY_ip # If we have _no_ sync script, we probably have a shared # (or replicated) directory, and need to fsync, or we might # end up with the just truncated file after failover, exactly # when we need it. # # If we _do_ have a sync script, it is not that important whether # the local state file is fsync'ed or not, the sync script is # responsible to "atomically" communicate the state to the peer(s). if [ -z "$OCF_RESKEY_sync_script" ]; then get_established_tcp_connections | dd of="$statefile".new conv=fsync status=none && mv "$statefile".new "$statefile" else get_established_tcp_connections > $statefile $OCF_RESKEY_sync_script $statefile > /dev/null 2>&1 & fi } tickle_remote() { [ -z "$OCF_RESKEY_tickle_dir" ] && return f=$OCF_RESKEY_tickle_dir/$OCF_RESKEY_ip [ -r $f ] || return $TICKLETCP -n 3 < $f } tickle_local() { [ -z "$OCF_RESKEY_tickle_dir" ] && return f=$OCF_RESKEY_tickle_dir/$OCF_RESKEY_ip [ -r $f ] || return # swap "local" and "remote" address, # so we tickle ourselves. # We set up a REJECT with tcp-reset before we do so, so we get rid of # the no longer wanted potentially long lived "ESTABLISHED" connection # entries on the IP we are going to delet in a sec. These would get in # the way if we switch-over and then switch-back in quick succession. local i awk '{ print $2, $1; }' $f | $TICKLETCP $ss_or_netstat | grep -Fw $OCF_RESKEY_ip || return for i in 0.1 0.5 1 2 4 ; do sleep $i # now kill what is currently in the list, # not what was recorded during last monitor get_established_tcp_connections swap | $TICKLETCP $ss_or_netstat | grep -Fw $OCF_RESKEY_ip || break done } SayActive() { echo "$CMD DROP rule [$*] is running (OK)" } SayConsideredActive() { echo "$CMD DROP rule [$*] considered to be running (OK)" } SayInactive() { echo "$CMD DROP rule [$*] is inactive" } #IptablesStatus {udp|tcp} portno,portno ip {in|out|both} {block|unblock} IptablesStatus() { local rc rc=$OCF_ERR_GENERIC is_active=0 if [ "$4" = "in" ] || [ "$4" = "both" ]; then chain_isactive "$1" "$2" "$3" INPUT is_active=$? fi if [ "$4" = "out" ] || [ "$4" = "both" ]; then chain_isactive "$1" "$2" "$3" OUTPUT r=$? [ $r -gt $is_active ] && is_active=$r fi if [ $is_active -eq 0 ]; then case $5 in block) SayActive $* rc=$OCF_SUCCESS ;; *) SayInactive $* rc=$OCF_NOT_RUNNING ;; esac else case $5 in block) if ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" status; then SayConsideredActive $* rc=$OCF_SUCCESS else SayInactive $* rc=$OCF_NOT_RUNNING fi ;; *) if ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" status; then SayActive $* #This is only run on real monitor events. save_tcp_connections rc=$OCF_SUCCESS else SayInactive $* rc=$OCF_NOT_RUNNING fi ;; esac fi return $rc } #DoIptables {-I|-D} {udp|tcp} portno,portno ip chain DoIptables() { op=$1 proto=$2 ports=$3 ip=$4 chain=$5 active=0; chain_isactive "$proto" "$ports" "$ip" "$chain" && active=1 want_active=0; [ "$op" = "-I" ] && want_active=1 echo "active: $active want_active: $want_active" if [ $active -eq $want_active ] ; then : Chain already in desired state else [ "$chain" = "OUTPUT" ] && ds="s" || ds="d" $IPTABLES $wait "$op" "$chain" -p "$proto" -${ds} "$ip" -m multiport --${ds}ports "$ports" -j DROP fi } #IptablesBLOCK {udp|tcp} portno,portno ip {in|out|both} {block|unblock} IptablesBLOCK() { local rc_in=0 local rc_out=0 if [ "$4" = "in" ] || [ "$4" = "both" ]; then local try_reset=false if [ "$1/$5/$__OCF_ACTION" = tcp/unblock/stop ] && ocf_is_true $reset_local_on_unblock_stop then try_reset=true fi if chain_isactive "$1" "$2" "$3" INPUT then : OK -- chain already active else if $try_reset ; then $IPTABLES $wait -I OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset tickle_local fi $IPTABLES $wait -I INPUT -p "$1" -d "$3" -m multiport --dports "$2" -j DROP rc_in=$? if $try_reset ; then $IPTABLES $wait -D OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset fi fi fi if [ "$4" = "out" ] || [ "$4" = "both" ]; then DoIptables -I "$1" "$2" "$3" OUTPUT rc_out=$? fi [ $rc_in -gt $rc_out ] && return $rc_in || return $rc_out } #IptablesUNBLOCK {udp|tcp} portno,portno ip {in|out|both} IptablesUNBLOCK() { if [ "$4" = "in" ] || [ "$4" = "both" ]; then DoIptables -D "$1" "$2" "$3" INPUT fi if [ "$4" = "out" ] || [ "$4" = "both" ]; then DoIptables -D "$1" "$2" "$3" OUTPUT fi return $? } #IptablesStart {udp|tcp} portno,portno ip {in|out|both} {block|unblock} IptablesStart() { ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" start case $5 in block) IptablesBLOCK "$@";; unblock) IptablesUNBLOCK "$@" rc=$? tickle_remote #ignore run_tickle_tcp exit code! return $rc ;; *) usage; return 1; esac return $? } #IptablesStop {udp|tcp} portno,portno ip {in|out|both} {block|unblock} IptablesStop() { ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" stop case $5 in block) IptablesUNBLOCK "$@";; unblock) save_tcp_connections IptablesBLOCK "$@" ;; *) usage; return 1;; esac return $? } # # Check if the port is valid, this function code is not decent, but works # CheckPort() { # Examples of valid port: "1080", "1", "0080" # Examples of invalid port: "1080bad", "0", "0000", "" echo $1 |egrep -qx '[0-9]+(:[0-9]+)?(,[0-9]+(:[0-9]+)?)*' } IptablesValidateAll() { check_binary $IPTABLES case $protocol in tcp|udp) ;; *) ocf_log err "Invalid protocol $protocol!" exit $OCF_ERR_CONFIGURED ;; esac if CheckPort "$portno"; then : else ocf_log err "Invalid port number $portno!" exit $OCF_ERR_CONFIGURED fi if [ -n "$OCF_RESKEY_tickle_dir" ]; then if [ x"$action" != x"unblock" ]; then ocf_log err "Tickles are only useful with action=unblock!" exit $OCF_ERR_CONFIGURED fi if [ ! -d "$OCF_RESKEY_tickle_dir" ]; then ocf_log err "The tickle dir doesn't exist!" exit $OCF_ERR_INSTALLED fi fi case $action in block|unblock) ;; *) ocf_log err "Invalid action $action!" exit $OCF_ERR_CONFIGURED ;; esac if ocf_is_true $reset_local_on_unblock_stop; then if [ $action != unblock ] ; then ocf_log err "reset_local_on_unblock_stop is only relevant with action=unblock" exit $OCF_ERR_CONFIGURED fi if [ -z $OCF_RESKEY_tickle_dir ] ; then ocf_log warn "reset_local_on_unblock_stop works best with tickle_dir enabled as well" fi fi return $OCF_SUCCESS } if ( [ $# -ne 1 ] ) then usage exit $OCF_ERR_ARGS fi case $1 in meta-data) meta_data exit $OCF_SUCCESS ;; usage) usage exit $OCF_SUCCESS ;; *) ;; esac if [ -z "$OCF_RESKEY_protocol" ]; then ocf_log err "Please set OCF_RESKEY_protocol" exit $OCF_ERR_CONFIGURED fi if [ -z "$OCF_RESKEY_portno" ]; then ocf_log err "Please set OCF_RESKEY_portno" exit $OCF_ERR_CONFIGURED fi if [ -z "$OCF_RESKEY_action" ]; then ocf_log err "Please set OCF_RESKEY_action" exit $OCF_ERR_CONFIGURED fi # iptables v1.4.20+ is required to use -w (wait) version=$(iptables -V | awk -F ' v' '{print $NF}') ocf_version_cmp "$version" "1.4.19.1" if [ "$?" -eq "2" ]; then wait="-w" else wait="" fi protocol=$OCF_RESKEY_protocol portno=$OCF_RESKEY_portno direction=$OCF_RESKEY_direction action=$OCF_RESKEY_action ip=$OCF_RESKEY_ip reset_local_on_unblock_stop=$OCF_RESKEY_reset_local_on_unblock_stop # If "tickle" is enabled, we need to record the list of currently established # connections during monitor. Use ss where available, and netstat otherwise. if [ -n "$OCF_RESKEY_tickle_dir" ] ; then if have_binary ss ; then ss_or_netstat="ss -Htn" elif have_binary netstat ; then ss_or_netstat="netstat -tn" else ocf_log err "Neither ss nor netstat found, but needed to record estblished connections." exit $OCF_ERR_INSTALLED fi fi case $1 in start) IptablesStart $protocol $portno $ip $direction $action ;; stop) IptablesStop $protocol $portno $ip $direction $action ;; status|monitor) IptablesStatus $protocol $portno $ip $direction $action ;; validate-all) IptablesValidateAll ;; *) usage exit $OCF_ERR_UNIMPLEMENTED ;; esac exit $?