MAC-IP table
This commit is contained in:
parent
31ee6ef787
commit
ec80954927
12 changed files with 453 additions and 119 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
config.ini
|
||||
Pipfile*
|
||||
# Created by https://www.gitignore.io/api/vim,python
|
||||
# Edit at https://www.gitignore.io/?templates=vim,python
|
||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "re2oapi"]
|
||||
path = re2oapi
|
||||
url = https://gitlab.federez.net/re2o/re2oapi.git
|
|
@ -1,9 +1,13 @@
|
|||
# Table checkmac, à bas le spoof d'ips.
|
||||
|
||||
table firewall {
|
||||
table inet firewall {
|
||||
set ip_mac {
|
||||
type ipv4_addr . ether_addr
|
||||
}
|
||||
chain checkmac {
|
||||
meta iifname $if_adherent ip saddr . ether saddr != @ip_mac drop
|
||||
meta iifname $if_aloes ip saddr . ether saddr != @ip_mac drop
|
||||
}
|
||||
}
|
||||
|
||||
# Note :
|
||||
|
|
9
config.ini.example
Normal file
9
config.ini.example
Normal file
|
@ -0,0 +1,9 @@
|
|||
[NAT]
|
||||
range_in_adherent=10.69.0.0/31
|
||||
range_out_adherent=193.48.225.11-193.48.225.12
|
||||
first_port_adherent=11135
|
||||
last_port_adherent=65535
|
||||
[Re2o]
|
||||
hostname=re2o.rezometz.org
|
||||
password=some_pass
|
||||
username=service-daemon
|
|
@ -1,4 +1,4 @@
|
|||
#! /usr/sbin/nft -f
|
||||
#! /usr/sbin/nft -I /usr/local/firewall -f
|
||||
|
||||
# Remise à zéro des règles du pare-feu
|
||||
flush ruleset
|
||||
|
@ -27,7 +27,10 @@ table inet firewall {
|
|||
policy drop;
|
||||
|
||||
# Applique la politique globale
|
||||
#jump global
|
||||
jump global
|
||||
|
||||
# Passage par le checkmac pour les concernés
|
||||
jump checkmac
|
||||
|
||||
# Filtre sur les interfaces entrantes, ne pas accepter
|
||||
# directement dans la chaine, mais retourner.
|
||||
|
|
309
firewall.py
309
firewall.py
|
@ -33,13 +33,14 @@ Module for nftables set management.
|
|||
|
||||
import logging
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
import netaddr # MAC, IPv4, IPv6
|
||||
import requests
|
||||
|
||||
from collections import Iterable
|
||||
|
||||
from config import Config
|
||||
from configparser import ConfigParser
|
||||
|
||||
|
||||
class ExecError(Exception):
|
||||
|
@ -95,13 +96,14 @@ class Parser:
|
|||
ip: can either be a tuple (in this case returns an IPRange), a
|
||||
single IP address or a IP Network.
|
||||
"""
|
||||
if isinstance(ip, tuple):
|
||||
begin, end = ip
|
||||
return netaddr.IPRange(begin, end, version=4)
|
||||
try:
|
||||
return netaddr.IPAddress(ip, version=4)
|
||||
except ValueError:
|
||||
return netaddr.IPNetwork(ip, version=4)
|
||||
except netaddr.core.AddrFormatError:
|
||||
try:
|
||||
return netaddr.IPNetwork(ip, version=4)
|
||||
except netaddr.core.AddrFormatError:
|
||||
begin, end = ip.split('-')
|
||||
return netaddr.IPRange(begin, end)
|
||||
@staticmethod
|
||||
def IPv6(ip):
|
||||
"""Check a IPv6 validity.
|
||||
|
@ -127,9 +129,15 @@ class Parser:
|
|||
@staticmethod
|
||||
def port_number(port):
|
||||
"""Check a port validity."""
|
||||
port_number = int(port)
|
||||
if 0 <= port_number < 65536:
|
||||
return port_number
|
||||
try:
|
||||
port_number = int(port)
|
||||
if 0 <= port_number < 65536:
|
||||
return port_number
|
||||
except ValueError:
|
||||
begin, end = port.split('-')
|
||||
begin, end = int(begin), int(end)
|
||||
if 0 <= begin < end <= 65536:
|
||||
return port
|
||||
raise ValueError('Invalid port number: "{}".'.format(port))
|
||||
|
||||
class NetfilterSet:
|
||||
|
@ -145,6 +153,8 @@ class NetfilterSet:
|
|||
|
||||
FLAGS = {'constant', 'interval', 'timeout'}
|
||||
|
||||
NFT_TYPE = {'set', 'map'}
|
||||
|
||||
def __init__(self,
|
||||
name,
|
||||
type_, # e.g.: ('MAC', 'IPv4')
|
||||
|
@ -152,21 +162,30 @@ class NetfilterSet:
|
|||
use_sudo=True,
|
||||
address_family='inet', # Manage both IPv4 and IPv6.
|
||||
table_name='filter',
|
||||
flags = []
|
||||
flags = [],
|
||||
type_from=None
|
||||
):
|
||||
self.name = name
|
||||
self.content = set()
|
||||
# self.type
|
||||
self.set_type(type_)
|
||||
if type_from:
|
||||
self.set_type_from(type_from)
|
||||
self.nft_type = 'map'
|
||||
self.key_filters = tuple(self.FILTERS[i] for i in self.type_from)
|
||||
else:
|
||||
self.nft_type = 'set'
|
||||
self.filters = tuple(self.FILTERS[i] for i in self.type)
|
||||
self.set_flags(flags)
|
||||
# self.address_family
|
||||
self.set_address_family(address_family)
|
||||
self.table = table_name
|
||||
self.set_flags(flags)
|
||||
sudo = ["/usr/bin/sudo"] * int(bool(use_sudo))
|
||||
self.nft = [*sudo, "/usr/sbin/nft"]
|
||||
if target_content:
|
||||
if target_content and self.nft_type == 'set':
|
||||
self._target_content = self.validate_set_data(target_content)
|
||||
elif target_content and self.nft_type == 'map':
|
||||
self._target_content = self.validate_map_data(target_content)
|
||||
else:
|
||||
self._target_content = set()
|
||||
|
||||
|
@ -181,6 +200,9 @@ class NetfilterSet:
|
|||
def filter(self, elements):
|
||||
return (self.filters[i](element) for i, element in enumerate(elements))
|
||||
|
||||
def filter_key(self, elements):
|
||||
return (self.key_filters[i](element) for i, element in enumerate(elements))
|
||||
|
||||
def set_type(self, type_):
|
||||
"""Check set type validity and store it along with a type checker."""
|
||||
for element_type in type_:
|
||||
|
@ -188,6 +210,13 @@ class NetfilterSet:
|
|||
raise ValueError('Invalid type: "{}".'.format(element_type))
|
||||
self.type = type_
|
||||
|
||||
def set_type_from(self, type_):
|
||||
"""Check set type validity and store it along with a type checker."""
|
||||
for element_type in type_:
|
||||
if element_type not in self.TYPES:
|
||||
raise ValueError('Invalid type: "{}".'.format(element_type))
|
||||
self.type_from = type_
|
||||
|
||||
def set_address_family(self, address_family='ip'):
|
||||
"""Set set addres_family, defaulting to "ip" like nftables."""
|
||||
if address_family not in self.ADDRESS_FAMILIES:
|
||||
|
@ -200,7 +229,7 @@ class NetfilterSet:
|
|||
for f in flags_:
|
||||
if f not in self.FLAGS:
|
||||
raise ValueError('Invalid flag: "{}".'.format(f))
|
||||
self.flags = _flags
|
||||
self.flags = set(flags_)
|
||||
|
||||
def create_in_kernel(self):
|
||||
"""Create the set, removing existing set if needed."""
|
||||
|
@ -217,21 +246,35 @@ class NetfilterSet:
|
|||
"""Delete the set, table and set must exist."""
|
||||
CommandExec.run([
|
||||
*self.nft,
|
||||
'delete set {addr_family} {table} {set_}'.format(
|
||||
'delete {nft_type} {addr_family} {table} {set_}'.format(
|
||||
nft_type=self.nft_type,
|
||||
addr_family=self.address_family, table=self.table,
|
||||
set_=self.name)
|
||||
])
|
||||
|
||||
def _create_new_set_in_kernel(self):
|
||||
"""Create the non-existing set, creating table if needed."""
|
||||
if self.flags:
|
||||
nft_command = 'add {nft_type} {addr_family} {table} {set_} {{ type {type_} ; flags {flags};}}'.format(
|
||||
nft_type=self.nft_type,
|
||||
addr_family=self.address_family,
|
||||
table=self.table,
|
||||
set_=self.name,
|
||||
type_=self.format_type(),
|
||||
flags=', '.join(self.flags)
|
||||
)
|
||||
else:
|
||||
nft_command = 'add {nft_type} {addr_family} {table} {set_} {{ type {type_} ;}}'.format(
|
||||
nft_type=self.nft_type,
|
||||
addr_family=self.address_family,
|
||||
table=self.table,
|
||||
set_=self.name,
|
||||
type_=self.format_type(),
|
||||
)
|
||||
create_set = [
|
||||
*self.nft,
|
||||
'add set {addr_family} {table} {set_} {{ type {type_} ; flags {flags}}}'.format(
|
||||
addr_family=self.address_family, table=self.table,
|
||||
set_=self.name,
|
||||
type_=' . '.join(self.TYPES[i] for i in self.type)),
|
||||
flags=', '.join(self.flags)
|
||||
]
|
||||
nft_command
|
||||
]
|
||||
return_code = CommandExec.run(create_set, allowed_return_codes=(0, 1))
|
||||
if return_code == 0:
|
||||
return # Set creation successful.
|
||||
|
@ -261,7 +304,33 @@ class NetfilterSet:
|
|||
.format(len(errors), '",\n"'.join(map(str, errors))))
|
||||
return set_
|
||||
|
||||
def validate_map_data(self, dict_data):
|
||||
"""
|
||||
Validate data, returning it or raising a ValueError.
|
||||
|
||||
For MAC-IPv4 set, data must be an iterable of (MAC, IPv4) iterables.
|
||||
"""
|
||||
set_ = {}
|
||||
errors = []
|
||||
for key in dict_data:
|
||||
try:
|
||||
set_[tuple(self.filter_key(key))] = tuple(self.filter(dict_data[key]))
|
||||
except Exception as err:
|
||||
errors.append(err)
|
||||
if errors:
|
||||
raise ValueError(
|
||||
'Error parsing data, encountered the folowing {} errors.\n"{}"'
|
||||
.format(len(errors), '",\n"'.join(map(str, errors))))
|
||||
return set_
|
||||
|
||||
def _apply_target_content(self):
|
||||
"""Change netfilter content to target set."""
|
||||
if self.nft_type == 'set':
|
||||
self._apply_target_content_set()
|
||||
else:
|
||||
self._apply_target_content_map()
|
||||
|
||||
def _apply_target_content_set(self):
|
||||
"""Change netfilter set content to target set."""
|
||||
current_set = self.get_netfilter_set_content()
|
||||
if current_set is None:
|
||||
|
@ -272,6 +341,23 @@ class NetfilterSet:
|
|||
to_add = self._target_content - current_set
|
||||
self._change_set_content(delete=to_delete, add=to_add)
|
||||
|
||||
def _apply_target_content_map(self):
|
||||
"""Change netfilter set content to target set."""
|
||||
current_map = self.get_netfilter_map_content()
|
||||
if current_map is None:
|
||||
raise ValueError('Cannot change "{}" netfilter map content: map '
|
||||
'do not exist in "{}" "{}".'.format(
|
||||
self.name, self.address_family, self.table))
|
||||
keys_to_delete = current_map.keys() - self._target_content.keys()
|
||||
keys_to_add = self._target_content.keys() - current_map.keys()
|
||||
keys_to_check = current_map.keys() & self._target_content.keys()
|
||||
for k in keys_to_check:
|
||||
if current_map[k] != self._target_content[k]:
|
||||
keys_to_add.add(k)
|
||||
keys_to_delete.add(k)
|
||||
to_add = {k : self._target_content[k] for k in keys_to_add}
|
||||
self._change_map_content(delete=keys_to_delete, add=to_add)
|
||||
|
||||
def _change_set_content(self, delete=None, add=None):
|
||||
todo = [tuple_ for tuple_ in (('add', add), ('delete', delete))
|
||||
if tuple_[1]]
|
||||
|
@ -286,6 +372,32 @@ class NetfilterSet:
|
|||
]
|
||||
CommandExec.run(command)
|
||||
|
||||
def _change_map_content(self, delete=None, add=None):
|
||||
if delete:
|
||||
content = ', '.join(' . '.join(str(element) for element in tuple_)
|
||||
for tuple_ in delete)
|
||||
command = [
|
||||
*self.nft,
|
||||
'delete element {addr_family} {table} {set_} {{{content}}}' \
|
||||
.format(addr_family=self.address_family,
|
||||
table=self.table, set_=self.name, content=content)
|
||||
]
|
||||
CommandExec.run(command)
|
||||
if add:
|
||||
content = ', '.join(
|
||||
' . '.join(str(element) for element in tuple_)
|
||||
+ ' : '
|
||||
+ ' . '.join(str(element) for element in add[tuple_])
|
||||
for tuple_ in add
|
||||
)
|
||||
command = [
|
||||
*self.nft,
|
||||
'add element {addr_family} {table} {set_} {{{content}}}' \
|
||||
.format(addr_family=self.address_family,
|
||||
table=self.table, set_=self.name, content=content)
|
||||
]
|
||||
CommandExec.run(command)
|
||||
|
||||
def _get_raw_netfilter_set(self, parse_elements=True):
|
||||
"""Return a dict describing the netfilter set matching self or None."""
|
||||
_, stdout, _ = CommandExec.run_check_output(
|
||||
|
@ -301,9 +413,25 @@ class NetfilterSet:
|
|||
if netfilter_set['name'] != self.name \
|
||||
or netfilter_set['address_family'] != self.address_family \
|
||||
or netfilter_set['table'] != self.table \
|
||||
or netfilter_set['type'] != [
|
||||
self.TYPES[type_] for type_ in self.type]:
|
||||
raise ValueError('Did not get the right set, too wrong to fix.')
|
||||
or not self.has_type(netfilter_set['type']) \
|
||||
or netfilter_set.get('flags', set()) != self.flags:
|
||||
raise ValueError(
|
||||
'Did not get the right set, too wrong to fix. Got '
|
||||
+ str(netfilter_set)
|
||||
+ ("\nExpected : "
|
||||
"\n\tname: {name}"
|
||||
"\n\taddress_family: {family}"
|
||||
"\n\ttable: {table}"
|
||||
"\n\tflags: {flags}"
|
||||
"\n\ttypes: {types}"
|
||||
).format(
|
||||
name=self.name,
|
||||
family=self.address_family,
|
||||
table=self.table,
|
||||
flags=self.flags,
|
||||
types=tuple(self.TYPES[t] for t in self.type)
|
||||
)
|
||||
)
|
||||
if parse_elements:
|
||||
if netfilter_set['raw_content']:
|
||||
netfilter_set['content'] = self.validate_set_data((
|
||||
|
@ -313,21 +441,79 @@ class NetfilterSet:
|
|||
netfilter_set['content'] = set()
|
||||
return netfilter_set
|
||||
|
||||
def _get_raw_netfilter_map(self, parse_elements=True):
|
||||
"""Return a dict describing the netfilter map matching self or None."""
|
||||
_, stdout, _ = CommandExec.run_check_output(
|
||||
[*self.nft, '-nn', 'list map {addr_family} {table} {set_}'.format(
|
||||
addr_family=self.address_family, table=self.table,
|
||||
set_=self.name)],
|
||||
allowed_return_codes=(0, 1) # In case table do not exist
|
||||
)
|
||||
if not stdout:
|
||||
return None
|
||||
else:
|
||||
netfilter_set = self._parse_netfilter_map_string(stdout)
|
||||
if netfilter_set['name'] != self.name \
|
||||
or netfilter_set['address_family'] != self.address_family \
|
||||
or netfilter_set['table'] != self.table \
|
||||
or not self.has_type(netfilter_set['type']):
|
||||
raise ValueError('Did not get the right map, too wrong to fix.')
|
||||
if parse_elements:
|
||||
if netfilter_set['raw_content']:
|
||||
netfilter_set['content'] = self.validate_map_data({
|
||||
(element.strip() for element in n_uplet.split(' : ')[0].split(' . ')) :
|
||||
(element.strip() for element in n_uplet.split(' : ')[1].split(' . '))
|
||||
for n_uplet in netfilter_set['raw_content'].split(',')
|
||||
})
|
||||
else:
|
||||
netfilter_set['content'] = {}
|
||||
return netfilter_set
|
||||
|
||||
@staticmethod
|
||||
def _parse_netfilter_set_string(set_string):
|
||||
"""
|
||||
Parse netfilter set definition and return set as dict.
|
||||
|
||||
Do not validate content type against detected set type.
|
||||
Return a dict with 'name', 'address_family', 'table', 'type',
|
||||
Return a dict with 'name', 'address_family', 'table', 'type', 'flags',
|
||||
'raw_content' keys (all strings, 'raw_content' can be None).
|
||||
Raise ValueError in case of unexpected syntax.
|
||||
"""
|
||||
# A.K.A. Really, I don't hate you, so please don't hate me...
|
||||
regexp = (
|
||||
"table (?P<address_family>\w+)+ (?P<table>\w+) \{\n"
|
||||
"\s*set (?P<name>\w+) \{\n"
|
||||
"\s*type (?P<type>(\w+( \. )?)+)\n"
|
||||
"(\s*elements = \{ "
|
||||
"(?P<elements>((\n\s*)?([\w:\.]+( \. )?)+,?)*) "
|
||||
"\}\n)?"
|
||||
"\s*\}\n"
|
||||
"\s*\}"
|
||||
)
|
||||
values = re.match(regexp, set_string).groupdict()
|
||||
return {
|
||||
'address_family': values['address_family'],
|
||||
'table': values['table'],
|
||||
'name': values['name'],
|
||||
'type': values['type'].split(' . '),
|
||||
'raw_content': values['elements'],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _parse_netfilter_map_string(set_string):
|
||||
"""
|
||||
Parse netfilter map definition and return map as dict.
|
||||
|
||||
Do not validate content type against detected map type.
|
||||
Return a dict with 'name', 'address_family', 'table', 'type', 'flags'
|
||||
'raw_content' keys (all strings, 'raw_content' can be None).
|
||||
Raise ValueError in case of unexpected syntax.
|
||||
"""
|
||||
# Fragile code since using lexer / parser would be quite heavy
|
||||
lines = [line.lstrip('\t ') for line in set_string.strip().splitlines()]
|
||||
errors = []
|
||||
# 5 lines when empty, 6 with elements = { … }
|
||||
if len(lines) not in (5, 6):
|
||||
# 5 lines when empty, 6 with elements = { … } + one for flags
|
||||
if len(lines) not in (5, 6, 7):
|
||||
errors.append('Error, expecting 5 or 6 lines for set definition, '
|
||||
'got "{}".'.format(set_string))
|
||||
|
||||
|
@ -346,30 +532,54 @@ class NetfilterSet:
|
|||
|
||||
line = next(line_iterator).split(' ') # line #2
|
||||
# 'set <name> {'
|
||||
if len(line) != 3 or line[0] != 'set' or line[2] != '{':
|
||||
if len(line) != 3 or line[0] != 'map' or line[2] != '{':
|
||||
errors.append('Cannot parse set definition, expecting "set <name> '
|
||||
'{{", got "{}".' .format(' '.join(line)))
|
||||
else:
|
||||
set_definition['name'] = line[1]
|
||||
|
||||
line = next(line_iterator).split(' ') # line #3
|
||||
# 'type <type> [. <type>]...'
|
||||
if len(line) < 2 or len(line) % 2 != 0 or line[0] != 'type' \
|
||||
or any(element != '.' for element in line[2::2]):
|
||||
line, elements_type = next(line_iterator).split(' : ') # line #3
|
||||
# 'type <type> [. <type>]... : <type> [. <type>]...'
|
||||
line = line.split(' ')
|
||||
if len(line) < 2:
|
||||
errors.append(
|
||||
'Cannot parse type definition, left side of \':\' is too short : %s' % line
|
||||
)
|
||||
type_, keys_type = line[0], line[1:]
|
||||
elements_type = elements_type.split(' ')
|
||||
if type_ != 'type':
|
||||
errors.append(
|
||||
'Cannot parse type definition, expected first word \'type\', got %s' % type_
|
||||
)
|
||||
elif len(elements_type) % 2 != 1 or len(keys_type) % 2 != 1 \
|
||||
or any(e != '.' for e in elements_type[1::2]) \
|
||||
or any(e != '.' for e in keys_type[1::2]):
|
||||
errors.append(
|
||||
'Cannot parse type definition, expecting "type <type> '
|
||||
'[. <type>]...", got "{}".'.format(' '.join(line)))
|
||||
'[. <type>]... : <type> [. <type>]...", got "{}".'.format(' '.join(line)))
|
||||
else:
|
||||
set_definition['type'] = line[1::2]
|
||||
set_definition['type'] = (keys_type[::2], elements_type[::2])
|
||||
|
||||
if len(lines) == 6:
|
||||
# here we can have the flags, if there are any
|
||||
# flags <flag_1>, <flag_2>, ...
|
||||
if len(lines) >= 6:
|
||||
line = next(line_iterator)
|
||||
if line[:5] == 'flags': # If there are actually flags
|
||||
set_definition['flags'] = {f.strip() for f in line[:5].strip().split(',')}
|
||||
|
||||
if len(lines) >= 6:
|
||||
# set is not empty, getting raw elements
|
||||
line = next(line_iterator) # Unsplit line #4
|
||||
if line[:13] != 'elements = { ' or line[-1] != '}':
|
||||
errors.append('Cannot parse set elements, expecting "elements '
|
||||
if 'flags' in set_definition and len(lines) == 7: # the line unsplitted previously has been used.
|
||||
line = next(line_iterator) # Unsplit line #4
|
||||
print(line)
|
||||
if ('flags' in set_definition and len(lines)==7) or ('flags' not in set_definition and len(lines)==6) :
|
||||
if line[:13] != 'elements = { ' or line[-1] != '}':
|
||||
errors.append('Cannot parse set elements, expecting "elements '
|
||||
'= {{ <…>}}", got "{}".'.format(line))
|
||||
else:
|
||||
set_definition['raw_content'] = line[13:-1].strip()
|
||||
else:
|
||||
set_definition['raw_content'] = line[13:-1].strip()
|
||||
set_definition['raw_content'] = None
|
||||
else:
|
||||
set_definition['raw_content'] = None
|
||||
|
||||
|
@ -393,22 +603,41 @@ class NetfilterSet:
|
|||
else:
|
||||
return netfilter_set['content']
|
||||
|
||||
def get_netfilter_map_content(self):
|
||||
"""Return current set content from netfilter."""
|
||||
netfilter_set = self._get_raw_netfilter_map(parse_elements=True)
|
||||
if netfilter_set is None:
|
||||
return None
|
||||
else:
|
||||
return netfilter_set['content']
|
||||
|
||||
def has_type(self, type_):
|
||||
"""Check if some type match the set's one."""
|
||||
return tuple(self.TYPES[t] for t in self.type) == tuple(type_)
|
||||
if self.nft_type == 'set':
|
||||
return tuple(self.TYPES[t] for t in self.type) == tuple(type_)
|
||||
else:
|
||||
return tuple(self.TYPES[t] for t in self.type) == tuple(type_[1]) and \
|
||||
tuple(self.TYPES[t] for t in self.type_from) == tuple(type_[0])
|
||||
|
||||
def manage(self):
|
||||
"""Create set if needed and populate it with target content."""
|
||||
self.create_in_kernel()
|
||||
self._apply_target_content()
|
||||
|
||||
def format_type(self):
|
||||
if self.nft_type == 'set':
|
||||
return ' . '.join(self.TYPES[i] for i in self.type)
|
||||
else:
|
||||
return ' . '.join(self.TYPES[i] for i in self.type_from) + ' : ' + ' . '.join(self.TYPES[i] for i in self.type)
|
||||
|
||||
|
||||
class Firewall:
|
||||
"""Manages the firewall using nftables."""
|
||||
|
||||
@staticmethod
|
||||
def manage_sets(sets, address_family=None, table=None, use_sudo=None):
|
||||
CONFIG = Config()
|
||||
CONFIG = ConfigParser()
|
||||
CONFIG.read('config.ini')
|
||||
address_family = address_family or CONFIG['address_family'] or 'inet'
|
||||
table = table or CONFIG['table'] or 'filter'
|
||||
sudo = use_sudo or (use_sudo is None and CONFIG['use_sudo'])
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
|
||||
table firewall {
|
||||
table inet firewall {
|
||||
chain global {
|
||||
# Interdiction de l'encapsulation ipv6 dans ipv4
|
||||
ip protocol 6 drop;
|
||||
|
||||
# On accepte les connexions déjà établies
|
||||
ct state established,related accept
|
||||
# Gestion de l'ICMP :
|
||||
# On empêche le ping flood
|
||||
icmp type echo-request limit rate over 50/second drop;
|
||||
|
|
78
mac_ip.py
Normal file
78
mac_ip.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
#! /usr/bin/python3
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Copyright © 2019 Hugo Levy-Falk <me@klafyvel.me>
|
||||
|
||||
"""
|
||||
Creates the nat set.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from configparser import ConfigParser
|
||||
|
||||
from re2oapi import Re2oAPIClient
|
||||
|
||||
from firewall import NetfilterSet
|
||||
|
||||
CONFIG = ConfigParser()
|
||||
CONFIG.read('config.ini')
|
||||
|
||||
api_hostname = CONFIG.get('Re2o', 'hostname')
|
||||
api_password = CONFIG.get('Re2o', 'password')
|
||||
api_username = CONFIG.get('Re2o', 'username')
|
||||
|
||||
api_client = Re2oAPIClient(api_hostname, api_username, api_password)
|
||||
|
||||
api_client.list('dhcp/hostmacip')
|
||||
|
||||
def gen_ip_mac_set():
|
||||
"""Generates the ip_mac set in nftables.
|
||||
|
||||
Returns:
|
||||
A NetfilterSet object with the allowed ip - mac pairs.
|
||||
"""
|
||||
hosts = api_client.list('dhcp/hostmacip')
|
||||
content = [
|
||||
(h['ipv4'], h['mac_address'])
|
||||
for h in hosts
|
||||
if h['ipv4'] and h['mac_address']
|
||||
]
|
||||
return NetfilterSet(
|
||||
target_content=content,
|
||||
type_=('IPv4', 'MAC'),
|
||||
name='ip_mac',
|
||||
table_name='firewall',
|
||||
)
|
||||
|
||||
|
||||
def update_macip():
|
||||
log = logging.getLogger(__name__)
|
||||
if not log.hasHandlers():
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s %(levelname)s %(name)s %(message)s"
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
log.setLevel(logging.INFO)
|
||||
log.info('Updating the ip - mac set...')
|
||||
ip_mac = gen_ip_mac_set()
|
||||
log.info('Applying modifications...')
|
||||
ip_mac.manage()
|
||||
log.info('Done')
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
update_macip()
|
13
nat.nft
13
nat.nft
|
@ -10,17 +10,8 @@ table ip nat {
|
|||
chain postrouting {
|
||||
type nat hook postrouting priority 100
|
||||
|
||||
meta oifname != $if_supelec return
|
||||
|
||||
meta iifname vmap {
|
||||
$if_adherent : goto adh_nat,
|
||||
$if_admin : goto adm_nat,
|
||||
$if_aloes : goto aloes_nat,
|
||||
$if_federez : goto federez_nat,
|
||||
$if_prerezotage : goto prerezotage_nat,
|
||||
}
|
||||
|
||||
ip saddr 10.0.0.0/8 masquerade
|
||||
# ip saddr 10.0.0.0/8 snat to 193.48.225.3
|
||||
meta oifname $if_supelec snat to 193.48.225.3
|
||||
|
||||
}
|
||||
|
||||
|
|
133
nat.py
133
nat.py
|
@ -30,6 +30,18 @@ CONFIG = ConfigParser()
|
|||
CONFIG.read('config.ini')
|
||||
|
||||
|
||||
def get_ip_iterable_from_str(ip):
|
||||
try:
|
||||
ret = netaddr.IPGlob(ip)
|
||||
except netaddr.core.AddrFormatError:
|
||||
try:
|
||||
ret = netaddr.IPNetwork(ip)
|
||||
except netaddr.core.AddrFormatError:
|
||||
begin,end = ip.split('-')
|
||||
ret = netaddr.IPRange(begin,end)
|
||||
return ret
|
||||
|
||||
|
||||
def create_nat(name, range_in, range_out, first_port, last_port):
|
||||
"""Create two nftables tables for the nat:
|
||||
- <name>_address : which link a (or a range of) local address to a
|
||||
|
@ -46,55 +58,53 @@ def create_nat(name, range_in, range_out, first_port, last_port):
|
|||
Returns:
|
||||
(<name>_address, <name>_port) which are NetfilterSet
|
||||
"""
|
||||
assert last_port >= first_port, (name + ": Your first_port "
|
||||
assert 0 <= first_port < last_port < 65536, (name + ": Your first_port "
|
||||
"is lower than your last_port")
|
||||
nb_private_by_public = range_in.size / range_out.size
|
||||
nb_port_by_ip = (last_port - first_port + 1) / nb_private_by_public
|
||||
nb_private_by_public = range_in.size // range_out.size
|
||||
nb_port_by_ip = (last_port - first_port + 1) // nb_private_by_public
|
||||
|
||||
ports = []
|
||||
ips = []
|
||||
ports = {}
|
||||
ips = {}
|
||||
|
||||
port = first_port
|
||||
for ip, port in range_in:
|
||||
ports.append((
|
||||
str(netaddr.IPAddress(ip)),
|
||||
"%d-%d" % (port, port+nb_port_by_ip)
|
||||
))
|
||||
for ip in range_in:
|
||||
ports[(str(netaddr.IPAddress(ip)),)] = ("%d-%d" % (port, min(port+nb_port_by_ip, 65535)),)
|
||||
port += nb_port_by_ip + 1
|
||||
if port >= last_port:
|
||||
port = first_port
|
||||
ip = range_in.first
|
||||
for ip_out in range_out:
|
||||
ips.append((
|
||||
'-'.join([
|
||||
ips[('-'.join([
|
||||
str(netaddr.IPAddress(ip)),
|
||||
str(netaddr.IPAddress(ip+nb_private_by_public))
|
||||
]),
|
||||
str(ip_out)
|
||||
))
|
||||
]),)] = (str(ip_out),)
|
||||
ip += nb_private_by_public + 1
|
||||
|
||||
return (
|
||||
NetfilterSet(
|
||||
target_content=ips,
|
||||
type_=('IPv4', 'IPv4'),
|
||||
name=name,
|
||||
type_=('IPv4',),
|
||||
name=name+'_nat_address',
|
||||
table_name='nat',
|
||||
flags=('interval',),
|
||||
type_from=('IPv4',)
|
||||
),
|
||||
NetfilterSet(
|
||||
target_content=ports,
|
||||
type_=('IPv4', 'port'),
|
||||
name=name,
|
||||
type_=('port',),
|
||||
name=name+'_nat_port',
|
||||
table_name='nat',
|
||||
flags=('interval',),
|
||||
type_from=('IPv4',)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def create_nat_adherent():
|
||||
range_in = netaddr.IPRange(CONFIG['range_in_adherent'])
|
||||
range_out = netaddr.IPRange(CONFIG['range_out_adherent'])
|
||||
first_port = CONFIG['first_port_adherent']
|
||||
last_port = CONFIG['last_port_adherent']
|
||||
range_in = get_ip_iterable_from_str(CONFIG['NAT']['range_in_adherent'])
|
||||
range_out = get_ip_iterable_from_str(CONFIG['NAT']['range_out_adherent'])
|
||||
first_port = int(CONFIG['NAT']['first_port_adherent'])
|
||||
last_port = int(CONFIG['NAT']['last_port_adherent'])
|
||||
return create_nat(
|
||||
'adherent',
|
||||
range_in,
|
||||
|
@ -105,10 +115,10 @@ def create_nat_adherent():
|
|||
|
||||
|
||||
def create_nat_federez():
|
||||
range_in = netaddr.IPRange(CONFIG['range_in_federez'])
|
||||
range_out = netaddr.IPRange(CONFIG['range_out_federez'])
|
||||
first_port = CONFIG['first_port_federez']
|
||||
last_port = CONFIG['last_port_federez']
|
||||
range_in = get_ip_iterable_from_str(CONFIG['NAT']['range_in_federez'])
|
||||
range_out = get_ip_iterable_from_str(CONFIG['NAT']['range_out_federez'])
|
||||
first_port = CONFIG['NAT']['first_port_federez']
|
||||
last_port = CONFIG['NAT']['last_port_federez']
|
||||
return create_nat(
|
||||
'federez',
|
||||
range_in,
|
||||
|
@ -119,10 +129,10 @@ def create_nat_federez():
|
|||
|
||||
|
||||
def create_nat_aloes():
|
||||
range_in = netaddr.IPRange(CONFIG['range_in_aloes'])
|
||||
range_out = netaddr.IPRange(CONFIG['range_out_aloes'])
|
||||
first_port = CONFIG['first_port_aloes']
|
||||
last_port = CONFIG['last_port_aloes']
|
||||
range_in = get_ip_iterable_from_str(CONFIG['NAT']['range_in_aloes'])
|
||||
range_out = get_ip_iterable_from_str(CONFIG['NAT']['range_out_aloes'])
|
||||
first_port = CONFIG['NAT']['first_port_aloes']
|
||||
last_port = CONFIG['NAT']['last_port_aloes']
|
||||
return create_nat(
|
||||
'aloes',
|
||||
range_in,
|
||||
|
@ -133,10 +143,10 @@ def create_nat_aloes():
|
|||
|
||||
|
||||
def create_nat_admin():
|
||||
range_in = netaddr.IPRange(CONFIG['range_in_admin'])
|
||||
range_out = netaddr.IPRange(CONFIG['range_out_admin'])
|
||||
first_port = CONFIG['first_port_admin']
|
||||
last_port = CONFIG['last_port_admin']
|
||||
range_in = get_ip_iterable_from_str(CONFIG['NAT']['range_in_admin'])
|
||||
range_out = get_ip_iterable_from_str(CONFIG['NAT']['range_out_admin'])
|
||||
first_port = CONFIG['NAT']['first_port_admin']
|
||||
last_port = CONFIG['NAT']['last_port_admin']
|
||||
return create_nat(
|
||||
'admin',
|
||||
range_in,
|
||||
|
@ -147,10 +157,10 @@ def create_nat_admin():
|
|||
|
||||
|
||||
def create_nat_prerezotage():
|
||||
range_in = netaddr.IPRange(CONFIG['range_in_prerezotage'])
|
||||
range_out = netaddr.IPRange(CONFIG['range_out_prerezotage'])
|
||||
first_port = CONFIG['first_port_prerezotage']
|
||||
last_port = CONFIG['last_port_prerezotage']
|
||||
range_in = get_ip_iterable_from_str(CONFIG['NAT']['range_in_prerezotage'])
|
||||
range_out = get_ip_iterable_from_str(CONFIG['NAT']['range_out_prerezotage'])
|
||||
first_port = CONFIG['NAT']['first_port_prerezotage']
|
||||
last_port = CONFIG['NAT']['last_port_prerezotage']
|
||||
return create_nat(
|
||||
'prerezotage',
|
||||
range_in,
|
||||
|
@ -166,23 +176,28 @@ def main():
|
|||
address.manage()
|
||||
port.manage()
|
||||
logging.info("Done.")
|
||||
logging.info("Creating federez nat...")
|
||||
address, port = create_nat_federez()
|
||||
address.manage()
|
||||
port.manage()
|
||||
logging.info("Done.")
|
||||
logging.info("Creating aloes nat...")
|
||||
address, port = create_nat_aloes()
|
||||
address.manage()
|
||||
port.manage()
|
||||
logging.info("Done.")
|
||||
logging.info("Creating admin nat...")
|
||||
address, port = create_nat_admin()
|
||||
address.manage()
|
||||
port.manage()
|
||||
logging.info("Done.")
|
||||
logging.info("Creating prerezotage nat...")
|
||||
address, port = create_nat_prerezotage()
|
||||
address.manage()
|
||||
port.manage()
|
||||
logging.info("Done.")
|
||||
#logging.info("Creating federez nat...")
|
||||
#address, port = create_nat_federez()
|
||||
#address.manage()
|
||||
#port.manage()
|
||||
#logging.info("Done.")
|
||||
#logging.info("Creating aloes nat...")
|
||||
#address, port = create_nat_aloes()
|
||||
#address.manage()
|
||||
#port.manage()
|
||||
#logging.info("Done.")
|
||||
#logging.info("Creating admin nat...")
|
||||
#address, port = create_nat_admin()
|
||||
#address.manage()
|
||||
#port.manage()
|
||||
#logging.info("Done.")
|
||||
#logging.info("Creating prerezotage nat...")
|
||||
#address, port = create_nat_prerezotage()
|
||||
#address.manage()
|
||||
#port.manage()
|
||||
#logging.info("Done.")
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
logging.info('Updating the NAT table.')
|
||||
main()
|
||||
|
|
1
re2oapi
Submodule
1
re2oapi
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit b12df74fe73f351986ff51c8122089644218f8fe
|
|
@ -7,8 +7,6 @@ table inet firewall {
|
|||
}
|
||||
|
||||
chain from_adh {
|
||||
# On passe d'abord par le checkmac pour éviter le spoof d'ip:
|
||||
#ip saddr . ether saddr != @ip_mac drop
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,10 +20,13 @@ table ip nat {
|
|||
# nft add element nat adherent_nat_address {10.69.0.1-10.69.0.31 : 193.48.225.11}
|
||||
map adherent_nat_address {
|
||||
type ipv4_addr: ipv4_addr
|
||||
flags interval
|
||||
}
|
||||
# exemple: 10.69.0.1 : 11135-12834
|
||||
# On peut aussi ajouter dynamiquement des éléments :
|
||||
# nft add element nat adherent_nat_port {10.69.0.1 : 11135-12834}
|
||||
# Sauf qu'on peut pas faire de maps d'intervalles (seules les clés peuvent en être)
|
||||
# du coup je vois rien d'autre à faire que de modifier à la volée les règles...
|
||||
map adherent_nat_port {
|
||||
type ipv4_addr: inet_service
|
||||
flags interval
|
||||
|
|
Loading…
Reference in a new issue