diff --git a/.gitignore b/.gitignore index a0642d6e..71641545 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ rootCA.pem vendor .env locale/**/*.mo +app/net_activity diff --git a/app/css/all.css b/app/css/all.css index b42a34e2..6c054e18 100644 --- a/app/css/all.css +++ b/app/css/all.css @@ -661,3 +661,13 @@ a.inactive:focus { max-width: 250px; } +.led-pulse { + opacity: 0.3 !important; +} + +.hostapd-led { + color: #28a745; + opacity: 1; + transition: opacity 0.05s; +} + diff --git a/app/js/custom.js b/app/js/custom.js index ffd2fc39..fba8b8bf 100644 --- a/app/js/custom.js +++ b/app/js/custom.js @@ -1022,6 +1022,31 @@ function disableValidation(form) { }); } +function updateActivityLED() { + const threshold_bytes = 300; + fetch('/app/net_activity') + .then(res => res.text()) + .then(data => { + const activity = parseInt(data.trim()); + const leds = document.querySelectorAll('.hostapd-led'); + + if (!isNaN(activity)) { + leds.forEach(led => { + if (activity > threshold_bytes) { + led.classList.add('led-pulse'); + setTimeout(() => { + led.classList.remove('led-pulse'); + }, 50); + } else { + led.classList.remove('led-pulse'); + } + }); + } + }) + .catch(() => { /* ignore fetch errors */ }); +} +setInterval(updateActivityLED, 100); + $(document).ready(function() { const $htmlElement = $('html'); const $modeswitch = $('#night-mode'); diff --git a/includes/functions.php b/includes/functions.php index 4f58feab..f4e26eca 100755 --- a/includes/functions.php +++ b/includes/functions.php @@ -971,7 +971,7 @@ function renderStatus($hostapd_led, $hostapd_status, $memused_led, $memused, $cp
Status
- +
% diff --git a/includes/hostapd.php b/includes/hostapd.php index 93386c5e..f9bd53bc 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -57,11 +57,17 @@ function DisplayHostAPDConfig() if (isset($_POST['StartHotspot']) || isset($_POST['RestartHotspot'])) { $status->addMessage('Attempting to start hotspot', 'info'); if ($arrHostapdConf['BridgedEnable'] == 1) { - exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface br0 --seconds 2', $return); + exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface br0 --seconds 1', $return); } elseif ($arrHostapdConf['WifiAPEnable'] == 1) { - exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface uap0 --seconds 2', $return); + exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface uap0 --seconds 1', $return); } else { - exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --seconds 2', $return); + // systemctl expects a unit name like raspap-network-activity@wlan0.service, no extra quotes + $iface_nonescaped = $_POST['interface']; + if (preg_match('/^[a-zA-Z0-9_-]+$/', $iface_nonescaped)) { // validate interface name + exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface ' .$iface_nonescaped. ' --seconds 1', $return); + } else { + throw new \Exception('Invalid network interface'); + } } foreach ($return as $line) { $status->addMessage($line, 'info'); @@ -69,6 +75,7 @@ function DisplayHostAPDConfig() } elseif (isset($_POST['StopHotspot'])) { $status->addMessage('Attempting to stop hotspot', 'info'); exec('sudo /bin/systemctl stop hostapd.service', $return); + exec('sudo systemctl stop "raspap-network-activity@*.service"'); foreach ($return as $line) { $status->addMessage($line, 'info'); } diff --git a/installers/common.sh b/installers/common.sh index a6a56261..2b619304 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -75,6 +75,7 @@ function _update_raspap() { _download_latest_files _change_file_ownership _patch_system_files + _enable_network_activity_monitor _create_plugin_scripts _install_complete } @@ -818,9 +819,38 @@ function _configure_networking() { echo -e _enable_raspap_daemon fi + + # Enable RaspAP network activity monitor + _enable_network_activity_monitor + _install_status 0 } +# Install and enable RaspAP network activity monitor +function _enable_network_activity_monitor() { + _install_log "Enabling RaspAP network activity monitor" + echo "Compiling raspap-network-monitor.c to /usr/local/bin/" + if ! command -v gcc >/dev/null 2>&1; then + echo "gcc not found, installing..." + sudo apt-get update + sudo apt-get install -y gcc || _install_status 1 "Failed to install gcc" + fi + sudo gcc -O2 -o /usr/local/bin/raspap-network-monitor $webroot_dir/installers/raspap-network-monitor.c || _install_status 1 "Failed to compile raspap-network-monitor.c" + echo "Copying raspap-network-activity@.service to /lib/systemd/system/" + sudo cp $webroot_dir/installers/raspap-network-activity@.service /lib/systemd/system/ || _install_status 1 "Unable to move raspap-network-activity.service file" + sudo systemctl daemon-reload + echo "Enabling raspap-network-activity@wlan0.service" + sudo systemctl enable raspap-network-activity@wlan0.service || _install_status 1 "Failed to enable raspap-network-activity.service" + echo "Starting raspap-network-activity@wlan0.service" + sudo systemctl start raspap-network-activity@wlan0.service || _install_status 1 "Failed to start raspap-network-activity.service" + sleep 0.5 + echo "Symlinking /dev/shm/net_activity to $webroot_dir/app/net_activity" + sudo ln -sf /dev/shm/net_activity $webroot_dir/app/net_activity || _install_status 1 "Failed to link net_activity to ${webroot_dir}/app" + echo "Setting ownership for ${raspap_user} on ${webroot_dir}/app/net_activity" + sudo chown -R $raspap_user:$raspap_user $webroot_dir/app/net_activity || _install_status 1 "Unable to set ownership of ${webroot_dir}/app/net_activity" + echo "Network activity monitor enabled" +} + # Prompt to configure TCP BBR option function _prompt_configure_tcp_bbr() { _install_log "Configure TCP BBR congestion control" diff --git a/installers/raspap-network-activity@.service b/installers/raspap-network-activity@.service new file mode 100644 index 00000000..615a7547 --- /dev/null +++ b/installers/raspap-network-activity@.service @@ -0,0 +1,15 @@ +# Author: BillZ + +[Unit] +Description=RaspAP Network Activity Monitor for %I +After=network.target + +[Service] +ExecStart=/usr/local/bin/raspap-network-monitor %i +Restart=always +RestartSec=2 +User=root + +[Install] +WantedBy=multi-user.target + diff --git a/installers/raspap-network-monitor.c b/installers/raspap-network-monitor.c new file mode 100644 index 00000000..93a556cf --- /dev/null +++ b/installers/raspap-network-monitor.c @@ -0,0 +1,106 @@ +// raspap-network-monitor.c + +/* +RaspAP Network Activity Monitor +Author: @billz +Author URI: https://github.com/billz/ +License: GNU General Public License v3.0 +License URI: https://github.com/raspap/raspap-webgui/blob/master/LICENSE + +Usage: raspap-network-monitor [interface] +*/ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TMPFILE "/dev/shm/net_activity" +#define POLL_INTERVAL_MS 100 // 100 milliseconds + +unsigned long read_interface_bytes(const char *iface) { + FILE *fp = fopen("/proc/net/dev", "r"); + if (!fp) return 0; + + char line[512]; + unsigned long rx = 0, tx = 0; + + while (fgets(line, sizeof(line), fp)) { + if (strstr(line, iface)) { + char *ptr = strchr(line, ':'); + if (ptr) { + sscanf(ptr + 1, "%lu %*u %*u %*u %*u %*u %*u %*u %lu", &rx, &tx); + } + break; + } + } + + fclose(fp); + return rx + tx; +} + +int main(int argc, char *argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return EXIT_FAILURE; + } + + const char *iface = argv[1]; + unsigned long prev_total = read_interface_bytes(iface); + + // create a timerfd + int tfd = timerfd_create(CLOCK_MONOTONIC, 0); + if (tfd == -1) { + perror("timerfd_create"); + return EXIT_FAILURE; + } + + struct itimerspec timer; + timer.it_interval.tv_sec = 0; + timer.it_interval.tv_nsec = POLL_INTERVAL_MS * 1000000; // interval + timer.it_value.tv_sec = 0; + timer.it_value.tv_nsec = POLL_INTERVAL_MS * 1000000; // initial expiration + + if (timerfd_settime(tfd, 0, &timer, NULL) == -1) { + perror("timerfd_settime"); + close(tfd); + return EXIT_FAILURE; + } + + struct pollfd fds; + fds.fd = tfd; + fds.events = POLLIN; + + for (;;) { + int ret = poll(&fds, 1, -1); + if (ret == -1) { + perror("poll"); + break; + } + + if (fds.revents & POLLIN) { + uint64_t expirations; + read(tfd, &expirations, sizeof(expirations)); // clear timer + + unsigned long curr_total = read_interface_bytes(iface); + unsigned long diff = (curr_total >= prev_total) ? (curr_total - prev_total) : 0; + prev_total = curr_total; + + FILE *out = fopen(TMPFILE, "w"); + if (out) { + fprintf(out, "%lu\n", diff); + fclose(out); + } + } + } + + close(tfd); + return EXIT_SUCCESS; +} + diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index c770a01b..9933cf18 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -80,3 +80,5 @@ www-data ALL=(ALL) NOPASSWD:/bin/truncate -s 0 /tmp/*.log,/bin/truncate -s 0 /va www-data ALL=(ALL) NOPASSWD:/usr/bin/vnstat * www-data ALL=(ALL) NOPASSWD:/usr/sbin/visudo -cf * www-data ALL=(ALL) NOPASSWD:/etc/raspap/plugins/plugin_helper.sh +www-data ALL=(ALL) NOPASSWD: /bin/systemctl start raspap-network-activity@*.service +www-data ALL=(ALL) NOPASSWD: /bin/systemctl stop raspap-network-activity@*.service diff --git a/installers/raspapd.service b/installers/raspapd.service index c5738465..776cb16b 100644 --- a/installers/raspapd.service +++ b/installers/raspapd.service @@ -16,7 +16,7 @@ After=multi-user.target [Service] Type=oneshot -ExecStart=/bin/bash /etc/raspap/hostapd/servicestart.sh --interface uap0 --seconds 3 +ExecStart=/bin/bash /etc/raspap/hostapd/servicestart.sh --seconds 1 RemainAfterExit=no [Install] diff --git a/installers/servicestart.sh b/installers/servicestart.sh index 377678ca..157a41a4 100755 --- a/installers/servicestart.sh +++ b/installers/servicestart.sh @@ -34,6 +34,20 @@ esac done set -- "${positional[@]}" +# Load config file into associative array +declare -A config +if [ -r "$CONFIGFILE" ]; then + while IFS=" = " read -r key value; do + config["$key"]="$value" + done < "$CONFIGFILE" +fi + +# Set interface from config if not set by parameter +if [ -z "$interface" ] && [ -n "${config[WifiInterface]}" ]; then + interface="${config[WifiInterface]}" + echo "Interface not provided. Using interface from config: $interface" +fi + echo "Stopping network services..." if [ $OPENVPNENABLED -eq 1 ]; then systemctl stop openvpn-client@client @@ -42,64 +56,58 @@ systemctl stop systemd-networkd systemctl stop hostapd.service systemctl stop dnsmasq.service systemctl stop dhcpcd.service +systemctl stop 'raspap-network-activity@*.service' if [ "${action}" = "stop" ]; then echo "Services stopped. Exiting." exit 0 fi -if [ -f "$DAEMONPATH" ] && [ ! -z "$interface" ]; then +if [ -f "$DAEMONPATH" ] && [ -n "$interface" ]; then echo "Changing RaspAP Daemon --interface to $interface" sed -i "s/\(--interface \)[[:alnum:]]*/\1$interface/" "$DAEMONPATH" fi -if [ -r "$CONFIGFILE" ]; then - declare -A config - while IFS=" = " read -r key value; do - config["$key"]="$value" - done < "$CONFIGFILE" +if [ "${config[BridgedEnable]}" = 1 ]; then + if [ "${interface}" = "br0" ]; then + echo "Stopping systemd-networkd" + systemctl stop systemd-networkd - if [ "${config[BridgedEnable]}" = 1 ]; then - if [ "${interface}" = "br0" ]; then - echo "Stopping systemd-networkd" - systemctl stop systemd-networkd + echo "Restarting eth0 interface..." + ip link set down eth0 + ip link set up eth0 - echo "Restarting eth0 interface..." - ip link set down eth0 - ip link set up eth0 + echo "Removing uap0 interface..." + iw dev uap0 del - echo "Removing uap0 interface..." - iw dev uap0 del + echo "Enabling systemd-networkd" + systemctl start systemd-networkd + systemctl enable systemd-networkd + fi +else + echo "Disabling systemd-networkd" + systemctl disable systemd-networkd - echo "Enabling systemd-networkd" - systemctl start systemd-networkd - systemctl enable systemd-networkd - fi - else - echo "Disabling systemd-networkd" - systemctl disable systemd-networkd + ip link ls up | grep -q 'br0' &> /dev/null + if [ $? == 0 ]; then + echo "Removing br0 interface..." + ip link set down br0 + ip link del dev br0 + fi - ip link ls up | grep -q 'br0' &> /dev/null - if [ $? == 0 ]; then - echo "Removing br0 interface..." - ip link set down br0 - ip link del dev br0 - fi + if [ "${config[WifiAPEnable]}" = 1 ]; then + if [ "${interface}" = "uap0" ]; then - if [ "${config[WifiAPEnable]}" = 1 ]; then - if [ "${interface}" = "uap0" ]; then - - ip link ls up | grep -q 'uap0' &> /dev/null - if [ $? == 0 ]; then - echo "Removing uap0 interface..." - iw dev uap0 del - fi - - echo "Adding uap0 interface to ${config[WifiManaged]}" - iw dev ${config[WifiManaged]} interface add uap0 type __ap - # Bring up uap0 interface - ifconfig uap0 up + ip link ls up | grep -q 'uap0' &> /dev/null + if [ $? == 0 ]; then + echo "Removing uap0 interface..." + iw dev uap0 del fi + + echo "Adding uap0 interface to ${config[WifiManaged]}" + iw dev ${config[WifiManaged]} interface add uap0 type __ap + # Bring up uap0 interface + ifconfig uap0 up fi fi fi @@ -114,12 +122,13 @@ sleep "${seconds}" systemctl start dnsmasq.service +echo "Starting raspap-network-activity@${interface}.service" +systemctl start raspap-network-activity@${interface}.service + if [ $OPENVPNENABLED -eq 1 ]; then systemctl start openvpn-client@client fi -# @mp035 found that the wifi client interface would stop every 8 seconds -# for about 16 seconds. Reassociating seems to solve this if [ "${config[WifiAPEnable]}" = 1 ]; then echo "Reassociating wifi client interface..." sleep "${seconds}" diff --git a/templates/dashboard.php b/templates/dashboard.php index c7db88f0..29b217b5 100755 --- a/templates/dashboard.php +++ b/templates/dashboard.php @@ -20,7 +20,7 @@
diff --git a/templates/hostapd.php b/templates/hostapd.php index ccdc1a62..746034b6 100755 --- a/templates/hostapd.php +++ b/templates/hostapd.php @@ -40,7 +40,7 @@