mirror of
https://github.com/saltstack/salt.git
synced 2025-04-16 09:40:20 +00:00
815 lines
18 KiB
Python
815 lines
18 KiB
Python
"""
|
|
Support for ipset
|
|
"""
|
|
|
|
import logging
|
|
|
|
import salt.utils.path
|
|
from salt._compat import ipaddress
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
_IPSET_FAMILIES = {
|
|
"ipv4": "inet",
|
|
"ip4": "inet",
|
|
"ipv6": "inet6",
|
|
"ip6": "inet6",
|
|
}
|
|
|
|
_IPSET_SET_TYPES = {
|
|
"bitmap:ip",
|
|
"bitmap:ip,mac",
|
|
"bitmap:port",
|
|
"hash:ip",
|
|
"hash:mac",
|
|
"hash:ip,port",
|
|
"hash:ip,port,ip",
|
|
"hash:ip,port,net",
|
|
"hash:net",
|
|
"hash:net,net",
|
|
"hash:net,iface",
|
|
"hash:net,port",
|
|
"hash:net,port,net",
|
|
"hash:ip,mark",
|
|
"list:set",
|
|
}
|
|
|
|
|
|
_CREATE_OPTIONS = {
|
|
"bitmap:ip": {"range", "netmask", "timeout", "counters", "comment", "skbinfo"},
|
|
"bitmap:ip,mac": {"range", "timeout", "counters", "comment", "skbinfo"},
|
|
"bitmap:port": {"range", "timeout", "counters", "comment", "skbinfo"},
|
|
"hash:ip": {
|
|
"family",
|
|
"hashsize",
|
|
"maxelem",
|
|
"netmask",
|
|
"timeout",
|
|
"counters",
|
|
"comment",
|
|
"skbinfo",
|
|
},
|
|
"hash:mac": {"hashsize", "maxelem", "timeout", "counters", "comment", "skbinfo"},
|
|
"hash:net": {
|
|
"family",
|
|
"hashsize",
|
|
"maxelem",
|
|
"netmask",
|
|
"timeout",
|
|
"counters",
|
|
"comment",
|
|
"skbinfo",
|
|
},
|
|
"hash:net,net": {
|
|
"family",
|
|
"hashsize",
|
|
"maxelem",
|
|
"netmask",
|
|
"timeout",
|
|
"counters",
|
|
"comment",
|
|
"skbinfo",
|
|
},
|
|
"hash:net,port": {
|
|
"family",
|
|
"hashsize",
|
|
"maxelem",
|
|
"netmask",
|
|
"timeout",
|
|
"counters",
|
|
"comment",
|
|
"skbinfo",
|
|
},
|
|
"hash:net,port,net": {
|
|
"family",
|
|
"hashsize",
|
|
"maxelem",
|
|
"netmask",
|
|
"timeout",
|
|
"counters",
|
|
"comment",
|
|
"skbinfo",
|
|
},
|
|
"hash:ip,port,ip": {
|
|
"family",
|
|
"hashsize",
|
|
"maxelem",
|
|
"netmask",
|
|
"timeout",
|
|
"counters",
|
|
"comment",
|
|
"skbinfo",
|
|
},
|
|
"hash:ip,port,net": {
|
|
"family",
|
|
"hashsize",
|
|
"maxelem",
|
|
"netmask",
|
|
"timeout",
|
|
"counters",
|
|
"comment",
|
|
"skbinfo",
|
|
},
|
|
"hash:ip,port": {
|
|
"family",
|
|
"hashsize",
|
|
"maxelem",
|
|
"netmask",
|
|
"timeout",
|
|
"counters",
|
|
"comment",
|
|
"skbinfo",
|
|
},
|
|
"hash:ip,mark": {
|
|
"family",
|
|
"markmask",
|
|
"hashsize",
|
|
"maxelem",
|
|
"timeout",
|
|
"counters",
|
|
"comment",
|
|
"skbinfo",
|
|
},
|
|
"hash:net,iface": {
|
|
"family",
|
|
"hashsize",
|
|
"maxelem",
|
|
"netmask",
|
|
"timeout",
|
|
"counters",
|
|
"comment",
|
|
"skbinfo",
|
|
},
|
|
"list:set": {"size", "timeout", "counters", "comment"},
|
|
}
|
|
|
|
_CREATE_OPTIONS_WITHOUT_VALUE = {"comment", "counters", "skbinfo"}
|
|
|
|
_CREATE_OPTIONS_REQUIRED = {
|
|
"bitmap:ip": ["range"],
|
|
"bitmap:ip,mac": ["range"],
|
|
"bitmap:port": ["range"],
|
|
"hash:ip": [],
|
|
"hash:mac": [],
|
|
"hash:net": [],
|
|
"hash:net,net": [],
|
|
"hash:ip,port": [],
|
|
"hash:net,port": [],
|
|
"hash:ip,port,ip": [],
|
|
"hash:ip,port,net": [],
|
|
"hash:net,port,net": [],
|
|
"hash:net,iface": [],
|
|
"hash:ip,mark": [],
|
|
"list:set": [],
|
|
}
|
|
|
|
|
|
_ADD_OPTIONS = {
|
|
"bitmap:ip": {"timeout", "packets", "bytes", "skbmark", "skbprio", "skbqueue"},
|
|
"bitmap:ip,mac": {"timeout", "packets", "bytes", "skbmark", "skbprio", "skbqueue"},
|
|
"bitmap:port": {"timeout", "packets", "bytes", "skbmark", "skbprio"},
|
|
"hash:ip": {"timeout", "packets", "bytes", "skbmark", "skbprio", "skbqueue"},
|
|
"hash:mac": {"timeout", "packets", "bytes", "skbmark", "skbprio", "skbqueue"},
|
|
"hash:net": {
|
|
"timeout",
|
|
"nomatch",
|
|
"packets",
|
|
"bytes",
|
|
"skbmark",
|
|
"skbprio",
|
|
"skbqueue",
|
|
},
|
|
"hash:net,net": {
|
|
"timeout",
|
|
"nomatch",
|
|
"packets",
|
|
"bytes",
|
|
"skbmark",
|
|
"skbprio",
|
|
"skbqueue",
|
|
},
|
|
"hash:net,port": {
|
|
"timeout",
|
|
"nomatch",
|
|
"packets",
|
|
"bytes",
|
|
"skbmark",
|
|
"skbprio",
|
|
"skbqueue",
|
|
},
|
|
"hash:net,port,net": {
|
|
"timeout",
|
|
"nomatch",
|
|
"packets",
|
|
"bytes",
|
|
"skbmark",
|
|
"skbprio",
|
|
"skbqueue",
|
|
},
|
|
"hash:ip,port,ip": {
|
|
"timeout",
|
|
"packets",
|
|
"bytes",
|
|
"skbmark",
|
|
"skbprio",
|
|
"skbqueue",
|
|
},
|
|
"hash:ip,port,net": {
|
|
"timeout",
|
|
"nomatch",
|
|
"packets",
|
|
"bytes",
|
|
"skbmark",
|
|
"skbprio",
|
|
"skbqueue",
|
|
},
|
|
"hash:ip,port": {
|
|
"timeout",
|
|
"nomatch",
|
|
"packets",
|
|
"bytes",
|
|
"skbmark",
|
|
"skbprio",
|
|
"skbqueue",
|
|
},
|
|
"hash:net,iface": {
|
|
"timeout",
|
|
"nomatch",
|
|
"packets",
|
|
"bytes",
|
|
"skbmark",
|
|
"skbprio",
|
|
"skbqueue",
|
|
},
|
|
"hash:ip,mark": {"timeout", "packets", "bytes", "skbmark", "skbprio", "skbqueue"},
|
|
"list:set": {"timeout", "packets", "bytes", "skbmark", "skbprio", "skbqueue"},
|
|
}
|
|
|
|
__virtualname__ = "ipset"
|
|
|
|
|
|
def __virtual__():
|
|
"""
|
|
Only load the module if ipset is installed
|
|
"""
|
|
if salt.utils.path.which("ipset"):
|
|
return True
|
|
return (
|
|
False,
|
|
"The ipset execution modules cannot be loaded: ipset binary not in path.",
|
|
)
|
|
|
|
|
|
def _ipset_cmd():
|
|
"""
|
|
Return correct command
|
|
"""
|
|
return salt.utils.path.which("ipset")
|
|
|
|
|
|
def version():
|
|
"""
|
|
Return version from ipset --version
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.version
|
|
|
|
"""
|
|
cmd = [_ipset_cmd(), "--version"]
|
|
out = __salt__["cmd.run"](cmd, python_shell=False).split()
|
|
return out[1]
|
|
|
|
|
|
def new_set(name=None, set_type=None, family="ipv4", comment=False, **kwargs):
|
|
"""
|
|
.. versionadded:: 2014.7.0
|
|
|
|
Create new custom set
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.new_set custom_set list:set
|
|
|
|
salt '*' ipset.new_set custom_set list:set comment=True
|
|
|
|
IPv6:
|
|
salt '*' ipset.new_set custom_set list:set family=ipv6
|
|
"""
|
|
ipset_family = _IPSET_FAMILIES[family]
|
|
if not name:
|
|
return "Error: Set Name needs to be specified"
|
|
|
|
if not set_type:
|
|
return "Error: Set Type needs to be specified"
|
|
|
|
if set_type not in _IPSET_SET_TYPES:
|
|
return "Error: Set Type is invalid"
|
|
|
|
# Check for required arguments
|
|
for item in _CREATE_OPTIONS_REQUIRED[set_type]:
|
|
if item not in kwargs:
|
|
return f"Error: {item} is a required argument"
|
|
|
|
cmd = [_ipset_cmd(), "create", name, set_type]
|
|
|
|
for item in _CREATE_OPTIONS[set_type]:
|
|
if item in kwargs:
|
|
if item in _CREATE_OPTIONS_WITHOUT_VALUE:
|
|
cmd.append(item)
|
|
else:
|
|
cmd.extend([item, kwargs[item]])
|
|
|
|
# Family only valid for certain set types
|
|
if "family" in _CREATE_OPTIONS[set_type]:
|
|
cmd.extend(["family", ipset_family])
|
|
|
|
if comment:
|
|
cmd.append("comment")
|
|
|
|
out = __salt__["cmd.run"](cmd, python_shell=False)
|
|
|
|
if not out:
|
|
out = True
|
|
return out
|
|
|
|
|
|
def delete_set(name=None, family="ipv4"):
|
|
"""
|
|
.. versionadded:: 2014.7.0
|
|
|
|
Delete ipset set.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.delete_set custom_set
|
|
|
|
IPv6:
|
|
salt '*' ipset.delete_set custom_set family=ipv6
|
|
"""
|
|
|
|
if not name:
|
|
return "Error: Set needs to be specified"
|
|
|
|
cmd = [_ipset_cmd(), "destroy", name]
|
|
out = __salt__["cmd.run"](cmd, python_shell=False)
|
|
|
|
if not out:
|
|
out = True
|
|
return out
|
|
|
|
|
|
def rename_set(name=None, new_set=None, family="ipv4"):
|
|
"""
|
|
.. versionadded:: 2014.7.0
|
|
|
|
Delete ipset set.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.rename_set custom_set new_set=new_set_name
|
|
|
|
IPv6:
|
|
salt '*' ipset.rename_set custom_set new_set=new_set_name family=ipv6
|
|
"""
|
|
|
|
if not name:
|
|
return "Error: Set needs to be specified"
|
|
|
|
if not new_set:
|
|
return "Error: New name for set needs to be specified"
|
|
|
|
settype = _find_set_type(name)
|
|
if not settype:
|
|
return "Error: Set does not exist"
|
|
|
|
settype = _find_set_type(new_set)
|
|
if settype:
|
|
return "Error: New Set already exists"
|
|
|
|
cmd = [_ipset_cmd(), "rename", name, new_set]
|
|
out = __salt__["cmd.run"](cmd, python_shell=False)
|
|
|
|
if not out:
|
|
out = True
|
|
return out
|
|
|
|
|
|
def list_sets(family="ipv4"):
|
|
"""
|
|
.. versionadded:: 2014.7.0
|
|
|
|
List all ipset sets.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.list_sets
|
|
|
|
"""
|
|
cmd = [_ipset_cmd(), "list", "-t"]
|
|
out = __salt__["cmd.run"](cmd, python_shell=False)
|
|
|
|
_tmp = out.split("\n")
|
|
|
|
count = 0
|
|
sets = []
|
|
sets.append({})
|
|
for item in _tmp:
|
|
if not item:
|
|
count = count + 1
|
|
sets.append({})
|
|
continue
|
|
key, value = item.split(":", 1)
|
|
sets[count][key] = value[1:]
|
|
return sets
|
|
|
|
|
|
def check_set(name=None, family="ipv4"):
|
|
"""
|
|
Check that given ipset set exists.
|
|
|
|
.. versionadded:: 2014.7.0
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.check_set name
|
|
|
|
"""
|
|
if not name:
|
|
return "Error: Set needs to be specified"
|
|
|
|
setinfo = _find_set_info(name)
|
|
if not setinfo:
|
|
return False
|
|
return True
|
|
|
|
|
|
def add(name=None, entry=None, family="ipv4", **kwargs):
|
|
"""
|
|
Append an entry to the specified set.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.add name 192.168.1.26
|
|
|
|
salt '*' ipset.add name 192.168.0.3,AA:BB:CC:DD:EE:FF
|
|
|
|
"""
|
|
if not name:
|
|
return "Error: Set needs to be specified"
|
|
if not entry:
|
|
return "Error: Entry needs to be specified"
|
|
|
|
setinfo = _find_set_info(name)
|
|
if not setinfo:
|
|
return f"Error: Set {name} does not exist"
|
|
|
|
settype = setinfo["Type"]
|
|
|
|
cmd = [_ipset_cmd(), "add", "-exist", name] + entry.split()
|
|
|
|
if "timeout" in kwargs:
|
|
if "timeout" not in setinfo["Header"]:
|
|
return f"Error: Set {name} not created with timeout support"
|
|
|
|
if "packets" in kwargs or "bytes" in kwargs:
|
|
if "counters" not in setinfo["Header"]:
|
|
return f"Error: Set {name} not created with counters support"
|
|
|
|
if "comment" in kwargs:
|
|
if "comment" not in setinfo["Header"]:
|
|
return f"Error: Set {name} not created with comment support"
|
|
if "comment" not in entry:
|
|
cmd = cmd + ["comment", f"{kwargs['comment']}"]
|
|
|
|
if {"skbmark", "skbprio", "skbqueue"} & set(kwargs.keys()):
|
|
if "skbinfo" not in setinfo["Header"]:
|
|
return f"Error: Set {name} not created with skbinfo support"
|
|
|
|
for item in _ADD_OPTIONS[settype]:
|
|
if item in kwargs:
|
|
cmd.extend([item, kwargs[item]])
|
|
|
|
current_members = _find_set_members(name)
|
|
if entry in current_members:
|
|
return f"Warn: Entry {entry} already exists in set {name}"
|
|
|
|
# Using -exist to ensure entries are updated if the comment changes
|
|
out = __salt__["cmd.run"](cmd, python_shell=False)
|
|
|
|
if not out:
|
|
return "Success"
|
|
return f"Error: {out}"
|
|
|
|
|
|
def delete(name=None, entry=None, family="ipv4", **kwargs):
|
|
"""
|
|
Delete an entry from the specified set.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.delete name 192.168.0.3,AA:BB:CC:DD:EE:FF
|
|
|
|
"""
|
|
if not name:
|
|
return "Error: Set needs to be specified"
|
|
if not entry:
|
|
return "Error: Entry needs to be specified"
|
|
|
|
settype = _find_set_type(name)
|
|
|
|
if not settype:
|
|
return f"Error: Set {name} does not exist"
|
|
|
|
cmd = [_ipset_cmd(), "del", name, entry]
|
|
out = __salt__["cmd.run"](cmd, python_shell=False)
|
|
|
|
if not out:
|
|
return "Success"
|
|
return f"Error: {out}"
|
|
|
|
|
|
def check(name=None, entry=None, family="ipv4"):
|
|
"""
|
|
Check that an entry exists in the specified set.
|
|
|
|
name
|
|
The ipset name
|
|
|
|
entry
|
|
An entry in the ipset. This parameter can be a single IP address, a
|
|
range of IP addresses, or a subnet block. Example:
|
|
|
|
.. code-block:: cfg
|
|
|
|
192.168.0.1
|
|
192.168.0.2-192.168.0.19
|
|
192.168.0.0/25
|
|
|
|
family
|
|
IP protocol version: ipv4 or ipv6
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.check name '192.168.0.1 comment "Hello"'
|
|
|
|
"""
|
|
if not name:
|
|
return "Error: Set needs to be specified"
|
|
if not entry:
|
|
return "Error: Entry needs to be specified"
|
|
|
|
settype = _find_set_type(name)
|
|
if not settype:
|
|
return f"Error: Set {name} does not exist"
|
|
|
|
current_members = _parse_members(settype, _find_set_members(name))
|
|
|
|
if not current_members:
|
|
return False
|
|
|
|
if isinstance(entry, list):
|
|
entries = _parse_members(settype, entry)
|
|
else:
|
|
entries = [_parse_member(settype, entry)]
|
|
|
|
for current_member in current_members:
|
|
for entry in entries:
|
|
if _member_contains(current_member, entry):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def test(name=None, entry=None, family="ipv4", **kwargs):
|
|
"""
|
|
Test if an entry is in the specified set.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.test name 192.168.0.2
|
|
|
|
IPv6:
|
|
salt '*' ipset.test name fd81:fc56:9ac7::/48
|
|
"""
|
|
if not name:
|
|
return "Error: Set needs to be specified"
|
|
if not entry:
|
|
return "Error: Entry needs to be specified"
|
|
|
|
settype = _find_set_type(name)
|
|
if not settype:
|
|
return f"Error: Set {name} does not exist"
|
|
|
|
cmd = [_ipset_cmd(), "test", name, entry]
|
|
out = __salt__["cmd.run_all"](cmd, python_shell=False)
|
|
|
|
if out["retcode"] > 0:
|
|
# Entry doesn't exist in set return false
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def flush(name=None, family="ipv4"):
|
|
"""
|
|
Flush entries in the specified set,
|
|
Flush all sets if set is not specified.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt '*' ipset.flush
|
|
|
|
salt '*' ipset.flush set
|
|
|
|
IPv6:
|
|
salt '*' ipset.flush
|
|
|
|
salt '*' ipset.flush set
|
|
"""
|
|
|
|
cmd = [_ipset_cmd(), "flush"]
|
|
if name:
|
|
cmd.append(name)
|
|
out = __salt__["cmd.run"](cmd, python_shell=False)
|
|
|
|
return not out
|
|
|
|
|
|
def _find_set_members(name):
|
|
"""
|
|
Return list of members for a set
|
|
"""
|
|
|
|
cmd = [_ipset_cmd(), "list", name]
|
|
out = __salt__["cmd.run_all"](cmd, python_shell=False)
|
|
|
|
if out["retcode"] > 0:
|
|
# Set doesn't exist return false
|
|
return False
|
|
|
|
_tmp = out["stdout"].split("\n")
|
|
members = []
|
|
startMembers = False
|
|
for i in _tmp:
|
|
if startMembers:
|
|
members.append(i)
|
|
if "Members:" in i:
|
|
startMembers = True
|
|
return members
|
|
|
|
|
|
def _find_set_info(name):
|
|
"""
|
|
Return information about the set
|
|
"""
|
|
|
|
cmd = [_ipset_cmd(), "list", "-t", name]
|
|
out = __salt__["cmd.run_all"](cmd, python_shell=False)
|
|
|
|
if out["retcode"] > 0:
|
|
# Set doesn't exist return false
|
|
return False
|
|
|
|
setinfo = {}
|
|
_tmp = out["stdout"].split("\n")
|
|
for item in _tmp:
|
|
# Only split if item has a colon
|
|
if ":" in item:
|
|
key, value = item.split(":", 1)
|
|
setinfo[key] = value[1:]
|
|
return setinfo
|
|
|
|
|
|
def _find_set_type(name):
|
|
"""
|
|
Find the type of the set
|
|
"""
|
|
setinfo = _find_set_info(name)
|
|
|
|
if setinfo:
|
|
return setinfo["Type"]
|
|
else:
|
|
return False
|
|
|
|
|
|
def _parse_members(settype, members):
|
|
if isinstance(members, str):
|
|
|
|
return [_parse_member(settype, members)]
|
|
|
|
return [_parse_member(settype, member) for member in members]
|
|
|
|
|
|
def _parse_member(settype, member, strict=False):
|
|
subtypes = settype.split(":")[1].split(",")
|
|
|
|
all_parts = member.split(" ", 1)
|
|
parts = all_parts[0].split(",")
|
|
|
|
parsed_member = []
|
|
for i, subtype in enumerate(subtypes):
|
|
part = parts[i]
|
|
|
|
if subtype in ["ip", "net"]:
|
|
try:
|
|
if "/" in part:
|
|
part = ipaddress.ip_network(part, strict=strict)
|
|
elif "-" in part:
|
|
start, end = list(map(ipaddress.ip_address, part.split("-")))
|
|
|
|
part = list(ipaddress.summarize_address_range(start, end))
|
|
else:
|
|
part = ipaddress.ip_address(part)
|
|
except ValueError:
|
|
pass
|
|
|
|
elif subtype == "port":
|
|
part = int(part)
|
|
|
|
parsed_member.append(part)
|
|
|
|
if len(all_parts) > 1:
|
|
parsed_member.append(all_parts[1])
|
|
|
|
return parsed_member
|
|
|
|
|
|
def _members_contain(members, entry):
|
|
pass
|
|
|
|
|
|
def _member_contains(member, entry):
|
|
if len(member) < len(entry):
|
|
return False
|
|
|
|
for i, _entry in enumerate(entry):
|
|
if not _compare_member_parts(member[i], _entry):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def _compare_member_parts(member_part, entry_part):
|
|
if member_part == entry_part:
|
|
# this covers int, string, and equal ip and net
|
|
return True
|
|
|
|
# for ip ranges parsed with summarize_address_range
|
|
if isinstance(entry_part, list):
|
|
for entry_part_item in entry_part:
|
|
if not _compare_member_parts(member_part, entry_part_item):
|
|
return False
|
|
|
|
return True
|
|
|
|
# below we only deal with ip and net objects
|
|
if _is_address(member_part):
|
|
if _is_network(entry_part):
|
|
return member_part in entry_part
|
|
|
|
elif _is_network(member_part):
|
|
if _is_address(entry_part):
|
|
return entry_part in member_part
|
|
|
|
# both are networks, and == was false
|
|
return False
|
|
|
|
# This could be changed to support things like
|
|
# 192.168.0.4/30 contains 192.168.0.4/31
|
|
#
|
|
# return entry_part.network_address in member_part \
|
|
# and entry_part.broadcast_address in member_part
|
|
|
|
return False
|
|
|
|
|
|
def _is_network(o):
|
|
return isinstance(o, (ipaddress.IPv4Network, ipaddress.IPv6Network))
|
|
|
|
|
|
def _is_address(o):
|
|
return isinstance(o, (ipaddress.IPv4Address, ipaddress.IPv6Address))
|