<?php
/*
 * unbound.inc
 *
 * part of pfSense (https://www.pfsense.org)
 * Copyright (c) 2015 Warren Baker <warren@percol8.co.za>
 * Copyright (c) 2015-2016 Electric Sheep Fencing, LLC
 * All rights reserved.
 *
 * originally part of m0n0wall (http://m0n0.ch/wall)
 * Copyright (c) 2003-2004 Manuel Kasper <mk@neon1.net>.
 * 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.
 *
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgment:
 *    "This product includes software developed by the pfSense Project
 *    for use in the pfSense® software distribution. (http://www.pfsense.org/).
 *
 * 4. The names "pfSense" and "pfSense Project" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For written permission, please contact
 *    coreteam@pfsense.org.
 *
 * 5. Products derived from this software may not be called "pfSense"
 *    nor may "pfSense" appear in their names without prior written
 *    permission of the Electric Sheep Fencing, LLC.
 *
 * 6. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *
 * "This product includes software developed by the pfSense Project
 * for use in the pfSense software distribution (http://www.pfsense.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY THE pfSense PROJECT ``AS IS'' AND ANY
 * EXPRESSED 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 pfSense PROJECT OR
 * ITS CONTRIBUTORS 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.
 */

/* include all configuration functions */
require_once("config.inc");
require_once("functions.inc");
require_once("filter.inc");
require_once("shaper.inc");

function create_unbound_chroot_path($cfgsubdir = "") {
	global $config, $g;

	// Configure chroot
	if (!is_dir($g['unbound_chroot_path'])) {
		mkdir($g['unbound_chroot_path']);
		chown($g['unbound_chroot_path'], "unbound");
		chgrp($g['unbound_chroot_path'], "unbound");
	}

	if ($cfgsubdir != "") {
		$cfgdir = $g['unbound_chroot_path'] . $cfgsubdir;
		if (!is_dir($cfgdir)) {
			mkdir($cfgdir);
			chown($cfgdir, "unbound");
			chgrp($cfgdir, "unbound");
		}
	}
}

/* Optimize Unbound for environment */
function unbound_optimization() {
	global $config;

	$optimization_settings = array();

	/*
	 * Set the number of threads equal to number of CPUs.
	 * Use 1 to disable threading, if for some reason this sysctl fails.
	 */
	$numprocs = intval(get_single_sysctl('kern.smp.cpus'));
	if ($numprocs > 1) {
		$optimization['number_threads'] = "num-threads: {$numprocs}";
		$optimize_num = pow(2, floor(log($numprocs, 2)));
	} else {
		$optimization['number_threads'] = "num-threads: 1";
		$optimize_num = 4;
	}

	// Slabs to help reduce lock contention.
	$optimization['msg_cache_slabs'] = "msg-cache-slabs: {$optimize_num}";
	$optimization['rrset_cache_slabs'] = "rrset-cache-slabs: {$optimize_num}";
	$optimization['infra_cache_slabs'] = "infra-cache-slabs: {$optimize_num}";
	$optimization['key_cache_slabs'] = "key-cache-slabs: {$optimize_num}";

	/*
	 * Larger socket buffer for busy servers
	 * Check that it is set to 4MB (by default the OS has it configured to 4MB)
	 */
	if (is_array($config['sysctl']) && is_array($config['sysctl']['item'])) {
		foreach ($config['sysctl']['item'] as $tunable) {
			if ($tunable['tunable'] == 'kern.ipc.maxsockbuf') {
				$so = floor(($tunable['value']/1024/1024)-4);
				// Check to ensure that the number is not a negative
				if ($so >= 4) {
					// Limit to 32MB, users might set maxsockbuf very high for other reasons.
					// We do not want unbound to fail because of that.
					$so = min($so, 32);
					$optimization['so_rcvbuf'] = "so-rcvbuf: {$so}m";
				} else {
					unset($optimization['so_rcvbuf']);
				}
			}
		}
	}
	// Safety check in case kern.ipc.maxsockbuf is not available.
	if (!isset($optimization['so_rcvbuf'])) {
		$optimization['so_rcvbuf'] = "#so-rcvbuf: 4m";
	}

	return $optimization;

}

