This commit is contained in:
Clément Fiere 2026-04-09 17:17:31 +02:00 committed by GitHub
commit fb7ce721bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1248 additions and 0 deletions

7
net/rtsphelper/Makefile Normal file
View file

@ -0,0 +1,7 @@
PLUGIN_NAME= rtsphelper
PLUGIN_VERSION= 1.2
#PLUGIN_DEPENDS=
PLUGIN_COMMENT= RTSP Port Forwarder Helper
PLUGIN_MAINTAINER= quentin.canel@o2r.fr
.include "../../Mk/plugins.mk"

1
net/rtsphelper/pkg-descr Normal file
View file

@ -0,0 +1 @@
A simple helper script that opens ports to allow mis-configured RTSP servers to work behind NAT environment

View file

@ -0,0 +1,112 @@
<?php
use OPNsense\RTSPHelper\General;
function rtsphelper_enabled()
{
$model = new General();
return (string)$model->general->enabled == '1';
}
function rtsphelper_firewall($fw)
{
if (!rtsphelper_enabled()) {
return;
}
$fw->registerAnchor('rtsphelper', 'rdr');
$fw->registerAnchor('rtsphelper', 'fw');
}
function rtsphelper_services()
{
$services = array();
if (!rtsphelper_enabled()) {
return $services;
}
$pconfig = array();
$pconfig['name'] = 'rtsphelper';
$pconfig['description'] = gettext('RTSP Helper');
$pconfig['php']['restart'] = array('rtsphelper_stop', 'rtsphelper_start');
$pconfig['php']['start'] = array('rtsphelper_start');
$pconfig['php']['stop'] = array('rtsphelper_stop');
$pconfig['pidfile'] = '/var/run/rtsphelper.pid';
$services[] = $pconfig;
return $services;
}
function rtsphelper_start()
{
if (!rtsphelper_enabled()) {
return;
}
if (isvalidpid('/var/run/rtsphelper.pid')) {
return;
}
mwexec_bg('/usr/local/bin/python3 /usr/local/opnsense/scripts/net/rtsphelper/rtsphelper.py');
}
function rtsphelper_stop()
{
killbypid('/var/run/rtsphelper.pid', 'TERM', true);
mwexec('/sbin/pfctl -artsphelper -Fr 2>&1 >/dev/null');
mwexec('/sbin/pfctl -artsphelper -Fn 2>&1 >/dev/null');
}
function rtsphelper_configure()
{
return array('bootup' => array('rtsphelper_configure_do'));
}
function rtsphelper_configure_do($verbose = false)
{
rtsphelper_stop();
if (!rtsphelper_enabled()) {
return;
}
if ($verbose) {
echo 'Starting RTSP Helper...';
flush();
}
$model = new General();
$ext_iface = (string)$model->general->ext_iface;
$ext_ifname = get_real_interface($ext_iface);
// Log a warning if interface couldn't be resolved
// This can happen if the selected interface was deleted from the system
if ($ext_ifname == $ext_iface) {
syslog(LOG_WARNING, "rtsphelper: Interface '{$ext_iface}' could not be resolved to a device name");
}
$config_text = "ext_ifname={$ext_ifname}\n";
/* RTSP Helper access restrictions */
foreach ($model->permissions->permission->iterateItems() as $perm) {
$network = (string)$perm->network;
$port = (string)$perm->port;
$config_text .= "allow={$network} {$port}\n";
}
foreach ($model->hosts->host->iterateItems() as $host) {
$ip = (string)$host->ip;
$port = (string)$host->port;
$config_text .= "forward={$ip}:{$port}\n";
}
/* write out the configuration */
file_put_contents('/var/etc/rtsphelper.conf', $config_text);
rtsphelper_start();
if ($verbose) {
echo "done.\n";
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace OPNsense\RTSPHelper\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Backend;
class ServiceController extends ApiControllerBase
{
public function startAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun('rtsphelper start');
return array("response" => $response);
}
return array("response" => array());
}
public function stopAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun('rtsphelper stop');
return array("response" => $response);
}
return array("response" => array());
}
public function restartAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun('rtsphelper restart');
return array("response" => $response);
}
return array("response" => array());
}
public function statusAction()
{
$backend = new Backend();
$response = $backend->configdRun('rtsphelper status');
return array("status" => trim($response));
}
public function reconfigureAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun('rtsphelper configure');
return array("response" => $response);
}
return array("response" => array());
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace OPNsense\RTSPHelper\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
class SettingsController extends ApiMutableModelControllerBase
{
protected static $internalModelClass = '\OPNsense\RTSPHelper\General';
protected static $internalModelName = 'rtsphelper';
}

View file

