#!/usr/bin/ucode -R let fs = require("fs"); let ubus = require("ubus"); let uci = require("uci"); let fnv1a = function(s) { let h = 2166136261, i; for (i = 0; i < length(s); i++) { h ^= ord(s, i); h = (h * 16777619) & 0xffffffff; } return h; }; let slug = function(name) { return sprintf("%08x", fnv1a(name)); }; let host_veth = function(name) { return "vh-" + slug(name); }; let cont_veth = function(name) { return "vc-" + slug(name); }; let bh_host_veth = function(name) { return "bh-" + slug(name); }; let bh_cont_veth = function(name) { return "bc-" + slug(name); }; let bh_bridge = function(id) { return "bhr-" + slug(id); }; let accif = function(name) { return "ca" + slug(name); }; let gwif = function(name) { return "cg" + slug(name); }; let bhif = function(name) { return "cb" + slug(name); }; let bhseg = function(id) { return "cs" + slug(id); }; let contzone = function(name) { return "cz" + slug(name); }; let injail_dir = "/tmp/run/uxc-net"; let injail_path = function(name) { return injail_dir + "/" + name + ".network"; }; let shared_lock_path = injail_dir + "/.shared.lock"; let loopback_section = "config interface 'loopback'\n" + "\toption device 'lo'\n" + "\toption proto 'static'\n" + "\toption ipaddr '127.0.0.1'\n" + "\toption netmask '255.0.0.0'\n\n"; let call = function(object, method, data) { ubus.call({ object: object, method: method, data: data }); let err = ubus.error(); if (err) warn(sprintf("uxc-net: ubus %s.%s failed: %s\n", object, method, err)); return err ? -1 : 0; }; let pkg_reload = function(pkg) { ubus.call({ object: "service", method: "event", data: { type: "config.change", data: { package: pkg } } }); }; let quiet_call = function(object, method, data) { ubus.call({ object: object, method: method, data: data }); return ubus.error() ? -1 : 0; }; let network_reload = function() { return call("network", "reload", {}); }; let autonet_service = "uxc-net"; let icmpv6_types = [ "echo-request", "echo-reply", "destination-unreachable", "packet-too-big", "time-exceeded", "bad-header", "unknown-header-type", "router-solicitation", "neighbour-solicitation", "router-advertisement", "neighbour-advertisement", ]; let sidecar_path = function(name) { return "/tmp/run/uvol/.meta/uxc/" + name + ".annotations"; }; let state_dir = "/tmp/run/uvol/.meta/uxc/state"; let macstore_dir = function(name) { return state_dir + "/" + name; }; let macstore_path = function(name) { return macstore_dir(name) + "/macaddrs"; }; let rand_mac = function() { let f = fs.open("/dev/urandom", "r"); if (!f) return null; let b = f.read(6); f.close(); if (type(b) != "string" || length(b) != 6) return null; return sprintf("%02x:%02x:%02x:%02x:%02x:%02x", (ord(b, 0) & 0xfc) | 0x02, ord(b, 1), ord(b, 2), ord(b, 3), ord(b, 4), ord(b, 5)); }; let ensure_macs = function(name, roles) { let store = {}, out = {}, changed = false, raw, s, r, m; raw = fs.readfile(macstore_path(name)); if (raw) { try { s = json(raw); } catch (e) { s = null; } if (type(s) == "object") store = s; } for (r in roles) { if (!store[r]) { m = rand_mac(); if (m) { store[r] = m; changed = true; } } out[r] = store[r]; } if (changed) { fs.mkdir(state_dir, 0700); fs.mkdir(macstore_dir(name), 0700); let f = fs.open(macstore_path(name), "w"); if (f) { f.write(sprintf("%J\n", store)); f.close(); } } return out; }; let with_macs = function(dev, m, hostrole, peerrole) { if (m[hostrole]) dev.macaddr = m[hostrole]; if (m[peerrole]) dev.peer_macaddr = m[peerrole]; return dev; }; let veth_device = function(vc, m, hostrole, peerrole) { return with_macs({ type: "veth", peer_name: vc }, m, hostrole, peerrole); }; let jailed_interface = function(name, vc, jail_device, zone) { let ifc = { proto: "none", device: vc, jail: name, jail_device: jail_device }; if (zone) ifc.zone = zone; return ifc; }; let read_annotations = function(bundle, name) { let ann = {}, raw, side, cfg, s, k, v; if (bundle) { raw = fs.readfile(bundle + "/config.json"); if (!raw) return null; try { cfg = json(raw); } catch (e) { return null; } ann = cfg?.annotations ?? {}; } side = fs.readfile(sidecar_path(name)); if (side) { try { s = json(side); } catch (e) { s = null; } if (type(s) == "object") for (k, v in s) ann[k] = v; } return ann; }; let parse_attach = function(ann) { let attach = ann["org.openwrt.network.attach"]; if (attach == null || attach == "" || attach == "none") return { kind: "none" }; if (attach == "host") return { kind: "host" }; if (attach == "routed") return { kind: "routed" }; let m = match(attach, /^bridged:([A-Za-z0-9_]+)$/); if (m) return { kind: "bridged", network: m[1] }; return { kind: "unknown", raw: attach }; }; let parse_backhaul = function(ann) { let id = ann["org.openwrt.network.backhaul"]; if (id == null || id == "") return null; return { id: id, address: ann["org.openwrt.network.backhaul-address"] }; }; let iface_status = function(iface) { let r = ubus.call({ object: "network.interface." + iface, method: "status", data: {} }); if (ubus.error() || type(r) != "object") return null; return r; }; let uci_network_exists = function(net) { let cursor = uci.cursor(); if (!cursor || !cursor.load("network")) return false; return cursor.get("network", net) != null; }; let device_status = function(dev) { let d = ubus.call({ object: "network.device", method: "status", data: { name: dev } }); if (ubus.error() || type(d) != "object") return null; return d; }; let device_is_bridge = function(dev) { return type(device_status(dev)?.["bridge-members"]) == "array"; }; let bridge_vlan_ids = function(br) { return map(device_status(br)?.["bridge-vlans"] ?? [], function(v) { return v.id; }); }; let bridge_port = function(br, pvid, vlans) { let port = { device: br }, flags = [], have, v; if (pvid != null) push(flags, pvid + ":*"); if (length(vlans)) have = bridge_vlan_ids(br); for (v in vlans) { if (v.vid == pvid) { warn(sprintf("uxc-net: VLAN %d on '%s' is already the network's own VLAN\n", v.vid, br)); return null; } if (index(have, v.vid) < 0) { warn(sprintf("uxc-net: bridge '%s' has no VLAN %d\n", br, v.vid)); return null; } push(flags, v.vid + (v.tagged ? ":t" : "")); } if (length(flags)) port.vlans = flags; return port; }; let bridge_of = function(net, status, vlans) { let dev = status.device, m; if (type(dev) != "string") { warn(sprintf("uxc-net: network '%s' has no device to attach to\n", net)); return null; } if (device_is_bridge(dev)) return bridge_port(dev, null, vlans); m = match(dev, /^(.+)\.([0-9]+)$/); if (m && device_is_bridge(m[1])) return bridge_port(m[1], +m[2], vlans); warn(sprintf("uxc-net: device '%s' of network '%s' is not a bridge\n", dev, net)); return null; }; let append_injail = function(name, section) { let existed, f; fs.mkdir(injail_dir, 0755); existed = !!fs.stat(injail_path(name)); f = fs.open(injail_path(name), "a"); if (!f) return; if (!existed) f.write(loopback_section); f.write(section); f.close(); }; let render_section = function(iface, device, proto, opts) { let s = "config interface '" + iface + "'\n" + "\toption device '" + device + "'\n" + "\toption proto '" + proto + "'\n", k, v; for (k, v in opts) s += "\toption " + k + " '" + v + "'\n"; return s; }; let injail_proto = function(ann, kind) { let proto = ann["org.openwrt.network.proto"]; if (proto == null || proto == "") return kind == "routed" ? "static" : "dhcp"; if (proto == "static") return proto; if (proto == "dhcp") { if (kind != "routed") return proto; warn("uxc-net: routed cannot use proto 'dhcp': the /31 gateway link has no DHCP server\n"); return null; } warn(sprintf("uxc-net: unsupported org.openwrt.network.proto '%s'\n", proto)); return null; }; let bridged_static_opts = function(ann) { let addr = ann["org.openwrt.network.address"], opts; if (addr == null || index(addr, "/") < 0) { warn("uxc-net: bridged proto 'static' needs org.openwrt.network.address as address/prefix\n"); return null; } opts = { ipaddr: addr }; if (ann["org.openwrt.network.gateway"]) opts.gateway = ann["org.openwrt.network.gateway"]; if (ann["org.openwrt.network.dns"]) opts.dns = ann["org.openwrt.network.dns"]; return opts; }; let injail_proto6 = function(ann) { let proto6 = ann["org.openwrt.network.proto6"]; if (proto6 == null || proto6 == "" || proto6 == "none") return "none"; if (proto6 == "dhcpv6" || proto6 == "slaac" || proto6 == "static") return proto6; warn(sprintf("uxc-net: unsupported org.openwrt.network.proto6 '%s'\n", proto6)); return null; }; let injail6_section = function(ann) { let proto6 = injail_proto6(ann); let ifaceid = ann["org.openwrt.network.ip6ifaceid"]; let addr6 = ann["org.openwrt.network.address6"]; let opts = {}; if (!proto6) return null; if (proto6 == "none") return ""; if (proto6 == "static") { if (addr6 == null || index(addr6, "/") < 0) { warn("uxc-net: proto6 'static' needs org.openwrt.network.address6 as address/prefix\n"); return null; } opts.ip6addr = addr6; if (ann["org.openwrt.network.gateway6"]) opts.ip6gw = ann["org.openwrt.network.gateway6"]; return render_section("lan6", "eth0", "static", opts); } if (proto6 == "slaac") { opts.reqaddress = "none"; opts.reqprefix = "no"; } if (ifaceid) { if (!match(ifaceid, /^::/)) { warn(sprintf("uxc-net: org.openwrt.network.ip6ifaceid '%s' must have a zero network part\n", ifaceid)); return null; } opts.ip6ifaceid = ifaceid; } return render_section("lan6", "eth0", "dhcpv6", opts); }; let render_vlan_device = function(parent, vid) { return "config device\n" + "\toption type '8021q'\n" + "\toption name '" + parent + "." + vid + "'\n" + "\toption ifname '" + parent + "'\n" + "\toption vid '" + vid + "'\n"; }; let injail_vlan_section = function(ann, vid) { let addr = ann["org.openwrt.network.vlan." + vid + ".address"]; let proto = "none", opts = {}; if (addr != null && addr != "") { if (index(addr, "/") < 0) { warn(sprintf("uxc-net: org.openwrt.network.vlan.%d.address must be address/prefix\n", vid)); return null; } proto = "static"; opts.ipaddr = addr; } return render_vlan_device("eth0", vid) + render_section("vlan" + vid, "eth0." + vid, proto, opts); }; let injail_vlan_sections = function(ann, vlans) { let out = "", s, v; for (v in vlans) { if (!v.tagged) continue; s = injail_vlan_section(ann, v.vid); if (s == null) return null; out += s; } return out; }; let csv = function(val) { let list = [], i, parts; if (type(val) != "string") return list; parts = split(val, /[ ,]+/); for (i = 0; i < length(parts); i++) if (parts[i] != "") push(list, parts[i]); return list; }; let parse_vlans = function(ann) { let list = [], vids = [], tok, m, vid, tagged, i; for (tok in csv(ann["org.openwrt.network.vlans"])) { m = match(tok, /^([0-9]+)(:t)?$/); vid = m ? +m[1] : 0; if (vid < 1 || vid > 4094) { warn(sprintf("uxc-net: bad org.openwrt.network.vlans entry '%s'\n", tok)); return null; } tagged = m[2] != null; i = index(vids, vid); if (i >= 0 && list[i].tagged == tagged) continue; if (i >= 0) { warn(sprintf("uxc-net: VLAN %d is listed both tagged and untagged in org.openwrt.network.vlans\n", vid)); return null; } push(vids, vid); push(list, { vid: vid, tagged: tagged }); } return list; }; let zone_of = function(token) { let m = match(token, /^(vpn|container):([A-Za-z0-9_.]+)$/); if (m) return m[1] == "container" ? contzone(m[2]) : m[2]; return token; }; let ip2int = function(s) { let p = split(s ?? "", "."); if (length(p) != 4) return null; return (+p[0] * 16777216) + (+p[1] * 65536) + (+p[2] * 256) + (+p[3]); }; let range_add = function(ranges, ipint, plen) { let size, start; plen = +plen; if (ipint == null || plen < 0 || plen > 32) return; size = (plen == 0) ? 4294967296 : (1 << (32 - plen)); start = ipint - (ipint % size); push(ranges, [ start, start + size - 1 ]); }; let range_add_cidr = function(ranges, cidr) { let p = split(cidr ?? "", "/"); range_add(ranges, ip2int(p[0]), p[1] ?? 32); }; let published_ranges = function(ranges, shared) { let ifc, a; for (ifc in values(shared.interfaces)) for (a in ifc.ipaddr ?? []) range_add_cidr(ranges, a); }; let used_ranges = function(shared) { let ranges = []; let d = ubus.call({ object: "network.interface", method: "dump", data: {} }); let list = (type(d) == "object" && type(d.interface) == "array") ? d.interface : []; for (let intf in list) { for (let a in intf["ipv4-address"] ?? []) range_add(ranges, ip2int(a.address), a.mask); for (let r in intf.route ?? []) if (r.target != "0.0.0.0") range_add(ranges, ip2int(r.target), r.mask); } published_ranges(ranges, shared); return ranges; }; let pick_subnet = function(shared) { let used = used_ranges(shared); let avail = function(prefix) { let base = ip2int(prefix + ".0"); for (let r in used) if (base <= r[1] && r[0] <= base + 255) return false; return true; }; for (let x = 16; x <= 254; x++) if (avail(sprintf("192.168.%d", x))) return sprintf("192.168.%d", x); for (let a = 16; a <= 31; a++) for (let x = 0; x <= 255; x++) if (avail(sprintf("172.%d.%d", a, x))) return sprintf("172.%d.%d", a, x); for (let a = 0; a <= 255; a++) for (let x = 0; x <= 255; x++) if (avail(sprintf("10.%d.%d", a, x))) return sprintf("10.%d.%d", a, x); return null; }; let shared_lock = function() { let f; fs.mkdir(injail_dir, 0755); f = fs.open(shared_lock_path, "w"); if (!f || !f.lock("x")) { warn("uxc-net: cannot lock the shared network state\n"); return null; } return f; }; let shared_unlock = function(f) { if (!f) return; f.lock("u"); f.close(); }; let shared_read = function() { let d = ubus.call({ object: "service", method: "get_data", data: { name: autonet_service } }); let s = {}, cur; if (!ubus.error() && type(d) == "object") cur = d[autonet_service]?.["*"]; if (type(cur) == "object") s = cur; return { present: type(d?.[autonet_service]) == "object", devices: type(s["network-device"]) == "object" ? s["network-device"] : {}, interfaces: type(s["network-interface"]) == "object" ? s["network-interface"] : {}, }; }; let shared_empty = function(shared) { return !length(shared.devices) && !length(shared.interfaces); }; let collect_devices = function(inst, names) { let dev; if (type(inst?.["network-device"]) != "object") return; for (dev in keys(inst["network-device"])) names[dev] = true; }; let published_devices = function() { let d = ubus.call({ object: "service", method: "get_data", data: { type: "network-device" } }); let names = {}, svc_name, svc, inst_name, inst; if (ubus.error() || type(d) != "object") return names; for (svc_name, svc in d) { if (type(svc) != "object") continue; for (inst_name, inst in svc) { if (svc_name == autonet_service && inst_name == "*") continue; collect_devices(inst, names); } } return names; }; let shared_prune = function(shared, live) { let dead = [], br, dev, ifc_name, ifc; for (br, dev in shared.devices) { if (dev.type != "bridge") continue; dev.ports = filter(dev.ports ?? [], function(p) { return live[p] == true; }); if (!length(dev.ports)) push(dead, br); } for (br in dead) delete shared.devices[br]; dead = []; for (ifc_name, ifc in shared.interfaces) if (!shared.devices[ifc.device]) push(dead, ifc_name); for (ifc_name in dead) delete shared.interfaces[ifc_name]; }; let shared_bridge_ensure = function(shared, br, ipv6) { let dev = shared.devices[br]; if (dev) return dev; dev = { type: "bridge" }; if (ipv6 == false) dev.ipv6 = false; dev.bridge_empty = true; dev.ports = []; shared.devices[br] = dev; return dev; }; let shared_add_port = function(shared, br, port) { let dev = shared.devices[br]; if (index(dev.ports, port) < 0) push(dev.ports, port); sort(dev.ports); }; let autonet_nets = function(shared) { let nets = [], n, ifc; for (n, ifc in shared.interfaces) if (ifc.zone == n) push(nets, n); return sort(nets); }; let autonet_specs = function(nets) { let fw = [], dh = [], net; for (net in nets) { push(fw, { type: "zone", name: net, network: [ net ], input: "ACCEPT", output: "ACCEPT", forward: "REJECT", }); push(dh, { type: "dhcp", interface: net, start: 100, limit: 150, leasetime: "12h", dhcpv4: "server", }); } return { firewall: fw, dhcp: dh }; }; let shared_write = function(shared, before) { let specs, data; if (shared_empty(shared)) { if (!shared.present) return 0; quiet_call("service", "delete", { name: autonet_service }); return 1; } if (sprintf("%J", [ shared.devices, shared.interfaces ]) == before) return 0; specs = autonet_specs(autonet_nets(shared)); data = { "network-device": shared.devices, "network-interface": shared.interfaces, firewall: specs.firewall, dhcp: specs.dhcp, }; if (call("service", "set", { name: autonet_service, data: data })) return -1; return 1; }; let shared_snapshot = function(shared) { if (shared_empty(shared)) return null; return sprintf("%J", [ shared.devices, shared.interfaces ]); }; let autonet_create = function(shared, net, vh) { let br = "br-" + net; let subnet = pick_subnet(shared); if (!subnet) { warn(sprintf("uxc-net: no free subnet for network '%s'\n", net)); return -1; } shared_bridge_ensure(shared, br, null); shared_add_port(shared, br, vh); shared.interfaces[net] = { proto: "static", device: br, ipaddr: [ subnet + ".1/24" ], ip6assign: 64, ip6ifaceid: "::1", force_link: true, zone: net, }; return 0; }; let spec_new = function() { return { devices: {}, interfaces: {}, bridge_ports: {}, firewall: [], dhcp: [] }; }; let instance_publish = function(name, spec) { return call("service", "set_data", { name: name, instance: name, data: { "network-device": spec.devices, "network-interface": spec.interfaces, "bridge-port": spec.bridge_ports, firewall: spec.firewall, dhcp: spec.dhcp, } }); }; let instance_withdraw = function(name) { return quiet_call("service", "set_data", { name: name, instance: name, data: {} }); }; let instance_published = function(name) { let d = ubus.call({ object: "service", method: "get_data", data: { name: name, instance: name } }); let inst; if (ubus.error() || type(d) != "object") return false; inst = d[name]?.[name]; return type(inst) == "object" && length(inst) > 0; }; let bridged_up = function(name, ann, attach, m, spec, shared, owned) { let net = attach.network; let vh = host_veth(name), vc = cont_veth(name); let proto = injail_proto(ann, "bridged"), opts = {}; let section6 = injail6_section(ann); let vlans = parse_vlans(ann); let vlan_sections, on_demand, status, br; if (!proto || section6 == null || vlans == null) return 1; vlan_sections = injail_vlan_sections(ann, vlans); if (vlan_sections == null) return 1; if (proto == "static") { opts = bridged_static_opts(ann); if (!opts) return 1; } if (ann["org.openwrt.network.egress"] || ann["org.openwrt.network.ingress"]) warn("uxc-net: bridged takes no egress/ingress (L2 inherits the joined network's zone); ignoring\n"); on_demand = shared.interfaces[net] != null || index(owned, net) >= 0 || !uci_network_exists(net); if (on_demand && length(vlans)) { warn(sprintf("uxc-net: network '%s' is created on demand and has no VLANs to attach to\n", net)); return 1; } if (shared.interfaces[net]) { shared_add_port(shared, "br-" + net, vh); } else if (on_demand) { if (autonet_create(shared, net, vh)) return 1; } else { status = iface_status(net); if (!status) { warn(sprintf("uxc-net: network '%s' is not known to netifd\n", net)); return 1; } br = bridge_of(net, status, vlans); if (!br) return 1; spec.bridge_ports[vh] = br; } spec.devices[vh] = veth_device(vc, m, "h", "c"); spec.interfaces[accif(name)] = jailed_interface(name, vc, "eth0", net); append_injail(name, render_section("lan", "eth0", proto, opts)); if (section6 != "") append_injail(name, section6); if (vlan_sections != "") append_injail(name, vlan_sections); return 0; }; let routed_subnet = function(name, ann) { let h = 0, i, a, b, c; for (i = 0; i < length(name); i++) h = (h * 31 + ord(name, i)) & 0xffffff; a = 1 + ((h & 0xff) % 254); b = (h >> 8) & 0xff; c = ((h >> 16) & 0x7f) * 2; return { gw_cidr: ann["org.openwrt.network.gateway"] ?? sprintf("10.%d.%d.%d/31", a, b, c + 1), container: ann["org.openwrt.network.address"] ?? sprintf("10.%d.%d.%d", a, b, c), }; }; let fw_specs = function(name, ann, net, proto6) { let czone = contzone(name); let list = [], i, m, sub; push(list, { type: "zone", name: czone, network: [ gwif(name) ], input: "DROP", output: "ACCEPT", forward: "DROP", }); sub = csv(ann["org.openwrt.network.egress"]); for (i = 0; i < length(sub); i++) push(list, { type: "forwarding", src: czone, dest: zone_of(sub[i]), }); sub = csv(ann["org.openwrt.network.ingress"]); for (i = 0; i < length(sub); i++) { m = match(sub[i], /^([A-Za-z0-9_:]+):(tcp|udp)\/([0-9]+(-[0-9]+)?)$/); if (!m) { warn(sprintf("uxc-net: ignoring bad ingress '%s'\n", sub[i])); continue; } push(list, { type: "redirect", name: czone + "-in" + i, src: zone_of(m[1]), dest: czone, proto: m[2], src_dport: m[3], dest_ip: net.container, dest_port: m[3], target: "DNAT", }); } sub = csv(ann["org.openwrt.network.host"]); for (i = 0; i < length(sub); i++) { m = match(sub[i], /^(tcp|udp)\/([0-9]+(-[0-9]+)?)$/); if (!m) { warn(sprintf("uxc-net: ignoring bad host port '%s'\n", sub[i])); continue; } push(list, { type: "rule", name: czone + "-host" + i, src: czone, proto: m[1], dest_port: m[2], target: "ACCEPT", }); } push(list, { type: "rule", name: czone + "-dns", src: czone, proto: [ "tcp", "udp" ], dest_port: "53", target: "ACCEPT", }); if (proto6 != "none") { push(list, { type: "rule", name: czone + "-icmpv6", src: czone, proto: "icmp", family: "ipv6", icmp_type: icmpv6_types, limit: "1000/sec", target: "ACCEPT", }); } if (proto6 == "dhcpv6") { push(list, { type: "rule", name: czone + "-dhcpv6", src: czone, proto: "udp", family: "ipv6", dest_port: "547", target: "ACCEPT", }); } return list; }; let dhcp_specs = function(name) { return [ { type: "dhcp", interface: gwif(name), ra: "server", dhcpv6: "server", } ]; }; let routed_up = function(name, ann, m, spec) { let czone = contzone(name); let net = routed_subnet(name, ann); let vh = host_veth(name), vc = cont_veth(name); let gw_ip = split(net.gw_cidr, "/")[0]; let proto = injail_proto(ann, "routed"); let section6 = injail6_section(ann); if (!proto || section6 == null) return 1; spec.devices[vh] = veth_device(vc, m, "h", "c"); spec.interfaces[gwif(name)] = { proto: "static", device: vh, ipaddr: [ net.gw_cidr ], ip6assign: 64, ip6ifaceid: "::1", force_link: true, zone: czone, }; spec.interfaces[accif(name)] = jailed_interface(name, vc, "eth0", czone); spec.firewall = fw_specs(name, ann, net, injail_proto6(ann)); spec.dhcp = dhcp_specs(name); append_injail(name, render_section("lan", "eth0", proto, { ipaddr: net.container, netmask: "255.255.255.254", gateway: gw_ip, dns: gw_ip, })); if (section6 != "") append_injail(name, section6); return 0; }; let backhaul_up = function(name, bh, m, spec, shared) { let br = bh_bridge(bh.id); let iface = bhseg(bh.id); let vh = bh_host_veth(name), vc = bh_cont_veth(name); shared_bridge_ensure(shared, br, false); shared_add_port(shared, br, vh); if (!shared.interfaces[iface]) shared.interfaces[iface] = { proto: "none", device: br }; spec.devices[vh] = veth_device(vc, m, "bh", "bc"); spec.interfaces[bhif(name)] = jailed_interface(name, vc, "bh0", null); if (bh.address) append_injail(name, render_section("backhaul", "bh0", "static", { ipaddr: bh.address, netmask: "255.255.255.0", })); return 0; }; let shared_reconcile = function() { let lock = shared_lock(), shared, before, changed; if (!lock) return 0; shared = shared_read(); before = shared_snapshot(shared); shared_prune(shared, published_devices()); changed = shared_write(shared, before); shared_unlock(lock); return changed; }; let instance_rollback = function(name) { instance_withdraw(name); shared_reconcile(); }; let do_up = function(name, bundle) { let attach, bh, ann, roles, m, spec, shared, owned, before, lock, rc, written; if (!bundle) { warn("uxc-net: 'up' needs a bundle path\n"); return 1; } ann = read_annotations(bundle, name); if (ann == null) { warn("uxc-net: cannot read annotations from " + bundle + "/config.json\n"); return 1; } attach = parse_attach(ann); bh = parse_backhaul(ann); fs.unlink(injail_path(name)); if (attach.kind == "unknown") { warn(sprintf("uxc-net: attach '%s' not implemented\n", attach.raw ?? attach.kind)); return 1; } if (attach.kind != "bridged" && attach.kind != "routed" && !bh) return 0; roles = []; if (attach.kind == "bridged" || attach.kind == "routed") { push(roles, "h"); push(roles, "c"); } if (bh) { push(roles, "bh"); push(roles, "bc"); } m = ensure_macs(name, roles); spec = spec_new(); lock = shared_lock(); if (!lock) return 1; shared = shared_read(); owned = autonet_nets(shared); before = shared_snapshot(shared); shared_prune(shared, published_devices()); rc = 0; if (attach.kind == "bridged") rc = bridged_up(name, ann, attach, m, spec, shared, owned); else if (attach.kind == "routed") rc = routed_up(name, ann, m, spec); if (!rc && bh) rc = backhaul_up(name, bh, m, spec, shared); if (!rc && shared_write(shared, before) < 0) rc = 1; written = !rc; if (!rc && instance_publish(name, spec)) rc = 1; shared_unlock(lock); if (!rc && network_reload()) rc = 1; if (rc) { if (written) instance_rollback(name); return 1; } pkg_reload("firewall"); pkg_reload("dhcp"); return 0; }; let do_down = function(name) { let had, changed; fs.unlink(injail_path(name)); had = instance_published(name); if (had) instance_withdraw(name); changed = shared_reconcile(); if (!had && changed <= 0) return 0; network_reload(); pkg_reload("firewall"); pkg_reload("dhcp"); return 0; }; let name = ARGV[0]; let action = ARGV[1]; let bundle = ARGV[2]; if (!name || !action) { warn("usage: uxc-net [bundle]\n"); exit(22); } if (action == "up") exit(do_up(name, bundle)); else if (action == "down") exit(do_down(name)); warn(sprintf("uxc-net: unknown action '%s'\n", action)); exit(22);