function test_unbound_config($unboundcfg, &$output) {
	global $g;

	$cfgsubdir = "/test";
	unbound_generate_config($unboundcfg, $cfgsubdir);
	unbound_remote_control_setup($cfgsubdir);
	do_as_unbound_user("unbound-anchor", $cfgsubdir);

	$cfgdir = "{$g['unbound_chroot_path']}{$cfgsubdir}";

	$rv = 0;
	exec("/usr/local/sbin/unbound-checkconf {$cfgdir}/unbound.conf 2>&1", $output, $rv);
	rmdir_recursive($cfgdir);

	return $rv;
}


function unbound_generate_config($unboundcfg = NULL, $cfgsubdir = "") {
	global $g;

	$unboundcfgtxt = unbound_generate_config_text($unboundcfg, $cfgsubdir);

	// Configure static Host entries
	unbound_add_host_entries($cfgsubdir);

	// Configure Domain Overrides
	unbound_add_domain_overrides("", $cfgsubdir);

	// Configure Unbound access-lists
	unbound_acls_config($cfgsubdir);

	create_unbound_chroot_path($cfgsubdir);
	file_put_contents("{$g['unbound_chroot_path']}{$cfgsubdir}/unbound.conf", $unboundcfgtxt);
}


function unbound_generate_config_text($unboundcfg = NULL, $cfgsubdir = "") {

	global $config, $g;
	if (is_null($unboundcfg)) {
		$unboundcfg = $config['unbound'];
	}

	// Setup optimization
	$optimization = unbound_optimization();

	// Setup DNSSEC support
	if (isset($unboundcfg['dnssec'])) {
		$module_config = "validator iterator";
		$anchor_file = "auto-trust-anchor-file: {$g['unbound_chroot_path']}{$cfgsubdir}/root.key";
	} else {
		$module_config = "iterator";
	}

	// Setup DNS Rebinding
	if (!isset($config['system']['webgui']['nodnsrebindcheck'])) {
		// Private-addresses for DNS Rebinding
		$private_addr = <<<EOF
# For DNS Rebinding prevention
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 169.254.0.0/16
private-address: 192.168.0.0/16
private-address: fd00::/8
private-address: fe80::/10
EOF;
	}

	// Determine interfaces to run on
	$bindints = "";
	if (!empty($unboundcfg['active_interface'])) {
		$active_interfaces = explode(",", $unboundcfg['active_interface']);
		if (in_array("all", $active_interfaces, true)) {
			$bindints .= "interface: 0.0.0.0\n";
			$bindints .= "interface: ::0\n";
			$bindints .= "interface-automatic: yes\n";
		} else {
			foreach ($active_interfaces as $ubif) {
				if (is_ipaddr($ubif)) {
					$bindints .= "interface: $ubif\n";
				} else {
					$intip = get_interface_ip($ubif);
					if (is_ipaddrv4($intip)) {
						$bindints .= "interface: $intip\n";
					}
					$intip = get_interface_ipv6($ubif);
					if (is_ipaddrv6($intip)) {
						$bindints .= "interface: $intip\n";
					}
				}
			}
		}
	} else {
		$bindints .= "interface: 0.0.0.0\n";
		$bindints .= "interface: ::0\n";
		/* If the active interface array is empty, treat it the same as "All" as is done above. Otherwise it breaks CARP with a default config. */
		$bindints .= "interface-automatic: yes\n";
	}

	// Determine interfaces to run on
	$outgoingints = "";
	if (!empty($unboundcfg['outgoing_interface'])) {
		$outgoingints = "# Outgoing interfaces to be used\n";
		$outgoing_interfaces = explode(",", $unboundcfg['outgoing_interface']);
		foreach ($outgoing_interfaces as $outif) {
			$outip = get_interface_ip($outif);
			if (is_ipaddr($outip)) {
				$outgoingints .= "outgoing-interface: $outip\n";
			}
			$outip = get_interface_ipv6($outif);
			if (is_ipaddrv6($outip)) {
				$outgoingints .= "outgoing-interface: $outip\n";
			}
		}
	}

	// Allow DNS Rebind for forwarded domains
	if (isset($unboundcfg['domainoverrides']) && is_array($unboundcfg['domainoverrides'])) {
		if (!isset($config['system']['webgui']['nodnsrebindcheck'])) {
			$private_domains = "# Set private domains in case authoritative name server returns a Private IP address\n";
			$private_domains .= unbound_add_domain_overrides("private");
		}
		$reverse_zones .= unbound_add_domain_overrides("reverse");
	}

	// Configure Unbound statistics
	$statistics = unbound_statistics();

	// Add custom Unbound options
	if ($unboundcfg['custom_options']) {
		$custom_options_source = explode("\n", base64_decode($unboundcfg['custom_options']));
		$custom_options = "# Unbound custom options\n";
		foreach ($custom_options_source as $ent) {
			$custom_options .= $ent."\n";
		}
	}

	// Server configuration variables
	$port = (is_port($unboundcfg['port'])) ? $unboundcfg['port'] : "53";
	$hide_identity = isset($unboundcfg['hideidentity']) ? "yes" : "no";
	$hide_version = isset($unboundcfg['hideversion']) ? "yes" : "no";
	$harden_dnssec_stripped = isset($unboundcfg['dnssecstripped']) ? "yes" : "no";
	$prefetch = isset($unboundcfg['prefetch']) ? "yes" : "no";
	$prefetch_key = isset($unboundcfg['prefetchkey']) ? "yes" : "no";
	$outgoing_num_tcp = isset($unboundcfg['outgoing_num_tcp']) ? $unboundcfg['outgoing_num_tcp'] : "10";
	$incoming_num_tcp = isset($unboundcfg['incoming_num_tcp']) ? $unboundcfg['incoming_num_tcp'] : "10";
	$edns_buffer_size = (!empty($unboundcfg['edns_buffer_size'])) ? $unboundcfg['edns_buffer_size'] : "4096";
	$num_queries_per_thread = (!empty($unboundcfg['num_queries_per_thread'])) ? $unboundcfg['num_queries_per_thread'] : "4096";
	$jostle_timeout = (!empty($unboundcfg['jostle_timeout'])) ? $unboundcfg['jostle_timeout'] : "200";
	$cache_max_ttl = (!empty($unboundcfg['cache_max_ttl'])) ? $unboundcfg['cache_max_ttl'] : "86400";
	$cache_min_ttl = (!empty($unboundcfg['cache_min_ttl'])) ? $unboundcfg['cache_min_ttl'] : "0";
	$infra_host_ttl = (!empty($unboundcfg['infra_host_ttl'])) ? $unboundcfg['infra_host_ttl'] : "900";
	$infra_cache_numhosts = (!empty($unboundcfg['infra_cache_numhosts'])) ? $unboundcfg['infra_cache_numhosts'] : "10000";
	$unwanted_reply_threshold = (!empty($unboundcfg['unwanted_reply_threshold'])) ? $unboundcfg['unwanted_reply_threshold'] : "0";
	if ($unwanted_reply_threshold == "disabled") {
		$unwanted_reply_threshold = "0";
	}
	$msg_cache_size = (!empty($unboundcfg['msgcachesize'])) ? $unboundcfg['msgcachesize'] : "4";
	$verbosity = isset($unboundcfg['log_verbosity']) ? $unboundcfg['log_verbosity'] : 1;
	$use_caps = isset($unboundcfg['use_caps']) ? "yes" : "no";

	// Set up forwarding if it is configured
	if (isset($unboundcfg['forwarding'])) {
		$dnsservers = array();
		if (isset($config['system']['dnsallowoverride'])) {
			$ns = array_unique(get_nameservers());
			foreach ($ns as $nameserver) {
				if ($nameserver) {
					$dnsservers[] = $nameserver;
				}
			}
		} else {
			$ns = array();
		}
		$sys_dnsservers = array_unique(get_dns_servers());
		foreach ($sys_dnsservers as $sys_dnsserver) {
			if ($sys_dnsserver && (!in_array($sys_dnsserver, $ns))) {
				$dnsservers[] = $sys_dnsserver;
			}
		}

		if (!empty($dnsservers)) {
			$forward_conf .=<<<EOD
# Forwarding
forward-zone:
	name: "."

EOD;
			foreach ($dnsservers as $dnsserver) {
				if (is_ipaddr($dnsserver) && !ip_in_subnet($dnsserver, "127.0.0.0/8")) {
					$forward_conf .= "\tforward-addr: $dnsserver\n";
				}
			}
		}
	} else {
		$forward_conf = "";
	}

	// Size of the RRset cache == 2 * msg-cache-size per Unbound's recommendations
	$rrset_cache_size = $msg_cache_size * 2;

	$unboundconf = <<<EOD
##########################
# Unbound Configuration
##########################

##
# Server configuration
##
server:
{$reverse_zones}
chroot: {$g['unbound_chroot_path']}
username: "unbound"
directory: "{$g['unbound_chroot_path']}"
pidfile: "/var/run/unbound.pid"
use-syslog: yes
port: {$port}
verbosity: {$verbosity}
hide-identity: {$hide_identity}
hide-version: {$hide_version}
harden-glue: yes
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
do-daemonize: yes
module-config: "{$module_config}"
unwanted-reply-threshold: {$unwanted_reply_threshold}
num-queries-per-thread: {$num_queries_per_thread}
jostle-timeout: {$jostle_timeout}
infra-host-ttl: {$infra_host_ttl}
infra-cache-numhosts: {$infra_cache_numhosts}
outgoing-num-tcp: {$outgoing_num_tcp}
incoming-num-tcp: {$incoming_num_tcp}
edns-buffer-size: {$edns_buffer_size}
cache-max-ttl: {$cache_max_ttl}
cache-min-ttl: {$cache_min_ttl}
harden-dnssec-stripped: {$harden_dnssec_stripped}
msg-cache-size: {$msg_cache_size}m
rrset-cache-size: {$rrset_cache_size}m

{$optimization['number_threads']}
{$optimization['msg_cache_slabs']}
{$optimization['rrset_cache_slabs']}
{$optimization['infra_cache_slabs']}
{$optimization['key_cache_slabs']}
outgoing-range: 4096
{$optimization['so_rcvbuf']}
{$anchor_file}
prefetch: {$prefetch}
prefetch-key: {$prefetch_key}
use-caps-for-id: {$use_caps}
# Statistics
{$statistics}
# Interface IP(s) to bind to
{$bindints}
{$outgoingints}

# DNS Rebinding
{$private_addr}
{$private_domains}

# Access lists
include: {$g['unbound_chroot_path']}{$cfgsubdir}/access_lists.conf

# Static host entries
include: {$g['unbound_chroot_path']}{$cfgsubdir}/host_entries.conf

# dhcp lease entries
include: {$g['unbound_chroot_path']}{$cfgsubdir}/dhcpleases_entries.conf

# Domain overrides
include: {$g['unbound_chroot_path']}{$cfgsubdir}/domainoverrides.conf
{$forward_conf}

{$custom_options}

###
# Remote Control Config
###
include: {$g['unbound_chroot_path']}{$cfgsubdir}/remotecontrol.conf

EOD;

	return $unboundconf;
}

