Browse Source

Add support for miniupnpd and cjdns

RISCi_ATOM 6 years ago
parent
commit
18612d7168

+ 148 - 0
net/cjdns/Makefile

@@ -0,0 +1,148 @@
+#
+# Copyright (C) 2014,2015 Hyperboria.net
+#
+# You may redistribute this program and/or modify it under the terms of
+# the GNU General Public License as published by the Free Software Foundation,
+# either version 3 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=cjdns
+PKG_VERSION:=0.17
+PKG_RELEASE:=3
+
+PKG_SOURCE_URL:=https://github.com/hyperboria/cjdns.git
+PKG_SOURCE_PROTO:=git
+PKG_SOURCE_VERSION:=40e87d9419c19063e772e39c7c59a8a8771c5ee8
+PKG_LICENSE:=GPL-3.0
+PKG_SOURCE:=$(PKG_NAME)-$(PKG_SOURCE_VERSION).tar.bz2
+PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_SOURCE_VERSION)
+PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_SOURCE_VERSION)
+
+include $(INCLUDE_DIR)/package.mk
+
+
+define Package/cjdns
+	SECTION:=net
+	CATEGORY:=Network
+	SUBMENU:=Routing and Redirection
+	TITLE:=Encrypted near-zero-conf mesh routing protocol
+	URL:=https://github.com/hyperboria/cjdns
+	MAINTAINER:=Lars Gierth <larsg@systemli.org>
+	DEPENDS:=@IPV6 +kmod-tun +libnl-tiny +libpthread +librt \
+		+libuci-lua +lua-bencode +dkjson +luasocket +lua-sha2
+endef
+
+define Package/cjdns/description
+	Cjdns implements an encrypted IPv6 network using public-key cryptography \
+	for address allocation and a distributed hash table for routing. \
+	This provides near-zero-configuration networking, and prevents many \
+	of the security and scalability issues that plague existing networks.
+endef
+
+define Package/cjdns-tests
+	SECTION:=net
+	CATEGORY:=Network
+	SUBMENU:=Routing and Redirection
+	TITLE:=cjdns test cases
+	URL:=https://github.com/hyperboria/cjdns
+	MAINTAINER:=Lars Gierth <larsg@systemli.org>
+	DEPENDS:=+libpthread +librt
+endef
+
+define Package/cjdns-test/description
+	cjdns test cases
+endef
+
+define Build/Configure
+endef
+
+PKG_DO_VARS:=CJDNS_RELEASE_VERSION=$(PKG_SOURCE_VERSION)
+
+ifneq ($(CONFIG_KERNEL_SECCOMP_FILTER),y)
+PKG_DO_VARS+= Seccomp_NO=1
+endif
+
+ifneq ($(CONFIG_USE_UCLIBC),)
+PKG_DO_VARS+= UCLIBC=1
+endif
+
+define Build/Compile
+	$(INSTALL_DIR) $(PKG_BUILD_DIR)/tmp
+	CROSS="true" \
+	CC="$(TARGET_CC)" \
+	AR="$(TARGET_AR)" \
+	RANLIB="$(TARGET_RANLIB)" \
+	CFLAGS="$(TARGET_CFLAGS)" \
+	LDFLAGS="$(TARGET_LDFLAGS)" \
+	SYSTEM="linux" \
+	TARGET_ARCH="$(CONFIG_ARCH)" \
+	SSP_SUPPORT="$(CONFIG_SSP_SUPPORT)" \
+	GYP_ADDITIONAL_ARGS="-f make-linux" \
+	CJDNS_BUILD_TMPDIR="$(PKG_BUILD_DIR)/tmp" \
+	$(PKG_DO_VARS) \
+	$(PKG_BUILD_DIR)/do
+endef
+
+define Package/cjdns/install
+	$(INSTALL_DIR) \
+		$(1)/usr/sbin \
+		$(1)/usr/bin \
+		$(1)/etc/config \
+		$(1)/etc/init.d \
+		$(1)/etc/uci-defaults \
+		$(1)/usr/lib/lua/cjdns
+
+	$(INSTALL_BIN) \
+		./files/cjdrouteconf \
+		$(1)/usr/bin
+
+	$(INSTALL_BIN) \
+		$(PKG_BUILD_DIR)/cjdroute \
+		$(1)/usr/sbin
+
+	$(INSTALL_BIN) \
+		$(PKG_BUILD_DIR)/publictoip6 \
+		$(1)/usr/bin
+
+	$(INSTALL_BIN) \
+		./files/cjdns.init \
+		$(1)/etc/init.d/cjdns
+
+	$(INSTALL_BIN) \
+		./files/cjdns.defaults \
+		$(1)/etc/uci-defaults/cjdns
+
+	$(CP) \
+		./lua/cjdns/* \
+		$(1)/usr/lib/lua/cjdns
+endef
+
+define Package/cjdns/postinst
+#!/bin/sh
+if [ -z $${IPKG_INSTROOT} ] ; then
+	( . /etc/uci-defaults/cjdns ) && rm -f /etc/uci-defaults/cjdns
+	# TODO: we should have an 'Enable' button instead
+	/etc/init.d/cjdns enabled || /etc/init.d/cjdns enable
+	exit 0
+fi
+endef
+
+define Package/cjdns-tests/install
+	$(INSTALL_DIR) $(1)/usr/bin
+	$(INSTALL_BIN) \
+		$(PKG_BUILD_DIR)/build_linux/test_testcjdroute_c \
+		$(1)/usr/bin
+endef
+
+$(eval $(call BuildPackage,cjdns))
+$(eval $(call BuildPackage,cjdns-tests))

+ 127 - 0
net/cjdns/files/cjdns.defaults

@@ -0,0 +1,127 @@
+#!/bin/sh
+
+# if there is an existing config, our work is already done
+uci get cjdns.cjdns.ipv6 >/dev/null 2>&1
+if [ $? -ne 0 ]; then
+
+  # register commit handler
+  uci -q batch <<-EOF >/dev/null
+    delete ucitrack.@cjdns[-1]
+    add ucitrack cjdns
+    set ucitrack.@cjdns[-1].init=cjdns
+    commit ucitrack
+EOF
+
+  # generate configuration
+  touch /etc/config/cjdns
+  cjdroute --genconf | cjdroute --cleanconf | cjdrouteconf set
+
+  # make sure config is present (might fail for any reason)
+  uci get cjdns.cjdns.ipv6 >/dev/null 2>&1
+  if [ $? -ne 0 ]; then
+    exit 1
+  fi
+
+  # enable auto-peering on ethernet interface lan, if existing
+  uci get network.lan | grep interface >/dev/null 2>&1
+  if [ $? -eq 0 ]; then
+    uci get network.lan.type | grep bridge >/dev/null 2>&1
+    if [ $? -eq 0 ]; then
+      # most routers will set up an ethernet bridge for the lan
+      ifname="br-lan"
+    else
+      # docker containers don't have permission to create bridges by default,
+      # so we bind to the underlying interface instead (likely eth0)
+      ifname=`uci get network.lan.ifname`
+    fi
+    uci -q batch <<-EOF >/dev/null
+      add cjdns eth_interface
+      set cjdns.@eth_interface[-1].beacon=2
+      set cjdns.@eth_interface[-1].bind=$ifname
+EOF
+  fi
+  # set the tun interface name
+  uci set cjdns.cjdns.tun_device=tuncjdns
+
+  # create the network interface
+  uci -q batch <<-EOF >/dev/null
+    set network.cjdns=interface
+    set network.cjdns.ifname=tuncjdns
+    set network.cjdns.proto=none
+EOF
+
+  # firewall rules by @dangowrt -- thanks <3
+
+  # create the firewall zone
+  uci -q batch <<-EOF >/dev/null
+    add firewall zone
+    set firewall.@zone[-1].name=cjdns
+    add_list firewall.@zone[-1].network=cjdns
+    set firewall.@zone[-1].input=REJECT
+    set firewall.@zone[-1].output=ACCEPT
+    set firewall.@zone[-1].forward=REJECT
+    set firewall.@zone[-1].conntrack=1
+    set firewall.@zone[-1].family=ipv6
+EOF
+
+  # allow ICMP from cjdns zone, e.g. ping6
+  uci -q batch <<-EOF >/dev/null
+    add firewall rule
+    set firewall.@rule[-1].name='Allow-ICMPv6-cjdns'
+    set firewall.@rule[-1].src=cjdns
+    set firewall.@rule[-1].proto=icmp
+    add_list firewall.@rule[-1].icmp_type=echo-request
+    add_list firewall.@rule[-1].icmp_type=echo-reply
+    add_list firewall.@rule[-1].icmp_type=destination-unreachable
+    add_list firewall.@rule[-1].icmp_type=packet-too-big
+    add_list firewall.@rule[-1].icmp_type=time-exceeded
+    add_list firewall.@rule[-1].icmp_type=bad-header
+    add_list firewall.@rule[-1].icmp_type=unknown-header-type
+    set firewall.@rule[-1].limit='1000/sec'
+    set firewall.@rule[-1].family=ipv6
+    set firewall.@rule[-1].target=ACCEPT
+EOF
+
+  # allow SSH from cjdns zone, needs to be explicitly enabled
+  uci -q batch <<-EOF >/dev/null
+    add firewall rule
+    set firewall.@rule[-1].enabled=0
+    set firewall.@rule[-1].name='Allow-SSH-cjdns'
+    set firewall.@rule[-1].src=cjdns
+    set firewall.@rule[-1].proto=tcp
+    set firewall.@rule[-1].dest_port=22
+    set firewall.@rule[-1].target=ACCEPT
+EOF
+
+  # allow LuCI access from cjdns zone, needs to be explicitly enabled
+  uci -q batch <<-EOF >/dev/null
+    add firewall rule
+    set firewall.@rule[-1].enabled=0
+    set firewall.@rule[-1].name='Allow-HTTP-cjdns'
+    set firewall.@rule[-1].src=cjdns
+    set firewall.@rule[-1].proto=tcp
+    set firewall.@rule[-1].dest_port=80
+    set firewall.@rule[-1].target=ACCEPT
+EOF
+
+  # allow UDP peering from wan zone, if it exists
+  uci show network.wan >/dev/null 2>&1
+  if [ $? -eq 0 ]; then
+    peeringPort=`uci get cjdns.@udp_interface[0].port`
+    uci -q batch <<-EOF >/dev/null
+      add firewall rule
+      set firewall.@rule[-1].name='Allow-cjdns-wan'
+      set firewall.@rule[-1].src=wan
+      set firewall.@rule[-1].proto=udp
+      set firewall.@rule[-1].dest_port=$peeringPort
+      set firewall.@rule[-1].target=ACCEPT
+EOF
+  fi
+
+  uci commit cjdns
+  uci commit firewall
+  uci commit network
+
+fi
+
+exit 0

+ 32 - 0
net/cjdns/files/cjdns.init

@@ -0,0 +1,32 @@
+#!/bin/sh /etc/rc.common
+
+START=90
+STOP=85
+
+USE_PROCD=1
+
+start_service()
+{
+	[ -f /etc/uci-defaults/cjdns ] && ( . /etc/uci-defaults/cjdns )
+
+	procd_open_instance
+	procd_set_param respawn
+	procd_set_param command /bin/ash -c "cjdrouteconf get | tee /tmp/etc/cjdroute.conf | cjdroute --nobg | logger -t cjdns"
+	procd_close_instance
+}
+
+stop_service()
+{
+	killall cjdroute
+}
+
+reload_service()
+{
+	# cat /tmp/etc/cjdroute.conf | cjdrouteconf reload
+	restart
+}
+
+service_triggers()
+{
+	procd_add_reload_trigger cjdns
+}

+ 30 - 0
net/cjdns/files/cjdrouteconf

@@ -0,0 +1,30 @@
+#!/usr/bin/env lua
+
+dkjson = require("dkjson")
+cjdns = require("cjdns")
+require("cjdns/uci")
+
+function help()
+  print("JSON interface to /etc/config/cjdns\n\nExamples: \
+    cjdrouteconf get > /tmp/etc/cjdroute.conf \
+    cat /tmp/etc/cjdroute.conf | cjdrouteconf set \
+    uci changes \
+    cjdrouteconf get | cjdroute")
+end
+
+if arg[1] == "get" then
+  local json = dkjson.encode(cjdns.uci.get(), { indent = true })
+  print(json)
+elseif arg[1] == "set" then
+  local json = io.stdin:read("*a")
+  local obj, pos, err = dkjson.decode(json, 1, nil)
+
+  if obj then
+    cjdns.uci.set(obj)
+  else
+    print("dkjson: " .. err .. " (try cjdroute --cleanconf)")
+    os.exit(1)
+  end
+else
+  help()
+end

+ 105 - 0
net/cjdns/lua/cjdns/admin.lua

@@ -0,0 +1,105 @@
+-- Cjdns admin module for Lua
+-- Written by Philip Horger
+
+common = require 'cjdns/common'
+
+AdminInterface = {}
+AdminInterface.__index = AdminInterface
+common.AdminInterface = AdminInterface
+
+function AdminInterface.new(properties)
+    properties = properties or {}
+
+    properties.host     = properties.host or "127.0.0.1"
+    properties.port     = properties.port or 11234
+    properties.password = properties.password or nil
+    properties.config   = properties.config   or common.ConfigFile.new("/etc/cjdroute.conf", false)
+    properties.timeout  = properties.timeout  or 2
+
+    properties.udp      = common.UDPInterface.new(properties)
+
+    return setmetatable(properties, AdminInterface)
+end
+
+function AdminInterface:send(object)
+    local bencoded, err = bencode.encode(object)
+    if err then
+        return nil, err
+    end
+
+    local sock_obj = assert(socket.udp())
+    sock_obj:settimeout(self.timeout)
+
+    local _, err = sock_obj:sendto(bencoded, self.host, self.port)
+    if err then
+        return nil, err
+    end
+
+    return sock_obj
+end
+
+function AdminInterface:recv(sock_obj)
+    local retrieved, err = sock_obj:receive()
+    if not retrieved then
+        return nil, "ai:recv > " .. err
+    end
+    local bencoded, err = bencode.decode(retrieved)
+    if bencoded then
+        return bencoded
+    else
+        return nil, "ai:recv > " .. err
+    end
+end
+
+function AdminInterface:call(request)
+    local sock_obj, err = self:send(request)
+    if err then
+        return nil, "ai:call > " .. err
+    end
+
+    return self:recv(sock_obj)
+end
+
+function AdminInterface:getCookie()
+    local cookie_response, err = self:call({ q = "cookie" })
+    if not cookie_response then
+        return nil, "ai:getCookie > " .. err
+    end
+    return cookie_response.cookie
+end
+
+function AdminInterface:auth(request)
+    local funcname = request.q
+    local args = {}
+    for k, v in pairs(request) do
+        args[k] = v
+    end
+
+    -- Step 1: Get cookie
+    local cookie, err = self:getCookie()
+    if err then
+        return nil, err
+    end
+
+    -- Step 2: Calculate hash1 (password + cookie)
+    local plaintext1 = self.password .. cookie
+    local hash1 = sha2.sha256hex(plaintext1)
+
+    -- Step 3: Calculate hash2 (intermediate stage request)
+    local request = {
+        q      = "auth",
+        aq     = funcname,
+        args   = args,
+        hash   = hash1,
+        cookie = cookie
+    }
+    local plaintext2, err = bencode.encode(request)
+    if err then
+        return nil, err
+    end
+    local hash2 = sha2.sha256hex(plaintext2)
+
+    -- Step 4: Update hash in request, then ship it out
+    request.hash = hash2
+    return self:call(request)
+end

+ 7 - 0
net/cjdns/lua/cjdns/common.lua

@@ -0,0 +1,7 @@
+-- Cjdns admin module for Lua
+-- Written by Philip Horger
+
+-- This table is preserved over multiple imports, and collects
+-- submodules import-by-import via init.lua.
+
+return {}

+ 12 - 0
net/cjdns/lua/cjdns/init.lua

@@ -0,0 +1,12 @@
+-- Cjdns admin module for Lua
+-- Written by Philip Horger
+
+bencode = require "bencode" -- https://bitbucket.org/wilhelmy/lua-bencode/
+dkjson  = require "dkjson"  -- http://dkolf.de/src/dkjson-lua.fsl/home
+socket  = require "socket"  -- http://w3.impa.br/~diego/software/luasocket/
+sha2    = require "sha2"    -- https://code.google.com/p/sha2/
+
+require "cjdns/admin"
+require "cjdns/udp"
+
+return require "cjdns/common"

+ 289 - 0
net/cjdns/lua/cjdns/uci.lua

@@ -0,0 +1,289 @@
+common = require("cjdns/common")
+uci    = require("uci")
+
+UCI = {}
+common.uci = UCI
+
+--- Return the configuration defaults as a table suitable for JSON output
+--
+-- Mostly taken from cjdroute --genconf
+-- @return table with configuration defaults
+function UCI.defaults()
+  return {
+    security = {
+      { setuser = "nobody", keepNetAdmin = 1 },
+      { chroot = "/var/run/" },
+      { nofiles = 0 },
+      { noforks = 1 },
+      { seccomp = 0 },
+      { setupComplete = 1 }
+    },
+    router = {
+        ipTunnel = { outgoingConnections = {}, allowedConnections = {} },
+        interface = { type = "TUNInterface" }
+    },
+    interfaces = { UDPInterface = {}, ETHInterface = {} },
+    authorizedPasswords = {},
+    logging = { logTo = "stdout" }
+  }
+end
+
+--- Return the cjdns configuration as a table suitable for JSON output
+--
+-- Iterates over cjdns, eth_interface, udp_interface, eth_peer, udp_peer,
+-- and password sections. Doesn't include IPTunnel related options yet.
+-- @return table with cjdns configuration
+function UCI.get()
+  local obj = UCI.defaults()
+
+  local cursor = uci.cursor()
+
+  local config = cursor:get_all("cjdns", "cjdns")
+  if not config then return obj end
+
+  obj.ipv6 = config.ipv6
+  obj.publicKey = config.public_key
+  obj.privateKey = config.private_key
+  obj.admin = {
+    bind = config.admin_address .. ":" .. config.admin_port,
+    password = config.admin_password }
+
+  if config.tun_device and string.len(config.tun_device) > 0 then
+    obj.router.interface.tunDevice = config.tun_device
+  end
+
+  for i,section in pairs(obj.security) do
+    if type(section.seccomp) == "number" then
+      obj.security[i].seccomp = tonumber(config.seccomp)
+    end
+  end
+
+  cursor:foreach("cjdns", "iptunnel_outgoing", function(outgoing)
+    table.insert(obj.router.ipTunnel.outgoingConnections, outgoing.public_key)
+  end)
+
+  cursor:foreach("cjdns", "iptunnel_allowed", function(allowed)
+    entry = { publicKey = allowed.public_key }
+    if allowed.ipv4 then
+      entry["ip4Address"] = allowed.ipv4
+    end
+    if allowed.ipv6 then
+      entry["ip6Address"] = allowed.ipv6
+    end
+    table.insert(obj.router.ipTunnel.allowedConnections, entry)
+  end)
+
+  cursor:foreach("cjdns", "eth_interface", function(eth_interface)
+    table.insert(obj.interfaces.ETHInterface, {
+      bind = eth_interface.bind,
+      beacon = tonumber(eth_interface.beacon),
+      connectTo = {}
+    })
+  end)
+
+  cursor:foreach("cjdns", "udp_interface", function(udp_interface)
+    table.insert(obj.interfaces.UDPInterface, {
+      bind = udp_interface.address .. ":" .. udp_interface.port,
+      connectTo = {}
+    })
+  end)
+
+  cursor:foreach("cjdns", "eth_peer", function(eth_peer)
+    if not eth_peer.address == "" then
+      local i = tonumber(eth_peer.interface)
+      obj.interfaces.ETHInterface[i].connectTo[eth_peer.address] = {
+        publicKey = eth_peer.public_key,
+        password = eth_peer.password
+      }
+    end
+  end)
+
+  cursor:foreach("cjdns", "udp_peer", function(udp_peer)
+    local bind = udp_peer.address .. ":" .. udp_peer.port
+    local i = tonumber(udp_peer.interface)
+    obj.interfaces.UDPInterface[i].connectTo[bind] = {
+      user = udp_peer.user,
+      publicKey = udp_peer.public_key,
+      password = udp_peer.password
+    }
+  end)
+
+  cursor:foreach("cjdns", "password", function(password)
+    table.insert(obj.authorizedPasswords, {
+      password = password.password,
+      user = password.user,
+      contact = password.contact
+    })
+  end)
+
+  return obj
+end
+
+--- Parse and save updated configuration from JSON input
+--
+-- Transforms general settings, ETHInterface, UDPInterface, connectTo, and
+-- authorizedPasswords fields into UCI sections, and replaces the UCI config's
+-- contents with them.
+-- @param table JSON input
+-- @return Boolean whether saving succeeded
+function UCI.set(obj)
+  local cursor = uci.cursor()
+
+  for i, section in pairs(cursor:get_all("cjdns")) do
+    cursor:delete("cjdns", section[".name"])
+  end
+
+  local admin_address, admin_port = string.match(obj.admin.bind, "^(.*):(.*)$")
+  UCI.cursor_section(cursor, "cjdns", "cjdns", "cjdns", {
+    ipv6 = obj.ipv6,
+    public_key = obj.publicKey,
+    private_key = obj.privateKey,
+    admin_password = obj.admin.password,
+    admin_address = admin_address,
+    admin_port = admin_port
+  })
+
+  if obj.router.interface.tunDevice then
+    UCI.cursor_section(cursor, "cjdns", "cjdns", "cjdns", {
+      tun_device = tostring(obj.router.interface.tunDevice)
+    })
+  end
+
+  if obj.security then
+    for i,section in pairs(obj.security) do
+      for key,value in pairs(section) do
+        if key == "seccomp" then
+          UCI.cursor_section(cursor, "cjdns", "cjdns", "cjdns", {
+            seccomp = tonumber(value)
+          })
+        end
+      end
+    end
+  end
+
+  if obj.router.ipTunnel.outgoingConnections then
+    for i,public_key in pairs(obj.router.ipTunnel.outgoingConnections) do
+      UCI.cursor_section(cursor, "cjdns", "iptunnel_outgoing", nil, {
+        public_key = public_key
+      })
+    end
+  end
+
+  if obj.router.ipTunnel.allowedConnections then
+    for i,allowed in pairs(obj.router.ipTunnel.allowedConnections) do
+      entry = { public_key = allowed.publicKey }
+      if allowed.ip4Address then
+        entry["ipv4"] = allowed.ip4Address
+      end
+      if allowed.ip6Address then
+        entry["ipv6"] = allowed.ip6Address
+      end
+
+      UCI.cursor_section(cursor, "cjdns", "iptunnel_allowed", nil, entry)
+    end
+  end
+
+  if obj.interfaces.ETHInterface then
+    for i,interface in pairs(obj.interfaces.ETHInterface) do
+      UCI.cursor_section(cursor, "cjdns", "eth_interface", nil, {
+        bind = interface.bind,
+        beacon = tostring(interface.beacon)
+      })
+
+      if interface.connectTo then
+        for peer_address,peer in pairs(interface.connectTo) do
+          UCI.cursor_section(cursor, "cjdns", "eth_peer", nil, {
+            interface = i,
+            address = peer_address,
+            public_key = peer.publicKey,
+            password = peer.password
+          })
+        end
+      end
+    end
+  end
+
+  if obj.interfaces.UDPInterface then
+    for i,interface in pairs(obj.interfaces.UDPInterface) do
+      local address, port = string.match(interface.bind, "^(.*):(.*)$")
+      UCI.cursor_section(cursor, "cjdns", "udp_interface", nil, {
+        address = address,
+        port = port
+      })
+
+      if interface.connectTo then
+        for peer_bind,peer in pairs(interface.connectTo) do
+          local peer_address, peer_port = string.match(peer_bind, "^(.*):(.*)$")
+          UCI.cursor_section(cursor, "cjdns", "udp_peer", nil, {
+            interface = i,
+            address = peer_address,
+            port = peer_port,
+            user = peer.user,
+            public_key = peer.publicKey,
+            password = peer.password
+          })
+        end
+      end
+    end
+  end
+
+  if obj.authorizedPasswords then
+    for i,password in pairs(obj.authorizedPasswords) do
+      local user = password.user
+      if not user or string.len(user) == 0 then
+        user = "user-" .. UCI.random_string(6)
+      end
+
+      UCI.cursor_section(cursor, "cjdns", "password", nil, {
+        password = password.password,
+        user = user,
+        contact = password.contact
+      })
+    end
+  end
+
+  return cursor:save("cjdns")
+end
+
+--- Simple backport of Cursor:section from luci.model.uci
+--
+-- Backport reason: we don't wanna depend on LuCI.
+-- @param Cursor the UCI cursor to operate on
+-- @param string name of the config
+-- @param string type of the section
+-- @param string name of the section (optional)
+-- @param table config values
+function UCI.cursor_section(cursor, config, type, section, values)
+  if section then
+    cursor:set(config, section, type)
+  else
+    section = cursor:add("cjdns", type)
+  end
+
+  for k,v in pairs(values) do
+    cursor:set(config, section, k, v)
+  end
+end
+
+function UCI.makeInterface()
+  local cursor = uci.cursor()
+
+  local config = cursor:get_all("cjdns", "cjdns")
+  if not config then return nil end
+
+  return common.AdminInterface.new({
+    host = config.admin_address,
+    port = config.admin_port,
+    password = config.admin_password,
+    config = UCI.get(),
+    timeout = 2
+  })
+end
+
+function UCI.random_string(length)
+  -- tr -cd 'A-Za-z0-9' < /dev/urandom
+  local urandom = io.popen("tr -cd 'A-Za-z0-9' 2> /dev/null < /dev/urandom", "r")
+  local string = urandom:read(length)
+  urandom:close()
+  return string
+end

+ 102 - 0
net/cjdns/lua/cjdns/udp.lua

@@ -0,0 +1,102 @@
+-- Cjdns admin module for Lua
+-- Written by Philip Horger
+
+common = require 'cjdns/common'
+
+UDPInterface = {}
+UDPInterface.__index = UDPInterface
+common.UDPInterface = UDPInterface
+
+function UDPInterface.new(ai, config, ptype)
+    properties = {
+        ai     = ai,
+        config = config or ai.config,
+        ptype  = ptype or "ai"
+    }
+
+    return setmetatable(properties, UDPInterface)
+end
+
+function UDPInterface:call(name, args)
+    local func = self[name .. "_" .. self.ptype]
+    return func(self, unpack(args))
+end
+
+function UDPInterface:newBind(...)
+    return self:call("newBind", arg)
+end
+
+function UDPInterface:beginConnection(...)
+    return self:call("beginConnection", arg)
+end
+
+function UDPInterface:newBind_ai(address)
+    local response, err = self.ai:auth({
+        q = "UDPInterface_new",
+        bindAddress = address
+    })
+    if not response then
+        return nil, err
+    elseif response.error ~= "none" then
+        return nil, response.error
+    elseif response.interfaceNumber then
+        return response.interfaceNumber
+    else
+        return nil, "bad response format"
+    end
+end
+
+function UDPInterface:newBind_config(address)
+    local udpif       = self.config.contents.interfaces.UDPInterface
+    local new_interface = {
+        bind = address,
+        connectTo = {}
+    }
+    table.insert(udpif, new_interface)
+    return (#udpif - 1), new_interface
+end
+
+function UDPInterface:newBind_perm(...)
+    return
+        self:newBind_config(unpack(arg)),
+        self:newBind_ai(unpack(arg))
+end
+
+function UDPInterface:beginConnection_ai(pubkey, addr, password, interface)
+    local request = {
+        q = "UDPInterface_beginConnection",
+        publicKey = pubkey,
+        address   = addr,
+        password  = password
+    }
+    if interface then
+        request.interfaceNumber = interface
+    end
+
+    local response, err = self.ai:auth(request)
+    if not response then
+        return nil, err
+    elseif response.error == "none" then
+        -- Unfortunately, no real success indicator either.
+        return "No error"
+    else
+        return nil, response.error
+    end
+end
+
+function UDPInterface:beginConnection_config(pubkey, addr, password, interface)
+    local udpif       = self.config.contents.interfaces.UDPInterface
+    local connections = udpif[(interface or 0) + 1].connectTo
+    local this_conn   = {
+        password  = password,
+        publicKey = pubkey
+    }
+    connections[addr] = this_conn
+    return this_conn -- allows adding metadata fields afterwards
+end
+
+function UDPInterface:beginConnection_perm(...)
+    return
+        self:beginConnection_config(unpack(arg)),
+        self:beginConnection_ai(unpack(arg))
+end

+ 54 - 0
net/luci-app-cjdns/Makefile

@@ -0,0 +1,54 @@
+#
+# Copyright (C) 2014,2015 Hyperboria.net
+#
+# You may redistribute this program and/or modify it under the terms of
+# the GNU General Public License as published by the Free Software Foundation,
+# either version 3 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=luci-app-cjdns
+PKG_VERSION:=1.3
+PKG_RELEASE:=5
+
+PKG_LICENSE:=GPL-3.0
+
+include $(INCLUDE_DIR)/package.mk
+
+define Package/luci-app-cjdns
+	SECTION:=luci
+	CATEGORY:=LuCI
+	SUBMENU:=3. Applications
+	TITLE:=Encrypted near-zero-conf mesh routing protocol
+	URL:=https://github.com/hyperboria/cjdns
+	MAINTAINER:=Lars Gierth <larsg@systemli.org>
+	DEPENDS:=+cjdns +luci-base
+endef
+
+define Package/luci-app-cjdns/description
+	This package allows you to configure and inspect cjdns networking using LuCI.
+
+	Cjdns implements an encrypted IPv6 network using public-key cryptography
+	for address allocation and a distributed hash table for routing.
+	This provides near-zero-configuration networking, and prevents many
+	of the security and scalability issues that plague existing networks.
+endef
+
+define Build/Compile
+endef
+
+define Package/luci-app-cjdns/install
+	$(INSTALL_DIR) $(1)/usr/lib/lua/luci
+	$(CP) ./luasrc/* $(1)/usr/lib/lua/luci
+endef
+
+$(eval $(call BuildPackage,luci-app-cjdns))

+ 105 - 0
net/luci-app-cjdns/luasrc/controller/cjdns.lua

@@ -0,0 +1,105 @@
+module("luci.controller.cjdns", package.seeall)
+
+cjdns  = require "cjdns/init"
+dkjson = require "dkjson"
+
+function index()
+	if not nixio.fs.access("/etc/config/cjdns") then
+		return
+	end
+
+	entry({"admin", "services", "cjdns"},
+		cbi("cjdns/overview"), _("cjdns")).dependent = true
+
+	entry({"admin", "services", "cjdns", "overview"},
+		cbi("cjdns/overview"), _("Overview"), 1).leaf = false
+
+	entry({"admin", "services", "cjdns", "peering"},
+		cbi("cjdns/peering"), _("Peers"), 2).leaf = false
+
+	entry({"admin", "services", "cjdns", "iptunnel"},
+		cbi("cjdns/iptunnel"), _("IP Tunnel"), 3).leaf = false
+
+	entry({"admin", "services", "cjdns", "settings"},
+		cbi("cjdns/settings"), _("Settings"), 4).leaf = false
+
+	entry({"admin", "services", "cjdns", "cjdrouteconf"},
+		cbi("cjdns/cjdrouteconf"), _("cjdroute.conf"), 5).leaf = false
+
+	entry({"admin", "services", "cjdns", "peers"}, call("act_peers")).leaf = true
+	entry({"admin", "services", "cjdns", "ping"}, call("act_ping")).leaf = true
+end
+
+function act_peers()
+	require("cjdns/uci")
+	admin = cjdns.uci.makeInterface()
+
+	local page = 0
+	local peers = {}
+
+	while page do
+		local response, err = admin:auth({
+			q = "InterfaceController_peerStats",
+			page = page
+		})
+
+		if err or response.error then
+			luci.http.status(502, "Bad Gateway")
+			luci.http.prepare_content("application/json")
+			luci.http.write_json({ err = err, response = response })
+			return
+		end
+
+		for i,peer in pairs(response.peers) do
+			peer.ipv6 = publictoip6(peer.publicKey)
+			if peer.user == nil then
+				peer.user = ''
+				uci.cursor():foreach("cjdns", "udp_peer", function(udp_peer)
+					if peer.publicKey == udp_peer.public_key then
+						peer.user = udp_peer.user
+					end
+				end)
+			end
+			peers[#peers + 1] = peer
+		end
+
+		if response.more then
+			page = page + 1
+		else
+			page = nil
+		end
+	end
+
+	luci.http.status(200, "OK")
+	luci.http.prepare_content("application/json")
+	luci.http.write_json(peers)
+end
+
+function act_ping()
+	require("cjdns/uci")
+	admin = cjdns.uci.makeInterface()
+
+	local response, err = admin:auth({
+		q = "SwitchPinger_ping",
+		path = luci.http.formvalue("label"),
+		timeout = tonumber(luci.http.formvalue("timeout"))
+	})
+
+	if err or response.error then
+		luci.http.status(502, "Bad Gateway")
+		luci.http.prepare_content("application/json")
+		luci.http.write_json({ err = err, response = response })
+		return
+	end
+
+	luci.http.status(200, "OK")
+	luci.http.prepare_content("application/json")
+	luci.http.write_json(response)
+end
+
+function publictoip6(publicKey)
+	local process = io.popen("/usr/bin/publictoip6 " .. publicKey, "r")
+	local ipv6    = process:read()
+	process:close()
+	return ipv6
+end

+ 32 - 0
net/luci-app-cjdns/luasrc/model/cbi/cjdns/cjdrouteconf.lua

@@ -0,0 +1,32 @@
+m = Map("cjdns", translate("cjdns"),
+  translate("Implements an encrypted IPv6 network using public-key \
+    cryptography for address allocation and a distributed hash table for \
+    routing. This provides near-zero-configuration networking, and prevents \
+    many of the security and scalability issues that plague existing \
+    networks."))
+
+dkjson = require("dkjson")
+cjdns = require("cjdns")
+require("cjdns/uci")
+
+local f = SimpleForm("cjdrouteconf", translate("Edit cjdroute.conf"),
+	translate("JSON interface to what's /etc/cjdroute.conf on other systems. \
+    Will be parsed and written to UCI by <code>cjdrouteconf set</code>."))
+
+local o = f:field(Value, "_cjdrouteconf")
+o.template = "cbi/tvalue"
+o.rows = 25
+
+function o.cfgvalue(self, section)
+	return dkjson.encode(cjdns.uci.get(), { indent = true })
+end
+
+function o.write(self, section, value)
+  local obj, pos, err = dkjson.decode(value, 1, nil)
+
+  if obj then
+    cjdns.uci.set(obj)
+  end
+end
+
+return f

+ 46 - 0
net/luci-app-cjdns/luasrc/model/cbi/cjdns/iptunnel.lua

@@ -0,0 +1,46 @@
+uci = require "luci.model.uci"
+cursor = uci:cursor_state()
+
+m = Map("cjdns", translate("cjdns"),
+  translate("Implements an encrypted IPv6 network using public-key \
+    cryptography for address allocation and a distributed hash table for \
+    routing. This provides near-zero-configuration networking, and prevents \
+    many of the security and scalability issues that plague existing \
+    networks."))
+
+m.on_after_commit = function(self)
+  os.execute("/etc/init.d/cjdns restart")
+end
+
+-- Outgoing
+outgoing = m:section(TypedSection, "iptunnel_outgoing", translate("Outgoing IP Tunnel Connections"),
+  translate("Enter the public keys of the nodes that will provide Internet access."))
+outgoing.anonymous = true
+outgoing.addremove = true
+outgoing.template  = "cbi/tblsection"
+
+outgoing:option(Value, "public_key", translate("Public Key")).size = 55
+
+-- Allowed
+allowed = m:section(TypedSection, "iptunnel_allowed", translate("Allowed IP Tunnel Connections"),
+  translate("Enter the public key of the node you will provide Internet access to, along with the \
+             IPv4 and/or IPv6 address you will assign them."))
+allowed.anonymous = true
+allowed.addremove = true
+
+public_key = allowed:option(Value, "public_key", translate("Public Key"))
+public_key.template = "cjdns/value"
+public_key.size = 55
+
+ipv4 = allowed:option(Value, "ipv4", translate("IPv4"))
+ipv4.template = "cjdns/value"
+ipv4.datatype = 'ipaddr'
+ipv4.size = 55
+
+ipv6 = allowed:option(Value, "ipv6", translate("IPv6"),
+  translate("IPv6 addresses should be entered <em>without</em> brackets here, e.g. <code>2001:123:ab::10</code>."))
+ipv6.template = "cjdns/value"
+ipv6.datatype = 'ip6addr'
+ipv6.size = 55
+
+return m

+ 10 - 0
net/luci-app-cjdns/luasrc/model/cbi/cjdns/overview.lua

@@ -0,0 +1,10 @@
+m = Map("cjdns", translate("cjdns"),
+  translate("Implements an encrypted IPv6 network using public-key \
+    cryptography for address allocation and a distributed hash table for \
+    routing. This provides near-zero-configuration networking, and prevents \
+    many of the security and scalability issues that plague existing \
+    networks."))
+
+m:section(SimpleSection).template  = "cjdns/status"
+
+return m

+ 73 - 0
net/luci-app-cjdns/luasrc/model/cbi/cjdns/peering.lua

@@ -0,0 +1,73 @@
+uci = require "luci.model.uci"
+cursor = uci:cursor_state()
+
+cjdns = require("cjdns")
+require("cjdns/uci")
+
+m = Map("cjdns", translate("cjdns"),
+  translate("Implements an encrypted IPv6 network using public-key \
+    cryptography for address allocation and a distributed hash table for \
+    routing. This provides near-zero-configuration networking, and prevents \
+    many of the security and scalability issues that plague existing \
+    networks."))
+
+m.on_after_commit = function(self)
+  os.execute("/etc/init.d/cjdns restart")
+end
+
+-- Authorized Passwords
+passwords = m:section(TypedSection, "password", translate("Authorized Passwords"),
+  translate("Anyone offering one of the these passwords will be allowed to peer with you on the existing UDP and Ethernet interfaces."))
+passwords.anonymous = true
+passwords.addremove = true
+passwords.template  = "cbi/tblsection"
+
+passwords:option(Value, "user", translate("User/Name"),
+  translate("Must be unique.")
+).default = "user-" .. cjdns.uci.random_string(6)
+passwords:option(Value, "contact", translate("Contact"), translate("Optional, for out-of-band communication."))
+passwords:option(Value, "password", translate("Password"),
+  translate("Hand out to your peer, in accordance with the peering best practices of the network.")
+).default = cjdns.uci.random_string(32)
+
+-- UDP Peers
+udp_peers = m:section(TypedSection, "udp_peer", translate("Outgoing UDP Peers"),
+  translate("For peering via public IP networks, the peer handed you their Public Key and IP address/port along with a password. IPv6 addresses should be entered with square brackets, like so: <code>[2001::1]</code>."))
+udp_peers.anonymous = true
+udp_peers.addremove = true
+udp_peers.template  = "cbi/tblsection"
+udp_peers:option(Value, "user", translate("User/Name")).datatype = "string"
+
+udp_interface = udp_peers:option(Value, "interface", translate("UDP interface"))
+local index = 1
+for i,section in pairs(cursor:get_all("cjdns")) do
+  if section[".type"] == "udp_interface" then
+    udp_interface:value(index, section.address .. ":" .. section.port)
+  end
+end
+udp_interface.default = 1
+udp_peers:option(Value, "address", translate("IP address"))
+udp_peers:option(Value, "port", translate("Port")).datatype = "portrange"
+udp_peers:option(Value, "public_key", translate("Public key"))
+udp_peers:option(Value, "password", translate("Password"))
+
+-- Ethernet Peers
+eth_peers = m:section(TypedSection, "eth_peer", translate("Outgoing Ethernet Peers"),
+  translate("For peering via local Ethernet networks, the peer handed you their Public Key and MAC address along with a password."))
+eth_peers.anonymous = true
+eth_peers.addremove = true
+eth_peers.template  = "cbi/tblsection"
+
+eth_interface = eth_peers:option(Value, "interface", translate("Ethernet interface"))
+local index = 1
+for i,section in pairs(cursor:get_all("cjdns")) do
+  if section[".type"] == "eth_interface" then
+    eth_interface:value(index, section.bind)
+  end
+end
+eth_interface.default = 1
+eth_peers:option(Value, "address", translate("MAC address")).datatype = "macaddr"
+eth_peers:option(Value, "public_key", translate("Public key"))
+eth_peers:option(Value, "password", translate("Password"))
+
+return m

+ 67 - 0
net/luci-app-cjdns/luasrc/model/cbi/cjdns/settings.lua

@@ -0,0 +1,67 @@
+m = Map("cjdns", translate("cjdns"),
+  translate("Implements an encrypted IPv6 network using public-key \
+    cryptography for address allocation and a distributed hash table for \
+    routing. This provides near-zero-configuration networking, and prevents \
+    many of the security and scalability issues that plague existing \
+    networks."))
+
+m.on_after_commit = function(self)
+  os.execute("/etc/init.d/cjdns restart")
+end
+
+s = m:section(NamedSection, "cjdns", nil, translate("Settings"))
+s.addremove = false
+
+-- Identity
+s:tab("identity", translate("Identity"))
+node6 = s:taboption("identity", Value, "ipv6", translate("IPv6 address"),
+      translate("This node's IPv6 address within the cjdns network."))
+node6.datatype = "ip6addr"
+pbkey = s:taboption("identity", Value, "public_key", translate("Public key"),
+      translate("Used for packet encryption and authentication."))
+pbkey.datatype = "string"
+prkey = s:taboption("identity", Value, "private_key", translate("Private key"),
+      translate("Keep this private. When compromised, generate a new keypair and IPv6."))
+prkey.datatype = "string"
+
+-- Admin Interface
+s:tab("admin", translate("Admin API"), translate("The Admin API can be used by other applications or services to configure and inspect cjdns' routing and peering.<br/><br/>Documentation: <a href=\"https://github.com/cjdelisle/cjdns/tree/master/admin#cjdns-admin-api\">admin/README.md</a>"))
+aip = s:taboption("admin", Value, "admin_address", translate("IP Address"),
+      translate("IPv6 addresses should be entered like so: <code>[2001::1]</code>."))
+apt = s:taboption("admin", Value, "admin_port", translate("Port"))
+apt.datatype = "port"
+apw = s:taboption("admin", Value, "admin_password", translate("Password"))
+apw.datatype = "string"
+
+-- Security
+s:tab("security", translate("Security"), translate("Functionality related to hardening the cjdroute process."))
+s:taboption("security", Flag, "seccomp", translate("SecComp sandboxing"))
+
+-- UDP Interfaces
+udp_interfaces = m:section(TypedSection, "udp_interface", translate("UDP Interfaces"),
+  translate("These interfaces allow peering via public IP networks, such as the Internet, or many community-operated wireless networks. IPv6 addresses should be entered with square brackets, like so: <code>[2001::1]</code>."))
+udp_interfaces.anonymous = true
+udp_interfaces.addremove = true
+udp_interfaces.template = "cbi/tblsection"
+
+udp_address = udp_interfaces:option(Value, "address", translate("IP Address"))
+udp_address.placeholder = "0.0.0.0"
+udp_interfaces:option(Value, "port", translate("Port")).datatype = "portrange"
+
+-- Ethernet Interfaces
+eth_interfaces = m:section(TypedSection, "eth_interface", translate("Ethernet Interfaces"),
+  translate("These interfaces allow peering via local Ethernet networks, such as home or office networks, or phone tethering. If an interface name is set to \"all\" each available device will be used."))
+eth_interfaces.anonymous = true
+eth_interfaces.addremove = true
+eth_interfaces.template = "cbi/tblsection"
+
+eth_bind = eth_interfaces:option(Value, "bind", translate("Network Interface"))
+eth_bind.placeholder = "br-lan"
+eth_beacon = eth_interfaces:option(Value, "beacon", translate("Beacon Mode"))
+eth_beacon:value(0, translate("0 -- Disabled"))
+eth_beacon:value(1, translate("1 -- Accept beacons"))
+eth_beacon:value(2, translate("2 -- Accept and send beacons"))
+eth_beacon.default = 2
+eth_beacon.datatype = "integer(range(0,2))"
+
+return m

+ 1 - 0
net/luci-app-cjdns/luasrc/view/admin_status/index/cjdns.htm

@@ -0,0 +1 @@
+<%+cjdns/status%>

+ 116 - 0
net/luci-app-cjdns/luasrc/view/cjdns/status.htm

@@ -0,0 +1,116 @@
+<script type="text/javascript">//<![CDATA[
+
+	var peersURI = '<%=luci.dispatcher.build_url("admin", "services", "cjdns", "peers")%>';
+	var updatePeers = function(x, peers) {
+		var table = document.getElementById('cjdns-peerings');
+		while (table.rows.length > 1) {
+			table.deleteRow(1);
+		}
+
+		if ((peers) && ((peers.err) || (typeof peers.length === 'undefined'))) {
+			var errpeer = (peers.err)
+						? 'Socket Error: unable to connect to Admin API'
+						: 'No active peers';
+			var row = table.insertRow(-1);
+			row.className = 'cbi-section-table-row';
+			var cell = row.insertCell(-1);
+			cell.colSpan = 7;
+			cell.textContent = errpeer;
+			return;
+		};
+
+		peers.forEach(function(peer, i) {
+			if (peer.user == null) {
+				var user = '';
+			} else if (peer.user == 'Local Peers') {
+				var user = 'beacon';
+			} else {
+				var user = peer.user;
+			}
+
+			if (peer.isIncoming === 0) {
+				var interface = 'outgoing';
+			} else {
+				var interface = 'incoming';
+			}
+
+			var status = interface + ', ' + peer.state.toLowerCase();
+
+			if (peer.version === 0) {
+				var version = '-';
+			} else {
+				var version = 'v' + peer.version;
+			}
+
+			var rxtx = lbbytes(peer.bytesIn) + ' / ' + lbbytes(peer.bytesOut);
+
+			var row = table.insertRow(-1);
+			row.className = 'cbi-section-table-row cbi-rowstyle-' + ((i % 2) + 1);
+			row.insertCell(-1).textContent = user;
+			row.insertCell(-1).textContent = peer.ipv6;
+			row.insertCell(-1).textContent = status;
+			row.insertCell(-1).textContent = version;
+			row.insertCell(-1).textContent = rxtx;
+			var latencyCell = row.insertCell(-1);
+			latencyCell.textContent = 'waiting';
+
+			var pingURI = '<%=luci.dispatcher.build_url("admin", "services", "cjdns", "ping")%>';
+			var timeout = 2000;
+			XHR.get(pingURI, { label: peer.switchLabel, timeout: timeout }, function(x, pong) {
+				var pongrsp = ((pong.err == "ai:recv > timeout") || (pong == "undefined") || (pong.ms >= timeout))
+					? '> ' + timeout + ' ms'
+					: pong.ms + ' ms';
+				latencyCell.textContent = pongrsp;
+			})
+		});
+
+	};
+
+	XHR.get(peersURI, null, updatePeers);
+	XHR.poll(5, peersURI, null, updatePeers);
+
+//]]></script>
+
+<script type="text/javascript">
+<%# Author: [GitHub/75lb] -%>
+//<![CDATA[
+function lbbytes (bytes){
+
+	var kilobyte = 1024,
+	    megabyte = kilobyte * 1024,
+	    gigabyte = megabyte * 1024,
+	    terabyte = gigabyte * 1024;
+
+	if ((bytes >= 0) && (bytes < kilobyte)) {
+		return bytes + " B";
+	} else if ((bytes >= kilobyte) && (bytes < megabyte)) {
+		return (bytes / kilobyte).toFixed(2) + " KB";
+	} else if ((bytes >= megabyte) && (bytes < gigabyte)) {
+		return (bytes / megabyte).toFixed(2) + " MB";
+	} else if ((bytes >= gigabyte) && (bytes < terabyte)) {
+		return (bytes / gigabyte).toFixed(2) + " GB";
+	} else if (bytes >= terabyte) {
+		return (bytes / terabyte).toFixed(2) + " TB";
+	} else {
+		return bytes + " B";
+	}
+};
+//]]>
+</script>
+
+<fieldset class="cbi-section">
+	<legend>Active cjdns peers</legend>
+	<table class="cbi-section-table" id="cjdns-peerings">
+		<tr class="cbi-section-table-titles">
+			<th class="cbi-section-table-cell">User/Name</th>
+			<th class="cbi-section-table-cell">IPv6</th>
+			<th class="cbi-section-table-cell">Status</th>
+			<th class="cbi-section-table-cell">Version</th>
+			<th class="cbi-section-table-cell">Rx / Tx</th>
+			<th class="cbi-section-table-cell">Latency</th>
+		</tr>
+		<tr class="cbi-section-table-row">
+			<td colspan="7">Querying Admin API</td>
+		</tr>
+	</table>
+</fieldset>

+ 35 - 0
net/luci-app-cjdns/luasrc/view/cjdns/value.htm

@@ -0,0 +1,35 @@
+<%+cbi/valueheader%>
+	<input type="<%=self.password and 'password" class="cbi-input-password' or 'text" class="cbi-input-text' %>" onchange="cbi_d_update(this.id)"<%=
+		attr("name", cbid) .. attr("id", cbid) .. attr("value", self:cfgvalue(section) or self.default) ..
+		ifattr(self.size, "size") .. ifattr(self.placeholder, "placeholder")
+	%> style="width: auto" />
+	<% if self.password then %><img src="<%=resource%>/cbi/reload.gif" style="vertical-align:middle" title="<%:Reveal/hide password%>" onclick="var e = document.getElementById('<%=cbid%>'); e.type = (e.type=='password') ? 'text' : 'password';" /><% end %>
+	<% if #self.keylist > 0 or self.datatype then -%>
+	<script type="text/javascript">//<![CDATA[
+		<% if #self.keylist > 0 then -%>
+		cbi_combobox_init('<%=cbid%>', {
+		<%-
+			for i, k in ipairs(self.keylist) do
+		-%>
+			<%-=string.format("%q", k) .. ":" .. string.format("%q", self.vallist[i])-%>
+			<%-if i<#self.keylist then-%>,<%-end-%>
+		<%-
+			end
+		-%>
+		}, '<%- if not self.rmempty and not self.optional then -%>
+			<%-: -- Please choose -- -%>
+			<%- elseif self.placeholder then -%>
+			<%-= pcdata(self.placeholder) -%>
+		<%- end -%>', '
+		<%- if self.combobox_manual then -%>
+			<%-=self.combobox_manual-%>
+		<%- else -%>
+			<%-: -- custom -- -%>
+		<%- end -%>');
+		<%- end %>
+		<% if self.datatype then -%>
+		cbi_validate_field('<%=cbid%>', <%=tostring((self.optional or self.rmempty) == true)%>, '<%=self.datatype:gsub("'", "\\'")%>');
+		<%- end %>
+	//]]></script>
+	<% end -%>
+<%+cbi/valuefooter%>

+ 81 - 0
net/miniupnpd/Makefile

@@ -0,0 +1,81 @@
+#
+# Copyright (C) 2006-2014 OpenWrt.org
+#
+# This is free software, licensed under the GNU General Public License v2.
+# See /LICENSE for more information.
+#
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=miniupnpd
+PKG_VERSION:=2.0.20170421
+PKG_RELEASE:=1
+
+PKG_SOURCE_URL:=http://miniupnp.free.fr/files
+PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
+PKG_HASH:=9677aeccadf73b4bf8bb9d832c32b5da8266b4d58eed888f3fd43d7656405643
+
+PKG_MAINTAINER:=Markus Stenberg <fingon@iki.fi>
+PKG_LICENSE:=BSD-3-Clause
+
+include $(INCLUDE_DIR)/package.mk
+
+define Package/miniupnpd
+  SECTION:=net
+  CATEGORY:=Network
+  DEPENDS:=+iptables +libip4tc +IPV6:libip6tc +IPV6:ip6tables +libuuid
+  TITLE:=Lightweight UPnP IGD, NAT-PMP & PCP daemon
+  SUBMENU:=Firewall
+  URL:=http://miniupnp.free.fr/
+endef
+
+define Package/miniupnpd/config
+config MINIUPNPD_IGDv2
+	bool
+	default y
+	prompt "Enable IGDv2"
+endef
+
+define Package/miniupnpd/conffiles
+/etc/config/upnpd
+endef
+
+define Package/miniupnpd/postinst
+#!/bin/sh
+
+if [ -z "$$IPKG_INSTROOT" ]; then
+  ( . /etc/uci-defaults/99-miniupnpd )
+  rm -f /etc/uci-defaults/99-miniupnpd
+fi
+
+exit 0
+endef
+
+define Build/Prepare
+	$(call Build/Prepare/Default)
+	echo "OpenWrt" | tr \(\)\  _ >$(PKG_BUILD_DIR)/os.openwrt
+endef
+
+MAKE_FLAGS += \
+	TARGET_OPENWRT=1 TEST=0 \
+	LIBS="" \
+	CC="$(TARGET_CC) -DIPTABLES_143 \
+		-lip4tc $(if $(CONFIG_IPV6),-lip6tc) -luuid" \
+	CONFIG_OPTIONS="--portinuse --leasefile \
+		$(if $(CONFIG_IPV6),--ipv6) \
+		$(if $(CONFIG_MINIUPNPD_IGDv2),--igd2)" \
+	-f Makefile.linux \
+	miniupnpd
+
+
+define Package/miniupnpd/install
+	$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/config $(1)/etc/uci-defaults $(1)/etc/hotplug.d/iface $(1)/usr/share/miniupnpd
+	$(INSTALL_BIN) $(PKG_BUILD_DIR)/miniupnpd $(1)/usr/sbin/miniupnpd
+	$(INSTALL_BIN) ./files/miniupnpd.init $(1)/etc/init.d/miniupnpd
+	$(INSTALL_CONF) ./files/upnpd.config $(1)/etc/config/upnpd
+	$(INSTALL_DATA) ./files/miniupnpd.hotplug $(1)/etc/hotplug.d/iface/50-miniupnpd
+	$(INSTALL_DATA) ./files/miniupnpd.defaults $(1)/etc/uci-defaults/99-miniupnpd
+	$(INSTALL_DATA) ./files/firewall.include $(1)/usr/share/miniupnpd/firewall.include
+endef
+
+$(eval $(call BuildPackage,miniupnpd))

+ 57 - 0
net/miniupnpd/files/firewall.include

@@ -0,0 +1,57 @@
+#!/bin/sh
+# miniupnpd integration for firewall3
+
+IP6TABLES=/usr/sbin/ip6tables
+
+iptables -t filter -N MINIUPNPD 2>/dev/null
+iptables -t nat -N MINIUPNPD 2>/dev/null
+iptables -t nat -N MINIUPNPD-POSTROUTING 2>/dev/null
+
+[ -x $IP6TABLES ] && $IP6TABLES -t filter -N MINIUPNPD 2>/dev/null
+
+. /lib/functions/network.sh
+
+ADDED=0
+
+add_extzone_rules() {
+    local ext_zone=$1
+
+    [ -z "$ext_zone" ] && return
+
+    # IPv4 - due to NAT, need to add both to nat and filter table
+    iptables -t filter -I zone_${ext_zone}_forward -j MINIUPNPD
+    iptables -t nat -I zone_${ext_zone}_prerouting -j MINIUPNPD
+    iptables -t nat -I zone_${ext_zone}_postrouting -j MINIUPNPD-POSTROUTING
+
+    # IPv6 if available - filter only
+    [ -x $IP6TABLES ] && {
+        $IP6TABLES -t filter -I zone_${ext_zone}_forward -j MINIUPNPD
+    }
+    ADDED=$(($ADDED + 1))
+}
+
+# By default, user configuration is king.
+
+for ext_iface in $(uci -q get upnpd.config.external_iface); do
+    add_extzone_rules $(fw3 -q network "$ext_iface")
+done
+
+add_extzone_rules $(uci -q get upnpd.config.external_zone)
+
+[ ! $ADDED = 0 ] && exit 0
+
+
+# If really nothing is available, resort to network_find_wan{,6} and
+# assume external interfaces all have same firewall zone.
+
+# (This heuristic may fail horribly, in case of e.g. multihoming, so
+# please set external_zone in that case!)
+
+network_find_wan wan_iface
+network_find_wan6 wan6_iface
+
+for ext_iface in $wan_iface $wan6_iface; do
+    # fw3 -q network fails on sub-interfaces => map to device first
+    network_get_device ext_device $ext_iface
+    add_extzone_rules $(fw3 -q device "$ext_device")
+done

+ 13 - 0
net/miniupnpd/files/miniupnpd.defaults

@@ -0,0 +1,13 @@
+#!/bin/sh
+
+uci -q batch <<-EOT
+	delete firewall.miniupnpd
+	set firewall.miniupnpd=include
+	set firewall.miniupnpd.type=script
+	set firewall.miniupnpd.path=/usr/share/miniupnpd/firewall.include
+	set firewall.miniupnpd.family=any
+	set firewall.miniupnpd.reload=1
+	commit firewall
+EOT
+
+exit 0

+ 39 - 0
net/miniupnpd/files/miniupnpd.hotplug

@@ -0,0 +1,39 @@
+#!/bin/sh
+
+/etc/init.d/miniupnpd enabled || exit 0
+
+. /lib/functions/service.sh
+
+# If miniupnpd is not running:
+# - check on _any_ event (even updates may contribute to network_find_wan*)
+
+# If miniupnpd _is_ running:
+# - check only on ifup (otherwise lease updates etc would cause
+#   miniupnpd state loss)
+
+[ ! "$ACTION" = "ifup" ] && service_check /usr/sbin/miniupnpd && exit 0
+
+tmpconf="/var/etc/miniupnpd.conf"
+extiface=$(uci get upnpd.config.external_iface)
+extzone=$(uci get upnpd.config.external_zone)
+
+. /lib/functions/network.sh
+
+for iface in $(uci get upnpd.config.internal_iface); do
+    network_get_device device $iface
+    [ "$DEVICE" = "$device" ] && /etc/init.d/miniupnpd restart && exit 0
+done
+
+
+if [ -z "$extiface" ] ; then
+  # manual external zone (if dynamically find interfaces
+  # belonging to it) overrides network_find_wan*
+  if [ -n "$extzone" ] ; then
+    ifname=$(fw3 -q zone $extzone | head -1)
+  fi
+  [ -n "$extiface" ] || network_find_wan extiface
+  [ -n "$extiface" ] || network_find_wan6 extiface
+fi
+
+[ -n "$ifname" ] || network_get_device ifname ${extiface}
+grep -q "ext_ifname=$ifname" $tmpconf || /etc/init.d/miniupnpd restart

+ 212 - 0
net/miniupnpd/files/miniupnpd.init

@@ -0,0 +1,212 @@
+#!/bin/sh /etc/rc.common
+# Copyright (C) 2006-2014 OpenWrt.org
+
+START=94
+STOP=15
+
+SERVICE_USE_PID=1
+
+upnpd_get_port_range() {
+	local _var="$1"; shift
+	local _val
+
+	config_get _val "$@"
+
+	case "$_val" in
+		[0-9]*[:-][0-9]*)
+			export -n -- "${_var}_start=${_val%%[:-]*}"
+			export -n -- "${_var}_end=${_val##*[:-]}"
+		;;
+		[0-9]*)
+			export -n -- "${_var}_start=$_val"
+			export -n -- "${_var}_end="
+		;;
+	esac
+}
+
+conf_rule_add() {
+	local cfg="$1"
+	local tmpconf="$2"
+	local action external_port_start external_port_end int_addr
+	local internal_port_start internal_port_end
+
+	config_get action "$cfg" action "deny"               # allow or deny
+	upnpd_get_port_range "ext" "$cfg" ext_ports "0-65535" # external ports: x, x-y, x:y
+	config_get int_addr "$cfg" int_addr "0.0.0.0/0"       # ip or network and subnet mask (internal)
+	upnpd_get_port_range "int" "$cfg" int_ports "0-65535" # internal ports: x, x-y, x:y or range
+
+	# Make a single IP IP/32 so that miniupnpd.conf can use it.
+	case "$int_addr" in
+		*/*) ;;
+		*) int_addr="$int_addr/32" ;;
+	esac
+
+	echo "${action} ${ext_start}${ext_end:+-}${ext_end} ${int_addr} ${int_start}${int_end:+-}${int_end}" >>$tmpconf
+}
+
+upnpd_write_bool() {
+	local opt="$1"
+	local def="${2:-0}"
+	local alt="$3"
+	local val
+
+	config_get_bool val config "$opt" "$def"
+	if [ "$val" -eq 0 ]; then
+		echo "${alt:-$opt}=no" >> $tmpconf
+	else
+		echo "${alt:-$opt}=yes" >> $tmpconf
+	fi
+}
+
+boot() {
+	return
+}
+
+start() {
+	config_load "upnpd"
+	local extiface intiface upload download logging secure enabled natpmp
+	local extip port usesysuptime conffile serial_number model_number
+	local uuid notify_interval presentation_url enable_upnp
+	local upnp_lease_file clean_ruleset_threshold clean_ruleset_interval
+        local ipv6_listening_ip enabled
+
+	config_get_bool enabled config enabled 1
+
+	[ "$enabled" -gt 0 ] || return 1
+
+	config_get extiface config external_iface
+	config_get extzone config external_zone
+	config_get intiface config internal_iface
+	config_get extip config external_ip
+	config_get port config port 5000
+	config_get upload   config upload
+	config_get download config download
+	config_get_bool logging config log_output 0
+	config_get conffile config config_file
+	config_get serial_number config serial_number
+	config_get model_number config model_number
+	config_get uuid config uuid
+	config_get notify_interval config notify_interval
+	config_get presentation_url config presentation_url
+	config_get upnp_lease_file config upnp_lease_file
+	config_get clean_ruleset_threshold config clean_ruleset_threshold
+	config_get clean_ruleset_interval config clean_ruleset_interval
+	config_get ipv6_listening_ip config ipv6_listening_ip
+
+	local args
+
+	. /lib/functions/network.sh
+
+	local ifname
+
+        # manual external interface overrides everything
+        if [ -z "$extiface" ] ; then
+            # manual external zone (if dynamically find interfaces
+            # belonging to it) overrides network_find_wan*
+            if [ -n "$extzone" ] ; then
+                ifname=$(fw3 -q zone $extzone | head -1)
+            fi
+            [ -n "$extiface" ] || network_find_wan extiface
+            [ -n "$extiface" ] || network_find_wan6 extiface
+        fi
+
+	[ -n "$ifname" ] || network_get_device ifname ${extiface}
+
+	if [ -n "$conffile" ]; then
+		args="-f $conffile"
+	else
+		local tmpconf="/var/etc/miniupnpd.conf"
+		args="-f $tmpconf"
+		mkdir -p /var/etc
+
+		echo "ext_ifname=$ifname" >$tmpconf
+
+		[ -n "$extip" ] && \
+			echo "ext_ip=$extip" >>$tmpconf
+
+		local iface
+		for iface in ${intiface:-lan}; do
+			local device
+			network_get_device device "$iface" && {
+				echo "listening_ip=$device" >>$tmpconf
+			}
+		done
+
+		[ "$port" != "auto" ] && \
+			echo "port=$port" >>$tmpconf
+
+		config_load "upnpd"
+		upnpd_write_bool enable_natpmp 1
+		upnpd_write_bool enable_upnp 1
+		upnpd_write_bool secure_mode 1
+		upnpd_write_bool pcp_allow_thirdparty 0
+		upnpd_write_bool system_uptime 1
+
+		[ -n "$upnp_lease_file" ] && \
+			echo "lease_file=$upnp_lease_file" >>$tmpconf
+
+		[ -n "$upload" -a -n "$download" ] && {
+			echo "bitrate_down=$(($download * 1024 * 8))" >>$tmpconf
+			echo "bitrate_up=$(($upload * 1024 * 8))" >>$tmpconf
+		}
+
+		[ -n "${presentation_url}" ] && \
+			echo "presentation_url=${presentation_url}" >>$tmpconf
+
+		[ -n "${notify_interval}" ] && \
+			echo "notify_interval=${notify_interval}" >>$tmpconf
+
+		[ -n "${clean_ruleset_threshold}" ] && \
+			echo "clean_ruleset_threshold=${clean_ruleset_threshold}" >>$tmpconf
+
+		[ -n "${clean_ruleset_interval}" ] && \
+			echo "clean_ruleset_interval=${clean_ruleset_interval}" >>$tmpconf
+
+		[ -n "${ipv6_listening_ip}" ] && \
+			echo "ipv6_listening_ip=${ipv6_listening_ip}" >>$tmpconf
+
+		[ -z "$uuid" ] && {
+			uuid="$(cat /proc/sys/kernel/random/uuid)"
+			uci set upnpd.config.uuid=$uuid
+			uci commit upnpd
+		}
+
+		[ "$uuid" = "nocli" ] || \
+			echo "uuid=$uuid" >>$tmpconf
+
+		[ -n "${serial_number}" ] && \
+			echo "serial=${serial_number}" >>$tmpconf
+
+		[ -n "${model_number}" ] && \
+			echo "model_number=${model_number}" >>$tmpconf
+
+	    config_foreach conf_rule_add perm_rule "$tmpconf"
+	fi
+
+
+	if [ -n "$ifname" ]; then
+		# start firewall
+		iptables -L MINIUPNPD >/dev/null 2>/dev/null || fw3 reload
+
+		if [ "$logging" = "1" ]; then
+			SERVICE_DAEMONIZE=1 \
+			service_start /usr/sbin/miniupnpd $args -d
+		else
+			SERVICE_DAEMONIZE= \
+			service_start /usr/sbin/miniupnpd $args
+		fi
+	else
+		logger -t "upnp daemon" "external interface not found, not starting"
+	fi
+}
+
+stop() {
+	service_stop /usr/sbin/miniupnpd
+
+	iptables -t nat -F MINIUPNPD 2>/dev/null
+	iptables -t filter -F MINIUPNPD 2>/dev/null
+
+        [ -x /usr/sbin/ip6tables ] && {
+	    ip6tables -t filter -F MINIUPNPD 2>/dev/null
+        }
+}

