1#!/usr/local/bin/bash
2#
3# portblock: iptables temporary portblocking control
4#
5# Author:	Sun Jiang Dong (initial version)
6#               Philipp Reisner (per-IP filtering)
7#
8# License:	GNU General Public License (GPL)
9#
10# Copyright:	(C) 2005 International Business Machines
11#
12#	  OCF parameters are as below:
13#		OCF_RESKEY_protocol
14#		OCF_RESKEY_portno
15#		OCF_RESKEY_action
16#		OCF_RESKEY_ip
17#		OCF_RESKEY_tickle_dir
18#		OCF_RESKEY_sync_script
19#######################################################################
20# Initialization:
21
22: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
23. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
24
25# Defaults
26OCF_RESKEY_protocol_default=""
27OCF_RESKEY_portno_default=""
28OCF_RESKEY_action_default=""
29OCF_RESKEY_ip_default="0.0.0.0/0"
30OCF_RESKEY_reset_local_on_unblock_stop_default="false"
31OCF_RESKEY_tickle_dir_default=""
32OCF_RESKEY_sync_script_default=""
33
34: ${OCF_RESKEY_protocol=${OCF_RESKEY_protocol_default}}
35: ${OCF_RESKEY_portno=${OCF_RESKEY_portno_default}}
36: ${OCF_RESKEY_action=${OCF_RESKEY_action_default}}
37: ${OCF_RESKEY_ip=${OCF_RESKEY_ip_default}}
38: ${OCF_RESKEY_reset_local_on_unblock_stop=${OCF_RESKEY_reset_local_on_unblock_stop_default}}
39: ${OCF_RESKEY_tickle_dir=${OCF_RESKEY_tickle_dir_default}}
40: ${OCF_RESKEY_sync_script=${OCF_RESKEY_sync_script_default}}
41#######################################################################
42CMD=`basename $0`
43TICKLETCP=$HA_BIN/tickle_tcp
44
45usage()
46{
47	cat <<END >&2
48	usage: $CMD {start|stop|status|monitor|meta-data|validate-all}
49
50	$CMD is used to temporarily block ports using iptables.
51
52	It can be used to blackhole a port before bringing
53	up an IP address, and enable it after a service is started.
54	To do that for samba, the following can be used:
55
56	crm configure <<EOF
57	primitive portblock-samba ocf:heartbeat:portblock \\
58	  params protocol=tcp portno=137,138 action=block
59	primitive portunblock-samba ocf:heartbeat:portblock \\
60	  params protocol=tcp portno=137,138 action=unblock
61	primitive samba-vip ocf:heartbeat:IPaddr2 \\
62	  params ip=10.10.10.20
63	group g-samba \\
64	  portblock-samba samba-vip nmbd smbd portunblock-samba
65	EOF
66
67	This will do the following things:
68
69	  - DROP all incoming packets for TCP ports 137 and 138
70	  - Bring up the IP alias 10.10.10.20
71	  - start the nmbd and smbd services
72	  - Re-enable TCP ports 137 and 138
73	        (enable normal firewall rules on those ports)
74
75	This prevents clients from getting TCP RST if they try to reconnect
76	to the service after the alias is enabled but before nmbd and smbd
77	are running.  These packets will cause some clients to give up
78	attempting to reconnect to the server.
79
80	Attempts to connect to UDP and other non-TCP ports which have nothing
81	listening can result in ICMP port unreachable responses, which can
82	have the same undesirable affect on some clients.
83
84	NOTE: iptables is Linux-specific.
85
86	An additional feature in the portblock RA is the tickle ACK function
87	enabled by specifying the tickle_dir parameter. The tickle ACK 
88	triggers the clients to faster reconnect their TCP connections to the 
89	fail-overed server.
90
91	Please note that this feature is often used for the floating IP fail-
92	over scenario where the long-lived TCP connections need to be tickled.
93	It doesn't support the cluster alias IP scenario.
94
95	When using the tickle ACK function, in addition to the normal usage
96	of portblock RA, the parameter tickle_dir must be specified in the 
97	action=unblock instance of the portblock resources.
98	For example, you may stack resources like below:
99		portblock action=block
100		services
101		portblock action=unblock tickle_dir=/tickle/state/dir
102
103	If you want to tickle all the TCP connections which connected to _one_
104	floating IP but different ports, no matter how many portblock resources 
105	you have defined, you should enable tickles for _one_ portblock 
106	resource(action=unblock) only.
107	
108	The tickle_dir is a location which stores the established TCP 
109	connections. It can be a shared directory(which is cluster-visible to 
110	all nodes) or a local directory.
111	If you use the shared directory, you needn't do any other things.
112	If you use the local directory, you must also specify the sync_script
113	paramater. We recommend you to use csync2 as the sync_script.
114	For example, if you use the local directory /tmp/tickle as tickle_dir, 
115	you could setup the csync2 as the csync2 documentation says and 
116	configure your /etc/csync2/csync2.cfg like:
117		group ticklegroup {
118		  host node1;
119		  host node2;
120		  key  /etc/csync2/ticklegroup.key;
121		  include /etc/csync2/csync2.cfg;
122		  include /tmp/tickle;
123		  auto younger;
124		}
125	Then specify the parameter sync_script as "csync2 -xv".
126
127END
128}
129
130meta_data() {
131	cat <<END
132<?xml version="1.0"?>
133<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
134<resource-agent name="portblock">
135<version>1.0</version>
136
137<longdesc lang="en">
138Resource script for portblock. It is used to temporarily block ports 
139using iptables. In addition, it may allow for faster TCP reconnects
140for clients on failover. Use that if there are long lived TCP
141connections to an HA service. This feature is enabled by setting the
142tickle_dir parameter and only in concert with action set to unblock.
143Note that the tickle ACK function is new as of version 3.0.2 and
144hasn't yet seen widespread use.
145</longdesc>
146<shortdesc lang="en">Block and unblocks access to TCP and UDP ports</shortdesc>
147
148<parameters>
149<parameter name="protocol" unique="0" required="1">
150<longdesc lang="en">
151The protocol used to be blocked/unblocked.
152</longdesc>
153<shortdesc lang="en">protocol</shortdesc>
154<content type="string" default="${OCF_RESKEY_protocol_default}" />
155</parameter>
156
157<parameter name="portno" unique="0" required="1">
158<longdesc lang="en">
159The port number used to be blocked/unblocked.
160</longdesc>
161<shortdesc lang="en">portno</shortdesc>
162<content type="string" default="${OCF_RESKEY_portno_default}" />
163</parameter>
164
165<parameter name="action" unique="0" required="1">
166<longdesc lang="en">
167The action (block/unblock) to be done on the protocol::portno.
168</longdesc>
169<shortdesc lang="en">action</shortdesc>
170<content type="string" default="${OCF_RESKEY_action_default}" />
171</parameter>
172
173<parameter name="reset_local_on_unblock_stop" unique="0" required="0">
174<longdesc lang="en">
175If for some reason the long lived server side TCP sessions won't be cleaned up
176by a reconfiguration/flush/stop of whatever services this portblock protects,
177they would linger in the connection table, even after the IP is gone
178and services have been switched over to another node.
179
180An example would be the default NFS kernel server.
181
182These "known" connections may seriously confuse and delay a later switchback.
183
184Enabling this option will cause this agent to try to get rid of these connections
185by injecting a temporary iptables rule to TCP-reset outgoing packets from the
186blocked ports, and additionally tickle them locally,
187just before it starts to DROP incoming packets on "unblock stop".
188</longdesc>
189<shortdesc lang="en">(try to) reset server TCP sessions when unblock stops</shortdesc>
190<content type="boolean" default="${OCF_RESKEY_reset_local_on_unblock_stop_default}" />
191</parameter>
192
193<parameter name="ip" unique="0" required="0">
194<longdesc lang="en">
195The IP address used to be blocked/unblocked.
196</longdesc>
197<shortdesc lang="en">ip</shortdesc>
198<content type="string" default="${OCF_RESKEY_ip_default}" />
199</parameter>
200
201<parameter name="tickle_dir" unique="0" required="0">
202<longdesc lang="en">
203The shared or local directory (_must_ be absolute path) which 
204stores the established TCP connections.
205</longdesc>
206<shortdesc lang="en">Tickle directory</shortdesc>
207<content type="string" default="${OCF_RESKEY_tickle_dir_default}" />
208</parameter>
209
210<parameter name="sync_script" unique="0" required="0">
211<longdesc lang="en">
212If the tickle_dir is a local directory, then the TCP connection state
213file has to be replicated to other nodes in the cluster. It can be
214csync2 (default), some wrapper of rsync, or whatever. It takes the
215file name as a single argument. For csync2, set it to "csync2 -xv".
216</longdesc>
217<shortdesc lang="en">Connection state file synchronization script</shortdesc>
218<content type="string" default="${OCF_RESKEY_sync_script_default}" />
219</parameter>
220</parameters>
221
222<actions>
223<action name="start" timeout="20s" />
224<action name="stop" timeout="20s" />
225<action name="status" depth="0" timeout="10s" interval="10s" />
226<action name="monitor" depth="0" timeout="10s" interval="10s" />
227<action name="meta-data" timeout="5s" />
228<action name="validate-all" timeout="5s" />
229</actions>
230</resource-agent>
231END
232}
233
234
235#
236#	Because this is the normal usage, we consider "block"
237#	resources to be pseudo-resources -- that is, their status can't
238#	be reliably determined through external means.
239#	This is because we expect an "unblock" resource to come along
240#	and disable us -- but we're still in some sense active...
241#
242
243#active_grep_pat {udp|tcp} portno,portno
244active_grep_pat()
245{
246  w="[ 	][ 	]*"
247  any="0\\.0\\.0\\.0/0"
248  echo "^DROP${w}${1}${w}--${w}${any}${w}${3}${w}multiport${w}dports${w}${2}\>"
249}
250
251#chain_isactive  {udp|tcp} portno,portno ip
252chain_isactive()
253{
254  PAT=`active_grep_pat "$1" "$2" "$3"`
255  $IPTABLES $wait -n -L INPUT | grep "$PAT" >/dev/null
256}
257
258save_tcp_connections()
259{
260	[ -z "$OCF_RESKEY_tickle_dir" ] && return
261	statefile=$OCF_RESKEY_tickle_dir/$OCF_RESKEY_ip
262	if [ -z "$OCF_RESKEY_sync_script" ]; then
263		netstat -tn |awk -F '[:[:space:]]+' '
264			$8 == "ESTABLISHED" && $4 == "'$OCF_RESKEY_ip'" \
265			{printf "%s:%s\t%s:%s\n", $4,$5, $6,$7}' |
266			dd of="$statefile".new conv=fsync status=none &&
267			mv "$statefile".new "$statefile"
268	else
269		netstat -tn |awk -F '[:[:space:]]+' '
270			$8 == "ESTABLISHED" && $4 == "'$OCF_RESKEY_ip'" \
271			{printf "%s:%s\t%s:%s\n", $4,$5, $6,$7}' \
272			> $statefile
273		$OCF_RESKEY_sync_script $statefile > /dev/null 2>&1 &
274	fi
275}
276
277tickle_remote()
278{
279	[ -z "$OCF_RESKEY_tickle_dir" ] && return
280	echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle
281	f=$OCF_RESKEY_tickle_dir/$OCF_RESKEY_ip
282	[ -r $f ] || return
283	$TICKLETCP -n 3 < $f
284}
285
286tickle_local()
287{
288	[ -z "$OCF_RESKEY_tickle_dir" ] && return
289	f=$OCF_RESKEY_tickle_dir/$OCF_RESKEY_ip
290	[ -r $f ] || return
291
292	checkcmd="netstat -tn"
293	if ! have_binary "netstat"; then
294		checkcmd="ss -Htn"
295	fi
296
297	# swap "local" and "remote" address,
298	# so we tickle ourselves.
299	# We set up a REJECT with tcp-reset before we do so, so we get rid of
300	# the no longer wanted potentially long lived "ESTABLISHED" connection
301	# entries on the IP we are going to delet in a sec.  These would get in
302	# the way if we switch-over and then switch-back in quick succession.
303	local i
304	awk '{ print $2, $1; }' $f | $TICKLETCP
305	$checkcmd | grep -Fw $OCF_RESKEY_ip || return
306	for i in 0.1 0.5 1 2 4 ; do
307		sleep $i
308		awk '{ print $2, $1; }' $f | $TICKLETCP
309		$checkcmd | grep -Fw $OCF_RESKEY_ip || break
310	done
311}
312
313SayActive()
314{
315  echo "$CMD DROP rule for INPUT chain [$*]  is running (OK)"
316}
317
318SayConsideredActive()
319{
320  echo "$CMD DROP rule for INPUT chain [$*] considered to be running (OK)"
321}
322
323SayInactive()
324{
325  echo "$CMD DROP rule for INPUT chain [$*] is inactive"
326}
327
328#IptablesStatus  {udp|tcp} portno,portno ip {block|unblock}
329IptablesStatus() {
330    local rc
331    rc=$OCF_ERR_GENERIC
332    activewords="$CMD $1 $2 is running (OK)"
333    if chain_isactive "$1" "$2" "$3"; then
334	case $4 in
335	    block)
336		SayActive $*
337		rc=$OCF_SUCCESS
338		;;
339	    *)
340 		SayInactive $*
341		rc=$OCF_NOT_RUNNING
342		;;
343	esac
344    else
345	case $4 in
346	    block)
347		if ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" status; then
348		    SayConsideredActive $*
349		    rc=$OCF_SUCCESS
350		else
351		    SayInactive $*
352		    rc=$OCF_NOT_RUNNING
353		fi
354		;;
355
356	    *)
357		if ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" status; then
358			SayActive $*
359			#This is only run on real monitor events.
360			save_tcp_connections
361			rc=$OCF_SUCCESS
362		else
363			SayInactive $*
364			rc=$OCF_NOT_RUNNING
365		fi
366		;;
367	esac
368    fi
369
370    return $rc
371}
372
373#IptablesBLOCK  {udp|tcp} portno,portno ip
374IptablesBLOCK()
375{
376  local rc=0
377  local try_reset=false
378  if	[ "$1/$4/$__OCF_ACTION" = tcp/unblock/stop ] &&
379	ocf_is_true $reset_local_on_unblock_stop
380  then
381	try_reset=true
382  fi
383  if
384    chain_isactive "$1" "$2" "$3"
385  then
386    : OK -- chain already active
387  else
388    if $try_reset ; then
389      $IPTABLES $wait -I OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset
390      tickle_local
391    fi
392    $IPTABLES $wait -I INPUT -p "$1" -d "$3" -m multiport --dports "$2" -j DROP
393    rc=$?
394    if $try_reset ; then
395      $IPTABLES $wait -D OUTPUT -p "$1" -s "$3" -m multiport --sports "$2" -j REJECT --reject-with tcp-reset
396    fi
397  fi
398
399  return $rc
400}
401
402#IptablesUNBLOCK  {udp|tcp} portno,portno ip
403IptablesUNBLOCK()
404{
405  if
406    chain_isactive "$1" "$2" "$3"
407  then
408    $IPTABLES $wait -D INPUT -p "$1" -d "$3" -m multiport --dports "$2" -j DROP
409  else
410    : Chain Not active
411  fi
412
413  return $?
414}
415
416#IptablesStart  {udp|tcp} portno,portno ip {block|unblock}
417IptablesStart()
418{
419  ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" start
420  case $4 in
421    block)	IptablesBLOCK "$@";;
422    unblock)
423		IptablesUNBLOCK "$@"
424		rc=$?
425		tickle_remote
426		#ignore run_tickle_tcp exit code!
427		return $rc
428		;;
429    *)		usage; return 1;
430  esac
431
432  return $?
433}
434
435#IptablesStop  {udp|tcp} portno,portno ip {block|unblock}
436IptablesStop()
437{
438  ha_pseudo_resource "${OCF_RESOURCE_INSTANCE}" stop
439  case $4 in
440    block)	IptablesUNBLOCK "$@";;
441    unblock)
442		save_tcp_connections
443		IptablesBLOCK "$@"
444		;;
445    *)		usage; return 1;;
446  esac
447
448  return $?
449}
450
451#
452#	Check if the port is valid, this function code is not decent, but works
453#
454CheckPort() {
455#	Examples of valid port: "1080", "1", "0080"
456#	Examples of invalid port: "1080bad", "0", "0000", ""
457  echo $1 |egrep -qx '[0-9]+(:[0-9]+)?(,[0-9]+(:[0-9]+)?)*'
458}
459
460IptablesValidateAll()
461{
462  check_binary $IPTABLES
463  case $protocol in
464    tcp|udp)
465	;;
466    *)
467	ocf_log err "Invalid protocol $protocol!"
468	exit $OCF_ERR_CONFIGURED
469	;;
470  esac
471
472  if CheckPort "$portno"; then
473	:
474  else
475	ocf_log err "Invalid port number $portno!"
476	exit $OCF_ERR_CONFIGURED
477  fi
478
479  if [ -n "$OCF_RESKEY_tickle_dir" ]; then
480	if [ x"$action" != x"unblock" ]; then
481		ocf_log err "Tickles are only useful with action=unblock!"
482		exit $OCF_ERR_CONFIGURED
483	fi
484	if [ ! -d "$OCF_RESKEY_tickle_dir" ]; then
485		ocf_log err "The tickle dir doesn't exist!"
486		exit $OCF_ERR_INSTALLED
487	fi
488  fi
489
490  case $action in
491    block|unblock)
492	;;
493    *)
494	ocf_log err "Invalid action $action!"
495	exit $OCF_ERR_CONFIGURED
496	;;
497  esac
498
499  if ocf_is_true $reset_local_on_unblock_stop; then
500	if [ $action != unblock ] ; then
501		ocf_log err "reset_local_on_unblock_stop is only relevant with action=unblock"
502		exit $OCF_ERR_CONFIGURED
503	fi
504	if [ -z $OCF_RESKEY_tickle_dir ] ; then
505		ocf_log warn "reset_local_on_unblock_stop works best with tickle_dir enabled as well"
506	fi
507  fi
508
509  return $OCF_SUCCESS
510}
511
512if
513  ( [ $# -ne 1 ] )
514then
515  usage
516  exit $OCF_ERR_ARGS
517fi
518
519case $1 in
520  meta-data)		meta_data
521			exit $OCF_SUCCESS
522			;;
523
524  usage)		usage
525			exit $OCF_SUCCESS
526			;;
527  *)			;;
528esac
529
530if [ -z "$OCF_RESKEY_protocol" ]; then
531  ocf_log err "Please set OCF_RESKEY_protocol"
532  exit $OCF_ERR_CONFIGURED
533fi
534
535if [ -z "$OCF_RESKEY_portno" ]; then
536  ocf_log err "Please set OCF_RESKEY_portno"
537  exit $OCF_ERR_CONFIGURED
538fi
539
540if [ -z "$OCF_RESKEY_action" ]; then
541  ocf_log err "Please set OCF_RESKEY_action"
542  exit $OCF_ERR_CONFIGURED
543fi
544
545# iptables v1.4.20+ is required to use -w (wait)
546version=$(iptables -V | awk -F ' v' '{print $NF}')
547ocf_version_cmp "$version" "1.4.19.1"
548if [ "$?" -eq "2" ]; then
549    wait="-w"
550else
551    wait=""
552fi
553
554protocol=$OCF_RESKEY_protocol
555portno=$OCF_RESKEY_portno
556action=$OCF_RESKEY_action
557ip=$OCF_RESKEY_ip
558reset_local_on_unblock_stop=$OCF_RESKEY_reset_local_on_unblock_stop
559
560case $1 in
561  start)
562			IptablesStart $protocol $portno $ip $action
563			;;
564
565  stop)
566			IptablesStop $protocol $portno $ip $action
567			;;
568
569  status|monitor)
570			IptablesStatus $protocol $portno $ip $action
571			;;
572
573  validate-all)
574			IptablesValidateAll
575			;;
576
577  *)			usage
578			exit $OCF_ERR_UNIMPLEMENTED
579			;;
580esac
581
582exit $?
583