function unbound_remote_control_setup($cfgsubdir = "") {
	global $g;

	if (!file_exists("{$g['unbound_chroot_path']}{$cfgsubdir}/remotecontrol.conf") || !file_exists("{$g['unbound_chroot_path']}{$cfgsubdir}/unbound_control.key")) {
		$remotcfg = <<<EOF
remote-control:
	control-enable: yes
	control-interface: 127.0.0.1
	control-port: 953
	server-key-file: "{$g['unbound_chroot_path']}{$cfgsubdir}/unbound_server.key"
	server-cert-file: "{$g['unbound_chroot_path']}{$cfgsubdir}/unbound_server.pem"
	control-key-file: "{$g['unbound_chroot_path']}{$cfgsubdir}/unbound_control.key"
	control-cert-file: "{$g['unbound_chroot_path']}{$cfgsubdir}/unbound_control.pem"

EOF;

		create_unbound_chroot_path($cfgsubdir);
		file_put_contents("{$g['unbound_chroot_path']}{$cfgsubdir}/remotecontrol.conf", $remotcfg);

		// Generate our keys
		do_as_unbound_user("unbound-control-setup", $cfgsubdir);

	}
}

// Read /etc/hosts
function read_hosts() {

	/* Open /etc/hosts and extract the only dhcpleases info
	 * XXX - to convert to an unbound C library which reads /etc/hosts automatically
	 */
	$etc_hosts = array();
	foreach (file('/etc/hosts') as $line) {
		if (strpos($line, "dhcpleases automatically entered")) {
			break;
		}
		$d = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY);
		if (empty($d) || substr(reset($d), 0, 1) == "#") {
			continue;
		}
		$ip = array_shift($d);
		$fqdn = array_shift($d);
		$name = array_shift($d);
		if (!empty($fqdn) && $fqdn != "empty") {
			if (!empty($name) && $name != "empty") {
				array_push($etc_hosts, array(ipaddr => "$ip", fqdn => "$fqdn", name => "$name"));
			} else {
				array_push($etc_hosts, array(ipaddr => "$ip", fqdn => "$fqdn"));
			}
		}
	}
	return $etc_hosts;
}