+ 27 - 0
net/miniupnpd/files/upnpd.config

@@ -0,0 +1,27 @@
+config upnpd config
+	option enabled		0
+	option enable_natpmp	1
+	option enable_upnp	1
+	option secure_mode	1
+	option log_output	0
+	option download 	1024
+	option upload   	512
+        #by default, looked up dynamically from ubus
+	#option external_iface	wan
+	option internal_iface	lan
+	option port		5000
+	option upnp_lease_file	/var/upnp.leases
+	
+config perm_rule
+	option action		allow
+	option ext_ports	1024-65535
+	option int_addr		0.0.0.0/0	# Does not override secure_mode
+	option int_ports	1024-65535
+	option comment		"Allow high ports"
+
+config perm_rule
+       option action		deny
+       option ext_ports		0-65535
+       option int_addr		0.0.0.0/0
+       option int_ports		0-65535
+       option comment		"Default deny"

+ 23 - 0
net/miniupnpd/patches/101-no-ssl-uuid.patch

@@ -0,0 +1,23 @@
+We do not need to autodetect SSL/UUID; SSL we do not support, UUID we always do.
+
+--- a/Makefile.linux
++++ b/Makefile.linux
+@@ -153,14 +153,18 @@ LDLIBS += $(shell $(PKG_CONFIG) --static
+ LDLIBS += $(shell $(PKG_CONFIG) --static --libs-only-l libnetfilter_conntrack)
+ endif # ($(TEST),1)
+ 
++ifeq ($(TARGET_OPENWRT),)
++# n/a - we don't enable https server for IGD v2 anyway in OpenWrt
+ LDLIBS += $(shell $(PKG_CONFIG) --static --libs-only-l libssl)
+ 
++# n/a - we hardcodedly support libuuid
+ TEST := $(shell $(PKG_CONFIG) --exists uuid && echo 1)
+ ifeq ($(TEST),1)
+ LDLIBS += $(shell $(PKG_CONFIG) --static --libs-only-l uuid)
+ else
+ $(info please install uuid-dev package / libuuid)
+ endif # ($(TEST),1)
++endif
+ 
+ TESTUPNPDESCGENOBJS = testupnpdescgen.o upnpdescgen.o
+ 

+ 10 - 0
net/miniupnpd/patches/102-ipv6-ext-port.patch

@@ -0,0 +1,10 @@
+--- a/pcpserver.c
++++ b/pcpserver.c
+@@ -982,6 +982,7 @@ static int CreatePCPMap_NAT(pcp_info_t *
+ 				   timestamp);
+ 	if (r < 0)
+ 		return PCP_ERR_NO_RESOURCES;
++	pcp_msg_info->ext_port = pcp_msg_info->int_port;
+ 	return PCP_SUCCESS;
+ }
+ 

