diff --git a/net/turnserver/Makefile b/net/turnserver/Makefile
new file mode 100644
index 0000000000..dfe3b1ce6e
--- /dev/null
+++ b/net/turnserver/Makefile
@@ -0,0 +1,7 @@
+PLUGIN_NAME= turnserver
+PLUGIN_VERSION= 1.0
+PLUGIN_COMMENT= The coturn STUN/TURN Server
+PLUGIN_DEPENDS= turnserver
+PLUGIN_MAINTAINER= opnsense@moov.de
+
+.include "../../Mk/plugins.mk"
diff --git a/net/turnserver/pkg-descr b/net/turnserver/pkg-descr
new file mode 100644
index 0000000000..f801217423
--- /dev/null
+++ b/net/turnserver/pkg-descr
@@ -0,0 +1,4 @@
+Coturn is a free open source implementation of TURN and STUN Server.
+The TURN Server is a VoIP media traffic NAT traversal server and gateway.
+
+WWW: https://github.com/coturn/coturn
diff --git a/net/turnserver/src/etc/inc/plugins.inc.d/turnserver.inc b/net/turnserver/src/etc/inc/plugins.inc.d/turnserver.inc
new file mode 100644
index 0000000000..26757713ba
--- /dev/null
+++ b/net/turnserver/src/etc/inc/plugins.inc.d/turnserver.inc
@@ -0,0 +1,71 @@
+ gettext('coturn STUN/TURN Server'),
+ 'pidfile' => '/var/run/turnserver.pid',
+ 'configd' => array(
+ 'restart' => array('turnserver restart'),
+ 'start' => array('turnserver start'),
+ 'stop' => array('turnserver stop'),
+ ),
+ 'name' => 'turnserver',
+ );
+
+ return $services;
+}
+
+function turnserver_xmlrpc_sync()
+{
+ $result = array();
+ $result['id'] = 'turnserver';
+ $result['section'] = 'OPNsense.turnserver';
+ $result['description'] = gettext('coturn STUN/TURN Server');
+ $result['services'] = ['turnserver'];
+ return array($result);
+}
diff --git a/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/Api/ServiceController.php b/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/Api/ServiceController.php
new file mode 100644
index 0000000000..3973ff2e60
--- /dev/null
+++ b/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/Api/ServiceController.php
@@ -0,0 +1,42 @@
+view->pick('OPNsense/Turnserver/index');
+ // fetch form data
+ $this->view->settingsForm = $this->getForm("settings");
+ }
+}
diff --git a/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/forms/settings.xml b/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/forms/settings.xml
new file mode 100644
index 0000000000..37f6633cb3
--- /dev/null
+++ b/net/turnserver/src/opnsense/mvc/app/controllers/OPNsense/Turnserver/forms/settings.xml
@@ -0,0 +1,127 @@
+
diff --git a/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/ACL/ACL.xml b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/ACL/ACL.xml
new file mode 100644
index 0000000000..2fa8a37a6d
--- /dev/null
+++ b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/ACL/ACL.xml
@@ -0,0 +1,9 @@
+
+
+ Services: Turnserver
+
+ ui/turnserver/*
+ api/turnserver/*
+
+
+
diff --git a/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Menu/Menu.xml b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Menu/Menu.xml
new file mode 100644
index 0000000000..8b40dc89a1
--- /dev/null
+++ b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Menu/Menu.xml
@@ -0,0 +1,5 @@
+
diff --git a/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.php b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.php
new file mode 100644
index 0000000000..3d004cb77d
--- /dev/null
+++ b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.php
@@ -0,0 +1,53 @@
+settings->enabled === "1") {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.xml b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.xml
new file mode 100644
index 0000000000..1acae266f7
--- /dev/null
+++ b/net/turnserver/src/opnsense/mvc/app/models/OPNsense/Turnserver/Turnserver.xml
@@ -0,0 +1,97 @@
+
+ //OPNsense/turnserver
+ 1.0.0
+ The coturn STUN/TURN Server
+
+
+
+ 0
+ Y
+
+
+ 127.0.0.1
+ ,
+ Y
+ Y
+
+
+ 3478
+ Y
+
+
+ 49152
+ Y
+
+
+ 65535
+ Y
+
+
+ 0
+ Y
+
+
+ N
+ N
+ Please select a valid certificate from the list.
+
+
+ 5349
+ Y
+
+
+ 1
+ Y
+
+
+ N
+ /^.{16,128}$/u
+ Should be a string between 16 and 128 characters.
+
+
+ N
+ /^.{1,128}$/u
+ Should be a string between 1 and 128 characters.
+
+
+ 1
+ Y
+
+
+ 0
+ 0
+ 1000000000
+ Please specify a value between 0 and 1000000000.
+ Y
+
+
+ 0
+ 0
+ 1000000000
+ Please specify a value between 0 and 1000000000.
+ Y
+
+
+ 600
+ 1
+ 1000000000
+ Please specify a value between 1 and 1000000000.
+ Y
+
+
+ 600
+ 1
+ 1000000000
+ Please specify a value between 1 and 1000000000.
+ Y
+
+
+ 300
+ 1
+ 1000000000
+ Please specify a value between 1 and 1000000000.
+ Y
+
+
+
+
diff --git a/net/turnserver/src/opnsense/mvc/app/views/OPNsense/Turnserver/index.volt b/net/turnserver/src/opnsense/mvc/app/views/OPNsense/Turnserver/index.volt
new file mode 100644
index 0000000000..950b9d7685
--- /dev/null
+++ b/net/turnserver/src/opnsense/mvc/app/views/OPNsense/Turnserver/index.volt
@@ -0,0 +1,57 @@
+{#
+
+Copyright (C) 2025 Frank Wall
+OPNsense® is Copyright © 2014 – 2015 by 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.
+
+#}
+
+
+
+
+
+
+
+
+ {{ partial("layout_partials/base_form",['fields':settingsForm,'id':'frm_Settings'])}}
+
+
+
+
+
diff --git a/net/turnserver/src/opnsense/scripts/OPNsense/Turnserver/export_certs.php b/net/turnserver/src/opnsense/scripts/OPNsense/Turnserver/export_certs.php
new file mode 100755
index 0000000000..247d301fee
--- /dev/null
+++ b/net/turnserver/src/opnsense/scripts/OPNsense/Turnserver/export_certs.php
@@ -0,0 +1,61 @@
+#!/usr/local/bin/php
+object();
+if (isset($configObj->OPNsense->turnserver->settings->TlsCertificate) and !empty((string)$configObj->OPNsense->turnserver->settings->TlsCertificate)) {
+ $cert_refid = (string)$configObj->OPNsense->turnserver->settings->TlsCertificate;
+ foreach ((new Cert())->cert->iterateItems() as $cert) {
+ $refid = (string)$cert->refid;
+
+ if ($cert_refid == $refid) {
+ $cert_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->crt)));
+ $pkey_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->prv)));
+
+ if (!empty((string)$cert->caref)) {
+ $ca = CertStore::getCaChain((string)$cert->caref);
+ if ($ca) {
+ $cert_content .= "\n" . $ca;
+ }
+ }
+
+ file_put_contents($cert_filename, $cert_content);
+ file_put_contents($pkey_filename, $pkey_content);
+ chmod($pkey_filename, 0600);
+ }
+ }
+}
diff --git a/net/turnserver/src/opnsense/scripts/OPNsense/Turnserver/setup.sh b/net/turnserver/src/opnsense/scripts/OPNsense/Turnserver/setup.sh
new file mode 100755
index 0000000000..137843af5f
--- /dev/null
+++ b/net/turnserver/src/opnsense/scripts/OPNsense/Turnserver/setup.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+/usr/local/opnsense/scripts/OPNsense/Turnserver/export_certs.php > /dev/null 2>&1
+exit 0
diff --git a/net/turnserver/src/opnsense/service/conf/actions.d/actions_turnserver.conf b/net/turnserver/src/opnsense/service/conf/actions.d/actions_turnserver.conf
new file mode 100644
index 0000000000..372e02aa3b
--- /dev/null
+++ b/net/turnserver/src/opnsense/service/conf/actions.d/actions_turnserver.conf
@@ -0,0 +1,26 @@
+[start]
+command:/usr/local/opnsense/scripts/OPNsense/Turnserver/setup.sh; /usr/local/etc/rc.d/turnserver start
+parameters:
+type:script
+description:Start Turnserver
+message:starting turnserver
+
+[stop]
+command:/usr/local/etc/rc.d/turnserver onestop
+parameters:
+type:script
+description:Stop Turnserver
+message:stopping turnserver
+
+[restart]
+command:/usr/local/opnsense/scripts/OPNsense/Turnserver/setup.sh; /usr/local/etc/rc.d/turnserver restart
+parameters:
+type:script
+description:Restart Turnserver
+message:restarting turnserver
+
+[status]
+command:/usr/local/etc/rc.d/turnserver status || exit 0
+parameters:
+type:script_output
+message:requesting turnserver status
diff --git a/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/+TARGETS b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/+TARGETS
new file mode 100644
index 0000000000..52ca689507
--- /dev/null
+++ b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/+TARGETS
@@ -0,0 +1,2 @@
+turnserver.conf:/usr/local/etc/turnserver.conf
+rc.conf.d:/etc/rc.conf.d/turnserver
diff --git a/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/rc.conf.d b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/rc.conf.d
new file mode 100644
index 0000000000..f292a04861
--- /dev/null
+++ b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/rc.conf.d
@@ -0,0 +1,5 @@
+{% if helpers.exists('OPNsense.turnserver.settings.Enabled') and OPNsense.turnserver.settings.Enabled|default("0") == "1" %}
+turnserver_enable=YES
+{% else %}
+turnserver_enable=NO
+{% endif %}
diff --git a/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/turnserver.conf b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/turnserver.conf
new file mode 100644
index 0000000000..67c96cbabb
--- /dev/null
+++ b/net/turnserver/src/opnsense/service/templates/OPNsense/Turnserver/turnserver.conf
@@ -0,0 +1,60 @@
+# General
+{% if helpers.exists('OPNsense.turnserver.settings.ListenIP') and OPNsense.turnserver.settings.ListenIP|default("") != "" %}
+{% for listenip in OPNsense.turnserver.settings.ListenIP.split(",") %}
+listening-ip={{ listenip }}
+{% endfor %}
+{% endif %}
+listening-port={{ OPNsense.turnserver.settings.ListenPort }}
+min-port={{ OPNsense.turnserver.settings.MinPort }}
+max-port={{ OPNsense.turnserver.settings.MaxPort }}
+
+# TLS
+{% if helpers.exists('OPNsense.turnserver.settings.TlsEnabled') and OPNsense.turnserver.settings.TlsEnabled|default("") == "1" %}
+{% if OPNsense.turnserver.settings.TlsCertificate|default("") != "" %}
+tls-listening-port={{ OPNsense.turnserver.settings.TlsPort }}
+cert=/usr/local/etc/turnserver_cert.pem
+pkey=/usr/local/etc/turnserver_pkey.pem
+{% else %}
+# ERROR: Required TLS certificate was not specified. TLS support will be disabled.
+no-tls
+no-dtls
+{% endif %}
+{% else %}
+no-tls
+no-dtls
+{% endif %}
+
+# Security
+{% if helpers.exists('OPNsense.turnserver.settings.UseAuthSecret') and OPNsense.turnserver.settings.UseAuthSecret|default("") == "1" %}
+{% if OPNsense.turnserver.settings.StaticAuthSecret|default("") != "" %}
+use-auth-secret
+static-auth-secret={{ OPNsense.turnserver.settings.StaticAuthSecret }}
+{% else %}
+# ERROR: Required Auth Secret was not specified; this feature will be disabled.
+{% endif %}
+{% endif %}
+
+# Features
+{% if OPNsense.turnserver.settings.Realm|default("") != "" %}
+realm={{ OPNsense.turnserver.settings.Realm }}
+{% endif %}
+{% if OPNsense.turnserver.settings.FingerprintsEnabled|default("") == "1" %}
+fingerprint
+{% endif %}
+
+# Tuning
+user-quota={{ OPNsense.turnserver.settings.UserQuota }}
+total-quota={{ OPNsense.turnserver.settings.TotalQuota }}
+stale-nonce={{ OPNsense.turnserver.settings.StaleNonce }}
+channel-lifetime={{ OPNsense.turnserver.settings.ChannelLifetime }}
+permission-lifetime={{ OPNsense.turnserver.settings.PermissionLifetime }}
+
+# Defaults
+no-cli
+no-software-attribute
+no-multicast-peers
+no-tlsv1
+no-tlsv1_1
+no-rfc5780
+no-stun-backward-compatibility
+response-origin-only-with-rfc5780