diff --git a/utils/prometheus-node-exporter-ucode/Makefile b/utils/prometheus-node-exporter-ucode/Makefile new file mode 100644 index 0000000000..3d712c10a3 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/Makefile @@ -0,0 +1,74 @@ +# Copyright (C) 2013-2017 OpenWrt.org + +include $(TOPDIR)/rules.mk + +PKG_NAME:=prometheus-node-exporter-ucode +PKG_VERSION:=2022.12.02 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=Andre Heider +PKG_LICENSE:=Apache-2.0 + +include $(INCLUDE_DIR)/package.mk + +Build/Compile= + +define Package/$(PKG_NAME)/Default + SECTION:=utils + CATEGORY:=Utilities + TITLE:=Prometheus node exporter + PKGARCH:=all +endef + +define Package/$(PKG_NAME) + $(call Package/$(PKG_NAME)/Default) + DEPENDS:=+uhttpd +uhttpd-mod-ucode +rpcd +ucode-mod-fs +ucode-mod-ubus +endef + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/config $(1)/etc/config/$(PKG_NAME) + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/init $(1)/etc/init.d/$(PKG_NAME) + $(INSTALL_DIR) $(1)/usr/share/ucode/node-exporter/lib + $(INSTALL_DATA) ./files/metrics.uc $(1)/usr/share/ucode/node-exporter/ + $(INSTALL_DATA) ./files/base/*.uc $(1)/usr/share/ucode/node-exporter/lib/ + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/run.sh $(1)/usr/bin/$(PKG_NAME) +endef + +define Package/$(PKG_NAME)/conffiles +/etc/config/$(PKG_NAME) +endef + +define Package/$(PKG_NAME)/description + Provides node metrics as Prometheus scraping endpoint. + + This service is a lightweight rewrite in ucode of the offical Prometheus node_exporter. +endef + +$(eval $(call BuildPackage,prometheus-node-exporter-ucode)) + +define Collector + define Package/$(PKG_NAME)-$(1) + $$(call Package/$(PKG_NAME)/Default) + TITLE+= ($(2)) + DEPENDS:=$(PKG_NAME) $(3) + endef + + define Package/$(PKG_NAME)-$(1)/install + $$(INSTALL_DIR) $$(1)/usr/share/ucode/node-exporter/lib + $$(INSTALL_DATA) ./files/extra/$(1).uc $$(1)/usr/share/ucode/node-exporter/lib/ + endef + + $$(eval $$(call BuildPackage,$(PKG_NAME)-$(1))) +endef + +$(eval $(call Collector,dnsmasq,Dnsmasq collector,@dnsmasq)) +$(eval $(call Collector,ltq-dsl,Lantiq/Intel/MaxLinear DSL collector,@ltq-dsl-app)) +$(eval $(call Collector,netstat,netstat collector,)) +$(eval $(call Collector,openwrt,OpenWrt collector,)) +$(eval $(call Collector,snmp6,snmp6 collector,)) +$(eval $(call Collector,uci_dhcp_host,UCI DHCP host collector,)) +$(eval $(call Collector,wifi,Wi-Fi collector,+ucode-mod-nl80211)) +$(eval $(call Collector,wireguard,Wireguard collector,+rpcd-mod-wireguard)) diff --git a/utils/prometheus-node-exporter-ucode/files/base/conntrack.uc b/utils/prometheus-node-exporter-ucode/files/base/conntrack.uc new file mode 100644 index 0000000000..b3ee3f3bc9 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/conntrack.uc @@ -0,0 +1,4 @@ +gauge("node_nf_conntrack_entries") + (null, oneline("/proc/sys/net/netfilter/nf_conntrack_count")); +gauge("node_nf_conntrack_entries_limit") + (null, oneline("/proc/sys/net/netfilter/nf_conntrack_max")); diff --git a/utils/prometheus-node-exporter-ucode/files/base/cpu.uc b/utils/prometheus-node-exporter-ucode/files/base/cpu.uc new file mode 100644 index 0000000000..574655df8e --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/cpu.uc @@ -0,0 +1,44 @@ +let f = fs.open("/proc/stat"); + +if (!f) + return false; + +const desc = [ + null, + "user", + "nice", + "system", + "idle", + "iowait", + "irq", + "softirq", + "steal", + "guest", + "guest_nice", +]; +const m_cpu = counter("node_cpu_seconds_total"); + +let line; +while (line = nextline(f)) { + const x = wsplit(line); + + if (length(x) < 2) + continue; + + if (match(x[0], /^cpu\d+/)) { + const count = min(length(x), length(desc)); + for (let i = 1; i < count; i++) + m_cpu({ cpu: x[0], mode: desc[i] }, x[i] / 100.0); + } else if (x[0] == "intr") + counter("node_intr_total")(null, x[1]); + else if (x[0] == "ctxt") + counter("node_context_switches_total")(null, x[1]); + else if (x[0] == "btime") + gauge("node_boot_time_seconds")(null, x[1]); + else if (x[0] == "processes") + counter("node_forks_total")(null, x[1]); + else if (x[0] == "procs_running") + gauge("node_procs_running_total")(null, x[1]); + else if (x[0] == "procs_blocked") + gauge("node_procs_blocked_total")(null, x[1]); +} diff --git a/utils/prometheus-node-exporter-ucode/files/base/entropy.uc b/utils/prometheus-node-exporter-ucode/files/base/entropy.uc new file mode 100644 index 0000000000..2df44261a3 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/entropy.uc @@ -0,0 +1,4 @@ +gauge("node_entropy_available_bits") + (null, oneline("/proc/sys/kernel/random/entropy_avail")); +gauge("node_entropy_pool_size_bits") + (null, oneline("/proc/sys/kernel/random/poolsize")); diff --git a/utils/prometheus-node-exporter-ucode/files/base/filefd.uc b/utils/prometheus-node-exporter-ucode/files/base/filefd.uc new file mode 100644 index 0000000000..359cffd4eb --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/filefd.uc @@ -0,0 +1,7 @@ +const x = wsplit(oneline("/proc/sys/fs/file-nr")); + +if (length(x) < 3) + return false; + +gauge("node_filefd_allocated")(null, x[0]); +gauge("node_filefd_maximum")(null, x[2]); diff --git a/utils/prometheus-node-exporter-ucode/files/base/loadavg.uc b/utils/prometheus-node-exporter-ucode/files/base/loadavg.uc new file mode 100644 index 0000000000..ba67dafe34 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/loadavg.uc @@ -0,0 +1,8 @@ +const x = wsplit(oneline("/proc/loadavg")); + +if (length(x) < 3) + return false; + +gauge("node_load1")(null, x[0]); +gauge("node_load5")(null, x[1]); +gauge("node_load15")(null, x[2]); diff --git a/utils/prometheus-node-exporter-ucode/files/base/meminfo.uc b/utils/prometheus-node-exporter-ucode/files/base/meminfo.uc new file mode 100644 index 0000000000..3cecb124dc --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/meminfo.uc @@ -0,0 +1,24 @@ +let f = fs.open("/proc/meminfo"); + +if (!f) + return false; + +let line; +while (line = nextline(f)) { + const x = wsplit(line); + + if (length(x) < 2) + continue; + + if (substr(x[0], -1) != ":") + continue; + + let name; + if (substr(x[0], -2) == "):") + name = replace(substr(x[0], 0, -2), "(", "_"); + else + name = substr(x[0], 0, -1); + + gauge(`node_memory_${name}_bytes`) + (null, x[2] == "kB" ? x[1] * 1024 : x[1]); +} diff --git a/utils/prometheus-node-exporter-ucode/files/base/netclass.uc b/utils/prometheus-node-exporter-ucode/files/base/netclass.uc new file mode 100644 index 0000000000..10b3cfd343 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/netclass.uc @@ -0,0 +1,48 @@ +const root = "/sys/class/net/"; +const devices = fs.lsdir(root); + +if (length(devices) < 1) + return false; + +const m_info = gauge("node_network_info"); +const m_speed = gauge("node_network_speed_bytes"); +const metrics = { + addr_assign_type: gauge("node_network_address_assign_type"), + carrier: gauge("node_network_carrier"), + carrier_changes: counter("node_network_carrier_changes_total"), + carrier_down_count: counter("node_network_carrier_down_changes_total"), + carrier_up_count: counter("node_network_carrier_up_changes_total"), + dev_id: gauge("node_network_device_id"), + dormant: gauge("node_network_dormant"), + flags: gauge("node_network_flags"), + ifindex: gauge("node_network_iface_id"), + iflink: gauge("node_network_iface_link"), + link_mode: gauge("node_network_iface_link_mode"), + mtu: gauge("node_network_mtu_bytes"), + name_assign_type: gauge("node_network_name_assign_type"), + netdev_group: gauge("node_network_net_dev_group"), + type: gauge("node_network_protocol_type"), + tx_queue_len: gauge("node_network_transmit_queue_length"), +}; + +for (let device in devices) { + const devroot = root + device + "/"; + + m_info({ + device, + address: oneline(devroot + "address"), + broadcast: oneline(devroot + "broadcast"), + duplex: oneline(devroot + "duplex"), + operstate: oneline(devroot + "operstate"), + ifalias: oneline(devroot + "ifalias"), + }, 1); + + for (let m in metrics) { + let line = oneline(devroot + m); + metrics[m]({ device }, line); + } + + const speed = int(oneline(devroot + "speed")); + if (speed > 0) + m_speed({ device }, speed * 1000 * 1000 / 8); +} diff --git a/utils/prometheus-node-exporter-ucode/files/base/netdev.uc b/utils/prometheus-node-exporter-ucode/files/base/netdev.uc new file mode 100644 index 0000000000..f8fc68de44 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/netdev.uc @@ -0,0 +1,40 @@ +let f = fs.open("/proc/net/dev"); + +if (!f) + return false; + +const m = [ + null, + counter("node_network_receive_bytes_total"), + counter("node_network_receive_packets_total"), + counter("node_network_receive_errs_total"), + counter("node_network_receive_drop_total"), + counter("node_network_receive_fifo_total"), + counter("node_network_receive_frame_total"), + counter("node_network_receive_compressed_total"), + counter("node_network_receive_multicast_total"), + counter("node_network_transmit_bytes_total"), + counter("node_network_transmit_packets_total"), + counter("node_network_transmit_errs_total"), + counter("node_network_transmit_drop_total"), + counter("node_network_transmit_fifo_total"), + counter("node_network_transmit_colls_total"), + counter("node_network_transmit_carrier_total"), + counter("node_network_transmit_compressed_total"), +]; + +let line; +while (line = nextline(f)) { + const x = wsplit(ltrim(line), " "); + + if (length(x) < 2) + continue; + + if (substr(x[0], -1) != ":") + continue; + + const count = min(length(x), length(m)); + const labels = { device: substr(x[0], 0, -1) }; + for (let i = 1; i < count; i++) + m[i](labels, x[i]); +} diff --git a/utils/prometheus-node-exporter-ucode/files/base/selinux.uc b/utils/prometheus-node-exporter-ucode/files/base/selinux.uc new file mode 100644 index 0000000000..11840a8305 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/selinux.uc @@ -0,0 +1,10 @@ +const mode = oneline("/sys/fs/selinux/enforce"); +const enabled = gauge("node_selinux_enabled"); + +if (mode == null) { + enabled(null, 0); + return; +} + +enabled(null, 1); +gauge("node_selinux_current_mode")(null, mode); diff --git a/utils/prometheus-node-exporter-ucode/files/base/time.uc b/utils/prometheus-node-exporter-ucode/files/base/time.uc new file mode 100644 index 0000000000..7d13ea841a --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/time.uc @@ -0,0 +1 @@ +gauge("node_time_seconds")(null, time()); diff --git a/utils/prometheus-node-exporter-ucode/files/base/uname.uc b/utils/prometheus-node-exporter-ucode/files/base/uname.uc new file mode 100644 index 0000000000..50cb352ac1 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/base/uname.uc @@ -0,0 +1,8 @@ +gauge("node_uname_info")({ + sysname: oneline("/proc/sys/kernel/ostype"), + nodename: oneline("/proc/sys/kernel/hostname"), + release: oneline("/proc/sys/kernel/osrelease"), + version: oneline("/proc/sys/kernel/version"), + machine: poneline("uname -m"), // TODO lame + domainname: oneline("/proc/sys/kernel/domainname"), +}, 1); diff --git a/utils/prometheus-node-exporter-ucode/files/config b/utils/prometheus-node-exporter-ucode/files/config new file mode 100644 index 0000000000..8741f4a255 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/config @@ -0,0 +1,7 @@ +config prometheus-node-exporter-ucode 'main' + option listen_interface 'loopback' + option listen_port '9101' + option http_keepalive '70' + +config collector 'wifi' + option stations '1' diff --git a/utils/prometheus-node-exporter-ucode/files/extra/dnsmasq.uc b/utils/prometheus-node-exporter-ucode/files/extra/dnsmasq.uc new file mode 100644 index 0000000000..3644b20f29 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/extra/dnsmasq.uc @@ -0,0 +1,6 @@ +const x = ubus.call("dnsmasq", "metrics"); +if (!x) + return false; + +for (let i in x) + gauge(`dnsmasq_${i}_total`)(null, x[i]); diff --git a/utils/prometheus-node-exporter-ucode/files/extra/ltq-dsl.uc b/utils/prometheus-node-exporter-ucode/files/extra/ltq-dsl.uc new file mode 100644 index 0000000000..164449722e --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/extra/ltq-dsl.uc @@ -0,0 +1,72 @@ +const x = ubus.call("dsl", "metrics"); + +if (!x) + return false; + +gauge("dsl_info")({ + atuc_vendor: x.atu_c.vendor, + atuc_system_vendor: x.atu_c.system_vendor, + chipset: x.chipset, + firmware_version: x.firmware_version, + api_version: x.api_version, + driver_version: x.driver_version, +}, 1); + +gauge("dsl_line_info")({ + annex: x.annex, + standard: x.standard, + mode: x.mode, + profile: x.profile, +}, 1); + +gauge("dsl_up")({ detail: x.state }, x.up); +gauge("dsl_uptime_seconds")(null, x.uptime); + +gauge("dsl_line_attenuation_db") + ({ direction: "down" }, x.downstream.latn) + ({ direction: "up" }, x.upstream.latn); +gauge("dsl_signal_attenuation_db") + ({ direction: "down" }, x.downstream.satn) + ({ direction: "up" }, x.upstream.satn); +gauge("dsl_signal_to_noise_margin_db") + ({ direction: "down" }, x.downstream.snr) + ({ direction: "up" }, x.upstream.snr); +gauge("dsl_aggregated_transmit_power_db") + ({ direction: "down" }, x.downstream.actatp) + ({ direction: "up" }, x.upstream.actatp); + +if (x.downstream.interleave_delay) + gauge("dsl_latency_seconds") + ({ direction: "down" }, x.downstream.interleave_delay / 1000000.0) + ({ direction: "up" }, x.upstream.interleave_delay / 1000000.0); +gauge("dsl_datarate") + ({ direction: "down" }, x.downstream.data_rate) + ({ direction: "up" }, x.upstream.data_rate); +gauge("dsl_max_datarate") + ({ direction: "down" }, x.downstream.attndr) + ({ direction: "up" }, x.upstream.attndr); + +counter("dsl_error_seconds_total") + ({ err: "forward error correction", loc: "near" }, x.errors.near.fecs) + ({ err: "forward error correction", loc: "far" }, x.errors.far.fecs) + ({ err: "errored", loc: "near" }, x.errors.near.es) + ({ err: "errored", loc: "far" }, x.errors.far.es) + ({ err: "severely errored", loc: "near" }, x.errors.near.ses) + ({ err: "severely errored", loc: "far" }, x.errors.far.ses) + ({ err: "loss of signal", loc: "near" }, x.errors.near.loss) + ({ err: "loss of signal", loc: "far" }, x.errors.far.loss) + ({ err: "unavailable", loc: "near" }, x.errors.near.uas) + ({ err: "unavailable", loc: "far" }, x.errors.far.uas); + +counter("dsl_errors_total") + ({ err: "header error code error", loc: "near" }, x.errors.near.hec) + ({ err: "header error code error", loc: "far" }, x.errors.far.hec) + ({ err: "non pre-emptive crc error", loc: "near" }, x.errors.near.crc_p) + ({ err: "non pre-emptive crc error", loc: "far" }, x.errors.far.crc_p) + ({ err: "pre-emptive crc error", loc: "near" }, x.errors.near.crcp_p) + ({ err: "pre-emptive crc error", loc: "far" }, x.errors.far.crcp_p); + +if (x.erb) + counter("dsl_erb_total") + ({ counter: "sent" }, x.erb.sent) + ({ counter: "discarded" }, x.erb.discarded); diff --git a/utils/prometheus-node-exporter-ucode/files/extra/netstat.uc b/utils/prometheus-node-exporter-ucode/files/extra/netstat.uc new file mode 100644 index 0000000000..7449305145 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/extra/netstat.uc @@ -0,0 +1,30 @@ +function parse(fn) { + let f = fs.open(fn); + + if (!f) + return false; + + let names, values; + while (names = nextline(f), values = nextline(f)) { + const name = wsplit(names); + const value = wsplit(values); + + if (name[0] != value[0]) + continue; + + if (length(name) != length(value)) + continue; + + let prefix = substr(name[0], 0, -1); + for (let i = 1; i < length(name); i++) + gauge(`node_netstat_${prefix}_${name[i]}`)(null, value[i]); + } + + return true; +} + +let n = parse("/proc/net/netstat"); +let s = parse("/proc/net/snmp"); + +if (!n && !s) + return false; diff --git a/utils/prometheus-node-exporter-ucode/files/extra/openwrt.uc b/utils/prometheus-node-exporter-ucode/files/extra/openwrt.uc new file mode 100644 index 0000000000..10c15a10ca --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/extra/openwrt.uc @@ -0,0 +1,14 @@ +const x = ubus.call("system", "board"); + +if (!x) + return false; + +gauge("node_openwrt_info")({ + board_name: x.board_name, + id: x.release.distribution, + model: x.model, + release: x.release.version, + revision: x.release.revision, + system: x.system, + target: x.release.target, +}, 1); diff --git a/utils/prometheus-node-exporter-ucode/files/extra/snmp6.uc b/utils/prometheus-node-exporter-ucode/files/extra/snmp6.uc new file mode 100644 index 0000000000..d440a889e2 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/extra/snmp6.uc @@ -0,0 +1,23 @@ +function parse(fn, device, skipdecl) { + let f = fs.open(fn); + + if (!f) + return false; + + const labels = { device }; + let line; + while (line = nextline(f)) { + const x = wsplit(line); + + if (length(x) < 2) + continue; + + counter(`snmp6_${x[0]}`, null, skipdecl)(labels, x[1]); + } +} + +parse("/proc/net/snmp6", "all"); + +const root = "/proc/net/dev_snmp6/"; +for (let device in fs.lsdir(root)) + parse(root + device, device, true); diff --git a/utils/prometheus-node-exporter-ucode/files/extra/uci_dhcp_host.uc b/utils/prometheus-node-exporter-ucode/files/extra/uci_dhcp_host.uc new file mode 100644 index 0000000000..0d55724f97 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/extra/uci_dhcp_host.uc @@ -0,0 +1,14 @@ +import { cursor } from "uci"; + +const uci = cursor(); +uci.load("dhcp"); + +let m = gauge("dhcp_host_info"); + +uci.foreach('dhcp', `host`, (s) => { + m({ + name: s.name, + mac: s.mac, + ip: s.ip, + }, 1); +}); diff --git a/utils/prometheus-node-exporter-ucode/files/extra/wifi.uc b/utils/prometheus-node-exporter-ucode/files/extra/wifi.uc new file mode 100644 index 0000000000..f5f58ef687 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/extra/wifi.uc @@ -0,0 +1,118 @@ +import { request, 'const' as wlconst } from 'nl80211'; + +const x = ubus.call("network.wireless", "status"); + +if (!x) + return false; + +const iftypes = [ + "Unknown", + "Ad-Hoc", + "Client", + "Master", + "Master (VLAN)", + "WDS", + "Monitor", + "Mesh Point", + "P2P Client", + "P2P Go", + "P2P Device", + "OCB", +]; + +let m_radio_info = gauge("wifi_radio_info"); +let m_network_info = gauge("wifi_network_info"); +let m_network_quality = gauge("wifi_network_quality"); +let m_network_bitrate = gauge("wifi_network_bitrate"); +let m_network_noise = gauge("wifi_network_noise_dbm"); +let m_network_signal = gauge("wifi_network_signal_dbm"); +let m_stations_total = counter("wifi_stations_total"); +let m_station_inactive = gauge("wifi_station_inactive_milliseconds"); +let m_station_rx_bytes = counter("wifi_station_receive_bytes_total"); +let m_station_tx_bytes = counter("wifi_station_transmit_bytes_total"); +let m_station_rx_packets = counter("wifi_station_receive_packets_total"); +let m_station_tx_packets = counter("wifi_station_transmit_packets_total"); +let m_station_signal = gauge("wifi_station_signal_dbm"); +let m_station_rx_bitrate = gauge("wifi_station_receive_kilobits_per_second"); +let m_station_tx_bitrate = gauge("wifi_station_transmit_kilobits_per_second"); +let m_station_exp_tp = gauge("wifi_station_expected_throughput_kilobits_per_second"); + +for (let radio in x) { + const rc = x[radio]["config"]; + + m_radio_info({ + radio, + htmode: rc["htmode"], + channel: rc["channel"], + country: rc["country"], + } ,1); + + for (let iface in x[radio]["interfaces"]) { + const ifname = iface["ifname"]; + const nc = iface["config"]; + const wif = request(wlconst.NL80211_CMD_GET_INTERFACE, 0, { dev: ifname }); + + if (!wif) + continue; + + m_network_info({ + radio, + ifname, + ssid: nc["ssid"] || nc["mesh_id"], + bssid: wif["mac"], + mode: iftypes[wif["iftype"]], + }, 1); + + const wsta = request(wlconst.NL80211_CMD_GET_STATION, wlconst.NLM_F_DUMP, { dev: ifname }); + let signal = 0; + let bitrate = 0; + const stations = length(wsta) || 0; + if (stations) { + for (let sta in wsta) { + signal += sta["sta_info"].signal; + bitrate += sta["sta_info"]["tx_bitrate"].bitrate32; + } + bitrate /= stations * 0.01; + signal /= stations; + } + + let labels = { radio, ifname }; + m_network_bitrate(labels, bitrate || NaN); + m_network_signal(labels, signal || NaN); + m_network_quality(labels, signal ? 100.0 / 70 * (signal + 110) : NaN); + + const wsur = request(wlconst.NL80211_CMD_GET_SURVEY, wlconst.NLM_F_DUMP, { dev: ifname }); + let noise = 0; + for (let i in wsur) { + if (i["survey_info"]["frequency"] != wif["wiphy_freq"]) + continue; + + noise = i["survey_info"]["noise"]; + break; + } + + m_network_noise(labels, noise || NaN); + + if (config["stations"] != "1") + continue; + + m_stations_total(labels, stations); + if (!stations) + continue; + + for (let sta in wsta) { + labels["mac"] = sta["mac"]; + const info = sta["sta_info"]; + + m_station_inactive(labels, info["inactive_time"]); + m_station_rx_bytes(labels, info["rx_bytes64"]); + m_station_tx_bytes(labels, info["tx_bytes64"]); + m_station_rx_packets(labels, info["rx_packets"]); + m_station_tx_packets(labels, info["tx_packets"]); + m_station_signal(labels, info["signal"]); + m_station_rx_bitrate(labels, info["rx_bitrate"]["bitrate32"] * 100); + m_station_tx_bitrate(labels, info["tx_bitrate"]["bitrate32"] * 100); + m_station_exp_tp(labels, info["expected_throughput"]); + } + } +} diff --git a/utils/prometheus-node-exporter-ucode/files/extra/wireguard.uc b/utils/prometheus-node-exporter-ucode/files/extra/wireguard.uc new file mode 100644 index 0000000000..12ae56af91 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/extra/wireguard.uc @@ -0,0 +1,49 @@ +import { cursor } from "uci"; + +const x = ubus.call("wireguard", "status"); +if (!x) + return false; + +const uci = cursor(); +uci.load("network"); + +let m_wg_iface_info = gauge("wireguard_interface_info"); +let m_wg_peer_info = gauge("wireguard_peer_info"); +let m_wg_handshake = gauge ("wireguard_latest_handshake_seconds"); +let m_wg_rx = gauge ("wireguard_received_bytes_total"); +let m_wg_tx = gauge ("wireguard_sent_bytes_total"); + +for (let iface in x) { + const wc = x[iface]; + + m_wg_iface_info({ + name: iface, + public_key: wc["public_key"], + listen_port: wc["listen_port"], + fwmark: wc["fwmark"] || NaN, + }, 1); + + for (let peer in wc["peers"]) { + let description; + uci.foreach('network', `wireguard_${iface}`, (s) => { + if (s.public_key == peer) + description = s.description; + }); + + const pc = wc["peers"][peer]; + + m_wg_peer_info({ + interface: iface, + public_key: peer, + description, + endpoint: pc["endpoint"], + persistent_keepalive_interval: pc["persistent_keepalive_interval"] || NaN, + }, 1); + + const labels = { public_key: peer }; + + m_wg_handshake(labels, pc["last_handshake"]); + m_wg_rx(labels, pc["rx_bytes"]); + m_wg_tx(labels, pc["tx_bytes"]); + } +} diff --git a/utils/prometheus-node-exporter-ucode/files/init b/utils/prometheus-node-exporter-ucode/files/init new file mode 100644 index 0000000000..1735236f72 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/init @@ -0,0 +1,73 @@ +#!/bin/sh /etc/rc.common +# Copyright (C) 2013-2017 OpenWrt.org + +START=60 +USE_PROCD=1 + +_log() { + logger -p daemon.info -t prometheus-node-exporter-ucode "$@" +} + +start_service() { + . /lib/functions/network.sh + + local interface port bind4 bind6 + + config_load prometheus-node-exporter-ucode.main + config_get interface "main" listen_interface "loopback" + config_get port "main" listen_port 9101 + config_get keepalive "main" http_keepalive 70 + + [ "$interface" = "*" ] || { + network_get_ipaddr bind4 "$interface" + network_get_ipaddr6 bind6 "$interface" + [ -n "$bind4$bind6" ] || { + _log "defering start until listen interface $interface becomes ready" + return 0 + } + } + + procd_open_instance + + procd_set_param command /usr/sbin/uhttpd -f -c /dev/null -h /dev/null -S -D -o /metrics -O /usr/share/ucode/node-exporter/metrics.uc + + if [ "$interface" = "*" ]; then + procd_append_param command -p $port + else + [ -n "$bind4" ] && procd_append_param command -p $bind4:$port + [ -n "$bind6" ] && procd_append_param command -p [$bind6]:$port + fi + [ $keepalive -gt 0 ] && procd_append_param command -k $keepalive + + procd_add_jail prometheus-node-exporter-ucode log procfs sysfs ubus + procd_add_jail_mount "/usr/lib/uhttpd_ucode.so" + procd_add_jail_mount "/lib/libubus.so*" + procd_add_jail_mount "/lib/libuci.so" + procd_add_jail_mount "/usr/lib/ucode" + procd_add_jail_mount "/usr/lib/libnl*.so*" + procd_add_jail_mount "/usr/share/ucode/node-exporter" + procd_add_jail_mount "/etc/config" + + # TODO breaks the dsl collector? + #procd_set_param user nobody + #procd_set_param group nogroup + procd_set_param no_new_privs 1 + + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param respawn + + procd_close_instance +} + +service_triggers() +{ + local interface + + procd_add_reload_trigger "prometheus-node-exporter-ucode" + + config_load prometheus-node-exporter-ucode.main + config_get interface "main" listen_interface "loopback" + + [ "$interface" = "*" ] || procd_add_reload_interface_trigger "$interface" +} diff --git a/utils/prometheus-node-exporter-ucode/files/metrics.uc b/utils/prometheus-node-exporter-ucode/files/metrics.uc new file mode 100644 index 0000000000..3dce77accb --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/metrics.uc @@ -0,0 +1,227 @@ +{% +'use strict'; + +import * as fs from "fs"; +import { connect } from "ubus"; +import { cursor } from "uci"; + +function debug(...s) { + if (global.debug) + warn("DEBUG: ", ...s, "\n"); +} + +function puts(...s) { + return uhttpd.send(...s, "\n"); +} + +function govalue(value) { + if (value == Infinity) + return "+Inf"; + else if (value == -Infinity) + return "-Inf"; + else if (value != value) + return "NaN"; + else if (type(value) in [ "int", "double" ]) + return value; + else if (type(value) in [ "bool", "string" ]) + return +value; + + return null; +} + +function metric(name, mtype, help, skipdecl) { + let func; + let decl = skipdecl == true ? false : true; + + let yield = function(labels, value) { + let v = govalue(value); + + if (v == null) { + debug(`skipping metric: unsupported value '${value}' (${name})`); + return func; + } + + let labels_str = ""; + if (length(labels)) { + let sep = ""; + let s; + labels_str = "{"; + for (let l in labels) { + if (labels[l] == null) + s = ""; + else if (type(labels[l]) == "string") { + s = labels[l]; + s = replace(labels[l], "\\", "\\\\"); + s = replace(s, "\"", "\\\""); + s = replace(s, "\n", "\\n"); + } else { + s = govalue(labels[l]); + + if (!s) + continue; + } + + labels_str += sep + l + "=\"" + s + "\""; + sep = ","; + } + labels_str += "}"; + } + + if (decl) { + if (help) + puts("# HELP ", name, " ", help); + puts("# TYPE ", name, " ", mtype); + decl = false; + } + + puts(name, labels_str, " ", v); + return func; + }; + + func = yield; + return func; +} + +function counter(name, help, skipdecl) { + return metric(name, "counter", help, skipdecl); +} + +function gauge(name, help, skipdecl) { + return metric(name, "gauge", help, skipdecl); +} + +function httpstatus(status) { + puts("Status: ", status, "\nContent-Type: text/plain; version=0.0.4; charset=utf-8\n"); +} + +function clockdiff(t1, t2) { + return (t2[0] - t1[0]) * 1000000000 + t2[1] - t1[1]; +} + +let collectors = {}; + +global.handle_request = function(env) { + let scope = { + config: null, + fs, + ubus: connect(), + counter, + gauge, + wsplit: function(line) { + return split(line, /\s+/); + }, + nextline: function(f) { + return rtrim(f.read("line"), "\n"); + }, + oneline: function(fn) { + let f = fs.open(fn); + + if (!f) + return null; + + return nextline(f); + }, + poneline: function(cmd) { + let f = fs.popen(cmd); + + if (!f) + return null; + + return nextline(f); + }, + }; + + if (length(collectors) < 1) { + httpstatus("404 No Collectors found"); + return; + } + + let cols = []; + for (let q in split(env.QUERY_STRING, "&")) { + let s = split(q, "=", 2); + if (length(s) == 2 && s[0] == "collect") { + if (!(s[1] in collectors)) { + httpstatus(`404 Collector ${s[1]} not found`); + return; + } + + push(cols, s[1]); + } + } + + if (length(cols) > 0) + cols = uniq(cols); + else + cols = keys(collectors); + + httpstatus("200 OK"); + + let duration = gauge("node_scrape_collector_duration_seconds"); + let success = gauge("node_scrape_collector_success"); + + for (let col in cols) { + let ok = false; + let t1, t2; + + scope["config"] = collectors[col].config; + t1 = clock(true); + try { + ok = call(collectors[col].func, null, scope) != false; + } catch(e) { + warn(`error running collector '${col}':\n${e.message}\n`); + } + t2 = clock(true); + + duration({ collector: col }, clockdiff(t1, t2) / 1000000000.0); + success({ collector: col }, ok); + } +}; + +const lib = "/usr/share/ucode/node-exporter/lib"; +const opts = { + strict_declarations: true, + raw_mode: true, +}; + +let cols = fs.lsdir(lib, "*.uc"); +for (let col in cols) { + let func; + let uci = cursor(); + + try { + func = loadfile(lib + "/" + col, opts); + } catch(e) { + warn(`error compiling collector '${col}':\n${e.message}\n`); + continue; + } + + let name = substr(col, 0, -3); + let config = uci.get_all("prometheus-node-exporter-ucode", name); + if (!config || config[".type"] != "collector") + config = {}; + else { + delete config[".anonymous"]; + delete config[".type"]; + delete config[".name"]; + } + + collectors[name] = { + func, + config, + }; +} + +warn(`prometheus-node-exporter-ucode now serving requests with ${length(collectors)} collectors\n`); + +if (!("uhttpd" in global)) { + global.debug = true; + + puts = function(...s) { + return print(...s, "\n"); + }; + + handle_request({ + QUERY_STRING: join("&", map(ARGV, v => "collect=" + v)), + }); +} +%} diff --git a/utils/prometheus-node-exporter-ucode/files/run.sh b/utils/prometheus-node-exporter-ucode/files/run.sh new file mode 100755 index 0000000000..a7cefeff38 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/files/run.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec /usr/bin/ucode -T /usr/share/ucode/node-exporter/metrics.uc $* diff --git a/utils/prometheus-node-exporter-ucode/test.sh b/utils/prometheus-node-exporter-ucode/test.sh new file mode 100755 index 0000000000..e72942b8e0 --- /dev/null +++ b/utils/prometheus-node-exporter-ucode/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +prometheus-node-exporter-ucode time diff --git a/utils/rpcd-mod-wireguard/Makefile b/utils/rpcd-mod-wireguard/Makefile new file mode 100644 index 0000000000..9fb7d7e882 --- /dev/null +++ b/utils/rpcd-mod-wireguard/Makefile @@ -0,0 +1,31 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=rpcd-mod-wireguard +PKG_RELEASE=1 + +PKG_LICENSE:=LGPL-2.1+ +PKG_BUILD_FLAGS:=gc-sections + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk + +define Package/rpcd-mod-wireguard + SECTION:=libs + CATEGORY:=Libraries + TITLE:=WireGuard rpcd module + DEPENDS:=+rpcd +kmod-wireguard + MAINTAINER:=Andre Heider +endef + +define Package/rpcd-mod-wireguard/install + $(INSTALL_DIR) $(1)/usr/lib/rpcd + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/lib/rpcd/wireguard.so \ + $(1)/usr/lib/rpcd/ +endef + +define Package/rpcd-mod-wireguard/postinst +#!/bin/sh +[ -n "$$IPKG_INSTROOT" ] || /etc/init.d/rpcd reload +endef + +$(eval $(call BuildPackage,rpcd-mod-wireguard)) diff --git a/utils/rpcd-mod-wireguard/src/CMakeLists.txt b/utils/rpcd-mod-wireguard/src/CMakeLists.txt new file mode 100644 index 0000000000..97e06aa317 --- /dev/null +++ b/utils/rpcd-mod-wireguard/src/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 2.8.12) +PROJECT(rpcd-mod-wireguard) +ADD_DEFINITIONS(-Os -Wall -Werror --std=gnu99 -g3 -Wmissing-declarations) + +SET(SOURCES wireguard.c api.c) + +ADD_LIBRARY(rpcd-mod-wireguard SHARED ${SOURCES}) + +SET_TARGET_PROPERTIES(rpcd-mod-wireguard PROPERTIES OUTPUT_NAME wireguard PREFIX "") +INSTALL(TARGETS rpcd-mod-wireguard LIBRARY DESTINATION lib/rpcd) diff --git a/utils/rpcd-mod-wireguard/src/api.c b/utils/rpcd-mod-wireguard/src/api.c new file mode 100644 index 0000000000..e80ed53033 --- /dev/null +++ b/utils/rpcd-mod-wireguard/src/api.c @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: LGPL-2.1+ +// Copyright (C) 2023 Andre Heider + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +#include "wireguard.h" + +static struct blob_buf buf; + +enum { + RPC_PK_DEVICE, + __RPC_PK_MAX, +}; + +static const struct blobmsg_policy rpc_privatekey_policy[__RPC_PK_MAX] = { + [RPC_PK_DEVICE] = { .name = "private", .type = BLOBMSG_TYPE_STRING }, +}; + +static void rpc_wireguard_add_endpoint(const wg_endpoint *endpoint) +{ + char host[1025]; // NI_MAXHOST + char serv[32]; // NI_MAXSERV + char res[sizeof(host) + sizeof(serv) + 4]; + socklen_t addr_len; + + memset(res, 0, sizeof(res)); + if (endpoint->addr.sa_family == AF_INET) + addr_len = sizeof(struct sockaddr_in); + else if (endpoint->addr.sa_family == AF_INET6) + addr_len = sizeof(struct sockaddr_in6); + else + return; + + if (getnameinfo(&endpoint->addr, addr_len, host, sizeof(host), serv, sizeof(serv), + NI_DGRAM | NI_NUMERICHOST | NI_NUMERICSERV)) + return; + + if (endpoint->addr.sa_family == AF_INET6 && strchr(host, ':')) + snprintf(res, sizeof(res), "[%s]:%s", host, serv); + else + snprintf(res, sizeof(res), "%s:%s", host, serv); + res[sizeof(res) - 1] = 0; + + blobmsg_add_string(&buf, "endpoint", res); +} + +static void rpc_wireguard_add_allowedip(const wg_allowedip *allowedip) +{ + char res[INET6_ADDRSTRLEN + 4 + 1]; + + memset(res, 0, sizeof(res)); + if (allowedip->family == AF_INET) + inet_ntop(AF_INET, &allowedip->ip4, res, INET6_ADDRSTRLEN); + else if (allowedip->family == AF_INET6) + inet_ntop(AF_INET6, &allowedip->ip6, res, INET6_ADDRSTRLEN); + else + return; + + if (!res[0]) + return; + + sprintf(res + strlen(res), "/%u", allowedip->cidr); + res[sizeof(res) - 1] = 0; + + blobmsg_add_string(&buf, NULL, res); +} + +static void rpc_wireguard_add_peer(const wg_peer *peer) +{ + void *c; + struct wg_allowedip *allowedip; + + rpc_wireguard_add_endpoint(&peer->endpoint); + + c = blobmsg_open_array(&buf, "allowed_ips"); + wg_for_each_allowedip(peer, allowedip) + rpc_wireguard_add_allowedip(allowedip); + blobmsg_close_array(&buf, c); + + blobmsg_add_u64(&buf, "last_handshake", peer->last_handshake_time.tv_sec); + blobmsg_add_u64(&buf, "rx_bytes", peer->rx_bytes); + blobmsg_add_u64(&buf, "tx_bytes", peer->tx_bytes); + if (peer->persistent_keepalive_interval) + blobmsg_add_u16(&buf, "persistent_keepalive_interval", peer->persistent_keepalive_interval); +} + +static void rpc_wireguard_add_device(const wg_device *device) +{ + void *c, *d; + wg_peer *peer; + wg_key_b64_string key; + + blobmsg_add_u32(&buf, "ifindex", device->ifindex); + + if (device->flags & WGDEVICE_HAS_PUBLIC_KEY) { + wg_key_to_base64(key, device->public_key); + blobmsg_add_string(&buf, "public_key", key); + } + + if (device->listen_port) + blobmsg_add_u16(&buf, "listen_port", device->listen_port); + + if (device->fwmark) + blobmsg_add_u32(&buf, "fwmark", device->fwmark); + + c = blobmsg_open_table(&buf, "peers"); + wg_for_each_peer(device, peer) { + wg_key_to_base64(key, peer->public_key); + d = blobmsg_open_table(&buf, key); + rpc_wireguard_add_peer(peer); + blobmsg_close_table(&buf, d); + } + blobmsg_close_table(&buf, c); +} + +static int rpc_wireguard_status(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, struct blob_attr *msg) +{ + void *c; + char *device_names, *device_name; + size_t len; + + device_names = wg_list_device_names(); + if (!device_names) + return UBUS_STATUS_NOT_FOUND; + + blob_buf_init(&buf, 0); + + wg_for_each_device_name(device_names, device_name, len) { + wg_device *device; + + if (wg_get_device(&device, device_name) < 0) + continue; + + c = blobmsg_open_table(&buf, device_name); + rpc_wireguard_add_device(device); + blobmsg_close_table(&buf, c); + + wg_free_device(device); + } + + free(device_names); + + ubus_send_reply(ctx, req, buf.head); + + return UBUS_STATUS_OK; +} + +static int rpc_wireguard_genkey(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, struct blob_attr *msg) +{ + wg_key private_key, public_key; + wg_key_b64_string key; + + wg_generate_private_key(private_key); + wg_generate_public_key(public_key, private_key); + + blob_buf_init(&buf, 0); + wg_key_to_base64(key, private_key); + blobmsg_add_string(&buf, "private", key); + wg_key_to_base64(key, public_key); + blobmsg_add_string(&buf, "public", key); + ubus_send_reply(ctx, req, buf.head); + + return UBUS_STATUS_OK; +} + +static int rpc_wireguard_genpsk(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, struct blob_attr *msg) +{ + wg_key preshared_key; + wg_key_b64_string key; + + wg_generate_preshared_key(preshared_key); + + blob_buf_init(&buf, 0); + wg_key_to_base64(key, preshared_key); + blobmsg_add_string(&buf, "preshared", key); + ubus_send_reply(ctx, req, buf.head); + + return UBUS_STATUS_OK; +} + +static int rpc_wireguard_pubkey(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, struct blob_attr *msg) +{ + static struct blob_attr *tb[__RPC_PK_MAX]; + wg_key_b64_string key; + wg_key private_key, public_key; + + blobmsg_parse(rpc_privatekey_policy, __RPC_PK_MAX, tb, blob_data(msg), blob_len(msg)); + + if (!tb[RPC_PK_DEVICE]) + return UBUS_STATUS_INVALID_ARGUMENT; + + if (wg_key_from_base64(private_key, blobmsg_get_string(tb[RPC_PK_DEVICE]))) + return UBUS_STATUS_INVALID_ARGUMENT; + + wg_generate_public_key(public_key, private_key); + blob_buf_init(&buf, 0); + wg_key_to_base64(key, public_key); + blobmsg_add_string(&buf, "public", key); + ubus_send_reply(ctx, req, buf.head); + + return UBUS_STATUS_OK; +} + +static int rpc_wireguard_api_init(const struct rpc_daemon_ops *ops, struct ubus_context *ctx) +{ + static const struct ubus_method wireguard_methods[] = { + UBUS_METHOD_NOARG("status", rpc_wireguard_status), + UBUS_METHOD_NOARG("genkey", rpc_wireguard_genkey), + UBUS_METHOD_NOARG("genpsk", rpc_wireguard_genpsk), + UBUS_METHOD("pubkey", rpc_wireguard_pubkey, rpc_privatekey_policy), + }; + + static struct ubus_object_type wireguard_type = + UBUS_OBJECT_TYPE("rpcd-plugin-wireguard", wireguard_methods); + + static struct ubus_object obj = { + .name = "wireguard", + .type = &wireguard_type, + .methods = wireguard_methods, + .n_methods = ARRAY_SIZE(wireguard_methods), + }; + + return ubus_add_object(ctx, &obj); +} + +struct rpc_plugin rpc_plugin = { + .init = rpc_wireguard_api_init +}; diff --git a/utils/rpcd-mod-wireguard/src/wireguard.c b/utils/rpcd-mod-wireguard/src/wireguard.c new file mode 100644 index 0000000000..4941549af7 --- /dev/null +++ b/utils/rpcd-mod-wireguard/src/wireguard.c @@ -0,0 +1,1755 @@ +// SPDX-License-Identifier: LGPL-2.1+ +/* + * Copyright (C) 2015-2020 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2008-2012 Pablo Neira Ayuso . + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wireguard.h" + +/* wireguard.h netlink uapi: */ + +#define WG_GENL_NAME "wireguard" +#define WG_GENL_VERSION 1 + +enum wg_cmd { + WG_CMD_GET_DEVICE, + WG_CMD_SET_DEVICE, + __WG_CMD_MAX +}; + +enum wgdevice_flag { + WGDEVICE_F_REPLACE_PEERS = 1U << 0 +}; +enum wgdevice_attribute { + WGDEVICE_A_UNSPEC, + WGDEVICE_A_IFINDEX, + WGDEVICE_A_IFNAME, + WGDEVICE_A_PRIVATE_KEY, + WGDEVICE_A_PUBLIC_KEY, + WGDEVICE_A_FLAGS, + WGDEVICE_A_LISTEN_PORT, + WGDEVICE_A_FWMARK, + WGDEVICE_A_PEERS, + __WGDEVICE_A_LAST +}; + +enum wgpeer_flag { + WGPEER_F_REMOVE_ME = 1U << 0, + WGPEER_F_REPLACE_ALLOWEDIPS = 1U << 1 +}; +enum wgpeer_attribute { + WGPEER_A_UNSPEC, + WGPEER_A_PUBLIC_KEY, + WGPEER_A_PRESHARED_KEY, + WGPEER_A_FLAGS, + WGPEER_A_ENDPOINT, + WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL, + WGPEER_A_LAST_HANDSHAKE_TIME, + WGPEER_A_RX_BYTES, + WGPEER_A_TX_BYTES, + WGPEER_A_ALLOWEDIPS, + WGPEER_A_PROTOCOL_VERSION, + __WGPEER_A_LAST +}; + +enum wgallowedip_attribute { + WGALLOWEDIP_A_UNSPEC, + WGALLOWEDIP_A_FAMILY, + WGALLOWEDIP_A_IPADDR, + WGALLOWEDIP_A_CIDR_MASK, + __WGALLOWEDIP_A_LAST +}; + +/* libmnl mini library: */ + +#define MNL_SOCKET_AUTOPID 0 +#define MNL_ALIGNTO 4 +#define MNL_ALIGN(len) (((len)+MNL_ALIGNTO-1) & ~(MNL_ALIGNTO-1)) +#define MNL_NLMSG_HDRLEN MNL_ALIGN(sizeof(struct nlmsghdr)) +#define MNL_ATTR_HDRLEN MNL_ALIGN(sizeof(struct nlattr)) + +enum mnl_attr_data_type { + MNL_TYPE_UNSPEC, + MNL_TYPE_U8, + MNL_TYPE_U16, + MNL_TYPE_U32, + MNL_TYPE_U64, + MNL_TYPE_STRING, + MNL_TYPE_FLAG, + MNL_TYPE_MSECS, + MNL_TYPE_NESTED, + MNL_TYPE_NESTED_COMPAT, + MNL_TYPE_NUL_STRING, + MNL_TYPE_BINARY, + MNL_TYPE_MAX, +}; + +#define mnl_attr_for_each(attr, nlh, offset) \ + for ((attr) = mnl_nlmsg_get_payload_offset((nlh), (offset)); \ + mnl_attr_ok((attr), (char *)mnl_nlmsg_get_payload_tail(nlh) - (char *)(attr)); \ + (attr) = mnl_attr_next(attr)) + +#define mnl_attr_for_each_nested(attr, nest) \ + for ((attr) = mnl_attr_get_payload(nest); \ + mnl_attr_ok((attr), (char *)mnl_attr_get_payload(nest) + mnl_attr_get_payload_len(nest) - (char *)(attr)); \ + (attr) = mnl_attr_next(attr)) + +#define mnl_attr_for_each_payload(payload, payload_size) \ + for ((attr) = (payload); \ + mnl_attr_ok((attr), (char *)(payload) + payload_size - (char *)(attr)); \ + (attr) = mnl_attr_next(attr)) + +#define MNL_CB_ERROR -1 +#define MNL_CB_STOP 0 +#define MNL_CB_OK 1 + +typedef int (*mnl_attr_cb_t)(const struct nlattr *attr, void *data); +typedef int (*mnl_cb_t)(const struct nlmsghdr *nlh, void *data); + +#ifndef MNL_ARRAY_SIZE +#define MNL_ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0])) +#endif + +static size_t mnl_ideal_socket_buffer_size(void) +{ + static size_t size = 0; + + if (size) + return size; + size = (size_t)sysconf(_SC_PAGESIZE); + if (size > 8192) + size = 8192; + return size; +} + +static size_t mnl_nlmsg_size(size_t len) +{ + return len + MNL_NLMSG_HDRLEN; +} + +static struct nlmsghdr *mnl_nlmsg_put_header(void *buf) +{ + int len = MNL_ALIGN(sizeof(struct nlmsghdr)); + struct nlmsghdr *nlh = buf; + + memset(buf, 0, len); + nlh->nlmsg_len = len; + return nlh; +} + +static void *mnl_nlmsg_put_extra_header(struct nlmsghdr *nlh, size_t size) +{ + char *ptr = (char *)nlh + nlh->nlmsg_len; + size_t len = MNL_ALIGN(size); + nlh->nlmsg_len += len; + memset(ptr, 0, len); + return ptr; +} + +static void *mnl_nlmsg_get_payload(const struct nlmsghdr *nlh) +{ + return (void *)nlh + MNL_NLMSG_HDRLEN; +} + +static void *mnl_nlmsg_get_payload_offset(const struct nlmsghdr *nlh, size_t offset) +{ + return (void *)nlh + MNL_NLMSG_HDRLEN + MNL_ALIGN(offset); +} + +static bool mnl_nlmsg_ok(const struct nlmsghdr *nlh, int len) +{ + return len >= (int)sizeof(struct nlmsghdr) && + nlh->nlmsg_len >= sizeof(struct nlmsghdr) && + (int)nlh->nlmsg_len <= len; +} + +static struct nlmsghdr *mnl_nlmsg_next(const struct nlmsghdr *nlh, int *len) +{ + *len -= MNL_ALIGN(nlh->nlmsg_len); + return (struct nlmsghdr *)((void *)nlh + MNL_ALIGN(nlh->nlmsg_len)); +} + +static void *mnl_nlmsg_get_payload_tail(const struct nlmsghdr *nlh) +{ + return (void *)nlh + MNL_ALIGN(nlh->nlmsg_len); +} + +static bool mnl_nlmsg_seq_ok(const struct nlmsghdr *nlh, unsigned int seq) +{ + return nlh->nlmsg_seq && seq ? nlh->nlmsg_seq == seq : true; +} + +static bool mnl_nlmsg_portid_ok(const struct nlmsghdr *nlh, unsigned int portid) +{ + return nlh->nlmsg_pid && portid ? nlh->nlmsg_pid == portid : true; +} + +static uint16_t mnl_attr_get_type(const struct nlattr *attr) +{ + return attr->nla_type & NLA_TYPE_MASK; +} + +static uint16_t mnl_attr_get_payload_len(const struct nlattr *attr) +{ + return attr->nla_len - MNL_ATTR_HDRLEN; +} + +static void *mnl_attr_get_payload(const struct nlattr *attr) +{ + return (void *)attr + MNL_ATTR_HDRLEN; +} + +static bool mnl_attr_ok(const struct nlattr *attr, int len) +{ + return len >= (int)sizeof(struct nlattr) && + attr->nla_len >= sizeof(struct nlattr) && + (int)attr->nla_len <= len; +} + +static struct nlattr *mnl_attr_next(const struct nlattr *attr) +{ + return (struct nlattr *)((void *)attr + MNL_ALIGN(attr->nla_len)); +} + +static int mnl_attr_type_valid(const struct nlattr *attr, uint16_t max) +{ + if (mnl_attr_get_type(attr) > max) { + errno = EOPNOTSUPP; + return -1; + } + return 1; +} + +static int __mnl_attr_validate(const struct nlattr *attr, + enum mnl_attr_data_type type, size_t exp_len) +{ + uint16_t attr_len = mnl_attr_get_payload_len(attr); + const char *attr_data = mnl_attr_get_payload(attr); + + if (attr_len < exp_len) { + errno = ERANGE; + return -1; + } + switch(type) { + case MNL_TYPE_FLAG: + if (attr_len > 0) { + errno = ERANGE; + return -1; + } + break; + case MNL_TYPE_NUL_STRING: + if (attr_len == 0) { + errno = ERANGE; + return -1; + } + if (attr_data[attr_len-1] != '\0') { + errno = EINVAL; + return -1; + } + break; + case MNL_TYPE_STRING: + if (attr_len == 0) { + errno = ERANGE; + return -1; + } + break; + case MNL_TYPE_NESTED: + + if (attr_len == 0) + break; + + if (attr_len < MNL_ATTR_HDRLEN) { + errno = ERANGE; + return -1; + } + break; + default: + + break; + } + if (exp_len && attr_len > exp_len) { + errno = ERANGE; + return -1; + } + return 0; +} + +static const size_t mnl_attr_data_type_len[MNL_TYPE_MAX] = { + [MNL_TYPE_U8] = sizeof(uint8_t), + [MNL_TYPE_U16] = sizeof(uint16_t), + [MNL_TYPE_U32] = sizeof(uint32_t), + [MNL_TYPE_U64] = sizeof(uint64_t), + [MNL_TYPE_MSECS] = sizeof(uint64_t), +}; + +static int mnl_attr_validate(const struct nlattr *attr, enum mnl_attr_data_type type) +{ + int exp_len; + + if (type >= MNL_TYPE_MAX) { + errno = EINVAL; + return -1; + } + exp_len = mnl_attr_data_type_len[type]; + return __mnl_attr_validate(attr, type, exp_len); +} + +static int mnl_attr_parse(const struct nlmsghdr *nlh, unsigned int offset, + mnl_attr_cb_t cb, void *data) +{ + int ret = MNL_CB_OK; + const struct nlattr *attr; + + mnl_attr_for_each(attr, nlh, offset) + if ((ret = cb(attr, data)) <= MNL_CB_STOP) + return ret; + return ret; +} + +static int mnl_attr_parse_nested(const struct nlattr *nested, mnl_attr_cb_t cb, + void *data) +{ + int ret = MNL_CB_OK; + const struct nlattr *attr; + + mnl_attr_for_each_nested(attr, nested) + if ((ret = cb(attr, data)) <= MNL_CB_STOP) + return ret; + return ret; +} + +static uint8_t mnl_attr_get_u8(const struct nlattr *attr) +{ + return *((uint8_t *)mnl_attr_get_payload(attr)); +} + +static uint16_t mnl_attr_get_u16(const struct nlattr *attr) +{ + return *((uint16_t *)mnl_attr_get_payload(attr)); +} + +static uint32_t mnl_attr_get_u32(const struct nlattr *attr) +{ + return *((uint32_t *)mnl_attr_get_payload(attr)); +} + +static uint64_t mnl_attr_get_u64(const struct nlattr *attr) +{ + uint64_t tmp; + memcpy(&tmp, mnl_attr_get_payload(attr), sizeof(tmp)); + return tmp; +} + +static const char *mnl_attr_get_str(const struct nlattr *attr) +{ + return mnl_attr_get_payload(attr); +} + +static void mnl_attr_put(struct nlmsghdr *nlh, uint16_t type, size_t len, + const void *data) +{ + struct nlattr *attr = mnl_nlmsg_get_payload_tail(nlh); + uint16_t payload_len = MNL_ALIGN(sizeof(struct nlattr)) + len; + int pad; + + attr->nla_type = type; + attr->nla_len = payload_len; + memcpy(mnl_attr_get_payload(attr), data, len); + nlh->nlmsg_len += MNL_ALIGN(payload_len); + pad = MNL_ALIGN(len) - len; + if (pad > 0) + memset(mnl_attr_get_payload(attr) + len, 0, pad); +} + +static void mnl_attr_put_u16(struct nlmsghdr *nlh, uint16_t type, uint16_t data) +{ + mnl_attr_put(nlh, type, sizeof(uint16_t), &data); +} + +static void mnl_attr_put_u32(struct nlmsghdr *nlh, uint16_t type, uint32_t data) +{ + mnl_attr_put(nlh, type, sizeof(uint32_t), &data); +} + +static void mnl_attr_put_strz(struct nlmsghdr *nlh, uint16_t type, const char *data) +{ + mnl_attr_put(nlh, type, strlen(data)+1, data); +} + +static struct nlattr *mnl_attr_nest_start(struct nlmsghdr *nlh, uint16_t type) +{ + struct nlattr *start = mnl_nlmsg_get_payload_tail(nlh); + + start->nla_type = NLA_F_NESTED | type; + nlh->nlmsg_len += MNL_ALIGN(sizeof(struct nlattr)); + return start; +} + +static bool mnl_attr_put_check(struct nlmsghdr *nlh, size_t buflen, + uint16_t type, size_t len, const void *data) +{ + if (nlh->nlmsg_len + MNL_ATTR_HDRLEN + MNL_ALIGN(len) > buflen) + return false; + mnl_attr_put(nlh, type, len, data); + return true; +} + +static bool mnl_attr_put_u8_check(struct nlmsghdr *nlh, size_t buflen, + uint16_t type, uint8_t data) +{ + return mnl_attr_put_check(nlh, buflen, type, sizeof(uint8_t), &data); +} + +static bool mnl_attr_put_u16_check(struct nlmsghdr *nlh, size_t buflen, + uint16_t type, uint16_t data) +{ + return mnl_attr_put_check(nlh, buflen, type, sizeof(uint16_t), &data); +} + +static bool mnl_attr_put_u32_check(struct nlmsghdr *nlh, size_t buflen, + uint16_t type, uint32_t data) +{ + return mnl_attr_put_check(nlh, buflen, type, sizeof(uint32_t), &data); +} + +static struct nlattr *mnl_attr_nest_start_check(struct nlmsghdr *nlh, size_t buflen, + uint16_t type) +{ + if (nlh->nlmsg_len + MNL_ATTR_HDRLEN > buflen) + return NULL; + return mnl_attr_nest_start(nlh, type); +} + +static void mnl_attr_nest_end(struct nlmsghdr *nlh, struct nlattr *start) +{ + start->nla_len = mnl_nlmsg_get_payload_tail(nlh) - (void *)start; +} + +static void mnl_attr_nest_cancel(struct nlmsghdr *nlh, struct nlattr *start) +{ + nlh->nlmsg_len -= mnl_nlmsg_get_payload_tail(nlh) - (void *)start; +} + +static int mnl_cb_noop(__attribute__((unused)) const struct nlmsghdr *nlh, __attribute__((unused)) void *data) +{ + return MNL_CB_OK; +} + +static int mnl_cb_error(const struct nlmsghdr *nlh, __attribute__((unused)) void *data) +{ + const struct nlmsgerr *err = mnl_nlmsg_get_payload(nlh); + + if (nlh->nlmsg_len < mnl_nlmsg_size(sizeof(struct nlmsgerr))) { + errno = EBADMSG; + return MNL_CB_ERROR; + } + + if (err->error < 0) + errno = -err->error; + else + errno = err->error; + + return err->error == 0 ? MNL_CB_STOP : MNL_CB_ERROR; +} + +static int mnl_cb_stop(__attribute__((unused)) const struct nlmsghdr *nlh, __attribute__((unused)) void *data) +{ + return MNL_CB_STOP; +} + +static const mnl_cb_t default_cb_array[NLMSG_MIN_TYPE] = { + [NLMSG_NOOP] = mnl_cb_noop, + [NLMSG_ERROR] = mnl_cb_error, + [NLMSG_DONE] = mnl_cb_stop, + [NLMSG_OVERRUN] = mnl_cb_noop, +}; + +static int __mnl_cb_run(const void *buf, size_t numbytes, + unsigned int seq, unsigned int portid, + mnl_cb_t cb_data, void *data, + const mnl_cb_t *cb_ctl_array, + unsigned int cb_ctl_array_len) +{ + int ret = MNL_CB_OK, len = numbytes; + const struct nlmsghdr *nlh = buf; + + while (mnl_nlmsg_ok(nlh, len)) { + + if (!mnl_nlmsg_portid_ok(nlh, portid)) { + errno = ESRCH; + return -1; + } + + if (!mnl_nlmsg_seq_ok(nlh, seq)) { + errno = EPROTO; + return -1; + } + + if (nlh->nlmsg_flags & NLM_F_DUMP_INTR) { + errno = EINTR; + return -1; + } + + if (nlh->nlmsg_type >= NLMSG_MIN_TYPE) { + if (cb_data){ + ret = cb_data(nlh, data); + if (ret <= MNL_CB_STOP) + goto out; + } + } else if (nlh->nlmsg_type < cb_ctl_array_len) { + if (cb_ctl_array && cb_ctl_array[nlh->nlmsg_type]) { + ret = cb_ctl_array[nlh->nlmsg_type](nlh, data); + if (ret <= MNL_CB_STOP) + goto out; + } + } else if (default_cb_array[nlh->nlmsg_type]) { + ret = default_cb_array[nlh->nlmsg_type](nlh, data); + if (ret <= MNL_CB_STOP) + goto out; + } + nlh = mnl_nlmsg_next(nlh, &len); + } +out: + return ret; +} + +static int mnl_cb_run2(const void *buf, size_t numbytes, unsigned int seq, + unsigned int portid, mnl_cb_t cb_data, void *data, + const mnl_cb_t *cb_ctl_array, unsigned int cb_ctl_array_len) +{ + return __mnl_cb_run(buf, numbytes, seq, portid, cb_data, data, + cb_ctl_array, cb_ctl_array_len); +} + +static int mnl_cb_run(const void *buf, size_t numbytes, unsigned int seq, + unsigned int portid, mnl_cb_t cb_data, void *data) +{ + return __mnl_cb_run(buf, numbytes, seq, portid, cb_data, data, NULL, 0); +} + +struct mnl_socket { + int fd; + struct sockaddr_nl addr; +}; + +static unsigned int mnl_socket_get_portid(const struct mnl_socket *nl) +{ + return nl->addr.nl_pid; +} + +static struct mnl_socket *__mnl_socket_open(int bus, int flags) +{ + struct mnl_socket *nl; + + nl = calloc(1, sizeof(struct mnl_socket)); + if (nl == NULL) + return NULL; + + nl->fd = socket(AF_NETLINK, SOCK_RAW | flags, bus); + if (nl->fd == -1) { + free(nl); + return NULL; + } + + return nl; +} + +static struct mnl_socket *mnl_socket_open(int bus) +{ + return __mnl_socket_open(bus, 0); +} + +static int mnl_socket_bind(struct mnl_socket *nl, unsigned int groups, pid_t pid) +{ + int ret; + socklen_t addr_len; + + nl->addr.nl_family = AF_NETLINK; + nl->addr.nl_groups = groups; + nl->addr.nl_pid = pid; + + ret = bind(nl->fd, (struct sockaddr *) &nl->addr, sizeof (nl->addr)); + if (ret < 0) + return ret; + + addr_len = sizeof(nl->addr); + ret = getsockname(nl->fd, (struct sockaddr *) &nl->addr, &addr_len); + if (ret < 0) + return ret; + + if (addr_len != sizeof(nl->addr)) { + errno = EINVAL; + return -1; + } + if (nl->addr.nl_family != AF_NETLINK) { + errno = EINVAL; + return -1; + } + return 0; +} + +static ssize_t mnl_socket_sendto(const struct mnl_socket *nl, const void *buf, + size_t len) +{ + static const struct sockaddr_nl snl = { + .nl_family = AF_NETLINK + }; + return sendto(nl->fd, buf, len, 0, + (struct sockaddr *) &snl, sizeof(snl)); +} + +static ssize_t mnl_socket_recvfrom(const struct mnl_socket *nl, void *buf, + size_t bufsiz) +{ + ssize_t ret; + struct sockaddr_nl addr; + struct iovec iov = { + .iov_base = buf, + .iov_len = bufsiz, + }; + struct msghdr msg = { + .msg_name = &addr, + .msg_namelen = sizeof(struct sockaddr_nl), + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = NULL, + .msg_controllen = 0, + .msg_flags = 0, + }; + ret = recvmsg(nl->fd, &msg, 0); + if (ret == -1) + return ret; + + if (msg.msg_flags & MSG_TRUNC) { + errno = ENOSPC; + return -1; + } + if (msg.msg_namelen != sizeof(struct sockaddr_nl)) { + errno = EINVAL; + return -1; + } + return ret; +} + +static int mnl_socket_close(struct mnl_socket *nl) +{ + int ret = close(nl->fd); + free(nl); + return ret; +} + +/* mnlg mini library: */ + +struct mnlg_socket { + struct mnl_socket *nl; + char *buf; + uint16_t id; + uint8_t version; + unsigned int seq; + unsigned int portid; +}; + +static struct nlmsghdr *__mnlg_msg_prepare(struct mnlg_socket *nlg, uint8_t cmd, + uint16_t flags, uint16_t id, + uint8_t version) +{ + struct nlmsghdr *nlh; + struct genlmsghdr *genl; + + nlh = mnl_nlmsg_put_header(nlg->buf); + nlh->nlmsg_type = id; + nlh->nlmsg_flags = flags; + nlg->seq = time(NULL); + nlh->nlmsg_seq = nlg->seq; + + genl = mnl_nlmsg_put_extra_header(nlh, sizeof(struct genlmsghdr)); + genl->cmd = cmd; + genl->version = version; + + return nlh; +} + +static struct nlmsghdr *mnlg_msg_prepare(struct mnlg_socket *nlg, uint8_t cmd, + uint16_t flags) +{ + return __mnlg_msg_prepare(nlg, cmd, flags, nlg->id, nlg->version); +} + +static int mnlg_socket_send(struct mnlg_socket *nlg, const struct nlmsghdr *nlh) +{ + return mnl_socket_sendto(nlg->nl, nlh, nlh->nlmsg_len); +} + +static int mnlg_cb_noop(const struct nlmsghdr *nlh, void *data) +{ + (void)nlh; + (void)data; + return MNL_CB_OK; +} + +static int mnlg_cb_error(const struct nlmsghdr *nlh, void *data) +{ + const struct nlmsgerr *err = mnl_nlmsg_get_payload(nlh); + (void)data; + + if (nlh->nlmsg_len < mnl_nlmsg_size(sizeof(struct nlmsgerr))) { + errno = EBADMSG; + return MNL_CB_ERROR; + } + /* Netlink subsystems returns the errno value with different signess */ + if (err->error < 0) + errno = -err->error; + else + errno = err->error; + + return err->error == 0 ? MNL_CB_STOP : MNL_CB_ERROR; +} + +static int mnlg_cb_stop(const struct nlmsghdr *nlh, void *data) +{ + (void)data; + if (nlh->nlmsg_flags & NLM_F_MULTI && nlh->nlmsg_len == mnl_nlmsg_size(sizeof(int))) { + int error = *(int *)mnl_nlmsg_get_payload(nlh); + /* Netlink subsystems returns the errno value with different signess */ + if (error < 0) + errno = -error; + else + errno = error; + + return error == 0 ? MNL_CB_STOP : MNL_CB_ERROR; + } + return MNL_CB_STOP; +} + +static const mnl_cb_t mnlg_cb_array[] = { + [NLMSG_NOOP] = mnlg_cb_noop, + [NLMSG_ERROR] = mnlg_cb_error, + [NLMSG_DONE] = mnlg_cb_stop, + [NLMSG_OVERRUN] = mnlg_cb_noop, +}; + +static int mnlg_socket_recv_run(struct mnlg_socket *nlg, mnl_cb_t data_cb, void *data) +{ + int err; + + do { + err = mnl_socket_recvfrom(nlg->nl, nlg->buf, + mnl_ideal_socket_buffer_size()); + if (err <= 0) + break; + err = mnl_cb_run2(nlg->buf, err, nlg->seq, nlg->portid, + data_cb, data, mnlg_cb_array, MNL_ARRAY_SIZE(mnlg_cb_array)); + } while (err > 0); + + return err; +} + +static int get_family_id_attr_cb(const struct nlattr *attr, void *data) +{ + const struct nlattr **tb = data; + int type = mnl_attr_get_type(attr); + + if (mnl_attr_type_valid(attr, CTRL_ATTR_MAX) < 0) + return MNL_CB_ERROR; + + if (type == CTRL_ATTR_FAMILY_ID && + mnl_attr_validate(attr, MNL_TYPE_U16) < 0) + return MNL_CB_ERROR; + tb[type] = attr; + return MNL_CB_OK; +} + +static int get_family_id_cb(const struct nlmsghdr *nlh, void *data) +{ + uint16_t *p_id = data; + struct nlattr *tb[CTRL_ATTR_MAX + 1] = { 0 }; + + mnl_attr_parse(nlh, sizeof(struct genlmsghdr), get_family_id_attr_cb, tb); + if (!tb[CTRL_ATTR_FAMILY_ID]) + return MNL_CB_ERROR; + *p_id = mnl_attr_get_u16(tb[CTRL_ATTR_FAMILY_ID]); + return MNL_CB_OK; +} + +static struct mnlg_socket *mnlg_socket_open(const char *family_name, uint8_t version) +{ + struct mnlg_socket *nlg; + struct nlmsghdr *nlh; + int err; + + nlg = malloc(sizeof(*nlg)); + if (!nlg) + return NULL; + nlg->id = 0; + + err = -ENOMEM; + nlg->buf = malloc(mnl_ideal_socket_buffer_size()); + if (!nlg->buf) + goto err_buf_alloc; + + nlg->nl = mnl_socket_open(NETLINK_GENERIC); + if (!nlg->nl) { + err = -errno; + goto err_mnl_socket_open; + } + + if (mnl_socket_bind(nlg->nl, 0, MNL_SOCKET_AUTOPID) < 0) { + err = -errno; + goto err_mnl_socket_bind; + } + + nlg->portid = mnl_socket_get_portid(nlg->nl); + + nlh = __mnlg_msg_prepare(nlg, CTRL_CMD_GETFAMILY, + NLM_F_REQUEST | NLM_F_ACK, GENL_ID_CTRL, 1); + mnl_attr_put_strz(nlh, CTRL_ATTR_FAMILY_NAME, family_name); + + if (mnlg_socket_send(nlg, nlh) < 0) { + err = -errno; + goto err_mnlg_socket_send; + } + + errno = 0; + if (mnlg_socket_recv_run(nlg, get_family_id_cb, &nlg->id) < 0) { + errno = errno == ENOENT ? EPROTONOSUPPORT : errno; + err = errno ? -errno : -ENOSYS; + goto err_mnlg_socket_recv_run; + } + + nlg->version = version; + errno = 0; + return nlg; + +err_mnlg_socket_recv_run: +err_mnlg_socket_send: +err_mnl_socket_bind: + mnl_socket_close(nlg->nl); +err_mnl_socket_open: + free(nlg->buf); +err_buf_alloc: + free(nlg); + errno = -err; + return NULL; +} + +static void mnlg_socket_close(struct mnlg_socket *nlg) +{ + mnl_socket_close(nlg->nl); + free(nlg->buf); + free(nlg); +} + +/* wireguard-specific parts: */ + +struct string_list { + char *buffer; + size_t len; + size_t cap; +}; + +static int string_list_add(struct string_list *list, const char *str) +{ + size_t len = strlen(str) + 1; + + if (len == 1) + return 0; + + if (len >= list->cap - list->len) { + char *new_buffer; + size_t new_cap = list->cap * 2; + + if (new_cap < list->len +len + 1) + new_cap = list->len + len + 1; + new_buffer = realloc(list->buffer, new_cap); + if (!new_buffer) + return -errno; + list->buffer = new_buffer; + list->cap = new_cap; + } + memcpy(list->buffer + list->len, str, len); + list->len += len; + list->buffer[list->len] = '\0'; + return 0; +} + +struct interface { + const char *name; + bool is_wireguard; +}; + +static int parse_linkinfo(const struct nlattr *attr, void *data) +{ + struct interface *interface = data; + + if (mnl_attr_get_type(attr) == IFLA_INFO_KIND && !strcmp(WG_GENL_NAME, mnl_attr_get_str(attr))) + interface->is_wireguard = true; + return MNL_CB_OK; +} + +static int parse_infomsg(const struct nlattr *attr, void *data) +{ + struct interface *interface = data; + + if (mnl_attr_get_type(attr) == IFLA_LINKINFO) + return mnl_attr_parse_nested(attr, parse_linkinfo, data); + else if (mnl_attr_get_type(attr) == IFLA_IFNAME) + interface->name = mnl_attr_get_str(attr); + return MNL_CB_OK; +} + +static int read_devices_cb(const struct nlmsghdr *nlh, void *data) +{ + struct string_list *list = data; + struct interface interface = { 0 }; + int ret; + + ret = mnl_attr_parse(nlh, sizeof(struct ifinfomsg), parse_infomsg, &interface); + if (ret != MNL_CB_OK) + return ret; + if (interface.name && interface.is_wireguard) + ret = string_list_add(list, interface.name); + if (ret < 0) + return ret; + if (nlh->nlmsg_type != NLMSG_DONE) + return MNL_CB_OK + 1; + return MNL_CB_OK; +} + +static int fetch_device_names(struct string_list *list) +{ + struct mnl_socket *nl = NULL; + char *rtnl_buffer = NULL; + size_t message_len; + unsigned int portid, seq; + ssize_t len; + int ret = 0; + struct nlmsghdr *nlh; + struct ifinfomsg *ifm; + + ret = -ENOMEM; + rtnl_buffer = calloc(mnl_ideal_socket_buffer_size(), 1); + if (!rtnl_buffer) + goto cleanup; + + nl = mnl_socket_open(NETLINK_ROUTE); + if (!nl) { + ret = -errno; + goto cleanup; + } + + if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) { + ret = -errno; + goto cleanup; + } + + seq = time(NULL); + portid = mnl_socket_get_portid(nl); + nlh = mnl_nlmsg_put_header(rtnl_buffer); + nlh->nlmsg_type = RTM_GETLINK; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP; + nlh->nlmsg_seq = seq; + ifm = mnl_nlmsg_put_extra_header(nlh, sizeof(*ifm)); + ifm->ifi_family = AF_UNSPEC; + message_len = nlh->nlmsg_len; + + if (mnl_socket_sendto(nl, rtnl_buffer, message_len) < 0) { + ret = -errno; + goto cleanup; + } + +another: + if ((len = mnl_socket_recvfrom(nl, rtnl_buffer, mnl_ideal_socket_buffer_size())) < 0) { + ret = -errno; + goto cleanup; + } + if ((len = mnl_cb_run(rtnl_buffer, len, seq, portid, read_devices_cb, list)) < 0) { + /* Netlink returns NLM_F_DUMP_INTR if the set of all tunnels changed + * during the dump. That's unfortunate, but is pretty common on busy + * systems that are adding and removing tunnels all the time. Rather + * than retrying, potentially indefinitely, we just work with the + * partial results. */ + if (errno != EINTR) { + ret = -errno; + goto cleanup; + } + } + if (len == MNL_CB_OK + 1) + goto another; + ret = 0; + +cleanup: + free(rtnl_buffer); + if (nl) + mnl_socket_close(nl); + return ret; +} + +static int add_del_iface(const char *ifname, bool add) +{ + struct mnl_socket *nl = NULL; + char *rtnl_buffer; + ssize_t len; + int ret; + struct nlmsghdr *nlh; + struct ifinfomsg *ifm; + struct nlattr *nest; + + rtnl_buffer = calloc(mnl_ideal_socket_buffer_size(), 1); + if (!rtnl_buffer) { + ret = -ENOMEM; + goto cleanup; + } + + nl = mnl_socket_open(NETLINK_ROUTE); + if (!nl) { + ret = -errno; + goto cleanup; + } + + if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) { + ret = -errno; + goto cleanup; + } + + nlh = mnl_nlmsg_put_header(rtnl_buffer); + nlh->nlmsg_type = add ? RTM_NEWLINK : RTM_DELLINK; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | (add ? NLM_F_CREATE | NLM_F_EXCL : 0); + nlh->nlmsg_seq = time(NULL); + ifm = mnl_nlmsg_put_extra_header(nlh, sizeof(*ifm)); + ifm->ifi_family = AF_UNSPEC; + mnl_attr_put_strz(nlh, IFLA_IFNAME, ifname); + nest = mnl_attr_nest_start(nlh, IFLA_LINKINFO); + mnl_attr_put_strz(nlh, IFLA_INFO_KIND, WG_GENL_NAME); + mnl_attr_nest_end(nlh, nest); + + if (mnl_socket_sendto(nl, rtnl_buffer, nlh->nlmsg_len) < 0) { + ret = -errno; + goto cleanup; + } + if ((len = mnl_socket_recvfrom(nl, rtnl_buffer, mnl_ideal_socket_buffer_size())) < 0) { + ret = -errno; + goto cleanup; + } + if (mnl_cb_run(rtnl_buffer, len, nlh->nlmsg_seq, mnl_socket_get_portid(nl), NULL, NULL) < 0) { + ret = -errno; + goto cleanup; + } + ret = 0; + +cleanup: + free(rtnl_buffer); + if (nl) + mnl_socket_close(nl); + return ret; +} + +int wg_set_device(wg_device *dev) +{ + int ret = 0; + wg_peer *peer = NULL; + wg_allowedip *allowedip = NULL; + struct nlattr *peers_nest, *peer_nest, *allowedips_nest, *allowedip_nest; + struct nlmsghdr *nlh; + struct mnlg_socket *nlg; + + nlg = mnlg_socket_open(WG_GENL_NAME, WG_GENL_VERSION); + if (!nlg) + return -errno; + +again: + nlh = mnlg_msg_prepare(nlg, WG_CMD_SET_DEVICE, NLM_F_REQUEST | NLM_F_ACK); + mnl_attr_put_strz(nlh, WGDEVICE_A_IFNAME, dev->name); + + if (!peer) { + uint32_t flags = 0; + + if (dev->flags & WGDEVICE_HAS_PRIVATE_KEY) + mnl_attr_put(nlh, WGDEVICE_A_PRIVATE_KEY, sizeof(dev->private_key), dev->private_key); + if (dev->flags & WGDEVICE_HAS_LISTEN_PORT) + mnl_attr_put_u16(nlh, WGDEVICE_A_LISTEN_PORT, dev->listen_port); + if (dev->flags & WGDEVICE_HAS_FWMARK) + mnl_attr_put_u32(nlh, WGDEVICE_A_FWMARK, dev->fwmark); + if (dev->flags & WGDEVICE_REPLACE_PEERS) + flags |= WGDEVICE_F_REPLACE_PEERS; + if (flags) + mnl_attr_put_u32(nlh, WGDEVICE_A_FLAGS, flags); + } + if (!dev->first_peer) + goto send; + peers_nest = peer_nest = allowedips_nest = allowedip_nest = NULL; + peers_nest = mnl_attr_nest_start(nlh, WGDEVICE_A_PEERS); + for (peer = peer ? peer : dev->first_peer; peer; peer = peer->next_peer) { + uint32_t flags = 0; + + peer_nest = mnl_attr_nest_start_check(nlh, mnl_ideal_socket_buffer_size(), 0); + if (!peer_nest) + goto toobig_peers; + if (!mnl_attr_put_check(nlh, mnl_ideal_socket_buffer_size(), WGPEER_A_PUBLIC_KEY, sizeof(peer->public_key), peer->public_key)) + goto toobig_peers; + if (peer->flags & WGPEER_REMOVE_ME) + flags |= WGPEER_F_REMOVE_ME; + if (!allowedip) { + if (peer->flags & WGPEER_REPLACE_ALLOWEDIPS) + flags |= WGPEER_F_REPLACE_ALLOWEDIPS; + if (peer->flags & WGPEER_HAS_PRESHARED_KEY) { + if (!mnl_attr_put_check(nlh, mnl_ideal_socket_buffer_size(), WGPEER_A_PRESHARED_KEY, sizeof(peer->preshared_key), peer->preshared_key)) + goto toobig_peers; + } + if (peer->endpoint.addr.sa_family == AF_INET) { + if (!mnl_attr_put_check(nlh, mnl_ideal_socket_buffer_size(), WGPEER_A_ENDPOINT, sizeof(peer->endpoint.addr4), &peer->endpoint.addr4)) + goto toobig_peers; + } else if (peer->endpoint.addr.sa_family == AF_INET6) { + if (!mnl_attr_put_check(nlh, mnl_ideal_socket_buffer_size(), WGPEER_A_ENDPOINT, sizeof(peer->endpoint.addr6), &peer->endpoint.addr6)) + goto toobig_peers; + } + if (peer->flags & WGPEER_HAS_PERSISTENT_KEEPALIVE_INTERVAL) { + if (!mnl_attr_put_u16_check(nlh, mnl_ideal_socket_buffer_size(), WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL, peer->persistent_keepalive_interval)) + goto toobig_peers; + } + } + if (flags) { + if (!mnl_attr_put_u32_check(nlh, mnl_ideal_socket_buffer_size(), WGPEER_A_FLAGS, flags)) + goto toobig_peers; + } + if (peer->first_allowedip) { + if (!allowedip) + allowedip = peer->first_allowedip; + allowedips_nest = mnl_attr_nest_start_check(nlh, mnl_ideal_socket_buffer_size(), WGPEER_A_ALLOWEDIPS); + if (!allowedips_nest) + goto toobig_allowedips; + for (; allowedip; allowedip = allowedip->next_allowedip) { + allowedip_nest = mnl_attr_nest_start_check(nlh, mnl_ideal_socket_buffer_size(), 0); + if (!allowedip_nest) + goto toobig_allowedips; + if (!mnl_attr_put_u16_check(nlh, mnl_ideal_socket_buffer_size(), WGALLOWEDIP_A_FAMILY, allowedip->family)) + goto toobig_allowedips; + if (allowedip->family == AF_INET) { + if (!mnl_attr_put_check(nlh, mnl_ideal_socket_buffer_size(), WGALLOWEDIP_A_IPADDR, sizeof(allowedip->ip4), &allowedip->ip4)) + goto toobig_allowedips; + } else if (allowedip->family == AF_INET6) { + if (!mnl_attr_put_check(nlh, mnl_ideal_socket_buffer_size(), WGALLOWEDIP_A_IPADDR, sizeof(allowedip->ip6), &allowedip->ip6)) + goto toobig_allowedips; + } + if (!mnl_attr_put_u8_check(nlh, mnl_ideal_socket_buffer_size(), WGALLOWEDIP_A_CIDR_MASK, allowedip->cidr)) + goto toobig_allowedips; + mnl_attr_nest_end(nlh, allowedip_nest); + allowedip_nest = NULL; + } + mnl_attr_nest_end(nlh, allowedips_nest); + allowedips_nest = NULL; + } + + mnl_attr_nest_end(nlh, peer_nest); + peer_nest = NULL; + } + mnl_attr_nest_end(nlh, peers_nest); + peers_nest = NULL; + goto send; +toobig_allowedips: + if (allowedip_nest) + mnl_attr_nest_cancel(nlh, allowedip_nest); + if (allowedips_nest) + mnl_attr_nest_end(nlh, allowedips_nest); + mnl_attr_nest_end(nlh, peer_nest); + mnl_attr_nest_end(nlh, peers_nest); + goto send; +toobig_peers: + if (peer_nest) + mnl_attr_nest_cancel(nlh, peer_nest); + mnl_attr_nest_end(nlh, peers_nest); + goto send; +send: + if (mnlg_socket_send(nlg, nlh) < 0) { + ret = -errno; + goto out; + } + errno = 0; + if (mnlg_socket_recv_run(nlg, NULL, NULL) < 0) { + ret = errno ? -errno : -EINVAL; + goto out; + } + if (peer) + goto again; + +out: + mnlg_socket_close(nlg); + errno = -ret; + return ret; +} + +static int parse_allowedip(const struct nlattr *attr, void *data) +{ + wg_allowedip *allowedip = data; + + switch (mnl_attr_get_type(attr)) { + case WGALLOWEDIP_A_UNSPEC: + break; + case WGALLOWEDIP_A_FAMILY: + if (!mnl_attr_validate(attr, MNL_TYPE_U16)) + allowedip->family = mnl_attr_get_u16(attr); + break; + case WGALLOWEDIP_A_IPADDR: + if (mnl_attr_get_payload_len(attr) == sizeof(allowedip->ip4)) + memcpy(&allowedip->ip4, mnl_attr_get_payload(attr), sizeof(allowedip->ip4)); + else if (mnl_attr_get_payload_len(attr) == sizeof(allowedip->ip6)) + memcpy(&allowedip->ip6, mnl_attr_get_payload(attr), sizeof(allowedip->ip6)); + break; + case WGALLOWEDIP_A_CIDR_MASK: + if (!mnl_attr_validate(attr, MNL_TYPE_U8)) + allowedip->cidr = mnl_attr_get_u8(attr); + break; + } + + return MNL_CB_OK; +} + +static int parse_allowedips(const struct nlattr *attr, void *data) +{ + wg_peer *peer = data; + wg_allowedip *new_allowedip = calloc(1, sizeof(wg_allowedip)); + int ret; + + if (!new_allowedip) + return MNL_CB_ERROR; + if (!peer->first_allowedip) + peer->first_allowedip = peer->last_allowedip = new_allowedip; + else { + peer->last_allowedip->next_allowedip = new_allowedip; + peer->last_allowedip = new_allowedip; + } + ret = mnl_attr_parse_nested(attr, parse_allowedip, new_allowedip); + if (!ret) + return ret; + if (!((new_allowedip->family == AF_INET && new_allowedip->cidr <= 32) || (new_allowedip->family == AF_INET6 && new_allowedip->cidr <= 128))) { + errno = EAFNOSUPPORT; + return MNL_CB_ERROR; + } + return MNL_CB_OK; +} + +bool wg_key_is_zero(const wg_key key) +{ + volatile uint8_t acc = 0; + unsigned int i; + + for (i = 0; i < sizeof(wg_key); ++i) { + acc |= key[i]; + __asm__ ("" : "=r" (acc) : "0" (acc)); + } + return 1 & ((acc - 1) >> 8); +} + +static int parse_peer(const struct nlattr *attr, void *data) +{ + wg_peer *peer = data; + + switch (mnl_attr_get_type(attr)) { + case WGPEER_A_UNSPEC: + break; + case WGPEER_A_PUBLIC_KEY: + if (mnl_attr_get_payload_len(attr) == sizeof(peer->public_key)) { + memcpy(peer->public_key, mnl_attr_get_payload(attr), sizeof(peer->public_key)); + peer->flags |= WGPEER_HAS_PUBLIC_KEY; + } + break; + case WGPEER_A_PRESHARED_KEY: + if (mnl_attr_get_payload_len(attr) == sizeof(peer->preshared_key)) { + memcpy(peer->preshared_key, mnl_attr_get_payload(attr), sizeof(peer->preshared_key)); + if (!wg_key_is_zero(peer->preshared_key)) + peer->flags |= WGPEER_HAS_PRESHARED_KEY; + } + break; + case WGPEER_A_ENDPOINT: { + struct sockaddr *addr; + + if (mnl_attr_get_payload_len(attr) < sizeof(*addr)) + break; + addr = mnl_attr_get_payload(attr); + if (addr->sa_family == AF_INET && mnl_attr_get_payload_len(attr) == sizeof(peer->endpoint.addr4)) + memcpy(&peer->endpoint.addr4, addr, sizeof(peer->endpoint.addr4)); + else if (addr->sa_family == AF_INET6 && mnl_attr_get_payload_len(attr) == sizeof(peer->endpoint.addr6)) + memcpy(&peer->endpoint.addr6, addr, sizeof(peer->endpoint.addr6)); + break; + } + case WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL: + if (!mnl_attr_validate(attr, MNL_TYPE_U16)) + peer->persistent_keepalive_interval = mnl_attr_get_u16(attr); + break; + case WGPEER_A_LAST_HANDSHAKE_TIME: + if (mnl_attr_get_payload_len(attr) == sizeof(peer->last_handshake_time)) + memcpy(&peer->last_handshake_time, mnl_attr_get_payload(attr), sizeof(peer->last_handshake_time)); + break; + case WGPEER_A_RX_BYTES: + if (!mnl_attr_validate(attr, MNL_TYPE_U64)) + peer->rx_bytes = mnl_attr_get_u64(attr); + break; + case WGPEER_A_TX_BYTES: + if (!mnl_attr_validate(attr, MNL_TYPE_U64)) + peer->tx_bytes = mnl_attr_get_u64(attr); + break; + case WGPEER_A_ALLOWEDIPS: + return mnl_attr_parse_nested(attr, parse_allowedips, peer); + } + + return MNL_CB_OK; +} + +static int parse_peers(const struct nlattr *attr, void *data) +{ + wg_device *device = data; + wg_peer *new_peer = calloc(1, sizeof(wg_peer)); + int ret; + + if (!new_peer) + return MNL_CB_ERROR; + if (!device->first_peer) + device->first_peer = device->last_peer = new_peer; + else { + device->last_peer->next_peer = new_peer; + device->last_peer = new_peer; + } + ret = mnl_attr_parse_nested(attr, parse_peer, new_peer); + if (!ret) + return ret; + if (!(new_peer->flags & WGPEER_HAS_PUBLIC_KEY)) { + errno = ENXIO; + return MNL_CB_ERROR; + } + return MNL_CB_OK; +} + +static int parse_device(const struct nlattr *attr, void *data) +{ + wg_device *device = data; + + switch (mnl_attr_get_type(attr)) { + case WGDEVICE_A_UNSPEC: + break; + case WGDEVICE_A_IFINDEX: + if (!mnl_attr_validate(attr, MNL_TYPE_U32)) + device->ifindex = mnl_attr_get_u32(attr); + break; + case WGDEVICE_A_IFNAME: + if (!mnl_attr_validate(attr, MNL_TYPE_STRING)) { + strncpy(device->name, mnl_attr_get_str(attr), sizeof(device->name) - 1); + device->name[sizeof(device->name) - 1] = '\0'; + } + break; + case WGDEVICE_A_PRIVATE_KEY: + if (mnl_attr_get_payload_len(attr) == sizeof(device->private_key)) { + memcpy(device->private_key, mnl_attr_get_payload(attr), sizeof(device->private_key)); + device->flags |= WGDEVICE_HAS_PRIVATE_KEY; + } + break; + case WGDEVICE_A_PUBLIC_KEY: + if (mnl_attr_get_payload_len(attr) == sizeof(device->public_key)) { + memcpy(device->public_key, mnl_attr_get_payload(attr), sizeof(device->public_key)); + device->flags |= WGDEVICE_HAS_PUBLIC_KEY; + } + break; + case WGDEVICE_A_LISTEN_PORT: + if (!mnl_attr_validate(attr, MNL_TYPE_U16)) + device->listen_port = mnl_attr_get_u16(attr); + break; + case WGDEVICE_A_FWMARK: + if (!mnl_attr_validate(attr, MNL_TYPE_U32)) + device->fwmark = mnl_attr_get_u32(attr); + break; + case WGDEVICE_A_PEERS: + return mnl_attr_parse_nested(attr, parse_peers, device); + } + + return MNL_CB_OK; +} + +static int read_device_cb(const struct nlmsghdr *nlh, void *data) +{ + return mnl_attr_parse(nlh, sizeof(struct genlmsghdr), parse_device, data); +} + +static void coalesce_peers(wg_device *device) +{ + wg_peer *old_next_peer, *peer = device->first_peer; + + while (peer && peer->next_peer) { + if (memcmp(peer->public_key, peer->next_peer->public_key, sizeof(wg_key))) { + peer = peer->next_peer; + continue; + } + if (!peer->first_allowedip) { + peer->first_allowedip = peer->next_peer->first_allowedip; + peer->last_allowedip = peer->next_peer->last_allowedip; + } else { + peer->last_allowedip->next_allowedip = peer->next_peer->first_allowedip; + peer->last_allowedip = peer->next_peer->last_allowedip; + } + old_next_peer = peer->next_peer; + peer->next_peer = old_next_peer->next_peer; + free(old_next_peer); + } +} + +int wg_get_device(wg_device **device, const char *device_name) +{ + int ret = 0; + struct nlmsghdr *nlh; + struct mnlg_socket *nlg; + +try_again: + *device = calloc(1, sizeof(wg_device)); + if (!*device) + return -errno; + + nlg = mnlg_socket_open(WG_GENL_NAME, WG_GENL_VERSION); + if (!nlg) { + wg_free_device(*device); + *device = NULL; + return -errno; + } + + nlh = mnlg_msg_prepare(nlg, WG_CMD_GET_DEVICE, NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP); + mnl_attr_put_strz(nlh, WGDEVICE_A_IFNAME, device_name); + if (mnlg_socket_send(nlg, nlh) < 0) { + ret = -errno; + goto out; + } + errno = 0; + if (mnlg_socket_recv_run(nlg, read_device_cb, *device) < 0) { + ret = errno ? -errno : -EINVAL; + goto out; + } + coalesce_peers(*device); + +out: + if (nlg) + mnlg_socket_close(nlg); + if (ret) { + wg_free_device(*device); + if (ret == -EINTR) + goto try_again; + *device = NULL; + } + errno = -ret; + return ret; +} + +/* first\0second\0third\0forth\0last\0\0 */ +char *wg_list_device_names(void) +{ + struct string_list list = { 0 }; + int ret = fetch_device_names(&list); + + errno = -ret; + if (errno) { + free(list.buffer); + return NULL; + } + return list.buffer ?: strdup("\0"); +} + +int wg_add_device(const char *device_name) +{ + return add_del_iface(device_name, true); +} + +int wg_del_device(const char *device_name) +{ + return add_del_iface(device_name, false); +} + +void wg_free_device(wg_device *dev) +{ + wg_peer *peer, *np; + wg_allowedip *allowedip, *na; + + if (!dev) + return; + for (peer = dev->first_peer, np = peer ? peer->next_peer : NULL; peer; peer = np, np = peer ? peer->next_peer : NULL) { + for (allowedip = peer->first_allowedip, na = allowedip ? allowedip->next_allowedip : NULL; allowedip; allowedip = na, na = allowedip ? allowedip->next_allowedip : NULL) + free(allowedip); + free(peer); + } + free(dev); +} + +static void encode_base64(char dest[static 4], const uint8_t src[static 3]) +{ + const uint8_t input[] = { (src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63 }; + unsigned int i; + + for (i = 0; i < 4; ++i) + dest[i] = input[i] + 'A' + + (((25 - input[i]) >> 8) & 6) + - (((51 - input[i]) >> 8) & 75) + - (((61 - input[i]) >> 8) & 15) + + (((62 - input[i]) >> 8) & 3); + +} + +void wg_key_to_base64(wg_key_b64_string base64, const wg_key key) +{ + unsigned int i; + + for (i = 0; i < 32 / 3; ++i) + encode_base64(&base64[i * 4], &key[i * 3]); + encode_base64(&base64[i * 4], (const uint8_t[]){ key[i * 3 + 0], key[i * 3 + 1], 0 }); + base64[sizeof(wg_key_b64_string) - 2] = '='; + base64[sizeof(wg_key_b64_string) - 1] = '\0'; +} + +static int decode_base64(const char src[static 4]) +{ + int val = 0; + unsigned int i; + + for (i = 0; i < 4; ++i) + val |= (-1 + + ((((('A' - 1) - src[i]) & (src[i] - ('Z' + 1))) >> 8) & (src[i] - 64)) + + ((((('a' - 1) - src[i]) & (src[i] - ('z' + 1))) >> 8) & (src[i] - 70)) + + ((((('0' - 1) - src[i]) & (src[i] - ('9' + 1))) >> 8) & (src[i] + 5)) + + ((((('+' - 1) - src[i]) & (src[i] - ('+' + 1))) >> 8) & 63) + + ((((('/' - 1) - src[i]) & (src[i] - ('/' + 1))) >> 8) & 64) + ) << (18 - 6 * i); + return val; +} + +int wg_key_from_base64(wg_key key, const wg_key_b64_string base64) +{ + unsigned int i; + int val; + volatile uint8_t ret = 0; + + if (strlen(base64) != sizeof(wg_key_b64_string) - 1 || base64[sizeof(wg_key_b64_string) - 2] != '=') { + errno = EINVAL; + goto out; + } + + for (i = 0; i < 32 / 3; ++i) { + val = decode_base64(&base64[i * 4]); + ret |= (uint32_t)val >> 31; + key[i * 3 + 0] = (val >> 16) & 0xff; + key[i * 3 + 1] = (val >> 8) & 0xff; + key[i * 3 + 2] = val & 0xff; + } + val = decode_base64((const char[]){ base64[i * 4 + 0], base64[i * 4 + 1], base64[i * 4 + 2], 'A' }); + ret |= ((uint32_t)val >> 31) | (val & 0xff); + key[i * 3 + 0] = (val >> 16) & 0xff; + key[i * 3 + 1] = (val >> 8) & 0xff; + errno = EINVAL & ~((ret - 1) >> 8); +out: + return -errno; +} + +typedef int64_t fe[16]; + +static __attribute__((noinline)) void memzero_explicit(void *s, size_t count) +{ + memset(s, 0, count); + __asm__ __volatile__("": :"r"(s) :"memory"); +} + +static void carry(fe o) +{ + int i; + + for (i = 0; i < 16; ++i) { + o[(i + 1) % 16] += (i == 15 ? 38 : 1) * (o[i] >> 16); + o[i] &= 0xffff; + } +} + +static void cswap(fe p, fe q, int b) +{ + int i; + int64_t t, c = ~(b - 1); + + for (i = 0; i < 16; ++i) { + t = c & (p[i] ^ q[i]); + p[i] ^= t; + q[i] ^= t; + } + + memzero_explicit(&t, sizeof(t)); + memzero_explicit(&c, sizeof(c)); + memzero_explicit(&b, sizeof(b)); +} + +static void pack(uint8_t *o, const fe n) +{ + int i, j, b; + fe m, t; + + memcpy(t, n, sizeof(t)); + carry(t); + carry(t); + carry(t); + for (j = 0; j < 2; ++j) { + m[0] = t[0] - 0xffed; + for (i = 1; i < 15; ++i) { + m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1); + m[i - 1] &= 0xffff; + } + m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1); + b = (m[15] >> 16) & 1; + m[14] &= 0xffff; + cswap(t, m, 1 - b); + } + for (i = 0; i < 16; ++i) { + o[2 * i] = t[i] & 0xff; + o[2 * i + 1] = t[i] >> 8; + } + + memzero_explicit(m, sizeof(m)); + memzero_explicit(t, sizeof(t)); + memzero_explicit(&b, sizeof(b)); +} + +static void add(fe o, const fe a, const fe b) +{ + int i; + + for (i = 0; i < 16; ++i) + o[i] = a[i] + b[i]; +} + +static void subtract(fe o, const fe a, const fe b) +{ + int i; + + for (i = 0; i < 16; ++i) + o[i] = a[i] - b[i]; +} + +static void multmod(fe o, const fe a, const fe b) +{ + int i, j; + int64_t t[31] = { 0 }; + + for (i = 0; i < 16; ++i) { + for (j = 0; j < 16; ++j) + t[i + j] += a[i] * b[j]; + } + for (i = 0; i < 15; ++i) + t[i] += 38 * t[i + 16]; + memcpy(o, t, sizeof(fe)); + carry(o); + carry(o); + + memzero_explicit(t, sizeof(t)); +} + +static void invert(fe o, const fe i) +{ + fe c; + int a; + + memcpy(c, i, sizeof(c)); + for (a = 253; a >= 0; --a) { + multmod(c, c, c); + if (a != 2 && a != 4) + multmod(c, c, i); + } + memcpy(o, c, sizeof(fe)); + + memzero_explicit(c, sizeof(c)); +} + +static void clamp_key(uint8_t *z) +{ + z[31] = (z[31] & 127) | 64; + z[0] &= 248; +} + +void wg_generate_public_key(wg_key public_key, const wg_key private_key) +{ + int i, r; + uint8_t z[32]; + fe a = { 1 }, b = { 9 }, c = { 0 }, d = { 1 }, e, f; + + memcpy(z, private_key, sizeof(z)); + clamp_key(z); + + for (i = 254; i >= 0; --i) { + r = (z[i >> 3] >> (i & 7)) & 1; + cswap(a, b, r); + cswap(c, d, r); + add(e, a, c); + subtract(a, a, c); + add(c, b, d); + subtract(b, b, d); + multmod(d, e, e); + multmod(f, a, a); + multmod(a, c, a); + multmod(c, b, e); + add(e, a, c); + subtract(a, a, c); + multmod(b, a, a); + subtract(c, d, f); + multmod(a, c, (const fe){ 0xdb41, 1 }); + add(a, a, d); + multmod(c, c, a); + multmod(a, d, f); + multmod(d, b, (const fe){ 9 }); + multmod(b, e, e); + cswap(a, b, r); + cswap(c, d, r); + } + invert(c, c); + multmod(a, a, c); + pack(public_key, a); + + memzero_explicit(&r, sizeof(r)); + memzero_explicit(z, sizeof(z)); + memzero_explicit(a, sizeof(a)); + memzero_explicit(b, sizeof(b)); + memzero_explicit(c, sizeof(c)); + memzero_explicit(d, sizeof(d)); + memzero_explicit(e, sizeof(e)); + memzero_explicit(f, sizeof(f)); +} + +void wg_generate_private_key(wg_key private_key) +{ + wg_generate_preshared_key(private_key); + clamp_key(private_key); +} + +void wg_generate_preshared_key(wg_key preshared_key) +{ + ssize_t ret; + size_t i; + int fd; +#if defined(__OpenBSD__) || (defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_12) || (defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 25))) + if (!getentropy(preshared_key, sizeof(wg_key))) + return; +#endif +#if defined(__NR_getrandom) && defined(__linux__) + if (syscall(__NR_getrandom, preshared_key, sizeof(wg_key), 0) == sizeof(wg_key)) + return; +#endif + fd = open("/dev/urandom", O_RDONLY); + assert(fd >= 0); + for (i = 0; i < sizeof(wg_key); i += ret) { + ret = read(fd, preshared_key + i, sizeof(wg_key) - i); + assert(ret > 0); + } + close(fd); +} diff --git a/utils/rpcd-mod-wireguard/src/wireguard.h b/utils/rpcd-mod-wireguard/src/wireguard.h new file mode 100644 index 0000000000..328fcb423d --- /dev/null +++ b/utils/rpcd-mod-wireguard/src/wireguard.h @@ -0,0 +1,105 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +/* + * Copyright (C) 2015-2020 Jason A. Donenfeld . All Rights Reserved. + */ + +#ifndef WIREGUARD_H +#define WIREGUARD_H + +#include +#include +#include +#include +#include +#include + +typedef uint8_t wg_key[32]; +typedef char wg_key_b64_string[((sizeof(wg_key) + 2) / 3) * 4 + 1]; + +/* Cross platform __kernel_timespec */ +struct timespec64 { + int64_t tv_sec; + int64_t tv_nsec; +}; + +typedef struct wg_allowedip { + uint16_t family; + union { + struct in_addr ip4; + struct in6_addr ip6; + }; + uint8_t cidr; + struct wg_allowedip *next_allowedip; +} wg_allowedip; + +enum wg_peer_flags { + WGPEER_REMOVE_ME = 1U << 0, + WGPEER_REPLACE_ALLOWEDIPS = 1U << 1, + WGPEER_HAS_PUBLIC_KEY = 1U << 2, + WGPEER_HAS_PRESHARED_KEY = 1U << 3, + WGPEER_HAS_PERSISTENT_KEEPALIVE_INTERVAL = 1U << 4 +}; + +typedef union wg_endpoint { + struct sockaddr addr; + struct sockaddr_in addr4; + struct sockaddr_in6 addr6; +} wg_endpoint; + +typedef struct wg_peer { + enum wg_peer_flags flags; + + wg_key public_key; + wg_key preshared_key; + + wg_endpoint endpoint; + + struct timespec64 last_handshake_time; + uint64_t rx_bytes, tx_bytes; + uint16_t persistent_keepalive_interval; + + struct wg_allowedip *first_allowedip, *last_allowedip; + struct wg_peer *next_peer; +} wg_peer; + +enum wg_device_flags { + WGDEVICE_REPLACE_PEERS = 1U << 0, + WGDEVICE_HAS_PRIVATE_KEY = 1U << 1, + WGDEVICE_HAS_PUBLIC_KEY = 1U << 2, + WGDEVICE_HAS_LISTEN_PORT = 1U << 3, + WGDEVICE_HAS_FWMARK = 1U << 4 +}; + +typedef struct wg_device { + char name[IFNAMSIZ]; + uint32_t ifindex; + + enum wg_device_flags flags; + + wg_key public_key; + wg_key private_key; + + uint32_t fwmark; + uint16_t listen_port; + + struct wg_peer *first_peer, *last_peer; +} wg_device; + +#define wg_for_each_device_name(__names, __name, __len) for ((__name) = (__names), (__len) = 0; ((__len) = strlen(__name)); (__name) += (__len) + 1) +#define wg_for_each_peer(__dev, __peer) for ((__peer) = (__dev)->first_peer; (__peer); (__peer) = (__peer)->next_peer) +#define wg_for_each_allowedip(__peer, __allowedip) for ((__allowedip) = (__peer)->first_allowedip; (__allowedip); (__allowedip) = (__allowedip)->next_allowedip) + +int wg_set_device(wg_device *dev); +int wg_get_device(wg_device **dev, const char *device_name); +int wg_add_device(const char *device_name); +int wg_del_device(const char *device_name); +void wg_free_device(wg_device *dev); +char *wg_list_device_names(void); /* first\0second\0third\0forth\0last\0\0 */ +void wg_key_to_base64(wg_key_b64_string base64, const wg_key key); +int wg_key_from_base64(wg_key key, const wg_key_b64_string base64); +bool wg_key_is_zero(const wg_key key); +void wg_generate_public_key(wg_key public_key, const wg_key private_key); +void wg_generate_private_key(wg_key private_key); +void wg_generate_preshared_key(wg_key preshared_key); + +#endif