Small Office Services: Difference between revisions
m (→Setup Host Box) |
(use relative symlinks) |
||
(75 intermediate revisions by 8 users not shown) | |||
Line 1: | Line 1: | ||
{{Tip|At the time of writing | '''Abstract''': This document will outline how to provide various network services for a small remote office, using Linux containerization (LXC). It is designed to be a complement to the [[Dynamic Multipoint VPN (DMVPN)|DMVPN]] spoke node. | ||
The following services will be available in addition to the encrypted communications between offices provided by the DMVPN network: | |||
* Internet browsing proxy server with domain filtering (wired clients on protected internal network) | |||
* Separate proxy for wifi clients | |||
* SIP phone system including web based provisioning and basic voicemail services | |||
The assumption is made that the following VLANs and subnets are used as the DMVPN document did: | |||
{|class="wikitable" | |||
!'''Interface''' | |||
!'''Description''' | |||
!'''Subnet''' | |||
|- | |||
|bond0.3 | |||
|Management | |||
|10.1.0.129/26 | |||
|- | |||
|bond0.101 | |||
|LAN | |||
|10.1.0.0/25 | |||
|- | |||
|bond0.256 | |||
|Internet from ISP1 | |||
|Allocated from ISP | |||
|- | |||
|bond0.257 | |||
|Internet from ISP2 | |||
|Allocated from ISP | |||
|- | |||
|bond0.620 | |||
|Transit between wifi proxy and dmvpn spoke node | |||
|10.1.0.252/30 | |||
|- | |||
|bond0.701 | |||
|WiFi clients (no access to DMVPN network) | |||
|172.17.48.0/24 | |||
|- | |||
|bond0.1101 | |||
|Voice | |||
|10.2.0.0/24 | |||
|} | |||
{{Tip|At the time of writing, the recommended Alpine version for building the Host box for the containers should be at minimum 2.7.9 64 bit.}} | |||
= Hardware = | = Hardware = | ||
For an office that will serve under 20 people, the following containers can easily run on low-power hardware such as a Via Nano 1.6Ghz Jetway board with 8GB RAM with dual 500GB SATA hard drives running in RAID 1 (software). | |||
= Setup LXC Host Box = | = Setup LXC Host Box = | ||
== Boot Alpine USB == | == Boot Alpine USB == | ||
Follow the instructions | Follow the instructions at [[Create a Bootable Device]]. | ||
== Alpine Setup == | == Alpine Setup == | ||
Line 62: | Line 106: | ||
|''Press Enter confirming 'UTC''' | |''Press Enter confirming 'UTC''' | ||
|- | |- | ||
|<code>HTTP/FTP proxy URL? (e.g. 'http://proxy:8080', or 'none') [none]</code> | |<code>HTTP/FTP proxy URL? (e.g. '<nowiki>http://proxy:8080</nowiki>', or 'none') [none]</code> | ||
|''http://'''<% | |''http://'''<%DMVPN_LAN_IP%>''':8080'' | ||
|- | |- | ||
|<code>Enter mirror number (1-9) or URL to add (or r/f/e/done) [f]:</code> | |<code>Enter mirror number (1-9) or URL to add (or r/f/e/done) [f]:</code> | ||
Line 99: | Line 143: | ||
== Setup Networking == | == Setup Networking == | ||
With your favorite editor configure /etc/network/interfaces | With your favorite editor configure /etc/network/interfaces | ||
{{cat|/etc/ | {{cat|/etc/network/interfaces| | ||
auto lo | auto lo | ||
iface lo inet loopback | iface lo inet loopback | ||
Line 106: | Line 150: | ||
iface bond0 inet manual | iface bond0 inet manual | ||
bond-slaves eth0 eth1 | bond-slaves eth0 eth1 | ||
bond-mode balance-tlb | |||
bond-miimon 100 | |||
bond-updelay 500 | |||
up ip link set $IFACE up | up ip link set $IFACE up | ||
down ip link set $IFACE down | down ip link set $IFACE down | ||
Line 128: | Line 173: | ||
auto bond0.701 | auto bond0.701 | ||
iface bond0. | iface bond0.701 inet manual | ||
up ip link set $IFACE up | up ip link set $IFACE up | ||
down ip link set $IFACE down | down ip link set $IFACE down | ||
Line 134: | Line 179: | ||
Apply changes by restarting networking | Apply changes by restarting networking | ||
{{Cmd| | {{Cmd|rc-service networking restart}} | ||
== Enable IP Forwarding == | == Enable IP Forwarding == | ||
Line 141: | Line 186: | ||
{{Cmd|apk add acf-awall}} | {{Cmd|apk add acf-awall}} | ||
With your favorite editor, create | With your favorite editor, create the base policy for the firewall | ||
{{cat|/etc/awall/optional/base.json| | {{cat|/etc/awall/optional/base.json| | ||
{ | { | ||
"description": " | "description": "Management", | ||
"policy": [ | "policy": [ | ||
Line 159: | Line 204: | ||
} | } | ||
}} | }} | ||
Activate the | Activate the firewall, and allow iptables to startup automatically at boot | ||
{{Cmd|modprobe ip_tables | {{Cmd|modprobe ip_tables | ||
awall enable base | awall enable base | ||
awall activate | awall activate -f | ||
rc-update add iptables}} | |||
== Install LXC == | == Install LXC == | ||
Line 187: | Line 230: | ||
{{Cmd|lbu ci | {{Cmd|lbu ci | ||
reboot}} | reboot}} | ||
= Install the Web Proxy Container = | |||
== Create and Configure the container == | |||
{{Cmd|lxc-create -n webproxy -f /etc/lxc/default.conf -t alpine}} | |||
Create the startup Script | |||
{{Cmd|ln -s lxc /etc/init.d/lxc.webproxy}} | |||
Edit the container's config file found at /var/lib/lxc/webproxy/config, to reflect the network for the web proxy container | |||
{{cat|/var/lib/lxc/webproxy/config| | |||
... | |||
lxc.network.link {{=}} bond0.101 | |||
... | |||
}} | |||
Start the container | |||
{{Cmd|rc-service lxc.webproxy start}} | |||
Configure the container to automatically start | |||
{{Cmd|rc-update add lxc.webproxy}} | |||
== Enter the webproxy container == | |||
{{Cmd|lxc-console -n webproxy}} | |||
Login as root | |||
{{Note|If the need arises to exit the container press {{Key| Ctrl}}+{{Key| a}} + {{Key| q}}}} | |||
Remove obsolete /etc/network/interfaces | |||
{{Cmd|rm /etc/network/interfaces}} | |||
Create and configure the new /etc/network/interfaces as shown below: | |||
{{cat|/etc/network/interfaces| | |||
auto lo | |||
iface lo inet loopback | |||
auto eth0 | |||
iface eth0 inet static | |||
address 10.1.0.2 | |||
netmask 255.255.255.192 | |||
gateway 10.1.0.1 | |||
}} | |||
Startup networking | |||
{{Cmd| rc-service networking start}} | |||
Add rule to DMVPN awall policy to allow this proxy out to the internet | |||
{{Note| this is to be configured on the DMVPN awall config}} | |||
{{cat| /etc/awall/optional/internet-host.json| | |||
{ | |||
"in": "B", | |||
"src": "$10.1.0.2", | |||
"out": "E", | |||
"action": "accept", | |||
}, | |||
}} | |||
Configure remote administration | |||
{{Cmd|apk update | |||
setup-sshd -c openssh | |||
sed -i "s/.PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config | |||
sed -i "s/.UseDNS yes/UseDNS no/" /etc/ssh/sshd_config}} | |||
Start ssh | |||
{{Cmd|rc-service sshd start}} | |||
Configure a passwd for the container | |||
{{Cmd|passwd}} | |||
Setup acf for web administration | |||
{{Cmd|setup-acf}} | |||
== Setup Firewall == | |||
{{Cmd|apk add acf-awall}} | |||
With your favorite editor, create the policies for the firewall | |||
{{cat|/etc/awall/optional/base.json| | |||
{ | |||
"description": "Management", | |||
"policy": [ | |||
{ "in": "_fw", "action": "accept" } | |||
], | |||
"filter": [ | |||
{ | |||
"out": "_fw", | |||
"service": [ "ssh", "https", "ping" ], | |||
"action": "accept" | |||
} | |||
] | |||
} | |||
}} | |||
{{cat|/etc/awall/optional/webproxy.json| | |||
{ | |||
"description": "Web Proxy", | |||
"filter": [ | |||
{ | |||
"out": "_fw", | |||
"service": [ "http", "http-alt" ], | |||
"action": "accept" | |||
} | |||
] | |||
} | |||
}} | |||
Activate the firewall, and allow iptables to startup automatically at boot | |||
{{Cmd|awall enable base | |||
awall enable webproxy | |||
awall activate -f | |||
rc-update add iptables | |||
}} | |||
== Install and Configure the Squid Web Proxy Service == | |||
Install the required packages | |||
{{Cmd|apk add acf-squid squark acf-lighttpd}} | |||
Configure /etc/squid/squid.conf, replace <%WEBPROXY_IP_ADDRESS%>, <%HOSTNAME%>, and <%DOMAIN%> | |||
{{cat|/etc/squid/squid.conf| | |||
<pre> | |||
#Squid config for webproxy | |||
# This port listens for client requests | |||
http_port 8080 | |||
visible_hostname <%HOSTNAME%>.<%DOMAIN%> | |||
cache_mem 8 MB | |||
# If you don't have an HD installed comment the "cache_dir" line below | |||
cache_dir aufs /var/cache/squid 900 16 256 | |||
# Even though we only use one proxy, this line is recommended | |||
# More info: http://www.squid-cache.org/Versions/v2/2.7/cfgman/hierarchy_stoplist.html | |||
hierarchy_stoplist cgi-bin ? | |||
# Keep 7 days of access logs | |||
logfile_rotate 7 | |||
logformat squark %ts.%03tu %6tr %>a %Ss/%03>Hs %<st %rm %ru %un %Sh/%<A %mt %rG | |||
access_log /var/log/squid/access.log squark | |||
cache_store_log none | |||
pid_filename /var/run/squid.pid | |||
# Make sure client IP is passed to Squark | |||
log_uses_indirect_client on | |||
acl_uses_indirect_client on | |||
# Fix for problems with branch file transfer application | |||
# ignore_expect_100 on (deprecated) | |||
# Debugging Squid, see http://wiki.squid-cache.org/KnowledgeBase/DebugSections | |||
# for more info | |||
# Keep 7 days of cache log | |||
debug_options rotate=7 | |||
# Web auditors want to see the full uri, even with the query terms | |||
strip_query_terms off | |||
refresh_pattern ^ftp: 1440 20% 10080 | |||
refresh_pattern ^gopher: 1440 0% 1440 | |||
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 | |||
refresh_pattern . 0 20% 4320 | |||
coredump_dir /var/cache/squid | |||
# | |||
# Authentication | |||
# | |||
# | |||
# Access Control Lists (ACL's) | |||
# | |||
# Standard ACL settings | |||
acl QUERY urlpath_regex cgi-bin \? asp aspx jsp | |||
acl to_localhost dst <%WEBPROXY_IP_ADDRESS%> | |||
acl SSL_ports port 443 563 8004 9000 | |||
acl Safe_ports port 21 70 80 81 210 280 443 563 499 591 777 1024 1022 1025-65535 | |||
acl purge method PURGE | |||
acl CONNECT method CONNECT | |||
# Squark filter | |||
url_rewrite_program /usr/bin/squark-filter | |||
url_rewrite_children 1 concurrency=128 | |||
# Require authentication | |||
acl userlist src all | |||
# Definition of zones | |||
acl Zone_B src <%LAN_SUBNET%>/<%LAN_SLASH_NOTATION%> | |||
#acl Zone_D src <%WiFi_SUBNET%>/<%WiFi_SLASH_NOTATION%> | |||
# Settings migrated from smn | |||
acl Zone_B_AllowedUserDomains dstdomain "/etc/squid/alloweduserdomains" | |||
acl Zone_B_AllowedServicesHosts src "/etc/squid/allowedserviceshosts" | |||
acl Zone_B_AllowedServicesDomains dstdomain "/etc/squid/allowedservicesdomains" | |||
# Settings migrated from services | |||
acl AnonBrowsers browser "/etc/squid/anonbrowserlist" | |||
acl AnonIPAddrs src "/etc/squid/anoniplist" | |||
acl AnonDomain url_regex "/etc/squid/anondomainlist" | |||
# | |||
# Access restrictions | |||
# | |||
cache deny QUERY | |||
# Only allow cachemgr access from localhost | |||
http_access allow manager localhost | |||
http_access deny manager | |||
# Only allow purge requests from localhost | |||
http_access allow purge localhost | |||
http_access deny purge | |||
# Deny requests to unknown ports | |||
http_access deny !Safe_ports | |||
# Deny CONNECT to other than SSL ports | |||
http_access deny CONNECT !SSL_ports | |||
# Allow hosts in Zone_B and Zone_C to access hosts listed in | |||
# /etc/squid/alloweduserdomains | |||
http_access allow Zone_B Zone_B_AllowedUserDomains | |||
# Allow hosts listed in /etc/squid/allowedserviceshosts to | |||
# access domains listed in /etc/squid/allowedservicesdomains | |||
http_access allow Zone_B_AllowedServicesHosts Zone_B_AllowedServicesDomains | |||
# Denying all access not explictly allowed | |||
http_access deny all | |||
##Squark URL rewriter | |||
#Prevent squark from filtering itself | |||
url_rewrite_access deny manager | |||
url_rewrite_access deny to_localhost | |||
#We do not want authentication for these sites: | |||
url_rewrite_access deny Zone_B Zone_B_AllowedUserDomains | |||
url_rewrite_access deny Zone_B Zone_B_AllowedServicesDomains | |||
http_reply_access allow all | |||
icp_access allow all | |||
</pre> | |||
}} | |||
Configure /etc/lighttpd/lighttpd.conf, replace <%WEBPROXY_IP_ADDRESS%> | |||
{{cat|/etc/lighttpd/lighttpd.conf| | |||
<pre> | |||
############################################################################## | |||
# Default lighttpd.conf for Gentoo. | |||
# $Header: /var/cvsroot/gentoo-x86/www-servers/lighttpd/files/conf/lighttpd.conf,v 1.3 2005/09/01 14:22:35 ka0ttic Exp $ | |||
############################################################################### | |||
var.basedir = "/var/www/localhost" | |||
var.logdir = "/var/log/lighttpd" | |||
var.statedir = "/var/lib/lighttpd" | |||
server.modules = ( | |||
"mod_access", | |||
"mod_accesslog", | |||
"mod_extforward" | |||
) | |||
include "mime-types.conf" | |||
include "mod_cgi.conf" | |||
server.username = "lighttpd" | |||
server.groupname = "lighttpd" | |||
server.document-root = var.basedir + "/squark" | |||
server.pid-file = "/var/run/lighttpd.pid" | |||
server.errorlog = var.logdir + "/error.log" | |||
server.indexfiles = ("index.php", "index.html", | |||
"index.htm", "default.htm") | |||
server.follow-symlink = "enable" | |||
static-file.exclude-extensions = (".php", ".pl", ".cgi", ".fcgi") | |||
accesslog.filename = var.logdir + "/access.log" | |||
url.access-deny = ("~", ".inc") | |||
extforward.forwarder = ("<%WEBPROXY_IP_ADDRESS%>" => "trust") | |||
</pre> | |||
}} | |||
Configure mod_cgi.conf | |||
{{cat|/etc/lighttpd/mod_cgi.conf| | |||
<pre> | |||
############################################################################### | |||
# mod_cgi.conf | |||
# include'd by lighttpd.conf. | |||
# $Header: /var/cvsroot/gentoo-x86/www-servers/lighttpd/files/conf/mod_cgi.conf,v 1.1 2005/08/27 12:36:13 ka0ttic Exp $ | |||
############################################################################### | |||
# | |||
# see cgi.txt for more information on using mod_cgi | |||
# | |||
server.modules += ("mod_cgi") | |||
# NOTE: this requires mod_alias | |||
alias.url = ( | |||
"/cgi-bin/" => var.basedir + "/cgi-bin/" | |||
) | |||
# | |||
# Note that you'll also want to enable the | |||
# cgi-bin alias via mod_alias (above). | |||
# | |||
$HTTP["url"] =~ "^/cgi-bin/" { | |||
# disable directory listings | |||
dir-listing.activate = "disable" | |||
# only allow cgi's in this directory | |||
cgi.assign = ( | |||
".pl" => "/usr/bin/perl", | |||
".cgi" => "/usr/bin/haserl" | |||
) | |||
} | |||
</pre> | |||
}} | |||
Link the Squark web pages to the Web server home directory | |||
{{Cmd|ln -s /usr/share/squark/www/ /var/www/localhost/squark}} | |||
Create a Squark group | |||
{{Cmd|addgroup squark}} | |||
Make 'squid' and 'lighttpd' users member of the group squark | |||
{{Cmd|addgroup squid squark | |||
addgroup lighttpd squark}} | |||
Start lighttpd, and configure the service to start on when container is booted | |||
{{Cmd|rc-service lighttpd start | |||
rc-update add lighttpd}} | |||
Start Squid, and configure to start at boot | |||
{{Cmd|rc-service squid start | |||
rc-update add squid}} | |||
= Install the DHCP and DNS server Container = | |||
== Create and Configure the container == | |||
{{Cmd|lxc-create -n dhcpdns -f /etc/lxc/default.conf -t alpine}} | |||
Create the startup Script | |||
{{Cmd|ln -s lxc /etc/init.d/lxc.dhcpdns}} | |||
Edit the container's config file found at /var/lib/lxc/dhcpdns/config, to reflect the network for the web proxy container | |||
{{cat|/var/lib/lxc/dhcpdns/config| | |||
<pre> | |||
#Management Network Config | |||
lxc.network.type = macvlan | |||
lxc.network.macvlan.mode = bridge | |||
lxc.network.link = bond0.3 | |||
lxc.network.name = eth0 | |||
#WiFi Network Config | |||
lxc.network.type = macvlan | |||
lxc.network.macvlan.mode = bridge | |||
lxc.network.link = bond0.701 | |||
lxc.network.name = eth1 | |||
#Voice Network Config | |||
lxc.network.type = macvlan | |||
lxc.network.macvlan.mode = bridge | |||
lxc.network.link = bond0.1101 | |||
lxc.network.name = eth2 | |||
</pre> | |||
}} | |||
Start the container | |||
{{Cmd|rc-service lxc.dhcpdns start}} | |||
Configure the container to automatically start | |||
{{Cmd|rc-update add lxc.dhcpdns}} | |||
== Enter the dhcpdns container == | |||
{{Cmd|lxc-console -n dhcpdns}} | |||
Login as root | |||
{{Note|If the need arises to exit the container press {{Key| Ctrl}}+{{Key| a}} + {{Key| q}}}} | |||
Remove obsolete /etc/network/interfaces | |||
{{Cmd|rm /etc/network/interfaces}} | |||
Create and configure the new /etc/network/interfaces as shown below: | |||
{{cat|/etc/network/interfaces| | |||
auto lo | |||
iface lo inet loopback | |||
#Management VLAN | |||
auto eth0 | |||
iface eth0 inet static | |||
address 10.1.0.130 | |||
netmask 255.255.255.192 | |||
#WiFi VLAN | |||
auto eth1 | |||
iface eth1 inet static | |||
address 172.16.48.2 | |||
netmask 255.255.255.0 | |||
#Voice VLAN | |||
auto eth2 | |||
iface eth2 inet static | |||
address 10.2.0.2 | |||
netmask 255.255.255.0 | |||
gateway 10.2.0.1 | |||
up ip address add 10.2.0.3/24 dev eth0 | |||
}} | |||
Startup networking | |||
{{Cmd| rc-service networking start}} | |||
Configure and enable proxy settings | |||
{{Cmd|setup-proxy <nowiki>http://10.1.0.2:8080</nowiki> | |||
. /etc/profile.d/proxy.sh}} | |||
Configure remote administration | |||
{{Cmd|apk update | |||
setup-sshd -c openssh | |||
sed -i "s/.PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config | |||
sed -i "s/.UseDNS yes/UseDNS no/" /etc/ssh/sshd_config}} | |||
Start ssh | |||
{{Cmd|rc-service sshd start}} | |||
Configure a passwd for the container | |||
{{Cmd|passwd}} | |||
Setup acf for web administration | |||
{{Cmd|setup-acf}} | |||
== Setup Firewall == | |||
{{Cmd|apk add acf-awall}} | |||
With your favorite editor, create the policies for the firewall | |||
{{cat|/etc/awall/optional/base.json| | |||
{ | |||
"description": "Management", | |||
"policy": [ | |||
{ "in": "_fw", "action": "accept" } | |||
], | |||
"filter": [ | |||
{ | |||
"out": "_fw", | |||
"service": [ "ssh", "https", "ping" ], | |||
"action": "accept" | |||
} | |||
] | |||
} | |||
}} | |||
{{cat|/etc/awall/optional/dhcp.json| | |||
{ | |||
"description": "DHCP", | |||
"filter": [ | |||
{ | |||
"out": "_fw", | |||
"service": "dhcp", | |||
"action": "accept" | |||
} | |||
] | |||
} | |||
}} | |||
{{cat|/etc/awall/optional/dns.json| | |||
{ | |||
"description": "DNS", | |||
"filter": [ | |||
{ | |||
"out": "_fw", | |||
"service": "dns", | |||
"action": "accept" | |||
} | |||
] | |||
} | |||
}} | |||
Activate the firewall, and allow iptables to startup automatically at boot | |||
{{Cmd|awall enable base | |||
awall enable dhcp | |||
awall enable dns | |||
awall activate -f | |||
rc-update add iptables | |||
}} | |||
== Install and Configure DHCP and DNS services == | |||
install the dhcpd package | |||
{{Cmd|apk add acf-dhcp}} | |||
Create a new dhcpd.conf file | |||
{{cat|/etc/dhcp/dhcpd.conf| | |||
<pre> | |||
## Common settings | |||
default-lease-time 302400; | |||
max-lease-time 604800; | |||
ddns-update-style none; | |||
log-facility local7; | |||
authoritative; | |||
## Common options | |||
option time-servers 10.2.0.1; | |||
option boot-server code 66 = string; | |||
## Voice | |||
subnet 10.2.0.0 netmask 255.255.255.0 | |||
{ | |||
range "10.2.0.20 10.2.0.250"; | |||
option domain-name-servers 10.2.0.2; | |||
option routers 10.2.0.1; | |||
option boot-server "http://10.2.0.4"; | |||
option domain-name "office.example.net"; | |||
} | |||
## WiFi | |||
subnet 172.17.48.0 netmask 255.255.255.0 | |||
{ | |||
range "172.17.48.10 172.17.48.250"; | |||
option routers 172.17.48.1; | |||
option domain-name-servers 172.17.48.1; | |||
} | |||
</pre> | |||
}} | |||
Start DHCP service and add to runlevel default | |||
{{Cmd|rc-service dhcpd start | |||
rc-update add dhcpd}} | |||
Install nsd and unbound packages | |||
{{Cmd|apk add unbound }} | |||
Remove unbound.conf | |||
{{Cmd|rm /etc/unbound/unbound.conf}} | |||
Create with your favorite editor a new configuration for unbound | |||
{{cat|/etc/unbound/unbound.conf| | |||
#Recursive DNS configuration | |||
server: | |||
interface: 10.2.0.2 | |||
do-not-query-localhost: no | |||
verbosity: 1 | |||
do-ip4: yes | |||
do-ip6: no | |||
do-udp: yes | |||
do-tcp: yes | |||
do-daemonize: yes | |||
access-control: 10.1.0.0/16 allow | |||
access-control: 127.0.0.0/8 allow | |||
#use the root.hints file to determine where to send DNS queries outside of network | |||
root-hints: "/etc/unbound/root.hints" | |||
stub-zone: | |||
name: "office.example.net" | |||
stub-addr: 10.2.0.3 | |||
stub-zone: | |||
name: "example.net" | |||
stub-addr: 172.16.255.1 | |||
stub-addr: 172.16.255.2 | |||
stub-addr: 172.16.255.3 | |||
stub-addr: 172.16.255.4 | |||
stub-addr: 172.16.255.5 | |||
stub-addr: 172.16.255.7 | |||
stub-zone: | |||
name: "example2.net" | |||
stub-addr: 172.16.255.1 | |||
stub-addr: 172.16.255.2 | |||
stub-addr: 172.16.255.3 | |||
stub-addr: 172.16.255.4 | |||
stub-addr: 172.16.255.5 | |||
stub-addr: 172.16.255.7 | |||
}} | |||
Start Unbound and allow the container to use it | |||
{{Cmd|rc-service unbound start | |||
rc-update add unbound | |||
echo nameserver 10.2.0.2 > /etc/resolv.conf | |||
Install nsd | |||
{{Cmd|apk add nsd}} | |||
Configure nsd configuration | |||
{{cat|/etc/nsd/nsd.conf| | |||
server: | |||
ip-address: 10.2.0.3 | |||
port: 53 | |||
server-count: 1 | |||
ip4-only: yes | |||
hide-version: yes | |||
identity: "" | |||
zonesdir: "/etc/nsd" | |||
zone: | |||
name: office.example.net | |||
zonefile: office.example.net.zone | |||
}} | |||
Configure Zone file for nsd | |||
{{cat|/etc/nsd/nsd.conf| | |||
$ORIGIN office.example.net. | |||
$TTL 86400 | |||
@ IN SOA ns admin ( | |||
2013032200 ; Serial number [yyyymmddnn] | |||
28800 ; Refresh | |||
7200 ; Retry | |||
864000 ; Expire | |||
86400 ; Min TTL | |||
) | |||
@ NS ns1 | |||
; NSA Servers | |||
ns1 IN A 10.2.0.3 | |||
;A Records for SIP Devices | |||
sip IN A 10.2.0.4 | |||
media IN A 10.2.0.5 | |||
;NAPTR Records | |||
sip IN NAPTR 10 1 "s" "SIP+D2U" "" _sip._udp.sip.office.example.net. | |||
;SIP SRV Record | |||
_sip._udp.sip IN SRV 10 100 5060 sip | |||
}} | |||
Check nsd configuration and start service | |||
{{Cmd|nsd-checkconf /etc/nsd/nsd.conf | |||
rc-service nsd start | |||
rc-update add nsd}} | |||
= Install the SIP Container = | |||
== Create and Configure the container == | |||
{{Cmd|lxc-create -n sip -f /etc/lxc/default.conf -t alpine}} | |||
Create the startup Script | |||
{{Cmd|ln -s lxc /etc/init.d/lxc.sip}} | |||
Edit the container's config file found at /var/lib/lxc/sip/config, to reflect the network for the sip container | |||
{{cat|/var/lib/lxc/sip/config| | |||
... | |||
lxc.network.link {{=}} bond0.1101 | |||
... | |||
}} | |||
Start the container | |||
{{Cmd|/etc/iniit.d/lxc.sip}} | |||
Configure the container to automatically start | |||
{{Cmd|rc-update add lxc.sip}} | |||
== Enter the sip container == | |||
{{Cmd|lxc-console -n sip}} | |||
Login as root | |||
{{Note|If the need arises to exit the container press {{Key| Ctrl}}+{{Key| a}} + {{Key| q}}}} | |||
Remove obsolete /etc/network/interfaces | |||
{{Cmd|rm /etc/network/interfaces}} | |||
Create and configure the new /etc/network/interfaces as shown below: | |||
{{cat|/etc/network/interfaces| | |||
auto lo | |||
iface lo inet loopback | |||
auto eth0 | |||
iface eth0 inet static | |||
address 10.2.0.4 | |||
netmask 255.255.255.0 | |||
gateway 10.2.0.1 | |||
}} | |||
Startup networking | |||
{{Cmd| rc-service networking start}} | |||
Configure and enable proxy settings | |||
{{Cmd|setup-proxy <nowiki>http://10.1.0.2:8080</nowiki> | |||
. /etc/profile.d/proxy.sh}} | |||
Configure remote administration | |||
{{Cmd|apk update | |||
setup-sshd -c openssh | |||
sed -i "s/.PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config | |||
sed -i "s/.UseDNS yes/UseDNS no/" /etc/ssh/sshd_config}} | |||
Start ssh | |||
{{Cmd|rc-service sshd start}} | |||
Configure a passwd for the container | |||
{{Cmd|passwd}} | |||
Setup acf for web administration | |||
{{Cmd|setup-acf}} | |||
== Setup Firewall == | |||
{{Cmd|apk add acf-awall}} | |||
With your favorite editor, create the policies for the firewall | |||
{{cat|/etc/awall/optional/base.json| | |||
{ | |||
"description": "Management", | |||
"policy": [ | |||
{ "in": "_fw", "action": "accept" } | |||
], | |||
"filter": [ | |||
{ | |||
"out": "_fw", | |||
"service": [ "ssh", "https", "ping" ], | |||
"action": "accept" | |||
} | |||
] | |||
} | |||
}} | |||
{{cat|/etc/awall/optional/sip.json| | |||
{ | |||
"description": "Phone System", | |||
"filter": [ | |||
{ | |||
"out": "_fw", | |||
"service": [ "sip", "sip-tls" ], | |||
"action": "accept", | |||
} | |||
] | |||
} | |||
}} | |||
{{cat|/etc/awall/optional/syslog.json| | |||
{ | |||
"description": "Syslog server", | |||
"filter": [ | |||
{ | |||
"out": "_fw", | |||
"service": "syslog", | |||
"action": "accept" | |||
} | |||
] | |||
} | |||
}} | |||
Activate the firewall, and allow iptables to startup automatically at boot | |||
{{Cmd|awall enable base | |||
awall enable sip | |||
awall enable syslog | |||
awall activate -f | |||
rc-update add iptables | |||
}} | |||
==Install and Configure Postgresql== | |||
Install postgresql package | |||
{{Cmd|apk update | |||
apk add acf-postgresql}} | |||
Prepare the database | |||
{{Cmd|rc-service postgresql setup}} | |||
Configure /var/lib/postgresql/9.3/data/postgresql.conf to set the 'log_destination' variable to show: | |||
{{cat|/var/lib/postgresql/9.3/data/postresql.conf| | |||
.. | |||
log_destination {{=}}'syslog' | |||
}} | |||
Start up the database and configure postgresql to start at boot up | |||
{{Cmd|rc-service postgresql start | |||
rc-update add postgresql}} | |||
== Install acf-provisioning == | |||
* vi /etc/kamailio/kamctlrc | |||
<pre> | |||
SIP_DOMAIN=sip.office.example.net | |||
DBENGINE=PGSQL | |||
DBHOST=127.0.0.1 | |||
DBNAME=openser | |||
DBRWUSER=openser | |||
DBRWPW="openser" | |||
DBROUSER=openserro | |||
DBROPW=openserro | |||
DBROOTUSER="postgres" | |||
</pre> | |||
* yes | kamdbctl create openser | |||
* apk add acf-provisioning lua-socket lua-expat | |||
* Create /etc/provisioning/update_device_params.lua: | |||
<pre> | |||
-- This is the script run after editing device params - basically only worried about extension and password | |||
local functions, params, oldparams = ... | |||
require("posix") | |||
require("luasql.postgres") | |||
local root = "/var/www/provisioning/htdocs/" | |||
local b62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" | |||
APP.logevent("got to update_device_params script") | |||
local function generatepw() | |||
-- generate a random 12-character alphanumeric string | |||
local file = io.open("/dev/urandom") | |||
local str = "" | |||
if file == nil then return nil end | |||
local size = 12 | |||
while (size > 0 ) do | |||
local offset = (string.byte(file:read(1)) % 62) + 1 | |||
str = str .. string.sub (b62, offset, offset) | |||
size = size - 1 | |||
end | |||
return str | |||
end | |||
local function findip(mac) | |||
if not mac or mac == "" then | |||
return nil | |||
end | |||
local ipaddr = functions.getselectresponse("SELECT ip FROM provisioning_requests WHERE mac~*'"..mac.."'") | |||
if ipaddr and ipaddr[1] then | |||
return ipaddr[1].ip | |||
end | |||
end | |||
local function addfuturenotify(ipaddr, extension) | |||
local res, err = pcall(function() | |||
functions.runsqlcommand("DELETE FROM notify WHERE ipaddr='"..ipaddr.."' AND extension='"..extension.."'") | |||
end) | |||
if not res and err then | |||
if string.match(err, "relation \"(%S+)\" does not exist") then | |||
functions.runsqlcommand("CREATE TABLE notify (ipaddr text, extension text, seasoned boolean DEFAULT false)") | |||
else | |||
assert(res, err) | |||
end | |||
end | |||
-- if table missing, create it and delete again | |||
functions.runsqlcommand("INSERT INTO notify VALUES('"..ipaddr.."', '"..extension.."')") | |||
end | |||
local notify_device = function(mac, extension) | |||
local ipaddr = findip(mac) | |||
if ipaddr then | |||
APP.logevent("Notifying "..ipaddr.." to update for "..(mac or "")) | |||
os.execute("/etc/provisioning/notify_device "..ipaddr.." "..extension) | |||
addfuturenotify(ipaddr, extension) | |||
else | |||
APP.logevent("Warning - could not find IP address for "..(mac or "")) | |||
end | |||
end | |||
local kam = APP:new("kamailio/kamailio") | |||
-- A table of devices to notify to update | |||
local devices = {} | |||
-- First, we check the registration numbers / passwords to 1) set a random password (if necessary) 2) make sure password matches any other registrations to same extension 3) push changes (add / delete / or update) to Kam database | |||
-- Because this script also handles when device classes change, we have to consider that extensions are added / removed | |||
local regs = {} | |||
local forwarding = {"forwardnoanswerenable", "forwardnoanswer", "forwardbusyenable", "forwardbusy", "forwardallenable", "forwardall"} | |||
local forwardsettings = {} -- Collect forwarding settings | |||
local passwords = {} -- Collect old extension/password pairs | |||
for name,val in pairs(params.value) do | |||
if not regs[name] and string.match(name, "^reg") then | |||
regs[name] = true | |||
end | |||
end | |||
oldparams = oldparams or {value={}} | |||
for name,val in pairs(oldparams.value) do | |||
if string.match(name, "^reg") then | |||
if val.value and val.value.extension and val.value.password then | |||
passwords[val.value.extension.value] = val.value.password.value | |||
end | |||
if val.value and val.value.extension then | |||
local fwd = {} | |||
for i,f in ipairs(forwarding) do | |||
if val.value[f] then | |||
fwd[f] = val.value[f].value | |||
end | |||
end | |||
forwardsettings[val.value.extension.value] = fwd | |||
end | |||
if not regs[name] then | |||
regs[name] = true | |||
end | |||
end | |||
end | |||
local extension_id = params.value.reg1.value.extension.param_id | |||
local password_id = params.value.reg1.value.password.param_id | |||
functions.runsqlcommand("BEGIN TRANSACTION") | |||
for name,val in pairs(regs) do | |||
local new = "" | |||
local old = "" | |||
if params.value[name] then new = params.value[name].value.extension.value end | |||
if oldparams.value[name] then old = oldparams.value[name].value.extension.value end | |||
if new ~= old then | |||
-- Extension changed | |||
-- First, let's remove the stuff for the old extension | |||
if old ~= "" then | |||
local others = functions.getselectresponse("SELECT count(*) FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..old.."'") | |||
if others[1].count == "0" then | |||
APP.logevent("Removing old registration "..old) | |||
-- remove the registration | |||
kam.model.delete_user(old) | |||
end | |||
end | |||
-- Now, add the new extension | |||
if new ~= "" then | |||
local pass | |||
if params.value[name].value.password.value ~= "" and (not oldparams.value[name] or (oldparams.value[name].value.password.value ~= params.value[name].value.password.value)) then | |||
-- The password parameter was changed and not blank, so use it | |||
pass = params.value[name].value.password.value | |||
APP.logevent("Added a new registration "..new.." with specified password "..pass) | |||
-- There may be other devices with this extension | |||
local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
for i,o in ipairs(others) do | |||
-- We'll notify whether it changed or not | |||
devices[o.value] = new | |||
end | |||
elseif passwords[new] then | |||
pass = passwords[new] | |||
APP.logevent("Added a new registration "..new.." with reused password "..pass) | |||
else | |||
local others = functions.getselectresponse("SELECT * FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."' AND device_id!='"..params.value.device_id.value.."'") | |||
-- If this is a new registration, use a new password | |||
if #others == 0 then | |||
pass = generatepw() | |||
APP.logevent("Added a new registration "..new.." with random password "..pass) | |||
else | |||
-- Use the old password | |||
local p = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id='"..password_id.."' AND device_id='"..others[1].device_id.."' AND group_name='"..others[1].group_name.."' LIMIT 1") | |||
pass = p[1].value | |||
APP.logevent("Added a new registration "..new.." with reused password "..pass) | |||
end | |||
end | |||
passwords[new] = pass | |||
params.value[name].value.password.value = pass | |||
functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..password_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..password_id.."', '"..pass.."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
local u = kam.model.get_user(new) | |||
if u.value.username.errtxt then | |||
-- Add this registration to Kam | |||
local u = kam.model.get_new_user() | |||
u.value.username.value = new | |||
u.value.password.value = pass | |||
u.value.password_confirm.value = pass | |||
kam.model.create_new_user(u) | |||
else | |||
u.value.password.value = pass | |||
u.value.password_confirm.value = pass | |||
kam.model.update_user(u) | |||
end | |||
-- Let's also look at the forwarding settings | |||
local change = false | |||
local supported = false | |||
local fwd = {} | |||
for i,f in ipairs(forwarding) do | |||
if params.value[name].value[f] then | |||
supported = true | |||
fwd[f] = params.value[name].value[f].value | |||
if ((not oldparams.value[name] or not oldparams.value[name].value[f]) and (params.value[name].value[f].value ~= params.value[name].value[f].default)) or | |||
(oldparams.value[name] and oldparams.value[name].value[f] and (params.value[name].value[f].value ~= oldparams.value[name].value[f].value)) then | |||
change = true | |||
end | |||
end | |||
if change then | |||
local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
for i,o in ipairs(others) do | |||
-- We'll notify whether it changed or not | |||
devices[o.value] = new | |||
end | |||
end | |||
end | |||
if supported then | |||
if not change and forwardsettings[new] then | |||
fwd = forwardsettings[new] | |||
elseif not change then | |||
-- This is a new extension, and the forwarding has not been set. We should check to see if there are any other devices with this extension | |||
local others = functions.getselectresponse("SELECT * FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."' AND device_id!='"..params.value.device_id.value.."'") | |||
if #others > 0 then | |||
-- Use the existing settings | |||
for i,f in ipairs(forwarding) do | |||
local v = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id='"..params.value[name].value[f].param_id.."' AND device_id='"..others[1].device_id.."' AND group_name='"..others[1].group_name.."' LIMIT 1") | |||
if #v > 0 then | |||
fwd[f] = v[1].value | |||
else | |||
fwd[f] = params.value[name].value[f].default | |||
end | |||
end | |||
end | |||
end | |||
if not change then | |||
for i,f in ipairs(forwarding) do | |||
params.value[name].value[f].value = fwd[f] | |||
end | |||
end | |||
forwardsettings[new] = fwd | |||
for i,f in ipairs(forwarding) do | |||
functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..params.value[name].value[f].param_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
if fwd[f] ~= params.value[name].value[f].default then | |||
functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..params.value[name].value[f].param_id.."', '"..tostring(fwd[f]).."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
end | |||
end | |||
end | |||
end | |||
elseif params.value[name] and oldparams.value[name] and params.value[name].value.extension.value ~= "" then | |||
if params.value[name].value.password.value ~= oldparams.value[name].value.password.value then | |||
-- Password changed - make any other registrations to this extension also change | |||
local pass = params.value[name].value.password.value | |||
if pass and pass ~= "" then | |||
APP.logevent("Password changed for "..new.." from "..oldparams.value[name].value.password.value.." to "..pass) | |||
else | |||
pass = generatepw() | |||
APP.logevent("Password cleared for "..new..", so set new random password "..pass) | |||
end | |||
passwords[new] = pass | |||
functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..password_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..password_id.."', '"..pass.."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
local u = kam.model.get_user(new) | |||
u.value.password.value = params.value[name].value.password.value | |||
u.value.password_confirm.value = params.value[name].value.password.value | |||
kam.model.update_user(u) | |||
-- Have to notify those other devices too | |||
local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
for i,o in ipairs(others) do | |||
devices[o.value] = new | |||
end | |||
end | |||
local change = false | |||
local fwd = {} | |||
for i,f in ipairs(forwarding) do | |||
if params.value[name].value[f] and (not oldparams.value[name].value[f] or params.value[name].value[f].value ~= oldparams.value[name].value[f].value) then | |||
change = true | |||
break | |||
end | |||
end | |||
if change then | |||
-- Forwarding settings changed - make any other registrations to this extension also change | |||
for i,f in ipairs(forwarding) do | |||
functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..params.value[name].value[f].param_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
if params.value[name].value[f].value ~= params.value[name].value[f].default then | |||
functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..params.value[name].value[f].param_id.."', '"..tostring(params.value[name].value[f].value).."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
end | |||
end | |||
-- Have to notify those other devices too | |||
local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") | |||
for i,o in ipairs(others) do | |||
devices[o.value] = new | |||
end | |||
end | |||
end | |||
end | |||
functions.runsqlcommand("COMMIT") | |||
-- If reg1 or freepstn changed, then need to change free-pstn in kam | |||
-- Since there can be multiple devices with the same reg1, we can't rely on params and oldparams | |||
-- Check both reg's | |||
--APP.logevent(session.serialize("params", params)) | |||
local reg = {} | |||
if oldparams.value.reg1 and oldparams.value.reg1.value.extension and oldparams.value.reg1.value.extension.value ~= "" then | |||
reg[oldparams.value.reg1.value.extension.value] = false | |||
end | |||
if params.value.reg1 and params.value.reg1.value.extension and params.value.reg1.value.extension.value ~= "" then | |||
local pstn = false | |||
if params.value.routing and params.value.routing.value.freepstn then | |||
pstn = params.value.routing.value.freepstn.value | |||
end | |||
reg[params.value.reg1.value.extension.value] = pstn | |||
end | |||
APP.logevent("Looking at free-pstn") | |||
for r,f in pairs(reg) do | |||
-- if we're not sure, check provisioning database | |||
if not f then | |||
-- Most devices will have free-pstn due to class-of-service, but can be overridden, so this can get tricky | |||
-- Check all devices where reg1 extension = r, and see what the freepstn value is for each | |||
local others = functions.getselectresponse("SELECT CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS value ".. | |||
"FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING (class_id) JOIN provisioning_groups g USING(group_id) ".. | |||
"JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id)) LEFT JOIN provisioning_values v ON (d2t.device_id=v.device_id AND p.param_id=v.param_id AND g.name=v.group_name ) ".. | |||
"WHERE p.name='freepstn' AND d2t.device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND group_name='reg1' AND value='"..r.."')") | |||
for i,o in ipairs(others) do | |||
-- Now check the freepstn value for each one | |||
if o.value == "true" then | |||
f = true | |||
break | |||
end | |||
end | |||
end | |||
-- Now, check the Kamailio group table | |||
local alreadythere = false | |||
local entries = kam.model.list_table_entries("grp") | |||
for i,e in ipairs(entries.value.entries.value) do | |||
if e.username == r then | |||
alreadythere = true | |||
if not f then | |||
APP.logevent("Removing free-pstn for "..r) | |||
-- Remove free-pstn from the old extension | |||
kam.model.delete_table_entry("grp", e.id) | |||
end | |||
break | |||
end | |||
end | |||
if f and not alreadythere then | |||
APP.logevent("Adding free-pstn for "..r) | |||
-- Add free-pstn to the new extension | |||
local e = kam.model.get_table_entry("grp") | |||
e.value.username.value = r | |||
e.value.domain.value = "" | |||
e.value.grp.value = "free-pstn" | |||
e.value.last_modified.value = os.date("%c") | |||
kam.model.create_table_entry(e) | |||
end | |||
end | |||
kam:destroy() | |||
-- If the mac address changed for Polycom with valid MAC (not blank or all 0's), we need to move the associated config files | |||
if oldparams.value.device and oldparams.value.device.value.mac and oldparams.value.device.value.mac.value ~= "" and params.value.device and params.value.device.value.mac and params.value.device.value.mac.value ~= oldparams.value.device.value.mac.value then | |||
if string.match(oldparams.value.device.label, "Polycom") and string.match(oldparams.value.device.value.mac.value, "[1-9A-F]") then | |||
local deletefiles = true | |||
if string.match(params.value.device.value.mac.value, "[1-9A-F]") then | |||
--APP.logevent("Moving files for "..oldparams.value.device.value.mac.value) | |||
deletefiles = false | |||
else | |||
--APP.logevent("Deleting files for "..oldparams.value.device.value.mac.value) | |||
end | |||
local path = root.."Polycom/" | |||
if posix.stat(path, "type") == "directory" then | |||
for d in posix.files(path) do | |||
if string.match(d, string.lower(oldparams.value.device.value.mac.value)) and posix.stat(path..d, "type") == "regular" then | |||
local newfile = string.gsub(d, string.lower(oldparams.value.device.value.mac.value), string.lower(params.value.device.value.mac.value)) | |||
if deletefiles then | |||
--APP.logevent("deleting "..path..d) | |||
os.remove(path..d) | |||
else | |||
--APP.logevent("moving "..path..d.." to "..path..newfile) | |||
os.rename(path..d, path..newfile) | |||
end | |||
end | |||
end | |||
end | |||
end | |||
end | |||
-- Then, notify the phone to pull it's config | |||
-- Try to get a valid extension currently on the device | |||
local oldexten = "" | |||
for name,val in pairs(oldparams.value) do | |||
if string.match(name, "^reg") and val.value.extension and val.value.extension.value ~= "" then | |||
oldexten = val.value.extension.value | |||
break | |||
end | |||
end | |||
if params.value.device and params.value.device.value.mac and params.value.device.value.mac.value ~= "" then | |||
devices[params.value.device.value.mac.value] = oldexten | |||
end | |||
if oldparams.value.device and oldparams.value.device.value.mac and oldparams.value.device.value.mac.value ~= "" then | |||
devices[oldparams.value.device.value.mac.value] = oldexten | |||
end | |||
for name,value in pairs(devices) do | |||
notify_device(name,value) | |||
end | |||
</pre> | |||
* Create /etc/provisioning/provisioning_db_script | |||
<pre> | |||
#!/usr/bin/lua | |||
local path = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin " | |||
local creation_script = { | |||
-- Parameters | |||
"INSERT INTO provisioning_params VALUES(default, 'freepstn', 'boolean', 'Free PSTN Access', '', 'false', '200', '')", | |||
-- Parameter Groups | |||
"INSERT INTO provisioning_groups VALUES(default, 'routing', 'Free PSTN Access', '31')", | |||
"INSERT INTO provisioning_groups VALUES(default, 'routing', 'No Free PSTN Access', '32')", | |||
-- param_groups_to_params (group_id, param_id, value, editable) | |||
"INSERT INTO param_groups_to_params VALUES((SELECT group_id FROM provisioning_groups WHERE label='Free PSTN Access'), (SELECT param_id FROM provisioning_params WHERE name='freepstn'), 'true', false)", | |||
"INSERT INTO param_groups_to_params VALUES((SELECT group_id FROM provisioning_groups WHERE label='No Free PSTN Access'), (SELECT param_id FROM provisioning_params WHERE name='freepstn'), 'false', false)", | |||
-- Classes | |||
-- provisioning_class_groups (class_group_id, name, label, seq) | |||
"INSERT INTO provisioning_class_groups VALUES(default, 'routing', 'Routing', '3')", | |||
-- provisioning_classes (class_id, class_group_id, label, seq) | |||
"INSERT INTO provisioning_classes VALUES(default, (SELECT class_group_id FROM provisioning_class_groups WHERE name='routing'), 'Free PSTN Access', '1')", | |||
"INSERT INTO provisioning_classes VALUES(default, (SELECT class_group_id FROM provisioning_class_groups WHERE name='routing'), 'No Free PSTN Access', '2')", | |||
-- classes_to_param_groups (class_id, group_id) | |||
"INSERT INTO classes_to_param_groups VALUES((SELECT class_id FROM provisioning_classes WHERE label='Free PSTN Access'), (SELECT group_id FROM provisioning_groups WHERE label='Free PSTN Access'))", | |||
"INSERT INTO classes_to_param_groups VALUES((SELECT class_id FROM provisioning_classes WHERE label='No Free PSTN Access'), (SELECT group_id FROM provisioning_groups WHERE label='No Free PSTN Access'))", | |||
-- provisioning_options | |||
"INSERT INTO provisioning_options VALUES((SELECT param_id FROM provisioning_params WHERE name='polycomringtone'), 'Warble', '15', '15')", | |||
"INSERT INTO provisioning_options VALUES((SELECT param_id FROM provisioning_params WHERE name='polycomringtone'), 'Analog Ring', '16', '16')", | |||
} | |||
local f = io.popen("/usr/share/acf/www/cgi-bin/cli /provisioning/provisioning/getdevicevalues") | |||
print(f:read("*a")) | |||
f:close() | |||
for i,c in ipairs(creation_script) do | |||
print(path..'psql -U postgres -c "'..c..'" provisioning 2>&1') | |||
local f = io.popen(path..'psql -U postgres -c "'..c..'" provisioning 2>&1') | |||
print(f:read("*a")) | |||
f:close() | |||
end | |||
</pre> | |||
* chmod 755 /etc/provisioning/provisioning_db_script | |||
* /etc/provisioning/provisioning_db_script | |||
* echo '* * * * * run-parts /etc/periodic/1min' >> /etc/crontabs/root | |||
* mkdir /etc/periodic/1min/ | |||
* /etc/periodic/1min/notify_device | |||
<pre> | |||
#!/usr/bin/lua | |||
-- Load libraries | |||
require("luasql.postgres") | |||
-- Set variables | |||
local DatabaseName = "provisioning" | |||
local DatabaseUser = "postgres" | |||
local DatabasePassword | |||
local path = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin " | |||
local env | |||
local con | |||
-- ################################################################################ | |||
-- LOCAL FUNCTIONS | |||
local function assert (v, m) | |||
if not v then | |||
m = m or "Assertion failed!" | |||
error(m, 0) | |||
end | |||
return v, m | |||
end | |||
-- Escape special characters in sql statements | |||
local escape = function(sql) | |||
sql = sql or "" | |||
sql = string.gsub(sql, "'", "''") | |||
return string.gsub(sql, "\\", "\\\\") | |||
end | |||
local databaseconnect = function() | |||
if not con then | |||
-- create environment object | |||
env = assert (luasql.postgres()) | |||
-- connect to data source | |||
local err | |||
con, err = assert(env:connect(DatabaseName, DatabaseUser, DatabasePassword)) | |||
return true | |||
end | |||
return false | |||
end | |||
local databasedisconnect = function() | |||
if env then | |||
env:close() | |||
env = nil | |||
end | |||
if con then | |||
con:close() | |||
con = nil | |||
end | |||
end | |||
local getselectresponse = function(sql) | |||
local retval = {} | |||
local cur = assert (con:execute(sql)) | |||
local row = cur:fetch ({}, "a") | |||
while row do | |||
local tmp = {} | |||
for name,val in pairs(row) do | |||
tmp[name] = val | |||
end | |||
retval[#retval + 1] = tmp | |||
row = cur:fetch (row, "a") | |||
end | |||
cur:close() | |||
return retval | |||
end | |||
databaseconnect() | |||
-- First, let's notify for the seasoned requests | |||
local reqs = getselectresponse("SELECT * FROM notify WHERE seasoned='true'") | |||
for i,r in ipairs(reqs) do | |||
os.execute("/etc/provisioning/notify_device "..r.ipaddr.." "..r.extension) | |||
end | |||
-- Then mark any others as seasoned | |||
assert(con:execute("DELETE FROM notify WHERE seasoned='true'")) | |||
assert(con:execute("UPDATE notify SET seasoned='true' WHERE seasoned='false'")) | |||
databasedisconnect() | |||
</pre> | |||
* apk add lighttpd | |||
* rm /etc/lighttpd/lighttpd.conf | |||
* ln -s /etc/provisioning/lighttpd.sample.conf /etc/lighttpd/lighttpd.conf | |||
* /etc/lighttpd/mod_cgi.conf: | |||
<pre> | |||
############################################################################### | |||
# mod_cgi.conf | |||
# include'd by lighttpd.conf. | |||
# $Header: /var/cvsroot/gentoo-x86/www-servers/lighttpd/files/conf/mod_cgi.conf,v 1.1 2005/08/27 12:36:13 ka0ttic Exp $ | |||
############################################################################### | |||
# | |||
# see cgi.txt for more information on using mod_cgi | |||
# | |||
server.modules += ("mod_cgi") | |||
# NOTE: this requires mod_alias | |||
alias.url = ( | |||
"/cgi-bin/" => var.basedir + "/cgi-bin/" | |||
) | |||
# | |||
# Note that you'll also want to enable the | |||
# cgi-bin alias via mod_alias (above). | |||
# | |||
$HTTP["url"] =~ "^/cgi-bin/" { | |||
# disable directory listings | |||
dir-listing.activate = "disable" | |||
# only allow cgi's in this directory | |||
cgi.assign = ( | |||
".pl" => "/usr/bin/perl", | |||
".cgi" => "", | |||
"" => "" | |||
) | |||
} | |||
# vim: set ft=conf foldmethod=marker et : | |||
</pre> | |||
* rc-service lighttpd start | |||
* rc-update add lighttpd | |||
* Customize provisioning: | |||
<pre> | |||
Parameter Default Value | |||
registrar IP address or host name of SIP Router(s) | |||
digitmap Digit map for your phone system (See Polycom Digit Map Reference) | |||
digitmaptimeout Timeout in seconds corresponding to digitmap | |||
sntpserver 10.2.0.1 (DMVPN spoke node) | |||
timezone Timezone information for this location - see below | |||
musiconhold SIP uri of the music-on-hold service - moh@media.office.example.net | |||
adminpassword Administration password for advanced settings on phone (on-screen and web interface) | |||
</pre> | |||
* apk fetch --stdout acf-provisioning-polycom | tar -C / -zx | |||
* Create a test Polycom device and boot it in the voice network to verify that it works | |||
== Install Kamailio == | |||
* apk add kamailio kamailio-presence kamailio-pcre kamailio-postgres | |||
* rc-service kamailio start | |||
* rc-update add kamailio | |||
* FUTURE: POST CONFIG THAT USES SUBSCRIBER TABLE FOR AUTH | |||
= Install the SIP Media container = | |||
== Create and Configure the container == | |||
{{Cmd|lxc-create -n sipmedia -f /etc/lxc/default.conf -t alpine}} | |||
Create the startup Script | |||
{{Cmd|ln -s lxc /etc/init.d/lxc.sipmedia}} | |||
Edit the container's config file found at /var/lib/lxc/sipmedia/config, to reflect the network for the SIP Media container | |||
{{cat|/var/lib/lxc/sipmedia/config| | |||
... | |||
lxc.network.link {{=}} bond0.1101 | |||
... | |||
}} | |||
Start the container | |||
{{Cmd|rc-service lxc.sipmedia}} | |||
Configure the container to automatically start | |||
{{Cmd|rc-update add lxc.sipmedia}} | |||
== Enter the SIP Media container == | |||
{{Cmd|lxc-console -n sipmedia}} | |||
Login as root | |||
{{Note|If the need arises to exit the container press {{Key| Ctrl}}+{{Key| a}} + {{Key| q}}}} | |||
Remove obsolete /etc/network/interfaces | |||
{{Cmd|rm /etc/network/interfaces}} | |||
Create and configure the new /etc/network/interfaces as shown below: | |||
{{cat|/etc/network/interfaces| | |||
auto lo | |||
iface lo inet loopback | |||
auto eth0 | |||
iface eth0 inet static | |||
address 10.2.0.5 | |||
netmask 255.255.255.0 | |||
gateway 10.2.0.1 | |||
}} | |||
Startup networking | |||
{{Cmd| rc-service networking start}} | |||
Configure and enable proxy settings | |||
{{Cmd|setup-proxy <nowiki>http://10.1.0.2:8080</nowiki> | |||
. /etc/profile.d/proxy.sh}} | |||
Configure remote administration | |||
{{Cmd|apk update | |||
setup-sshd -c openssh | |||
sed -i "s/.PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config | |||
sed -i "s/.UseDNS yes/UseDNS no/" /etc/ssh/sshd_config}} | |||
Start ssh | |||
{{Cmd|rc-service sshd start}} | |||
Configure a passwd for the container | |||
{{Cmd|passwd}} | |||
Setup acf for web administration | |||
{{Cmd|setup-acf}} | |||
== Setup Firewall == | |||
{{Cmd|apk add acf-awall}} | |||
With your favorite editor, create the policies for the firewall | |||
{{cat|/etc/awall/optional/base.json| | |||
{ | |||
"description": "Management", | |||
"policy": [ | |||
{ "in": "_fw", "action": "accept" } | |||
], | |||
"filter": [ | |||
{ | |||
"out": "_fw", | |||
"service": [ "ssh", "https", "ping" ], | |||
"action": "accept" | |||
} | |||
] | |||
} | |||
}} | |||
{{cat|/etc/awall/optional/sip-track.json| | |||
{ | |||
"description": "Phone system with SIP connection tracking", | |||
"filter": [ | |||
{ | |||
"out": "_fw", | |||
"service": [ "sip", "sip-tls" ], | |||
"action": "accept" | |||
} | |||
] | |||
} | |||
}} | |||
Enable and activate firewall policies, and configure iptables to start at boot | |||
{{Cmd|awall enable base | |||
awall enable sip-track | |||
awall activate -f | |||
rc-update add iptables | |||
}} | |||
== Install and Configure Freeswitch == | |||
Install package | |||
{{Cmd|Install Freeswitch Package}} | |||
Configure /etc/freeswitch/freeswitch.xml | |||
{{cat|/etc/freeswitch/freeswitch.xml| | |||
<pre> | |||
... | |||
<extension name="hold_music"> | |||
<condition field="destination_number" expression="^moh$"> | |||
<action application="answer"/> | |||
<action application="playback" data="$${hold_music}"/> | |||
</condition> | |||
</extension> | |||
... | |||
<X-PRE-PROCESS cmd="set" data="hold_music=local_stream://default"/> | |||
... | |||
FUTURE: ADD VOICEMAIL | |||
</pre> | |||
}} | |||
Start Freeswitch and configure to start at boot | |||
{{Cmd|rc-service freeswitch start | |||
rc-update add freeswitch}} | |||
= Install the WiFi Web Proxy Container = | |||
== Create and Configure the container == | |||
{{Cmd|lxc-create -n wifi -f /etc/lxc/default.conf -t alpine}} | |||
Create the startup Script | |||
{{Cmd|ln -s lxc /etc/init.d/lxc.wifi}} | |||
Edit the container's config file found at /var/lib/lxc/wifi/config, to reflect the network for the wifi container | |||
{{cat|/var/lib/lxc/wifi/config| | |||
... | |||
lxc.network.link {{=}} bond0.701 | |||
... | |||
}} | |||
Start the container | |||
{{Cmd|/etc/iniit.d/lxc.wifi}} | |||
Configure the container to automatically start | |||
{{Cmd|rc-update add lxc.wifi}} | |||
== Enter the wifi container == | |||
{{Cmd|lxc-console -n wifi}} | |||
Login as root | |||
{{Note|If the need arises to exit the container press {{Key| Ctrl}}+{{Key| a}} + {{Key| q}}}} | |||
Remove obsolete /etc/network/interfaces | |||
{{Cmd|rm /etc/network/interfaces}} | |||
Create and configure the new /etc/network/interfaces as shown below: | |||
{{cat|/etc/network/interfaces| | |||
auto lo | |||
iface lo inet loopback | |||
auto eth0 | |||
iface eth0 inet static | |||
address 172.17.48.1 | |||
netmask 255.255.255.0 | |||
auto eth1 | |||
iface eth1 inet static | |||
address 10.1.0.254 | |||
netmask 255.255.255.252 | |||
gateway 10.1.0.253 | |||
auto eth2 | |||
iface eth2 inet static | |||
address 10.1.0.131 | |||
netmask 255.255.255.192 | |||
}} | |||
Startup networking | |||
{{Cmd| rc-service networking start}} | |||
Configure remote administration | |||
{{Cmd|apk update | |||
setup-sshd -c openssh | |||
sed -i "s/.PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config | |||
sed -i "s/.UseDNS yes/UseDNS no/" /etc/ssh/sshd_config}} | |||
Start ssh | |||
{{Cmd|rc-service sshd start}} | |||
Configure a passwd for the container | |||
{{Cmd|passwd}} | |||
Setup acf for web administration | |||
{{Cmd|setup-acf}} | |||
== Setup Firewall == | |||
{{Cmd|apk add acf-awall}} | |||
{{Todo|Need to lock down firewall rules}} | |||
==Install and Configure the Recursive DNS Service == | |||
Install unbound package | |||
{{Cmd|apk add unbound}} | |||
With your favorite editor configure /etc/unbound/unbound.conf | |||
{{cat|/etc/unbound/unobund.conf| | |||
server: | |||
verbosity: 1 | |||
interface: 172.17.48.1 | |||
do-ip4: yes | |||
do-ip6: no | |||
do-udp: yes | |||
do-tcp: yes | |||
do-daemonize: yes | |||
access-control: 172.17.0.0/16 allow | |||
access-control: 127.0.0.0/8 allow | |||
do-not-query-localhost: no | |||
root-hints: "/etc/unbound/root.hints" | |||
python: | |||
remote-control: | |||
control-enable: no | |||
}} | |||
== Install and Configure the Proxy service == | |||
Install the necessary packages | |||
{{Cmd|apk add squid squark lighttpd}} | |||
With your preferred editor configure /etc/squid/squid.conf | |||
{{cat|/etc/squid/squid.conf| | |||
<pre> | |||
#Squid config | |||
# This port listens for client requests | |||
http_port 172.17.48.1:8080 transparent | |||
http_port 127.0.0.1:8081 | |||
visible_hostname wifi.local | |||
cache_mem 8 MB | |||
# If you don't have an HD installed comment the "cache_dir" line below | |||
cache_dir aufs /var/cache/squid 900 16 256 | |||
# Even though we only use one proxy, this line is recommended | |||
# More info: http://www.squid-cache.org/Versions/v2/2.7/cfgman/hierarchy_stoplist.html | |||
hierarchy_stoplist cgi-bin ? | |||
# Keep 7 days of access logs | |||
logfile_rotate 7 | |||
logformat squark %ts.%03tu %6tr %>a %Ss/%03>Hs %<st %rm %ru %un %Sh/%<A %mt %rG | |||
access_log /var/log/squid/access.log squark | |||
cache_store_log none | |||
pid_filename /var/run/squid.pid | |||
# Make sure client IP is passed to Squark | |||
log_uses_indirect_client on | |||
acl_uses_indirect_client on | |||
# Debugging Squid, see http://wiki.squid-cache.org/KnowledgeBase/DebugSections | |||
# for more info | |||
# Keep 7 days of cache log | |||
debug_options rotate=7 | |||
# Web auditors want to see the full uri, even with the query terms | |||
strip_query_terms off | |||
refresh_pattern ^ftp: 1440 20% 10080 | |||
refresh_pattern ^gopher: 1440 0% 1440 | |||
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 | |||
refresh_pattern . 0 20% 4320 | |||
coredump_dir /var/cache/squid | |||
dns_nameservers 172.17.48.1 | |||
# | |||
# Authentication | |||
# | |||
# Squark external acl | |||
#external_acl_type squark_snmp_auth_D children-max=1 ttl=4 grace=1 negative_ttl=0 concurrency=128 %SRC /usr/bin/squark-auth-snmp -c public -R <SWITCH_IP> -i <D_VLAN_IF> -v <D_VLAN_ID> -f "%N-%i=%I" -T /etc/squark/topology.conf | |||
# | |||
# Access Control Lists (ACL's) | |||
# | |||
# Standard ACL settings | |||
acl QUERY urlpath_regex cgi-bin \? asp aspx jsp | |||
acl to_localhost dst 172.17.48.1 | |||
acl SSL_ports port 443 563 8004 9000 | |||
acl Safe_ports port 21 70 80 81 210 280 443 563 499 591 777 1024 1022 1025-65535 | |||
acl purge method PURGE | |||
acl CONNECT method CONNECT | |||
#acl SquarkAuth external squark_auth | |||
#acl SquarkSnmpAuthD external squark_snmp_auth_D | |||
# Squark filter | |||
url_rewrite_program /usr/bin/squark-filter | |||
url_rewrite_children 1 concurrency=128 | |||
# Require authentication | |||
acl userlist src all | |||
# Definition of zones | |||
acl Zone_D src 172.17.48.0/24 | |||
# | |||
# Access restrictions | |||
# | |||
cache deny QUERY | |||
# Only allow cachemgr access from localhost | |||
http_access allow manager localhost | |||
http_access deny manager | |||
# Only allow purge requests from localhost | |||
http_access allow purge localhost | |||
http_access deny purge | |||
# Deny requests to unknown ports | |||
http_access deny !Safe_ports | |||
# Deny CONNECT to other than SSL ports | |||
http_access deny CONNECT !SSL_ports | |||
# Allow hosts in Zone_D to access the entire Internet | |||
http_access allow Zone_D | |||
# Denying all access not explictly allowed | |||
http_access deny all | |||
##Squark URL rewriter | |||
#Prevent squark from filtering itself | |||
url_rewrite_access deny manager | |||
url_rewrite_access deny to_localhost | |||
#Finally, permit access | |||
url_rewrite_access allow Zone_D | |||
http_reply_access allow all | |||
icp_access allow all | |||
</pre> | |||
}} | |||
Configure lighttpd | |||
{{cat|/etc/lighttpd/lighttpd.conf| | |||
<pre> | |||
var.basedir = "/var/www/localhost" | |||
var.logdir = "/var/log/lighttpd" | |||
var.statedir = "/var/lib/lighttpd" | |||
server.modules = ( | |||
"mod_access", | |||
"mod_accesslog", | |||
"mod_extforward" | |||
) | |||
include "mime-types.conf" | |||
include "mod_cgi.conf" | |||
server.username = "lighttpd" | |||
server.groupname = "lighttpd" | |||
server.document-root = var.basedir + "/squark" | |||
server.pid-file = "/var/run/lighttpd.pid" | |||
server.errorlog = var.logdir + "/error.log" | |||
server.indexfiles = ("index.php", "index.html", | |||
"index.htm", "default.htm") | |||
server.follow-symlink = "enable" | |||
server.port = 81 | |||
server.bind = "172.17.48.1" | |||
static-file.exclude-extensions = (".php", ".pl", ".cgi", ".fcgi") | |||
accesslog.filename = var.logdir + "/access.log" | |||
url.access-deny = ("~", ".inc") | |||
extforward.forwarder = ("172.17.48.1" => "trust") | |||
</pre> | |||
}} | |||
{{cat|/etc/lighttpd/mod_cgi.conf| | |||
<pre> | |||
############################################################################### | |||
# mod_cgi.conf | |||
# include'd by lighttpd.conf. | |||
# $Header: /var/cvsroot/gentoo-x86/www-servers/lighttpd/files/conf/mod_cgi.conf,v 1.1 2005/08/27 12:36:13 ka0ttic Exp $ | |||
############################################################################### | |||
# | |||
# see cgi.txt for more information on using mod_cgi | |||
# | |||
server.modules += ("mod_cgi") | |||
# NOTE: this requires mod_alias | |||
alias.url = ( | |||
"/cgi-bin/" => var.basedir + "/cgi-bin/" | |||
) | |||
# | |||
# Note that you'll also want to enable the | |||
# cgi-bin alias via mod_alias (above). | |||
# | |||
$HTTP["url"] =~ "^/cgi-bin/" { | |||
# disable directory listings | |||
dir-listing.activate = "disable" | |||
# only allow cgi's in this directory | |||
cgi.assign = ( | |||
".pl" => "/usr/bin/perl", | |||
".cgi" => "/usr/bin/haserl" | |||
) | |||
} | |||
# vim: set ft=conf foldmethod=marker et : | |||
</pre> | |||
}} | |||
Link Squark web pages to the Web server home directory | |||
{{Cmd|ln -s /usr/share/squark/www/ /var/www/localhost/squark}} | |||
Make 'squid' and 'lighttpd' users member of the group squark | |||
{{Cmd|addgroup squid squark | |||
addgroup lighttpd squark}} | |||
Start lighttpd and configure the Web service to start at boot | |||
{{Cmd|rc-service lighttpd start | |||
rc-update add lighttpd}} | |||
Start Squid and configure it to start at boot | |||
{{Cmd|rc-service squid start | |||
rc-update add squid}} | |||
= See also = | |||
* [[Freeswitch Voicemail On Alpine Linux]] | |||
[[Category:ACF]] |
Latest revision as of 10:48, 17 November 2023
Abstract: This document will outline how to provide various network services for a small remote office, using Linux containerization (LXC). It is designed to be a complement to the DMVPN spoke node.
The following services will be available in addition to the encrypted communications between offices provided by the DMVPN network:
- Internet browsing proxy server with domain filtering (wired clients on protected internal network)
- Separate proxy for wifi clients
- SIP phone system including web based provisioning and basic voicemail services
The assumption is made that the following VLANs and subnets are used as the DMVPN document did:
Interface | Description | Subnet |
---|---|---|
bond0.3 | Management | 10.1.0.129/26 |
bond0.101 | LAN | 10.1.0.0/25 |
bond0.256 | Internet from ISP1 | Allocated from ISP |
bond0.257 | Internet from ISP2 | Allocated from ISP |
bond0.620 | Transit between wifi proxy and dmvpn spoke node | 10.1.0.252/30 |
bond0.701 | WiFi clients (no access to DMVPN network) | 172.17.48.0/24 |
bond0.1101 | Voice | 10.2.0.0/24 |
Hardware
For an office that will serve under 20 people, the following containers can easily run on low-power hardware such as a Via Nano 1.6Ghz Jetway board with 8GB RAM with dual 500GB SATA hard drives running in RAID 1 (software).
Setup LXC Host Box
Boot Alpine USB
Follow the instructions at Create a Bootable Device.
Alpine Setup
setup-alpine
You will be prompted something like this... | Suggestion on what you could enter... |
---|---|
Select keyboard layout [none]:
|
Type an appropriate layout for you |
Select variant:
|
Type an appropriate layout for you (if prompted) |
Enter system hostname (short form, e.g. 'foo') [localhost]:
|
Enter the hostname, e.g. lxc-host |
Available interfaces are: eth0
|
Enter bond0.3 |
Available bond slaves are: eth0 eth1
|
eth0 eth1 |
IP address for bond0? (or 'dhcp', 'none', '?') [dhcp]:
|
Press Enter confirming 'none' |
IP address for bond0.3? (or 'dhcp', 'none', '?') [dhcp]:
|
<%LXCHOST_MANAGEMENT_IP_ADDRESS%> |
Netmask? [255.255.255.0]:
|
<%DMVPN_MANAGEMENT_NETMASK%> |
Gateway? (or 'none') [none]:
|
<%DMVPN_MANAGEMENT_NET_IP%> |
Do you want to do any manual network configuration? [no]
|
no |
DNS domain name? (e.g. 'bar.com') []:
|
Enter the domain name of your intranet, e.g., office.example.net |
DNS nameservers(s)? []:
|
8.8.8.8 8.8.4.4 (we will change them later) |
Changing password for root
|
Enter a secure password for the console |
Retype password:
|
Retype the above password |
Which timezone are you in? ('?' for list) [UTC]:
|
Press Enter confirming 'UTC' |
HTTP/FTP proxy URL? (e.g. 'http://proxy:8080', or 'none') [none]
|
http://<%DMVPN_LAN_IP%>:8080 |
Enter mirror number (1-9) or URL to add (or r/f/e/done) [f]:
|
Select a mirror close to you and press Enter |
Which SSH server? ('openssh', 'dropbear' or 'none') [openssh]:
|
Press Enter confirming 'openssh' |
Which NTP client to run? ('openntpd', 'chrony' or 'none') [chrony]:
|
Press Enter confirming 'chrony' |
Which disk(s) would you like to use? (or '?' for help or 'none') [none]:
|
sda sdb |
How would you like to use them? ('sys', 'data' or '?' for help):
|
data |
Enter where to store configs ('floppy', 'usb' or 'none') [usb]:
|
Press Enter confirming 'usb' |
Enter apk cache directory (or '?' or 'none') [/media/usb/cache]:
|
Press Enter confirming '/media/usb/cache' |
Upgrade packages
apk update apk upgrade
Save Changes
lbu commit
Finish Setup with a reboot
reboot
Setup Networking
With your favorite editor configure /etc/network/interfaces
Contents of /etc/network/interfaces
Apply changes by restarting networking
rc-service networking restart
Enable IP Forwarding
echo "1" > /proc/sys/net/ipv4/ip_forward
Setup Firewall
apk add acf-awall
With your favorite editor, create the base policy for the firewall
Contents of /etc/awall/optional/base.json
Activate the firewall, and allow iptables to startup automatically at boot
modprobe ip_tables awall enable base awall activate -f rc-update add iptables
Install LXC
Install the LXC and Bridge packages
apk add lxc bridge
With your favorite editor configure /etc/lxc/default.conf
Contents of /etc/lxc/default.conf
Finish Installation
lbu ci reboot
Install the Web Proxy Container
Create and Configure the container
lxc-create -n webproxy -f /etc/lxc/default.conf -t alpine
Create the startup Script
ln -s lxc /etc/init.d/lxc.webproxy
Edit the container's config file found at /var/lib/lxc/webproxy/config, to reflect the network for the web proxy container
Contents of /var/lib/lxc/webproxy/config
Start the container
rc-service lxc.webproxy start
Configure the container to automatically start
rc-update add lxc.webproxy
Enter the webproxy container
lxc-console -n webproxy
Login as root
Remove obsolete /etc/network/interfaces
rm /etc/network/interfaces
Create and configure the new /etc/network/interfaces as shown below:
Contents of /etc/network/interfaces
Startup networking
rc-service networking start
Add rule to DMVPN awall policy to allow this proxy out to the internet
Contents of /etc/awall/optional/internet-host.json
Configure remote administration
apk update setup-sshd -c openssh sed -i "s/.PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config sed -i "s/.UseDNS yes/UseDNS no/" /etc/ssh/sshd_config
Start ssh
rc-service sshd start
Configure a passwd for the container
passwd
Setup acf for web administration
setup-acf
Setup Firewall
apk add acf-awall
With your favorite editor, create the policies for the firewall
Contents of /etc/awall/optional/base.json
Contents of /etc/awall/optional/webproxy.json
Activate the firewall, and allow iptables to startup automatically at boot
awall enable base awall enable webproxy awall activate -f rc-update add iptables
Install and Configure the Squid Web Proxy Service
Install the required packages
apk add acf-squid squark acf-lighttpd
Configure /etc/squid/squid.conf, replace <%WEBPROXY_IP_ADDRESS%>, <%HOSTNAME%>, and <%DOMAIN%>
Contents of /etc/squid/squid.conf
#Squid config for webproxy # This port listens for client requests http_port 8080 visible_hostname <%HOSTNAME%>.<%DOMAIN%> cache_mem 8 MB # If you don't have an HD installed comment the "cache_dir" line below cache_dir aufs /var/cache/squid 900 16 256 # Even though we only use one proxy, this line is recommended # More info: http://www.squid-cache.org/Versions/v2/2.7/cfgman/hierarchy_stoplist.html hierarchy_stoplist cgi-bin ? # Keep 7 days of access logs logfile_rotate 7 logformat squark %ts.%03tu %6tr %>a %Ss/%03>Hs %<st %rm %ru %un %Sh/%<A %mt %rG access_log /var/log/squid/access.log squark cache_store_log none pid_filename /var/run/squid.pid # Make sure client IP is passed to Squark log_uses_indirect_client on acl_uses_indirect_client on # Fix for problems with branch file transfer application # ignore_expect_100 on (deprecated) # Debugging Squid, see http://wiki.squid-cache.org/KnowledgeBase/DebugSections # for more info # Keep 7 days of cache log debug_options rotate=7 # Web auditors want to see the full uri, even with the query terms strip_query_terms off refresh_pattern ^ftp: 1440 20% 10080 refresh_pattern ^gopher: 1440 0% 1440 refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 refresh_pattern . 0 20% 4320 coredump_dir /var/cache/squid # # Authentication # # # Access Control Lists (ACL's) # # Standard ACL settings acl QUERY urlpath_regex cgi-bin \? asp aspx jsp acl to_localhost dst <%WEBPROXY_IP_ADDRESS%> acl SSL_ports port 443 563 8004 9000 acl Safe_ports port 21 70 80 81 210 280 443 563 499 591 777 1024 1022 1025-65535 acl purge method PURGE acl CONNECT method CONNECT # Squark filter url_rewrite_program /usr/bin/squark-filter url_rewrite_children 1 concurrency=128 # Require authentication acl userlist src all # Definition of zones acl Zone_B src <%LAN_SUBNET%>/<%LAN_SLASH_NOTATION%> #acl Zone_D src <%WiFi_SUBNET%>/<%WiFi_SLASH_NOTATION%> # Settings migrated from smn acl Zone_B_AllowedUserDomains dstdomain "/etc/squid/alloweduserdomains" acl Zone_B_AllowedServicesHosts src "/etc/squid/allowedserviceshosts" acl Zone_B_AllowedServicesDomains dstdomain "/etc/squid/allowedservicesdomains" # Settings migrated from services acl AnonBrowsers browser "/etc/squid/anonbrowserlist" acl AnonIPAddrs src "/etc/squid/anoniplist" acl AnonDomain url_regex "/etc/squid/anondomainlist" # # Access restrictions # cache deny QUERY # Only allow cachemgr access from localhost http_access allow manager localhost http_access deny manager # Only allow purge requests from localhost http_access allow purge localhost http_access deny purge # Deny requests to unknown ports http_access deny !Safe_ports # Deny CONNECT to other than SSL ports http_access deny CONNECT !SSL_ports # Allow hosts in Zone_B and Zone_C to access hosts listed in # /etc/squid/alloweduserdomains http_access allow Zone_B Zone_B_AllowedUserDomains # Allow hosts listed in /etc/squid/allowedserviceshosts to # access domains listed in /etc/squid/allowedservicesdomains http_access allow Zone_B_AllowedServicesHosts Zone_B_AllowedServicesDomains # Denying all access not explictly allowed http_access deny all ##Squark URL rewriter #Prevent squark from filtering itself url_rewrite_access deny manager url_rewrite_access deny to_localhost #We do not want authentication for these sites: url_rewrite_access deny Zone_B Zone_B_AllowedUserDomains url_rewrite_access deny Zone_B Zone_B_AllowedServicesDomains http_reply_access allow all icp_access allow all
Configure /etc/lighttpd/lighttpd.conf, replace <%WEBPROXY_IP_ADDRESS%>
Contents of /etc/lighttpd/lighttpd.conf
############################################################################## # Default lighttpd.conf for Gentoo. # $Header: /var/cvsroot/gentoo-x86/www-servers/lighttpd/files/conf/lighttpd.conf,v 1.3 2005/09/01 14:22:35 ka0ttic Exp $ ############################################################################### var.basedir = "/var/www/localhost" var.logdir = "/var/log/lighttpd" var.statedir = "/var/lib/lighttpd" server.modules = ( "mod_access", "mod_accesslog", "mod_extforward" ) include "mime-types.conf" include "mod_cgi.conf" server.username = "lighttpd" server.groupname = "lighttpd" server.document-root = var.basedir + "/squark" server.pid-file = "/var/run/lighttpd.pid" server.errorlog = var.logdir + "/error.log" server.indexfiles = ("index.php", "index.html", "index.htm", "default.htm") server.follow-symlink = "enable" static-file.exclude-extensions = (".php", ".pl", ".cgi", ".fcgi") accesslog.filename = var.logdir + "/access.log" url.access-deny = ("~", ".inc") extforward.forwarder = ("<%WEBPROXY_IP_ADDRESS%>" => "trust")
Configure mod_cgi.conf
Contents of /etc/lighttpd/mod_cgi.conf
############################################################################### # mod_cgi.conf # include'd by lighttpd.conf. # $Header: /var/cvsroot/gentoo-x86/www-servers/lighttpd/files/conf/mod_cgi.conf,v 1.1 2005/08/27 12:36:13 ka0ttic Exp $ ############################################################################### # # see cgi.txt for more information on using mod_cgi # server.modules += ("mod_cgi") # NOTE: this requires mod_alias alias.url = ( "/cgi-bin/" => var.basedir + "/cgi-bin/" ) # # Note that you'll also want to enable the # cgi-bin alias via mod_alias (above). # $HTTP["url"] =~ "^/cgi-bin/" { # disable directory listings dir-listing.activate = "disable" # only allow cgi's in this directory cgi.assign = ( ".pl" => "/usr/bin/perl", ".cgi" => "/usr/bin/haserl" ) }
Link the Squark web pages to the Web server home directory
ln -s /usr/share/squark/www/ /var/www/localhost/squark
Create a Squark group
addgroup squark
Make 'squid' and 'lighttpd' users member of the group squark
addgroup squid squark addgroup lighttpd squark
Start lighttpd, and configure the service to start on when container is booted
rc-service lighttpd start rc-update add lighttpd
Start Squid, and configure to start at boot
rc-service squid start rc-update add squid
Install the DHCP and DNS server Container
Create and Configure the container
lxc-create -n dhcpdns -f /etc/lxc/default.conf -t alpine
Create the startup Script
ln -s lxc /etc/init.d/lxc.dhcpdns
Edit the container's config file found at /var/lib/lxc/dhcpdns/config, to reflect the network for the web proxy container
Contents of /var/lib/lxc/dhcpdns/config
#Management Network Config lxc.network.type = macvlan lxc.network.macvlan.mode = bridge lxc.network.link = bond0.3 lxc.network.name = eth0 #WiFi Network Config lxc.network.type = macvlan lxc.network.macvlan.mode = bridge lxc.network.link = bond0.701 lxc.network.name = eth1 #Voice Network Config lxc.network.type = macvlan lxc.network.macvlan.mode = bridge lxc.network.link = bond0.1101 lxc.network.name = eth2
Start the container
rc-service lxc.dhcpdns start
Configure the container to automatically start
rc-update add lxc.dhcpdns
Enter the dhcpdns container
lxc-console -n dhcpdns
Login as root
Remove obsolete /etc/network/interfaces
rm /etc/network/interfaces
Create and configure the new /etc/network/interfaces as shown below:
Contents of /etc/network/interfaces
Startup networking
rc-service networking start
Configure and enable proxy settings
setup-proxy http://10.1.0.2:8080 . /etc/profile.d/proxy.sh
Configure remote administration
apk update setup-sshd -c openssh sed -i "s/.PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config sed -i "s/.UseDNS yes/UseDNS no/" /etc/ssh/sshd_config
Start ssh
rc-service sshd start
Configure a passwd for the container
passwd
Setup acf for web administration
setup-acf
Setup Firewall
apk add acf-awall
With your favorite editor, create the policies for the firewall
Contents of /etc/awall/optional/base.json
Contents of /etc/awall/optional/dhcp.json
Contents of /etc/awall/optional/dns.json
Activate the firewall, and allow iptables to startup automatically at boot
awall enable base awall enable dhcp awall enable dns awall activate -f rc-update add iptables
Install and Configure DHCP and DNS services
install the dhcpd package
apk add acf-dhcp
Create a new dhcpd.conf file
Contents of /etc/dhcp/dhcpd.conf
## Common settings default-lease-time 302400; max-lease-time 604800; ddns-update-style none; log-facility local7; authoritative; ## Common options option time-servers 10.2.0.1; option boot-server code 66 = string; ## Voice subnet 10.2.0.0 netmask 255.255.255.0 { range "10.2.0.20 10.2.0.250"; option domain-name-servers 10.2.0.2; option routers 10.2.0.1; option boot-server "http://10.2.0.4"; option domain-name "office.example.net"; } ## WiFi subnet 172.17.48.0 netmask 255.255.255.0 { range "172.17.48.10 172.17.48.250"; option routers 172.17.48.1; option domain-name-servers 172.17.48.1; }
Start DHCP service and add to runlevel default
rc-service dhcpd start rc-update add dhcpd
Install nsd and unbound packages
apk add unbound
Remove unbound.conf
rm /etc/unbound/unbound.conf
Create with your favorite editor a new configuration for unbound
Contents of /etc/unbound/unbound.conf
Start Unbound and allow the container to use it {{Cmd|rc-service unbound start rc-update add unbound echo nameserver 10.2.0.2 > /etc/resolv.conf
Install nsd
apk add nsd
Configure nsd configuration
Contents of /etc/nsd/nsd.conf
Configure Zone file for nsd
Contents of /etc/nsd/nsd.conf
Check nsd configuration and start service
nsd-checkconf /etc/nsd/nsd.conf rc-service nsd start rc-update add nsd
Install the SIP Container
Create and Configure the container
lxc-create -n sip -f /etc/lxc/default.conf -t alpine
Create the startup Script
ln -s lxc /etc/init.d/lxc.sip
Edit the container's config file found at /var/lib/lxc/sip/config, to reflect the network for the sip container
Contents of /var/lib/lxc/sip/config
Start the container
/etc/iniit.d/lxc.sip
Configure the container to automatically start
rc-update add lxc.sip
Enter the sip container
lxc-console -n sip
Login as root
Remove obsolete /etc/network/interfaces
rm /etc/network/interfaces
Create and configure the new /etc/network/interfaces as shown below:
Contents of /etc/network/interfaces
Startup networking
rc-service networking start
Configure and enable proxy settings
setup-proxy http://10.1.0.2:8080 . /etc/profile.d/proxy.sh
Configure remote administration
apk update setup-sshd -c openssh sed -i "s/.PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config sed -i "s/.UseDNS yes/UseDNS no/" /etc/ssh/sshd_config
Start ssh
rc-service sshd start
Configure a passwd for the container
passwd
Setup acf for web administration
setup-acf
Setup Firewall
apk add acf-awall
With your favorite editor, create the policies for the firewall
Contents of /etc/awall/optional/base.json
Contents of /etc/awall/optional/sip.json
Contents of /etc/awall/optional/syslog.json
Activate the firewall, and allow iptables to startup automatically at boot
awall enable base awall enable sip awall enable syslog awall activate -f rc-update add iptables
Install and Configure Postgresql
Install postgresql package
apk update apk add acf-postgresql
Prepare the database
rc-service postgresql setup
Configure /var/lib/postgresql/9.3/data/postgresql.conf to set the 'log_destination' variable to show:
Contents of /var/lib/postgresql/9.3/data/postresql.conf
Start up the database and configure postgresql to start at boot up
rc-service postgresql start rc-update add postgresql
Install acf-provisioning
- vi /etc/kamailio/kamctlrc
SIP_DOMAIN=sip.office.example.net DBENGINE=PGSQL DBHOST=127.0.0.1 DBNAME=openser DBRWUSER=openser DBRWPW="openser" DBROUSER=openserro DBROPW=openserro DBROOTUSER="postgres"
- yes | kamdbctl create openser
- apk add acf-provisioning lua-socket lua-expat
- Create /etc/provisioning/update_device_params.lua:
-- This is the script run after editing device params - basically only worried about extension and password local functions, params, oldparams = ... require("posix") require("luasql.postgres") local root = "/var/www/provisioning/htdocs/" local b62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" APP.logevent("got to update_device_params script") local function generatepw() -- generate a random 12-character alphanumeric string local file = io.open("/dev/urandom") local str = "" if file == nil then return nil end local size = 12 while (size > 0 ) do local offset = (string.byte(file:read(1)) % 62) + 1 str = str .. string.sub (b62, offset, offset) size = size - 1 end return str end local function findip(mac) if not mac or mac == "" then return nil end local ipaddr = functions.getselectresponse("SELECT ip FROM provisioning_requests WHERE mac~*'"..mac.."'") if ipaddr and ipaddr[1] then return ipaddr[1].ip end end local function addfuturenotify(ipaddr, extension) local res, err = pcall(function() functions.runsqlcommand("DELETE FROM notify WHERE ipaddr='"..ipaddr.."' AND extension='"..extension.."'") end) if not res and err then if string.match(err, "relation \"(%S+)\" does not exist") then functions.runsqlcommand("CREATE TABLE notify (ipaddr text, extension text, seasoned boolean DEFAULT false)") else assert(res, err) end end -- if table missing, create it and delete again functions.runsqlcommand("INSERT INTO notify VALUES('"..ipaddr.."', '"..extension.."')") end local notify_device = function(mac, extension) local ipaddr = findip(mac) if ipaddr then APP.logevent("Notifying "..ipaddr.." to update for "..(mac or "")) os.execute("/etc/provisioning/notify_device "..ipaddr.." "..extension) addfuturenotify(ipaddr, extension) else APP.logevent("Warning - could not find IP address for "..(mac or "")) end end local kam = APP:new("kamailio/kamailio") -- A table of devices to notify to update local devices = {} -- First, we check the registration numbers / passwords to 1) set a random password (if necessary) 2) make sure password matches any other registrations to same extension 3) push changes (add / delete / or update) to Kam database -- Because this script also handles when device classes change, we have to consider that extensions are added / removed local regs = {} local forwarding = {"forwardnoanswerenable", "forwardnoanswer", "forwardbusyenable", "forwardbusy", "forwardallenable", "forwardall"} local forwardsettings = {} -- Collect forwarding settings local passwords = {} -- Collect old extension/password pairs for name,val in pairs(params.value) do if not regs[name] and string.match(name, "^reg") then regs[name] = true end end oldparams = oldparams or {value={}} for name,val in pairs(oldparams.value) do if string.match(name, "^reg") then if val.value and val.value.extension and val.value.password then passwords[val.value.extension.value] = val.value.password.value end if val.value and val.value.extension then local fwd = {} for i,f in ipairs(forwarding) do if val.value[f] then fwd[f] = val.value[f].value end end forwardsettings[val.value.extension.value] = fwd end if not regs[name] then regs[name] = true end end end local extension_id = params.value.reg1.value.extension.param_id local password_id = params.value.reg1.value.password.param_id functions.runsqlcommand("BEGIN TRANSACTION") for name,val in pairs(regs) do local new = "" local old = "" if params.value[name] then new = params.value[name].value.extension.value end if oldparams.value[name] then old = oldparams.value[name].value.extension.value end if new ~= old then -- Extension changed -- First, let's remove the stuff for the old extension if old ~= "" then local others = functions.getselectresponse("SELECT count(*) FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..old.."'") if others[1].count == "0" then APP.logevent("Removing old registration "..old) -- remove the registration kam.model.delete_user(old) end end -- Now, add the new extension if new ~= "" then local pass if params.value[name].value.password.value ~= "" and (not oldparams.value[name] or (oldparams.value[name].value.password.value ~= params.value[name].value.password.value)) then -- The password parameter was changed and not blank, so use it pass = params.value[name].value.password.value APP.logevent("Added a new registration "..new.." with specified password "..pass) -- There may be other devices with this extension local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") for i,o in ipairs(others) do -- We'll notify whether it changed or not devices[o.value] = new end elseif passwords[new] then pass = passwords[new] APP.logevent("Added a new registration "..new.." with reused password "..pass) else local others = functions.getselectresponse("SELECT * FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."' AND device_id!='"..params.value.device_id.value.."'") -- If this is a new registration, use a new password if #others == 0 then pass = generatepw() APP.logevent("Added a new registration "..new.." with random password "..pass) else -- Use the old password local p = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id='"..password_id.."' AND device_id='"..others[1].device_id.."' AND group_name='"..others[1].group_name.."' LIMIT 1") pass = p[1].value APP.logevent("Added a new registration "..new.." with reused password "..pass) end end passwords[new] = pass params.value[name].value.password.value = pass functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..password_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..password_id.."', '"..pass.."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") local u = kam.model.get_user(new) if u.value.username.errtxt then -- Add this registration to Kam local u = kam.model.get_new_user() u.value.username.value = new u.value.password.value = pass u.value.password_confirm.value = pass kam.model.create_new_user(u) else u.value.password.value = pass u.value.password_confirm.value = pass kam.model.update_user(u) end -- Let's also look at the forwarding settings local change = false local supported = false local fwd = {} for i,f in ipairs(forwarding) do if params.value[name].value[f] then supported = true fwd[f] = params.value[name].value[f].value if ((not oldparams.value[name] or not oldparams.value[name].value[f]) and (params.value[name].value[f].value ~= params.value[name].value[f].default)) or (oldparams.value[name] and oldparams.value[name].value[f] and (params.value[name].value[f].value ~= oldparams.value[name].value[f].value)) then change = true end end if change then local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") for i,o in ipairs(others) do -- We'll notify whether it changed or not devices[o.value] = new end end end if supported then if not change and forwardsettings[new] then fwd = forwardsettings[new] elseif not change then -- This is a new extension, and the forwarding has not been set. We should check to see if there are any other devices with this extension local others = functions.getselectresponse("SELECT * FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."' AND device_id!='"..params.value.device_id.value.."'") if #others > 0 then -- Use the existing settings for i,f in ipairs(forwarding) do local v = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id='"..params.value[name].value[f].param_id.."' AND device_id='"..others[1].device_id.."' AND group_name='"..others[1].group_name.."' LIMIT 1") if #v > 0 then fwd[f] = v[1].value else fwd[f] = params.value[name].value[f].default end end end end if not change then for i,f in ipairs(forwarding) do params.value[name].value[f].value = fwd[f] end end forwardsettings[new] = fwd for i,f in ipairs(forwarding) do functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..params.value[name].value[f].param_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") if fwd[f] ~= params.value[name].value[f].default then functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..params.value[name].value[f].param_id.."', '"..tostring(fwd[f]).."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") end end end end elseif params.value[name] and oldparams.value[name] and params.value[name].value.extension.value ~= "" then if params.value[name].value.password.value ~= oldparams.value[name].value.password.value then -- Password changed - make any other registrations to this extension also change local pass = params.value[name].value.password.value if pass and pass ~= "" then APP.logevent("Password changed for "..new.." from "..oldparams.value[name].value.password.value.." to "..pass) else pass = generatepw() APP.logevent("Password cleared for "..new..", so set new random password "..pass) end passwords[new] = pass functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..password_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..password_id.."', '"..pass.."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") local u = kam.model.get_user(new) u.value.password.value = params.value[name].value.password.value u.value.password_confirm.value = params.value[name].value.password.value kam.model.update_user(u) -- Have to notify those other devices too local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") for i,o in ipairs(others) do devices[o.value] = new end end local change = false local fwd = {} for i,f in ipairs(forwarding) do if params.value[name].value[f] and (not oldparams.value[name].value[f] or params.value[name].value[f].value ~= oldparams.value[name].value[f].value) then change = true break end end if change then -- Forwarding settings changed - make any other registrations to this extension also change for i,f in ipairs(forwarding) do functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..params.value[name].value[f].param_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") if params.value[name].value[f].value ~= params.value[name].value[f].default then functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..params.value[name].value[f].param_id.."', '"..tostring(params.value[name].value[f].value).."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") end end -- Have to notify those other devices too local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')") for i,o in ipairs(others) do devices[o.value] = new end end end end functions.runsqlcommand("COMMIT") -- If reg1 or freepstn changed, then need to change free-pstn in kam -- Since there can be multiple devices with the same reg1, we can't rely on params and oldparams -- Check both reg's --APP.logevent(session.serialize("params", params)) local reg = {} if oldparams.value.reg1 and oldparams.value.reg1.value.extension and oldparams.value.reg1.value.extension.value ~= "" then reg[oldparams.value.reg1.value.extension.value] = false end if params.value.reg1 and params.value.reg1.value.extension and params.value.reg1.value.extension.value ~= "" then local pstn = false if params.value.routing and params.value.routing.value.freepstn then pstn = params.value.routing.value.freepstn.value end reg[params.value.reg1.value.extension.value] = pstn end APP.logevent("Looking at free-pstn") for r,f in pairs(reg) do -- if we're not sure, check provisioning database if not f then -- Most devices will have free-pstn due to class-of-service, but can be overridden, so this can get tricky -- Check all devices where reg1 extension = r, and see what the freepstn value is for each local others = functions.getselectresponse("SELECT CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS value ".. "FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING (class_id) JOIN provisioning_groups g USING(group_id) ".. "JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id)) LEFT JOIN provisioning_values v ON (d2t.device_id=v.device_id AND p.param_id=v.param_id AND g.name=v.group_name ) ".. "WHERE p.name='freepstn' AND d2t.device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND group_name='reg1' AND value='"..r.."')") for i,o in ipairs(others) do -- Now check the freepstn value for each one if o.value == "true" then f = true break end end end -- Now, check the Kamailio group table local alreadythere = false local entries = kam.model.list_table_entries("grp") for i,e in ipairs(entries.value.entries.value) do if e.username == r then alreadythere = true if not f then APP.logevent("Removing free-pstn for "..r) -- Remove free-pstn from the old extension kam.model.delete_table_entry("grp", e.id) end break end end if f and not alreadythere then APP.logevent("Adding free-pstn for "..r) -- Add free-pstn to the new extension local e = kam.model.get_table_entry("grp") e.value.username.value = r e.value.domain.value = "" e.value.grp.value = "free-pstn" e.value.last_modified.value = os.date("%c") kam.model.create_table_entry(e) end end kam:destroy() -- If the mac address changed for Polycom with valid MAC (not blank or all 0's), we need to move the associated config files if oldparams.value.device and oldparams.value.device.value.mac and oldparams.value.device.value.mac.value ~= "" and params.value.device and params.value.device.value.mac and params.value.device.value.mac.value ~= oldparams.value.device.value.mac.value then if string.match(oldparams.value.device.label, "Polycom") and string.match(oldparams.value.device.value.mac.value, "[1-9A-F]") then local deletefiles = true if string.match(params.value.device.value.mac.value, "[1-9A-F]") then --APP.logevent("Moving files for "..oldparams.value.device.value.mac.value) deletefiles = false else --APP.logevent("Deleting files for "..oldparams.value.device.value.mac.value) end local path = root.."Polycom/" if posix.stat(path, "type") == "directory" then for d in posix.files(path) do if string.match(d, string.lower(oldparams.value.device.value.mac.value)) and posix.stat(path..d, "type") == "regular" then local newfile = string.gsub(d, string.lower(oldparams.value.device.value.mac.value), string.lower(params.value.device.value.mac.value)) if deletefiles then --APP.logevent("deleting "..path..d) os.remove(path..d) else --APP.logevent("moving "..path..d.." to "..path..newfile) os.rename(path..d, path..newfile) end end end end end end -- Then, notify the phone to pull it's config -- Try to get a valid extension currently on the device local oldexten = "" for name,val in pairs(oldparams.value) do if string.match(name, "^reg") and val.value.extension and val.value.extension.value ~= "" then oldexten = val.value.extension.value break end end if params.value.device and params.value.device.value.mac and params.value.device.value.mac.value ~= "" then devices[params.value.device.value.mac.value] = oldexten end if oldparams.value.device and oldparams.value.device.value.mac and oldparams.value.device.value.mac.value ~= "" then devices[oldparams.value.device.value.mac.value] = oldexten end for name,value in pairs(devices) do notify_device(name,value) end
- Create /etc/provisioning/provisioning_db_script
#!/usr/bin/lua local path = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin " local creation_script = { -- Parameters "INSERT INTO provisioning_params VALUES(default, 'freepstn', 'boolean', 'Free PSTN Access', '', 'false', '200', '')", -- Parameter Groups "INSERT INTO provisioning_groups VALUES(default, 'routing', 'Free PSTN Access', '31')", "INSERT INTO provisioning_groups VALUES(default, 'routing', 'No Free PSTN Access', '32')", -- param_groups_to_params (group_id, param_id, value, editable) "INSERT INTO param_groups_to_params VALUES((SELECT group_id FROM provisioning_groups WHERE label='Free PSTN Access'), (SELECT param_id FROM provisioning_params WHERE name='freepstn'), 'true', false)", "INSERT INTO param_groups_to_params VALUES((SELECT group_id FROM provisioning_groups WHERE label='No Free PSTN Access'), (SELECT param_id FROM provisioning_params WHERE name='freepstn'), 'false', false)", -- Classes -- provisioning_class_groups (class_group_id, name, label, seq) "INSERT INTO provisioning_class_groups VALUES(default, 'routing', 'Routing', '3')", -- provisioning_classes (class_id, class_group_id, label, seq) "INSERT INTO provisioning_classes VALUES(default, (SELECT class_group_id FROM provisioning_class_groups WHERE name='routing'), 'Free PSTN Access', '1')", "INSERT INTO provisioning_classes VALUES(default, (SELECT class_group_id FROM provisioning_class_groups WHERE name='routing'), 'No Free PSTN Access', '2')", -- classes_to_param_groups (class_id, group_id) "INSERT INTO classes_to_param_groups VALUES((SELECT class_id FROM provisioning_classes WHERE label='Free PSTN Access'), (SELECT group_id FROM provisioning_groups WHERE label='Free PSTN Access'))", "INSERT INTO classes_to_param_groups VALUES((SELECT class_id FROM provisioning_classes WHERE label='No Free PSTN Access'), (SELECT group_id FROM provisioning_groups WHERE label='No Free PSTN Access'))", -- provisioning_options "INSERT INTO provisioning_options VALUES((SELECT param_id FROM provisioning_params WHERE name='polycomringtone'), 'Warble', '15', '15')", "INSERT INTO provisioning_options VALUES((SELECT param_id FROM provisioning_params WHERE name='polycomringtone'), 'Analog Ring', '16', '16')", } local f = io.popen("/usr/share/acf/www/cgi-bin/cli /provisioning/provisioning/getdevicevalues") print(f:read("*a")) f:close() for i,c in ipairs(creation_script) do print(path..'psql -U postgres -c "'..c..'" provisioning 2>&1') local f = io.popen(path..'psql -U postgres -c "'..c..'" provisioning 2>&1') print(f:read("*a")) f:close() end
- chmod 755 /etc/provisioning/provisioning_db_script
- /etc/provisioning/provisioning_db_script
- echo '* * * * * run-parts /etc/periodic/1min' >> /etc/crontabs/root
- mkdir /etc/periodic/1min/
- /etc/periodic/1min/notify_device
#!/usr/bin/lua -- Load libraries require("luasql.postgres") -- Set variables local DatabaseName = "provisioning" local DatabaseUser = "postgres" local DatabasePassword local path = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin " local env local con -- ################################################################################ -- LOCAL FUNCTIONS local function assert (v, m) if not v then m = m or "Assertion failed!" error(m, 0) end return v, m end -- Escape special characters in sql statements local escape = function(sql) sql = sql or "" sql = string.gsub(sql, "'", "''") return string.gsub(sql, "\\", "\\\\") end local databaseconnect = function() if not con then -- create environment object env = assert (luasql.postgres()) -- connect to data source local err con, err = assert(env:connect(DatabaseName, DatabaseUser, DatabasePassword)) return true end return false end local databasedisconnect = function() if env then env:close() env = nil end if con then con:close() con = nil end end local getselectresponse = function(sql) local retval = {} local cur = assert (con:execute(sql)) local row = cur:fetch ({}, "a") while row do local tmp = {} for name,val in pairs(row) do tmp[name] = val end retval[#retval + 1] = tmp row = cur:fetch (row, "a") end cur:close() return retval end databaseconnect() -- First, let's notify for the seasoned requests local reqs = getselectresponse("SELECT * FROM notify WHERE seasoned='true'") for i,r in ipairs(reqs) do os.execute("/etc/provisioning/notify_device "..r.ipaddr.." "..r.extension) end -- Then mark any others as seasoned assert(con:execute("DELETE FROM notify WHERE seasoned='true'")) assert(con:execute("UPDATE notify SET seasoned='true' WHERE seasoned='false'")) databasedisconnect()
- apk add lighttpd
- rm /etc/lighttpd/lighttpd.conf
- ln -s /etc/provisioning/lighttpd.sample.conf /etc/lighttpd/lighttpd.conf
- /etc/lighttpd/mod_cgi.conf:
############################################################################### # mod_cgi.conf # include'd by lighttpd.conf. # $Header: /var/cvsroot/gentoo-x86/www-servers/lighttpd/files/conf/mod_cgi.conf,v 1.1 2005/08/27 12:36:13 ka0ttic Exp $ ############################################################################### # # see cgi.txt for more information on using mod_cgi # server.modules += ("mod_cgi") # NOTE: this requires mod_alias alias.url = ( "/cgi-bin/" => var.basedir + "/cgi-bin/" ) # # Note that you'll also want to enable the # cgi-bin alias via mod_alias (above). # $HTTP["url"] =~ "^/cgi-bin/" { # disable directory listings dir-listing.activate = "disable" # only allow cgi's in this directory cgi.assign = ( ".pl" => "/usr/bin/perl", ".cgi" => "", "" => "" ) } # vim: set ft=conf foldmethod=marker et :
- rc-service lighttpd start
- rc-update add lighttpd
- Customize provisioning:
Parameter Default Value registrar IP address or host name of SIP Router(s) digitmap Digit map for your phone system (See Polycom Digit Map Reference) digitmaptimeout Timeout in seconds corresponding to digitmap sntpserver 10.2.0.1 (DMVPN spoke node) timezone Timezone information for this location - see below musiconhold SIP uri of the music-on-hold service - moh@media.office.example.net adminpassword Administration password for advanced settings on phone (on-screen and web interface)
- apk fetch --stdout acf-provisioning-polycom | tar -C / -zx
- Create a test Polycom device and boot it in the voice network to verify that it works
Install Kamailio
- apk add kamailio kamailio-presence kamailio-pcre kamailio-postgres
- rc-service kamailio start
- rc-update add kamailio
- FUTURE: POST CONFIG THAT USES SUBSCRIBER TABLE FOR AUTH
Install the SIP Media container
Create and Configure the container
lxc-create -n sipmedia -f /etc/lxc/default.conf -t alpine
Create the startup Script
ln -s lxc /etc/init.d/lxc.sipmedia
Edit the container's config file found at /var/lib/lxc/sipmedia/config, to reflect the network for the SIP Media container
Contents of /var/lib/lxc/sipmedia/config
Start the container
rc-service lxc.sipmedia
Configure the container to automatically start
rc-update add lxc.sipmedia
Enter the SIP Media container
lxc-console -n sipmedia
Login as root
Remove obsolete /etc/network/interfaces
rm /etc/network/interfaces
Create and configure the new /etc/network/interfaces as shown below:
Contents of /etc/network/interfaces
Startup networking
rc-service networking start
Configure and enable proxy settings
setup-proxy http://10.1.0.2:8080 . /etc/profile.d/proxy.sh
Configure remote administration
apk update setup-sshd -c openssh sed -i "s/.PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config sed -i "s/.UseDNS yes/UseDNS no/" /etc/ssh/sshd_config
Start ssh
rc-service sshd start
Configure a passwd for the container
passwd
Setup acf for web administration
setup-acf
Setup Firewall
apk add acf-awall
With your favorite editor, create the policies for the firewall
Contents of /etc/awall/optional/base.json
Contents of /etc/awall/optional/sip-track.json
Enable and activate firewall policies, and configure iptables to start at boot
awall enable base awall enable sip-track awall activate -f rc-update add iptables
Install and Configure Freeswitch
Install package
Install Freeswitch Package
Configure /etc/freeswitch/freeswitch.xml
Contents of /etc/freeswitch/freeswitch.xml
... <extension name="hold_music"> <condition field="destination_number" expression="^moh$"> <action application="answer"/> <action application="playback" data="$${hold_music}"/> </condition> </extension> ... <X-PRE-PROCESS cmd="set" data="hold_music=local_stream://default"/> ... FUTURE: ADD VOICEMAIL
Start Freeswitch and configure to start at boot
rc-service freeswitch start rc-update add freeswitch
Install the WiFi Web Proxy Container
Create and Configure the container
lxc-create -n wifi -f /etc/lxc/default.conf -t alpine
Create the startup Script
ln -s lxc /etc/init.d/lxc.wifi
Edit the container's config file found at /var/lib/lxc/wifi/config, to reflect the network for the wifi container
Contents of /var/lib/lxc/wifi/config
Start the container
/etc/iniit.d/lxc.wifi
Configure the container to automatically start
rc-update add lxc.wifi
Enter the wifi container
lxc-console -n wifi
Login as root
Remove obsolete /etc/network/interfaces
rm /etc/network/interfaces
Create and configure the new /etc/network/interfaces as shown below:
Contents of /etc/network/interfaces
Startup networking
rc-service networking start
Configure remote administration
apk update setup-sshd -c openssh sed -i "s/.PasswordAuthentication yes/PasswordAuthentication no/" /etc/ssh/sshd_config sed -i "s/.UseDNS yes/UseDNS no/" /etc/ssh/sshd_config
Start ssh
rc-service sshd start
Configure a passwd for the container
passwd
Setup acf for web administration
setup-acf
Setup Firewall
apk add acf-awall
Install and Configure the Recursive DNS Service
Install unbound package
apk add unbound
With your favorite editor configure /etc/unbound/unbound.conf
Contents of /etc/unbound/unobund.conf
Install and Configure the Proxy service
Install the necessary packages
apk add squid squark lighttpd
With your preferred editor configure /etc/squid/squid.conf
Contents of /etc/squid/squid.conf
#Squid config # This port listens for client requests http_port 172.17.48.1:8080 transparent http_port 127.0.0.1:8081 visible_hostname wifi.local cache_mem 8 MB # If you don't have an HD installed comment the "cache_dir" line below cache_dir aufs /var/cache/squid 900 16 256 # Even though we only use one proxy, this line is recommended # More info: http://www.squid-cache.org/Versions/v2/2.7/cfgman/hierarchy_stoplist.html hierarchy_stoplist cgi-bin ? # Keep 7 days of access logs logfile_rotate 7 logformat squark %ts.%03tu %6tr %>a %Ss/%03>Hs %<st %rm %ru %un %Sh/%<A %mt %rG access_log /var/log/squid/access.log squark cache_store_log none pid_filename /var/run/squid.pid # Make sure client IP is passed to Squark log_uses_indirect_client on acl_uses_indirect_client on # Debugging Squid, see http://wiki.squid-cache.org/KnowledgeBase/DebugSections # for more info # Keep 7 days of cache log debug_options rotate=7 # Web auditors want to see the full uri, even with the query terms strip_query_terms off refresh_pattern ^ftp: 1440 20% 10080 refresh_pattern ^gopher: 1440 0% 1440 refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 refresh_pattern . 0 20% 4320 coredump_dir /var/cache/squid dns_nameservers 172.17.48.1 # # Authentication # # Squark external acl #external_acl_type squark_snmp_auth_D children-max=1 ttl=4 grace=1 negative_ttl=0 concurrency=128 %SRC /usr/bin/squark-auth-snmp -c public -R <SWITCH_IP> -i <D_VLAN_IF> -v <D_VLAN_ID> -f "%N-%i=%I" -T /etc/squark/topology.conf # # Access Control Lists (ACL's) # # Standard ACL settings acl QUERY urlpath_regex cgi-bin \? asp aspx jsp acl to_localhost dst 172.17.48.1 acl SSL_ports port 443 563 8004 9000 acl Safe_ports port 21 70 80 81 210 280 443 563 499 591 777 1024 1022 1025-65535 acl purge method PURGE acl CONNECT method CONNECT #acl SquarkAuth external squark_auth #acl SquarkSnmpAuthD external squark_snmp_auth_D # Squark filter url_rewrite_program /usr/bin/squark-filter url_rewrite_children 1 concurrency=128 # Require authentication acl userlist src all # Definition of zones acl Zone_D src 172.17.48.0/24 # # Access restrictions # cache deny QUERY # Only allow cachemgr access from localhost http_access allow manager localhost http_access deny manager # Only allow purge requests from localhost http_access allow purge localhost http_access deny purge # Deny requests to unknown ports http_access deny !Safe_ports # Deny CONNECT to other than SSL ports http_access deny CONNECT !SSL_ports # Allow hosts in Zone_D to access the entire Internet http_access allow Zone_D # Denying all access not explictly allowed http_access deny all ##Squark URL rewriter #Prevent squark from filtering itself url_rewrite_access deny manager url_rewrite_access deny to_localhost #Finally, permit access url_rewrite_access allow Zone_D http_reply_access allow all icp_access allow all
Configure lighttpd
Contents of /etc/lighttpd/lighttpd.conf
var.basedir = "/var/www/localhost" var.logdir = "/var/log/lighttpd" var.statedir = "/var/lib/lighttpd" server.modules = ( "mod_access", "mod_accesslog", "mod_extforward" ) include "mime-types.conf" include "mod_cgi.conf" server.username = "lighttpd" server.groupname = "lighttpd" server.document-root = var.basedir + "/squark" server.pid-file = "/var/run/lighttpd.pid" server.errorlog = var.logdir + "/error.log" server.indexfiles = ("index.php", "index.html", "index.htm", "default.htm") server.follow-symlink = "enable" server.port = 81 server.bind = "172.17.48.1" static-file.exclude-extensions = (".php", ".pl", ".cgi", ".fcgi") accesslog.filename = var.logdir + "/access.log" url.access-deny = ("~", ".inc") extforward.forwarder = ("172.17.48.1" => "trust")
Contents of /etc/lighttpd/mod_cgi.conf
############################################################################### # mod_cgi.conf # include'd by lighttpd.conf. # $Header: /var/cvsroot/gentoo-x86/www-servers/lighttpd/files/conf/mod_cgi.conf,v 1.1 2005/08/27 12:36:13 ka0ttic Exp $ ############################################################################### # # see cgi.txt for more information on using mod_cgi # server.modules += ("mod_cgi") # NOTE: this requires mod_alias alias.url = ( "/cgi-bin/" => var.basedir + "/cgi-bin/" ) # # Note that you'll also want to enable the # cgi-bin alias via mod_alias (above). # $HTTP["url"] =~ "^/cgi-bin/" { # disable directory listings dir-listing.activate = "disable" # only allow cgi's in this directory cgi.assign = ( ".pl" => "/usr/bin/perl", ".cgi" => "/usr/bin/haserl" ) } # vim: set ft=conf foldmethod=marker et :
Link Squark web pages to the Web server home directory
ln -s /usr/share/squark/www/ /var/www/localhost/squark
Make 'squid' and 'lighttpd' users member of the group squark
addgroup squid squark addgroup lighttpd squark
Start lighttpd and configure the Web service to start at boot
rc-service lighttpd start rc-update add lighttpd
Start Squid and configure it to start at boot
rc-service squid start rc-update add squid