function sync_unbound_service() {
	global $config, $g;

	create_unbound_chroot_path();

	// Configure our Unbound service
	do_as_unbound_user("unbound-anchor");
	unbound_remote_control_setup();
	unbound_generate_config();
	do_as_unbound_user("start");
	require_once("service-utils.inc");
	if (is_service_running("unbound")) {
		do_as_unbound_user("restore_cache");
	}

}

function unbound_acl_id_used($id) {
	global $config;

	if (is_array($config['unbound']['acls'])) {
		foreach ($config['unbound']['acls'] as & $acls) {
			if ($id == $acls['aclid']) {
				return true;
			}
		}
	}

	return false;
}

function unbound_get_next_id() {
	$aclid = 0;
	while (unbound_acl_id_used($aclid)) {
		$aclid++;
	}
	return $aclid;
}

// Execute commands as the user unbound
function do_as_unbound_user($cmd, $param1 = "") {
	global $g;

	switch ($cmd) {
		case "start":
			mwexec("/usr/local/sbin/unbound -c {$g['unbound_chroot_path']}/unbound.conf");
			break;
		case "stop":
			mwexec("echo '/usr/local/sbin/unbound-control stop' | /usr/bin/su -m unbound", true);
			break;
		case "reload":
			mwexec("echo '/usr/local/sbin/unbound-control reload' | /usr/bin/su -m unbound", true);
			break;
		case "unbound-anchor":
			$root_key_file = "{$g['unbound_chroot_path']}{$param1}/root.key";
			// sanity check root.key because unbound-anchor will fail without manual removal otherwise. redmine #5334
			if (file_exists($root_key_file)) {
				$rootkeycheck = mwexec("/usr/bin/grep 'autotrust trust anchor file' {$root_key_file}", true);
				if ($rootkeycheck != "0") {
					log_error("Unbound {$root_key_file} file is corrupt, removing and recreating.");
					unlink_if_exists($root_key_file);
				}
			}
			mwexec("echo '/usr/local/sbin/unbound-anchor -a {$root_key_file}' | /usr/bin/su -m unbound", true);
			// Only sync the file if this is the real (default) one, not a test one.
			if ($param1 == "") {
				pfSense_fsync($root_key_file);
			}
			break;
		case "unbound-control-setup":
			mwexec("echo '/usr/local/sbin/unbound-control-setup -d {$g['unbound_chroot_path']}{$param1}' | /usr/bin/su -m unbound", true);
			break;
		default:
			break;
	}
}