@ -0,0 +1,31 @@
<?php
namespace OPNsense\RTSPHelper\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Backend;
class StatusController extends ApiControllerBase
{
public function connectionsAction()
{
$backend = new Backend();
$response = $backend->configdRun('rtsphelper connections');
$rows = array();
foreach (explode("\n", $response) as $line) {
if (preg_match("/on (.*) inet proto (.*) from (.*) to (.*) port = (.*) -> (.*)/", $line, $matches)) {
$rows[] = array(
"interface" => $matches[1],
"proto" => $matches[2],
"source" => $matches[3],
"destination" => $matches[4],
"port" => $matches[5],
"redirect_to" => $matches[6]
);
}
}
return array("rows" => $rows);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace OPNsense\RTSPHelper;
use OPNsense\Base\IndexController;
class SettingsController extends IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/RTSPHelper/index');
$this->view->formGeneral = $this->getForm("general");
$this->view->formDialogHost = $this->getForm("dialog_host");
$this->view->formDialogPermission = $this->getForm("dialog_permission");
}
}

View file

@ -0,0 +1,14 @@
<form>
<field>
<id>host.ip</id>
<label>IP Address</label>
<type>text</type>
<help>Internal IP address.</help>
</field>
<field>
<id>host.port</id>
<label>Port</label>
<type>text</type>
<help>Port number.</help>
</field>
</form>

View file

@ -0,0 +1,14 @@
<form>
<field>
<id>permission.network</id>
<label>Network</label>
<type>text</type>
<help>Network (CIDR) or IP address.</help>
</field>
<field>
<id>permission.port</id>
<label>Port / Range</label>
<type>text</type>
<help>Port or port range (e.g. 1024-65535).</help>
</field>
</form>

View file

@ -0,0 +1,14 @@
<form>
<field>
<id>general.enabled</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable RTSP Helper</help>
</field>
<field>
<id>general.ext_iface</id>
<label>External Interface</label>
<type>dropdown</type>
<help>Select your primary WAN interface.</help>
</field>
</form>

View file

