mirror of
https://github.com/netbirdio/plugins.git
synced 2026-05-22 18:44:07 -07:00
@@ -33,6 +33,39 @@ function stunnel_configure()
|
||||
);
|
||||
}
|
||||
|
||||
function stunnel_services()
|
||||
{
|
||||
$services = array();
|
||||
$mdl = new \OPNsense\Stunnel\Stunnel();
|
||||
if ($mdl->general->enabled == '1') {
|
||||
$services[] = array(
|
||||
'description' => gettext('Stunnel'),
|
||||
'stunnel' => array(
|
||||
'restart' => array('stunnel restart'),
|
||||
'start' => array('stunnel start'),
|
||||
'stop' => array('stunnel stop'),
|
||||
),
|
||||
'name' => 'stunnel',
|
||||
'pidfile' => '/var/run/stunnel/stunnel.pid',
|
||||
);
|
||||
if ($mdl->general->enable_ident_server == '1') {
|
||||
// only report status from identd seperately, control is combined with stunnel
|
||||
$services[] = array(
|
||||
'description' => gettext('Identd (stunnel)'),
|
||||
'stunnel' => array(
|
||||
'restart' => array('stunnel restart'),
|
||||
'start' => array('stunnel start'),
|
||||
'stop' => array('stunnel stop'),
|
||||
),
|
||||
'name' => 'identd_stunnel',
|
||||
'pidfile' => '/var/run/stunnel_identd.pid',
|
||||
);
|
||||
}
|
||||
}
|
||||
return $services;
|
||||
}
|
||||
|
||||
|
||||
function stunnel_refresh_crls()
|
||||
{
|
||||
$stunnel = new OPNsense\Stunnel\Stunnel();
|
||||
@@ -90,7 +123,7 @@ function stunnel_syslog()
|
||||
{
|
||||
$logfacilities = array();
|
||||
$logfacilities['stunnel'] = array(
|
||||
'facility' => array('stunnel')
|
||||
'facility' => array('stunnel', 'identd_stunnel')
|
||||
);
|
||||
return $logfacilities;
|
||||
}
|
||||
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# $FreeBSD$
|
||||
#
|
||||
# PROVIDE: identd_stunnel
|
||||
# REQUIRE: SERVERS
|
||||
# KEYWORD: shutdown
|
||||
#
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name=identd_stunnel
|
||||
rcvar=identd_stunnel_enable
|
||||
command=/usr/local/opnsense/scripts/stunnel/identd_stunnel.py
|
||||
command_interpreter=/usr/local/bin/python3
|
||||
pidfile="/var/run/${name}.pid"
|
||||
load_rc_config $name
|
||||
|
||||
# Set defaults
|
||||
: ${identd_stunnel_enable:=NO}
|
||||
|
||||
stop_cmd=identd_stunnel_stop
|
||||
|
||||
# kill configd
|
||||
identd_stunnel_stop()
|
||||
{
|
||||
if [ -z "$rc_pid" ]; then
|
||||
[ -n "$rc_fast" ] && return 0
|
||||
_run_rc_notrunning
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -n "Stopping ${name}."
|
||||
# first ask gently to exit
|
||||
kill -15 ${rc_pid}
|
||||
|
||||
# wait max 5 seconds for gentle exit
|
||||
for i in $(seq 1 50);
|
||||
do
|
||||
if [ -z "`/bin/ps -ax | /usr/bin/awk '{print $1;}' | /usr/bin/grep "^${rc_pid}"`" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# kill any remaining identd_stunnel processes (if still running)
|
||||
for identd_stunnel_pid in `/bin/ps -ax | grep 'identd_stunnel.py' | /usr/bin/awk '{print $1;}' `
|
||||
do
|
||||
kill -9 $identd_stunnel_pid >/dev/null 2>&1
|
||||
done
|
||||
|
||||
echo "..done"
|
||||
}
|
||||
|
||||
run_rc_command $1
|
||||
+10
-1
@@ -45,7 +45,7 @@ class ServicesController extends ApiMutableModelControllerBase
|
||||
break;
|
||||
}
|
||||
}
|
||||
parent::save();
|
||||
return parent::save();
|
||||
}
|
||||
|
||||
public function searchItemAction()
|
||||
@@ -77,4 +77,13 @@ class ServicesController extends ApiMutableModelControllerBase
|
||||
{
|
||||
return $this->toggleBase("services.service", $uuid, $enabled);
|
||||
}
|
||||
|
||||
public function getAction()
|
||||
{
|
||||
$result = array();
|
||||
$result[static::$internalModelName] = [
|
||||
"general" => $this->getModel()->general->getNodes()
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -36,5 +36,6 @@ class ServicesController extends IndexController
|
||||
{
|
||||
$this->view->pick('OPNsense/Stunnel/services');
|
||||
$this->view->formDialogService = $this->getForm("dialogService");
|
||||
$this->view->formGeneral = $this->getForm("general");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<form>
|
||||
<field>
|
||||
<id>stunnel.general.chroot</id>
|
||||
<label>chroot service</label>
|
||||
<type>checkbox</type>
|
||||
<help>Start stunnel in it a chroot, although this is a more secure option there are small points of attention before
|
||||
using this. Since system logging is detached after startup, stunnel seems to have difficulties handing syslog configuration changes
|
||||
which need a service restart. If this happens, you need to restart stunnel manually as well.
|
||||
</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>stunnel.general.enable_ident_server</id>
|
||||
<label>enable ident protocol</label>
|
||||
<type>checkbox</type>
|
||||
<help>Enable internal ident service (rfc1413), which tracks authenticated tcp sessions and returns the associated user
|
||||
of the certificate used by stunnel (cn part). When enabled, this service listens on port tcp/113 and accepts port pairs as defined by rfc1413.
|
||||
Make sure you deny untrusted clients access to this facility, usually it only makes sense to allow access from this
|
||||
firewall (allowed by default).
|
||||
</help>
|
||||
</field>
|
||||
</form>
|
||||
@@ -11,6 +11,14 @@
|
||||
<default>1</default>
|
||||
<Required>Y</Required>
|
||||
</enabled>
|
||||
<chroot type="BooleanField">
|
||||
<default>0</default>
|
||||
<Required>Y</Required>
|
||||
</chroot>
|
||||
<enable_ident_server type="BooleanField">
|
||||
<default>0</default>
|
||||
<Required>Y</Required>
|
||||
</enable_ident_server>
|
||||
</general>
|
||||
<services>
|
||||
<service type="ArrayField">
|
||||
|
||||
@@ -35,14 +35,30 @@
|
||||
toggle:'/api/stunnel/services/toggleItem/'
|
||||
}
|
||||
);
|
||||
$("#reconfigureAct").SimpleActionButton();
|
||||
$("#reconfigureAct").SimpleActionButton({
|
||||
onPreAction: function() {
|
||||
const dfObj = new $.Deferred();
|
||||
saveFormToEndpoint("/api/stunnel/services/set", 'frm_general_settings', function(){
|
||||
dfObj.resolve();
|
||||
});
|
||||
return dfObj;
|
||||
}
|
||||
});
|
||||
updateServiceControlUI('stunnel');
|
||||
|
||||
let data_get_map = {'frm_general_settings':"/api/stunnel/services/get"};
|
||||
mapDataToFormUI(data_get_map).done(function(data){
|
||||
formatTokenizersUI();
|
||||
$('.selectpicker').selectpicker('refresh');
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
|
||||
<li class="active"><a data-toggle="tab" id="destinations" href="#tab_services">{{ lang._('Services') }}</a></li>
|
||||
<li><a data-toggle="tab" id="general" href="#tab_general">{{ lang._('General') }}</a></li>
|
||||
</ul>
|
||||
<div class="tab-content content-box">
|
||||
<div id="tab_services" class="tab-pane fade in active">
|
||||
@@ -69,6 +85,10 @@
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div id="tab_general" class="tab-pane">
|
||||
<!-- tab page "general" -->
|
||||
{{ partial("layout_partials/base_form",['fields':formGeneral,'id':'frm_general_settings'])}}
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div id="stunnelChangeMessage" class="alert alert-info" style="display: none" role="alert">
|
||||
{{ lang._('After changing settings, please remember to apply them with the button below') }}
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 =[] %}
|
||||
|
||||
+22
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user