function unbound_add_domain_overrides($pvt_rev="", $cfgsubdir = "") {
	global $config, $g;

	$domains = $config['unbound']['domainoverrides'];

	$sorted_domains = msort($domains, "domain");
	$result = array();
	foreach ($sorted_domains as $domain) {
		$domain_key = current($domain);
		if (!isset($result[$domain_key])) {
			$result[$domain_key] = array();
		}
		$result[$domain_key][] = $domain['ip'];
	}

	// Domain overrides that have multiple entries need multiple stub-addr: added
	$domain_entries = "";
	foreach ($result as $domain=>$ips) {
		if ($pvt_rev == "private") {
			$domain_entries .= "private-domain: \"$domain\"\n";
			$domain_entries .= "domain-insecure: \"$domain\"\n";
		} else if ($pvt_rev == "reverse") {
			if ((substr($domain, -14) == ".in-addr.arpa.") || (substr($domain, -13) == ".in-addr.arpa")) {
				$domain_entries .= "local-zone: \"$domain\" typetransparent\n";
			}
		} else {
			$domain_entries .= "forward-zone:\n";
			$domain_entries .= "\tname: \"$domain\"\n";
			foreach ($ips as $ip) {
				$domain_entries .= "\tforward-addr: $ip\n";
			}
		}
	}

	if ($pvt_rev != "") {
		return $domain_entries;
	} else {
		create_unbound_chroot_path($cfgsubdir);
		file_put_contents("{$g['unbound_chroot_path']}{$cfgsubdir}/domainoverrides.conf", $domain_entries);
	}
}

