Przeglądaj źródła

ncd: add router example

ambrop7 12 lat temu
rodzic
commit
edd3faca44

+ 36 - 0
ncd/examples/router/README

@@ -0,0 +1,36 @@
+NCD Router Example
+
+-- Operation ---
+
+These are the NCD scripts I run on my home router.
+Three network interfaces are being configured:
+
+1. The LAN interface.
+The DHCP server is started for this interface.
+2. The Internet interface.
+This is a PPPoE interface with NAT.
+3. The ServerIf interface.
+This one behaves similarly to the LAN interface, except that there is no DHCP server.
+The intention is to put servers here so you can restrict communication not only between Internet and the servers,
+but also between LAN and the servers (though this configuration doesn't actually do the latter).
+
+Hosts on the LAN and ServerIf interfaces can access the Internet, and source NAT is used here.
+Additionally, it is possible to add port forwardings (DNAT) from the Internet interface to either
+of those two interfaces. These can be managed with the scripts {list,add,remove}-port-forwarding.
+The list of port forwarding is stored in the file /var/lib/ncd-port-forwardings.ncdvalue.
+However, you should NOT modify this file while NCD is running. You should not modify it at all, because
+NCD may accidentally overwrite your changes. Just use the scripts.
+
+Iptables is used to filter incoming connections from the Internet interface.
+Exceptions can be added; for example, there's a commented line in template network_internet_pppoe_preup which allows access to the local SSH server.
+To allow access to servers running on other hosts (LAN or ServerIf interface), a port forwarding should be added dynamically.
+
+-- Installation --
+
+The following pppd patch is required for PPPoE to work:
+https://code.google.com/p/ambro-gentoo-overlay/source/browse/trunk/net-dialup/ppp/files/pppd-configurable-paths.patch
+
+Copy ncd.conf to /etc/, and copy all other files here into a new directory /etc/ncd-network.
+Explanation: ncd.conf just loads network.ncdi, which is where the bulk of the configuration is defined.
+Make the {list,add,remove}-port-forwarding scripts executable. Additionally, if your NCD interpreter is not located at /usr/bin/badvpn-ncd,
+adjust the interpreter paths inside them.

+ 43 - 0
ncd/examples/router/add-port-forwarding

@@ -0,0 +1,43 @@
+#!/usr/bin/badvpn-ncd
+
+process main {
+    getargs() args;
+    value(args) args;
+
+    num_different(args.length, "4") bad_args;
+    If (bad_args) {
+        println("Usage: add-port-forwarding <protocol> <port_start> <port_end> <dest_addr>");
+        exit("1");
+    };
+
+    args->get("0") protocol;
+    args->get("1") port_start;
+    args->get("2") port_end;
+    args->get("3") dest_addr;
+
+    var("0") exit_status;
+
+    sys.request_client({"unix", "/run/ncd-control.socket"}) client;
+
+    var({"add-port-forwarding", protocol, port_start, port_end, dest_addr}) request_data;
+
+    client->request(request_data, "reply_handler", "finished_handler", {});
+}
+
+template reply_handler {
+    value(_reply.data) reply_data;
+    reply_data->get("0") status;
+    reply_data->get("1") text;
+
+    val_equal(status, "ok") is_ok;
+    If (is_ok) {
+        println(text);
+    } Else {
+        _caller.exit_status->set("1");
+        println("Error: ", text);
+    };
+}
+
+template finished_handler {
+    exit(_caller.exit_status);
+}

+ 60 - 0
ncd/examples/router/dhcp_server.ncdi

@@ -0,0 +1,60 @@
+include_guard "dhcp_server"
+
+template dhcp_server {
+    alias("_arg0") addr;
+    alias("_arg1") prefix;
+    alias("_arg2") range_start;
+    alias("_arg3") range_end;
+    alias("_arg4") routers;
+    alias("_arg5") dns_servers;
+
+    # Choose lease file.
+    concat("/var/lib/dhcp/dhcpd-", addr, ".leases") leases_file;
+
+    # Create leases file if it doesn't exist.
+    file_stat(leases_file) stat;
+    If (stat.succeeded) { print(); } Else {
+        file_write(leases_file, "");
+    };
+
+    # Create a temporary directory.
+    concat("/run/ncd-dhcp-server-", addr) run_dir;
+    run({"/bin/rm", "-rf", run_dir}, {});
+    run({"/bin/mkdir", run_dir}, {"/bin/rm", "-rf", run_dir});
+
+    # Compute path for dhcp.conf.
+    concat(run_dir, "/dhcp.conf") dhcp_conf_path;
+
+    # This is a template for dhcp.conf.
+    var("
+default-lease-time 43200;
+max-lease-time 43200;
+log-facility local7;
+ddns-update-style none;
+local-address <LOCAL_ADDRESS>;
+
+subnet <NETWORK> netmask <NETMASK> {
+        authoritative;
+        range <RANGE_START> <RANGE_END>;
+        option routers <ROUTERS>;
+        option domain-name-servers <DNS_SERVERS>;
+}
+"   ) config_template;
+
+    # Compute some of the variables.
+    ipv4_net_from_addr_and_prefix(addr, prefix) network;
+    ipv4_prefix_to_mask(prefix) netmask;
+    implode(", ", routers) routers_str;
+    implode(", ", dns_servers) dns_servers_str;
+
+    # Perform substitutions.
+    var({"<LOCAL_ADDRESS>", "<NETWORK>", "<NETMASK>", "<RANGE_START>", "<RANGE_END>", "<ROUTERS>", "<DNS_SERVERS>"}) regex;
+    var({addr, network, netmask, range_start, range_end, routers_str, dns_servers_str}) replace;
+    regex_replace(config_template, regex, replace) config_data;
+
+    # Write dhcp.conf.
+    file_write(dhcp_conf_path, config_data);
+
+    # Start dhcpd.
+    daemon({"/usr/sbin/dhcpd", "-f", "-cf", dhcp_conf_path, "-user", "dhcp", "-group", "dhcp", "--no-pid", "-lf", leases_file});
+}

+ 61 - 0
ncd/examples/router/list-port-forwardings

@@ -0,0 +1,61 @@
+#!/usr/bin/badvpn-ncd
+
+process main {
+    getargs() args;
+    value(args) args;
+
+    num_different(args.length, "0") bad_args;
+    If (bad_args) {
+        println("Usage: list-port-forwardings");
+        exit("1");
+    };
+
+    var("0") exit_status;
+
+    sys.request_client({"unix", "/run/ncd-control.socket"}) client;
+
+    var({"list-port-forwardings"}) request_data;
+
+    client->request(request_data, "reply_handler", "finished_handler", {});
+}
+
+template reply_handler {
+    value(_reply.data) reply_data;
+    reply_data->get("0") status;
+    reply_data->get("1") arg;
+
+    val_equal(status, "ok") is_ok;
+    If (is_ok) {
+        println("Protocol  Start  End    Destination");
+        Foreach (arg As entry) {
+            value(entry) entry;
+            entry->get("0") protocol;
+            entry->get("1") port_start;
+            entry->get("2") port_end;
+            entry->get("3") dest_addr;
+            call("append_spaces", {port_start, "5"}) fixed_start;
+            call("append_spaces", {port_end, "5"}) fixed_end;
+            println(protocol, "       ", fixed_start.result, "  ", fixed_end.result, "  ", dest_addr);
+        };
+    } Else {
+        _caller.exit_status->set("1");
+        println("Error: ", arg);
+    };
+}
+
+template finished_handler {
+    exit(_caller.exit_status);
+}
+
+template append_spaces {
+    alias("_arg0") input;
+    alias("_arg1") min_length;
+
+    value(input) result;
+    backtrack_point() point;
+    num_lesser(result.length, min_length) more;
+    If (more) {
+        result->append(" ");
+        point->go();
+    };
+}

+ 6 - 0
ncd/examples/router/ncd.conf

@@ -0,0 +1,6 @@
+include "/etc/ncd-network/network.ncdi"
+
+process main {
+    process_manager() mgr;
+    mgr->start("network_main", {});
+}

+ 356 - 0
ncd/examples/router/network.ncdi

@@ -0,0 +1,356 @@
+include_guard "network"
+
+include "pppoe.ncdi"
+include "dhcp_server.ncdi"
+include "unbound.ncdi"
+include "network_control_server.ncdi"
+include "port_forwarding.ncdi"
+
+template network_main {
+    log("notice", "NCD starting");
+    log_r("notice", "NCD stopped");
+
+    # Load ipv6 module so we can disable ipv6.
+    runonce({"/sbin/modprobe", "ipv6"});
+
+    # Set some sysctl's.
+    runonce({"/sbin/sysctl", "net.ipv4.ip_forward=1"});
+    runonce({"/sbin/sysctl", "net.ipv6.conf.all.disable_ipv6=1"});
+
+    # Setup iptables INPUT chain.
+    net.iptables.policy("filter", "INPUT", "ACCEPT", "ACCEPT");
+    net.iptables.append("filter", "INPUT", "-i", "lo", "-j", "ACCEPT");
+    net.iptables.append("filter", "INPUT", "-m", "conntrack", "--ctstate", "ESTABLISHED", "-j", "ACCEPT");
+
+    # Setup iptables OUTPUT chain.
+    net.iptables.policy("filter", "OUTPUT", "ACCEPT", "ACCEPT");
+
+    # Setup iptables FORWARD chain.
+    net.iptables.policy("filter", "FORWARD", "DROP", "ACCEPT");
+    net.iptables.append("filter", "FORWARD", "-m", "conntrack", "--ctstate", "ESTABLISHED", "-j", "ACCEPT");
+    net.iptables.append("filter", "FORWARD", "-m", "connmark", "--mark", "0x1/0x1", "-j", "ACCEPT");
+
+    # Create dependency scope.
+    depend_scope() depsc;
+
+    # Start processes.
+    process_manager() mgr;
+    mgr->start("network_lan", {});
+    mgr->start("network_serverif", {});
+    mgr->start("network_internet", {});
+    mgr->start("network_lan_internet_rules", {});
+    mgr->start("network_serverif_internet_rules", {});
+    mgr->start("network_lan_serverif_rules", {});
+    mgr->start("network_lan_dhcp_server", {});
+    mgr->start("network_unbound", {});
+    mgr->start("network_port_forwarding", {});
+    mgr->start("network_start_control_server", {});
+}
+
+template network_weak_hostmodel_rules {
+    alias("_arg0") dev;
+    alias("_arg1") addr;
+
+    concat("INPUT_hostmodel_drop_", dev) drop_chain;
+
+    net.iptables.newchain("filter", drop_chain);
+    net.iptables.append("filter", drop_chain, "-j", "DROP");
+    net.iptables.append("filter", "INPUT", "-d", addr, "!", "-i", dev, "-j", drop_chain);
+}
+
+template network_weak_hostmodel_exception {
+    alias("_arg0") dev;
+    alias("_arg1") match;
+
+    concat("INPUT_hostmodel_drop_", dev) drop_chain;
+
+    listfrom({"filter", drop_chain}, match, {"-j", "RETURN"}) args;
+    net.iptables.insert(args);
+}
+
+template network_lan {
+    alias("_caller") main;
+
+    # Some configuration.
+    var("enp1s0") dev;
+    var("192.168.111.1") addr;
+    var("24") prefix;
+    var("192.168.111.100") dhcp_start;
+    var("192.168.111.149") dhcp_end;
+
+    main.depsc->provide("lan_config");
+
+    # Wait for device, set up, wait for link.
+    net.backend.waitdevice(dev);
+    net.up(dev);
+    net.backend.waitlink(dev);
+
+    # Weak host model.
+    call("network_weak_hostmodel_rules", {dev, addr});
+
+    # Assign IP address.
+    net.ipv4.addr(dev, addr, prefix);
+
+    # Do SNAT for port forwardings when connections originate from the inside.
+    net.iptables.append("nat", "POSTROUTING", "-m", "connmark", "--mark", "0x2/0x2", "-j", "SNAT", "--to-source", addr, "--random");
+
+    main.depsc->provide("lan");
+}
+
+template network_serverif {
+    alias("_caller") main;
+
+    # Some configuration.
+    var("enp3s0") dev;
+    var("192.168.113.1") addr;
+    var("24") prefix;
+
+    main.depsc->provide("serverif_config");
+
+    # Wait for device, set up, wait for link.
+    net.backend.waitdevice(dev);
+    net.up(dev);
+    net.backend.waitlink(dev);
+
+    # Weak host model.
+    call("network_weak_hostmodel_rules", {dev, addr});
+
+    # Assign IP address.
+    net.ipv4.addr(dev, addr, prefix);
+
+    # Do SNAT for port forwardings when connections originate from the inside.
+    net.iptables.append("nat", "POSTROUTING", "-m", "connmark", "--mark", "0x4/0x4", "-j", "SNAT", "--to-source", addr, "--random");
+
+    main.depsc->provide("serverif");
+}
+
+template network_internet {
+    alias("_caller") main;
+
+    # Some configuration.
+    var("enp2s0") pppoe_dev;
+    var("MISSING") pppoe_username;
+    var("MISSING") pppoe_password;
+
+    # Wait for device, set up, wait for link.
+    net.backend.waitdevice(pppoe_dev);
+    net.up(pppoe_dev);
+    net.backend.waitlink(pppoe_dev);
+
+    log("notice", "PPPoE started");
+    log_r("notice", "PPPoE stopped");
+
+    # Start PPPoE.
+    call("pppoe", {pppoe_dev, pppoe_username, pppoe_password, "network_internet_pppoe_preup"}) pppoe;
+
+    # Grab configuration.
+    var(pppoe.ifname) dev;
+    var(pppoe.local_ip) addr;
+    var(pppoe.remote_ip) remote_addr;
+    var(pppoe.dns_servers) dns_servers;
+
+    to_string(dns_servers) dns_str;
+    log("notice", "PPPoE up dev=", dev, " local=", addr, " remote=", remote_addr, " dns=", dns_str);
+    log_r("notice", "PPPoE down");
+
+    # Add default route.
+    net.ipv4.route("0.0.0.0/0", remote_addr, "20", dev);
+
+    main.depsc->provide("internet");
+}
+
+template network_internet_pppoe_preup {
+    alias("_arg0") dev;
+    alias("_arg1") addr;
+    alias("_arg2") remote_ip;
+    alias("_arg3") dns_servers;
+
+    # Weak host model.
+    call("network_weak_hostmodel_rules", {dev, addr});
+
+    # Drop packets to this system, except some things.
+    net.iptables.newchain("filter", "INPUT_internet_drop");
+    #net.iptables.append("filter", "INPUT_internet_drop", "-p", "tcp", "--dport", "22", "-j", "RETURN");
+    net.iptables.append("filter", "INPUT_internet_drop", "-j", "DROP");
+    net.iptables.append("filter", "INPUT", "-i", dev, "-j", "INPUT_internet_drop");
+
+    # Do SNAT for packets going out.
+    net.iptables.append("nat", "POSTROUTING", "-o", dev, "-j", "SNAT", "--to-source", addr, "--random");
+
+    # Do MMS clamping.
+    net.iptables.append("mangle", "OUTPUT", "-o", dev, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu");
+    net.iptables.append("mangle", "FORWARD", "-o", dev, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu");
+}
+
+template network_lan_internet_rules {
+    alias("_caller") main;
+    main.depsc->depend({"lan"}) lan;
+    main.depsc->depend({"internet"}) internet;
+
+    # Add exception to weak host model of internet interface.
+    call("network_weak_hostmodel_exception", {internet.dev, {"-i", lan.dev}});
+    net.iptables.append("filter", "FORWARD", "-m", "conntrack", "--ctstate", "NEW", "-i", lan.dev, "-o", internet.dev, "-j", "ACCEPT");
+}
+
+template network_serverif_internet_rules {
+    alias("_caller") main;
+    main.depsc->depend({"serverif"}) serverif;
+    main.depsc->depend({"internet"}) internet;
+
+    # Allow traffic from LAN to Internet.
+    call("network_weak_hostmodel_exception", {internet.dev, {"-i", serverif.dev}});
+    net.iptables.append("filter", "FORWARD", "-m", "conntrack", "--ctstate", "NEW", "-i", serverif.dev, "-o", internet.dev, "-j", "ACCEPT");
+}
+
+template network_lan_serverif_rules {
+    alias("_caller") main;
+    main.depsc->depend({"lan"}) lan;
+    main.depsc->depend({"serverif"}) serverif;
+
+    # Allow traffic from serverif to LAN.
+    call("network_weak_hostmodel_exception", {serverif.dev, {"-i", lan.dev}});
+    net.iptables.append("filter", "FORWARD", "-m", "conntrack", "--ctstate", "NEW", "-i", lan.dev, "-o", serverif.dev, "-j", "ACCEPT");
+
+    # Allow traffic from LAN to serverif.
+    call("network_weak_hostmodel_exception", {lan.dev, {"-i", serverif.dev}});
+    net.iptables.append("filter", "FORWARD", "-m", "conntrack", "--ctstate", "NEW", "-i", serverif.dev, "-o", lan.dev, "-j", "ACCEPT");
+}
+
+template network_lan_dhcp_server {
+    alias("_caller") main;
+    main.depsc->depend({"lan"}) lan;
+
+    # Start DHCP server.
+    call("dhcp_server", {lan.addr, lan.prefix, lan.dhcp_start, lan.dhcp_end, {lan.addr}, {lan.addr}});
+}
+
+template network_unbound {
+    alias("_caller") main;
+    main.depsc->depend({"lan_config"}) lan_config;
+    main.depsc->depend({"serverif_config"}) serverif_config;
+
+    # Add DNS servers.
+    net.dns({"127.0.0.1"}, "20");
+
+    # Build configuration.
+    ipv4_net_from_addr_and_prefix(lan_config.addr, lan_config.prefix) lan_network;
+    ipv4_net_from_addr_and_prefix(serverif_config.addr, serverif_config.prefix) serverif_network;
+    var({
+        {lan_network, lan_config.prefix, "allow"},
+        {serverif_network, serverif_config.prefix, "allow"}
+    }) access_control_rules;
+
+    # Start Unbound.
+    call("unbound", {"lan", access_control_rules});
+}
+
+template network_port_forwarding {
+    alias("_caller") main;
+
+    # Start forwarding.
+    call("port_forwarding", {"/var/lib/ncd-port-forwardings.ncdvalue", "network_port_forwarding_rules"}) pf;
+
+    main.depsc->provide("port_forwarding");
+}
+
+template network_port_forwarding_rules {
+    alias("_caller.main") main;
+    alias("_arg0") protocol;
+    alias("_arg1") port_start;
+    alias("_arg2") port_end;
+    alias("_arg3") dest_addr;
+
+    # Get access to lan and serverif configuration.
+    main.depsc->depend({"lan_config"}) lan;
+    main.depsc->depend({"serverif_config"}) serverif;
+
+    # Wait for Internet interface.
+    main.depsc->depend({"internet"}) internet;
+
+    # Build port range string.
+    concat(port_start, ":", port_end) port_range;
+
+    # Add rules.
+    net.iptables.append("nat", "PREROUTING", "-d", internet.addr, "-p", protocol, "--dport", port_range, "-i", lan.dev, "-j", "CONNMARK", "--set-xmark", "0x3/0x3");
+    net.iptables.append("nat", "PREROUTING", "-d", internet.addr, "-p", protocol, "--dport", port_range, "-i", serverif.dev, "-j", "CONNMARK", "--set-xmark", "0x5/0x5");
+    net.iptables.append("nat", "PREROUTING", "-d", internet.addr, "-p", protocol, "--dport", port_range, "-i", internet.dev, "-j", "CONNMARK", "--set-xmark", "0x1/0x1");
+    net.iptables.append("nat", "PREROUTING", "-d", internet.addr, "-p", protocol, "--dport", port_range, "-j", "DNAT", "--to-destination", dest_addr);
+}
+
+template network_start_control_server {
+    alias("_caller") main;
+    main.depsc->depend({"lan_config"}) lan_config;
+
+    # Start control server.
+    call("network_control_server", {"/run/ncd-control.socket",
+        "network_control_list_port_forwardings",
+        "network_control_add_port_forwarding",
+        "network_control_remove_port_forwarding"});
+}
+
+template network_control_list_port_forwardings {
+    alias("_caller.main") main;
+
+    main.depsc->depend({"port_forwarding"}) port_forwarding;
+    var(port_forwarding.pf.map.keys) port_forwardings;
+}
+
+template network_control_add_port_forwarding {
+    alias("_caller.main") main;
+    alias("_arg0") protocol;
+    alias("_arg1") port_start;
+    alias("_arg2") port_end;
+    alias("_arg3") dest_addr;
+
+    var("") try_error_text;
+    try("network_verify_port_forwarding_try", {}) verify_try;
+
+    If (verify_try.succeeded) {
+        main.depsc->depend({"port_forwarding"}) port_forwarding;
+
+        call("port_forwarding_add", {"_caller.port_forwarding.pf", protocol, port_start, port_end, dest_addr}) call;
+        alias("call.succeeded") succeeded;
+        alias("call.error_text") error_text;
+    } Else {
+        var("false") succeeded;
+        alias("try_error_text") error_text;
+    } branch;
+
+    alias("branch.succeeded") succeeded;
+    alias("branch.error_text") error_text;
+}
+
+template network_control_remove_port_forwarding {
+    alias("_caller.main") main;
+    alias("_arg0") protocol;
+    alias("_arg1") port_start;
+    alias("_arg2") port_end;
+    alias("_arg3") dest_addr;
+
+    main.depsc->depend({"port_forwarding"}) port_forwarding;
+
+    call("port_forwarding_remove", {"_caller.port_forwarding.pf", protocol, port_start, port_end, dest_addr}) call;
+    alias("call.succeeded") succeeded;
+    alias("call.error_text") error_text;
+}
+
+template network_verify_port_forwarding_try {
+    alias("_caller") c;
+
+    c.main.depsc->depend({"lan_config"}) lan;
+    c.main.depsc->depend({"serverif_config"}) serverif;
+
+    net.ipv4.addr_in_network(c.dest_addr, lan.addr, lan.prefix) in_lan;
+    net.ipv4.addr_in_network(c.dest_addr, serverif.addr, serverif.prefix) in_serverif;
+
+    If (in_lan) {
+        print();
+    }
+    Elif (in_serverif) {
+        print();
+    }
+    Else {
+        c.try_error_text->set("Destination address does not belong to any permitted network.");
+        _try->assert("false");
+    };
+}

+ 96 - 0
ncd/examples/router/network_control_server.ncdi

@@ -0,0 +1,96 @@
+include_guard "network_control_server"
+
+template network_control_server {
+    alias("_arg0") socket_path;
+    alias("_arg1") template_list_port_forwardings;
+    alias("_arg2") template_add_port_forwarding;
+    alias("_arg3") template_remove_port_forwarding;
+
+    # Start request server.
+    sys.request_server({"unix", socket_path}, "network_control_server__request_handler", {});
+}
+
+template network_control_server__request_handler {
+    alias("_caller") server;
+
+    value(_request.data) data;
+
+    try("network_control_server__try", {});
+
+    _request->reply({"error", "Bad request."});
+    _request->finish();
+}
+
+template network_control_server__try {
+    alias("_caller._request") request;
+    alias("_caller.server") server;
+    alias("_caller.data") data;
+
+    val_equal(data.type, "list") a1;
+    _try->assert(a1);
+
+    num_greater_equal(data.length, "1") a2;
+    _try->assert(a2);
+
+    data->get("0") request_cmd;
+
+    val_equal(request_cmd, "list-port-forwardings") is_list_port_forwardings;
+    val_equal(request_cmd, "add-port-forwarding") is_add_port_forwarding;
+    val_equal(request_cmd, "remove-port-forwarding") is_remove_port_forwarding;
+    or(is_add_port_forwarding, is_remove_port_forwarding) is_add_remove_port_forwarding;
+
+    If (is_list_port_forwardings) {
+        num_equal(data.length, "1") a3;
+        _try->assert(a3);
+
+        call_with_caller_target(server.template_list_port_forwardings, {}, "server._caller") call;
+        request->reply({"ok", call.port_forwardings});
+    }
+    Elif (is_add_remove_port_forwarding) {
+        num_equal(data.length, "5") a4;
+        _try->assert(a4);
+
+        data->get("1") req_protocol;
+        data->get("2") req_port_start;
+        data->get("3") req_port_end;
+        data->get("4") req_dest_addr;
+
+        val_equal(req_protocol, "tcp") is_tcp;
+        val_equal(req_protocol, "udp") is_udp;
+        or(is_tcp, is_udp) a5;
+        _try->assert(a5);
+
+        parse_number(req_port_start) port_start;
+        _try->assert(port_start.succeeded);
+
+        parse_number(req_port_end) port_end;
+        _try->assert(port_end.succeeded);
+
+        num_lesser_equal(port_start, port_end) a6;
+        _try->assert(a6);
+
+        parse_ipv4_addr(req_dest_addr) dest_addr;
+        _try->assert(dest_addr.succeeded);
+
+        If (is_add_port_forwarding) {
+            call_with_caller_target(server.template_add_port_forwarding, {req_protocol, port_start, port_end, dest_addr}, "server._caller") call;
+            If (call.succeeded) {
+                request->reply({"ok", "Port forwarding added."});
+            } Else {
+                request->reply({"error", call.error_text});
+            };
+        } Else {
+            call_with_caller_target(server.template_remove_port_forwarding, {req_protocol, port_start, port_end, dest_addr}, "server._caller") call;
+            If (call.succeeded) {
+                request->reply({"ok", "Port forwarding removed."});
+            } Else {
+                request->reply({"error", call.error_text});
+            };
+        };
+    }
+    Else {
+        _try->assert("false");
+    };
+
+    request->finish();
+}

+ 170 - 0
ncd/examples/router/port_forwarding.ncdi

@@ -0,0 +1,170 @@
+include_guard "port_forwarding"
+
+template port_forwarding {
+    alias("_arg0") forwardings_file;
+    alias("_arg1") template_forward;
+
+    # Map which holds the set of current port forwardings.
+    # Enties are: {protocol, port_start, port_end, dest_addr}:""
+    value([]) map;
+
+    # Blocker which is initially down and is toggled down-up
+    # whenever the forwarding change.
+    blocker() update_blocker;
+
+    # Process manager, each forwarding has a port_forwarding__instance process.
+    # The process identifiers are the same as the keys in the map.
+    process_manager() mgr;
+
+    # Spawn a process for dealing with storage of port forwardings on disk.
+    spawn("port_forwarding__stored", {});
+}
+
+template port_forwarding__instance {
+    alias("_caller") pf;
+    alias("_arg0") protocol;
+    alias("_arg1") port_start;
+    alias("_arg2") port_end;
+    alias("_arg3") dest_addr;
+
+    log("notice", "adding port forwarding ", protocol, ":", port_start, ":", port_end, " to ", dest_addr);
+    log_r("notice", "removed port forwarding ", protocol, ":", port_start, ":", port_end, " to ", dest_addr);
+
+    # Do the forwarding.
+    call_with_caller_target(pf.template_forward, {protocol, port_start, port_end, dest_addr}, "pf._caller");
+}
+
+template port_forwarding_add {
+    alias(_arg0) pf;
+    alias("_arg1") protocol;
+    alias("_arg2") port_start;
+    alias("_arg3") port_end;
+    alias("_arg4") dest_addr;
+
+    var("false") succeeded;
+    var("") error_text;
+    var("true") not_finished;
+    backtrack_point() finished_point;
+
+    If (not_finished) {
+        # Check for conflicts with existing forwardings.
+        Foreach (pf.map.keys As entry) {
+            value(entry) entry;
+            entry->get("0") e_protocol;
+            entry->get("1") e_port_start;
+            entry->get("2") e_port_end;
+
+            val_different(protocol, e_protocol) different_protocol;
+            num_lesser(port_end, e_port_start) before;
+            num_greater(port_start, e_port_end) after;
+            or(different_protocol, before, after) no_conflict;
+            not(no_conflict) conflict;
+
+            If (conflict) {
+                error_text->set("Port forwarding conflicts with an existing forwarding.");
+                not_finished->set("false");
+                finished_point->go();
+            };
+        };
+
+        # Build entry key.
+        var({protocol, port_start, port_end, dest_addr}) key;
+
+        # Insert to map and toggle blocker.
+        pf.map->insert(key, "");
+        pf.update_blocker->downup();
+
+        # Start process.
+        pf.mgr->start(key, "port_forwarding__instance", {protocol, port_start, port_end, dest_addr});
+
+        succeeded->set("true");
+        not_finished->set("false");
+        finished_point->go();
+    };
+}
+
+template port_forwarding_remove {
+    alias(_arg0) pf;
+    alias("_arg1") protocol;
+    alias("_arg2") port_start;
+    alias("_arg3") port_end;
+    alias("_arg4") dest_addr;
+
+    var("false") succeeded;
+    var("") error_text;
+    var("true") not_finished;
+    backtrack_point() finished_point;
+
+    If (not_finished) {
+        # Build entry key.
+        var({protocol, port_start, port_end, dest_addr}) key;
+
+        # Check if the forwarding exists.
+        pf.map->try_get(key) entry;
+        not(entry.exists) does_not_exist;
+        If (does_not_exist) {
+            error_text->set("Port forwarding does not exist.");
+            not_finished->set("false");
+            finished_point->go();
+        };
+
+        # Stop process.
+        pf.mgr->stop(key);
+
+        # Remove from map and toggle blocker.
+        pf.map->remove(key);
+        pf.update_blocker->downup();
+
+        succeeded->set("true");
+        not_finished->set("false");
+        finished_point->go();
+    };
+}
+
+template port_forwarding__stored {
+    alias("_caller") pf;
+
+    # Create file if it doesn't exist.
+    file_stat(pf.forwardings_file) stat;
+    If (stat.succeeded) { print(); } Else {
+        file_write(pf.forwardings_file, "{}\n");
+    };
+
+    # Read port forwardings from file.
+    file_read(pf.forwardings_file) data;
+    from_string(data) forwardings;
+
+    # Add them.
+    Foreach (forwardings As fwd) {
+        value(fwd) fwd;
+        fwd->get("0") protocol;
+        fwd->get("1") port_start;
+        fwd->get("2") port_end;
+        fwd->get("3") dest_addr;
+        call("port_forwarding_add", {"_caller.pf", protocol, port_start, port_end, dest_addr});
+    };
+
+    # Write forwardings to file on exit.
+    imperative("<none>", {}, "port_forwarding__write", {}, "6000");
+
+    # Also write forwardings whenever they are changed.
+    pf.update_blocker->use();
+    call("port_forwarding__write", {});
+}
+
+template port_forwarding__write {
+    alias("_caller.pf") pf;
+
+    # Convert forwardings to string.
+    to_string(pf.map.keys) data;
+    concat(data, "\n") data;
+
+    # Build name of temporary file.
+    concat(pf.forwardings_file, ".new") temp_file;
+
+    # Write temporary file.
+    file_write(temp_file, data);
+
+    # Move to live file.
+    runonce({"/bin/mv", temp_file, pf.forwardings_file});
+}

+ 296 - 0
ncd/examples/router/pppoe.ncdi

@@ -0,0 +1,296 @@
+include_guard "pppoe"
+
+template pppoe {
+    alias("_arg0") dev;
+    alias("_arg1") username;
+    alias("_arg2") password;
+    alias("_arg3") pre_up_template;
+
+    # Choose which NCD interpreter will be used for the pppd event scripts.
+    var("/usr/local/badvpn/bin/badvpn-ncd") ncd_interpreter_path;
+
+    # Retry point here.
+    var("false") retrying;
+    backtrack_point() retry_point;
+    If (retrying) {
+        sleep("5000");
+    };
+    retrying->set("true");
+
+    # Create a temporary directory.
+    concat("/run/ncd-pppoe-", dev) run_dir;
+    run({"/bin/rm", "-rf", run_dir}, {});
+    run({"/bin/mkdir", run_dir}, {"/bin/rm", "-rf", run_dir});
+
+    # Build paths for pppd scripts and other files.
+    concat(run_dir, "/ncd-request.socket") socket_path;
+    concat(run_dir, "/pppoe.pid") pppoe_pid_path;
+    concat(run_dir, "/pap-secrets") pap_secrets_path;
+    concat(run_dir, "/chap-secrets") chap_secrets_path;
+    concat(run_dir, "/pppd2.tdb") pppdb_path;
+    concat(run_dir, "/resolv.conf") resolv_conf_path;
+    concat(run_dir, "/script-auth-up") path_auth_up;
+    concat(run_dir, "/script-auth-down") path_auth_down;
+    concat(run_dir, "/script-auth-fail") path_auth_fail;
+    concat(run_dir, "/script-ip-pre-up") path_ip_pre_up;
+    concat(run_dir, "/script-ip-up") path_ip_up;
+    concat(run_dir, "/script-ip-down") path_ip_down;
+    concat(run_dir, "/script-ipv6-up") path_ipv6_up;
+    concat(run_dir, "/script-ipv6-down") path_ipv6_down;
+    concat(run_dir, "/script-ipx-up") path_ipx_up;
+    concat(run_dir, "/script-ipx-down") path_ipx_down;
+
+    # Write secrets files.
+    call("pppoe__write_secrets", {pap_secrets_path, username, password});
+    call("pppoe__write_secrets", {chap_secrets_path, username, password});
+
+    # Write pppd scripts. These will contact us via the request socket.
+    call("pppoe__write_script", {"ip-pre-up", path_ip_pre_up});
+    call("pppoe__write_script", {"ip-up", path_ip_up});
+    call("pppoe__write_script", {"ip-down", path_ip_down});
+
+    # Build path arguments for pppd;
+    concat("pid-dir=", run_dir) arg_pid_dir;
+    concat("pap-secrets=", pap_secrets_path) arg_pap_secrets;
+    concat("chap-secrets=", chap_secrets_path) arg_chap_secrets;
+    concat("pppdb=", pppdb_path) arg_pppdb;
+    concat("resolv.conf=", resolv_conf_path) arg_resolv_conf;
+    concat("auth-up=", path_auth_up) arg_auth_up;
+    concat("auth-down=", path_auth_down) arg_auth_down;
+    concat("auth-fail=", path_auth_fail) arg_auth_fail;
+    concat("ip-pre-up=", path_ip_pre_up) arg_ip_pre_up;
+    concat("ip-up=", path_ip_up) arg_ip_up;
+    concat("ip-down=", path_ip_down) arg_ip_down;
+    concat("ipv6-up=", path_ipv6_up) arg_ipv6_up;
+    concat("ipv6-down=", path_ipv6_down) arg_ipv6_down;
+    concat("ipx-up=", path_ipx_up) arg_ipx_up;
+    concat("ipx-down=", path_ipx_down) arg_ipx_down;
+
+    # Create state variables and blockers. When the request server
+    # receives requests it will update those variables and blockers.
+    var("down") state;
+    var("") current_ifname;
+    var("") current_local_ip;
+    var("") current_remote_ip;
+    value({}) current_dns_servers;
+    blocker() ip_pre_up_blocker;
+    blocker() ip_pre_up_done_blocker;
+    blocker() ip_up_blocker;
+
+    # Start request server.
+    sys.request_server({"unix", socket_path}, "pppoe__request_handler", {});
+
+    # Start pppd.
+    sys.start_process({
+        "/usr/sbin/pppd", "nodetach", "plugin", "rp-pppoe.so", dev, "noipdefault", "hide-password",
+        "usepeerdns", "user", username,
+        "path", arg_pid_dir, "path", arg_pap_secrets, "path", arg_chap_secrets, "path", arg_pppdb,
+        "path", arg_resolv_conf,
+        "path", arg_auth_up, "path", arg_auth_down, "path", arg_auth_fail, "path", arg_ip_pre_up,
+        "path", arg_ip_up, "path", arg_ip_down, "path", arg_ipv6_up, "path", arg_ipv6_down,
+        "path", arg_ipx_up, "path", arg_ipx_down
+    }, "", ["deinit_kill_time":"2000"]) pppd;
+
+    # Start a process which will cause retrying when pppd dies.
+    spawn("pppoe__pppd_wait", {});
+
+    # Wait for ip-pre-up.
+    ip_pre_up_blocker->use();
+
+    # Grab the current state variables, so the user doesn't
+    # see any unexpected changes.
+    var(current_ifname) ifname;
+    var(current_local_ip) local_ip;
+    var(current_remote_ip) remote_ip;
+    var(current_dns_servers) dns_servers;
+
+    # Call pre-up callback template.
+    call_with_caller_target(pre_up_template, {ifname, local_ip, remote_ip, dns_servers}, "_caller");
+
+    # Allow pre-up script to terminate.
+    ip_pre_up_done_blocker->up();
+
+    # Wait for connection to go up.
+    ip_up_blocker->use();
+}
+
+template pppoe__pppd_wait {
+    # Wait for pppd to die.
+    _caller.pppd->wait();
+
+    # Retry.
+    _caller.retry_point->go();
+}
+
+template pppoe__write_secrets {
+    alias("_arg0") file_path;
+    alias("_arg1") username;
+    alias("_arg2") password;
+
+    # Escape username and password.
+    regex_replace(username, {"\""}, {"\\\""}) username_esc;
+    regex_replace(password, {"\""}, {"\\\""}) password_esc;
+
+    # Write empty file and chmod it.
+    file_write(file_path, "");
+    run({"/bin/chmod", "600", file_path}, {});
+
+    # Build contents.
+    concat("\"", username_esc, "\" * \"", password_esc, "\"\n") contents;
+
+    # Write file.
+    file_write(file_path, contents);
+}
+
+template pppoe__write_script {
+    alias("_arg0") event;
+    alias("_arg1") script_path;
+
+    # This string is an NCD script which will be run by pppd.
+    # When run, it will contact us via the request server,
+    # and the requests will be processed in pppoe__request_handler.
+    var("#!<NCD_INTERPRETER_PATH>
+
+process main {
+    # Hardcoded strings.
+    var(\"<EVENT>\") hardcoded_event;
+    var(\"<SOCKET>\") hardcoded_socket;
+
+    # Start timeout to kill us after some time if we don't manage
+    # to contact the server.
+    spawn(\"timeout_process\", {});
+
+    # Build event data map.
+    getargs() args;
+    value([\"EVENT\":hardcoded_event, \"ARGS\":args]) msg_map;
+    var({\"DEVICE\", \"IFNAME\", \"IPLOCAL\", \"IPREMOTE\", \"PEERNAME\", \"LOCALNAME\",
+         \"SPEED\", \"ORIG_UID\", \"PPPLOGNAME\", \"CONNECT_TIME\", \"BYTES_SENT\",
+         \"BYTES_RCVD\", \"LINKNAME\", \"DNS1\", \"DNS2\", \"WINS1\", \"WINS2\"}) var_names;
+    Foreach (var_names As var_name) {
+        getenv(var_name) env;
+        If (env.exists) {
+            msg_map->insert(var_name, env);
+        };
+    };
+
+    # Connect to socket.
+    sys.request_client({\"unix\", hardcoded_socket}) client;
+
+    # Send request.
+    client->request(msg_map, \"reply_handler\", \"finished_handler\", {});
+}
+
+template reply_handler {
+    print();
+}
+
+template finished_handler {
+    # Request was received by server, exit now.
+    exit(\"0\");
+}
+
+template timeout_process {
+    # Sleep some time.
+    sleep(\"5000\");
+    # Timed out, exit now.
+    exit(\"1\");
+}
+
+"   ) script_contents_template;
+
+    # Replace some constants in the script with the right values.
+    regex_replace(script_contents_template,
+                  {"<NCD_INTERPRETER_PATH>", "<EVENT>", "<SOCKET>"},
+                  {_caller.ncd_interpreter_path, event, _caller.socket_path}
+    ) script_contents;
+
+    # Write the script.
+    file_write(script_path, script_contents);
+
+    # Make it executable.
+    run({"/bin/chmod", "+x", script_path}, {});
+}
+
+template pppoe__request_handler {
+    alias("_caller") pppoe;
+
+    # Get event type from request.
+    value(_request.data) request;
+    request->get("EVENT") event;
+
+    # Match to known types.
+    val_equal(event, "ip-down") is_ip_down;
+    val_equal(event, "ip-pre-up") is_ip_pre_up;
+    val_equal(event, "ip-up") is_ip_up;
+
+    If (is_ip_down) {
+        # Set state.
+        pppoe.state->set("down");
+
+        # Set blockers down.
+        pppoe.ip_up_blocker->down();
+        pppoe.ip_pre_up_done_blocker->down();
+        pppoe.ip_pre_up_blocker->down();
+    }
+    Elif (is_ip_pre_up) {
+        # Expecting to be in "down" state here.
+        val_different(pppoe.state, "down") state_is_wrong;
+        If (state_is_wrong) {
+            pppoe.retry_point->go();
+            _request->finish();
+        };
+
+        # Get variables from request.
+        request->get("IFNAME") ifname;
+        request->get("IPLOCAL") local_ip;
+        request->get("IPREMOTE") remote_ip;
+        request->try_get("DNS1") dns1;
+        request->try_get("DNS2") dns2;
+
+        # Write variables.
+        pppoe.current_ifname->set(ifname);
+        pppoe.current_local_ip->set(local_ip);
+        pppoe.current_remote_ip->set(remote_ip);
+        pppoe.current_dns_servers->reset({});
+        If (dns1.exists) {
+            pppoe.current_dns_servers->insert(pppoe.current_dns_servers.length, dns1);
+        };
+        If (dns2.exists) {
+            pppoe.current_dns_servers->insert(pppoe.current_dns_servers.length, dns2);
+        };
+
+        # Set state.
+        pppoe.state->set("pre-up");
+
+        # Set ip-pre-up blocker up.
+        pppoe.ip_pre_up_blocker->up();
+
+        # Wait for pre-up to be finished. This causes the script contacting
+        # us to not return until then, and effectively delays pppd in setting
+        # the device up and calling the ip-up script.
+        pppoe.ip_pre_up_done_blocker->use();
+    }
+    Elif(is_ip_up) {
+        # Expecting to be in "pre-up" state here.
+        val_different(pppoe.state, "pre-up") state_is_wrong;
+        If (state_is_wrong) {
+            pppoe.retry_point->go();
+            _request->finish();
+        };
+
+        # Set state.
+        pppoe.state->set("up");
+
+        # Set ip-up blocker up.
+        pppoe.ip_up_blocker->up();
+    };
+
+    # Finish request.
+    _request->finish();
+}
+
+template pppoe__escapeshellarg {
+    alias("_arg0") input;
+    regex_replace(input, {"'"}, {"\\'"}) replaced;
+    concat("'", replaced, "'") result;
+}

+ 43 - 0
ncd/examples/router/remove-port-forwarding

@@ -0,0 +1,43 @@
+#!/usr/bin/badvpn-ncd
+
+process main {
+    getargs() args;
+    value(args) args;
+
+    num_different(args.length, "4") bad_args;
+    If (bad_args) {
+        println("Usage: remove-port-forwarding <protocol> <port_start> <port_end> <dest_addr>");
+        exit("1");
+    };
+
+    args->get("0") protocol;
+    args->get("1") port_start;
+    args->get("2") port_end;
+    args->get("3") dest_addr;
+
+    var("0") exit_status;
+
+    sys.request_client({"unix", "/run/ncd-control.socket"}) client;
+
+    var({"remove-port-forwarding", protocol, port_start, port_end, dest_addr}) request_data;
+
+    client->request(request_data, "reply_handler", "finished_handler", {});
+}
+
+template reply_handler {
+    value(_reply.data) reply_data;
+    reply_data->get("0") status;
+    reply_data->get("1") text;
+
+    val_equal(status, "ok") is_ok;
+    If (is_ok) {
+        println(text);
+    } Else {
+        _caller.exit_status->set("1");
+        println("Error: ", text);
+    };
+}
+
+template finished_handler {
+    exit(_caller.exit_status);
+}

+ 42 - 0
ncd/examples/router/unbound.ncdi

@@ -0,0 +1,42 @@
+include_guard "unbound"
+
+template unbound {
+    alias("_arg0") unique_id;
+    alias("_arg1") access_control_rules;
+
+    # Create a temporary directory.
+    concat("/run/ncd-unbound-", unique_id) run_dir;
+    run({"/bin/rm", "-rf", run_dir}, {});
+    run({"/bin/mkdir", run_dir}, {"/bin/rm", "-rf", run_dir});
+
+    # Compute path for unbound.conf.
+    concat(run_dir, "/unbound.conf") unbound_conf_path;
+
+    # This is a template for unbound.conf.
+    value("
+server:
+    verbosity: 1
+    do-ip4: yes
+    do-ip6: no
+    do-udp: yes
+    do-tcp: no
+    interface: 0.0.0.0
+    access-control: 127.0.0.0/8 allow
+"   ) config;
+
+    # Append access control rules.
+    Foreach (access_control_rules As rule) {
+        value(rule) rule;
+        rule->get("0") network;
+        rule->get("1") prefix;
+        rule->get("2") action;
+        concat("    access-control: ", network, "/", prefix, " ", action, "\n") line;
+        config->append(line);
+    };
+
+    # Write unbound.conf.
+    file_write(unbound_conf_path, config);
+
+    # Start unbound.
+    daemon({"/usr/sbin/unbound", "-d", "-c", unbound_conf_path});
+}