Repository: fail2ban/fail2ban

Issue:
ip6tables errors on systems not supporting ipv6
### Environment:

- Fail2Ban version (including any possible distribution suffixes): 0.10.0a1-r0
- OS, including release name/version: Alpine Linux 3.5 based docker container on slackware based host OS (Unraid 6.3.2)
- [X] Fail2Ban installed via OS/distribution mechanisms (through alpine's package manager, 3.5 repo)
- [X] You have not applied any additional foreign patches to the codebase
- [ ] Some customizations were done to the configuration (provide details below is so)

### The issue:

The host OS does not support ipv6. The default iptables-common.conf has the ipv6 section enabled so when the jails are starting, fail2ban attempts to use ip6tables and it fails. 

#### Steps to reproduce

Install the 0.10.0 version on a host OS that doesn't support ipv6 and enable a jail that uses an iptables-common based action

#### Expected behavior

Fail2ban could detect whether ipv6 is supported and enable it if so, or the ipv6 could be enabled via a local file rather than being on by default

#### Observed behavior

Errors in the log

#### Any additional information

I realize one is supposed to override conf files via local files. However, how is one supposed to override/disable the ipv6 section in iptables-common.conf via a local file? (out of curiosity):
``` 
[Init?family=inet6]

# Option:  blocktype (ipv6)
# Note:    This is what the action does with rules. This can be any jump target
#          as per the iptables man page (section 8). Common values are DROP
#          REJECT, REJECT --reject-with icmp6-port-unreachable
# Values:  STRING
blocktype = REJECT --reject-with icmp6-port-unreachable

# Option:  iptables (ipv6)
# Notes.:  Actual command to be executed, including common to all calls options
# Values:  STRING
iptables = ip6tables <lockingopt>
```
### Configuration, dump and another helpful excerpts

#### Any customizations done to /etc/fail2ban/ configuration
```
```

#### Relevant parts of /var/log/fail2ban.log file:

```
2017-03-28 21:06:46,579 fail2ban.utils          [262]: ERROR   ip6tables -w -N f2b-nginx-http-auth
ip6tables -w -A f2b-nginx-http-auth -j RETURN
ip6tables -w -I INPUT -p tcp -m multiport --dports http,https -j f2b-nginx-http-auth -- stderr:
2017-03-28 21:06:46,580 fail2ban.utils          [262]: ERROR    -- stderr: "modprobe: can't change directory to '/lib/modules': No such file or directory"
2017-03-28 21:06:46,580 fail2ban.utils          [262]: ERROR    -- stderr: "ip6tables v1.6.0: can't initialize ip6tables table `filter': Table does not exist (do you need to insmod?)"
2017-03-28 21:06:46,580 fail2ban.utils          [262]: ERROR    -- stderr: 'Perhaps ip6tables or your kernel needs to be upgraded.'
2017-03-28 21:06:46,580 fail2ban.utils          [262]: ERROR    -- stderr: "modprobe: can't change directory to '/lib/modules': No such file or directory"
2017-03-28 21:06:46,580 fail2ban.utils          [262]: ERROR    -- stderr: "ip6tables v1.6.0: can't initialize ip6tables table `filter': Table does not exist (do you need to insmod?)"
2017-03-28 21:06:46,580 fail2ban.utils          [262]: ERROR    -- stderr: 'Perhaps ip6tables or your kernel needs to be upgraded.'
2017-03-28 21:06:46,580 fail2ban.utils          [262]: ERROR    -- stderr: 'Could not open socket to kernel: Address family not supported by protocol'
2017-03-28 21:06:46,580 fail2ban.utils          [262]: ERROR   ip6tables -w -N f2b-nginx-http-auth
ip6tables -w -A f2b-nginx-http-auth -j RETURN
ip6tables -w -I INPUT -p tcp -m multiport --dports http,https -j f2b-nginx-http-auth -- returned 1
2017-03-28 21:06:46,580 fail2ban.actions        [262]: ERROR   Failed to start jail 'nginx-http-auth' action 'iptables-multiport': Error starting action Jail('nginx-http-auth')/iptables-multiport
```

#### Relevant lines from monitored log files in question:

```
```


Discussion:
confirmed, the solution follows

Resolving patch:
diff --git a/fail2ban/client/actionreader.py b/fail2ban/client/actionreader.py
--- a/fail2ban/client/actionreader.py
+++ b/fail2ban/client/actionreader.py
@@ -38,6 +38,7 @@ class ActionReader(DefinitionInitConfigReader):
 
 	_configOpts = {
 		"actionstart": ["string", None],
+		"actionstart_on_demand": ["string", None],
 		"actionstop": ["string", None],
 		"actionreload": ["string", None],
 		"actioncheck": ["string", None],
@@ -73,8 +74,10 @@ def convert(self):
 		opts = self.getCombined(
 			ignore=CommandAction._escapedTags | set(('timeout', 'bantime')))
 		# type-convert only after combined (otherwise boolean converting prevents substitution):
-		if opts.get('norestored'):
-			opts['norestored'] = self._convert_to_boolean(opts['norestored'])
+		for o in ('norestored', 'actionstart_on_demand'):
+			if opts.get(o):
+				opts[o] = self._convert_to_boolean(opts[o])
+		
 		# stream-convert:
 		head = ["set", self._jailName]
 		stream = list()
diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py
--- a/fail2ban/server/action.py
+++ b/fail2ban/server/action.py
@@ -50,6 +50,8 @@
 # capture groups from filter for map to ticket data:
 FCUSTAG_CRE = re.compile(r'<F-([A-Z0-9_\-]+)>'); # currently uppercase only
 
+CONDITIONAL_FAM_RE = re.compile(r"^(\w+)\?(family)=")
+
 # New line, space
 ADD_REPL_TAGS = {
   "br": "\n", 
@@ -201,17 +203,17 @@ def __init__(self, jail, name):
 		self._name = name
 		self._logSys = getLogger("fail2ban.%s" % self.__class__.__name__)
 
-	def start(self):
+	def start(self): # pragma: no cover - abstract
 		"""Executed when the jail/action is started.
 		"""
 		pass
 
-	def stop(self):
+	def stop(self): # pragma: no cover - abstract
 		"""Executed when the jail/action is stopped.
 		"""
 		pass
 
-	def ban(self, aInfo):
+	def ban(self, aInfo): # pragma: no cover - abstract
 		"""Executed when a ban occurs.
 
 		Parameters
@@ -222,7 +224,7 @@ def ban(self, aInfo):
 		"""
 		pass
 
-	def unban(self, aInfo):
+	def unban(self, aInfo): # pragma: no cover - abstract
 		"""Executed when a ban expires.
 
 		Parameters
@@ -290,6 +292,7 @@ def __init__(self, jail, name):
 		super(CommandAction, self).__init__(jail, name)
 		self.__init = 1
 		self.__properties = None
+		self.__started = {}
 		self.__substCache = {}
 		self.clearAllParams()
 		self._logSys.debug("Created %s" % self.__class__)
@@ -342,7 +345,11 @@ def _properties(self):
 	def _substCache(self):
 		return self.__substCache
 
-	def _executeOperation(self, tag, operation):
+	def _getOperation(self, tag, family):
+		return self.replaceTag(tag, self._properties,
+			conditional=('family=' + family), cache=self.__substCache)
+
+	def _executeOperation(self, tag, operation, family=[]):
 		"""Executes the operation commands (like "actionstart", "actionstop", etc).
 
 		Replace the tags in the action command with actions properties
@@ -352,14 +359,14 @@ def _executeOperation(self, tag, operation):
 		res = True
 		try:
 			# common (resp. ipv4):
-			startCmd = self.replaceTag(tag, self._properties, 
-				conditional='family=inet4', cache=self.__substCache)
-			if startCmd:
-				res &= self.executeCmd(startCmd, self.timeout)
+			startCmd = None
+			if not family or 'inet4' in family:
+				startCmd = self._getOperation(tag, 'inet4')
+				if startCmd:
+					res &= self.executeCmd(startCmd, self.timeout)
 			# start ipv6 actions if available:
-			if allowed_ipv6:
-				startCmd6 = self.replaceTag(tag, self._properties, 
-					conditional='family=inet6', cache=self.__substCache)
+			if allowed_ipv6 and (not family or 'inet6' in family):
+				startCmd6 = self._getOperation(tag, 'inet6')
 				if startCmd6 and startCmd6 != startCmd:
 					res &= self.executeCmd(startCmd6, self.timeout)
 			if not res:
@@ -367,13 +374,34 @@ def _executeOperation(self, tag, operation):
 		except ValueError as e:
 			raise RuntimeError("Error %s action %s/%s: %r" % (operation, self._jail, self._name, e))
 
-	def start(self):
+	COND_FAMILIES = {'inet4':1, 'inet6':1}
+
+	@property
+	def _startOnDemand(self):
+		"""Checks the action depends on family (conditional)"""
+		v = self._properties.get('actionstart_on_demand')
+		if v is None:
+			v = False
+			for n in self._properties:
+				if CONDITIONAL_FAM_RE.match(n):
+					v = True
+					break
+		self._properties['actionstart_on_demand'] = v
+		return v
+
+	def start(self, family=[]):
 		"""Executes the "actionstart" command.
 
 		Replace the tags in the action command with actions properties
 		and executes the resulting command.
 		"""
-		return self._executeOperation('<actionstart>', 'starting')
+		if not family:
+			# check the action depends on family (conditional):
+			if self._startOnDemand:
+				return True
+		elif self.__started.get(family): # pragma: no cover - normally unreachable
+			return True
+		return self._executeOperation('<actionstart>', 'starting', family=family)
 
 	def ban(self, aInfo):
 		"""Executes the "actionban" command.
@@ -387,6 +415,20 @@ def ban(self, aInfo):
 			Dictionary which includes information in relation to
 			the ban.
 		"""
+		# if we should start the action on demand (conditional by family):
+		if self._startOnDemand:
+			family = aInfo.get('family')
+			if not self.__started.get(family):
+				self.start(family)
+				self.__started[family] = 1
+				# mark also another families as "started" (-1), if they are equal
+				# (on demand, but the same for ipv4 and ipv6):
+				cmd = self._getOperation('<actionstart>', family)
+				for f in CommandAction.COND_FAMILIES:
+					if f != family and not self.__started.get(f):
+						if cmd == self._getOperation('<actionstart>', f):
+							self.__started[f] = -1
+		# ban:
 		if not self._processCmd('<actionban>', aInfo):
 			raise RuntimeError("Error banning %(ip)s" % aInfo)
 
@@ -411,7 +453,16 @@ def stop(self):
 		Replaces the tags in the action command with actions properties
 		and executes the resulting command.
 		"""
-		return self._executeOperation('<actionstop>', 'stopping')
+		family = []
+		# cumulate started families, if started on demand (conditional):
+		if self._startOnDemand:
+			for f in CommandAction.COND_FAMILIES:
+				if self.__started.get(f) == 1: # only real started:
+					family.append(f)
+					self.__started[f] = 0
+			# if no started (on demand) actions:
+			if not family: return True
+		return self._executeOperation('<actionstop>', 'stopping', family=family)
 
 	def reload(self, **kwargs):
 		"""Executes the "actionreload" command.


Test patch:
diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py
--- a/fail2ban/tests/servertestcase.py
+++ b/fail2ban/tests/servertestcase.py
@@ -1074,16 +1074,16 @@ def _testExecActions(self, server):
 				action.start()
 				# test ban ip4 :
 				logSys.debug('# === ban-ipv4 ==='); self.pruneLog()
-				action.ban({'ip': IPAddr('192.0.2.1')})
+				action.ban({'ip': IPAddr('192.0.2.1'), 'family': 'inet4'})
 				# test unban ip4 :
 				logSys.debug('# === unban ipv4 ==='); self.pruneLog()
-				action.unban({'ip': IPAddr('192.0.2.1')})
+				action.unban({'ip': IPAddr('192.0.2.1'), 'family': 'inet4'})
 				# test ban ip6 :
 				logSys.debug('# === ban ipv6 ==='); self.pruneLog()
-				action.ban({'ip': IPAddr('2001:DB8::')})
+				action.ban({'ip': IPAddr('2001:DB8::'), 'family': 'inet6'})
 				# test unban ip6 :
 				logSys.debug('# === unban ipv6 ==='); self.pruneLog()
-				action.unban({'ip': IPAddr('2001:DB8::')})
+				action.unban({'ip': IPAddr('2001:DB8::'), 'family': 'inet6'})
 				# test stop :
 				logSys.debug('# === stop ==='); self.pruneLog()
 				action.stop()
@@ -1185,10 +1185,12 @@ def testCheckStockCommandActions(self):
 				# iptables-multiport --
 				('j-w-iptables-mp', 'iptables-multiport[name=%(__name__)s, bantime="10m", port="http,https", protocol="tcp", chain="INPUT"]', {
 					'ip4': ('`iptables ', 'icmp-port-unreachable'), 'ip6': ('`ip6tables ', 'icmp6-port-unreachable'),
-					'start': (
+					'ip4-start': (
 						"`iptables -w -N f2b-j-w-iptables-mp`",
 						"`iptables -w -A f2b-j-w-iptables-mp -j RETURN`",
 						"`iptables -w -I INPUT -p tcp -m multiport --dports http,https -j f2b-j-w-iptables-mp`",
+					), 
+					'ip6-start': (
 						"`ip6tables -w -N f2b-j-w-iptables-mp`",
 						"`ip6tables -w -A f2b-j-w-iptables-mp -j RETURN`",
 						"`ip6tables -w -I INPUT -p tcp -m multiport --dports http,https -j f2b-j-w-iptables-mp`",
@@ -1223,10 +1225,12 @@ def testCheckStockCommandActions(self):
 				# iptables-allports --
 				('j-w-iptables-ap', 'iptables-allports[name=%(__name__)s, bantime="10m", protocol="tcp", chain="INPUT"]', {
 					'ip4': ('`iptables ', 'icmp-port-unreachable'), 'ip6': ('`ip6tables ', 'icmp6-port-unreachable'),
-					'start': (
+					'ip4-start': (
 						"`iptables -w -N f2b-j-w-iptables-ap`",
 						"`iptables -w -A f2b-j-w-iptables-ap -j RETURN`",
 						"`iptables -w -I INPUT -p tcp -j f2b-j-w-iptables-ap`",
+					), 
+					'ip6-start': (
 						"`ip6tables -w -N f2b-j-w-iptables-ap`",
 						"`ip6tables -w -A f2b-j-w-iptables-ap -j RETURN`",
 						"`ip6tables -w -I INPUT -p tcp -j f2b-j-w-iptables-ap`",
@@ -1261,9 +1265,11 @@ def testCheckStockCommandActions(self):
 				# iptables-ipset-proto6 --
 				('j-w-iptables-ipset', 'iptables-ipset-proto6[name=%(__name__)s, bantime="10m", port="http", protocol="tcp", chain="INPUT"]', {
 					'ip4': (' f2b-j-w-iptables-ipset ',), 'ip6': (' f2b-j-w-iptables-ipset6 ',),
-					'start': (
+					'ip4-start': (
 						"`ipset create f2b-j-w-iptables-ipset hash:ip timeout 600`",
 						"`iptables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset src -j REJECT --reject-with icmp-port-unreachable`",
+					), 
+					'ip6-start': (
 						"`ipset create f2b-j-w-iptables-ipset6 hash:ip timeout 600 family inet6`",
 						"`ip6tables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`",
 					),
@@ -1293,9 +1299,11 @@ def testCheckStockCommandActions(self):
 				# iptables-ipset-proto6-allports --
 				('j-w-iptables-ipset-ap', 'iptables-ipset-proto6-allports[name=%(__name__)s, bantime="10m", chain="INPUT"]', {
 					'ip4': (' f2b-j-w-iptables-ipset-ap ',), 'ip6': (' f2b-j-w-iptables-ipset-ap6 ',),
-					'start': (
+					'ip4-start': (
 						"`ipset create f2b-j-w-iptables-ipset-ap hash:ip timeout 600`",
 						"`iptables -w -I INPUT -m set --match-set f2b-j-w-iptables-ipset-ap src -j REJECT --reject-with icmp-port-unreachable`",
+					), 
+					'ip6-start': (
 						"`ipset create f2b-j-w-iptables-ipset-ap6 hash:ip timeout 600 family inet6`",
 						"`ip6tables -w -I INPUT -m set --match-set f2b-j-w-iptables-ipset-ap6 src -j REJECT --reject-with icmp6-port-unreachable`",
 					),
@@ -1325,10 +1333,12 @@ def testCheckStockCommandActions(self):
 				# iptables --
 				('j-w-iptables', 'iptables[name=%(__name__)s, bantime="10m", port="http", protocol="tcp", chain="INPUT"]', {
 					'ip4': ('`iptables ', 'icmp-port-unreachable'), 'ip6': ('`ip6tables ', 'icmp6-port-unreachable'),
-					'start': (
+					'ip4-start': (
 						"`iptables -w -N f2b-j-w-iptables`",
 						"`iptables -w -A f2b-j-w-iptables -j RETURN`",
 						"`iptables -w -I INPUT -p tcp --dport http -j f2b-j-w-iptables`",
+					), 
+					'ip6-start': (
 						"`ip6tables -w -N f2b-j-w-iptables`",
 						"`ip6tables -w -A f2b-j-w-iptables -j RETURN`",
 						"`ip6tables -w -I INPUT -p tcp --dport http -j f2b-j-w-iptables`",
@@ -1363,10 +1373,12 @@ def testCheckStockCommandActions(self):
 				# iptables-new --
 				('j-w-iptables-new', 'iptables-new[name=%(__name__)s, bantime="10m", port="http", protocol="tcp", chain="INPUT"]', {
 					'ip4': ('`iptables ', 'icmp-port-unreachable'), 'ip6': ('`ip6tables ', 'icmp6-port-unreachable'),
-					'start': (
+					'ip4-start': (
 						"`iptables -w -N f2b-j-w-iptables-new`",
 						"`iptables -w -A f2b-j-w-iptables-new -j RETURN`",
 						"`iptables -w -I INPUT -m state --state NEW -p tcp --dport http -j f2b-j-w-iptables-new`",
+					), 
+					'ip6-start': (
 						"`ip6tables -w -N f2b-j-w-iptables-new`",
 						"`ip6tables -w -A f2b-j-w-iptables-new -j RETURN`",
 						"`ip6tables -w -I INPUT -m state --state NEW -p tcp --dport http -j f2b-j-w-iptables-new`",
@@ -1401,8 +1413,10 @@ def testCheckStockCommandActions(self):
 				# iptables-xt_recent-echo --
 				('j-w-iptables-xtre', 'iptables-xt_recent-echo[name=%(__name__)s, bantime="10m", chain="INPUT"]', {
 					'ip4': ('`iptables ', '/f2b-j-w-iptables-xtre`'), 'ip6': ('`ip6tables ', '/f2b-j-w-iptables-xtre6`'),
-					'start': (
+					'ip4-start': (
 						"`if [ `id -u` -eq 0 ];then iptables -w -I INPUT -m recent --update --seconds 3600 --name f2b-j-w-iptables-xtre -j REJECT --reject-with icmp-port-unreachable;fi`",
+					), 
+					'ip6-start': (
 						"`if [ `id -u` -eq 0 ];then ip6tables -w -I INPUT -m recent --update --seconds 3600 --name f2b-j-w-iptables-xtre6 -j REJECT --reject-with icmp6-port-unreachable;fi`",
 					),
 					'stop': (
@@ -1431,7 +1445,7 @@ def testCheckStockCommandActions(self):
 					),
 				}),
 				# pf default -- multiport on default port (tag <port> set in jail.conf, but not in this test case)
-				('j-w-pf', 'pf[name=%(__name__)s]', {
+				('j-w-pf', 'pf[name=%(__name__)s, actionstart_on_demand=false]', {
 					'ip4': (), 'ip6': (),
 					'start': (
 						'`echo "table <f2b-j-w-pf> persist counters" | pfctl -f-`',
@@ -1468,13 +1482,14 @@ def testCheckStockCommandActions(self):
 					'ip6-ban':   ("`pfctl -t f2b-j-w-pf-mp -T add 2001:db8::`",),
 					'ip6-unban': ("`pfctl -t f2b-j-w-pf-mp -T delete 2001:db8::`",),
 				}),
-				# pf allports --
-				('j-w-pf-ap', 'pf[actiontype=<allports>][name=%(__name__)s]', {
+				# pf allports -- test additionally "actionstart_on_demand" was set to true
+				('j-w-pf-ap', 'pf[actiontype=<allports>, actionstart_on_demand=true][name=%(__name__)s]', {
 					'ip4': (), 'ip6': (),
-					'start': (
+					'ip4-start': (
 						'`echo "table <f2b-j-w-pf-ap> persist counters" | pfctl -f-`',
 						'`echo "block proto tcp from <f2b-j-w-pf-ap> to any" | pfctl -f-`',
 					),
+					'ip6-start': (), # the same as ipv4
 					'stop': (
 						'`pfctl -sr 2>/dev/null | grep -v f2b-j-w-pf-ap | pfctl -f-`',
 						'`pfctl -t f2b-j-w-pf-ap -T flush`',
@@ -1490,10 +1505,12 @@ def testCheckStockCommandActions(self):
 				# firewallcmd-multiport --
 				('j-w-fwcmd-mp', 'firewallcmd-multiport[name=%(__name__)s, bantime="10m", port="http,https", protocol="tcp", chain="INPUT"]', {
 					'ip4': (' ipv4 ', 'icmp-port-unreachable'), 'ip6': (' ipv6 ', 'icmp6-port-unreachable'),
-					'start': (
+					'ip4-start': (
 						"`firewall-cmd --direct --add-chain ipv4 filter f2b-j-w-fwcmd-mp`",
 						"`firewall-cmd --direct --add-rule ipv4 filter f2b-j-w-fwcmd-mp 1000 -j RETURN`",
 						"`firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -m conntrack --ctstate NEW -p tcp -m multiport --dports http,https -j f2b-j-w-fwcmd-mp`",
+					), 
+					'ip6-start': (
 						"`firewall-cmd --direct --add-chain ipv6 filter f2b-j-w-fwcmd-mp`",
 						"`firewall-cmd --direct --add-rule ipv6 filter f2b-j-w-fwcmd-mp 1000 -j RETURN`",
 						"`firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -m conntrack --ctstate NEW -p tcp -m multiport --dports http,https -j f2b-j-w-fwcmd-mp`",
@@ -1528,10 +1545,12 @@ def testCheckStockCommandActions(self):
 				# firewallcmd-allports --
 				('j-w-fwcmd-ap', 'firewallcmd-allports[name=%(__name__)s, bantime="10m", protocol="tcp", chain="INPUT"]', {
 					'ip4': (' ipv4 ', 'icmp-port-unreachable'), 'ip6': (' ipv6 ', 'icmp6-port-unreachable'),
-					'start': (
+					'ip4-start': (
 						"`firewall-cmd --direct --add-chain ipv4 filter f2b-j-w-fwcmd-ap`",
 						"`firewall-cmd --direct --add-rule ipv4 filter f2b-j-w-fwcmd-ap 1000 -j RETURN`",
 						"`firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -j f2b-j-w-fwcmd-ap`",
+					), 
+					'ip6-start': (
 						"`firewall-cmd --direct --add-chain ipv6 filter f2b-j-w-fwcmd-ap`",
 						"`firewall-cmd --direct --add-rule ipv6 filter f2b-j-w-fwcmd-ap 1000 -j RETURN`",
 						"`firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -j f2b-j-w-fwcmd-ap`",
@@ -1566,9 +1585,11 @@ def testCheckStockCommandActions(self):
 				# firewallcmd-ipset --
 				('j-w-fwcmd-ipset', 'firewallcmd-ipset[name=%(__name__)s, bantime="10m", port="http", protocol="tcp", chain="INPUT"]', {
 					'ip4': (' f2b-j-w-fwcmd-ipset ',), 'ip6': (' f2b-j-w-fwcmd-ipset6 ',),
-					'start': (
+					'ip4-start': (
 						"`ipset create f2b-j-w-fwcmd-ipset hash:ip timeout 600`",
 						"`firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp -m multiport --dports http -m set --match-set f2b-j-w-fwcmd-ipset src -j REJECT --reject-with icmp-port-unreachable`",
+					), 
+					'ip6-start': (
 						"`ipset create f2b-j-w-fwcmd-ipset6 hash:ip timeout 600`",
 						"`firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp -m multiport --dports http -m set --match-set f2b-j-w-fwcmd-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`",
 					),
@@ -1614,6 +1635,10 @@ def testCheckStockCommandActions(self):
 
 			jails = server._Server__jails
 
+			tickets = {
+				'ip4': BanTicket('192.0.2.1'),
+				'ip6': BanTicket('2001:DB8::'),
+			}
 			for jail, act, tests in testJailsActions:
 				# print(jail, jails[jail])
 				for a in jails[jail].actions:
@@ -1627,25 +1652,36 @@ def testCheckStockCommandActions(self):
 					# test start :
 					self.pruneLog('# === start ===')
 					action.start()
-					self.assertLogged(*tests['start'], all=True)
+					if tests.get('start'):
+						self.assertLogged(*tests['start'], all=True)
+					else:
+						self.assertNotLogged(*tests['ip4-start']+tests['ip6-start'], all=True)
+					ainfo = {
+						'ip4': _actions.Actions.ActionInfo(tickets['ip4'], jails[jail]),
+						'ip6': _actions.Actions.ActionInfo(tickets['ip6'], jails[jail]),
+					}
 					# test ban ip4 :
 					self.pruneLog('# === ban-ipv4 ===')
-					action.ban({'ip': IPAddr('192.0.2.1')})
+					action.ban(ainfo['ip4'])
+					if tests.get('ip4-start'): self.assertLogged(*tests['ip4-start'], all=True)
+					if tests.get('ip6-start'): self.assertNotLogged(*tests['ip6-start'], all=True)
 					self.assertLogged(*tests['ip4-check']+tests['ip4-ban'], all=True)
 					self.assertNotLogged(*tests['ip6'], all=True)
 					# test unban ip4 :
 					self.pruneLog('# === unban ipv4 ===')
-					action.unban({'ip': IPAddr('192.0.2.1')})
+					action.unban(ainfo['ip4'])
 					self.assertLogged(*tests['ip4-check']+tests['ip4-unban'], all=True)
 					self.assertNotLogged(*tests['ip6'], all=True)
 					# test ban ip6 :
 					self.pruneLog('# === ban ipv6 ===')
-					action.ban({'ip': IPAddr('2001:DB8::')})
+					action.ban(ainfo['ip6'])
+					if tests.get('ip6-start'): self.assertLogged(*tests['ip6-start'], all=True)
+					if tests.get('ip4-start'): self.assertNotLogged(*tests['ip4-start'], all=True)
 					self.assertLogged(*tests['ip6-check']+tests['ip6-ban'], all=True)
 					self.assertNotLogged(*tests['ip4'], all=True)
 					# test unban ip6 :
 					self.pruneLog('# === unban ipv6 ===')
-					action.unban({'ip': IPAddr('2001:DB8::')})
+					action.unban(ainfo['ip6'])
 					self.assertLogged(*tests['ip6-check']+tests['ip6-unban'], all=True)
 					self.assertNotLogged(*tests['ip4'], all=True)
 					# test stop :