function unbound_add_host_entries($cfgsubdir = "") {
	global $config, $g;

	// Make sure the config setting is a valid unbound local zone type.  If not use "transparent".
	if (array_key_exists($config['unbound']['system_domain_local_zone_type'], unbound_local_zone_types())) {
		$system_domain_local_zone_type = $config['unbound']['system_domain_local_zone_type'];
	} else {
		$system_domain_local_zone_type = "transparent";
	}

	$unbound_entries = "local-zone: \"{$config['system']['domain']}\" {$system_domain_local_zone_type}\n";

	$hosts = read_hosts();
	$added_ptr = array();
	foreach ($hosts as $host) {
		if (is_ipaddrv4($host['ipaddr'])) {
			$type = 'A';
		} else if (is_ipaddrv6($host['ipaddr'])) {
			$type = 'AAAA';
		} else {
			continue;
		}

		if (!$added_ptr[$host['ipaddr']]) {
			$unbound_entries .= "local-data-ptr: \"{$host['ipaddr']} {$host['fqdn']}\"\n";
			$added_ptr[$host['ipaddr']] = true;
		}
		$unbound_entries .= "local-data: \"{$host['fqdn']} {$type} {$host['ipaddr']}\"\n";
		if (isset($host['name'])) {
			$unbound_entries .= "local-data: \"{$host['name']} {$type} {$host['ipaddr']}\"\n";
		}
	}

	// Write out entries
	create_unbound_chroot_path($cfgsubdir);
	file_put_contents("{$g['unbound_chroot_path']}{$cfgsubdir}/host_entries.conf", $unbound_entries);

	/* dhcpleases will write to this config file, make sure it exists */
	@touch("{$g['unbound_chroot_path']}{$cfgsubdir}/dhcpleases_entries.conf");
}

function unbound_control($action) {
	global $config, $g;

	$cache_dumpfile = "/var/tmp/unbound_cache";

	switch ($action) {
	case "start":
		// Start Unbound
		if ($config['unbound']['enable'] == "on") {
			if (!is_service_running("unbound")) {
				do_as_unbound_user("start");
			}
		}
		break;
	case "stop":
		if ($config['unbound']['enable'] == "on") {
			do_as_unbound_user("stop");
		}
		break;
	case "reload":
		if ($config['unbound']['enable'] == "on") {
			do_as_unbound_user("reload");
		}
		break;
	case "dump_cache":
		// Dump Unbound's Cache
		if ($config['unbound']['dumpcache'] == "on") {
			do_as_unbound_user("dump_cache");
		}
		break;
	case "restore_cache":
		// Restore Unbound's Cache
		if ((is_service_running("unbound")) && ($config['unbound']['dumpcache'] == "on")) {
			if (file_exists($cache_dumpfile) && filesize($cache_dumpfile) > 0) {
				do_as_unbound_user("load_cache < /var/tmp/unbound_cache");
			}
		}
		break;
	default:
		break;

	}
}

// Generation of Unbound statistics
function unbound_statistics() {
	global $config;

	if ($config['stats'] == "on") {
		$stats_interval = $config['unbound']['stats_interval'];
		$cumulative_stats = $config['cumulative_stats'];
		if ($config['extended_stats'] == "on") {
			$extended_stats = "yes";
		} else {
			$extended_stats = "no";
		}
	} else {
		$stats_interval = "0";
		$cumulative_stats = "no";
		$extended_stats = "no";
	}
	/* XXX To do - add RRD graphs */
	$stats = <<<EOF
# Unbound Statistics
statistics-interval: {$stats_interval}
extended-statistics: yes
statistics-cumulative: yes

EOF;

	return $stats;
}

