diff --git a/docs/quickstart.md b/docs/quickstart.md index e0ff261a4..ab0e3d9db 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -31,6 +31,18 @@ See the [install page]({{site.manual_url}}/install.html) inside manual. See the [access page]({{site.manual_url}}/remote_access.html) inside manual. +## Console setup + +The image ships the `/usr/sbin/setup` helper for basic console-first +configuration. + +The command uses a `whiptail` interface to: + +- switch the console keyboard between `it` and the default `us` +- assign physical network cards to `lan` and `wan` +- configure `lan` IPv4/CIDR +- configure `wan` as `dhcp` or `static` + ### LuCI The following sections/options should not be changed from the web interface: diff --git a/packages/ns-api/Makefile b/packages/ns-api/Makefile index 7fbefd8db..89823d564 100644 --- a/packages/ns-api/Makefile +++ b/packages/ns-api/Makefile @@ -31,6 +31,7 @@ define Package/ns-api +python3-requests \ +python3-semver \ +python3-urllib \ + +whiptail \ +sshpass \ +wireguard-tools EXTRA_DEPENDS:=adblock (>=4.5.3) @@ -183,6 +184,7 @@ define Package/ns-api/install $(INSTALL_CONF) files/nat-helpers.keep $(1)/lib/upgrade/keep.d/nat-helpers $(LN) /usr/bin/msmtp $(1)/usr/sbin/sendmail $(INSTALL_BIN) ./files/load-kernel-modules $(1)/usr/sbin/load-kernel-modules + $(INSTALL_BIN) ./files/setup $(1)/usr/sbin/setup $(INSTALL_BIN) ./files/firewall-apply-default-logging $(1)/usr/sbin/firewall-apply-default-logging $(INSTALL_DIR) $(1)/etc/config $(INSTALL_CONF) ./files/config/ns-api $(1)/etc/config/ns-api diff --git a/packages/ns-api/files/setup b/packages/ns-api/files/setup new file mode 100644 index 000000000..62170e932 --- /dev/null +++ b/packages/ns-api/files/setup @@ -0,0 +1,1160 @@ +#!/bin/sh + +# +# Copyright (C) 2026 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-2.0-only +# + +. /usr/share/libubox/jshn.sh + +STATE_FILE="/run/setup.state" +DEVICE_DB="/run/setup.devices" +DEVICES_JSON="" +RPC_OUTPUT="" +WHIPTAIL_RESULT="" + +KEYMAP="us" +LAN_IFACE="" +LAN_DEVICE="" +LAN_PROTO="static" +LAN_IP="" +WAN_IFACE="" +WAN_DEVICE="" +WAN_PROTO="dhcp" +WAN_IP="" +WAN_GW="" + +CURRENT_KEYMAP="us" +CURRENT_LAN_IFACE="lan" +CURRENT_LAN_DEVICE="" +CURRENT_LAN_DEVICE_SECTION="" +CURRENT_LAN_PROTO="static" +CURRENT_LAN_IP="" +CURRENT_LAN_BRIDGED="0" +CURRENT_LAN_MTU="" +CURRENT_LAN_IPV6_ENABLED="false" +CURRENT_WAN_IFACE="wan" +CURRENT_WAN_DEVICE="" +CURRENT_WAN_DEVICE_SECTION="" +CURRENT_WAN_PROTO="dhcp" +CURRENT_WAN_IP="" +CURRENT_WAN_GW="" +CURRENT_WAN_MTU="" +CURRENT_WAN_IPV6_ENABLED="false" +CURRENT_WAN_METRIC="" +CURRENT_WAN_DHCP_CLIENT_ID="" +CURRENT_WAN_DHCP_VENDOR_CLASS="" +CURRENT_WAN_DHCP_HOSTNAME_TO_SEND="deviceHostname" +CURRENT_WAN_DHCP_CUSTOM_HOSTNAME="" + +SELECTABLE_DEVICES="" +LAN_INTERFACES="" +WAN_INTERFACES="" + +cleanup() { + rm -f "$STATE_FILE" + rm -f "$DEVICE_DB" +} + +trap cleanup EXIT HUP INT TERM +die() { + whiptail --title "Setup" --msgbox "$1" 10 70 + exit 1 +} + +read_current_keymap() { + CURRENT_KEYMAP="us" + if [ -f /etc/keymap ] && [ "$(cat /etc/keymap 2>/dev/null)" = "it" ]; then + CURRENT_KEYMAP="it" + fi +} + +run_dialog() { + WHIPTAIL_RESULT=$(whiptail "$@" 3>&1 1>&2 2>&3) +} + +zone_interface_name() { + zone_name="$1" + set -- $(uci -q get "firewall.ns_${zone_name}.network" 2>/dev/null) + if [ -n "$1" ]; then + printf '%s' "$1" + else + printf '%s' "$zone_name" + fi +} + +uci_bool() { + case "$1" in + 1|on|true|yes) printf '%s' 'true' ;; + *) printf '%s' 'false' ;; + esac +} + +current_interface_value() { + uci -q get "network.$1.$2" 2>/dev/null || true +} + +status_json() { + ubus call "network.interface.$1" status 2>/dev/null || true +} + +status_ipv4_cidr() { + json_output=$(status_json "$1") + [ -n "$json_output" ] || return 0 + addr=$(printf '%s' "$json_output" | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null) + mask=$(printf '%s' "$json_output" | jsonfilter -e '@["ipv4-address"][0].mask' 2>/dev/null) + if [ -n "$addr" ] && [ -n "$mask" ]; then + printf '%s/%s' "$addr" "$mask" + fi +} + +status_gateway() { + json_output=$(status_json "$1") + [ -n "$json_output" ] || return 0 + printf '%s' "$json_output" | jsonfilter -e '@.route[@.target="0.0.0.0" && @.mask=0][0].nexthop' 2>/dev/null +} + +load_state() { + if [ -f "$STATE_FILE" ]; then + # shellcheck disable=SC1090 + . "$STATE_FILE" + fi +} + +save_state() { + cat > "$STATE_FILE" <&1) + status=$? + if [ $status -ne 0 ]; then + return $status + fi + + case "$RPC_OUTPUT" in + *'"error"'*|*'"validation"'*) + return 1 + ;; + esac + + return 0 +} + +read_devices_json() { + DEVICES_JSON=$(printf '{}' | /usr/libexec/rpcd/ns.devices call list-devices 2>/dev/null) || return 1 + [ -n "$DEVICES_JSON" ] +} + +append_physical_device() { + device_name="$1" + for existing_device in $SELECTABLE_DEVICES; do + [ "$existing_device" = "$device_name" ] && return 0 + done + if [ -n "$SELECTABLE_DEVICES" ]; then + SELECTABLE_DEVICES="$SELECTABLE_DEVICES $device_name" + else + SELECTABLE_DEVICES="$device_name" + fi +} + +append_interface_name() { + list_name="$1" + interface_name="$2" + current_list=$(eval "printf '%s' \"\${$list_name}\"") + for existing_name in $current_list; do + [ "$existing_name" = "$interface_name" ] && return 0 + done + if [ -n "$current_list" ]; then + eval "$list_name=\"$current_list $interface_name\"" + else + eval "$list_name=\"$interface_name\"" + fi +} + +device_for_interface() { + iface_name="$1" + device_name=$(current_interface_value "$iface_name" device) + if [ -z "$device_name" ]; then + device_name=$(printf '%s' "$DEVICES_JSON" | jsonfilter -e "@.all_devices[@.iface['.name']='$iface_name'][0].name" 2>/dev/null) + fi + printf '%s' "$device_name" +} + +proto_for_interface() { + printf '%s' "$(current_interface_value "$1" proto)" +} + +ip_for_interface() { + iface_name="$1" + iface_proto="$2" + if [ "$iface_proto" = "dhcp" ]; then + status_ipv4_cidr "$iface_name" + else + ipaddr=$(current_interface_value "$iface_name" ipaddr) + netmask=$(current_interface_value "$iface_name" netmask) + if [ -n "$ipaddr" ] && [ -n "$netmask" ] && [ "${ipaddr#*/}" = "$ipaddr" ]; then + mask_prefix=$(ipcalc.sh "$netmask" 2>/dev/null | grep PREFIX | cut -d= -f2) + [ -n "$mask_prefix" ] && ipaddr="$ipaddr/$mask_prefix" + fi + printf '%s' "$ipaddr" + fi +} + +gateway_for_interface() { + iface_name="$1" + iface_proto="$2" + if [ "$iface_proto" = "dhcp" ]; then + status_gateway "$iface_name" + else + printf '%s' "$(current_interface_value "$iface_name" gateway)" + fi +} + +load_iface_state() { + role="$1" + iface_name="$2" + device_name=$(device_for_interface "$iface_name") + iface_proto=$(proto_for_interface "$iface_name") + [ -n "$iface_proto" ] || iface_proto="static" + iface_ip=$(ip_for_interface "$iface_name" "$iface_proto") + iface_gw=$(gateway_for_interface "$iface_name" "$iface_proto") + + case "$role" in + lan) + CURRENT_LAN_IFACE="$iface_name" + CURRENT_LAN_DEVICE="$device_name" + CURRENT_LAN_PROTO="$iface_proto" + CURRENT_LAN_IP="$iface_ip" + CURRENT_LAN_DEVICE_SECTION="$iface_name" + CURRENT_LAN_MTU=$(current_interface_value "$iface_name" mtu) + CURRENT_LAN_IPV6_ENABLED=$(uci_bool "$(current_interface_value "$iface_name" ipv6)") + LAN_IFACE="$iface_name" + LAN_DEVICE="$device_name" + LAN_PROTO="$iface_proto" + LAN_IP="$iface_ip" + ;; + wan) + CURRENT_WAN_IFACE="$iface_name" + CURRENT_WAN_DEVICE="$device_name" + CURRENT_WAN_PROTO="$iface_proto" + CURRENT_WAN_IP="$iface_ip" + CURRENT_WAN_GW="$iface_gw" + CURRENT_WAN_DEVICE_SECTION="$iface_name" + CURRENT_WAN_MTU=$(current_interface_value "$iface_name" mtu) + CURRENT_WAN_IPV6_ENABLED=$(uci_bool "$(current_interface_value "$iface_name" ipv6)") + CURRENT_WAN_METRIC=$(current_interface_value "$iface_name" metric) + CURRENT_WAN_DHCP_CLIENT_ID=$(current_interface_value "$iface_name" clientid) + CURRENT_WAN_DHCP_VENDOR_CLASS=$(current_interface_value "$iface_name" vendorid) + current_wan_hostname=$(current_interface_value "$iface_name" hostname) + case "$current_wan_hostname" in + "") + CURRENT_WAN_DHCP_HOSTNAME_TO_SEND="deviceHostname" + CURRENT_WAN_DHCP_CUSTOM_HOSTNAME="" + ;; + "*") + CURRENT_WAN_DHCP_HOSTNAME_TO_SEND="doNotSendHostname" + CURRENT_WAN_DHCP_CUSTOM_HOSTNAME="" + ;; + *) + CURRENT_WAN_DHCP_HOSTNAME_TO_SEND="customHostname" + CURRENT_WAN_DHCP_CUSTOM_HOSTNAME="$current_wan_hostname" + ;; + esac + WAN_IFACE="$iface_name" + WAN_DEVICE="$device_name" + WAN_PROTO="$iface_proto" + WAN_IP="$iface_ip" + WAN_GW="$iface_gw" + ;; + esac +} + +load_zone_interfaces() { + LAN_INTERFACES="" + WAN_INTERFACES="" + for iface_name in $(uci -q get firewall.ns_lan.network 2>/dev/null); do + append_interface_name LAN_INTERFACES "$iface_name" + done + for iface_name in $(uci -q get firewall.ns_wan.network 2>/dev/null); do + append_interface_name WAN_INTERFACES "$iface_name" + done +} + +device_record() { + device_name="$1" + awk -F '\t' -v device="$device_name" '$1 == device { print; exit }' "$DEVICE_DB" 2>/dev/null +} + +device_field() { + device_name="$1" + field_number="$2" + awk -F '\t' -v device="$device_name" -v field="$field_number" '$1 == device { print $field; exit }' "$DEVICE_DB" 2>/dev/null +} + +device_kind() { + device_field "$1" 2 +} + +device_ports() { + device_field "$1" 3 +} + +json_array_from_words() { + values="$1" + first="1" + printf '[' + for value in $values; do + [ -n "$value" ] || continue + if [ "$first" = "1" ]; then + first="0" + else + printf ',' + fi + printf '"%s"' "$value" + done + printf ']' +} + +short_text() { + max_len="$1" + text="$2" + text=$(printf '%s' "$text" | tr '\n' ' ' | tr -s ' ') + if [ "${#text}" -le "$max_len" ]; then + printf '%s' "$text" + else + cut_len=$((max_len - 3)) + [ "$cut_len" -lt 1 ] && cut_len=1 + printf '%s...' "$(printf '%s' "$text" | cut -c1-"$cut_len")" + fi +} + +device_description() { + device_name="$1" + kind=$(device_kind "$device_name") + description="" + if [ "$kind" = "bridge" ]; then + ports=$(device_ports "$device_name") + if [ -n "$ports" ]; then + printf 'Bridge %s' "$ports" + else + printf 'Bridge' + fi + return 0 + fi + + slot="" + if command -v ethtool >/dev/null 2>&1; then + slot=$(ethtool -i "$device_name" 2>/dev/null | sed -n 's/^bus-info: //p') + fi + if [ -z "$slot" ]; then + device_path=$(readlink -f "/sys/class/net/$device_name/device" 2>/dev/null) + while [ -n "$device_path" ] && [ "$device_path" != "/" ]; do + base_name=${device_path##*/} + case "$base_name" in + ????:??:??.?) + slot="$base_name" + break + ;; + esac + device_path=${device_path%/*} + done + fi + if [ -n "$slot" ] && command -v lspci >/dev/null 2>&1; then + description=$(lspci -s "$slot" 2>/dev/null | cut -d ' ' -f 2-) + description=${description#Ethernet controller: } + description=${description#Network controller: } + description=${description#Red Hat, Inc. } + description=${description% (rev *} + fi + if [ -n "$description" ]; then + short_text 22 "$description" + else + printf 'Ethernet' + fi +} + +device_mac() { + cat "/sys/class/net/$1/address" 2>/dev/null +} + +device_status() { + device_name="$1" + kind=$(device_kind "$device_name") + operstate=$(cat "/sys/class/net/$device_name/operstate" 2>/dev/null) + carrier=$(cat "/sys/class/net/$device_name/carrier" 2>/dev/null) + if [ "$kind" = "bridge" ]; then + if [ "$operstate" = "up" ]; then + printf 'Up' + else + printf '%s' "${operstate:-Down}" + fi + return 0 + fi + if [ "$carrier" = "1" ]; then + printf 'Connected' + elif [ "$carrier" = "0" ]; then + printf 'No cable' + elif [ "$operstate" = "up" ]; then + printf 'Up' + else + printf '%s' "${operstate:-Down}" + fi +} + +device_menu_label() { + device_name="$1" + description=$(device_description "$device_name") + mac_address=$(device_mac "$device_name") + status=$(device_status "$device_name") + printf '%s | %s | %s' "$description" "$mac_address" "$status" +} + +build_device_db() { + : > "$DEVICE_DB" + SELECTABLE_DEVICES="" + json_cleanup + json_load "$DEVICES_JSON" || return 1 + json_select all_devices || return 1 + json_get_keys device_indexes + + for device_index in $device_indexes; do + json_select "$device_index" + json_get_var name name + json_get_var link_type link_type + json_get_var type type + + if [ -z "$name" ]; then + json_select .. + continue + fi + + if [ "$type" = "bridge" ]; then + json_get_type ports_type ports + ports="" + if [ "$ports_type" = "array" ]; then + json_select ports + json_get_keys port_indexes + for port_index in $port_indexes; do + json_get_var port_name "$port_index" + if [ -n "$ports" ]; then + ports="$ports $port_name" + else + ports="$port_name" + fi + done + json_select .. + fi + printf '%s\t%s\t%s\n' "$name" bridge "$ports" >> "$DEVICE_DB" + append_physical_device "$name" + elif [ "$link_type" = "ether" ]; then + printf '%s\t%s\t%s\n' "$name" physical '' >> "$DEVICE_DB" + append_physical_device "$name" + fi + + json_select .. + done + + json_select .. + return 0 +} + +parse_devices_json() { + CURRENT_LAN_IFACE=$(zone_interface_name lan) + CURRENT_WAN_IFACE=$(zone_interface_name wan) + CURRENT_LAN_DEVICE="" + CURRENT_LAN_DEVICE_SECTION="" + CURRENT_LAN_IP="" + CURRENT_LAN_BRIDGED="0" + CURRENT_LAN_MTU="" + CURRENT_LAN_IPV6_ENABLED="false" + CURRENT_WAN_DEVICE="" + CURRENT_WAN_DEVICE_SECTION="" + CURRENT_WAN_PROTO="dhcp" + CURRENT_WAN_IP="" + CURRENT_WAN_GW="" + CURRENT_WAN_MTU="" + CURRENT_WAN_IPV6_ENABLED="false" + CURRENT_WAN_METRIC="" + CURRENT_WAN_DHCP_CLIENT_ID="" + CURRENT_WAN_DHCP_VENDOR_CLASS="" + CURRENT_WAN_DHCP_HOSTNAME_TO_SEND="deviceHostname" + CURRENT_WAN_DHCP_CUSTOM_HOSTNAME="" + + load_zone_interfaces + build_device_db || return 1 + + json_cleanup + json_load "$DEVICES_JSON" || return 1 + json_select all_devices || return 1 + json_get_keys device_indexes + + for device_index in $device_indexes; do + json_select "$device_index" + + json_get_var name name + json_get_var link_type link_type + json_get_var type type + json_get_type iface_type iface + + iface_name="" + iface_proto="" + iface_ipaddr="" + iface_gateway="" + if [ "$iface_type" = "object" ]; then + json_select iface + json_get_var iface_name .name + json_get_var iface_proto proto + json_get_var iface_ipaddr ipaddr + json_get_var iface_gateway gateway + json_select .. + fi + + if [ "$iface_name" = "$CURRENT_LAN_IFACE" ]; then + CURRENT_LAN_PROTO="$iface_proto" + CURRENT_LAN_IP="$iface_ipaddr" + CURRENT_LAN_DEVICE="$name" + json_get_var current_lan_device_section .name + CURRENT_LAN_DEVICE_SECTION="$current_lan_device_section" + CURRENT_LAN_MTU=$(current_interface_value "$current_lan_device_section" mtu) + json_get_var current_lan_ipv6 ipv6 + CURRENT_LAN_IPV6_ENABLED=$(uci_bool "$current_lan_ipv6") + if [ "$type" = "bridge" ]; then + CURRENT_LAN_BRIDGED="1" + fi + elif [ "$iface_name" = "$CURRENT_WAN_IFACE" ]; then + CURRENT_WAN_DEVICE="$name" + CURRENT_WAN_PROTO="$iface_proto" + CURRENT_WAN_IP="$iface_ipaddr" + CURRENT_WAN_GW="$iface_gateway" + json_get_var current_wan_device_section .name + CURRENT_WAN_DEVICE_SECTION="$current_wan_device_section" + CURRENT_WAN_MTU=$(current_interface_value "$current_wan_device_section" mtu) + json_get_var current_wan_ipv6 ipv6 + CURRENT_WAN_IPV6_ENABLED=$(uci_bool "$current_wan_ipv6") + fi + + json_select .. + done + + json_select .. + + if [ "$CURRENT_LAN_PROTO" = "dhcp" ] && [ -z "$CURRENT_LAN_IP" ]; then + CURRENT_LAN_IP=$(status_ipv4_cidr "$CURRENT_LAN_IFACE") + fi + if [ "$CURRENT_WAN_PROTO" = "dhcp" ] && [ -z "$CURRENT_WAN_IP" ]; then + CURRENT_WAN_IP=$(status_ipv4_cidr "$CURRENT_WAN_IFACE") + fi + if [ "$CURRENT_WAN_PROTO" = "dhcp" ] && [ -z "$CURRENT_WAN_GW" ]; then + CURRENT_WAN_GW=$(status_gateway "$CURRENT_WAN_IFACE") + fi + + CURRENT_WAN_METRIC=$(current_interface_value "$CURRENT_WAN_IFACE" metric) + CURRENT_WAN_DHCP_CLIENT_ID=$(current_interface_value "$CURRENT_WAN_IFACE" clientid) + CURRENT_WAN_DHCP_VENDOR_CLASS=$(current_interface_value "$CURRENT_WAN_IFACE" vendorid) + current_wan_hostname=$(current_interface_value "$CURRENT_WAN_IFACE" hostname) + case "$current_wan_hostname" in + "") + CURRENT_WAN_DHCP_HOSTNAME_TO_SEND="deviceHostname" + CURRENT_WAN_DHCP_CUSTOM_HOSTNAME="" + ;; + "*") + CURRENT_WAN_DHCP_HOSTNAME_TO_SEND="doNotSendHostname" + CURRENT_WAN_DHCP_CUSTOM_HOSTNAME="" + ;; + *) + CURRENT_WAN_DHCP_HOSTNAME_TO_SEND="customHostname" + CURRENT_WAN_DHCP_CUSTOM_HOSTNAME="$current_wan_hostname" + ;; + esac + + return 0 +} + +initialize_state() { + read_current_keymap + read_devices_json || die "Unable to read current device configuration." + parse_devices_json || die "Unable to parse current device configuration." + + KEYMAP="$CURRENT_KEYMAP" + LAN_IFACE="$CURRENT_LAN_IFACE" + WAN_IFACE="$CURRENT_WAN_IFACE" + load_iface_state lan "$LAN_IFACE" + load_iface_state wan "$WAN_IFACE" + + load_state + [ -n "$LAN_IFACE" ] && load_iface_state lan "$LAN_IFACE" + [ -n "$WAN_IFACE" ] && load_iface_state wan "$WAN_IFACE" + save_state +} + +pick_keymap() { + run_dialog --title "Keymap" --cancel-button "Cancel" --radiolist "Select keyboard layout (keymap)" 12 60 2 \ + it "Italian" "$( [ "$KEYMAP" = "it" ] && printf ON || printf OFF )" \ + us "US (default)" "$( [ "$KEYMAP" = "us" ] && printf ON || printf OFF )" || return 0 + + KEYMAP="$WHIPTAIL_RESULT" + apply_keymap || { + whiptail --title "Setup Error" --msgbox "Unable to apply keymap immediately." 10 70 + read_current_keymap + KEYMAP="$CURRENT_KEYMAP" + return 1 + } + CURRENT_KEYMAP="$KEYMAP" + save_state +} + +pick_device() { + title="$1" + prompt="$2" + selected="$3" + + set -- --title "$title" --radiolist "$prompt" 18 70 8 + for device_name in $SELECTABLE_DEVICES; do + status="OFF" + [ "$device_name" = "$selected" ] && status="ON" + set -- "$@" "$device_name" "$(device_menu_label "$device_name")" "$status" + done + + run_dialog "$@" || return 1 + printf '%s\n' "$WHIPTAIL_RESULT" + return 0 +} + +pick_interface() { + title="$1" + prompt="$2" + selected="$3" + interfaces="$4" + + set -- --title "$title" --radiolist "$prompt" 18 70 8 + for iface_name in $interfaces; do + status="OFF" + [ "$iface_name" = "$selected" ] && status="ON" + description=$(printf '%s | %s | %s' "$iface_name" "$(device_for_interface "$iface_name")" "$(proto_for_interface "$iface_name")") + set -- "$@" "$iface_name" "$description" "$status" + done + + run_dialog "$@" || return 1 + printf '%s\n' "$WHIPTAIL_RESULT" + return 0 +} + +prompt_value() { + title="$1" + prompt="$2" + default_value="$3" + + run_dialog --title "$title" --inputbox "$prompt" 12 70 "$default_value" || return 1 + printf '%s\n' "$WHIPTAIL_RESULT" + return 0 +} + +pick_lan_iface() { + choice=$(pick_interface "LAN Interface" "Select the LAN interface" "$LAN_IFACE" "$LAN_INTERFACES") || return 0 + load_iface_state lan "$choice" + save_state +} + +pick_lan_device() { + choice=$(pick_device "LAN Device" "Select the LAN network device" "$LAN_DEVICE") || return 0 + LAN_DEVICE="$choice" + save_state +} + +pick_lan_proto() { + run_dialog --title "LAN Protocol" --radiolist "Select the LAN protocol" 12 60 2 \ + dhcp "Automatic configuration" "$( [ "$LAN_PROTO" = "dhcp" ] && printf ON || printf OFF )" \ + static "Static IPv4 configuration" "$( [ "$LAN_PROTO" = "static" ] && printf ON || printf OFF )" \ + || return 0 + + LAN_PROTO="$WHIPTAIL_RESULT" + if [ "$LAN_PROTO" = "dhcp" ]; then + LAN_IP="$CURRENT_LAN_IP" + else + [ -n "$LAN_IP" ] || LAN_IP="$CURRENT_LAN_IP" + pick_lan_ip + fi + save_state +} + +pick_lan_ip() { + [ "$LAN_PROTO" = "static" ] || { + whiptail --title "LAN IPv4" --msgbox "LAN IPv4 address is only required for static LAN." 10 70 + return 0 + } + while true; do + value=$(prompt_value "LAN IPv4" "Enter LAN IPv4 address in CIDR notation (e.g. 192.168.1.1/24)" "$LAN_IP") || return 0 + if validate_cidr "$value"; then + LAN_IP="$value" + save_state + return 0 + fi + whiptail --title "LAN IPv4" --msgbox "Invalid address: '$value'\nExpected format: x.x.x.x/prefix (e.g. 192.168.1.1/24)" 10 70 + done +} + +pick_wan_iface() { + choice=$(pick_interface "WAN Interface" "Select the WAN interface" "$WAN_IFACE" "$WAN_INTERFACES") || return 0 + load_iface_state wan "$choice" + save_state +} + +pick_wan_device() { + choice=$(pick_device "WAN Device" "Select the WAN network device" "$WAN_DEVICE") || return 0 + WAN_DEVICE="$choice" + save_state +} + +pick_wan_proto() { + run_dialog --title "WAN Protocol" --radiolist "Select the WAN protocol" 12 60 2 \ + dhcp "Automatic configuration" "$( [ "$WAN_PROTO" = "dhcp" ] && printf ON || printf OFF )" \ + static "Static IPv4 configuration" "$( [ "$WAN_PROTO" = "static" ] && printf ON || printf OFF )" \ + || return 0 + + WAN_PROTO="$WHIPTAIL_RESULT" + if [ "$WAN_PROTO" = "dhcp" ]; then + WAN_IP="" + WAN_GW="" + else + [ -n "$WAN_IP" ] || WAN_IP="$CURRENT_WAN_IP" + [ -n "$WAN_GW" ] || WAN_GW="$CURRENT_WAN_GW" + pick_wan_ip + pick_wan_gw + fi + save_state +} + +pick_wan_ip() { + [ "$WAN_PROTO" = "static" ] || { + whiptail --title "WAN IPv4" --msgbox "WAN IPv4 address is only required for static WAN." 10 70 + return 0 + } + while true; do + value=$(prompt_value "WAN IPv4" "Enter WAN IPv4 address in CIDR notation (e.g. 1.2.3.4/24)" "$WAN_IP") || return 0 + if validate_cidr "$value"; then + WAN_IP="$value" + save_state + return 0 + fi + whiptail --title "WAN IPv4" --msgbox "Invalid address: '$value'\nExpected format: x.x.x.x/prefix (e.g. 1.2.3.4/24)" 10 70 + done +} + +pick_wan_gw() { + [ "$WAN_PROTO" = "static" ] || { + whiptail --title "WAN Gateway" --msgbox "WAN gateway is only required for static WAN." 10 70 + return 0 + } + while true; do + value=$(prompt_value "WAN Gateway" "Enter WAN IPv4 gateway (e.g. 1.2.3.1)" "$WAN_GW") || return 0 + if validate_ipv4 "$value"; then + WAN_GW="$value" + save_state + return 0 + fi + whiptail --title "WAN Gateway" --msgbox "Invalid address: '$value'\nExpected a valid IPv4 address (e.g. 1.2.3.1)" 10 70 + done +} + +network_menu() { + while true; do + run_dialog --title "Network" --cancel-button "Back" --menu "Select the interface to configure" 14 70 3 \ + LAN "Configure LAN, local network" \ + WAN "Configure WAN, external network" \ + apply "Apply network changes" || return 0 + + case "$WHIPTAIL_RESULT" in + LAN) lan_menu ;; + WAN) wan_menu ;; + apply) apply_changes ;; + esac + done +} + +lan_menu() { + while true; do + set -- --title "LAN" --cancel-button "Back" --menu "Configure the LAN interface" 16 70 5 \ + "Interface" "${LAN_IFACE:-unset}" \ + "Device" "${LAN_DEVICE:-unset}" \ + "Protocol" "${LAN_PROTO:-unset}" + if [ "$LAN_PROTO" = "static" ]; then + set -- "$@" \ + "IPv4" "${LAN_IP:-unset}" + fi + + run_dialog "$@" || return 0 + selection="$WHIPTAIL_RESULT" + + case "$selection" in + "Interface") pick_lan_iface ;; + "Device") pick_lan_device ;; + "Protocol") pick_lan_proto ;; + "IPv4") pick_lan_ip ;; + esac + done +} + +wan_menu() { + while true; do + set -- --title "WAN" --cancel-button "Back" --menu "Configure the WAN interface" 16 70 6 \ + "Interface" "${WAN_IFACE:-unset}" \ + "Device" "${WAN_DEVICE:-unset}" \ + "Protocol" "${WAN_PROTO:-unset}" + if [ "$WAN_PROTO" = "static" ]; then + set -- "$@" \ + "IPv4" "${WAN_IP:-unset}" \ + "Gateway" "${WAN_GW:-unset}" + fi + + run_dialog "$@" || return 0 + selection="$WHIPTAIL_RESULT" + + case "$selection" in + "Interface") pick_wan_iface ;; + "Device") pick_wan_device ;; + "Protocol") pick_wan_proto ;; + "IPv4") pick_wan_ip ;; + "Gateway") pick_wan_gw ;; + esac + done +} + +validate_ipv4() { + old_ifs="$IFS" + IFS=. + set -- $1 + IFS="$old_ifs" + [ $# -eq 4 ] || return 1 + for octet in "$@"; do + case "$octet" in + ''|*[!0-9]*) return 1 ;; + esac + [ "$octet" -ge 0 ] 2>/dev/null || return 1 + [ "$octet" -le 255 ] 2>/dev/null || return 1 + case "$octet" in + 0|[1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]) ;; + *) return 1 ;; + esac + done + return 0 +} + +validate_cidr() { + case "$1" in + */*) ;; + *) return 1 ;; + esac + ip_part=${1%/*} + prefix=${1#*/} + validate_ipv4 "$ip_part" || return 1 + case "$prefix" in + ''|*[!0-9]*) return 1 ;; + esac + [ "$prefix" -ge 0 ] 2>/dev/null || return 1 + [ "$prefix" -le 32 ] 2>/dev/null || return 1 + return 0 +} + +validate_config() { + if [ -z "$LAN_DEVICE" ]; then + printf '%s\n' "LAN device is required." + return 1 + fi + + if [ -z "$WAN_DEVICE" ]; then + printf '%s\n' "WAN device is required." + return 1 + fi + + if [ "$LAN_DEVICE" = "$WAN_DEVICE" ]; then + printf '%s\n' "LAN and WAN must use different devices." + return 1 + fi + + if [ "$(device_kind "$LAN_DEVICE")" = "bridge" ] && [ "$LAN_DEVICE" != "$CURRENT_LAN_DEVICE" ]; then + printf '%s\n' "Changing LAN to a different existing bridge is not supported." + return 1 + fi + + if [ "$(device_kind "$WAN_DEVICE")" = "bridge" ] && [ "$WAN_DEVICE" != "$CURRENT_WAN_DEVICE" ]; then + printf '%s\n' "Changing WAN to a different existing bridge is not supported." + return 1 + fi + + case "$LAN_PROTO" in + dhcp|static) ;; + *) + printf '%s\n' "LAN protocol is required." + return 1 + ;; + esac + + if [ "$LAN_PROTO" = "static" ]; then + if [ -z "$LAN_IP" ] || ! validate_cidr "$LAN_IP"; then + printf '%s\n' "Static LAN IPv4 address must be in CIDR notation." + return 1 + fi + fi + + if [ "$WAN_PROTO" = "static" ]; then + if [ -z "$WAN_IP" ] || ! validate_cidr "$WAN_IP"; then + printf '%s\n' "Static WAN IPv4 address must be in CIDR notation." + return 1 + fi + + if [ -z "$WAN_GW" ]; then + printf '%s\n' "Static WAN gateway is required." + return 1 + fi + + if ! validate_ipv4 "$WAN_GW"; then + printf '%s\n' "Static WAN gateway must be a valid IPv4 address." + return 1 + fi + fi + + return 0 +} + +warn_management_change() { + warning="" + + if [ "$LAN_DEVICE" != "$CURRENT_LAN_DEVICE" ]; then + warning="LAN device will change from ${CURRENT_LAN_DEVICE:-unset} to ${LAN_DEVICE}." + fi + + if [ "$LAN_PROTO" = "static" ] && [ "$LAN_IP" != "$CURRENT_LAN_IP" ]; then + if [ -n "$warning" ]; then + warning="$warning\n" + fi + warning="${warning}LAN IP will change from ${CURRENT_LAN_IP:-unset} to ${LAN_IP}." + fi + + if [ -n "$warning" ]; then + whiptail --title "Warning" --yesno "$warning\n\nYour current SSH session may disconnect. Continue?" 14 70 || return 1 + fi + + return 0 +} + +configure_lan() { + interface_name="$CURRENT_LAN_IFACE" + [ -n "$interface_name" ] || interface_name="lan" + interface_to_edit="$interface_name" + device_kind=$(device_kind "$LAN_DEVICE") + if [ -n "$CURRENT_LAN_DEVICE" ] && [ "$LAN_DEVICE" != "$CURRENT_LAN_DEVICE" ]; then + rpc_call ns.devices unconfigure-device "{\"iface_name\":\"$interface_name\"}" || return 1 + interface_to_edit="" + fi + + if [ "$LAN_PROTO" = "dhcp" ]; then + lan_ip_json='' + lan_gw_json='' + lan_proto='dhcp' + else + lan_ip_json="$LAN_IP" + lan_gw_json='' + lan_proto='static' + fi + + if [ "$device_kind" = "bridge" ] && [ -n "$interface_to_edit" ]; then + attached_devices=$(json_array_from_words "$(device_ports "$LAN_DEVICE")") + payload=$(printf '{"device_name":"%s","device_type":"logical","logical_type":"bridge","attached_devices":%s,"interface_name":"%s","interface_to_edit":"%s","protocol":"%s","zone":"lan","ip4_address":"%s","ip4_gateway":"%s","ip4_mtu":"%s","ip6_enabled":%s}' \ + "$LAN_DEVICE" "$attached_devices" "$interface_name" "$interface_to_edit" "$lan_proto" "$lan_ip_json" "$lan_gw_json" "$CURRENT_LAN_MTU" "$CURRENT_LAN_IPV6_ENABLED") + else + payload=$(printf '{"device_name":"%s","device_type":"physical","interface_name":"%s","interface_to_edit":"%s","protocol":"%s","zone":"lan","ip4_address":"%s","ip4_gateway":"%s","ip4_mtu":"%s","ip6_enabled":%s}' \ + "$LAN_DEVICE" "$interface_name" "$interface_to_edit" "$lan_proto" "$lan_ip_json" "$lan_gw_json" "$CURRENT_LAN_MTU" "$CURRENT_LAN_IPV6_ENABLED") + fi + rpc_call ns.devices configure-device "$payload" +} + +configure_wan() { + interface_name="$CURRENT_WAN_IFACE" + [ -n "$interface_name" ] || interface_name="wan" + interface_to_edit="$interface_name" + device_kind=$(device_kind "$WAN_DEVICE") + if [ -n "$CURRENT_WAN_DEVICE" ] && [ "$WAN_DEVICE" != "$CURRENT_WAN_DEVICE" ]; then + rpc_call ns.devices unconfigure-device "{\"iface_name\":\"$interface_name\"}" || return 1 + interface_to_edit="" + fi + + if [ "$device_kind" = "bridge" ] && [ -n "$interface_to_edit" ]; then + attached_devices=$(json_array_from_words "$(device_ports "$WAN_DEVICE")") + device_type_fields=$(printf '"device_type":"logical","logical_type":"bridge","attached_devices":%s' "$attached_devices") + else + device_type_fields='"device_type":"physical"' + fi + wan_metric_json='null' + if [ -n "$CURRENT_WAN_METRIC" ]; then + wan_metric_json="$CURRENT_WAN_METRIC" + fi + + if [ "$WAN_PROTO" = "static" ]; then + payload=$(printf '{"device_name":"%s",%s,"interface_name":"%s","interface_to_edit":"%s","protocol":"static","zone":"wan","ip4_address":"%s","ip4_gateway":"%s","ip4_mtu":"%s","ip6_enabled":%s,"metric":%s}' \ + "$WAN_DEVICE" "$device_type_fields" "$interface_name" "$interface_to_edit" "$WAN_IP" "$WAN_GW" "$CURRENT_WAN_MTU" "$CURRENT_WAN_IPV6_ENABLED" "$wan_metric_json") + else + payload=$(printf '{"device_name":"%s",%s,"interface_name":"%s","interface_to_edit":"%s","protocol":"dhcp","zone":"wan","ip4_address":"","ip4_gateway":"","ip4_mtu":"%s","ip6_enabled":%s,"metric":%s,"dhcp_client_id":"%s","dhcp_vendor_class":"%s","dhcp_hostname_to_send":"%s","dhcp_custom_hostname":"%s"}' \ + "$WAN_DEVICE" "$device_type_fields" "$interface_name" "$interface_to_edit" "$CURRENT_WAN_MTU" "$CURRENT_WAN_IPV6_ENABLED" "$wan_metric_json" "$CURRENT_WAN_DHCP_CLIENT_ID" "$CURRENT_WAN_DHCP_VENDOR_CLASS" "$CURRENT_WAN_DHCP_HOSTNAME_TO_SEND" "$CURRENT_WAN_DHCP_CUSTOM_HOSTNAME") + fi + + rpc_call ns.devices configure-device "$payload" +} + +commit_network() { + rpc_call ns.commit commit '{"changes":{"network":[],"firewall":[]}}' +} + +apply_keymap() { + if [ "$KEYMAP" = "it" ]; then + printf 'it' > /etc/keymap || return 1 + /sbin/loadkmap < /usr/share/keymaps/it.map.bin || return 1 + else + rm -f /etc/keymap || return 1 + /sbin/loadkmap < /usr/share/keymaps/us.map.bin || return 1 + fi + return 0 +} + +show_apply_error() { + message="$1" + if [ -n "$RPC_OUTPUT" ]; then + message="$message\n\n$RPC_OUTPUT" + fi + whiptail --title "Setup Error" --msgbox "$message" 18 78 +} + +display_value() { + if [ -n "$1" ]; then + printf '%s' "$1" + else + printf '%s' '-' + fi +} + +network_has_changes() { + [ "$LAN_DEVICE" != "$CURRENT_LAN_DEVICE" ] && return 0 + [ "$LAN_PROTO" != "$CURRENT_LAN_PROTO" ] && return 0 + [ "$WAN_DEVICE" != "$CURRENT_WAN_DEVICE" ] && return 0 + [ "$WAN_PROTO" != "$CURRENT_WAN_PROTO" ] && return 0 + if [ "$LAN_PROTO" = "static" ] && [ "$LAN_IP" != "$CURRENT_LAN_IP" ]; then + return 0 + fi + if [ "$WAN_PROTO" = "static" ]; then + [ "$WAN_IP" != "$CURRENT_WAN_IP" ] && return 0 + [ "$WAN_GW" != "$CURRENT_WAN_GW" ] && return 0 + fi + return 1 +} + +lan_has_changes() { + [ "$LAN_DEVICE" != "$CURRENT_LAN_DEVICE" ] && return 0 + [ "$LAN_PROTO" != "$CURRENT_LAN_PROTO" ] && return 0 + if [ "$LAN_PROTO" = "static" ] && [ "$LAN_IP" != "$CURRENT_LAN_IP" ]; then + return 0 + fi + return 1 +} + +wan_has_changes() { + [ "$WAN_DEVICE" != "$CURRENT_WAN_DEVICE" ] && return 0 + [ "$WAN_PROTO" != "$CURRENT_WAN_PROTO" ] && return 0 + if [ "$WAN_PROTO" = "static" ]; then + [ "$WAN_IP" != "$CURRENT_WAN_IP" ] && return 0 + [ "$WAN_GW" != "$CURRENT_WAN_GW" ] && return 0 + fi + return 1 +} + +apply_changes() { + if ! network_has_changes; then + whiptail --title "Setup" --msgbox "No network changes to apply." 10 70 + return 0 + fi + + validation_error=$(validate_config 2>&1) || { + RPC_OUTPUT="" + show_apply_error "$validation_error" + return 1 + } + + warn_management_change || return 1 + + if [ "$LAN_PROTO" = "static" ]; then + lan_ip_summary=$(display_value "$LAN_IP") + else + lan_ip_summary='-' + fi + summary=$(printf 'LAN Device: %s\nLAN Protocol: %s\nLAN IPv4/CIDR: %s\nWAN Device: %s\nWAN Protocol: %s\nWAN IPv4/CIDR: %s\nWAN Gateway: %s' \ + "$LAN_DEVICE" "$LAN_PROTO" "$lan_ip_summary" "$WAN_DEVICE" "$WAN_PROTO" "$(display_value "$WAN_IP")" "$(display_value "$WAN_GW")") + whiptail --title "Confirm Changes" --yesno "$summary\n\nApply these changes?" 18 78 || return 0 + + if lan_has_changes; then + configure_lan || { + show_apply_error "Unable to configure LAN." + return 1 + } + fi + + if wan_has_changes; then + configure_wan || { + show_apply_error "Unable to configure WAN." + return 1 + } + fi + + commit_network || { + show_apply_error "Unable to commit network configuration." + return 1 + } + + cleanup + initialize_state + whiptail --title "Setup" --msgbox "Configuration applied successfully." 10 70 +} + +ensure_root() { + [ "$(id -u)" -eq 0 ] || die "This command must be run as root." +} + +confirm_exit() { + network_has_changes || return 0 + whiptail --title "Exit" --yesno "You have unsaved network changes.\nExit without applying?" 10 70 +} + +main_menu() { + while true; do + run_dialog --title "Setup" --cancel-button "Exit" --menu "Choose an action" 14 70 3 \ + network "Configure network" \ + keymap "Keyboard layout: ${KEYMAP}" \ + exit "Exit without applying" || { confirm_exit && break; continue; } + selection="$WHIPTAIL_RESULT" + + case "$selection" in + keymap) pick_keymap ;; + network) network_menu ;; + exit) confirm_exit && break ;; + esac + done + + cleanup +} + +ensure_root +initialize_state +main_menu