+ 27 - 0
net/miniupnpd/patches/103-no-ipv6-autodetection.patch

@@ -0,0 +1,27 @@
+The miniupnpd makefile tries to autodetect iptables capabilities.
+This will incorrectly detect capabilities such as ipv6 support even though it is disabled for the target build.
+
+As the OpenWRT buildsystem already passes the right compile flags, we can skip the autodetection.
+
+
+--- a/netfilter/Makefile
++++ b/netfilter/Makefile
+@@ -38,8 +38,6 @@ endif
+ endif
+ endif
+ 
+-LIBS +=  /lib/libip4tc.so /lib/libip6tc.so
+-
+ all:	iptcrdr.o testiptcrdr iptpinhole.o \
+         testiptcrdr_peer testiptcrdr_dscp test_nfct_get
+ #        testiptpinhole
+--- a/Makefile.linux
++++ b/Makefile.linux
+@@ -73,7 +73,6 @@ CPPFLAGS += -DIPTABLES_143
+ endif
+ 
+ CFLAGS  += $(shell $(PKG_CONFIG) --cflags libiptc)
+-LDLIBS  += $(shell $(PKG_CONFIG) --static --libs-only-l libiptc)
+ LDFLAGS += $(shell $(PKG_CONFIG) --libs-only-L libiptc)
+ LDFLAGS += $(shell $(PKG_CONFIG) --libs-only-other libiptc)
+ else

+ 20 - 0
net/miniupnpd/patches/104-always-libuuid.patch

@@ -0,0 +1,20 @@
+As it turns out, the 'magic' libuuid/bsd uuid check just checks
+outside buildtree altogether for the uuid_generate. So we just
+hardcode it.
+
+--- a/genconfig.sh
++++ b/genconfig.sh
+@@ -367,12 +367,7 @@ case $FW in
+ esac
+ 
+ # UUID API
+-if grep uuid_create /usr/include/uuid.h > /dev/null 2>&1 ; then
+-	echo "#define BSD_UUID" >> ${CONFIGFILE}
+-fi
+-if grep uuid_generate /usr/include/uuid/uuid.h > /dev/null 2>&1 ; then
+-	echo "#define LIB_UUID" >> ${CONFIGFILE}
+-fi
++echo "#define LIB_UUID" >> ${CONFIGFILE}
+ 
+ # set V6SOCKETS_ARE_V6ONLY to 0 if it was not set above
+ if [ -z "$V6SOCKETS_ARE_V6ONLY" ] ; then