// Unbound Access lists
function unbound_acls_config($cfgsubdir = "") {
	global $g, $config;

	if (!isset($config['unbound']['disable_auto_added_access_control'])) {
		$aclcfg = "access-control: 127.0.0.1/32 allow\n";
		$aclcfg .= "access-control: ::1 allow\n";
		// Add our networks for active interfaces including localhost
		if (!empty($config['unbound']['active_interface'])) {
			$active_interfaces = array_flip(explode(",", $config['unbound']['active_interface']));
			if (in_array("all", $active_interfaces)) {
				$active_interfaces = get_configured_interface_with_descr();
			}
		} else {
			$active_interfaces = get_configured_interface_with_descr();
		}

		$bindints = "";
		foreach ($active_interfaces as $ubif => $ifdesc) {
			$ifip = get_interface_ip($ubif);
			if (is_ipaddrv4($ifip)) {
				// IPv4 is handled via NAT networks below
			}
			$ifip = get_interface_ipv6($ubif);
			if (is_ipaddrv6($ifip)) {
				if (!is_linklocal($ifip)) {
					$subnet_bits = get_interface_subnetv6($ubif);
					$subnet_ip = gen_subnetv6($ifip, $subnet_bits);
					// only add LAN-type interfaces
					if (!interface_has_gateway($ubif)) {
						$aclcfg .= "access-control: {$subnet_ip}/{$subnet_bits} allow\n";
					}
				}
				// add for IPv6 static routes to local networks
				// for safety, we include only routes reachable on an interface with no
				// gateway specified - read: not an Internet connection.
				$static_routes = get_staticroutes();
				foreach ($static_routes as $route) {
					if ((lookup_gateway_interface_by_name($route['gateway']) == $ubif) && !interface_has_gateway($ubif)) {
						// route is on this interface, interface doesn't have gateway, add it
						$aclcfg .= "access-control: {$route['network']} allow\n";
					}
				}
			}
		}

		// Generate IPv4 access-control entries using the same logic as automatic outbound NAT
		if (empty($FilterIflist)) {
			filter_generate_optcfg_array();
		}
		$natnetworks_array = array();
		$natnetworks_array = filter_nat_rules_automatic_tonathosts();
		foreach ($natnetworks_array as $allowednet) {
			$aclcfg .= "access-control: $allowednet allow \n";
		}
	}

	// Configure the custom ACLs
	if (is_array($config['unbound']['acls'])) {
		foreach ($config['unbound']['acls'] as $unbound_acl) {
			$aclcfg .= "#{$unbound_acl['aclname']}\n";
			foreach ($unbound_acl['row'] as $network) {
				if ($unbound_acl['aclaction'] == "allow snoop") {
					$unbound_acl['aclaction'] = "allow_snoop";
				}
				$aclcfg .= "access-control: {$network['acl_network']}/{$network['mask']} {$unbound_acl['aclaction']}\n";
			}
		}
	}
	// Write out Access list
	create_unbound_chroot_path($cfgsubdir);
	file_put_contents("{$g['unbound_chroot_path']}{$cfgsubdir}/access_lists.conf", $aclcfg);

}

// Generate hosts and reload services
function unbound_hosts_generate() {
	// Generate our hosts file
	unbound_add_host_entries();

	// Reload our service to read the updates
	unbound_control("reload");
}

// Array of valid unbound local zone types
function unbound_local_zone_types() {
	return array(
		"deny" => gettext("Deny"),
		"refuse" => gettext("Refuse"),
		"static" => gettext("Static"),
		"transparent" => gettext("Transparent"),
		"typetransparent" => gettext("Type Transparent"),
		"redirect" => gettext("Redirect"),
		"inform" => gettext("Inform"),
		"inform_deny" => gettext("Inform Deny"),
		"nodefault" => gettext("No Default")
	);
}

?>
