diff options
Diffstat (limited to 'heartbeat/portblock')
-rwxr-xr-x | heartbeat/portblock | 666 |
1 files changed, 666 insertions, 0 deletions
diff --git a/heartbeat/portblock b/heartbeat/portblock new file mode 100755 index 0000000..06fcc19 --- /dev/null +++ b/heartbeat/portblock @@ -0,0 +1,666 @@ +#!/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 <<END >&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 <<EOF + primitive portblock-samba ocf:heartbeat:portblock \\ + params protocol=tcp portno=137,138 action=block + primitive portunblock-samba ocf:heartbeat:portblock \\ + params protocol=tcp portno=137,138 action=unblock + primitive samba-vip ocf:heartbeat:IPaddr2 \\ + params ip=10.10.10.20 + group g-samba \\ + portblock-samba samba-vip nmbd smbd portunblock-samba + EOF + + This will do the following things: + + - DROP all incoming packets for TCP ports 137 and 138 + - Bring up the IP alias 10.10.10.20 + - start the nmbd and smbd services + - Re-enable TCP ports 137 and 138 + (enable normal firewall rules on those ports) + + This prevents clients from getting TCP RST if they try to reconnect + to the service after the alias is enabled but before nmbd and smbd + are running. These packets will cause some clients to give up + attempting to reconnect to the server. + + Attempts to connect to UDP and other non-TCP ports which have nothing + listening can result in ICMP port unreachable responses, which can + have the same undesirable affect on some clients. + + NOTE: iptables is Linux-specific. + + An additional feature in the portblock RA is the tickle ACK function + enabled by specifying the tickle_dir parameter. The tickle ACK + triggers the clients to faster reconnect their TCP connections to the + fail-overed server. + + Please note that this feature is often used for the floating IP fail- + over scenario where the long-lived TCP connections need to be tickled. + It doesn't support the cluster alias IP scenario. + + When using the tickle ACK function, in addition to the normal usage + of portblock RA, the parameter tickle_dir must be specified in the + action=unblock instance of the portblock resources. + For example, you may stack resources like below: + portblock action=block + services + portblock action=unblock tickle_dir=/tickle/state/dir + + If you want to tickle all the TCP connections which connected to _one_ + floating IP but different ports, no matter how many portblock resources + you have defined, you should enable tickles for _one_ portblock + resource(action=unblock) only. + + The tickle_dir is a location which stores the established TCP + connections. It can be a shared directory(which is cluster-visible to + all nodes) or a local directory. + If you use the shared directory, you needn't do any other things. + If you use the local directory, you must also specify the sync_script + paramater. We recommend you to use csync2 as the sync_script. + For example, if you use the local directory /tmp/tickle as tickle_dir, + you could setup the csync2 as the csync2 documentation says and + configure your /etc/csync2/csync2.cfg like: + group ticklegroup { + host node1; + host node2; + key /etc/csync2/ticklegroup.key; + include /etc/csync2/csync2.cfg; + include /tmp/tickle; + auto younger; + } + Then specify the parameter sync_script as "csync2 -xv". + +END +} + +meta_data() { + cat <<END +<?xml version="1.0"?> +<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd"> +<resource-agent name="portblock" version="1.0"> +<version>1.0</version> + +<longdesc lang="en"> +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. +</longdesc> +<shortdesc lang="en">Block and unblocks access to TCP and UDP ports</shortdesc> + +<parameters> +<parameter name="protocol" unique="0" required="1"> +<longdesc lang="en"> +The protocol used to be blocked/unblocked. +</longdesc> +<shortdesc lang="en">protocol</shortdesc> +<content type="string" default="${OCF_RESKEY_protocol_default}" /> +</parameter> + +<parameter name="portno" unique="0" required="1"> +<longdesc lang="en"> +The port number used to be blocked/unblocked. +</longdesc> +<shortdesc lang="en">portno</shortdesc> +<content type="string" default="${OCF_RESKEY_portno_default}" /> +</parameter> + +<parameter name="action" unique="0" required="1"> +<longdesc lang="en"> +The action (block/unblock) to be done on the protocol::portno. +</longdesc> +<shortdesc lang="en">action</shortdesc> +<content type="string" default="${OCF_RESKEY_action_default}" /> +</parameter> + +<parameter name="reset_local_on_unblock_stop" unique="0" required="0"> +<longdesc lang="en"> +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". +</longdesc> +<shortdesc lang="en">(try to) reset server TCP sessions when unblock stops</shortdesc> +<content type="boolean" default="${OCF_RESKEY_reset_local_on_unblock_stop_default}" /> +</parameter> + +<parameter name="ip" unique="0" required="0"> +<longdesc lang="en"> +The IP address used to be blocked/unblocked. +</longdesc> +<shortdesc lang="en">ip</shortdesc> +<content type="string" default="${OCF_RESKEY_ip_default}" /> +</parameter> + +<parameter name="tickle_dir" unique="0" required="0"> +<longdesc lang="en"> +The shared or local directory (_must_ be absolute path) which +stores the established TCP connections. +</longdesc> +<shortdesc lang="en">Tickle directory</shortdesc> +<content type="string" default="${OCF_RESKEY_tickle_dir_default}" /> +</parameter> + +<parameter name="sync_script" unique="0" required="0"> +<longdesc lang="en"> +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". +</longdesc> +<shortdesc lang="en">Connection state file synchronization script</shortdesc> +<content type="string" default="${OCF_RESKEY_sync_script_default}" /> +</parameter> + +<parameter name="direction" unique="0" required="0"> +<longdesc lang="en"> +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. +</longdesc> +<shortdesc lang="en">Whether to block incoming or outgoing traffic, or both</shortdesc> +<content type="string" default="${OCF_RESKEY_direction_default}" /> +</parameter> +</parameters> + +<actions> +<action name="start" timeout="20s" /> +<action name="stop" timeout="20s" /> +<action name="status" depth="0" timeout="10s" interval="10s" /> +<action name="monitor" depth="0" timeout="10s" interval="10s" /> +<action name="meta-data" timeout="5s" /> +<action name="validate-all" timeout="5s" /> +</actions> +</resource-agent> +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 + echo "^DROP${w}${1}${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 "$PAT" >/dev/null +} + +# 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 + echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle + 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 $? |