diff --git a/security/tailscale/Makefile b/security/tailscale/Makefile
new file mode 100644
index 000000000..b995e68c9
--- /dev/null
+++ b/security/tailscale/Makefile
@@ -0,0 +1,8 @@
+PLUGIN_NAME= tailscale
+PLUGIN_VERSION= 1.0.d
+PLUGIN_DEVEL= yes
+PLUGIN_COMMENT= Tailscale makes creating software-defined networks easy
+PLUGIN_DEPENDS= tailscale
+PLUGIN_MAINTAINER= sam@sheridan.co.uk
+
+.include "../../Mk/plugins.mk"
diff --git a/security/tailscale/pkg-descr b/security/tailscale/pkg-descr
new file mode 100644
index 000000000..9befef3b6
--- /dev/null
+++ b/security/tailscale/pkg-descr
@@ -0,0 +1,11 @@
+Tailscale is a mesh VPN alternative, based on WireGuard, that connects your
+computers, databases, and services together securely without any proxies.
+
+https://tailscale.com/
+
+Plugin Changelog
+================
+
+1.0
+
+* initial development release
diff --git a/security/tailscale/src/etc/inc/plugins.inc.d/tailscale.inc b/security/tailscale/src/etc/inc/plugins.inc.d/tailscale.inc
new file mode 100644
index 000000000..dc1bf6b0b
--- /dev/null
+++ b/security/tailscale/src/etc/inc/plugins.inc.d/tailscale.inc
@@ -0,0 +1,65 @@
+enabled);
+}
+
+function tailscale_services()
+{
+ $services = [];
+
+ if (!tailscale_enabled()) {
+ return $services;
+ }
+
+ $services[] = [
+ 'description' => gettext('Tailscale'),
+ 'pidfile' => '/var/run/tailscaled.pid',
+ 'configd' => [
+ 'restart' => ['tailscale restart'],
+ 'start' => ['tailscale start'],
+ 'stop' => ['tailscale stop'],
+ ],
+ 'name' => 'tailscale',
+ ];
+
+ return $services;
+}
+
+function tailscale_devices()
+{
+ return [[
+ 'pattern' => '^tailscale0$',
+ 'configurable' => false,
+ 'spoofmac' => false,
+ 'volatile' => true,
+ ]];
+}
+
diff --git a/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/Api/AuthenticationController.php b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/Api/AuthenticationController.php
new file mode 100644
index 000000000..0161b75b6
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/Api/AuthenticationController.php
@@ -0,0 +1,39 @@
+searchBase("subnets.subnet4", null, "subnet");
+ }
+ public function setSubnetAction($uuid)
+ {
+ return $this->setBase("subnet4", "subnets.subnet4", $uuid);
+ }
+ public function addSubnetAction()
+ {
+ return $this->addBase("subnet4", "subnets.subnet4");
+ }
+
+ public function getSubnetAction($uuid = null)
+ {
+ return $this->getBase("subnet4", "subnets.subnet4", $uuid);
+ }
+
+ public function delSubnetAction($uuid)
+ {
+ return $this->delBase("subnets.subnet4", $uuid);
+ }
+
+ public function reloadAction()
+ {
+ $mdl = $this->getModel();
+ $enabled = $mdl->enabled->__toString() === '1';
+ $response = $this->toggleTailScaleService($enabled);
+ return ['result ' => $response];
+ }
+
+ private function toggleTailscaleService($enabled)
+ {
+ $backend = new Backend();
+ $backend->configdRun('template reload OPNsense/Tailscale');
+ $action = $enabled ? 'start' : 'stop';
+ return trim($backend->configdRun('tailscale ' . $action));
+ }
+}
diff --git a/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/Api/StatusController.php b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/Api/StatusController.php
new file mode 100644
index 000000000..f3688ac9c
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/Api/StatusController.php
@@ -0,0 +1,65 @@
+configdRun('tailscale tailscale-status')), true);
+ if ($response !== null) {
+ return $response;
+ }
+ return ['error' => 'Unable to determine Tailscale status, is the service running?'];
+ }
+
+ public function ipAction()
+ {
+ $response = trim((new Backend())->configdRun('tailscale tailscale-ip'));
+ return ['result' => $response];
+ }
+
+ public function netAction()
+ {
+ $response = trim((new Backend())->configdRun('tailscale tailscale-netcheck'));
+ return ['result' => $response];
+ }
+
+ private function isJson($string) {
+ return is_string($string)
+ && (is_object(json_decode($string))
+ || is_array(json_decode($string)));
+ }
+}
diff --git a/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/AuthenticationController.php b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/AuthenticationController.php
new file mode 100644
index 000000000..98b9397b2
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/AuthenticationController.php
@@ -0,0 +1,38 @@
+view->pick('OPNsense/Tailscale/authentication');
+ $this->view->authenticationForm = $this->getForm('authentication');
+ }
+}
diff --git a/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/SettingsController.php b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/SettingsController.php
new file mode 100644
index 000000000..0170af957
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/SettingsController.php
@@ -0,0 +1,39 @@
+view->pick('OPNsense/Tailscale/settings');
+ $this->view->settingsForm = $this->getForm('settings');
+ $this->view->formDialogSubnet = $this->getForm("dialogSubnet4");
+ }
+}
diff --git a/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/StatusController.php b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/StatusController.php
new file mode 100644
index 000000000..707b41dd3
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/StatusController.php
@@ -0,0 +1,37 @@
+view->pick('OPNsense/Tailscale/status');
+ }
+}
diff --git a/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/forms/authentication.xml b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/forms/authentication.xml
new file mode 100644
index 000000000..b2bcae8b3
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/forms/authentication.xml
@@ -0,0 +1,14 @@
+
diff --git a/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/forms/dialogSubnet4.xml b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/forms/dialogSubnet4.xml
new file mode 100644
index 000000000..ce35d9f3d
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/forms/dialogSubnet4.xml
@@ -0,0 +1,14 @@
+
diff --git a/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/forms/settings.xml b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/forms/settings.xml
new file mode 100644
index 000000000..6b35707ae
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/controllers/OPNsense/Tailscale/forms/settings.xml
@@ -0,0 +1,32 @@
+
diff --git a/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/ACL/ACL.xml b/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/ACL/ACL.xml
new file mode 100644
index 000000000..d0f6a9138
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/ACL/ACL.xml
@@ -0,0 +1,9 @@
+
+
+ Tailscale
+
+ ui/tailscale/*
+ api/tailscale/*
+
+
+
diff --git a/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Authentication.php b/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Authentication.php
new file mode 100644
index 000000000..eb9ee4d7f
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Authentication.php
@@ -0,0 +1,35 @@
+
+ //OPNsense/tailscale/authentication
+ Tailscale authentication settings
+
+
+ https://controlplane.tailscale.com
+ Y
+ Please enter a valid URL
+
+
+ Y
+
+
+
diff --git a/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Menu/Menu.xml b/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Menu/Menu.xml
new file mode 100644
index 000000000..b28d1794f
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Menu/Menu.xml
@@ -0,0 +1,9 @@
+
diff --git a/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Settings.php b/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Settings.php
new file mode 100644
index 000000000..791f600e2
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Settings.php
@@ -0,0 +1,35 @@
+
+ //OPNsense/tailscale/settings
+ Tailscale general settings
+
+
+ 0
+ Y
+
+
+ 41641
+ Y
+
+
+ 1
+ Y
+
+
+ 0
+ Y
+
+
+ 0
+ Y
+
+
+
+
+ Y
+ ipv4
+ Y
+ Y
+
+
+
+
+
+
diff --git a/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Status.php b/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Status.php
new file mode 100644
index 000000000..b70a04b95
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/models/OPNsense/Tailscale/Status.php
@@ -0,0 +1,35 @@
+
+ //OPNsense/tailscale/status
+ Tailscale status settings
+
+
+
diff --git a/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/authentication.volt b/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/authentication.volt
new file mode 100644
index 000000000..52cb55b6a
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/authentication.volt
@@ -0,0 +1,39 @@
+
+
+ {{ partial("layout_partials/base_form",['fields':authenticationForm,'id':'frmAuthentication']) }}
+
+
+
+
+
+
+ {{ lang._('After changing settings, please remember to apply them') }}
+
+
+
+
+
+
diff --git a/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/settings.volt b/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/settings.volt
new file mode 100644
index 000000000..aee1bd69f
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/settings.volt
@@ -0,0 +1,75 @@
+
+
+
+
+ {{ partial("layout_partials/base_form",['fields':settingsForm,'id':'frmSettings'])}}
+
+
+
+
+
+ | {{ lang._('ID') }} |
+ {{ lang._('Subnet') }} |
+ {{ lang._('Description') }} |
+ {{ lang._('Commands') }} |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+{{ partial("layout_partials/base_dialog",['fields':formDialogSubnet,'id':'DialogSubnet','label':lang._('Edit Subnet')])}}
diff --git a/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/status.volt b/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/status.volt
new file mode 100644
index 000000000..b911f1393
--- /dev/null
+++ b/security/tailscale/src/opnsense/mvc/app/views/OPNsense/Tailscale/status.volt
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+ | {{ lang._('Name') }} |
+ {{ lang._('Value') }} |
+
+
+
+
+
+
+
+
+
+
+
+
+ | {{ lang._('Name') }} |
+ {{ lang._('Tailscale IPs') }} |
+ {{ lang._('Last Seen') }} |
+ {{ lang._('OS') }} |
+
+
+
+
+
+
+
+
+
+
diff --git a/security/tailscale/src/opnsense/service/conf/actions.d/actions_tailscale.conf b/security/tailscale/src/opnsense/service/conf/actions.d/actions_tailscale.conf
new file mode 100644
index 000000000..e5d6e3b3d
--- /dev/null
+++ b/security/tailscale/src/opnsense/service/conf/actions.d/actions_tailscale.conf
@@ -0,0 +1,47 @@
+[start]
+command:/usr/local/etc/rc.d/tailscaled start
+type: script
+message: starting tailscale service
+
+[stop]
+command:/usr/local/etc/rc.d/tailscaled stop; exit 0
+type: script
+message: stopping tailscale service
+
+[status]
+command:/usr/local/etc/rc.d/tailscaled status; exit 0
+type: script_output
+message: tailscaled status
+
+[restart]
+command:/usr/local/etc/rc.d/tailscaled restart
+type: script
+message: restarting tailscale services
+
+[reload]
+command:/usr/local/etc/rc.d/tailscaled reload
+type: script
+message: reload tailscale configuration
+
+[tailscale-status]
+command:/usr/local/bin/tailscale status --json; exit 0
+type:script_output
+message: request tailscale status
+
+[tailscale-ip]
+command:/usr/local/bin/tailscale ip; exit 0
+type:script_output
+message: request tailscale ip
+
+[tailscale-version]
+command:/usr/local/bin/tailscale version; exit 0
+type:script_output
+message: request tailscale version
+
+[tailscale-netcheck]
+command:/usr/local/bin/tailscale netcheck; exit 0
+type:script_output
+message: request tailscale netcheck
+
+
+
diff --git a/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/+TARGETS b/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/+TARGETS
new file mode 100644
index 000000000..e9380f1c2
--- /dev/null
+++ b/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/+TARGETS
@@ -0,0 +1 @@
+rc.conf.d:/etc/rc.conf.d/tailscaled
diff --git a/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/rc.conf.d b/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/rc.conf.d
new file mode 100644
index 000000000..6713fa87e
--- /dev/null
+++ b/security/tailscale/src/opnsense/service/templates/OPNsense/Tailscale/rc.conf.d
@@ -0,0 +1,49 @@
+# DO NOT EDIT
+# THIS FILE IS AUTOMATICALLY GENERATED - ANY CHANGES WILL BE OVERWRITTEN
+#
+{% if not helpers.empty('OPNsense.tailscale.settings.enabled') %}
+tailscaled_enable="YES"
+# Uncommenting the below breaks being able to access subnets
+# see - https://github.com/tailscale/tailscale/issues/5573#issuecomment-1584695981
+# tailscaled_env="TS_DEBUG_NETSTACK_SUBNETS=0"
+{% if helpers.exists('OPNsense.tailscale.settings.listenPort') %}
+tailscaled_port="{{ OPNsense.tailscale.settings.listenPort }}"
+{% endif %}
+{% set up_args = [] %}
+{% if helpers.exists('OPNsense.tailscale.settings.advertiseExitNode') and OPNsense.tailscale.settings.advertiseExitNode|default("0") == "1" %}
+{% do up_args.append("--advertise-exit-node") %}
+{% else %}
+{% do up_args.append("--advertise-exit-node=false") %}
+{% endif %}
+{% if helpers.exists('OPNsense.tailscale.settings.acceptSubnetRoutes') and OPNsense.tailscale.settings.acceptSubnetRoutes|default("0") == "1" %}
+{% do up_args.append("--accept-routes") %}
+{% else %}
+{% do up_args.append("--accept-routes=false") %}
+{% endif %}
+{% if helpers.exists('OPNsense.tailscale.settings.acceptDNS') and OPNsense.tailscale.settings.acceptDNS|default("0") == "1" %}
+{% do up_args.append("--accept-dns") %}
+{% else %}
+{% do up_args.append("--accept-dns=false") %}
+{% endif %}
+{% if helpers.exists('OPNsense.tailscale.authentication.loginServer') %}
+{% do up_args.append("--login-server=" + OPNsense.tailscale.authentication.loginServer) %}
+{% endif %}
+{% if helpers.exists('OPNsense.tailscale.authentication.preAuthKey') %}
+{% do up_args.append("--auth-key=" + OPNsense.tailscale.authentication.preAuthKey) %}
+{% endif %}
+{# loop through subnets to build list #}
+{% if helpers.exists('OPNsense.tailscale.settings.subnets.subnet4') %}
+{% set subnets = [] %}
+{% for subnet_list in helpers.toList('OPNsense.tailscale.settings.subnets.subnet4') %}
+{% do subnets.append(subnet_list.subnet) %}
+{% endfor %}
+{% set subnetString = subnets|join(',') %}
+{% do up_args.append("--advertise-routes=" + subnetString) %}
+{% else %}
+{% do up_args.append("--advertise-routes=") %}
+{% endif %}
+tailscaled_up_args="{{ up_args|join(' ') }}"
+{% else %}
+tailscaled_enable=NO
+{% endif %}
+