MAC-IP table

This commit is contained in:
Hugo Levy-Falk 2019-03-12 22:06:21 +01:00 committed by root
parent 31ee6ef787
commit ec80954927
12 changed files with 453 additions and 119 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
config.ini
Pipfile* Pipfile*
# Created by https://www.gitignore.io/api/vim,python # Created by https://www.gitignore.io/api/vim,python
# Edit at https://www.gitignore.io/?templates=vim,python # Edit at https://www.gitignore.io/?templates=vim,python

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "re2oapi"]
path = re2oapi
url = https://gitlab.federez.net/re2o/re2oapi.git

View file

@ -1,9 +1,13 @@
# Table checkmac, à bas le spoof d'ips. # Table checkmac, à bas le spoof d'ips.
table firewall { table inet firewall {
set ip_mac { set ip_mac {
type ipv4_addr . ether_addr 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 : # Note :

9
config.ini.example Normal file
View 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

View file

@ -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 # Remise à zéro des règles du pare-feu
flush ruleset flush ruleset
@ -27,7 +27,10 @@ table inet firewall {
policy drop; policy drop;
# Applique la politique globale # 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 # Filtre sur les interfaces entrantes, ne pas accepter
# directement dans la chaine, mais retourner. # directement dans la chaine, mais retourner.

View file

@ -33,13 +33,14 @@ Module for nftables set management.
import logging import logging
import subprocess import subprocess
import re
import netaddr # MAC, IPv4, IPv6 import netaddr # MAC, IPv4, IPv6
import requests import requests
from collections import Iterable from collections import Iterable
from config import Config from configparser import ConfigParser
class ExecError(Exception): class ExecError(Exception):
@ -95,13 +96,14 @@ class Parser:
ip: can either be a tuple (in this case returns an IPRange), a ip: can either be a tuple (in this case returns an IPRange), a
single IP address or a IP Network. single IP address or a IP Network.
""" """
if isinstance(ip, tuple):
begin, end = ip
return netaddr.IPRange(begin, end, version=4)
try: try:
return netaddr.IPAddress(ip, version=4) return netaddr.IPAddress(ip, version=4)
except ValueError: except netaddr.core.AddrFormatError:
try:
return netaddr.IPNetwork(ip, version=4) return netaddr.IPNetwork(ip, version=4)
except netaddr.core.AddrFormatError:
begin, end = ip.split('-')
return netaddr.IPRange(begin, end)
@staticmethod @staticmethod
def IPv6(ip): def IPv6(ip):
"""Check a IPv6 validity. """Check a IPv6 validity.
@ -127,9 +129,15 @@ class Parser:
@staticmethod @staticmethod
def port_number(port): def port_number(port):
"""Check a port validity.""" """Check a port validity."""
try:
port_number = int(port) port_number = int(port)
if 0 <= port_number < 65536: if 0 <= port_number < 65536:
return port_number 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)) raise ValueError('Invalid port number: "{}".'.format(port))
class NetfilterSet: class NetfilterSet:
@ -145,6 +153,8 @@ class NetfilterSet:
FLAGS = {'constant', 'interval', 'timeout'} FLAGS = {'constant', 'interval', 'timeout'}
NFT_TYPE = {'set', 'map'}
def __init__(self, def __init__(self,
name, name,
type_, # e.g.: ('MAC', 'IPv4') type_, # e.g.: ('MAC', 'IPv4')
@ -152,21 +162,30 @@ class NetfilterSet:
use_sudo=True, use_sudo=True,
address_family='inet', # Manage both IPv4 and IPv6. address_family='inet', # Manage both IPv4 and IPv6.
table_name='filter', table_name='filter',
flags = [] flags = [],
type_from=None
): ):
self.name = name self.name = name
self.content = set() self.content = set()
# self.type # self.type
self.set_type(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.filters = tuple(self.FILTERS[i] for i in self.type)
self.set_flags(flags)
# self.address_family # self.address_family
self.set_address_family(address_family) self.set_address_family(address_family)
self.table = table_name self.table = table_name
self.set_flags(flags)
sudo = ["/usr/bin/sudo"] * int(bool(use_sudo)) sudo = ["/usr/bin/sudo"] * int(bool(use_sudo))
self.nft = [*sudo, "/usr/sbin/nft"] 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) 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: else:
self._target_content = set() self._target_content = set()
@ -181,6 +200,9 @@ class NetfilterSet:
def filter(self, elements): def filter(self, elements):
return (self.filters[i](element) for i, element in enumerate(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_): def set_type(self, type_):
"""Check set type validity and store it along with a type checker.""" """Check set type validity and store it along with a type checker."""
for element_type in type_: for element_type in type_:
@ -188,6 +210,13 @@ class NetfilterSet:
raise ValueError('Invalid type: "{}".'.format(element_type)) raise ValueError('Invalid type: "{}".'.format(element_type))
self.type = 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'): def set_address_family(self, address_family='ip'):
"""Set set addres_family, defaulting to "ip" like nftables.""" """Set set addres_family, defaulting to "ip" like nftables."""
if address_family not in self.ADDRESS_FAMILIES: if address_family not in self.ADDRESS_FAMILIES:
@ -200,7 +229,7 @@ class NetfilterSet:
for f in flags_: for f in flags_:
if f not in self.FLAGS: if f not in self.FLAGS:
raise ValueError('Invalid flag: "{}".'.format(f)) raise ValueError('Invalid flag: "{}".'.format(f))
self.flags = _flags self.flags = set(flags_)
def create_in_kernel(self): def create_in_kernel(self):
"""Create the set, removing existing set if needed.""" """Create the set, removing existing set if needed."""
@ -217,20 +246,34 @@ class NetfilterSet:
"""Delete the set, table and set must exist.""" """Delete the set, table and set must exist."""
CommandExec.run([ CommandExec.run([
*self.nft, *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, addr_family=self.address_family, table=self.table,
set_=self.name) set_=self.name)
]) ])
def _create_new_set_in_kernel(self): def _create_new_set_in_kernel(self):
"""Create the non-existing set, creating table if needed.""" """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 = [ create_set = [
*self.nft, *self.nft,
'add set {addr_family} {table} {set_} {{ type {type_} ; flags {flags}}}'.format( nft_command
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)
] ]
return_code = CommandExec.run(create_set, allowed_return_codes=(0, 1)) return_code = CommandExec.run(create_set, allowed_return_codes=(0, 1))
if return_code == 0: if return_code == 0:
@ -261,7 +304,33 @@ class NetfilterSet:
.format(len(errors), '",\n"'.join(map(str, errors)))) .format(len(errors), '",\n"'.join(map(str, errors))))
return set_ 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): 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.""" """Change netfilter set content to target set."""
current_set = self.get_netfilter_set_content() current_set = self.get_netfilter_set_content()
if current_set is None: if current_set is None:
@ -272,6 +341,23 @@ class NetfilterSet:
to_add = self._target_content - current_set to_add = self._target_content - current_set
self._change_set_content(delete=to_delete, add=to_add) 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): def _change_set_content(self, delete=None, add=None):
todo = [tuple_ for tuple_ in (('add', add), ('delete', delete)) todo = [tuple_ for tuple_ in (('add', add), ('delete', delete))
if tuple_[1]] if tuple_[1]]
@ -286,6 +372,32 @@ class NetfilterSet:
] ]
CommandExec.run(command) 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): def _get_raw_netfilter_set(self, parse_elements=True):
"""Return a dict describing the netfilter set matching self or None.""" """Return a dict describing the netfilter set matching self or None."""
_, stdout, _ = CommandExec.run_check_output( _, stdout, _ = CommandExec.run_check_output(
@ -301,9 +413,25 @@ class NetfilterSet:
if netfilter_set['name'] != self.name \ if netfilter_set['name'] != self.name \
or netfilter_set['address_family'] != self.address_family \ or netfilter_set['address_family'] != self.address_family \
or netfilter_set['table'] != self.table \ or netfilter_set['table'] != self.table \
or netfilter_set['type'] != [ or not self.has_type(netfilter_set['type']) \
self.TYPES[type_] for type_ in self.type]: or netfilter_set.get('flags', set()) != self.flags:
raise ValueError('Did not get the right set, too wrong to fix.') 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 parse_elements:
if netfilter_set['raw_content']: if netfilter_set['raw_content']:
netfilter_set['content'] = self.validate_set_data(( netfilter_set['content'] = self.validate_set_data((
@ -313,21 +441,79 @@ class NetfilterSet:
netfilter_set['content'] = set() netfilter_set['content'] = set()
return netfilter_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 @staticmethod
def _parse_netfilter_set_string(set_string): def _parse_netfilter_set_string(set_string):
""" """
Parse netfilter set definition and return set as dict. Parse netfilter set definition and return set as dict.
Do not validate content type against detected set type. 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). 'raw_content' keys (all strings, 'raw_content' can be None).
Raise ValueError in case of unexpected syntax. Raise ValueError in case of unexpected syntax.
""" """
# Fragile code since using lexer / parser would be quite heavy # Fragile code since using lexer / parser would be quite heavy
lines = [line.lstrip('\t ') for line in set_string.strip().splitlines()] lines = [line.lstrip('\t ') for line in set_string.strip().splitlines()]
errors = [] errors = []
# 5 lines when empty, 6 with elements = { … } # 5 lines when empty, 6 with elements = { … } + one for flags
if len(lines) not in (5, 6): if len(lines) not in (5, 6, 7):
errors.append('Error, expecting 5 or 6 lines for set definition, ' errors.append('Error, expecting 5 or 6 lines for set definition, '
'got "{}".'.format(set_string)) 'got "{}".'.format(set_string))
@ -346,25 +532,47 @@ class NetfilterSet:
line = next(line_iterator).split(' ') # line #2 line = next(line_iterator).split(' ') # line #2
# 'set <name> {' # '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> ' errors.append('Cannot parse set definition, expecting "set <name> '
'{{", got "{}".' .format(' '.join(line))) '{{", got "{}".' .format(' '.join(line)))
else: else:
set_definition['name'] = line[1] set_definition['name'] = line[1]
line = next(line_iterator).split(' ') # line #3 line, elements_type = next(line_iterator).split(' : ') # line #3
# 'type <type> [. <type>]...' # 'type <type> [. <type>]... : <type> [. <type>]...'
if len(line) < 2 or len(line) % 2 != 0 or line[0] != 'type' \ line = line.split(' ')
or any(element != '.' for element in line[2::2]): 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( errors.append(
'Cannot parse type definition, expecting "type <type> ' 'Cannot parse type definition, expecting "type <type> '
'[. <type>]...", got "{}".'.format(' '.join(line))) '[. <type>]... : <type> [. <type>]...", got "{}".'.format(' '.join(line)))
else: 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 # set is not empty, getting raw elements
if 'flags' in set_definition and len(lines) == 7: # the line unsplitted previously has been used.
line = next(line_iterator) # Unsplit line #4 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] != '}': if line[:13] != 'elements = { ' or line[-1] != '}':
errors.append('Cannot parse set elements, expecting "elements ' errors.append('Cannot parse set elements, expecting "elements '
'= {{ <…>}}", got "{}".'.format(line)) '= {{ <…>}}", got "{}".'.format(line))
@ -372,6 +580,8 @@ class NetfilterSet:
set_definition['raw_content'] = line[13:-1].strip() set_definition['raw_content'] = line[13:-1].strip()
else: else:
set_definition['raw_content'] = None set_definition['raw_content'] = None
else:
set_definition['raw_content'] = None
# last two lines # last two lines
for i in range(2): for i in range(2):
@ -393,22 +603,41 @@ class NetfilterSet:
else: else:
return netfilter_set['content'] 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_): def has_type(self, type_):
"""Check if some type match the set's one.""" """Check if some type match the set's one."""
if self.nft_type == 'set':
return tuple(self.TYPES[t] for t in self.type) == tuple(type_) 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): def manage(self):
"""Create set if needed and populate it with target content.""" """Create set if needed and populate it with target content."""
self.create_in_kernel() self.create_in_kernel()
self._apply_target_content() 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: class Firewall:
"""Manages the firewall using nftables.""" """Manages the firewall using nftables."""
@staticmethod @staticmethod
def manage_sets(sets, address_family=None, table=None, use_sudo=None): 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' address_family = address_family or CONFIG['address_family'] or 'inet'
table = table or CONFIG['table'] or 'filter' table = table or CONFIG['table'] or 'filter'
sudo = use_sudo or (use_sudo is None and CONFIG['use_sudo']) sudo = use_sudo or (use_sudo is None and CONFIG['use_sudo'])

View file

@ -1,9 +1,8 @@
table firewall { table inet firewall {
chain global { chain global {
# Interdiction de l'encapsulation ipv6 dans ipv4 # On accepte les connexions déjà établies
ip protocol 6 drop; ct state established,related accept
# Gestion de l'ICMP : # Gestion de l'ICMP :
# On empêche le ping flood # On empêche le ping flood
icmp type echo-request limit rate over 50/second drop; icmp type echo-request limit rate over 50/second drop;

78
mac_ip.py Normal file
View 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
View file

@ -10,17 +10,8 @@ table ip nat {
chain postrouting { chain postrouting {
type nat hook postrouting priority 100 type nat hook postrouting priority 100
meta oifname != $if_supelec return # ip saddr 10.0.0.0/8 snat to 193.48.225.3
meta oifname $if_supelec snat to 193.48.225.3
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
} }

133
nat.py
View file

@ -30,6 +30,18 @@ CONFIG = ConfigParser()
CONFIG.read('config.ini') 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): def create_nat(name, range_in, range_out, first_port, last_port):
"""Create two nftables tables for the nat: """Create two nftables tables for the nat:
- <name>_address : which link a (or a range of) local address to a - <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: Returns:
(<name>_address, <name>_port) which are NetfilterSet (<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") "is lower than your last_port")
nb_private_by_public = range_in.size / range_out.size nb_private_by_public = range_in.size // range_out.size
nb_port_by_ip = (last_port - first_port + 1) / nb_private_by_public nb_port_by_ip = (last_port - first_port + 1) // nb_private_by_public
ports = [] ports = {}
ips = [] ips = {}
port = first_port port = first_port
for ip, port in range_in: for ip in range_in:
ports.append(( ports[(str(netaddr.IPAddress(ip)),)] = ("%d-%d" % (port, min(port+nb_port_by_ip, 65535)),)
str(netaddr.IPAddress(ip)),
"%d-%d" % (port, port+nb_port_by_ip)
))
port += nb_port_by_ip + 1 port += nb_port_by_ip + 1
if port >= last_port: if port >= last_port:
port = first_port port = first_port
ip = range_in.first ip = range_in.first
for ip_out in range_out: for ip_out in range_out:
ips.append(( ips[('-'.join([
'-'.join([
str(netaddr.IPAddress(ip)), str(netaddr.IPAddress(ip)),
str(netaddr.IPAddress(ip+nb_private_by_public)) str(netaddr.IPAddress(ip+nb_private_by_public))
]), ]),)] = (str(ip_out),)
str(ip_out)
))
ip += nb_private_by_public + 1 ip += nb_private_by_public + 1
return ( return (
NetfilterSet( NetfilterSet(
target_content=ips, target_content=ips,
type_=('IPv4', 'IPv4'), type_=('IPv4',),
name=name, name=name+'_nat_address',
table_name='nat', table_name='nat',
flags=('interval',),
type_from=('IPv4',)
), ),
NetfilterSet( NetfilterSet(
target_content=ports, target_content=ports,
type_=('IPv4', 'port'), type_=('port',),
name=name, name=name+'_nat_port',
table_name='nat', table_name='nat',
flags=('interval',),
type_from=('IPv4',)
), ),
) )
def create_nat_adherent(): def create_nat_adherent():
range_in = netaddr.IPRange(CONFIG['range_in_adherent']) range_in = get_ip_iterable_from_str(CONFIG['NAT']['range_in_adherent'])
range_out = netaddr.IPRange(CONFIG['range_out_adherent']) range_out = get_ip_iterable_from_str(CONFIG['NAT']['range_out_adherent'])
first_port = CONFIG['first_port_adherent'] first_port = int(CONFIG['NAT']['first_port_adherent'])
last_port = CONFIG['last_port_adherent'] last_port = int(CONFIG['NAT']['last_port_adherent'])
return create_nat( return create_nat(
'adherent', 'adherent',
range_in, range_in,
@ -105,10 +115,10 @@ def create_nat_adherent():
def create_nat_federez(): def create_nat_federez():
range_in = netaddr.IPRange(CONFIG['range_in_federez']) range_in = get_ip_iterable_from_str(CONFIG['NAT']['range_in_federez'])
range_out = netaddr.IPRange(CONFIG['range_out_federez']) range_out = get_ip_iterable_from_str(CONFIG['NAT']['range_out_federez'])
first_port = CONFIG['first_port_federez'] first_port = CONFIG['NAT']['first_port_federez']
last_port = CONFIG['last_port_federez'] last_port = CONFIG['NAT']['last_port_federez']
return create_nat( return create_nat(
'federez', 'federez',
range_in, range_in,
@ -119,10 +129,10 @@ def create_nat_federez():
def create_nat_aloes(): def create_nat_aloes():
range_in = netaddr.IPRange(CONFIG['range_in_aloes']) range_in = get_ip_iterable_from_str(CONFIG['NAT']['range_in_aloes'])
range_out = netaddr.IPRange(CONFIG['range_out_aloes']) range_out = get_ip_iterable_from_str(CONFIG['NAT']['range_out_aloes'])
first_port = CONFIG['first_port_aloes'] first_port = CONFIG['NAT']['first_port_aloes']
last_port = CONFIG['last_port_aloes'] last_port = CONFIG['NAT']['last_port_aloes']
return create_nat( return create_nat(
'aloes', 'aloes',
range_in, range_in,
@ -133,10 +143,10 @@ def create_nat_aloes():
def create_nat_admin(): def create_nat_admin():
range_in = netaddr.IPRange(CONFIG['range_in_admin']) range_in = get_ip_iterable_from_str(CONFIG['NAT']['range_in_admin'])
range_out = netaddr.IPRange(CONFIG['range_out_admin']) range_out = get_ip_iterable_from_str(CONFIG['NAT']['range_out_admin'])
first_port = CONFIG['first_port_admin'] first_port = CONFIG['NAT']['first_port_admin']
last_port = CONFIG['last_port_admin'] last_port = CONFIG['NAT']['last_port_admin']
return create_nat( return create_nat(
'admin', 'admin',
range_in, range_in,
@ -147,10 +157,10 @@ def create_nat_admin():
def create_nat_prerezotage(): def create_nat_prerezotage():
range_in = netaddr.IPRange(CONFIG['range_in_prerezotage']) range_in = get_ip_iterable_from_str(CONFIG['NAT']['range_in_prerezotage'])
range_out = netaddr.IPRange(CONFIG['range_out_prerezotage']) range_out = get_ip_iterable_from_str(CONFIG['NAT']['range_out_prerezotage'])
first_port = CONFIG['first_port_prerezotage'] first_port = CONFIG['NAT']['first_port_prerezotage']
last_port = CONFIG['last_port_prerezotage'] last_port = CONFIG['NAT']['last_port_prerezotage']
return create_nat( return create_nat(
'prerezotage', 'prerezotage',
range_in, range_in,
@ -166,23 +176,28 @@ def main():
address.manage() address.manage()
port.manage() port.manage()
logging.info("Done.") logging.info("Done.")
logging.info("Creating federez nat...") #logging.info("Creating federez nat...")
address, port = create_nat_federez() #address, port = create_nat_federez()
address.manage() #address.manage()
port.manage() #port.manage()
logging.info("Done.") #logging.info("Done.")
logging.info("Creating aloes nat...") #logging.info("Creating aloes nat...")
address, port = create_nat_aloes() #address, port = create_nat_aloes()
address.manage() #address.manage()
port.manage() #port.manage()
logging.info("Done.") #logging.info("Done.")
logging.info("Creating admin nat...") #logging.info("Creating admin nat...")
address, port = create_nat_admin() #address, port = create_nat_admin()
address.manage() #address.manage()
port.manage() #port.manage()
logging.info("Done.") #logging.info("Done.")
logging.info("Creating prerezotage nat...") #logging.info("Creating prerezotage nat...")
address, port = create_nat_prerezotage() #address, port = create_nat_prerezotage()
address.manage() #address.manage()
port.manage() #port.manage()
logging.info("Done.") #logging.info("Done.")
if __name__=='__main__':
logging.info('Updating the NAT table.')
main()

1
re2oapi Submodule

@ -0,0 +1 @@
Subproject commit b12df74fe73f351986ff51c8122089644218f8fe

View file

@ -7,8 +7,6 @@ table inet firewall {
} }
chain from_adh { 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} # nft add element nat adherent_nat_address {10.69.0.1-10.69.0.31 : 193.48.225.11}
map adherent_nat_address { map adherent_nat_address {
type ipv4_addr: ipv4_addr type ipv4_addr: ipv4_addr
flags interval
} }
# exemple: 10.69.0.1 : 11135-12834 # exemple: 10.69.0.1 : 11135-12834
# On peut aussi ajouter dynamiquement des éléments : # On peut aussi ajouter dynamiquement des éléments :
# nft add element nat adherent_nat_port {10.69.0.1 : 11135-12834} # 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 { map adherent_nat_port {
type ipv4_addr: inet_service type ipv4_addr: inet_service
flags interval flags interval