+
+ {{ partial("layout_partials/base_form",['fields':formGeneral,'id':'frm_general_settings'])}}
+
{{ lang._('After changing settings, please remember to apply them with the button below') }}
diff --git a/security/stunnel/src/opnsense/scripts/stunnel/identd_stunnel.py b/security/stunnel/src/opnsense/scripts/stunnel/identd_stunnel.py
new file mode 100755
index 000000000..179103aeb
--- /dev/null
+++ b/security/stunnel/src/opnsense/scripts/stunnel/identd_stunnel.py
@@ -0,0 +1,183 @@
+#!/usr/local/bin/python3
+
+import os
+import sys
+import argparse
+import syslog
+import socketserver
+import glob
+import time
+import traceback
+sys.path.insert(0, "/usr/local/opnsense/site-python")
+from daemonize import Daemonize
+
+class StunnelLog:
+ # ident log file location
+ base_log_path = "/var/run/stunnel/logs"
+ # maximum session length (after detect) in seconds
+ session_max_ttl = 600
+ # number of seconds after receiving "Connection closed" to remove session from cache
+ session_grace_period = 60
+ # amount of time in ms to wait before concluding a user is not found.
+ # generally intened to prevent syslog latency leading to false access denied statements
+ log_flush_grace_period_ms = 250
+ # time in ms to wait between polls (when initial fetch didn't result in an authenticated session)
+ log_flush_poll_interval_ms = 0.5
+
+ def __init__(self):
+ self._filename = None
+ self._fhandle = None
+ self._last_pos = None
+ self._local_cache = dict()
+ self._open()
+
+ def _open(self):
+ """ open last log file, also responsible for log rotate
+ """
+ filenames = sorted(glob.glob("%s/stunnel_ident_*.log" % self.base_log_path), reverse=True)
+ if len(filenames) > 0 and self._filename != filenames[0]:
+ self._filename = filenames[0]
+ self._last_pos = None
+ try:
+ self._fhandle = open(self._filename, 'r')
+ except IOError:
+ self._fhandle = None
+
+ # cleanup after rotate
+ if len(filenames) > 1:
+ for filename in filenames[1:]:
+ os.remove(filename)
+
+ def parse(self, search_key):
+ """ parse log file and detect new connected clients and the ones leaving (connection closed).
+ Accounts connections in self._local_cache (in address:source_port format)
+ :param search_key: when search_key isn't found, execute another pass to detect log-rotates
+ """
+ # we might need another pass
+ for i in range(2):
+ current_timestamp = time.time()
+ if self._fhandle is not None:
+ if self._last_pos is not None:
+ self._fhandle.seek(self._last_pos)
+
+ while True:
+ line = self._fhandle.readline()
+ if line:
+ # track session id's, which ease debugging (see logId setting in stunnel)
+ session_id = None
+ if line.find('[') > -1:
+ session_id = line.split('[')[1].split(']')[0]
+ if line.find('IDENT Service') > -1:
+ # Ident log line, username (CN=) is currently returned when an ident call is made
+ cert_subject = line.split('-->')[1].strip()
+ username = cert_subject[cert_subject.find('CN=')+3:].strip()
+ src = line.split(' from ')[1].split()[0]
+ self._local_cache[src] = {
+ 'username': username,
+ 'cn': cert_subject,
+ 'session_id': session_id,
+ 'expire': current_timestamp + self.session_max_ttl
+ }
+ elif line.find('Connection closed') > -1 and line.find('[') > -1:
+ # Connection closed lines are used to trigger cleanups in two stages
+ # 1. push expire to now() + session_grace_period
+ # 2. when expired, delete from cache
+ for src in list(self._local_cache):
+ is_expired = current_timestamp > self._local_cache[src]['expire']
+ if session_id == self._local_cache[src]['session_id']:
+ self._local_cache[src]['expire'] = current_timestamp + self.session_grace_period
+ elif is_expired:
+ del self._local_cache[src]
+ else:
+ break
+ self._last_pos = self._fhandle.tell()
+ if search_key in self._local_cache:
+ break
+ else:
+ # possible log rotate (new file)
+ self._open()
+
+
+ def whois(self, src_port, dst_port, address):
+ """ try to resolve user at src_port:address:dst_port for max log_flush_grace_period_ms time
+ :param src_port: source port
+ :param dst_port: destination port
+ :param address: address, usually the address stunnel connected to (target hostname)
+ :return: username or False if not found
+ """
+ search_key = "%s:%s" % (address, src_port)
+ start_time = time.time()
+ while True:
+ self.parse(search_key)
+ if search_key in self._local_cache:
+ return self._local_cache[search_key]['username']
+ elif (time.time() - start_time) * 1000.0 > self.log_flush_grace_period_ms:
+ break
+ else:
+ time.sleep(self.log_flush_poll_interval_ms/1000.0)
+
+ return False
+
+
+class RequestHandler(socketserver.StreamRequestHandler):
+ _stunnel_log = None
+
+ @staticmethod
+ def stunnel_ident(src_port, dst_port, address):
+ if RequestHandler._stunnel_log is None:
+ RequestHandler._stunnel_log = StunnelLog()
+
+ return RequestHandler._stunnel_log.whois(src_port, dst_port, address)
+
+ def handle(self):
+ """ connection handler, strip src/dst port pairs, resolve and return
+ """
+ start_time = time.time()
+ src_port, dst_port = [0, 0]
+ try:
+ req_data = self.rfile.readline().decode().strip()
+ src_port, dst_port = [ int(x.strip()) for x in req_data.split(',') ]
+ if src_port < 1 or src_port > 65535 or dst_port < 1 or dst_port > 65535:
+ syslog.syslog(syslog.LOG_WARNING, 'INVALID-PORT %d,%s,%d.' % (
+ src_port, self.client_address[0], dst_port
+ ))
+ self.wfile.write('{}, {} : ERROR : INVALID-PORT\r\n'.format(src_port, dst_port).encode())
+ else:
+ username = self.stunnel_ident(src_port, dst_port, self.client_address[0])
+ req_latency_ms = (time.time() - start_time) * 1000.0
+ if not username:
+ syslog.syslog(syslog.LOG_WARNING, 'NO-USER %d,%s,%d (%0.05f ms).' % (
+ src_port, self.client_address[0], dst_port, req_latency_ms
+ ))
+ self.wfile.write('{}, {} : ERROR : NO-USER\r\n'.format(src_port, dst_port).encode())
+ else:
+ syslog.syslog(syslog.LOG_NOTICE, 'USERID %d,%s,%d = %s (%0.05f ms).' % (
+ src_port, self.client_address[0], dst_port, username, req_latency_ms
+ ))
+ self.wfile.write('{}, {} : USERID : OTHER : {}\r\n'.format(src_port, dst_port, username).encode())
+ except:
+ self.wfile.write('{}, {} : ERROR : UNKNOWN-ERROR\r\n'.format(src_port, dst_port).encode())
+ syslog.syslog(syslog.LOG_ERR, traceback.format_exc().replace('\n', ' '))
+
+
+def run_listener():
+ server = socketserver.TCPServer(('0.0.0.0', 113), RequestHandler, bind_and_activate=False)
+ server.allow_reuse_address = True
+ server.server_bind()
+ server.server_activate()
+ server.serve_forever()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--foreground', help='run in forground', default=False, action='store_true')
+ inputargs = parser.parse_args()
+
+ syslog.openlog('identd_stunnel', logoption=syslog.LOG_DAEMON, facility=syslog.LOG_LOCAL4)
+
+ if inputargs.foreground:
+ run_listener()
+ else:
+ syslog.syslog(syslog.LOG_NOTICE, 'daemonize stunnel_identd.')
+ daemon = Daemonize(app="identd_stunnel", pid='/var/run/identd_stunnel.pid', action=run_listener)
+ daemon.start()
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
index ac583ea67..e0dc8de69 100644
--- a/security/stunnel/src/opnsense/service/conf/actions.d/actions_stunnel.conf
+++ b/security/stunnel/src/opnsense/service/conf/actions.d/actions_stunnel.conf
@@ -5,25 +5,25 @@ type:script_output
message:List SSL ciphers
[start]
-command:/usr/local/etc/rc.d/stunnel start
+command:/usr/local/etc/rc.d/stunnel start; /usr/local/etc/rc.d/identd_stunnel start; exit 0
parameters:
type:script
message:stunnel service start
[stop]
-command:/usr/local/etc/rc.d/stunnel stop
+command:/usr/local/etc/rc.d/stunnel stop; /usr/local/etc/rc.d/identd_stunnel stop; exit 0
parameters:
type:script
message:stunnel service stop
[restart]
-command:/usr/local/etc/rc.d/stunnel restart
+command:/usr/local/etc/rc.d/stunnel restart; /usr/local/etc/rc.d/identd_stunnel restart; exit 0
parameters:
type:script
message:stunnel service restart
[status]
-command:/usr/local/etc/rc.d/stunnel status; exit 0
+command:/usr/local/etc/rc.d/stunnel status; /usr/local/etc/rc.d/identd_stunnel onestatus; 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
index 177c36ce4..771bf9bab 100644
--- a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/+TARGETS
+++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/+TARGETS
@@ -1,2 +1,4 @@
stunnel.conf:/usr/local/etc/stunnel/stunnel.conf
rc.conf.d:/etc/rc.conf.d/stunnel
+identd.rc.conf.d:/etc/rc.conf.d/identd_stunnel
+syslog-ng-stunnel-ident.conf:/usr/local/etc/syslog-ng.conf.d/syslog-ng-stunnel-ident.conf
diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/identd.rc.conf.d b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/identd.rc.conf.d
new file mode 100644
index 000000000..8596cfe5b
--- /dev/null
+++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/identd.rc.conf.d
@@ -0,0 +1,6 @@
+{% if not helpers.empty('OPNsense.Stunnel.general.enabled') and
+ not helpers.empty('OPNsense.Stunnel.general.enable_ident_server') %}
+identd_stunnel_enable="YES"
+{% else %}
+identd_stunnel_enable="NO"
+{% endif %}
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
index 874ae1b74..9c3da50ff 100644
--- a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/rc.conf.d
+++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/rc.conf.d
@@ -3,6 +3,7 @@ stunnel_enable="YES"
stunnel_pidfile="/var/run/stunnel/stunnel.pid"
mkdir -p /var/run/stunnel/certs
+mkdir -p /var/run/stunnel/logs
chown -R stunnel:stunnel /var/run/stunnel
chmod -R 700 /var/run/stunnel
diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf
index 4f89d541e..1179e6bf1 100644
--- a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf
+++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf
@@ -1,7 +1,9 @@
setuid = stunnel
setgid = stunnel
+{% if not helpers.empty('OPNsense.Stunnel.general.chroot') %}
chroot = /var/run/stunnel
-pid = /stunnel.pid
+{% endif %}
+pid = {% if helpers.empty('OPNsense.Stunnel.general.chroot') %}/var/run/stunnel{% endif %}/stunnel.pid
debug = info
logId = unique
@@ -23,7 +25,7 @@ CAfile = /usr/local/etc/stunnel/certs/{{service['@uuid']}}.ca
requireCert = yes
verifyChain = yes
{% if service.enableCRL|default('0') == '1' %}
-CRLpath = /certs/
+CRLpath = {% if helpers.empty('OPNsense.Stunnel.general.chroot') %}/var/run/stunnel{% endif %}/certs/
{% endif %}
{% endif %}
{% set ciphers =[] %}
diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/syslog-ng-stunnel-ident.conf b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/syslog-ng-stunnel-ident.conf
new file mode 100644
index 000000000..7e9c4b723
--- /dev/null
+++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/syslog-ng-stunnel-ident.conf
@@ -0,0 +1,22 @@
+destination d_stunnel_ident {
+ file(
+ "/var/run/stunnel/logs/stunnel_ident_${YEAR}${MONTH}${DAY}.log"
+ flush-lines(0)
+ );
+};
+
+filter f_stunnel_ident {
+ program("stunnel")
+ and (
+ message(".*IDENT.*")
+ or
+ message(".*Connection closed.*")
+ );
+};
+
+
+log {
+ source(s_all);
+ filter(f_stunnel_ident);
+ destination(d_stunnel_ident);
+};