diff --git a/plist b/plist index 71b3ae2bd..e5ed70510 100644 --- a/plist +++ b/plist @@ -335,6 +335,7 @@ /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/KeyPairsController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/LeasesController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/LegacySubsystemController.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/ManualSpdController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/PoolsController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/PreSharedKeysController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/SadController.php @@ -359,6 +360,7 @@ /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPSK.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPool.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogRemote.xml +/usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogSPD.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogVTI.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/LoopbackSettingsController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VipSettingsController.php @@ -600,6 +602,7 @@ /usr/local/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/IKEAdressField.php /usr/local/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/IPsecProposalField.php /usr/local/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/PoolsField.php +/usr/local/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/SPDField.php /usr/local/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/VTIField.php /usr/local/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.php /usr/local/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.xml @@ -884,12 +887,14 @@ /usr/local/opnsense/scripts/ipsec/connect.py /usr/local/opnsense/scripts/ipsec/disconnect.py /usr/local/opnsense/scripts/ipsec/get_legacy_vti.php +/usr/local/opnsense/scripts/ipsec/lib/__init__.py /usr/local/opnsense/scripts/ipsec/list_leases.py /usr/local/opnsense/scripts/ipsec/list_sad.py /usr/local/opnsense/scripts/ipsec/list_spd.py /usr/local/opnsense/scripts/ipsec/list_status.py /usr/local/opnsense/scripts/ipsec/saddelete.py /usr/local/opnsense/scripts/ipsec/spddelete.py +/usr/local/opnsense/scripts/ipsec/updown_event.py /usr/local/opnsense/scripts/netflow/dump_log.py /usr/local/opnsense/scripts/netflow/export_details.py /usr/local/opnsense/scripts/netflow/flowctl_stats.py @@ -1043,6 +1048,8 @@ /usr/local/opnsense/service/templates/OPNsense/IPFW/ipfw_rule_tables.conf /usr/local/opnsense/service/templates/OPNsense/IPFW/rc.conf.d /usr/local/opnsense/service/templates/OPNsense/IPFW/rules.macro +/usr/local/opnsense/service/templates/OPNsense/IPsec/+TARGETS +/usr/local/opnsense/service/templates/OPNsense/IPsec/reqid_events.conf /usr/local/opnsense/service/templates/OPNsense/Macros/interface.macro /usr/local/opnsense/service/templates/OPNsense/Monit/+TARGETS /usr/local/opnsense/service/templates/OPNsense/Monit/monitrc diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.php b/src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.php index 96603f67f..8264e2721 100644 --- a/src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.php +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.php @@ -174,6 +174,12 @@ class Swanctl extends BaseModel if (!isset($data['connections'][$parent]['children'])) { $data['connections'][$parent]['children'] = []; } + if (!empty($thisnode['reqid'])) { + // trigger updown event handler when a reqid is set. + // currently this only handles manual spd entries, we may extend/refactor the script later + // if needed + $thisnode['updown'] = '/usr/local/opnsense/scripts/ipsec/updown_event.py'; + } $data['connections'][$parent]['children'][$node_uuid] = $thisnode; } else { $data['connections'][$parent][$key . '-' . $node_uuid] = $thisnode; diff --git a/src/opnsense/scripts/ipsec/lib/__init__.py b/src/opnsense/scripts/ipsec/lib/__init__.py new file mode 100644 index 000000000..35758a080 --- /dev/null +++ b/src/opnsense/scripts/ipsec/lib/__init__.py @@ -0,0 +1,57 @@ +""" + Copyright (c) 2022 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +""" +import subprocess + + +def list_spds(req_id=None, automatic=True): + result = [] + setkey_text = subprocess.run(['/sbin/setkey', '-PD'], capture_output=True, text=True).stdout.strip() + this_record = None + line_num = 0 + for line in setkey_text.split('\n'): + if not line.startswith('\t') and len(line) > 10: + parts = line.replace('[', ' ').replace(']', ' ').split() + this_record = { + 'src': parts[0], + 'dst': parts[2], + 'automatic': False, + 'reqid': None + } + line_num = 0 + elif this_record is None: + continue + elif line_num == 1: + this_record['direction'] = line.split()[0] + elif line.startswith('\tlifetime:') and line.find('ifname=') > 0: + this_record['automatic'] = True + elif line.find('/unique:') > 0: + this_record['reqid'] = line.split('/unique:')[1].split()[0] + elif line.startswith('\trefcnt'): + if (automatic or not this_record['automatic']) and (req_id is None or req_id == this_record['reqid']): + result.append(this_record) + line_num += 1 + + return result diff --git a/src/opnsense/scripts/ipsec/updown_event.py b/src/opnsense/scripts/ipsec/updown_event.py new file mode 100755 index 000000000..c895e0452 --- /dev/null +++ b/src/opnsense/scripts/ipsec/updown_event.py @@ -0,0 +1,88 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2022 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + -------------------------------------------------------------------------------------- + handle swanctl.conf updown event +""" +import os +import json +import subprocess +import argparse +import tempfile +from configparser import ConfigParser +from lib import list_spds + +spd_filename = '/usr/local/etc/swanctl/reqid_events.conf' + +spd_add_cmd = 'spdadd -%(ipproto)s %(source)s %(destination)s any ' \ + '-P out ipsec %(protocol)s/tunnel/%(local)s-%(remote)s/unique:%(reqid)s;' + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--reqid', default=os.environ.get('PLUTO_REQID')) + parser.add_argument('--local', default=os.environ.get('PLUTO_ME')) + parser.add_argument('--remote', default=os.environ.get('PLUTO_PEER')) + parser.add_argument('--action', default=os.environ.get('PLUTO_VERB')) + cmd_args = parser.parse_args() + # init spd's on up-host[-v6], up-client[-v6] + if cmd_args.action and cmd_args.action.startswith('up'): + if os.path.exists(spd_filename): + cnf = ConfigParser() + cnf.read(spd_filename) + conf_section = 'spd_%s' % cmd_args.reqid + if cnf.has_section(conf_section): + spds = {} + for opt in cnf.options(conf_section): + if opt.count('_') == 1: + tmp = opt.split('_') + seqid = tmp[1] + if seqid not in spds: + spds[seqid] = { + 'reqid': cmd_args.reqid, + 'local' : cmd_args.local, + 'remote' : cmd_args.remote, + 'destination': os.environ.get('PLUTO_PEER_CLIENT') + } + if cnf.get(conf_section, opt).strip() != '': + spds[seqid][tmp[0]] = cnf.get(conf_section, opt).strip() + # (re)aaply manual policies if specified + cur_spds = list_spds(req_id=cmd_args.reqid, automatic=False) + set_key = [] + for spd in cur_spds: + set_key.append('spddelete -n %(src)s %(dst)s any -P %(direction)s;' % spd) + for spd in spds.values(): + if None in spd.values(): + # incomplete, skip + continue + spd['ipproto'] = '4' if spd.get('source', '').find(':') == -1 else '6' + set_key.append(spd_add_cmd % spd) + if len(set_key) > 0: + f = tempfile.NamedTemporaryFile(mode='wt', delete=False) + f.write('\n'.join(set_key)) + f.close() + subprocess.run(['/sbin/setkey', '-f', f.name], capture_output=True, text=True) + os.unlink(f.name) diff --git a/src/opnsense/service/templates/OPNsense/IPsec/+TARGETS b/src/opnsense/service/templates/OPNsense/IPsec/+TARGETS new file mode 100644 index 000000000..cc9ee1a4f --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/IPsec/+TARGETS @@ -0,0 +1 @@ +reqid_events.conf:/usr/local/etc/swanctl/reqid_events.conf diff --git a/src/opnsense/service/templates/OPNsense/IPsec/reqid_events.conf b/src/opnsense/service/templates/OPNsense/IPsec/reqid_events.conf new file mode 100644 index 000000000..ac1f1ba52 --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/IPsec/reqid_events.conf @@ -0,0 +1,21 @@ +# +# Automatic generated configuration for IPsec. +# Do not edit this file manually. +# + +{% set prev_spd = {'reqid' : None } %} +{% for spd in helpers.toList('OPNsense.Swanctl.SPDs.SPD', 'reqid', 'int') %} +{% if spd.enabled == '1' %} +{% if prev_spd.reqid != spd.reqid %} + +[spd_{{spd.reqid}}] +{% endif %} +protocol_{{loop.index}}={{spd.protocol}} +reqid_{{loop.index}}={{spd.reqid}} +source_{{loop.index}}={{spd.source}} +destination_{{loop.index}}={{spd.destination}} +description_{{loop.index}}={{spd.description}} +uuid_{{loop.index}}={{spd['uuid']}} +{% do prev_spd.update({'reqid': spd.reqid}) %} +{% endif %} +{% endfor %}