diff --git a/net/tor/Makefile b/net/tor/Makefile
new file mode 100644
index 000000000..b6d00b634
--- /dev/null
+++ b/net/tor/Makefile
@@ -0,0 +1,8 @@
+PLUGIN_NAME= tor
+PLUGIN_VERSION= 0.1
+PLUGIN_COMMENT= The Onion Router
+PLUGIN_DEPENDS= tor
+PLUGIN_MAINTAINER= franz.fabian.94@gmail.com
+PLUGIN_DEVEL= yes
+
+.include "../../Mk/plugins.mk"
diff --git a/net/tor/src/etc/inc/plugins.inc.d/tor.inc b/net/tor/src/etc/inc/plugins.inc.d/tor.inc
new file mode 100644
index 000000000..b30b05ec2
--- /dev/null
+++ b/net/tor/src/etc/inc/plugins.inc.d/tor.inc
@@ -0,0 +1,65 @@
+enabled == '1') {
+ return true;
+ }
+
+ return false;
+}
+
+function tor_firewall($fw)
+{
+ if (tor_enabled()) {
+ }
+}
+
+function tor_services()
+{
+ global $config;
+
+ $services = array();
+
+ if (tor_enabled()) {
+ $services[] = array(
+ 'description' => gettext('The Onion Router'),
+ 'configd' => array(
+ 'restart' => array('tor restart'),
+ 'start' => array('tor start'),
+ 'stop' => array('tor stop'),
+ ),
+ 'name' => 'tor',
+ 'pidfile' => '/var/run/tor/tor.pid'
+ );
+ }
+
+ return $services;
+}
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ExitaclController.php b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ExitaclController.php
new file mode 100644
index 000000000..708603361
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/ExitaclController.php
@@ -0,0 +1,169 @@
+sessionClose();
+ $mdl = $this->getModel();
+ $grid = new UIModelGrid($mdl->policy);
+ return $grid->fetchBindRequest(
+ $this->request,
+ array('enabled', 'type', 'network', 'action', 'startport', 'endport')
+ );
+ }
+ public function getaclAction($uuid = null)
+ {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference('policy.' . $uuid);
+ if ($node != null) {
+ // return node
+ return array('exitpolicy' => $node->getNodes());
+ }
+ } else {
+ $node = $mdl->policy->add();
+ return array('exitpolicy' => $node->getNodes());
+ }
+ return array();
+ }
+ public function addaclAction()
+ {
+ $result = array('result' => 'failed');
+ if ($this->request->isPost() && $this->request->hasPost('exitpolicy')) {
+ $result = array('result' => 'failed', 'validations' => array());
+ $mdl = $this->getModel();
+ $node = $mdl->policy->Add();
+ $node->setNodes($this->request->getPost('exitpolicy'));
+ $valMsgs = $mdl->performValidation();
+
+ foreach ($valMsgs as $field => $msg) {
+ $fieldnm = str_replace($node->__reference, 'exitpolicy', $msg->getField());
+ $result['validations'][$fieldnm] = $msg->getMessage();
+ }
+
+ if (count($result['validations']) == 0) {
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ unset($result['validations']);
+ $result['result'] = 'saved';
+ }
+ }
+ return $result;
+ }
+ public function delaclAction($uuid)
+ {
+
+ $result = array('result' => 'failed');
+
+ if ($this->request->isPost()) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ if ($mdl->policy->del($uuid)) {
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ $result['result'] = 'deleted';
+ } else {
+ $result['result'] = 'not found';
+ }
+ }
+ }
+ return $result;
+ }
+ public function setaclAction($uuid)
+ {
+ if ($this->request->isPost() && $this->request->hasPost('exitpolicy')) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference('policy.' . $uuid);
+ if ($node != null) {
+ $result = array('result' => 'failed', 'validations' => array());
+ $info = $this->request->getPost('exitpolicy');
+
+ $node->setNodes($info);
+ $valMsgs = $mdl->performValidation();
+ foreach ($valMsgs as $field => $msg) {
+ $fieldnm = str_replace($node->__reference, 'exitpolicy', $msg->getField());
+ $result['validations'][$fieldnm] = $msg->getMessage();
+ }
+
+ if (count($result['validations']) == 0) {
+ // save config if validated correctly
+ $mdl->serializeToConfig();
+ unset($result['validations']);
+ Config::getInstance()->save();
+ $result = array('result' => 'saved');
+ }
+ return $result;
+ }
+ }
+ }
+ return array('result' => 'failed');
+ }
+ public function toggle_handler($uuid, $element)
+ {
+
+ $result = array('result' => 'failed');
+
+ if ($this->request->isPost()) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference($element . '.' . $uuid);
+ if ($node != null) {
+ if ($node->enabled->__toString() == '1') {
+ $result['result'] = 'Disabled';
+ $node->enabled = '0';
+ } else {
+ $result['result'] = 'Enabled';
+ $node->enabled = '1';
+ }
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ }
+ }
+ }
+ return $result;
+ }
+
+ public function toggleaclAction($uuid)
+ {
+ return $this->toggle_handler($uuid, 'policy');
+ }
+}
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/GeneralController.php b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/GeneralController.php
new file mode 100644
index 000000000..b24cb65c4
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/GeneralController.php
@@ -0,0 +1,73 @@
+'failed');
+ if ($this->request->isPost()) {
+ $mdl = new General();
+ $mdl->setNodes($this->request->getPost('general'));
+
+ // perform validation
+ $valMsgs = $mdl->performValidation();
+ foreach ($valMsgs as $field => $msg) {
+ if (!array_key_exists('validations', $result)) {
+ $result['validations'] = array();
+ }
+ $result['validations']['general.'.$msg->getField()] = $msg->getMessage();
+ }
+
+ if ($valMsgs->count() == 0) {
+ if (empty((string)$mdl->control_port_password) || empty((string)$mdl->control_port_password_hashed)) {
+ $backend = new Backend();
+ $keys = json_decode(trim($backend->configdRun('tor genkey')), true);
+ $mdl->control_port_password_hashed = $keys['hashed_control_password'];
+ $mdl->control_port_password = $keys['control_password'];
+ }
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ $result['result'] = 'saved';
+ }
+ }
+ return $result;
+ }
+}
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceController.php b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceController.php
new file mode 100644
index 000000000..5d6ea6617
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceController.php
@@ -0,0 +1,171 @@
+sessionClose();
+ $mdl = $this->getModel();
+ $grid = new UIModelGrid($mdl->service);
+ return $grid->fetchBindRequest(
+ $this->request,
+ array('enabled', 'name')
+ );
+ }
+ public function getserviceAction($uuid = null)
+ {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference('service.' . $uuid);
+ if ($node != null) {
+ // return node
+ return array('hiddenservice' => $node->getNodes());
+ }
+ } else {
+ $node = $mdl->service->add();
+ return array('hiddenservice' => $node->getNodes());
+ }
+ return array();
+ }
+ public function addserviceAction()
+ {
+ $result = array('result' => 'failed');
+ if ($this->request->isPost() && $this->request->hasPost('hiddenservice')) {
+ $result = array('result' => 'failed', 'validations' => array());
+ $mdl = $this->getModel();
+ $node = $mdl->service->Add();
+ $node->setNodes($this->request->getPost('hiddenservice'));
+ $valMsgs = $mdl->performValidation();
+
+ foreach ($valMsgs as $field => $msg) {
+ $fieldnm = str_replace($node->__reference, 'hiddenservice', $msg->getField());
+ $result['validations'][$fieldnm] = $msg->getMessage();
+ }
+
+ if (count($result['validations']) == 0) {
+ // save config if validated correctly
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ unset($result['validations']);
+ $result['result'] = 'saved';
+ }
+ }
+ return $result;
+ }
+ public function delserviceAction($uuid)
+ {
+
+ $result = array('result' => 'failed');
+
+ if ($this->request->isPost()) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ if ($mdl->service->del($uuid)) {
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ $result['result'] = 'deleted';
+ } else {
+ $result['result'] = 'not found';
+ }
+ }
+ }
+ return $result;
+ }
+ public function setserviceAction($uuid)
+ {
+ if ($this->request->isPost() && $this->request->hasPost('hiddenservice')) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference('service.' . $uuid);
+ if ($node != null) {
+ $result = array('result' => 'failed', 'validations' => array());
+ $info = $this->request->getPost('hiddenservice');
+
+ $node->setNodes($info);
+ $valMsgs = $mdl->performValidation();
+ foreach ($valMsgs as $field => $msg) {
+ $fieldnm = str_replace($node->__reference, 'hiddenservice', $msg->getField());
+ $result['validations'][$fieldnm] = $msg->getMessage();
+ }
+
+ if (count($result['validations']) == 0) {
+ // save config if validated correctly
+ $mdl->serializeToConfig();
+ unset($result['validations']);
+ Config::getInstance()->save();
+ $result = array('result' => 'saved');
+ }
+ return $result;
+ }
+ }
+ }
+ return array('result' => 'failed');
+ }
+ public function toggle_handler($uuid, $element)
+ {
+
+ $result = array('result' => 'failed');
+
+ if ($this->request->isPost()) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference($element . '.' . $uuid);
+ if ($node != null) {
+ if ($node->enabled->__toString() == '1') {
+ $result['result'] = 'Disabled';
+ $node->enabled = '0';
+ } else {
+ $result['result'] = 'Enabled';
+ $node->enabled = '1';
+ }
+ // if item has toggled, serialize to config and save
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ }
+ }
+ }
+ return $result;
+ }
+
+ public function toggleserviceAction($uuid)
+ {
+ return $this->toggle_handler($uuid, 'service');
+ }
+}
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceaclController.php b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceaclController.php
new file mode 100644
index 000000000..8688b7fec
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/HiddenserviceaclController.php
@@ -0,0 +1,170 @@
+sessionClose();
+ $mdl = $this->getModel();
+ $grid = new UIModelGrid($mdl->hiddenserviceacl);
+ return $grid->fetchBindRequest(
+ $this->request,
+ array('enabled', 'hiddenservice', 'port', 'target_host', 'target_port')
+ );
+ }
+ public function getaclAction($uuid = null)
+ {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference('hiddenserviceacl.' . $uuid);
+ if ($node != null) {
+ // return node
+ return array('hiddenserviceacl' => $node->getNodes());
+ }
+ } else {
+ $node = $mdl->hiddenserviceacl->add();
+ return array('hiddenserviceacl' => $node->getNodes());
+ }
+ return array();
+ }
+ public function addaclAction()
+ {
+ $result = array('result' => 'failed');
+ if ($this->request->isPost() && $this->request->hasPost('hiddenserviceacl')) {
+ $result = array('result' => 'failed', 'validations' => array());
+ $mdl = $this->getModel();
+ $node = $mdl->hiddenserviceacl->Add();
+ $node->setNodes($this->request->getPost('hiddenserviceacl'));
+ $valMsgs = $mdl->performValidation();
+
+ foreach ($valMsgs as $field => $msg) {
+ $fieldnm = str_replace($node->__reference, 'hiddenserviceacl', $msg->getField());
+ $result['validations'][$fieldnm] = $msg->getMessage();
+ }
+
+ if (count($result['validations']) == 0) {
+ // save config if validated correctly
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ unset($result['validations']);
+ $result['result'] = 'saved';
+ }
+ }
+ return $result;
+ }
+ public function delaclAction($uuid)
+ {
+
+ $result = array('result' => 'failed');
+
+ if ($this->request->isPost()) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ if ($mdl->hiddenserviceacl->del($uuid)) {
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ $result['result'] = 'deleted';
+ } else {
+ $result['result'] = 'not found';
+ }
+ }
+ }
+ return $result;
+ }
+ public function setaclAction($uuid)
+ {
+ if ($this->request->isPost() && $this->request->hasPost('hiddenserviceacl')) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference('hiddenserviceacl.' . $uuid);
+ if ($node != null) {
+ $result = array('result' => 'failed', 'validations' => array());
+ $info = $this->request->getPost('hiddenserviceacl');
+
+ $node->setNodes($info);
+ $valMsgs = $mdl->performValidation();
+ foreach ($valMsgs as $field => $msg) {
+ $fieldnm = str_replace($node->__reference, 'hiddenserviceacl', $msg->getField());
+ $result['validations'][$fieldnm] = $msg->getMessage();
+ }
+
+ if (count($result['validations']) == 0) {
+ // save config if validated correctly
+ $mdl->serializeToConfig();
+ unset($result['validations']);
+ Config::getInstance()->save();
+ $result = array('result' => 'saved');
+ }
+ return $result;
+ }
+ }
+ }
+ return array('result' => 'failed');
+ }
+ public function toggle_handler($uuid, $element)
+ {
+
+ $result = array('result' => 'failed');
+
+ if ($this->request->isPost()) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference($element . '.' . $uuid);
+ if ($node != null) {
+ if ($node->enabled->__toString() == '1') {
+ $result['result'] = 'Disabled';
+ $node->enabled = '0';
+ } else {
+ $result['result'] = 'Enabled';
+ $node->enabled = '1';
+ }
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ }
+ }
+ }
+ return $result;
+ }
+
+ public function toggleaclAction($uuid)
+ {
+ return $this->toggle_handler($uuid, 'hiddenserviceacl');
+ }
+}
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/RelayController.php b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/RelayController.php
new file mode 100644
index 000000000..088602ec0
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/RelayController.php
@@ -0,0 +1,38 @@
+request->isPost()) {
+ $backend = new Backend();
+ $response = $backend->configdRun('tor start');
+ $backend->configdRun('filter reload');
+ return array('response' => $response);
+ } else {
+ return array('response' => array());
+ }
+ }
+
+ /**
+ * stop tor service
+ * @return array
+ */
+ public function stopAction()
+ {
+ if ($this->request->isPost()) {
+ $backend = new Backend();
+ $response = $backend->configdRun('tor stop');
+ return array('response' => $response);
+ } else {
+ return array('response' => array());
+ }
+ }
+
+ /**
+ * query tor hidden service hostnames
+ * @return array
+ */
+ public function get_hidden_servicesAction()
+ {
+ $backend = new Backend();
+ $response = json_decode($backend->configdRun('tor gethostnames'));
+ return array('response' => $response);
+ }
+
+ /**
+ * restart tor service
+ * @return array
+ */
+ public function restartAction()
+ {
+ if ($this->request->isPost()) {
+ $backend = new Backend();
+ $response = $backend->configdRun('tor restart');
+ $backend->configdRun('filter reload');
+ return array('response' => $response);
+ } else {
+ return array('response' => array());
+ }
+ }
+
+ /**
+ * retrieve status of tor
+ * @return array
+ * @throws \Exception
+ */
+ public function statusAction()
+ {
+ $backend = new Backend();
+ $general = new General();
+ $response = $backend->configdRun('tor status');
+
+ if (strpos($response, 'not running') > 0) {
+ if ($general->enabled->__toString() == 1) {
+ $status = 'stopped';
+ } else {
+ $status = 'disabled';
+ }
+ } elseif (strpos($response, 'is running') > 0) {
+ $status = 'running';
+ } elseif ($general->enabled->__toString() == 0) {
+ $status = 'disabled';
+ } else {
+ $status = 'unknown';
+ }
+
+
+ return array('status' => $status);
+ }
+
+ /**
+ * reconfigure tor, generate config and reload
+ */
+ public function reconfigureAction()
+ {
+ if ($this->request->isPost()) {
+ // close session for long running action
+ $this->sessionClose();
+
+ $general = new General();
+ $backend = new Backend();
+
+ $runStatus = $this->statusAction();
+
+ // stop tor if it is running or not
+ $this->stopAction();
+
+ // generate template
+ $backend->configdRun('template reload OPNsense/Tor');
+
+ // (re)start daemon
+ if ($general->enabled->__toString() == 1) {
+ $this->startAction();
+ }
+
+ return array('status' => 'ok');
+ } else {
+ return array('status' => 'failed');
+ }
+ }
+}
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/SocksaclController.php b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/SocksaclController.php
new file mode 100644
index 000000000..0a357852c
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/Api/SocksaclController.php
@@ -0,0 +1,169 @@
+sessionClose();
+ $mdl = $this->getModel();
+ $grid = new UIModelGrid($mdl->policy);
+ return $grid->fetchBindRequest(
+ $this->request,
+ array('enabled', 'type', 'network', 'action')
+ );
+ }
+ public function getaclAction($uuid = null)
+ {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference('policy.' . $uuid);
+ if ($node != null) {
+ // return node
+ return array('policy' => $node->getNodes());
+ }
+ } else {
+ $node = $mdl->policy->add();
+ return array('policy' => $node->getNodes());
+ }
+ return array();
+ }
+ public function addaclAction()
+ {
+ $result = array('result' => 'failed');
+ if ($this->request->isPost() && $this->request->hasPost('policy')) {
+ $result = array('result' => 'failed', 'validations' => array());
+ $mdl = $this->getModel();
+ $node = $mdl->policy->Add();
+ $node->setNodes($this->request->getPost('policy'));
+ $valMsgs = $mdl->performValidation();
+
+ foreach ($valMsgs as $field => $msg) {
+ $fieldnm = str_replace($node->__reference, 'policy', $msg->getField());
+ $result['validations'][$fieldnm] = $msg->getMessage();
+ }
+
+ if (count($result['validations']) == 0) {
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ unset($result['validations']);
+ $result['result'] = 'saved';
+ }
+ }
+ return $result;
+ }
+ public function delaclAction($uuid)
+ {
+
+ $result = array('result' => 'failed');
+
+ if ($this->request->isPost()) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ if ($mdl->policy->del($uuid)) {
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ $result['result'] = 'deleted';
+ } else {
+ $result['result'] = 'not found';
+ }
+ }
+ }
+ return $result;
+ }
+ public function setaclAction($uuid)
+ {
+ if ($this->request->isPost() && $this->request->hasPost('policy')) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference('policy.' . $uuid);
+ if ($node != null) {
+ $result = array('result' => 'failed', 'validations' => array());
+ $info = $this->request->getPost('policy');
+
+ $node->setNodes($info);
+ $valMsgs = $mdl->performValidation();
+ foreach ($valMsgs as $field => $msg) {
+ $fieldnm = str_replace($node->__reference, 'policy', $msg->getField());
+ $result['validations'][$fieldnm] = $msg->getMessage();
+ }
+
+ if (count($result['validations']) == 0) {
+ // save config if validated correctly
+ $mdl->serializeToConfig();
+ unset($result['validations']);
+ Config::getInstance()->save();
+ $result = array('result' => 'saved');
+ }
+ return $result;
+ }
+ }
+ }
+ return array('result' => 'failed');
+ }
+ public function toggle_handler($uuid, $element)
+ {
+
+ $result = array('result' => 'failed');
+
+ if ($this->request->isPost()) {
+ $mdl = $this->getModel();
+ if ($uuid != null) {
+ $node = $mdl->getNodeByReference($element . '.' . $uuid);
+ if ($node != null) {
+ if ($node->enabled->__toString() == '1') {
+ $result['result'] = 'Disabled';
+ $node->enabled = '0';
+ } else {
+ $result['result'] = 'Enabled';
+ $node->enabled = '1';
+ }
+ $mdl->serializeToConfig();
+ Config::getInstance()->save();
+ }
+ }
+ }
+ return $result;
+ }
+
+ public function toggleaclAction($uuid)
+ {
+ return $this->toggle_handler($uuid, 'policy');
+ }
+}
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/IndexController.php b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/IndexController.php
new file mode 100644
index 000000000..b2e0739a0
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/IndexController.php
@@ -0,0 +1,51 @@
+view->title = gettext("The Onion Router");
+ $this->view->general = $this->getForm("general");
+ $this->view->toracl = $this->getForm("acl_sockspolicy");
+ $this->view->hidden_service = $this->getForm("hidden_service");
+ $this->view->hidden_service_acl = $this->getForm("hidden_service_acl");
+ $this->view->relay = $this->getForm("relay");
+ $this->view->exitpolicy = $this->getForm("acl_exitpolicy");
+ $this->view->pick('OPNsense/Tor/general');
+ }
+}
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/acl_exitpolicy.xml b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/acl_exitpolicy.xml
new file mode 100644
index 000000000..5e33e3331
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/acl_exitpolicy.xml
@@ -0,0 +1,37 @@
+
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/acl_sockspolicy.xml b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/acl_sockspolicy.xml
new file mode 100644
index 000000000..85e29c1fd
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/acl_sockspolicy.xml
@@ -0,0 +1,25 @@
+
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/general.xml b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/general.xml
new file mode 100644
index 000000000..4c2feea4b
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/general.xml
@@ -0,0 +1,104 @@
+
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/hidden_service.xml b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/hidden_service.xml
new file mode 100644
index 000000000..b1827ad43
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/hidden_service.xml
@@ -0,0 +1,14 @@
+
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/hidden_service_acl.xml b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/hidden_service_acl.xml
new file mode 100644
index 000000000..70c7979c3
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/hidden_service_acl.xml
@@ -0,0 +1,31 @@
+
diff --git a/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/relay.xml b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/relay.xml
new file mode 100644
index 000000000..02224c367
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/controllers/OPNsense/Tor/forms/relay.xml
@@ -0,0 +1,62 @@
+
diff --git a/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACL/ACL.xml b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACL/ACL.xml
new file mode 100644
index 000000000..e54af9693
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACL/ACL.xml
@@ -0,0 +1,9 @@
+
+
+ tor
+
+ ui/tor/*
+ api/tor/*
+
+
+
diff --git a/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLExitPolicy.php b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLExitPolicy.php
new file mode 100644
index 000000000..68369c7a5
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLExitPolicy.php
@@ -0,0 +1,34 @@
+
+ //OPNsense/tor/exitpolicy
+ ACL for Socks port
+
+
+
+ 1
+ Y
+
+
+ v6
+ Y
+
+ IPv4
+ IPv6
+
+
+
+ Y
+
+
+ 1
+ N
+ 65535
+ A valid Port number must be specified.
+
+
+ 1
+ N
+ 65535
+ A valid Port number must be specified.
+
+
+ accept
+ Y
+
+ Accept
+ Reject
+
+
+
+
+
diff --git a/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLSocksPolicy.php b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLSocksPolicy.php
new file mode 100644
index 000000000..e540e82e0
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/ACLSocksPolicy.php
@@ -0,0 +1,34 @@
+
+ //OPNsense/tor/aclsockspolicy
+ ACL for Socks port
+
+
+
+ 1
+ Y
+
+
+ v6
+ Y
+
+ IPv4
+ IPv6
+
+
+
+ Y
+
+
+ accept
+ Y
+
+ Accept
+ Reject
+
+
+
+
+
diff --git a/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/General.php b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/General.php
new file mode 100644
index 000000000..f82eeec35
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/General.php
@@ -0,0 +1,34 @@
+
+ //OPNsense/tor/general
+ General Tor configuration
+
+
+ 0
+ Y
+
+
+ N
+ Y
+
+
+ 9050
+ 0
+ Y
+ 65535
+ A valid Port number must be specified.
+
+
+ 9051
+ 0
+ N
+ 65535
+ A valid Port number must be specified.
+
+
+ N
+ /^.+$/
+
+
+ N
+ /^.+$/
+
+
+ 0
+ Y
+
+
+ Y
+ N
+ notifications
+
+ Errors
+ Warnings
+ Notifications
+ Informational
+ Debugging
+
+
+
+ 0
+ Y
+
+
+ Y
+ N
+ notifications
+
+ Errors
+ Warnings
+ Notifications
+ Informational
+ Debugging
+
+
+
+ 0
+ Y
+
+
+ 80,443
+ Y
+ /^(\d+,)*\d+$/
+
+
+ 0
+ Y
+
+
+ 9040
+ 0
+ Y
+ 65535
+ A valid Port number must be specified.
+
+
+ 9053
+ 0
+ Y
+ 65535
+ A valid Port number must be specified.
+
+
+ 172.29.0.0/16
+ Y
+
+
+ 0
+ Y
+
+
+
diff --git a/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenService.php b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenService.php
new file mode 100644
index 000000000..988a55f26
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenService.php
@@ -0,0 +1,34 @@
+
+ //OPNsense/tor/hiddenservice
+ Tor hidden service configuration
+
+
+
+ 1
+ Y
+
+
+ Y
+ /^[a-z0-9_-]+$/i
+ The name should only consist of alphanumeric characters, dashes and underscores.
+
+
+
+
diff --git a/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenServiceACL.php b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenServiceACL.php
new file mode 100644
index 000000000..8b6ee3441
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/HiddenServiceACL.php
@@ -0,0 +1,34 @@
+
+ //OPNsense/tor/hiddenserviceacl
+ Tor Hidden Service ACL
+
+
+
+ 1
+ Y
+
+
+
+
+ OPNsense.Tor.HiddenService
+ service
+ name
+
+
+ A hidden service must be set.
+ N
+ Y
+
+
+ 80
+ 1
+ Y
+ 65535
+ A valid Port number must be specified.
+
+
+ Y
+ 127.0.0.1
+
+
+ 80
+ 1
+ Y
+ 65535
+ A valid Port number must be specified.
+
+
+
+
diff --git a/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/Menu/Menu.xml b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/Menu/Menu.xml
new file mode 100644
index 000000000..9dd1e7b47
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/Menu/Menu.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/Relay.php b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/Relay.php
new file mode 100644
index 000000000..3a2caa6c0
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/models/OPNsense/Tor/Relay.php
@@ -0,0 +1,34 @@
+
+ //OPNsense/tor/relay
+ Tor Relay configuration
+
+
+ 0
+ Y
+
+
+ N
+ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[a-f0-9:]{2,})$/i
+
+
+ 9001
+ 0
+ Y
+ 65535
+ A valid Port number must be specified.
+
+
+ 1
+ N
+ 65535
+ A valid Port number must be specified.
+
+
+ N
+
+ /^[a-z0-9.-]+$/i
+
+
+ /^.+$/
+ N
+
+ /^[a-zA-Z0-9]+$/
+
+
+ N
+
+
+ N
+
+
+ 0
+ Y
+
+
+ 1
+ Y
+
+
+ 0
+ Y
+
+
+
diff --git a/net/tor/src/opnsense/mvc/app/views/OPNsense/Tor/general.volt b/net/tor/src/opnsense/mvc/app/views/OPNsense/Tor/general.volt
new file mode 100644
index 000000000..35d5d9d38
--- /dev/null
+++ b/net/tor/src/opnsense/mvc/app/views/OPNsense/Tor/general.volt
@@ -0,0 +1,240 @@
+{#
+
+ Copyright (C) 2017 Fabian Franz
+ 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': general,'id':'general'])}}
+
+
+ {{ lang._('Save') }}
+
+
+
+
+
+
+ {{ lang._('Enabled') }}
+ {{ lang._('Action') }}
+ {{ lang._('Protocol') }}
+ {{ lang._('Network') }}
+ {{ lang._('ID') }}
+ {{ lang._('Commands') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ lang._('Enabled') }}
+ {{ lang._('Name') }}
+ {{ lang._('ID') }}
+ {{ lang._('Commands') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ lang._('Enabled') }}
+ {{ lang._('Hidden Service') }}
+ {{ lang._('Port') }}
+ {{ lang._('Target Host') }}
+ {{ lang._('Target Port') }}
+ {{ lang._('ID') }}
+ {{ lang._('Commands') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ partial("layout_partials/base_form",['fields': relay,'id':'relay'])}}
+
+
+ {{ lang._('Save') }}
+
+
+
+
+
+ {{ lang._('Running an exit node may be lead to legal issues and seized hardware. Be careful with your settings here.') }}
+
+
+
+
+ {{ lang._('Enabled') }}
+ {{ lang._('Action') }}
+ {{ lang._('Protocol') }}
+ {{ lang._('Network') }}
+ {{ lang._('Start Port') }}
+ {{ lang._('End Port') }}
+ {{ lang._('ID') }}
+ {{ lang._('Commands') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ partial("layout_partials/base_dialog",['fields': toracl,'id':'toracldlg', 'label':lang._('Edit ACL Entry')]) }}
+{{ partial("layout_partials/base_dialog",['fields': hidden_service,'id':'hiddenservicedlg', 'label':lang._('Edit Hidden Service')]) }}
+{{ partial("layout_partials/base_dialog",['fields': hidden_service_acl,'id':'hiddenserviceacl', 'label':lang._('Edit Hidden Service Route')]) }}
+{{ partial("layout_partials/base_dialog",['fields': exitpolicy,'id':'torexitacldlg', 'label':lang._('Edit Exit Node ACL')]) }}
diff --git a/net/tor/src/opnsense/service/conf/actions.d/actions_tor.conf b/net/tor/src/opnsense/service/conf/actions.d/actions_tor.conf
new file mode 100644
index 000000000..b1c30a13f
--- /dev/null
+++ b/net/tor/src/opnsense/service/conf/actions.d/actions_tor.conf
@@ -0,0 +1,36 @@
+[start]
+command:/usr/local/opnsense/scripts/tor/setup.sh;/usr/local/etc/rc.d/tor start
+parameters:
+type:script
+message:starting tor
+
+[stop]
+command:/usr/local/etc/rc.d/tor onestop
+parameters:
+type:script
+message:stopping tor
+
+[restart]
+command:/usr/local/opnsense/scripts/tor/setup.sh;/usr/local/etc/rc.d/tor restart
+parameters:
+type:script
+message:restarting tor
+
+[status]
+command:/usr/local/etc/rc.d/tor status;exit 0
+parameters:
+type:script_output
+message:request tor status
+
+[genkey]
+command:/usr/local/opnsense/service/scripts/tor/gen_key
+parameters:
+type:script_output
+message:generate Tor control key
+
+[gethostnames]
+command:/usr/local/opnsense/service/scripts/tor/get_hostnames
+parameters:
+type:script_output
+message:query hostnames of hidden services
+
diff --git a/net/tor/src/opnsense/service/scripts/tor/gen_key b/net/tor/src/opnsense/service/scripts/tor/gen_key
new file mode 100755
index 000000000..9833a0b64
--- /dev/null
+++ b/net/tor/src/opnsense/service/scripts/tor/gen_key
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+PASSWORD=$( /usr/local/bin/openssl rand -base64 32 )
+echo "{"
+echo -n " \"hashed_control_password\":\""
+tor --quiet --hash-password "$PASSWORD" | tr -d '\n' | tr -d '\r'
+echo "\","
+echo " \"control_password\":\"$PASSWORD\""
+echo "}"
diff --git a/net/tor/src/opnsense/service/scripts/tor/get_hostnames b/net/tor/src/opnsense/service/scripts/tor/get_hostnames
new file mode 100755
index 000000000..a108dc51a
--- /dev/null
+++ b/net/tor/src/opnsense/service/scripts/tor/get_hostnames
@@ -0,0 +1,24 @@
+#!/usr/local/bin/php
+service->__items as $service) {
+ $directory_name = ((string)$service->name);
+ $hostnamefile = TOR_DATA_DIR . '/' . $directory_name . '/hostname';
+ if (file_exists($hostnamefile)) {
+ $hostname = @file_get_contents($hostnamefile);
+ }
+ if (empty($hostname)) {
+ $hostname = 'not available';
+ }
+ $hostname = trim($hostname);
+ $hostnames[$directory_name] = $hostname;
+}
+
+print json_encode($hostnames);
diff --git a/net/tor/src/opnsense/service/scripts/tor/setup.sh b/net/tor/src/opnsense/service/scripts/tor/setup.sh
new file mode 100755
index 000000000..b7464dde9
--- /dev/null
+++ b/net/tor/src/opnsense/service/scripts/tor/setup.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+mkdir -p /var/db/tor
+mkdir -p /var/log/tor
+mkdir -p /var/run/tor
+
+# required to access the pf device for nat
+/usr/sbin/pw groupmod proxy -m _tor
+
+
diff --git a/net/tor/src/opnsense/service/scripts/tor/tor_helper.php b/net/tor/src/opnsense/service/scripts/tor/tor_helper.php
new file mode 100644
index 000000000..3bcb37884
--- /dev/null
+++ b/net/tor/src/opnsense/service/scripts/tor/tor_helper.php
@@ -0,0 +1,4 @@
+
+
+{% if helpers.exists('OPNsense.tor.relay.directory_port') and OPNsense.tor.relay.directory_port != '' %}
+DirPort {{ OPNsense.tor.relay.directory_port }}
+{% endif %}
+## Uncomment to return an arbitrary blob of html on your DirPort. Now you
+## can explain what Tor is if anybody wonders why your IP address is
+## contacting them. See contrib/tor-exit-notice.html in Tor's source
+## distribution for a sample.
+#DirPortFrontPage /usr/local/etc/tor/tor-exit-notice.html
+
+{% if helpers.exists('OPNsense.tor.relay.exitrejectprivateip') %}
+ExitPolicyRejectPrivate {{ OPNsense.tor.relay.exitrejectprivateip }}
+{% endif %}
+
+
+{% if helpers.exists('OPNsense.tor.exitpolicy') %}
+{% if helpers.exists('OPNsense.tor.exitpolicy.policy') %}
+# exit node policy
+
+{% for policy in helpers.toList('OPNsense.tor.exitpolicy.policy') %}
+{% if policy.enabled == '1' %}
+ExitPolicy {{ policy.action }}{% if policy.type == 'v6' %}6{% endif
+ %} {% if policy.network == '' %}*{% if 'v' in policy.type %}{{ policy.type|replace('v','') }}{% endif%}{% else
+ %}{{ policy.network }}{% endif
+ %}{% if 'startport' in policy %}:{{ policy.startport
+ }}{% if 'endport' in policy %}-{{ policy.endport }}{% endif
+ %}{% endif %}
+
+{% endif %}
+{% endfor %}
+{% endif %}
+{% endif %}
+
+# default: don't pass anything
+ExitPolicy reject *:*
+ExitPolicy reject6 *:*
+
+
+BridgeRelay {{ OPNsense.tor.relay.relay|default('1') }}
+PublishServerDescriptor {{ OPNsense.tor.relay.publish|default('0') }}
+{% endif %}
+
+{% endif %}