@ -0,0 +1,9 @@
<acl>
<page-service-rtsphelper>
<name>Service: RTSP Helper</name>
<patterns>
<pattern>ui/rtsphelper/*</pattern>
<pattern>api/rtsphelper/*</pattern>
</patterns>
</page-service-rtsphelper>
</acl>

View file

@ -0,0 +1,9 @@
<?php
namespace OPNsense\RTSPHelper;
use OPNsense\Base\BaseModel;
class General extends BaseModel
{
}

View file

@ -0,0 +1,41 @@
<model>
<mount>//OPNsense/RTSPHelper</mount>
<version>1.0.0</version>
<items>
<general>
<enabled type="BooleanField">
<default>0</default>
<Required>Y</Required>
</enabled>
<ext_iface type="InterfaceField">
<Required>Y</Required>
<multiple>N</multiple>
</ext_iface>
</general>
<hosts>
<host type="ArrayField">
<ip type="NetworkField">
<Required>Y</Required>
<ValidationMessage>Please specify a valid IP address.</ValidationMessage>
</ip>
<port type="PortField">
<Required>Y</Required>
<ValidationMessage>Please specify a valid port number.</ValidationMessage>
</port>
</host>
</hosts>
<permissions>
<permission type="ArrayField">
<network type="NetworkField">
<Required>Y</Required>
<ValidationMessage>Please specify a valid network (CIDR) or IP address.</ValidationMessage>
</network>
<port type="TextField">
<Required>Y</Required>
<mask>/^(\d{1,5})(?:-(\d{1,5}))?$/</mask>
<ValidationMessage>Please specify a valid port or port range (e.g. 1024-65535).</ValidationMessage>
</port>
</permission>
</permissions>
</items>
</model>

View file

@ -0,0 +1,7 @@
<menu>
<Services>
<RTSPHelper VisibleName="RTSP Helper" cssClass="fa fa-plug fa-fw">
<Settings url="/ui/rtsphelper/settings"/>
</RTSPHelper>
</Services>
</menu>

View file

@ -0,0 +1,219 @@
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#settings">{{ lang._('Settings') }}</a></li>
<li><a data-toggle="tab" href="#status">{{ lang._('Status') }}</a></li>
</ul>
<div class="tab-content content-box">
<div id="settings" class="tab-pane fade in active">
{{ partial("layout_partials/base_form",['fields':formGeneral,'id':'frm_general_settings'])}}
<hr />
<h3>{{ lang._('Hosts to enable') }}</h3>
<table id="grid-hosts" class="table table-condensed table-hover table-striped" data-editDialog="DialogHost"
data-editAlert="RTSP Helper Host Change">
<thead>
<tr>
<th data-column-id="ip" data-type="string" data-identifier="true">{{ lang._('IP Address') }}</th>
<th data-column-id="port" data-type="string">{{ lang._('Port') }}</th>
<th data-column-id="commands" data-formatter="commands" data-sortable="false">{{ lang._('Commands')
}}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td colspan="5"></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span
class="fa fa-plus"></span></button>
</td>
</tr>
</tfoot>
</table>
<hr />
<h3>{{ lang._('User specified permissions') }}</h3>
<table id="grid-permissions" class="table table-condensed table-hover table-striped"
data-editDialog="DialogPermission" data-editAlert="RTSP Helper Permission Change">
<thead>
<tr>
<th data-column-id="network" data-type="string" data-identifier="true">{{ lang._('Network') }}</th>
<th data-column-id="port" data-type="string">{{ lang._('Port / Range') }}</th>
<th data-column-id="commands" data-formatter="commands" data-sortable="false">{{ lang._('Commands')
}}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td colspan="5"></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span
class="fa fa-plus"></span></button>
</td>
</tr>
</tfoot>
</table>
<div class="col-md-12">
<hr />
<button class="btn btn-primary" id="saveAct" type="button"><b>{{ lang._('Save') }}</b> <i
id="saveAct_progress"></i></button>
<br /><br />
</div>
</div>
<div id="status" class="tab-pane fade">
<table id="grid-status" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="interface" data-type="string">{{ lang._('Interface') }}</th>
<th data-column-id="proto" data-type="string">{{ lang._('Protocol') }}</th>
<th data-column-id="source" data-type="string">{{ lang._('Source') }}</th>
<th data-column-id="destination" data-type="string">{{ lang._('Destination') }}</th>
<th data-column-id="port" data-type="string">{{ lang._('Port') }}</th>
<th data-column-id="redirect_to" data-type="string">{{ lang._('Redirect To') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="col-md-12">
<br />
<button class="btn btn-primary" id="refreshAct" type="button"><b>{{ lang._('Refresh') }}</b></button>
<br /><br />
</div>
</div>
</div>
{{ partial("layout_partials/base_dialog",['fields':formDialogHost,'id':'DialogHost','label':lang._('Edit Host')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogPermission,'id':'DialogPermission','label':lang._('Edit
Permission')])}}
<script>
$(document).ready(function () {
var data_get_map = { 'frm_general_settings': "/api/rtsphelper/settings/get" };
mapDataToFormUI(data_get_map).done(function (data) {
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
});
$("#grid-hosts").bootgrid({
ajax: true,
selection: true,
multiSelect: true,
rowCount: [10, 25, 50, -1],
url: '/api/rtsphelper/settings/searchHost',
formatters: {
"commands": function (column, row) {
return "<button type=\"button\" class=\"btn btn-xs btn-default command-edit bootgrid-tooltip\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-pencil\"></span></button> " +
"<button type=\"button\" class=\"btn btn-xs btn-default command-delete bootgrid-tooltip\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-trash-o\"></span></button>";
}
}
}).on("loaded.rs.jquery.bootgrid", function (e) {
$("#grid-hosts").find(".command-edit").on("click", function (e) {
var uuid = $(this).data("row-id");
mapDataToFormUI({ 'DialogHost': "/api/rtsphelper/settings/getHost/" + uuid }).done(function () {
$("#DialogHost").attr('data-uuid', uuid);
$("#DialogHost").modal({ backdrop: 'static', keyboard: false });
});
});
$("#grid-hosts").find(".command-delete").on("click", function (e) {
var uuid = $(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirm') }}', '{{ lang._('Do you want to delete this host ? ') }}', function () {
ajaxCall(url = "/api/rtsphelper/settings/delHost/" + uuid, sendData = {}, callback = function (data, status) {
$("#grid-hosts").bootgrid("reload");
});
});
});
});
$("#grid-hosts").find("tfoot button[data-action='add']").on("click", function (e) {
$("#DialogHost").attr('data-uuid', '');
$("#DialogHost").modal({ backdrop: 'static', keyboard: false });
$("#DialogHost").find("input").val("");
});
$("#btn_DialogHost_save").unbind('click').click(function () {
var uuid = $("#DialogHost").attr('data-uuid');
var url = "/api/rtsphelper/settings/addHost";
if (uuid) {
url = "/api/rtsphelper/settings/setHost/" + uuid;
}
saveFormToEndpoint(url = url, formid = 'DialogHost', callback_ok = function () {
$("#DialogHost").modal('hide');
$("#grid-hosts").bootgrid("reload");
});
});
$("#grid-permissions").bootgrid({
ajax: true,
selection: true,
multiSelect: true,
rowCount: [10, 25, 50, -1],
url: '/api/rtsphelper/settings/searchPermission',
formatters: {
"commands": function (column, row) {
return "<button type=\"button\" class=\"btn btn-xs btn-default command-edit bootgrid-tooltip\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-pencil\"></span></button> " +
"<button type=\"button\" class=\"btn btn-xs btn-default command-delete bootgrid-tooltip\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-trash-o\"></span></button>";
}
}
}).on("loaded.rs.jquery.bootgrid", function (e) {
$("#grid-permissions").find(".command-edit").on("click", function (e) {
var uuid = $(this).data("row-id");
mapDataToFormUI({ 'DialogPermission': "/api/rtsphelper/settings/getPermission/" + uuid }).done(function () {
$("#DialogPermission").attr('data-uuid', uuid);
$("#DialogPermission").modal({ backdrop: 'static', keyboard: false });
});
});
$("#grid-permissions").find(".command-delete").on("click", function (e) {
var uuid = $(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirm') }}', '{{ lang._('Do you want to delete this permission ? ') }}', function () {
ajaxCall(url = "/api/rtsphelper/settings/delPermission/" + uuid, sendData = {}, callback = function (data, status) {
$("#grid-permissions").bootgrid("reload");
});
});
});
});
$("#grid-permissions").find("tfoot button[data-action='add']").on("click", function (e) {
$("#DialogPermission").attr('data-uuid', '');
$("#DialogPermission").modal({ backdrop: 'static', keyboard: false });
$("#DialogPermission").find("input").val("");
});
$("#btn_DialogPermission_save").unbind('click').click(function () {
var uuid = $("#DialogPermission").attr('data-uuid');
var url = "/api/rtsphelper/settings/addPermission";
if (uuid) {
url = "/api/rtsphelper/settings/setPermission/" + uuid;
}
saveFormToEndpoint(url = url, formid = 'DialogPermission', callback_ok = function () {
$("#DialogPermission").modal('hide');
$("#grid-permissions").bootgrid("reload");
});
});
$("#grid-status").bootgrid({
ajax: true,
selection: false,
multiSelect: false,
rowCount: [10, 25, 50, -1],
url: '/api/rtsphelper/status/connections',
});
$("#saveAct").click(function () {
saveFormToEndpoint(url = "/api/rtsphelper/settings/set", formid = 'frm_general_settings', callback_ok = function () {
$("#saveAct_progress").addClass("fa fa-spinner fa-pulse");
ajaxCall(url = "/api/rtsphelper/service/reconfigure", sendData = {}, callback = function (data, status) {
$("#saveAct_progress").removeClass("fa fa-spinner fa-pulse");
});
});
});
$("#refreshAct").click(function () {
$("#grid-status").bootgrid("reload");
});
});
</script>

View file

@ -0,0 +1,8 @@
#!/usr/local/bin/php
<?php
require_once(__DIR__ . "/../../../../etc/inc/plugins.inc.d/rtsphelper.inc");
require_once("config.inc");
require_once("util.inc");
rtsphelper_configure_do(true);

View file

@ -0,0 +1,291 @@
#!/usr/bin/python
from __future__ import annotations
import socket
import select
import time
import sys
import os
import signal
import subprocess
from typing import Any
buffer_size = 4096
delay = 0.0001
config_file = '/var/etc/rtsphelper.conf'
config: dict[str, Any] = {}
FNULL = open(os.devnull, 'w')
# Type alias for permissions structure: ((mask, net), (port_min, port_max))
PermType = tuple[tuple[int, int], tuple[str, str]]
class Forward:
def __init__(self) -> None:
self.forward: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
def start(self, host: str, port: int) -> socket.socket | bool:
try:
self.forward.connect((host, port))
return self.forward
except Exception as e:
print(e)
return False
class ProxyServer:
input_list: list[Any] = []
channel: dict[Any, Any] = {}
clients: list[list[Any]] = []
forward_to: list[str | int] = []
perms: list[PermType] = []
def __init__(self, remoteHost: str, remotePort: int, portManager: PortManager, perms: list[PermType]) -> None:
self.pm: PortManager = portManager
self.forward_to = [remoteHost, remotePort]
self.perms = perms
self.server: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind(('127.0.0.1', 0))
self.server.listen(200)
self.pm.addLocalBinding(remoteHost, remotePort, self.server.getsockname()[1])
self.input_list.append(self.server)
def main_loop(self) -> None:
ss = select.select
inputready, outputready, exceptready = ss(self.input_list, [], [])
for self.s in inputready:
if self.s == self.server:
self.on_accept()
break
try:
self.data = self.s.recv(buffer_size)
if len(self.data) == 0:
self.on_close()
break
else:
self.on_recv()
except socket.error as e:
self.on_close()
def on_accept(self) -> None:
clientsock, clientaddr = self.server.accept()
if allowedIP(clientaddr[0], self.perms):
forward = Forward().start(self.forward_to[0], self.forward_to[1]) # type: ignore
if forward:
self.clients.append([clientaddr,clientsock,forward])
self.pm.addClient(clientaddr)
self.input_list.append(clientsock)
self.input_list.append(forward)
self.channel[clientsock] = forward
self.channel[forward] = clientsock
else:
print("Can't establish connection with remote server.")
print("Closing connection with client side", clientaddr)
clientsock.close()
else:
print("Forbidden client IP")
clientsock.close()
def on_close(self) -> None:
#remove objects from input_list
self.input_list.remove(self.s)
self.input_list.remove(self.channel[self.s])
out = self.channel[self.s]
# close the connection with client
self.channel[out].close() # equivalent to do self.s.close()
# close the connection with remote server
self.channel[self.s].close()
# delete both objects from channel dict
del self.channel[out]
del self.channel[self.s]
for c in self.clients:
if c[1] == self.s:
break
self.clients.remove(c)
self.pm.removeClient(c[0])
def on_recv(self) -> None:
data = self.data
# here we can parse and/or modify the data before send forward
self.channel[self.s].send(data)
for c in self.clients:
if c[1] == self.s:
self.parseData(data, c)
break
def parseData(self, data: bytes, client: list[Any]) -> None:
for line in data.splitlines():
lineSplit = line.decode().split(':', 1)
if lineSplit[0] == "Transport":
for transportOpt in lineSplit[1].split(';'):
if transportOpt.split('=')[0] == "client_port":
askedPorts = transportOpt.split('=')[1].split('-')
allowedPorts = []
for port in askedPorts:
if allowedPortForward(client[0][0], port, self.perms):
allowedPorts.append(port)
self.pm.updatePorts(client[0], allowedPorts)
class PortManager:
forwardedPorts: dict[Any, list[str]] = {}
localBindings: list[list[str | int]] = []
allowedNets: list[str] = []
def __init__(self, perms: list[list[str]]) -> None:
for perm in perms:
network = perm[0]
self.allowedNets.append(network)
self.removeAll()
self.applyRules()
def addClient(self, client: Any) -> None:
self.forwardedPorts[client] = []
def updatePorts(self, client: Any, ports: list[str]) -> None:
print("Forwarding ports for client " + client[0] + ". New list of ports is: {0}".format(ports))
self.forwardedPorts[client] = ports
self.applyRules()
def removeClient(self, client: Any) -> None:
print("Remove client: " + client[0])
self.forwardedPorts.pop(client)
self.applyRules()
def removeAll(self) -> None:
f = open('/tmp/rtsphelper.rules', 'w')
f.close()
subprocess.call(['pfctl', '-a', 'rtsphelper', '-F', 'nat'], stdout=FNULL, stderr=subprocess.STDOUT)
subprocess.call(['pfctl', '-a', 'rtsphelper', '-F', 'rules'], stdout=FNULL, stderr=subprocess.STDOUT)
subprocess.call(['pfctl', '-a', 'rtsphelper', '-F', 'state'], stdout=FNULL, stderr=subprocess.STDOUT)
def addLocalBinding(self, ip: str, port: int, local_port: int) -> None:
self.localBindings.append([ip, port, local_port])
self.applyRules()
def applyRules(self) -> None:
config_rule_1 = 'rdr inet proto tcp from any to {} port {} -> {} port {}\n'
config_rule_2 = 'block in quick on {} proto tcp from any to {} port {}\n'
config_rule_3 = 'pass in quick proto tcp from {} to {} port {}\n'
pass_rule = 'pass in quick on {} inet proto udp from any to {} port {} keep state label "{}"\n'
rdr_rule = 'rdr on {} inet proto udp from any to any port {} -> {}\n'
f = open('/tmp/rtsphelper.rules', 'w')
for localBinding in self.localBindings:
f.write(config_rule_1.format(localBinding[0], localBinding[1], '127.0.0.1', localBinding[2]))
for client,ports in self.forwardedPorts.items():
ip = client[0]
for port in ports:
f.write(rdr_rule.format(config['ext_if'], port, ip))
f.write('\n')
for localBinding in self.localBindings:
f.write(config_rule_2.format(config['ext_if'], '127.0.0.1', localBinding[2]))
for network in self.allowedNets:
f.write(config_rule_3.format(network, '127.0.0.1', localBinding[2]))
for client,ports in self.forwardedPorts.items():
ip = client[0]
for port in ports:
f.write(pass_rule.format(config['ext_if'], ip, port, 'RTSP'))
f.close()
subprocess.call(['pfctl', '-a', 'rtsphelper', '-f', '/tmp/rtsphelper.rules'], stdout=FNULL)
def writePidFile() -> None:
pid = str(os.getpid())
f = open('/var/run/rtsphelper.pid', 'w')
f.write(pid)
f.close()
def ip_to_u32(ip: str) -> int:
return int(''.join('%02x' % int(d) for d in ip.split('.')), 16)
def allowedIP(ipstr: str, perms: list[PermType]) -> bool:
ip = ip_to_u32(ipstr)
for perm in perms:
mask, net = perm[0]
if ip & mask == net:
return True
return False
def allowedPortForward(ipstr: str, port: str, perms: list[PermType]) -> bool:
if not allowedIP(ipstr, perms):
return False
else:
ip = ip_to_u32(ipstr)
for perm in perms:
mask, net = perm[0]
ports = perm[1]
if ip & mask == net:
if int(port) >= int(ports[0]) and int(port) <= int(ports[1]):
return True
return False
def buildPerms(perms: list[list[str]]) -> list[PermType]:
masks: list[PermType] = []
for perm in perms:
cidr = perm[0]
portRange = perm[1]
if '/' in cidr:
netstr, bits = cidr.split('/')
mask: int = (0xffffffff << (32 - int(bits))) & 0xffffffff
net: int = ip_to_u32(netstr) & mask
else:
mask = 0xffffffff
net = ip_to_u32(cidr)
masks.append(((mask, net), (min(portRange.split('-')[0],portRange.split('-')[1]),max(portRange.split('-')[0],portRange.split('-')[1]))))
return masks
if __name__ == '__main__':
writePidFile()
config['forward_to'] = []
config['perms'] = []
with open(config_file, 'r') as cf:
line = cf.readline()
while line:
key,value = line.strip().split('=')
if key == 'ext_ifname':
config['ext_if'] = value
elif key == 'forward':
config['forward_to'].append([value.split(':')[0],int(value.split(':')[1])])
elif key == 'allow':
config['perms'].append(value.split(' '))
line = cf.readline()
perms = buildPerms(config['perms'])
servers: list[ProxyServer] = []
pm = PortManager(config['perms'])
for forward in config['forward_to']:
servers.append(ProxyServer(forward[0], forward[1], pm, perms))
def handle_exit_signal(sig: int, frame: Any) -> None:
handle_exit()
def handle_exit() -> None:
print("Exiting...")
pm.removeAll()
sys.exit(0)
signal.signal(signal.SIGTERM, handle_exit_signal)
try:
while 1:
time.sleep(delay)
for server in servers:
server.main_loop()
except KeyboardInterrupt:
print("Ctrl C - Stopping server")
handle_exit()
sys.exit(1)

View file

@ -0,0 +1,29 @@
[start]
command:/usr/local/bin/python3 /usr/local/opnsense/scripts/net/rtsphelper/rtsphelper.py
type:script
message:starting rtsphelper
[stop]
command:kill -TERM $(cat /var/run/rtsphelper.pid) 2> /dev/null; /sbin/pfctl -artsphelper -Fr 2> /dev/null; /sbin/pfctl -artsphelper -Fn 2> /dev/null; exit 0
type:script
message:stopping rtsphelper
[restart]
command:kill -TERM $(cat /var/run/rtsphelper.pid) 2> /dev/null; /sbin/pfctl -artsphelper -Fr 2> /dev/null; /sbin/pfctl -artsphelper -Fn 2> /dev/null; /usr/local/bin/python3 /usr/local/opnsense/scripts/net/rtsphelper/rtsphelper.py
type:script
message:restarting rtsphelper
[status]
command:if [ -f /var/run/rtsphelper.pid ] && pgrep -F /var/run/rtsphelper.pid > /dev/null; then echo "running"; else echo "stopped"; fi
type:script_output
message:get rtsphelper status
[connections]
command:/sbin/pfctl -artsphelper -sn 2> /dev/null
type:script_output
message:list rtsphelper connections
[configure]
command:/usr/local/bin/php /usr/local/opnsense/scripts/net/rtsphelper/configure.php
type:script
message:configuring rtsphelper

View file

@ -0,0 +1,279 @@
<?php
require_once("guiconfig.inc");
require_once("interfaces.inc");
require_once("filter.inc");
require_once("system.inc");
require_once("plugins.inc.d/rtsphelper.inc");
function rtsphelper_validate_ip($ip)
{
/* validate cidr */
$ip_array = array();
$ip_array = explode('/', $ip);
if (count($ip_array) == 2) {
if ($ip_array[1] < 1 || $ip_array[1] > 32) {
return false;
}
} elseif (count($ip_array) != 1) {
return false;
}
/* validate ip */
if (!is_ipaddr($ip_array[0])) {
return false;
}
return true;
}
function rtsphelper_validate_forward($forward)
{
$fw_array = array();
$fw_array = explode(':', $forward);
if (!is_ipaddr($fw_array[0])) {
return false;
}
$sub = $fw_array[1];
if ($sub < 0 || $sub > 65535 || !is_numeric($sub)) {
return false;
}
return true;
}
function rtsphelper_validate_port($port)
{
foreach (explode('-', $port) as $sub) {
if ($sub < 0 || $sub > 65535 || !is_numeric($sub)) {
return false;
}
}
return true;
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$pconfig = array();
$copy_fields = array('enable', 'ext_iface');
foreach (rtsphelper_permuser_list() as $permuser) {
$copy_fields[] = $permuser;
}
foreach (rtsphelper_forward_list() as $forward) {
$copy_fields[] = $forward;
}
foreach ($copy_fields as $fieldname) {
if (isset($config['installedpackages']['rtsphelper']['config'][0][$fieldname])) {
$pconfig[$fieldname] = $config['installedpackages']['rtsphelper']['config'][0][$fieldname];
}
}
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input_errors = array();
$pconfig = $_POST;
/* user permissions validation */
foreach (rtsphelper_permuser_list() as $i => $permuser) {
if (!empty($pconfig[$permuser])) {
$perm = explode(' ', $pconfig[$permuser]);
/* should explode to 2 args */
if (count($perm) != 2) {
$input_errors[] = sprintf(gettext("You must follow the specified format in the 'User specified permissions %s' field"), $i);
} else {
/* verify port or port range */
if (!rtsphelper_validate_port($perm[1]) ) {
$input_errors[] = sprintf(gettext("You must specify a port or port range between 0 and 65535 in the 'User specified permissions %s' field"), $i);
}
/* verify ip address */
if (!rtsphelper_validate_ip($perm[0])) {
$input_errors[] = sprintf(gettext("You must specify a valid ip address in the 'User specified permissions %s' field"), $i);
}
}
}
}
foreach (rtsphelper_forward_list() as $i => $forward) {
if (!empty($pconfig[$forward])) {
if (!rtsphelper_validate_forward($pconfig[$forward])) {
$input_errors[] = sprintf(gettext("You must specify a valid ip and port in the 'Hosts to enable %s' field"), $i);
}
}
}
if (count($input_errors) == 0) {
// save form data
$rtsp = array();
// boolean types
foreach (array('enable') as $fieldname) {
$rtsp[$fieldname] = !empty($pconfig[$fieldname]);
}
// text field types
foreach (array('ext_iface') as $fieldname) {
$rtsp[$fieldname] = $pconfig[$fieldname];
}
foreach (rtsphelper_permuser_list() as $fieldname) {
$rtsp[$fieldname] = $pconfig[$fieldname];
}
foreach (rtsphelper_forward_list() as $forward) {
$rtsp[$forward] = $pconfig[$forward];
}
// sync to config
$config['installedpackages']['rtsphelper']['config'] = $rtsp;
write_config('Modified RTSP Helper settings');
rtsphelper_configure_do();
filter_configure();
header(url_safe('Location: /services_rtsphelper.php'));
exit;
}
}
$service_hook = 'rtsphelper';
legacy_html_escape_form_data($pconfig);
include("head.inc");
?>
<body>
<?php include("fbegin.inc"); ?>
<section class="page-content-main">
<div class="container-fluid">
<div class="row">
<?php if (isset($input_errors) && count($input_errors) > 0) print_input_errors($input_errors); ?>
<form method="post" name="iform" id="iform">
<section class="col-xs-12">
<div class="content-box">
<div class="table-responsive">
<table class="table table-striped opnsense_standard_table_form">
<thead>
<tr>
<td style="width:22%">
<strong><?=gettext("RTSP Helper Settings");?></strong>
</td>
<td style="width:78%; text-align:right">
<small><?=gettext("full help"); ?> </small>
<i class="fa fa-toggle-off text-danger" style="cursor: pointer;" id="show_all_help_page"></i>
&nbsp;&nbsp;
</td>
</tr>
</thead>
<tbody>
<tr>
<td><i class="fa fa-info-circle text-muted"></i> <?=gettext("Enable");?></td>
<td>
<input name="enable" type="checkbox" value="yes" <?=!empty($pconfig['enable']) ? "checked=\"checked\"" : ""; ?> />
</td>
</tr>
<tr>
<td><a id="help_for_ext_iface" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("External Interface");?></td>
<td>
<select class="selectpicker" name="ext_iface">
<?php
foreach (get_configured_interface_with_descr() as $iface => $ifacename):?>
<option value="<?=$iface;?>" <?=$pconfig['ext_iface'] == $iface ? "selected=\"selected\"" : "";?>>
<?=htmlspecialchars($ifacename);?>
</option>
<?php
endforeach;?>
</select>
<div class="hidden" data-for="help_for_ext_iface">
<?=gettext("Select only your primary WAN interface (interface with your default route). Only one interface is allowed here, not multiple.");?>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="col-xs-12">
<div class="content-box">
<div class="table-responsive">
<table class="table table-striped opnsense_standard_table_form">
<thead>
<tr>
<th colspan="2"><?=gettext("Hosts to enable");?></th>
</tr>
</thead>
<tbody>
<?php foreach (rtsphelper_forward_list() as $i => $forward): ?>
<tr>
<?php if ($i == 1): ?>
<td style="width:22%"><a id="help_for_forward" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext('Entry') . ' ' . $i ?></td>
<?php else: ?>
<td style="width:22%"><i class="fa fa-info-circle text-muted"></i> <?=gettext('Entry') . ' ' . $i ?></td>
<?php endif ?>
<td style="width:78%">
<input name="<?= html_safe($forward) ?>" type="text" value="<?= $pconfig[$forward] ?>" />
<?php if ($i == 1): ?>
<div class="hidden" data-for="help_for_forward">
<?=gettext("Format: [ip:port]");?><br/>
<?=gettext("Example: 1.2.3.4:554");?>
</div>
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
</div>
</section>
<section class="col-xs-12">
<div class="content-box">
<div class="table-responsive">
<table class="table table-striped opnsense_standard_table_form">
<thead>
<tr>
<th colspan="2"><?=gettext("User specified permissions");?></th>
</tr>
</thead>
<tbody>
<?php foreach (rtsphelper_permuser_list() as $i => $permuser): ?>
<tr>
<?php if ($i == 1): ?>
<td style="width:22%"><a id="help_for_permuser" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext('Entry') . ' ' . $i ?></td>
<?php else: ?>
<td style="width:22%"><i class="fa fa-info-circle text-muted"></i> <?=gettext('Entry') . ' ' . $i ?></td>
<?php endif ?>
<td style="width:78%">
<input name="<?= html_safe($permuser) ?>" type="text" value="<?= $pconfig[$permuser] ?>" />
<?php if ($i == 1): ?>
<div class="hidden" data-for="help_for_permuser">
<?=gettext("Format: [int ipaddr or ipaddr/cdir] [int port or range]");?><br/>
<?=gettext("Example: 192.168.0.0/24 1024-65535");?>
</div>
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
</div>
</section>
<section class="col-xs-12">
<div class="content-box">
<div class="table-responsive">
<table class="table table-striped">
<tbody>
<tr>
<td style="width:22%; vertical-align:top">&nbsp;</td>
<td style="width:78%">
<input name="Submit" type="submit" class="btn btn-primary" value="<?=gettext("Save");?>" />
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</form>
</div>
</div>
</section>
<?php include("foot.inc"); ?>

