diff --git a/security/stunnel/Makefile b/security/stunnel/Makefile
new file mode 100644
index 000000000..d2a57c52b
--- /dev/null
+++ b/security/stunnel/Makefile
@@ -0,0 +1,8 @@
+PLUGIN_NAME= stunnel
+PLUGIN_VERSION= 0.1
+PLUGIN_COMMENT= stunnel TLS proxy
+PLUGIN_MAINTAINER= ad@opnsense.org
+PLUGIN_DEPENDS= stunnel
+PLUGIN_DEVEL= yes
+
+.include "../../Mk/plugins.mk"
diff --git a/security/stunnel/pkg-descr b/security/stunnel/pkg-descr
new file mode 100644
index 000000000..c25f04950
--- /dev/null
+++ b/security/stunnel/pkg-descr
@@ -0,0 +1,2 @@
+Stunnel is a proxy designed to add TLS encryption functionality to existing clients and servers without any changes in the programs' code.
+(https://www.stunnel.org/)
diff --git a/security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc b/security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc
new file mode 100644
index 000000000..822abd73d
--- /dev/null
+++ b/security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc
@@ -0,0 +1,107 @@
+ array('stunnel_refresh_crls')
+ );
+}
+
+function stunnel_refresh_crls()
+{
+ $stunnel = new OPNsense\Stunnel\Stunnel();
+ $configObj = OPNsense\Core\Config::getInstance()->object();
+ foreach ($stunnel->services->service->__items as $service) {
+ if (!empty((string)$service->enabled) && !empty((string)$service->enableCRL)) {
+ foreach (explode(",", (string)$service->cacert) as $cacert) {
+ $this_ca = null;
+ if (!empty($configObj->ca)) {
+ foreach ($configObj->ca as $ca) {
+ if ((string)$ca->refid == $cacert && !empty((string)$ca->prv)) {
+ $this_ca = $ca;
+ }
+ }
+ }
+ if ($this_ca) {
+ $ca_hash = null;
+ $ca_crt = base64_decode((string)$this_ca->crt);
+ $ca_key = base64_decode((string)$this_ca->prv);
+ $process = proc_open("openssl x509 -hash -noout", [["pipe", "r"], ["pipe", "w"]], $pipes);
+ if (is_resource($process)) {
+ fwrite($pipes[0], $ca_crt);
+ fclose($pipes[0]);
+ $ca_hash = trim(stream_get_contents($pipes[1]));
+ fclose($pipes[1]);
+ proc_close($process);
+ }
+ if ($ca_hash) {
+ $crlres = openssl_crl_new($ca_crt, 0, 9999);
+ if (!empty($configObj->crl)) {
+ foreach ($configObj->crl as $crl) {
+ if ($crl->caref == $cacert && !empty((string)$crl->cert)) {
+ foreach ($crl->cert as $cert) {
+ openssl_crl_revoke_cert(
+ $crlres,
+ base64_decode((string)$cert->crt),
+ (string)$cert->revoke_time,
+ (string)$cert->reason
+ );
+ }
+ }
+ }
+ }
+ $crl_text = "";
+ openssl_crl_export($crlres, $crl_text, $ca_key);
+ file_put_contents("/var/run/stunnel/certs/{$ca_hash}.r0", $crl_text);
+ }
+ }
+ }
+ }
+ }
+}
+
+function stunnel_syslog()
+{
+ $logfacilities = array();
+ $logfacilities['stunnel'] = array(
+ 'facility' => array('stunnel')
+ );
+ return $logfacilities;
+}
+
+function stunnel_xmlrpc_sync()
+{
+ $result = array();
+ $result[] = array(
+ 'description' => gettext('Stunnel'),
+ 'section' => 'OPNsense.Stunnel',
+ 'id' => 'stunnel',
+ );
+ return $result;
+}
diff --git a/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServiceController.php b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServiceController.php
new file mode 100644
index 000000000..dc1ba13c9
--- /dev/null
+++ b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServiceController.php
@@ -0,0 +1,42 @@
+getModel()->general->enabled = "0";
+ foreach ($this->getModel()->services->service->__items as $service) {
+ if ((string)$service->enabled == "1") {
+ $this->getModel()->general->enabled = "1";
+ break;
+ }
+ }
+ parent::save();
+ }
+
+ public function searchItemAction()
+ {
+ return $this->searchBase("services.service", array('enabled', 'description'), "description");
+ }
+
+ public function setItemAction($uuid)
+ {
+ return $this->setBase("service", "services.service", $uuid);
+ }
+
+ public function addItemAction()
+ {
+ return $this->addBase("service", "services.service");
+ }
+
+ public function getItemAction($uuid = null)
+ {
+ return $this->getBase("service", "services.service", $uuid);
+ }
+
+ public function delItemAction($uuid)
+ {
+ return $this->delBase("services.service", $uuid);
+ }
+
+ public function toggleItemAction($uuid, $enabled = null)
+ {
+ return $this->toggleBase("services.service", $uuid, $enabled);
+ }
+}
diff --git a/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/ServicesController.php b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/ServicesController.php
new file mode 100644
index 000000000..dfb898991
--- /dev/null
+++ b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/ServicesController.php
@@ -0,0 +1,39 @@
+view->pick('OPNsense/Stunnel/services');
+ $this->view->formDialogService = $this->getForm("dialogService");
+ }
+}
diff --git a/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/forms/dialogService.xml b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/forms/dialogService.xml
new file mode 100644
index 000000000..9d3230280
--- /dev/null
+++ b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/forms/dialogService.xml
@@ -0,0 +1,69 @@
+
diff --git a/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/ACL/ACL.xml b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/ACL/ACL.xml
new file mode 100644
index 000000000..8193662ca
--- /dev/null
+++ b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/ACL/ACL.xml
@@ -0,0 +1,9 @@
+
+
+ Services: Stunnel
+
+ ui/stunnel/*
+ api/stunnel/*
+
+
+
diff --git a/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Menu/Menu.xml b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Menu/Menu.xml
new file mode 100644
index 000000000..6cd50fa8c
--- /dev/null
+++ b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Menu/Menu.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.php b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.php
new file mode 100644
index 000000000..b4075e267
--- /dev/null
+++ b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.php
@@ -0,0 +1,35 @@
+
+ //OPNsense/Stunnel
+ 1.0.0
+ MFP
+
+ OPNsense firewall filter rules
+
+
+
+
+ 1
+ Y
+
+
+
+
+
+ 1
+ Y
+
+
+ 1
+ 65535
+ port needs to be an integer value between 1 and 65535
+ Y
+
+
+ Y
+ N
+ 127.0.0.1
+
+
+ Y
+
+
+ 1
+ 65535
+ port needs to be an integer value between 1 and 65535
+ Y
+
+
+ N
+ Y
+ ca
+ Please select a valid certificate from the list
+
+
+ 0
+ Y
+
+
+ TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,DHE-RSA-AES128-GCM-SHA256,DHE-RSA-AES256-GCM-SHA384
+ Y
+ Y
+ stunnel ssl ciphers
+ /tmp/stunnel_ciphers_list.json
+ 360
+ Please specify valid tls ciphers.
+
+
+ Y
+ cert
+ Please select a valid certificate from the list
+
+
+ N
+ /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){0,255}$/u
+ Description should be a string between 1 and 255 characters
+
+
+
+
+
diff --git a/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt b/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt
new file mode 100644
index 000000000..fd6ba46c4
--- /dev/null
+++ b/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt
@@ -0,0 +1,89 @@
+{#
+ # Copyright (c) 2020 Deciso B.V.
+ # 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.
+ #}
+
+
+
+
+
+
+
+
+
+
+
+ {{ lang._('ID') }}
+ {{ lang._('Enabled') }}
+ {{ lang._('Description') }}
+ {{ lang._('Commands') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ lang._('After changing settings, please remember to apply them with the button below') }}
+
+
+
+
+
+
+
+
+{{ partial("layout_partials/base_dialog",['fields':formDialogService, 'id':'DialogService','label':lang._('Edit Service')])}}
diff --git a/security/stunnel/src/opnsense/scripts/stunnel/generate_certs.php b/security/stunnel/src/opnsense/scripts/stunnel/generate_certs.php
new file mode 100755
index 000000000..b84979a96
--- /dev/null
+++ b/security/stunnel/src/opnsense/scripts/stunnel/generate_certs.php
@@ -0,0 +1,82 @@
+#!/usr/local/bin/php
+object();
+$all_certs = [];
+foreach ($stunnel->services->service->__items as $service) {
+ if (!empty((string)$service->enabled)) {
+ $this_uuid = $service->getAttributes()['uuid'];
+ $srv_certid = (string)$service->servercert;
+ foreach ($configObj->cert as $cert) {
+ if ($srv_certid == (string)$cert->refid) {
+ $all_certs["{$base_path}/{$this_uuid}.crt"] =
+ base64_decode((string)$cert->crt) . "\n" . base64_decode((string)$cert->prv);
+ }
+ }
+ if (!empty((string)$service->cacert)) {
+ $all_certs["{$base_path}/{$this_uuid}.ca"] = "";
+ foreach (explode(",", (string)$service->cacert) as $caid) {
+ foreach ($configObj->ca as $ca) {
+ if ((string)$ca->refid == $caid) {
+ $all_certs["{$base_path}/{$this_uuid}.ca"] .= base64_decode((string)$ca->crt)."\n";
+ }
+ }
+ }
+ }
+ }
+}
+
+if (!is_dir("/usr/local/etc/stunnel/certs")) {
+ mkdir("/usr/local/etc/stunnel/certs", 0700, true);
+ chown("/usr/local/etc/stunnel/certs", "stunnel");
+ chgrp("/usr/local/etc/stunnel/certs", "stunnel");
+}
+
+// cleanup stunnel cert directory
+foreach (glob("{$base_path}/*") as $filename) {
+ if (!isset($all_certs[$filename])) {
+ unlink($filename);
+ }
+}
+
+foreach($all_certs as $filename => $content) {
+ file_put_contents($filename, $content);
+ chown($filename, "stunnel");
+}
+
+// trigger certificate revocation lists update
+plugins_configure('crl');
diff --git a/security/stunnel/src/opnsense/service/conf/actions.d/actions_stunnel.conf b/security/stunnel/src/opnsense/service/conf/actions.d/actions_stunnel.conf
new file mode 100644
index 000000000..ac583ea67
--- /dev/null
+++ b/security/stunnel/src/opnsense/service/conf/actions.d/actions_stunnel.conf
@@ -0,0 +1,29 @@
+[ssl.ciphers]
+command:/usr/local/opnsense/scripts/system/ssl_ciphers.py --format=key_value
+parameters:
+type:script_output
+message:List SSL ciphers
+
+[start]
+command:/usr/local/etc/rc.d/stunnel start
+parameters:
+type:script
+message:stunnel service start
+
+[stop]
+command:/usr/local/etc/rc.d/stunnel stop
+parameters:
+type:script
+message:stunnel service stop
+
+[restart]
+command:/usr/local/etc/rc.d/stunnel restart
+parameters:
+type:script
+message:stunnel service restart
+
+[status]
+command:/usr/local/etc/rc.d/stunnel status; exit 0
+parameters:
+type:script_output
+message:stunnel status
diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/+TARGETS b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/+TARGETS
new file mode 100644
index 000000000..177c36ce4
--- /dev/null
+++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/+TARGETS
@@ -0,0 +1,2 @@
+stunnel.conf:/usr/local/etc/stunnel/stunnel.conf
+rc.conf.d:/etc/rc.conf.d/stunnel
diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/rc.conf.d b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/rc.conf.d
new file mode 100644
index 000000000..874ae1b74
--- /dev/null
+++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/rc.conf.d
@@ -0,0 +1,12 @@
+{% if not helpers.empty('OPNsense.Stunnel.general.enabled') %}
+stunnel_enable="YES"
+stunnel_pidfile="/var/run/stunnel/stunnel.pid"
+
+mkdir -p /var/run/stunnel/certs
+chown -R stunnel:stunnel /var/run/stunnel
+chmod -R 700 /var/run/stunnel
+
+/usr/local/opnsense/scripts/stunnel/generate_certs.php > /dev/null 2>&1
+{% else %}
+stunnel_enable="NO"
+{% endif %}
diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf
new file mode 100644
index 000000000..4f89d541e
--- /dev/null
+++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf
@@ -0,0 +1,44 @@
+setuid = stunnel
+setgid = stunnel
+chroot = /var/run/stunnel
+pid = /stunnel.pid
+debug = info
+logId = unique
+
+
+{% if helpers.exists('OPNsense.Stunnel.services.service') %}
+{% for service in helpers.toList('OPNsense.Stunnel.services.service') %}
+{% if service.enabled|default('0') == '1' %}
+
+
+; **************************************************************************
+; * {{ service.description }}
+; **************************************************************************
+[{{service['@uuid']}}]
+accept = {% if service.accept_address %}{{service.accept_address}}:{% endif %}{{service.accept_port}}
+connect = {% if service.connect_address.find(":") > -1 %}[{{service.connect_address}}]{% else %}{{service.connect_address}}{% endif %}:{{service.connect_port}}
+cert = /usr/local/etc/stunnel/certs/{{service['@uuid']}}.crt
+{% if service.cacert|default('') != '' %}
+CAfile = /usr/local/etc/stunnel/certs/{{service['@uuid']}}.ca
+requireCert = yes
+verifyChain = yes
+{% if service.enableCRL|default('0') == '1' %}
+CRLpath = /certs/
+{% endif %}
+{% endif %}
+{% set ciphers =[] %}
+{% set ciphersuites =[] %}
+{% for cipher in service.ciphers.split(',') %}
+{% if cipher.startswith('TLS') %}
+{% do ciphersuites.append(cipher) %}
+{% else %}
+{% do ciphers.append(cipher) %}
+{% endif %}
+{% endfor %}
+ciphers = {{ ciphers|join(':') }}
+ciphersuites = {{ ciphersuites|join(':') }}
+sslVersionMin=TLSv1.2
+sslVersionMax={% if ciphersuites %}TLSv1.3{% else %}TLSv1.2{% endif %}
+{% endif %}
+{% endfor %}
+{% endif %}
diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Syslog/local/stunnel.conf b/security/stunnel/src/opnsense/service/templates/OPNsense/Syslog/local/stunnel.conf
new file mode 100644
index 000000000..96af65e4d
--- /dev/null
+++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Syslog/local/stunnel.conf
@@ -0,0 +1,6 @@
+###################################################################
+# Local syslog-ng configuration filter definition [stunnel].
+###################################################################
+filter f_local_stunnel {
+ program("stunnel");
+};