View file

@ -0,0 +1,80 @@
<?php
require_once("guiconfig.inc");
require_once("interfaces.inc");
require_once("plugins.inc.d/rtsphelper.inc");
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!empty($_POST['clear'])) {
rtsphelper_stop();
rtsphelper_start();
header(url_safe('Location: /status_rtsphelper.php'));
exit;
}
}
$rdr_entries = array();
exec("/sbin/pfctl -artsphelper -sn", $rdr_entries, $pf_ret);
$service_hook = 'rtsphelper';
include("head.inc");
?>
<body>
<?php include("fbegin.inc"); ?>
<section class="page-content-main">
<div class="container-fluid">
<div class="row">
<section class="col-xs-12">
<div class="content-box">
<?php
if (empty($config['installedpackages']['rtsphelper']['config'][0]['enable'])): ?>
<header class="content-box-head container-fluid">
<h3><?= gettext('RTSP Helper is currently disabled.') ?></h3>
</header>
<?php
else: ?>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<td><?=gettext("Internal IP");?></td>
<td><?=gettext("Int. Port");?></td>
</tr>
</thead>
<tbody>
<?php
foreach ($rdr_entries as $rdr_entry):
if (!preg_match("/on (.*) inet proto (.*) from (.*) to (.*) port = (.*) -> (.*)/", $rdr_entry, $matches)) {
continue;
}
$rdr_ip = $matches[6];
$rdr_iport = $matches[5];
?>
<tr>
<td><?=$rdr_ip;?></td>
<td><?=$rdr_iport;?></td>
</tr>
<?php
endforeach;?>
</tbody>
<tfoot>
<tr>
<td colspan="5">
<form method="post">
<button type="submit" name="clear" id="clear" class="btn btn-primary" value="Clear"><?=gettext("Clear");?></button>
<?=gettext("all currently connected sessions");?>.
</form>
</td>
</tr>
</tfoot>
</table>
</div>
<?php
endif; ?>
</div>
</section>
</div>
</div>
</section>
<?php include("foot.inc"); ?>