├── rules ├── __init__.py ├── vpn.py ├── driver.py ├── wifi.py ├── tcp.py ├── interface.py └── dns.py ├── config.example.yaml ├── requirements.txt ├── .gitignore ├── LICENSE ├── README.md └── network_debug.py /rules/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Network optimization rules package. 3 | 4 | This package contains modular optimization rules that can be loaded dynamically. 5 | Each rule module should export an analyze() function that takes a SystemContext 6 | and returns a list of OptimizationRule objects. 7 | """ 8 | 9 | __version__ = "1.0.1" -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | # Network Debug Tool Configuration 2 | # This file allows you to configure which optimization rules are enabled/disabled 3 | 4 | # Rules to disable (by module name) 5 | disabled_rules: 6 | # Uncomment to disable specific rule categories 7 | # - tcp # Disable TCP stack optimizations 8 | # - wifi # Disable Wi-Fi optimizations 9 | # - dns # Disable DNS optimizations 10 | # - driver # Disable driver optimizations 11 | # - interface # Disable network interface optimizations 12 | # - vpn # Disable VPN detection 13 | 14 | # Future configuration options could include: 15 | # rule_specific_settings: 16 | # tcp: 17 | # max_buffer_size: 134217728 18 | # dns: 19 | # preferred_servers: ["1.1.1.1", "8.8.8.8"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Network Debug Tool Dependencies 2 | # Python 3.7+ required 3 | 4 | # Core dependency for configuration file parsing 5 | PyYAML>=5.1 6 | 7 | # Note: All other dependencies are Python standard library modules: 8 | # - argparse (built-in) 9 | # - json (built-in) 10 | # - logging (built-in) 11 | # - os (built-in) 12 | # - re (built-in) 13 | # - subprocess (built-in) 14 | # - sys (built-in) 15 | # - time (built-in) 16 | # - tempfile (built-in) 17 | # - dataclasses (built-in Python 3.7+) 18 | # - enum (built-in) 19 | # - pathlib (built-in) 20 | # - typing (built-in) 21 | # - urllib (built-in) 22 | # - configparser (built-in) 23 | # - ipaddress (built-in) 24 | # - socket (built-in) 25 | # - shutil (built-in) 26 | # - collections (built-in) 27 | # - concurrent.futures (built-in) 28 | # - datetime (built-in) 29 | # - importlib (built-in) 30 | # - pkgutil (built-in) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python cache and bytecode 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.pyc 6 | *.pyo 7 | *.pyd 8 | 9 | # Build and distribution artifacts 10 | build/ 11 | dist/ 12 | *.egg-info/ 13 | *.egg 14 | .eggs/ 15 | 16 | # Virtual environments 17 | .venv/ 18 | venv/ 19 | env/ 20 | ENV/ 21 | .env 22 | 23 | # IDE and editor files 24 | .vscode/ 25 | .idea/ 26 | *.sublime-project 27 | *.sublime-workspace 28 | *.swp 29 | *.swo 30 | *~ 31 | .vim/ 32 | 33 | # OS-specific files 34 | .DS_Store 35 | .DS_Store? 36 | ._* 37 | .Spotlight-V100 38 | .Trashes 39 | ehthumbs.db 40 | Thumbs.db 41 | Desktop.ini 42 | 43 | # Tool-generated files 44 | network_baseline_before.json 45 | network_baseline_after.json 46 | benchmark_comparison.md 47 | *.bak.* 48 | system-config.json 49 | diagnostic.json 50 | optimization_report.md 51 | 52 | # Log files 53 | *.log 54 | 55 | # Temporary files 56 | *.tmp 57 | *.temp 58 | .cache/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 shua-ie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rules/vpn.py: -------------------------------------------------------------------------------- 1 | """ 2 | VPN optimization rules. 3 | 4 | This module detects VPN connections and recommends optimizations 5 | for VPN-related performance issues. 6 | """ 7 | 8 | import sys 9 | import os 10 | from typing import List 11 | 12 | # Add parent directory to path to allow importing from main module 13 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 14 | 15 | from network_debug import OptimizationRule, Severity 16 | 17 | 18 | def analyze(context) -> List[OptimizationRule]: 19 | """Detect VPN or proxy that might impact performance""" 20 | rules = [] 21 | 22 | # Check for VPN interfaces 23 | code, stdout, _ = context.run_command(['ip', 'route', 'show']) 24 | if code == 0: 25 | vpn_detected = False 26 | for line in stdout.split('\n'): 27 | if any(vpn in line for vpn in ['tun', 'tap', 'ppp', 'wg']): 28 | vpn_detected = True 29 | break 30 | 31 | if vpn_detected: 32 | rules.append(OptimizationRule( 33 | name="VPN Performance Check", 34 | category="VPN", 35 | severity=Severity.INFO, 36 | description="VPN connection detected", 37 | current_value="VPN active", 38 | recommended_value="check MTU settings", 39 | rationale="VPN can cause MTU issues and performance problems", 40 | fix_command="Check VPN MTU settings and enable split tunneling if possible", 41 | impact="Proper VPN configuration can improve performance by 20-50%", 42 | safe_to_auto_apply=False 43 | )) 44 | 45 | return rules -------------------------------------------------------------------------------- /rules/driver.py: -------------------------------------------------------------------------------- 1 | """ 2 | Driver optimization rules. 3 | 4 | This module analyzes network driver configuration and recommends optimizations 5 | for driver-specific performance issues. 6 | """ 7 | 8 | import sys 9 | import os 10 | from typing import List 11 | 12 | # Add parent directory to path to allow importing from main module 13 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 14 | 15 | from network_debug import OptimizationRule, Severity 16 | 17 | 18 | def analyze(context) -> List[OptimizationRule]: 19 | """Analyze driver configuration and return optimization rules""" 20 | rules = [] 21 | driver_params = context.system_config.driver_params 22 | interfaces = context.interfaces 23 | 24 | for interface in interfaces: 25 | if interface.driver in ['iwlwifi', 'rtl8192ce', 'rtl8723be']: 26 | # Check for known problematic drivers 27 | if interface.driver == 'rtl8192ce': 28 | rules.append(OptimizationRule( 29 | name="Fix RTL8192CE Driver Issues", 30 | category="Driver", 31 | severity=Severity.WARNING, 32 | description="RTL8192CE driver has known performance issues", 33 | current_value="default parameters", 34 | recommended_value="optimized parameters", 35 | rationale="Default RTL8192CE parameters cause connection drops", 36 | fix_command=f"echo 'options rtl8192ce swenc=1 ips=0' >> /etc/modprobe.d/rtl8192ce.conf", 37 | impact="Stabilizes connection and improves throughput", 38 | safe_to_auto_apply=True 39 | )) 40 | 41 | elif interface.driver == 'iwlwifi': 42 | # Check for power management issues 43 | rules.append(OptimizationRule( 44 | name="Optimize Intel Wi-Fi Driver", 45 | category="Driver", 46 | severity=Severity.INFO, 47 | description="Intel iwlwifi driver can be optimized", 48 | current_value="default parameters", 49 | recommended_value="power_save=0", 50 | rationale="Disabling driver-level power saving improves performance", 51 | fix_command="echo 'options iwlwifi power_save=0' >> /etc/modprobe.d/iwlwifi.conf", 52 | impact="Reduces latency spikes and improves reliability", 53 | safe_to_auto_apply=True 54 | )) 55 | 56 | return rules -------------------------------------------------------------------------------- /rules/wifi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wi-Fi optimization rules. 3 | 4 | This module analyzes Wi-Fi configuration and recommends optimizations 5 | for wireless network performance. 6 | """ 7 | 8 | import shutil 9 | import sys 10 | import os 11 | from typing import List 12 | 13 | # Add parent directory to path to allow importing from main module 14 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 15 | 16 | from network_debug import OptimizationRule, Severity 17 | 18 | 19 | def analyze(context) -> List[OptimizationRule]: 20 | """Analyze Wi-Fi configuration and return optimization rules""" 21 | rules = [] 22 | wifi_info = context.wifi_info 23 | 24 | if not wifi_info: 25 | return rules 26 | 27 | # Check power management (requires iw) 28 | if shutil.which('iw'): 29 | code, stdout, _ = context.run_command(['iw', wifi_info.interface, 'get', 'power_save']) 30 | if code == 0 and 'Power save: on' in stdout: 31 | rules.append(OptimizationRule( 32 | name="Disable Wi-Fi Power Management", 33 | category="Wi-Fi", 34 | severity=Severity.WARNING, 35 | description="Wi-Fi power management is reducing performance", 36 | current_value="on", 37 | recommended_value="off", 38 | rationale="Power management causes latency spikes and reduced throughput", 39 | fix_command=f"iw {wifi_info.interface} set power_save off", 40 | impact="Reduces connection drops and improves latency by 10-50ms", 41 | safe_to_auto_apply=True 42 | )) 43 | 44 | # Check frequency band 45 | if wifi_info.frequency and wifi_info.frequency < 3.0: 46 | rules.append(OptimizationRule( 47 | name="Switch to 5GHz Band", 48 | category="Wi-Fi", 49 | severity=Severity.INFO, 50 | description="Connected to 2.4GHz band", 51 | current_value=f"{wifi_info.frequency:.1f} GHz", 52 | recommended_value="5GHz", 53 | rationale="5GHz band offers more bandwidth and less congestion", 54 | fix_command="Use NetworkManager to prefer 5GHz networks", 55 | impact="Potential 2-5x speed improvement", 56 | safe_to_auto_apply=False 57 | )) 58 | 59 | # Check signal strength 60 | if wifi_info.signal_dbm and wifi_info.signal_dbm < -70: 61 | rules.append(OptimizationRule( 62 | name="Improve Wi-Fi Signal Strength", 63 | category="Wi-Fi", 64 | severity=Severity.WARNING, 65 | description="Weak Wi-Fi signal detected", 66 | current_value=f"{wifi_info.signal_dbm} dBm", 67 | recommended_value="> -50 dBm", 68 | rationale="Poor signal strength causes retransmissions and reduces throughput", 69 | fix_command="Move closer to access point or use Wi-Fi extender", 70 | impact="Signal improvement can double connection speed", 71 | safe_to_auto_apply=False 72 | )) 73 | 74 | return rules -------------------------------------------------------------------------------- /rules/tcp.py: -------------------------------------------------------------------------------- 1 | """ 2 | TCP Stack optimization rules. 3 | 4 | This module analyzes TCP stack configuration and recommends optimizations 5 | for network performance. 6 | """ 7 | 8 | from typing import Dict, List 9 | import sys 10 | import os 11 | 12 | # Add parent directory to path to allow importing from main module 13 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 14 | 15 | from network_debug import OptimizationRule, Severity 16 | 17 | 18 | def analyze(context) -> List[OptimizationRule]: 19 | """Analyze TCP stack configuration and return optimization rules""" 20 | rules = [] 21 | sysctl_params = context.system_config.sysctl_params 22 | 23 | # Define optimal TCP parameters based on modern networks 24 | tcp_optimizations = { 25 | 'net.core.rmem_max': { 26 | 'recommended': '134217728', 27 | 'rationale': 'Maximum receive buffer size for high-bandwidth networks', 28 | 'impact': 'Prevents buffer overflow on fast connections' 29 | }, 30 | 'net.core.wmem_max': { 31 | 'recommended': '134217728', 32 | 'rationale': 'Maximum send buffer size for high-bandwidth networks', 33 | 'impact': 'Improves upload performance' 34 | }, 35 | 'net.ipv4.tcp_window_scaling': { 36 | 'recommended': '1', 37 | 'rationale': 'Enables TCP window scaling for high-bandwidth delay products', 38 | 'impact': 'Critical for connections >64KB bandwidth-delay product' 39 | }, 40 | 'net.ipv4.tcp_timestamps': { 41 | 'recommended': '1', 42 | 'rationale': 'Enables TCP timestamps for better RTT estimation', 43 | 'impact': 'Improves congestion control accuracy' 44 | }, 45 | 'net.ipv4.tcp_sack': { 46 | 'recommended': '1', 47 | 'rationale': 'Enables selective acknowledgments for better loss recovery', 48 | 'impact': 'Faster recovery from packet loss' 49 | }, 50 | 'net.ipv4.tcp_congestion_control': { 51 | 'recommended': 'bbr', 52 | 'rationale': 'BBR congestion control optimizes for bandwidth and latency', 53 | 'impact': 'Better performance on varied network conditions' 54 | }, 55 | 'net.core.netdev_max_backlog': { 56 | 'recommended': '5000', 57 | 'rationale': 'Increases packet processing queue size', 58 | 'impact': 'Reduces packet drops under high load' 59 | }, 60 | 'net.ipv4.tcp_fastopen': { 61 | 'recommended': '3', 62 | 'rationale': 'Enables TCP Fast Open for both client and server', 63 | 'impact': 'Reduces connection establishment latency' 64 | } 65 | } 66 | 67 | for param, config in tcp_optimizations.items(): 68 | current_value = sysctl_params.get(param) 69 | recommended = config['recommended'] 70 | 71 | if current_value != recommended: 72 | severity = Severity.WARNING if param in ['net.core.rmem_max', 'net.core.wmem_max'] else Severity.INFO 73 | 74 | rules.append(OptimizationRule( 75 | name=f"Optimize {param}", 76 | category="TCP Stack", 77 | severity=severity, 78 | description=f"Suboptimal {param} setting", 79 | current_value=current_value or 'not set', 80 | recommended_value=recommended, 81 | rationale=config['rationale'], 82 | fix_command=f"echo '{param} = {recommended}' >> /etc/sysctl.conf && sysctl -p", 83 | impact=config['impact'], 84 | safe_to_auto_apply=True 85 | )) 86 | 87 | return rules -------------------------------------------------------------------------------- /rules/interface.py: -------------------------------------------------------------------------------- 1 | """ 2 | Network interface optimization rules. 3 | 4 | This module analyzes network interface settings and recommends optimizations 5 | for hardware-level network performance. 6 | """ 7 | 8 | import re 9 | import shutil 10 | import sys 11 | import os 12 | from typing import List 13 | 14 | # Add parent directory to path to allow importing from main module 15 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 16 | 17 | from network_debug import OptimizationRule, Severity 18 | 19 | 20 | def analyze(context) -> List[OptimizationRule]: 21 | """Analyze network interface settings and return optimization rules""" 22 | rules = [] 23 | interface_settings = context.system_config.interface_settings 24 | 25 | # Skip if ethtool is not available 26 | if not shutil.which('ethtool'): 27 | context.logger.debug("ethtool not available, skipping interface settings analysis") 28 | return rules 29 | 30 | for iface_name, settings in interface_settings.items(): 31 | # Check ring buffer sizes 32 | ring_info = settings.get('ring_buffers', '') 33 | if ring_info and 'RX:' in ring_info: 34 | try: 35 | # Extract current RX value (first occurrence of "RX:") 36 | rx_match = re.search(r'RX:\s*(\d+)', ring_info) 37 | # Extract the preset maximum (appears after "Pre-set maximums:") 38 | rx_max_match = re.search(r'Pre-set maximums:[\s\S]*?RX:\s*(\d+)', ring_info) 39 | rx_current = int(rx_match.group(1)) if rx_match else None 40 | rx_max = int(rx_max_match.group(1)) if rx_max_match else None 41 | 42 | # Only suggest increasing the ring if the hardware maximum allows it 43 | if rx_current is not None and rx_current < 1024 and (rx_max is None or rx_max >= 1024): 44 | rules.append(OptimizationRule( 45 | name=f"Increase RX Ring Buffer for {iface_name}", 46 | category="Interface", 47 | severity=Severity.INFO, 48 | description="Small RX ring buffer may cause packet drops", 49 | current_value=str(rx_current), 50 | recommended_value="1024", 51 | rationale="Larger ring buffers reduce packet drops under load", 52 | fix_command=f"ethtool -G {iface_name} rx 1024", 53 | impact="Reduces packet loss during traffic bursts", 54 | safe_to_auto_apply=True 55 | )) 56 | except (AttributeError, ValueError): 57 | pass 58 | 59 | # Check offload settings 60 | offload_info = settings.get('offload_settings', '') 61 | if offload_info: 62 | # Skip if driver marks the feature as fixed/immutable 63 | fixed_pattern = re.compile(r'tcp-segmentation-offload:\s*off.*\[fixed\]', re.IGNORECASE) 64 | if 'tcp-segmentation-offload: off' in offload_info and not fixed_pattern.search(offload_info): 65 | rules.append(OptimizationRule( 66 | name=f"Enable TCP Segmentation Offload for {iface_name}", 67 | category="Interface", 68 | severity=Severity.INFO, 69 | description="TCP segmentation offload is disabled", 70 | current_value="off", 71 | recommended_value="on", 72 | rationale="Hardware TSO reduces CPU usage for large transfers", 73 | fix_command=f"ethtool -K {iface_name} sg on gso on tso on", 74 | impact="Reduces CPU usage and improves throughput", 75 | safe_to_auto_apply=True 76 | )) 77 | 78 | return rules -------------------------------------------------------------------------------- /rules/dns.py: -------------------------------------------------------------------------------- 1 | """ 2 | DNS optimization rules. 3 | 4 | This module analyzes DNS configuration and recommends optimizations 5 | for DNS resolution performance. 6 | """ 7 | 8 | import time 9 | import shutil 10 | import sys 11 | import os 12 | from typing import List 13 | 14 | # Add parent directory to path to allow importing from main module 15 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 16 | 17 | from network_debug import OptimizationRule, Severity 18 | 19 | 20 | def analyze(context) -> List[OptimizationRule]: 21 | """Analyze DNS configuration and return optimization rules""" 22 | rules = [] 23 | dns_config = context.system_config.dns_config 24 | 25 | # Check DNS servers (only if we have internet connectivity and tools) 26 | if not context.check_internet_connectivity(): 27 | context.logger.info("Skipping DNS server analysis — no internet connectivity") 28 | return rules 29 | 30 | # Check for DNS testing tools 31 | has_dig = shutil.which('dig') 32 | has_nslookup = shutil.which('nslookup') 33 | 34 | if not has_dig and not has_nslookup: 35 | context.logger.info("Skipping DNS latency tuning — neither dig nor nslookup available") 36 | else: 37 | nameservers = dns_config.get('nameservers', '') 38 | if nameservers: 39 | slow_dns = [] 40 | dns_tool = 'dig' if has_dig else 'nslookup' 41 | 42 | for ns in nameservers.split(','): 43 | if ns.strip(): 44 | # Test DNS latency using available tool 45 | start_time = time.time() 46 | if has_dig: 47 | code, _, _ = context.run_command(['dig', '@' + ns.strip(), 'google.com', '+short'], timeout=5) 48 | else: 49 | code, _, _ = context.run_command(['nslookup', 'google.com', ns.strip()], timeout=5) 50 | latency = (time.time() - start_time) * 1000 51 | 52 | if code != 0 or latency > 100: 53 | slow_dns.append(ns.strip()) 54 | context.logger.debug(f"DNS server {ns.strip()} slow: {latency:.1f}ms") 55 | 56 | if slow_dns: 57 | rules.append(OptimizationRule( 58 | name="Optimize DNS Servers", 59 | category="DNS", 60 | severity=Severity.WARNING, 61 | description=f"Slow DNS servers detected (tested with {dns_tool})", 62 | current_value=nameservers, 63 | recommended_value="1.1.1.1, 8.8.8.8", 64 | rationale="Fast DNS servers reduce page load times", 65 | fix_command="Update /etc/resolv.conf or NetworkManager settings", 66 | impact="Reduces web browsing latency by 50-200ms", 67 | safe_to_auto_apply=False 68 | )) 69 | 70 | # Check for DNS caching (only suggest systemd-resolved if systemd is active) 71 | systemd_resolved_active = dns_config.get('systemd_resolved_active', 'inactive') 72 | if systemd_resolved_active != 'active': 73 | is_systemd = context.is_systemd_active() 74 | 75 | if is_systemd: 76 | rules.append(OptimizationRule( 77 | name="Enable DNS Caching", 78 | category="DNS", 79 | severity=Severity.INFO, 80 | description="No DNS caching service detected", 81 | current_value="disabled", 82 | recommended_value="systemd-resolved", 83 | rationale="DNS caching reduces repeated lookup latency", 84 | fix_command="systemctl enable --now systemd-resolved", 85 | impact="Reduces DNS lookup time for repeated queries", 86 | safe_to_auto_apply=True 87 | )) 88 | else: 89 | context.logger.info("Skipping systemd-resolved suggestion — systemd not detected") 90 | # Could suggest alternatives like dnsmasq for non-systemd systems 91 | rules.append(OptimizationRule( 92 | name="Enable DNS Caching", 93 | category="DNS", 94 | severity=Severity.INFO, 95 | description="No DNS caching service detected", 96 | current_value="disabled", 97 | recommended_value="dnsmasq or unbound", 98 | rationale="DNS caching reduces repeated lookup latency", 99 | fix_command="Install and configure dnsmasq or unbound", 100 | impact="Reduces DNS lookup time for repeated queries", 101 | safe_to_auto_apply=False 102 | )) 103 | 104 | return rules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Network Debug Tool for Linux 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![Python 3.7+](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/) 5 | [![Linux](https://img.shields.io/badge/platform-linux-green.svg)](https://www.linux.org/) 6 | 7 | A comprehensive CLI tool for diagnosing Wi-Fi bottlenecks and optimizing network performance on Linux systems. Built for developers, system administrators, and power users who need production-grade network optimization with safety-first design. 8 | 9 | ## Features 10 | 11 | - **Comprehensive Network Diagnosis**: Automatically detects Wi-Fi, DNS, TCP, and driver configuration issues 12 | - **Safety-First Design**: Dry-run simulation, timestamped backups, and rollback capabilities 13 | - **Interactive & Automated Modes**: Choose between guided optimization or fully automated safe fixes 14 | - **Cross-Distribution Support**: Works on Ubuntu, Pop!_OS, Arch, Fedora, Alpine, and containers 15 | - **Professional Reporting**: JSON export, Markdown reports, and before/after benchmarking 16 | - **Modular Architecture**: Plugin-based optimization rules with 6 built-in modules 17 | 18 | ## Quick Start 19 | 20 | ```bash 21 | # Download and make executable 22 | chmod +x network_debug.py 23 | 24 | # Check system dependencies 25 | ./network_debug.py --check-deps 26 | 27 | # Run comprehensive diagnosis 28 | ./network_debug.py --diagnose 29 | 30 | # Apply safe optimizations (requires root) 31 | sudo ./network_debug.py --auto-fix --dry-run # Simulate first 32 | sudo ./network_debug.py --auto-fix # Apply changes 33 | ``` 34 | 35 | ## Installation 36 | 37 | ### Requirements 38 | - **Python**: 3.7 or higher 39 | - **Platform**: Linux (any distribution) 40 | - **Privileges**: Root access required for system-level optimizations 41 | 42 | ### Dependencies 43 | ```bash 44 | # Install Python dependency 45 | pip install PyYAML>=5.1 46 | 47 | # Or install system-wide 48 | sudo apt install python3-yaml # Ubuntu/Debian 49 | sudo dnf install python3-PyYAML # Fedora 50 | sudo pacman -S python-yaml # Arch 51 | sudo apk add py3-yaml # Alpine 52 | ``` 53 | 54 | ### Optional System Tools 55 | For full functionality, install these system utilities: 56 | ```bash 57 | # Ubuntu/Debian 58 | sudo apt install iw wireless-tools ethtool speedtest-cli iperf3 dnsutils 59 | 60 | # Fedora 61 | sudo dnf install iw wireless-tools ethtool speedtest-cli iperf3 bind-utils 62 | 63 | # Arch Linux 64 | sudo pacman -S iw wireless_tools ethtool speedtest-cli iperf3 bind 65 | 66 | # Alpine Linux 67 | sudo apk add iw wireless-tools ethtool speedtest-cli iperf3 bind-tools 68 | ``` 69 | 70 | ## Usage Examples 71 | 72 | ### Basic Operations 73 | ```bash 74 | # Comprehensive network diagnosis 75 | ./network_debug.py --diagnose 76 | 77 | # Speed test only 78 | ./network_debug.py --check-speed 79 | 80 | # List available optimization rules 81 | ./network_debug.py --list-rules 82 | 83 | # Check system dependencies 84 | ./network_debug.py --check-deps 85 | ``` 86 | 87 | ### Safe Optimization 88 | ```bash 89 | # Simulate optimizations (no changes made) 90 | ./network_debug.py --auto-fix --dry-run 91 | 92 | # Apply safe optimizations 93 | sudo ./network_debug.py --auto-fix 94 | 95 | # Interactive mode with user prompts 96 | sudo ./network_debug.py --interactive 97 | 98 | # Quick performance tuning 99 | sudo ./network_debug.py --tune 100 | ``` 101 | 102 | ### Advanced Features 103 | ```bash 104 | # Generate optimization suggestions only 105 | ./network_debug.py --optimize-suggestions 106 | 107 | # System configuration analysis 108 | ./network_debug.py --crawl-config --output system-config.json 109 | 110 | # Before/after performance benchmarking 111 | ./network_debug.py --benchmark-before 112 | # ... apply optimizations ... 113 | ./network_debug.py --benchmark-after --output benchmark-report.md 114 | 115 | # Export detailed diagnostics 116 | ./network_debug.py --expert-json --output diagnostic.json 117 | 118 | # Skip specific optimization categories 119 | ./network_debug.py --auto-fix --skip-rule vpn --skip-rule driver 120 | 121 | # Use custom configuration 122 | ./network_debug.py --auto-fix --config custom-rules.yaml 123 | ``` 124 | 125 | ## Command Line Options 126 | 127 | | Option | Description | 128 | |--------|-------------| 129 | | `--diagnose` | Perform comprehensive network diagnosis (default) | 130 | | `--check-speed` | Run network speed test only | 131 | | `--tune` | Apply automatic performance optimizations | 132 | | `--auto-fix` | Automatically apply safe optimizations | 133 | | `--interactive` | Interactively prompt before applying each optimization | 134 | | `--optimize-suggestions` | Generate optimization suggestions without applying | 135 | | `--benchmark-before` | Run baseline benchmark and save measurements | 136 | | `--benchmark-after` | Run benchmark and compare with baseline | 137 | | `--benchmark-report` | Generate markdown benchmark comparison report | 138 | | `--crawl-config` | Analyze system configuration files and parameters | 139 | | `--expert-json` | Export detailed diagnostic data as JSON | 140 | | `--list-rules` | List all available optimization rules | 141 | | `--check-deps` | Check availability of system dependencies | 142 | | `--dry-run` | Simulate changes without modifying system | 143 | | `--no-color` | Disable colored output | 144 | | `--verbose, -v` | Enable verbose debug output | 145 | | `--output, -o` | Specify output file for JSON/Markdown export | 146 | | `--skip-rule` | Skip specific optimization rule (can be used multiple times) | 147 | | `--config` | Path to configuration YAML file for rule settings | 148 | | `--version` | Show program version | 149 | 150 | ## Sample Output 151 | 152 | ### Diagnostic Report 153 | ``` 154 | ============================================================ 155 | Network Diagnostic Results 156 | ============================================================ 157 | 158 | System Information: 159 | Hostname: pop-os 160 | Distribution: Pop!_OS 161 | Kernel: 6.12.10-76061203-generic 162 | Timestamp: 2025-07-08 15:04:35 163 | 164 | Network Interfaces: 165 | enp7s0: DOWN (ethernet, r8169) 166 | wlo1: UP (wifi, iwlwifi) 167 | 168 | Wi-Fi Information: 169 | Interface: wlo1 170 | SSID: $SSID 171 | Frequency: 5.2 GHz (5GHz) 172 | Channel: 44 173 | Signal: -30 dBm 174 | Bitrate: 1.2009 Gb/s 175 | 176 | Diagnostic Findings: 177 | 178 | [⚠] WARNING: 179 | • Power saving enabled on wlo1 180 | → Disable power saving for better performance: iw wlo1 set power_save off 181 | 182 | [i] INFO: 183 | • TCP tuning can improve performance 184 | → Add to /etc/sysctl.conf: 185 | net.core.rmem_max = 134217728 186 | net.core.wmem_max = 134217728 187 | net.ipv4.tcp_window_scaling = 1 188 | 189 | Summary: 1 Warning, 1 Info 190 | 191 | System Context: 192 | Wi-Fi interface: ✓ Available 193 | Internet access: ✓ Connected 194 | Init system: systemd 195 | Network tools: ✓ 7/7 tools available 196 | ✓ Full diagnostic capability 197 | ============================================================ 198 | ``` 199 | 200 | ### Dry-Run Simulation 201 | ```bash 202 | $ sudo ./network_debug.py --tune --dry-run 203 | Dry-run mode: No system changes will be made 204 | 205 | Simulating performance tuning... 206 | → Would disable power saving on wlo1 207 | Would execute: iw wlo1 set power_save off 208 | Simulating TCP optimization parameters... 209 | → Would update /etc/sysctl.conf with: 210 | net.core.rmem_max = 134217728 211 | net.core.wmem_max = 134217728 212 | net.ipv4.tcp_window_scaling = 1 213 | net.ipv4.tcp_timestamps = 1 214 | net.ipv4.tcp_sack = 1 215 | Would run 'sysctl -p /etc/sysctl.conf' 216 | Would verify all parameters are set correctly 217 | ``` 218 | 219 | ### Dependency Check 220 | ```bash 221 | $ ./network_debug.py --check-deps 222 | 223 | Dependency Status: 224 | 225 | Required: 226 | ✓ ping 227 | ✓ ip 228 | 229 | Recommended: 230 | ✓ iperf3 231 | ✓ speedtest-cli 232 | ✓ iw 233 | ✓ ethtool 234 | ✓ nmcli 235 | 236 | Optional: 237 | ✓ dig 238 | ✓ nslookup 239 | ✓ iwconfig 240 | ✓ rfkill 241 | ``` 242 | 243 | ## Optimization Rules 244 | 245 | The tool includes 6 built-in optimization modules: 246 | 247 | | Rule | Category | Description | 248 | |------|----------|-------------| 249 | | **TCP** | System | TCP buffer optimization, congestion control, window scaling | 250 | | **Wi-Fi** | Wireless | Power management, band selection, signal strength analysis | 251 | | **DNS** | Network | DNS server performance, caching configuration | 252 | | **Driver** | Hardware | Known driver issues and optimizations | 253 | | **Interface** | Hardware | Network interface hardware settings | 254 | | **VPN** | Network | VPN detection and performance recommendations | 255 | 256 | ### Custom Rules 257 | Create custom optimization rules by adding Python files to the `rules/` directory: 258 | 259 | ```python 260 | """ 261 | Custom optimization rules. 262 | """ 263 | 264 | from typing import List 265 | from network_debug import OptimizationRule, Severity 266 | 267 | def analyze(context) -> List[OptimizationRule]: 268 | """Analyze system and return optimization rules""" 269 | rules = [] 270 | 271 | # Your custom analysis logic here 272 | # Access system data via context.system_config, context.interfaces, etc. 273 | 274 | rules.append(OptimizationRule( 275 | name="Custom Optimization", 276 | category="Custom", 277 | severity=Severity.INFO, 278 | description="Custom optimization description", 279 | current_value="current", 280 | recommended_value="recommended", 281 | rationale="Why this optimization helps", 282 | fix_command="command to apply fix", 283 | impact="Expected performance improvement", 284 | safe_to_auto_apply=True 285 | )) 286 | 287 | return rules 288 | ``` 289 | 290 | ## Safety Features 291 | 292 | - **Root Privilege Enforcement**: System-level changes require explicit root access 293 | - **Dry-Run Simulation**: Preview all changes before applying them 294 | - **Timestamped Backups**: Automatic backup of all modified configuration files 295 | - **Rollback Capability**: Automatic rollback on failed verification 296 | - **Verification Checks**: Confirm all applied settings are working correctly 297 | - **No Silent Changes**: All modifications are logged and reported 298 | 299 | ## Supported Systems 300 | 301 | ### Tested Distributions 302 | - **Ubuntu** 18.04+ (including flavors like Pop!_OS) 303 | - **Fedora** 30+ 304 | - **Arch Linux** (including Manjaro) 305 | - **Alpine Linux** 3.10+ 306 | - **Debian** 10+ 307 | - **openSUSE** Leap 15+ 308 | 309 | ### Hardware Compatibility 310 | - **Wi-Fi Chipsets**: Intel (iwlwifi), Realtek, Broadcom, Atheros, MediaTek 311 | - **Network Interfaces**: Ethernet, Wi-Fi, Virtual (bridges, containers) 312 | - **Init Systems**: systemd, OpenRC, SysV 313 | 314 | ### Known Limitations 315 | - Requires Linux kernel 3.10+ for full functionality 316 | - Some optimizations require specific hardware support 317 | - Container environments may have limited network access 318 | 319 | ## Performance Impact 320 | 321 | Typical improvements after optimization: 322 | 323 | - **Wi-Fi Latency**: 20-50ms reduction with power management disabled 324 | - **TCP Throughput**: 2-5x improvement on high-bandwidth connections 325 | - **DNS Resolution**: 50-200ms faster with optimized servers 326 | - **Connection Stability**: Reduced disconnections and timeouts 327 | 328 | ## Contributing 329 | 330 | This project follows production-grade development practices: 331 | 332 | 1. **Code Quality**: All contributions must pass linting and type checking 333 | 2. **Testing**: New features require comprehensive test coverage 334 | 3. **Documentation**: Update README and inline documentation 335 | 4. **Safety**: Maintain backwards compatibility and safety checks 336 | 337 | ## License 338 | 339 | MIT License - See [LICENSE](LICENSE) file for details. 340 | 341 | ## Support 342 | 343 | - **Issues**: Report bugs and feature requests via GitHub Issues 344 | - **Documentation**: This README and inline code documentation 345 | - **Community**: Contributions welcome following the coding standards 346 | 347 | --- 348 | 349 | **Version**: 1.0.1 350 | **Python**: 3.7+ 351 | **Platform**: Linux 352 | **License**: MIT 353 | -------------------------------------------------------------------------------- /network_debug.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Network Debug Tool for Linux Wi-Fi Performance Optimization 4 | =========================================================== 5 | 6 | A comprehensive CLI-based tool for diagnosing Wi-Fi bottlenecks and optimizing 7 | network performance on Linux systems. 8 | 9 | Version: 1.0.1 10 | License: MIT 11 | Python: 3.7+ 12 | 13 | Features: 14 | - Network speed testing with iperf3/speedtest fallback 15 | - Signal strength and quality analysis 16 | - Driver and configuration validation 17 | - Smart performance recommendations 18 | - Advanced logging and JSON export 19 | - Cross-distribution support (Ubuntu, Pop!_OS, Arch, Fedora, Alpine) 20 | """ 21 | 22 | import argparse 23 | import json 24 | import logging 25 | import os 26 | import re 27 | import subprocess 28 | import sys 29 | import time 30 | import tempfile 31 | from dataclasses import dataclass, asdict 32 | from enum import Enum 33 | from pathlib import Path 34 | from typing import Dict, List, Optional, Tuple, Union 35 | import urllib.request 36 | import urllib.error 37 | import configparser 38 | import ipaddress 39 | import socket 40 | import shutil 41 | from collections import defaultdict 42 | from concurrent.futures import ThreadPoolExecutor, as_completed 43 | from datetime import datetime 44 | import yaml 45 | import importlib 46 | import pkgutil 47 | 48 | # Tool version 49 | VERSION = "1.0.1" 50 | 51 | # Color codes for terminal output 52 | class Colors: 53 | RED = '\033[91m' 54 | GREEN = '\033[92m' 55 | YELLOW = '\033[93m' 56 | BLUE = '\033[94m' 57 | MAGENTA = '\033[95m' 58 | CYAN = '\033[96m' 59 | WHITE = '\033[97m' 60 | BOLD = '\033[1m' 61 | UNDERLINE = '\033[4m' 62 | RESET = '\033[0m' 63 | 64 | # Extended color palette for severity levels 65 | BRIGHT_RED = '\033[1;31m' 66 | BRIGHT_GREEN = '\033[1;32m' 67 | BRIGHT_YELLOW = '\033[1;33m' 68 | BRIGHT_BLUE = '\033[1;34m' 69 | BRIGHT_CYAN = '\033[1;36m' 70 | 71 | # Background colors for emphasis 72 | BG_RED = '\033[41m' 73 | BG_YELLOW = '\033[43m' 74 | BG_GREEN = '\033[42m' 75 | BG_BLUE = '\033[44m' 76 | 77 | class Severity(Enum): 78 | """Severity levels for diagnostic findings""" 79 | CRITICAL = "CRITICAL" 80 | WARNING = "WARNING" 81 | INFO = "INFO" 82 | SUCCESS = "SUCCESS" 83 | 84 | # Severity icons (text-based, no emojis) 85 | class SeverityIcons: 86 | CRITICAL = "[!]" 87 | WARNING = "[⚠]" 88 | INFO = "[i]" 89 | SUCCESS = "[✓]" 90 | 91 | class ColorManager: 92 | """Manages color output based on terminal capabilities and user preferences""" 93 | 94 | def __init__(self): 95 | self.colors_enabled = self._should_use_colors() 96 | 97 | def _should_use_colors(self) -> bool: 98 | """Determine if colors should be used based on terminal and environment""" 99 | # Check NO_COLOR environment variable (per no-color.org) 100 | if os.environ.get('NO_COLOR'): 101 | return False 102 | 103 | # Check if stdout is a TTY 104 | if not sys.stdout.isatty(): 105 | return False 106 | 107 | # Check TERM environment variable 108 | term = os.environ.get('TERM', '') 109 | if term in ['dumb', 'unknown']: 110 | return False 111 | 112 | return True 113 | 114 | def set_colors_enabled(self, enabled: bool): 115 | """Override color settings (for --no-color flag)""" 116 | self.colors_enabled = enabled 117 | 118 | def colorize(self, text: str, severity: Severity) -> str: 119 | """Apply color coding based on severity""" 120 | if not self.colors_enabled: 121 | return text 122 | 123 | color_map = { 124 | Severity.CRITICAL: f"{Colors.BRIGHT_RED}{Colors.BOLD}", 125 | Severity.WARNING: f"{Colors.BRIGHT_YELLOW}{Colors.BOLD}", 126 | Severity.INFO: f"{Colors.BRIGHT_CYAN}", 127 | Severity.SUCCESS: f"{Colors.BRIGHT_GREEN}{Colors.BOLD}" 128 | } 129 | 130 | color = color_map.get(severity, Colors.WHITE) 131 | return f"{color}{text}{Colors.RESET}" 132 | 133 | def color(self, color_code: str, text: str) -> str: 134 | """Apply specific color if colors are enabled""" 135 | if not self.colors_enabled: 136 | return text 137 | return f"{color_code}{text}{Colors.RESET}" 138 | 139 | def get_severity_display(self, severity: Severity) -> str: 140 | """Get colored severity icon and text""" 141 | icon_map = { 142 | Severity.CRITICAL: SeverityIcons.CRITICAL, 143 | Severity.WARNING: SeverityIcons.WARNING, 144 | Severity.INFO: SeverityIcons.INFO, 145 | Severity.SUCCESS: SeverityIcons.SUCCESS 146 | } 147 | 148 | icon = icon_map.get(severity, SeverityIcons.INFO) 149 | if self.colors_enabled: 150 | return self.colorize(f"{icon} {severity.value}", severity) 151 | else: 152 | return f"{icon} {severity.value}" 153 | 154 | @dataclass 155 | class NetworkInterface: 156 | """Network interface information""" 157 | name: str 158 | type: str 159 | driver: str 160 | state: str 161 | mac_address: str 162 | mtu: int 163 | speed: Optional[str] = None 164 | duplex: Optional[str] = None 165 | 166 | @dataclass 167 | class WifiInfo: 168 | """Wi-Fi specific information""" 169 | interface: str 170 | ssid: str 171 | frequency: Optional[float] = None 172 | channel: Optional[int] = None 173 | signal_dbm: Optional[int] = None 174 | bitrate: Optional[str] = None 175 | tx_power: Optional[int] = None 176 | mode: Optional[str] = None 177 | security: Optional[str] = None 178 | country_code: Optional[str] = None 179 | 180 | @dataclass 181 | class SpeedTestResult: 182 | """Speed test results""" 183 | download_mbps: float 184 | upload_mbps: float 185 | ping_ms: float 186 | server: str 187 | timestamp: str 188 | test_method: str 189 | 190 | @dataclass 191 | class DiagnosticResult: 192 | """Diagnostic finding""" 193 | category: str 194 | severity: Severity 195 | message: str 196 | recommendation: Optional[str] = None 197 | technical_details: Optional[str] = None 198 | 199 | @dataclass 200 | class SystemConfig: 201 | """System configuration data""" 202 | sysctl_params: Dict[str, str] 203 | network_manager_configs: Dict[str, Dict[str, str]] 204 | dns_config: Dict[str, str] 205 | firewall_rules: List[str] 206 | driver_params: Dict[str, Dict[str, str]] 207 | interface_settings: Dict[str, Dict[str, str]] 208 | 209 | @dataclass 210 | class OptimizationRule: 211 | """Performance optimization rule""" 212 | name: str 213 | category: str 214 | severity: Severity 215 | description: str 216 | current_value: Optional[str] 217 | recommended_value: str 218 | rationale: str 219 | fix_command: str 220 | impact: str 221 | safe_to_auto_apply: bool = False 222 | 223 | @dataclass 224 | class BenchmarkResult: 225 | """Network benchmark measurements""" 226 | timestamp: str 227 | download_mbps: float 228 | upload_mbps: float 229 | ping_ms: float 230 | tcp_retransmissions: int 231 | dns_latency_ms: float 232 | test_method: str 233 | 234 | @dataclass 235 | class BenchmarkComparison: 236 | """Before/after benchmark comparison""" 237 | before: BenchmarkResult 238 | after: BenchmarkResult 239 | improvements: Dict[str, str] 240 | regressions: Dict[str, str] 241 | overall_improvement: float 242 | 243 | @dataclass 244 | class NetworkOptimization: 245 | """Network optimization analysis""" 246 | timestamp: str 247 | hostname: str 248 | system_config: SystemConfig 249 | optimization_rules: List[OptimizationRule] 250 | performance_score: float 251 | bottlenecks: List[str] 252 | recommendations_by_category: Dict[str, List[OptimizationRule]] 253 | 254 | @dataclass 255 | class NetworkDiagnostic: 256 | """Complete network diagnostic data""" 257 | timestamp: str 258 | hostname: str 259 | distribution: str 260 | kernel_version: str 261 | interfaces: List[NetworkInterface] 262 | wifi_info: Optional[WifiInfo] 263 | speed_test: Optional[SpeedTestResult] 264 | findings: List[DiagnosticResult] 265 | system_info: Dict[str, str] 266 | optimization_analysis: Optional[NetworkOptimization] = None 267 | 268 | class NetworkDebugger: 269 | """Main network debugging class""" 270 | 271 | def __init__(self, verbose: bool = False, no_color: bool = False, dry_run: bool = False, 272 | skip_rules: Optional[List[str]] = None, config_file: Optional[str] = None): 273 | self.verbose = verbose 274 | self.dry_run = dry_run 275 | self.logger = self._setup_logging() 276 | self.findings: List[DiagnosticResult] = [] 277 | self.color_manager = ColorManager() 278 | 279 | # Override color settings if --no-color flag is used 280 | if no_color: 281 | self.color_manager.set_colors_enabled(False) 282 | 283 | # Initialize rule registry 284 | self.rule_registry = RuleRegistry(self.logger) 285 | self.rule_registry.load_rules() 286 | 287 | # Load configuration file if provided 288 | if config_file: 289 | self.rule_registry.load_config(config_file) 290 | 291 | # Disable rules specified via CLI 292 | if skip_rules: 293 | for rule in skip_rules: 294 | self.rule_registry.disable_rule(rule) 295 | 296 | def _setup_logging(self) -> logging.Logger: 297 | """Configure logging""" 298 | logger = logging.getLogger('network_debug') 299 | logger.setLevel(logging.DEBUG if self.verbose else logging.INFO) 300 | 301 | handler = logging.StreamHandler() 302 | formatter = logging.Formatter( 303 | '%(asctime)s - %(levelname)s - %(message)s' 304 | ) 305 | handler.setFormatter(formatter) 306 | logger.addHandler(handler) 307 | 308 | return logger 309 | 310 | def _run_command(self, cmd: Union[str, List[str]], 311 | timeout: int = 30, 312 | check: bool = False) -> Tuple[int, str, str]: 313 | """Execute system command with timeout and error handling""" 314 | try: 315 | if isinstance(cmd, str): 316 | cmd = cmd.split() 317 | 318 | self.logger.debug(f"Executing command: {' '.join(cmd)}") 319 | 320 | result = subprocess.run( 321 | cmd, 322 | capture_output=True, 323 | text=True, 324 | timeout=timeout, 325 | check=check 326 | ) 327 | 328 | self.logger.debug(f"Command exit code: {result.returncode}") 329 | if result.stdout: 330 | self.logger.debug(f"Command stdout: {result.stdout[:500]}...") 331 | if result.stderr: 332 | self.logger.debug(f"Command stderr: {result.stderr[:500]}...") 333 | 334 | return result.returncode, result.stdout, result.stderr 335 | 336 | except subprocess.TimeoutExpired: 337 | self.logger.error(f"Command timed out after {timeout}s: {' '.join(cmd)}") 338 | return -1, "", f"Command timed out after {timeout}s" 339 | except subprocess.CalledProcessError as e: 340 | self.logger.error(f"Command failed: {' '.join(cmd)}, error: {e}") 341 | return e.returncode, e.stdout, e.stderr 342 | except FileNotFoundError as e: 343 | # Common for missing optional tools, log as debug instead of error 344 | if any(tool in ' '.join(cmd) for tool in ['iw', 'dig', 'nft', 'nslookup', 'iwconfig']): 345 | self.logger.debug(f"Optional tool not found: {' '.join(cmd)}") 346 | else: 347 | self.logger.error(f"Required command not found: {' '.join(cmd)}") 348 | return -1, "", str(e) 349 | except Exception as e: 350 | self.logger.error(f"Unexpected error running command: {e}") 351 | return -1, "", str(e) 352 | 353 | def _get_severity_counts(self) -> Dict[Severity, int]: 354 | """Get count of findings by severity level""" 355 | counts = {severity: 0 for severity in Severity} 356 | for finding in self.findings: 357 | counts[finding.severity] += 1 358 | return counts 359 | 360 | def _print_diagnostic_summary(self): 361 | """Print diagnostic summary with severity counts""" 362 | counts = self._get_severity_counts() 363 | 364 | # Build summary parts 365 | summary_parts = [] 366 | if counts[Severity.CRITICAL] > 0: 367 | summary_parts.append(f"{counts[Severity.CRITICAL]} Critical") 368 | if counts[Severity.WARNING] > 0: 369 | summary_parts.append(f"{counts[Severity.WARNING]} Warning{'s' if counts[Severity.WARNING] > 1 else ''}") 370 | if counts[Severity.INFO] > 0: 371 | summary_parts.append(f"{counts[Severity.INFO]} Info") 372 | if counts[Severity.SUCCESS] > 0: 373 | summary_parts.append(f"{counts[Severity.SUCCESS]} Success") 374 | 375 | if summary_parts: 376 | summary_text = f"Summary: {', '.join(summary_parts)}" 377 | print(f"\n{self.color_manager.color(Colors.BOLD, summary_text)}") 378 | else: 379 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Summary: No findings')}") 380 | 381 | def _print_context_summary(self): 382 | """Print summary of what checks were performed and skipped""" 383 | # Check system capabilities 384 | has_wifi = self._get_wifi_interface() is not None 385 | has_systemd = self._is_systemd_active() 386 | has_internet = self._check_internet_connectivity() 387 | 388 | # Check available tools 389 | tools = { 390 | 'nmcli': shutil.which('nmcli') is not None, 391 | 'iw': shutil.which('iw') is not None, 392 | 'iwlist': shutil.which('iwlist') is not None, 393 | 'dig': shutil.which('dig') is not None, 394 | 'nslookup': shutil.which('nslookup') is not None, 395 | 'ethtool': shutil.which('ethtool') is not None, 396 | 'rfkill': shutil.which('rfkill') is not None 397 | } 398 | 399 | print(f"\n{self.color_manager.color(Colors.BOLD, 'System Context:')}") 400 | 401 | # Wireless capability 402 | wifi_status = self.color_manager.color(Colors.BRIGHT_GREEN, "✓ Available") if has_wifi else self.color_manager.color(Colors.YELLOW, "✗ Not detected") 403 | print(f" Wi-Fi interface: {wifi_status}") 404 | 405 | # Internet connectivity 406 | internet_status = self.color_manager.color(Colors.BRIGHT_GREEN, "✓ Connected") if has_internet else self.color_manager.color(Colors.YELLOW, "✗ No connectivity") 407 | print(f" Internet access: {internet_status}") 408 | 409 | # Init system 410 | init_system = "systemd" if has_systemd else "Other (OpenRC/SysV/etc.)" 411 | init_color = Colors.BRIGHT_GREEN if has_systemd else Colors.BRIGHT_CYAN 412 | print(f" Init system: {self.color_manager.color(init_color, init_system)}") 413 | 414 | # Tool availability summary 415 | available_tools = [tool for tool, available in tools.items() if available] 416 | missing_tools = [tool for tool, available in tools.items() if not available] 417 | 418 | if available_tools: 419 | tools_text = self.color_manager.color(Colors.BRIGHT_GREEN, f"✓ {len(available_tools)}/{'7'} tools available") 420 | print(f" Network tools: {tools_text}") 421 | if self.verbose: 422 | print(f" Available: {', '.join(available_tools)}") 423 | if missing_tools: 424 | print(f" Missing: {', '.join(missing_tools)}") 425 | else: 426 | tools_text = self.color_manager.color(Colors.YELLOW, "⚠ Limited toolset") 427 | print(f" Network tools: {tools_text}") 428 | 429 | # Limitations summary 430 | limitations = [] 431 | if not has_wifi: 432 | limitations.append("Wi-Fi optimizations skipped") 433 | if not has_internet: 434 | limitations.append("Internet-dependent checks skipped") 435 | if not tools['nmcli'] and not tools['iwlist']: 436 | limitations.append("Wi-Fi scanning limited") 437 | if not tools['dig'] and not tools['nslookup']: 438 | limitations.append("DNS latency testing skipped") 439 | 440 | if limitations: 441 | print(f" Limitations: {self.color_manager.color(Colors.YELLOW, '; '.join(limitations))}") 442 | else: 443 | print(f" {self.color_manager.color(Colors.BRIGHT_GREEN, '✓ Full diagnostic capability')}") 444 | 445 | def _add_finding(self, category: str, severity: Severity, message: str, 446 | recommendation: Optional[str] = None, 447 | technical_details: Optional[str] = None): 448 | """Add a diagnostic finding""" 449 | finding = DiagnosticResult( 450 | category=category, 451 | severity=severity, 452 | message=message, 453 | recommendation=recommendation, 454 | technical_details=technical_details 455 | ) 456 | self.findings.append(finding) 457 | 458 | def _detect_distribution(self) -> str: 459 | """Detect Linux distribution""" 460 | try: 461 | with open('/etc/os-release', 'r') as f: 462 | content = f.read() 463 | if 'Ubuntu' in content: 464 | return 'Ubuntu' 465 | elif 'Pop!_OS' in content: 466 | return 'Pop!_OS' 467 | elif 'Arch' in content: 468 | return 'Arch' 469 | elif 'Fedora' in content: 470 | return 'Fedora' 471 | elif 'Alpine' in content: 472 | return 'Alpine' 473 | else: 474 | # Try to extract NAME field 475 | for line in content.split('\n'): 476 | if line.startswith('NAME='): 477 | return line.split('=')[1].strip('"') 478 | except: 479 | pass 480 | 481 | return 'Unknown' 482 | 483 | def _get_kernel_version(self) -> str: 484 | """Get kernel version""" 485 | code, stdout, _ = self._run_command(['uname', '-r']) 486 | return stdout.strip() if code == 0 else 'Unknown' 487 | 488 | def _get_network_interfaces(self) -> List[NetworkInterface]: 489 | """Get network interface information""" 490 | interfaces = [] 491 | 492 | # Get basic interface info 493 | code, stdout, _ = self._run_command(['ip', 'link', 'show']) 494 | if code != 0: 495 | return interfaces 496 | 497 | for line in stdout.split('\n'): 498 | if ': ' in line and 'state' in line.lower(): 499 | parts = line.split() 500 | if len(parts) >= 2: 501 | iface_name = parts[1].rstrip(':') 502 | 503 | # Skip loopback and docker interfaces 504 | if iface_name.startswith(('lo', 'docker', 'br-')): 505 | continue 506 | 507 | # Determine interface type with smart detection 508 | iface_type = 'unknown' 509 | if self._is_wireless_interface(iface_name): 510 | iface_type = 'wifi' 511 | elif 'en' in iface_name or 'eth' in iface_name: 512 | iface_type = 'ethernet' 513 | # Could add more detection for other types (bridge, tun, etc.) 514 | 515 | # Get more detailed info 516 | driver = self._get_interface_driver(iface_name) 517 | state = self._get_interface_state(iface_name) 518 | mac = self._get_interface_mac(iface_name) 519 | mtu = self._get_interface_mtu(iface_name) 520 | 521 | interfaces.append(NetworkInterface( 522 | name=iface_name, 523 | type=iface_type, 524 | driver=driver, 525 | state=state, 526 | mac_address=mac, 527 | mtu=mtu 528 | )) 529 | 530 | return interfaces 531 | 532 | def _get_interface_driver(self, interface: str) -> str: 533 | """Get driver for network interface""" 534 | try: 535 | driver_path = f"/sys/class/net/{interface}/device/driver" 536 | if os.path.exists(driver_path): 537 | driver_link = os.readlink(driver_path) 538 | return os.path.basename(driver_link) 539 | except: 540 | pass 541 | return 'unknown' 542 | 543 | def _get_interface_state(self, interface: str) -> str: 544 | """Get interface state""" 545 | code, stdout, _ = self._run_command(['ip', 'link', 'show', interface]) 546 | if code == 0 and 'state' in stdout.lower(): 547 | for part in stdout.split(): 548 | if part.upper() in ['UP', 'DOWN', 'UNKNOWN']: 549 | return part.upper() 550 | return 'UNKNOWN' 551 | 552 | def _get_interface_mac(self, interface: str) -> str: 553 | """Get MAC address for interface""" 554 | try: 555 | with open(f'/sys/class/net/{interface}/address', 'r') as f: 556 | return f.read().strip() 557 | except: 558 | return 'unknown' 559 | 560 | def _get_interface_mtu(self, interface: str) -> int: 561 | """Get MTU for interface""" 562 | try: 563 | with open(f'/sys/class/net/{interface}/mtu', 'r') as f: 564 | return int(f.read().strip()) 565 | except: 566 | return 0 567 | 568 | def _is_wireless_interface(self, interface: str) -> bool: 569 | """Verify if an interface is truly wireless""" 570 | try: 571 | # Check for wireless directory in sysfs (most reliable method) 572 | wireless_path = f'/sys/class/net/{interface}/wireless' 573 | if os.path.exists(wireless_path): 574 | self.logger.debug(f"Interface {interface} confirmed wireless via sysfs") 575 | return True 576 | 577 | # Fallback: check with iw dev if available 578 | if shutil.which('iw'): 579 | code, stdout, _ = self._run_command(['iw', 'dev']) 580 | if code == 0: 581 | # Look for the interface in iw dev output 582 | current_interface = None 583 | for line in stdout.split('\n'): 584 | line = line.strip() 585 | if line.startswith('Interface '): 586 | current_interface = line.split()[1] 587 | elif current_interface == interface and 'type managed' in line: 588 | self.logger.debug(f"Interface {interface} confirmed wireless via iw dev") 589 | return True 590 | 591 | # Final fallback: check iwconfig 592 | if shutil.which('iwconfig'): 593 | code, stdout, _ = self._run_command(['iwconfig', interface]) 594 | if code == 0 and 'IEEE 802.11' in stdout: 595 | self.logger.debug(f"Interface {interface} confirmed wireless via iwconfig") 596 | return True 597 | 598 | self.logger.debug(f"Interface {interface} not confirmed as wireless") 599 | return False 600 | 601 | except Exception as e: 602 | self.logger.debug(f"Error checking wireless capability for {interface}: {e}") 603 | return False 604 | 605 | def _get_wifi_interface(self) -> Optional[str]: 606 | """Get the primary Wi-Fi interface with proper wireless verification""" 607 | potential_interfaces = [] 608 | 609 | # Check all network interfaces 610 | try: 611 | net_path = Path('/sys/class/net') 612 | if net_path.exists(): 613 | for iface_path in net_path.iterdir(): 614 | if iface_path.is_dir(): 615 | iface_name = iface_path.name 616 | # Skip obvious non-wireless interfaces 617 | if iface_name.startswith(('lo', 'docker', 'br-', 'veth', 'tun', 'tap')): 618 | continue 619 | potential_interfaces.append(iface_name) 620 | except Exception as e: 621 | self.logger.debug(f"Error scanning /sys/class/net: {e}") 622 | 623 | # Verify each potential interface 624 | for interface in potential_interfaces: 625 | if self._is_wireless_interface(interface): 626 | return interface 627 | 628 | # If no interfaces found via sysfs, try iwconfig as last resort 629 | if shutil.which('iwconfig'): 630 | code, stdout, _ = self._run_command(['iwconfig']) 631 | if code == 0: 632 | for line in stdout.split('\n'): 633 | if 'IEEE 802.11' in line and 'no wireless extensions' not in line.lower(): 634 | iface_name = line.split()[0] 635 | if self._is_wireless_interface(iface_name): 636 | return iface_name 637 | 638 | self.logger.debug("No wireless interfaces found") 639 | return None 640 | 641 | def _get_wifi_info(self) -> Optional[WifiInfo]: 642 | """Get detailed Wi-Fi information""" 643 | wifi_iface = self._get_wifi_interface() 644 | if not wifi_iface: 645 | return None 646 | 647 | wifi_info = WifiInfo(interface=wifi_iface, ssid='') 648 | 649 | # Get connection info from iwconfig 650 | code, stdout, _ = self._run_command(['iwconfig', wifi_iface]) 651 | if code == 0: 652 | for line in stdout.split('\n'): 653 | if 'ESSID:' in line: 654 | match = re.search(r'ESSID:"([^"]*)"', line) 655 | if match: 656 | wifi_info.ssid = match.group(1) 657 | elif 'Frequency:' in line: 658 | match = re.search(r'Frequency:([0-9.]+)', line) 659 | if match: 660 | wifi_info.frequency = float(match.group(1)) 661 | elif 'Bit Rate=' in line: 662 | match = re.search(r'Bit Rate=([0-9.]+\s*[MG]b/s)', line) 663 | if match: 664 | wifi_info.bitrate = match.group(1) 665 | elif 'Signal level=' in line: 666 | match = re.search(r'Signal level=(-?\d+)', line) 667 | if match: 668 | wifi_info.signal_dbm = int(match.group(1)) 669 | 670 | # Get additional info from iw 671 | code, stdout, _ = self._run_command(['iw', wifi_iface, 'info']) 672 | if code == 0: 673 | for line in stdout.split('\n'): 674 | if 'channel' in line.lower(): 675 | match = re.search(r'channel\s+(\d+)', line) 676 | if match: 677 | wifi_info.channel = int(match.group(1)) 678 | elif 'txpower' in line.lower(): 679 | match = re.search(r'txpower\s+([0-9.]+)', line) 680 | if match: 681 | wifi_info.tx_power = int(float(match.group(1))) 682 | 683 | # Get country code 684 | code, stdout, _ = self._run_command(['iw', 'reg', 'get']) 685 | if code == 0: 686 | match = re.search(r'country\s+([A-Z]{2})', stdout) 687 | if match: 688 | wifi_info.country_code = match.group(1) 689 | 690 | return wifi_info 691 | 692 | def _test_speed_iperf3(self) -> Optional[SpeedTestResult]: 693 | """Test network speed using iperf3""" 694 | # Try to find a public iperf3 server 695 | servers = [ 696 | 'iperf.he.net', 697 | 'ping.online.net', 698 | 'speedtest.serverius.net' 699 | ] 700 | 701 | for server in servers: 702 | self.logger.info(f"Testing speed with iperf3 server: {server}") 703 | 704 | # Test download 705 | code, stdout, _ = self._run_command([ 706 | 'iperf3', '-c', server, '-J', '-t', '10' 707 | ], timeout=60) 708 | 709 | if code == 0: 710 | try: 711 | result = json.loads(stdout) 712 | download_bps = result['end']['sum_received']['bits_per_second'] 713 | download_mbps = download_bps / 1_000_000 714 | 715 | # Test upload 716 | code, stdout, _ = self._run_command([ 717 | 'iperf3', '-c', server, '-J', '-t', '10', '-R' 718 | ], timeout=60) 719 | 720 | if code == 0: 721 | result = json.loads(stdout) 722 | upload_bps = result['end']['sum_sent']['bits_per_second'] 723 | upload_mbps = upload_bps / 1_000_000 724 | 725 | return SpeedTestResult( 726 | download_mbps=download_mbps, 727 | upload_mbps=upload_mbps, 728 | ping_ms=0.0, # iperf3 doesn't provide ping 729 | server=server, 730 | timestamp=time.strftime('%Y-%m-%d %H:%M:%S'), 731 | test_method='iperf3' 732 | ) 733 | except (json.JSONDecodeError, KeyError) as e: 734 | self.logger.debug(f"Failed to parse iperf3 output: {e}") 735 | continue 736 | 737 | return None 738 | 739 | def _test_speed_speedtest(self) -> Optional[SpeedTestResult]: 740 | """Test network speed using speedtest-cli""" 741 | code, stdout, _ = self._run_command([ 742 | 'speedtest-cli', '--json' 743 | ], timeout=120) 744 | 745 | if code == 0: 746 | try: 747 | result = json.loads(stdout) 748 | return SpeedTestResult( 749 | download_mbps=result['download'] / 1_000_000, 750 | upload_mbps=result['upload'] / 1_000_000, 751 | ping_ms=result['ping'], 752 | server=result['server']['name'], 753 | timestamp=time.strftime('%Y-%m-%d %H:%M:%S'), 754 | test_method='speedtest-cli' 755 | ) 756 | except (json.JSONDecodeError, KeyError) as e: 757 | self.logger.debug(f"Failed to parse speedtest-cli output: {e}") 758 | 759 | return None 760 | 761 | def _check_dns_latency(self) -> Optional[float]: 762 | """Check DNS resolution latency with smart tool detection""" 763 | # Check internet connectivity first 764 | if not self._check_internet_connectivity(): 765 | self.logger.info("Skipping DNS latency check — no internet connectivity") 766 | return None 767 | 768 | # Check for available DNS tools 769 | has_dig = shutil.which('dig') 770 | has_nslookup = shutil.which('nslookup') 771 | 772 | if not has_dig and not has_nslookup: 773 | self.logger.info("Skipping DNS latency check — neither dig nor nslookup available") 774 | return None 775 | 776 | # Use dig if available (more precise timing) 777 | if has_dig: 778 | code, stdout, _ = self._run_command([ 779 | 'dig', '+stats', 'google.com' 780 | ]) 781 | 782 | if code == 0: 783 | for line in stdout.split('\n'): 784 | if 'Query time:' in line: 785 | match = re.search(r'Query time:\s*(\d+)', line) 786 | if match: 787 | latency = float(match.group(1)) 788 | self.logger.debug(f"DNS latency measured with dig: {latency}ms") 789 | return latency 790 | 791 | # Fallback to nslookup with manual timing 792 | elif has_nslookup: 793 | start_time = time.time() 794 | code, stdout, _ = self._run_command(['nslookup', 'google.com']) 795 | latency = (time.time() - start_time) * 1000 796 | 797 | if code == 0: 798 | self.logger.debug(f"DNS latency measured with nslookup: {latency:.1f}ms") 799 | return latency 800 | 801 | return None 802 | 803 | def _check_tcp_retransmissions(self) -> Dict[str, int]: 804 | """Check TCP retransmission statistics""" 805 | stats = {} 806 | 807 | try: 808 | with open('/proc/net/netstat', 'r') as f: 809 | content = f.read() 810 | 811 | for line in content.split('\n'): 812 | if line.startswith('TcpExt:'): 813 | if 'TCPRetransSegs' in line: 814 | parts = line.split() 815 | for i, part in enumerate(parts): 816 | if part == 'TCPRetransSegs' and i + 1 < len(parts): 817 | stats['retransmissions'] = int(parts[i + 1]) 818 | break 819 | except: 820 | pass 821 | 822 | return stats 823 | 824 | def _check_driver_issues(self, interfaces: List[NetworkInterface]): 825 | """Check for known driver issues""" 826 | for interface in interfaces: 827 | if interface.type == 'wifi': 828 | driver = interface.driver 829 | 830 | # Check for known problematic drivers 831 | if driver in ['rtl8192ce', 'rtl8723be']: 832 | self._add_finding( 833 | 'driver', 834 | Severity.WARNING, 835 | f"Known issues with {driver} driver", 836 | f"Consider using alternative driver or kernel parameters: " 837 | f"echo 'options {driver} swenc=1 ips=0' >> /etc/modprobe.d/{driver}.conf" 838 | ) 839 | 840 | # Check power management settings 841 | self._check_power_management(interface.name) 842 | 843 | def _check_power_management(self, interface: str): 844 | """Check Wi-Fi power management settings""" 845 | if not shutil.which('iw'): 846 | self.logger.debug("iw not available, skipping power management check") 847 | return 848 | 849 | code, stdout, _ = self._run_command(['iw', interface, 'get', 'power_save']) 850 | if code == 0 and 'Power save: on' in stdout: 851 | self._add_finding( 852 | 'power_management', 853 | Severity.WARNING, 854 | f"Power saving enabled on {interface}", 855 | f"Disable power saving for better performance: " 856 | f"iw {interface} set power_save off" 857 | ) 858 | 859 | def _check_rfkill_status(self): 860 | """Check if Wi-Fi is blocked by rfkill""" 861 | if not shutil.which('rfkill'): 862 | self.logger.debug("rfkill not available, skipping rfkill check") 863 | return 864 | 865 | code, stdout, _ = self._run_command(['rfkill', 'list']) 866 | if code == 0: 867 | for line in stdout.split('\n'): 868 | if 'wlan' in line.lower() or 'wireless' in line.lower(): 869 | if 'blocked: yes' in line.lower(): 870 | self._add_finding( 871 | 'rfkill', 872 | Severity.CRITICAL, 873 | "Wi-Fi is blocked by rfkill", 874 | "Unblock Wi-Fi: rfkill unblock wifi" 875 | ) 876 | 877 | def _check_channel_congestion(self): 878 | """Check for Wi-Fi channel congestion with smart context awareness""" 879 | # First check if we have a wireless interface 880 | wifi_iface = self._get_wifi_interface() 881 | if not wifi_iface: 882 | self.logger.info("Skipping channel congestion check — no wireless interfaces detected") 883 | return 884 | 885 | if not shutil.which('nmcli'): 886 | self.logger.info("Skipping channel congestion check — nmcli not available") 887 | return 888 | 889 | code, stdout, _ = self._run_command(['nmcli', 'dev', 'wifi', 'list']) 890 | if code == 0: 891 | channels = {} 892 | networks_found = 0 893 | 894 | for line in stdout.split('\n')[1:]: # Skip header 895 | parts = line.split() 896 | if len(parts) >= 3 and parts[0] != '--': # Valid network entry 897 | try: 898 | channel = int(parts[2]) 899 | channels[channel] = channels.get(channel, 0) + 1 900 | networks_found += 1 901 | except ValueError: 902 | continue 903 | 904 | if networks_found == 0: 905 | self.logger.info("Skipping channel congestion check — no Wi-Fi networks detected in scan") 906 | return 907 | 908 | self.logger.debug(f"Found {networks_found} Wi-Fi networks across {len(channels)} channels") 909 | 910 | # Check for overcrowded channels 911 | congested_channels = [] 912 | for channel, count in channels.items(): 913 | if count > 3: 914 | congested_channels.append((channel, count)) 915 | 916 | if congested_channels: 917 | for channel, count in congested_channels: 918 | self._add_finding( 919 | 'channel_congestion', 920 | Severity.WARNING, 921 | f"Channel {channel} has {count} networks (congested)", 922 | f"Consider switching to a less congested channel" 923 | ) 924 | else: 925 | self.logger.debug("No channel congestion detected") 926 | else: 927 | self.logger.info("Skipping channel congestion check — nmcli wifi scan failed") 928 | 929 | def _scan_for_5ghz_networks(self, current_ssid: str) -> bool: 930 | """Scan for available 5GHz networks, preferably matching current SSID""" 931 | found_5ghz = False 932 | same_ssid_5ghz = False 933 | 934 | # Try nmcli first (most reliable) 935 | if shutil.which('nmcli'): 936 | self.logger.debug("Scanning for 5GHz networks with nmcli") 937 | code, stdout, _ = self._run_command(['nmcli', 'dev', 'wifi', 'list']) 938 | if code == 0: 939 | for line in stdout.split('\n')[1:]: # Skip header 940 | if line.strip(): 941 | parts = line.split() 942 | if len(parts) >= 4: 943 | try: 944 | # Extract SSID and frequency 945 | ssid = parts[1] if parts[1] != '--' else '' 946 | freq_str = parts[3] if len(parts) > 3 else '' 947 | 948 | # Check if frequency indicates 5GHz (5000+ MHz) 949 | if freq_str.isdigit() and int(freq_str) >= 5000: 950 | found_5ghz = True 951 | if ssid == current_ssid: 952 | same_ssid_5ghz = True 953 | self.logger.debug(f"Found 5GHz network for current SSID: {ssid}") 954 | break 955 | except (ValueError, IndexError): 956 | continue 957 | 958 | # Fallback to iwlist scan if nmcli not available 959 | elif shutil.which('iwlist'): 960 | wifi_iface = self._get_wifi_interface() 961 | if wifi_iface: 962 | self.logger.debug("Scanning for 5GHz networks with iwlist") 963 | code, stdout, _ = self._run_command(['iwlist', wifi_iface, 'scan']) 964 | if code == 0: 965 | current_ssid_block = False 966 | current_freq = None 967 | 968 | for line in stdout.split('\n'): 969 | line = line.strip() 970 | if 'ESSID:' in line: 971 | essid = line.split('ESSID:')[1].strip('"') 972 | if essid == current_ssid: 973 | current_ssid_block = True 974 | else: 975 | current_ssid_block = False 976 | elif 'Frequency:' in line: 977 | freq_match = re.search(r'Frequency:([0-9.]+)', line) 978 | if freq_match: 979 | freq_ghz = float(freq_match.group(1)) 980 | if freq_ghz >= 5.0: 981 | found_5ghz = True 982 | if current_ssid_block: 983 | same_ssid_5ghz = True 984 | self.logger.debug(f"Found 5GHz network for current SSID via iwlist") 985 | break 986 | else: 987 | self.logger.debug("Neither nmcli nor iwlist available for 5GHz scanning") 988 | return False 989 | 990 | if not found_5ghz: 991 | self.logger.info("No 5GHz networks detected in scan") 992 | elif same_ssid_5ghz: 993 | self.logger.info(f"5GHz variant of current SSID '{current_ssid}' detected") 994 | else: 995 | self.logger.info("5GHz networks detected but not for current SSID") 996 | 997 | return same_ssid_5ghz 998 | 999 | def _check_band_usage(self, wifi_info: WifiInfo): 1000 | """Check if using optimal Wi-Fi band with smart 5GHz detection""" 1001 | if wifi_info.frequency: 1002 | if wifi_info.frequency < 3.0: # 2.4GHz 1003 | self.logger.debug(f"Connected to 2.4GHz band ({wifi_info.frequency:.1f} GHz)") 1004 | 1005 | # Check if tools for 5GHz scanning are available 1006 | if not shutil.which('nmcli') and not shutil.which('iwlist'): 1007 | self.logger.info("Skipping 5GHz band check — neither nmcli nor iwlist available") 1008 | return 1009 | 1010 | # Scan for 5GHz networks 1011 | if self._scan_for_5ghz_networks(wifi_info.ssid): 1012 | self._add_finding( 1013 | 'band_usage', 1014 | Severity.WARNING, 1015 | f"Connected to 2.4GHz band, 5GHz variant of '{wifi_info.ssid}' available", 1016 | f"Switch to 5GHz band for better performance" 1017 | ) 1018 | else: 1019 | self.logger.info(f"Skipping 5GHz suggestion — no 5GHz variant of '{wifi_info.ssid}' detected") 1020 | 1021 | def _suggest_tcp_tuning(self): 1022 | """Suggest TCP tuning parameters""" 1023 | suggestions = [ 1024 | "net.core.rmem_max = 134217728", 1025 | "net.core.wmem_max = 134217728", 1026 | "net.ipv4.tcp_rmem = 4096 87380 134217728", 1027 | "net.ipv4.tcp_wmem = 4096 65536 134217728", 1028 | "net.ipv4.tcp_window_scaling = 1", 1029 | "net.ipv4.tcp_timestamps = 1", 1030 | "net.ipv4.tcp_sack = 1" 1031 | ] 1032 | 1033 | self._add_finding( 1034 | 'tcp_tuning', 1035 | Severity.INFO, 1036 | "TCP tuning can improve performance", 1037 | f"Add to /etc/sysctl.conf:\n" + "\n".join(suggestions) 1038 | ) 1039 | 1040 | def _crawl_sysctl_config(self) -> Dict[str, str]: 1041 | """Crawl current sysctl parameters""" 1042 | sysctl_params = {} 1043 | 1044 | # Get all current sysctl values 1045 | code, stdout, _ = self._run_command(['sysctl', '-a'], timeout=60) 1046 | if code == 0: 1047 | for line in stdout.split('\n'): 1048 | if '=' in line: 1049 | try: 1050 | key, value = line.split('=', 1) 1051 | sysctl_params[key.strip()] = value.strip() 1052 | except ValueError: 1053 | continue 1054 | 1055 | # Also read /etc/sysctl.conf if it exists 1056 | try: 1057 | with open('/etc/sysctl.conf', 'r') as f: 1058 | for line in f: 1059 | line = line.strip() 1060 | if line and not line.startswith('#') and '=' in line: 1061 | key, value = line.split('=', 1) 1062 | sysctl_params[f"config_{key.strip()}"] = value.strip() 1063 | except (FileNotFoundError, IOError): 1064 | pass 1065 | 1066 | return sysctl_params 1067 | 1068 | def _crawl_network_manager_configs(self) -> Dict[str, Dict[str, str]]: 1069 | """Crawl NetworkManager connection configurations""" 1070 | configs = {} 1071 | 1072 | # Skip if NetworkManager is not available (e.g., in Alpine/minimal containers) 1073 | if not shutil.which('nmcli'): 1074 | self.logger.debug("NetworkManager not available, skipping config crawl") 1075 | return configs 1076 | 1077 | nm_path = Path('/etc/NetworkManager/system-connections') 1078 | 1079 | if nm_path.exists(): 1080 | for config_file in nm_path.glob('*.nmconnection'): 1081 | try: 1082 | parser = configparser.ConfigParser() 1083 | parser.read(config_file) 1084 | 1085 | config_data = {} 1086 | for section in parser.sections(): 1087 | for key, value in parser.items(section): 1088 | config_data[f"{section}.{key}"] = value 1089 | 1090 | configs[config_file.stem] = config_data 1091 | except Exception as e: 1092 | self.logger.debug(f"Failed to parse {config_file}: {e}") 1093 | 1094 | return configs 1095 | 1096 | def _crawl_dns_config(self) -> Dict[str, str]: 1097 | """Crawl DNS configuration""" 1098 | dns_config = {} 1099 | 1100 | # Check /etc/resolv.conf 1101 | try: 1102 | with open('/etc/resolv.conf', 'r') as f: 1103 | content = f.read() 1104 | dns_config['resolv_conf_raw'] = content 1105 | 1106 | nameservers = [] 1107 | for line in content.split('\n'): 1108 | if line.strip().startswith('nameserver'): 1109 | nameservers.append(line.split()[1]) 1110 | dns_config['nameservers'] = ','.join(nameservers) 1111 | except (FileNotFoundError, IOError): 1112 | pass 1113 | 1114 | # Check systemd-resolved status 1115 | code, stdout, _ = self._run_command(['resolvectl', 'status']) 1116 | if code == 0: 1117 | dns_config['resolvectl_status'] = stdout 1118 | 1119 | # Check if systemd-resolved is active 1120 | code, stdout, _ = self._run_command(['systemctl', 'is-active', 'systemd-resolved']) 1121 | if code == 0: 1122 | dns_config['systemd_resolved_active'] = stdout.strip() 1123 | 1124 | return dns_config 1125 | 1126 | def _crawl_firewall_rules(self) -> List[str]: 1127 | """Crawl firewall rules that might impact performance""" 1128 | rules = [] 1129 | 1130 | # Check iptables 1131 | for table in ['filter', 'nat', 'mangle']: 1132 | code, stdout, _ = self._run_command(['iptables', '-t', table, '-L', '-n']) 1133 | if code == 0: 1134 | rule_count = len(stdout.split('\n')) 1135 | rules.append(f"iptables_{table}: {rule_count} rules") 1136 | 1137 | # Check nftables 1138 | code, stdout, _ = self._run_command(['nft', 'list', 'ruleset']) 1139 | if code == 0: 1140 | line_count = len(stdout.split('\n')) 1141 | rules.append(f"nftables: {line_count} lines") 1142 | 1143 | return rules 1144 | 1145 | def _crawl_driver_params(self) -> Dict[str, Dict[str, str]]: 1146 | """Crawl driver parameters""" 1147 | driver_params = {} 1148 | 1149 | # Check modprobe configs 1150 | modprobe_dirs = ['/etc/modprobe.d', '/usr/lib/modprobe.d'] 1151 | for mod_dir in modprobe_dirs: 1152 | if Path(mod_dir).exists(): 1153 | for config_file in Path(mod_dir).glob('*.conf'): 1154 | try: 1155 | with open(config_file, 'r') as f: 1156 | content = f.read() 1157 | driver_params[config_file.stem] = {'content': content} 1158 | except (FileNotFoundError, IOError): 1159 | pass 1160 | 1161 | # Check loaded modules 1162 | code, stdout, _ = self._run_command(['lsmod']) 1163 | if code == 0: 1164 | driver_params['loaded_modules'] = {'content': stdout} 1165 | 1166 | return driver_params 1167 | 1168 | def _crawl_interface_settings(self) -> Dict[str, Dict[str, str]]: 1169 | """Crawl network interface settings""" 1170 | interface_settings = {} 1171 | 1172 | # Skip if ethtool is not available 1173 | if not shutil.which('ethtool'): 1174 | self.logger.debug("ethtool not available, skipping interface settings crawl") 1175 | return interface_settings 1176 | 1177 | interfaces = self._get_network_interfaces() 1178 | for interface in interfaces: 1179 | iface_name = interface.name 1180 | settings = {} 1181 | 1182 | # Get ethtool information 1183 | code, stdout, _ = self._run_command(['ethtool', iface_name]) 1184 | if code == 0: 1185 | settings['ethtool_info'] = stdout 1186 | 1187 | # Get ring buffer settings 1188 | code, stdout, _ = self._run_command(['ethtool', '-g', iface_name]) 1189 | if code == 0: 1190 | settings['ring_buffers'] = stdout 1191 | 1192 | # Get offload settings 1193 | code, stdout, _ = self._run_command(['ethtool', '-k', iface_name]) 1194 | if code == 0: 1195 | settings['offload_settings'] = stdout 1196 | 1197 | # Get driver info 1198 | code, stdout, _ = self._run_command(['ethtool', '-i', iface_name]) 1199 | if code == 0: 1200 | settings['driver_info'] = stdout 1201 | 1202 | if settings: 1203 | interface_settings[iface_name] = settings 1204 | 1205 | return interface_settings 1206 | 1207 | def _calculate_performance_score(self, rules: List[OptimizationRule]) -> float: 1208 | """Calculate overall performance score""" 1209 | total_score = 100.0 1210 | 1211 | for rule in rules: 1212 | if rule.severity == Severity.CRITICAL: 1213 | total_score -= 15.0 1214 | elif rule.severity == Severity.WARNING: 1215 | total_score -= 10.0 1216 | elif rule.severity == Severity.INFO: 1217 | total_score -= 5.0 1218 | 1219 | return max(0.0, total_score) 1220 | 1221 | def _identify_bottlenecks(self, rules: List[OptimizationRule]) -> List[str]: 1222 | """Identify primary performance bottlenecks""" 1223 | bottlenecks = [] 1224 | 1225 | critical_rules = [r for r in rules if r.severity == Severity.CRITICAL] 1226 | warning_rules = [r for r in rules if r.severity == Severity.WARNING] 1227 | 1228 | if critical_rules: 1229 | bottlenecks.append("Critical configuration issues detected") 1230 | 1231 | if len(warning_rules) > 3: 1232 | bottlenecks.append("Multiple configuration warnings") 1233 | 1234 | # Check for specific bottleneck patterns 1235 | tcp_issues = [r for r in rules if r.category == "TCP Stack"] 1236 | if len(tcp_issues) > 2: 1237 | bottlenecks.append("TCP stack not optimized for high-bandwidth networks") 1238 | 1239 | wifi_issues = [r for r in rules if r.category == "Wi-Fi"] 1240 | if len(wifi_issues) > 1: 1241 | bottlenecks.append("Wi-Fi configuration issues affecting performance") 1242 | 1243 | return bottlenecks 1244 | 1245 | def crawl_config(self) -> SystemConfig: 1246 | """Crawl comprehensive system configuration""" 1247 | print(f"{self.color_manager.color(Colors.BOLD, 'Crawling system configuration...')}") 1248 | 1249 | # Use threading to parallelize I/O operations 1250 | with ThreadPoolExecutor(max_workers=6) as executor: 1251 | futures = { 1252 | executor.submit(self._crawl_sysctl_config): 'sysctl', 1253 | executor.submit(self._crawl_network_manager_configs): 'nm_configs', 1254 | executor.submit(self._crawl_dns_config): 'dns_config', 1255 | executor.submit(self._crawl_firewall_rules): 'firewall_rules', 1256 | executor.submit(self._crawl_driver_params): 'driver_params', 1257 | executor.submit(self._crawl_interface_settings): 'interface_settings' 1258 | } 1259 | 1260 | results = {} 1261 | for future in as_completed(futures): 1262 | key = futures[future] 1263 | try: 1264 | results[key] = future.result() 1265 | except Exception as e: 1266 | self.logger.error(f"Failed to crawl {key}: {e}") 1267 | results[key] = {} 1268 | 1269 | return SystemConfig( 1270 | sysctl_params=results.get('sysctl', {}), 1271 | network_manager_configs=results.get('nm_configs', {}), 1272 | dns_config=results.get('dns_config', {}), 1273 | firewall_rules=results.get('firewall_rules', []), 1274 | driver_params=results.get('driver_params', {}), 1275 | interface_settings=results.get('interface_settings', {}) 1276 | ) 1277 | 1278 | def analyze_optimizations(self, system_config: SystemConfig, 1279 | interfaces: List[NetworkInterface], 1280 | wifi_info: Optional[WifiInfo]) -> NetworkOptimization: 1281 | """Analyze system configuration and generate optimization recommendations""" 1282 | print(f"{self.color_manager.color(Colors.BOLD, 'Analyzing configuration for optimization opportunities...')}") 1283 | 1284 | # Create system context for rules 1285 | context = SystemContext( 1286 | system_config=system_config, 1287 | interfaces=interfaces, 1288 | wifi_info=wifi_info, 1289 | logger=self.logger, 1290 | _debugger=self 1291 | ) 1292 | 1293 | # Run all enabled rules through the registry 1294 | all_rules = self.rule_registry.analyze_all(context) 1295 | 1296 | # Group rules by category 1297 | rules_by_category = defaultdict(list) 1298 | for rule in all_rules: 1299 | rules_by_category[rule.category].append(rule) 1300 | 1301 | # Calculate performance metrics 1302 | performance_score = self._calculate_performance_score(all_rules) 1303 | bottlenecks = self._identify_bottlenecks(all_rules) 1304 | 1305 | return NetworkOptimization( 1306 | timestamp=time.strftime('%Y-%m-%d %H:%M:%S'), 1307 | hostname=os.uname().nodename, 1308 | system_config=system_config, 1309 | optimization_rules=all_rules, 1310 | performance_score=performance_score, 1311 | bottlenecks=bottlenecks, 1312 | recommendations_by_category=dict(rules_by_category) 1313 | ) 1314 | 1315 | def apply_safe_optimizations(self, optimization: NetworkOptimization) -> Dict[str, bool]: 1316 | """Apply safe optimizations automatically with enhanced safety features""" 1317 | mode_text = "Simulating safe optimizations..." if self.dry_run else "Applying safe optimizations..." 1318 | print(f"{self.color_manager.color(Colors.BOLD, mode_text)}") 1319 | 1320 | results = {} 1321 | safe_rules = [r for r in optimization.optimization_rules if r.safe_to_auto_apply] 1322 | 1323 | if not safe_rules: 1324 | info_msg = self.color_manager.color(Colors.BRIGHT_CYAN, "No safe optimizations to apply") 1325 | print(f" {info_msg}") 1326 | return results 1327 | 1328 | action_word = "simulate" if self.dry_run else "apply" 1329 | print(f" Found {len(safe_rules)} safe optimization(s) to {action_word}") 1330 | 1331 | for i, rule in enumerate(safe_rules, 1): 1332 | print(f"\n[{i}/{len(safe_rules)}] {rule.category}: {rule.name}") 1333 | 1334 | # Apply or simulate the optimization using the common method 1335 | success, details = self._apply_fix(rule, dry_run=self.dry_run) 1336 | results[rule.name] = success 1337 | 1338 | if success: 1339 | status_icon = "→" if self.dry_run else "✓" 1340 | status_color = Colors.BRIGHT_CYAN if self.dry_run else Colors.BRIGHT_GREEN 1341 | status_msg = self.color_manager.color(status_color, f"{status_icon} {rule.name}") 1342 | print(f" {status_msg}") 1343 | if details: 1344 | print(f" {details}") 1345 | else: 1346 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, f"✗ {rule.name}") 1347 | print(f" {error_msg}") 1348 | if details: 1349 | print(f" {details}") 1350 | 1351 | # Summary 1352 | applied = sum(1 for success in results.values() if success) 1353 | failed = len(results) - applied 1354 | 1355 | summary_title = "Simulation Results:" if self.dry_run else "Safe Optimization Results:" 1356 | print(f"\n{self.color_manager.color(Colors.BOLD, summary_title)}") 1357 | 1358 | if applied > 0: 1359 | action_past = "would be applied" if self.dry_run else "applied successfully" 1360 | success_msg = self.color_manager.color(Colors.BRIGHT_GREEN, 1361 | f"✓ {applied} optimization(s) {action_past}") 1362 | print(f" {success_msg}") 1363 | 1364 | if failed > 0: 1365 | fail_action = "could not be simulated" if self.dry_run else "failed" 1366 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, 1367 | f"✗ {failed} optimization(s) {fail_action}") 1368 | print(f" {error_msg}") 1369 | 1370 | # List failed optimizations 1371 | failed_rules = [name for name, success in results.items() if not success] 1372 | for failed_rule in failed_rules: 1373 | print(f" • {failed_rule}") 1374 | 1375 | # Verify applied settings if not in dry-run mode 1376 | if not self.dry_run and applied > 0: 1377 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Verifying Applied Settings:')}") 1378 | applied_rules = [rule for rule in safe_rules if results[rule.name]] 1379 | verification_results = self._verify_applied_settings(applied_rules) 1380 | 1381 | for rule_name, (verified, message) in verification_results.items(): 1382 | if verified: 1383 | verify_msg = self.color_manager.color(Colors.BRIGHT_GREEN, f"✓ {message}") 1384 | print(f" {verify_msg}") 1385 | else: 1386 | verify_msg = self.color_manager.color(Colors.BRIGHT_RED, f"✗ {message}") 1387 | print(f" {verify_msg}") 1388 | 1389 | return results 1390 | 1391 | def generate_optimization_report(self, optimization: NetworkOptimization) -> str: 1392 | """Generate comprehensive optimization report""" 1393 | report = [] 1394 | 1395 | report.append("# Network Performance Optimization Report") 1396 | report.append(f"Generated: {optimization.timestamp}") 1397 | report.append(f"Hostname: {optimization.hostname}") 1398 | report.append(f"Performance Score: {optimization.performance_score:.1f}/100") 1399 | report.append("") 1400 | 1401 | # Executive Summary 1402 | report.append("## Executive Summary") 1403 | if optimization.bottlenecks: 1404 | report.append("### Identified Bottlenecks:") 1405 | for bottleneck in optimization.bottlenecks: 1406 | report.append(f"- {bottleneck}") 1407 | else: 1408 | report.append("No major bottlenecks identified.") 1409 | report.append("") 1410 | 1411 | # Recommendations by Category 1412 | report.append("## Optimization Recommendations") 1413 | 1414 | for category, rules in optimization.recommendations_by_category.items(): 1415 | report.append(f"### {category}") 1416 | 1417 | for rule in sorted(rules, key=lambda r: r.severity.value): 1418 | severity_icon = "🔴" if rule.severity == Severity.CRITICAL else "⚠️" if rule.severity == Severity.WARNING else "ℹ️" 1419 | report.append(f"#### {severity_icon} {rule.name}") 1420 | report.append(f"**Current**: {rule.current_value}") 1421 | report.append(f"**Recommended**: {rule.recommended_value}") 1422 | report.append(f"**Rationale**: {rule.rationale}") 1423 | report.append(f"**Impact**: {rule.impact}") 1424 | report.append(f"**Fix**: `{rule.fix_command}`") 1425 | report.append("") 1426 | 1427 | return "\n".join(report) 1428 | 1429 | def print_optimization_results(self, optimization: NetworkOptimization): 1430 | """Print optimization analysis results""" 1431 | separator = "=" * 60 1432 | print(f"\n{self.color_manager.color(Colors.BOLD, separator)}") 1433 | print(f"{self.color_manager.color(Colors.BOLD, 'Network Performance Analysis')}") 1434 | print(f"{self.color_manager.color(Colors.BOLD, separator)}") 1435 | 1436 | # Performance Score 1437 | score = optimization.performance_score 1438 | score_color = Colors.BRIGHT_GREEN if score >= 80 else Colors.BRIGHT_YELLOW if score >= 60 else Colors.BRIGHT_RED 1439 | score_text = self.color_manager.color(score_color, f"{score:.1f}/100") 1440 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Performance Score:')} {score_text}") 1441 | 1442 | # Bottlenecks 1443 | if optimization.bottlenecks: 1444 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Primary Bottlenecks:')}") 1445 | for bottleneck in optimization.bottlenecks: 1446 | bottleneck_text = self.color_manager.color(Colors.BRIGHT_RED, f"• {bottleneck}") 1447 | print(f" {bottleneck_text}") 1448 | 1449 | # Recommendations by Category 1450 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Optimization Recommendations:')}") 1451 | 1452 | for category, rules in optimization.recommendations_by_category.items(): 1453 | category_text = self.color_manager.color(Colors.BOLD, f"{category.upper()}:") 1454 | print(f"\n {category_text}") 1455 | 1456 | for rule in sorted(rules, key=lambda r: r.severity.value): 1457 | severity_display = self.color_manager.get_severity_display(rule.severity) 1458 | print(f" {severity_display}: {rule.name}") 1459 | print(f" Current: {rule.current_value}") 1460 | recommended_text = self.color_manager.color(Colors.BRIGHT_CYAN, rule.recommended_value) 1461 | print(f" Recommended: {recommended_text}") 1462 | print(f" Impact: {rule.impact}") 1463 | fix_text = self.color_manager.color(Colors.BLUE, rule.fix_command) 1464 | print(f" Fix: {fix_text}") 1465 | if rule.safe_to_auto_apply: 1466 | safe_text = self.color_manager.color(Colors.BRIGHT_GREEN, "✓ Safe for auto-apply") 1467 | print(f" {safe_text}") 1468 | print() 1469 | 1470 | print(f"{self.color_manager.color(Colors.BOLD, separator)}") 1471 | 1472 | def _prompt_user_interactive(self, rule: OptimizationRule) -> str: 1473 | """Prompt user for interactive optimization application""" 1474 | print(f"\n{Colors.BOLD}Optimization Available:{Colors.RESET}") 1475 | print(f" Name: {rule.name}") 1476 | print(f" Category: {rule.category}") 1477 | print(f" Current: {rule.current_value}") 1478 | print(f" Recommended: {Colors.CYAN}{rule.recommended_value}{Colors.RESET}") 1479 | print(f" Impact: {rule.impact}") 1480 | print(f" Command: {Colors.BLUE}{rule.fix_command}{Colors.RESET}") 1481 | 1482 | while True: 1483 | choice = input(f"\nApply this optimization? [y/N/a/s/q]: ").lower().strip() 1484 | if choice in ['', 'n', 'no']: 1485 | return 'skip' 1486 | elif choice in ['y', 'yes']: 1487 | return 'apply' 1488 | elif choice in ['a', 'all']: 1489 | return 'apply_all' 1490 | elif choice in ['s', 'skip']: 1491 | return 'skip_all' 1492 | elif choice in ['q', 'quit']: 1493 | return 'quit' 1494 | else: 1495 | print("Please enter: y(es), n(o), a(pply all), s(kip all), or q(uit)") 1496 | 1497 | def _measure_ping_latency(self, target: str = "8.8.8.8") -> float: 1498 | """Measure ping latency to target""" 1499 | try: 1500 | code, stdout, _ = self._run_command(['ping', '-c', '3', '-W', '2', target]) 1501 | if code == 0: 1502 | for line in stdout.split('\n'): 1503 | if 'avg' in line and 'min/avg/max' in line: 1504 | # Extract average from: min/avg/max/mdev = 12.345/23.456/34.567/1.234 ms 1505 | avg_match = re.search(r'= [0-9.]+/([0-9.]+)/', line) 1506 | if avg_match: 1507 | return float(avg_match.group(1)) 1508 | except Exception as e: 1509 | self.logger.debug(f"Failed to measure ping latency: {e}") 1510 | return 0.0 1511 | 1512 | def _get_tcp_retransmissions(self) -> int: 1513 | """Get current TCP retransmission count""" 1514 | try: 1515 | with open('/proc/net/netstat', 'r') as f: 1516 | content = f.read() 1517 | 1518 | lines = content.strip().split('\n') 1519 | for i in range(0, len(lines), 2): 1520 | if i + 1 < len(lines) and 'TcpExt:' in lines[i]: 1521 | headers = lines[i].split() 1522 | values = lines[i + 1].split() 1523 | 1524 | for j, header in enumerate(headers): 1525 | if header == 'RetransSegs' and j < len(values): 1526 | return int(values[j]) 1527 | except Exception as e: 1528 | self.logger.debug(f"Failed to get TCP retransmissions: {e}") 1529 | return 0 1530 | 1531 | def run_benchmark(self, test_method: str = "auto") -> BenchmarkResult: 1532 | """Run comprehensive network benchmark""" 1533 | print(f"{Colors.BOLD}Running network benchmark...{Colors.RESET}") 1534 | 1535 | # Speed test (check connectivity first) 1536 | if test_method == "auto": 1537 | speed_result = self.check_speed() 1538 | else: 1539 | if not self._check_internet_connectivity(): 1540 | print(f"{Colors.YELLOW}Warning: No internet connectivity detected - skipping speed test{Colors.RESET}") 1541 | speed_result = None 1542 | elif test_method == "iperf3": 1543 | speed_result = self._test_speed_iperf3() 1544 | else: 1545 | speed_result = self._test_speed_speedtest() 1546 | 1547 | # Default values if speed test fails 1548 | download_mbps = speed_result.download_mbps if speed_result else 0.0 1549 | upload_mbps = speed_result.upload_mbps if speed_result else 0.0 1550 | speed_ping = speed_result.ping_ms if speed_result else 0.0 1551 | 1552 | # Ping latency 1553 | ping_ms = self._measure_ping_latency() 1554 | if ping_ms == 0.0 and speed_ping > 0: 1555 | ping_ms = speed_ping 1556 | 1557 | # TCP retransmissions 1558 | tcp_retrans = self._get_tcp_retransmissions() 1559 | 1560 | # DNS latency 1561 | dns_latency = self._check_dns_latency() or 0.0 1562 | 1563 | # Test method used 1564 | method = speed_result.test_method if speed_result else "unavailable" 1565 | 1566 | return BenchmarkResult( 1567 | timestamp=time.strftime('%Y-%m-%d %H:%M:%S'), 1568 | download_mbps=download_mbps, 1569 | upload_mbps=upload_mbps, 1570 | ping_ms=ping_ms, 1571 | tcp_retransmissions=tcp_retrans, 1572 | dns_latency_ms=dns_latency, 1573 | test_method=method 1574 | ) 1575 | 1576 | def save_benchmark(self, benchmark: BenchmarkResult, filename: str): 1577 | """Save benchmark results to file""" 1578 | try: 1579 | with open(filename, 'w') as f: 1580 | json.dump(asdict(benchmark), f, indent=2, default=str) 1581 | print(f"Benchmark saved to {filename}") 1582 | except Exception as e: 1583 | self.logger.error(f"Failed to save benchmark: {e}") 1584 | 1585 | def load_benchmark(self, filename: str) -> Optional[BenchmarkResult]: 1586 | """Load benchmark results from file""" 1587 | try: 1588 | with open(filename, 'r') as f: 1589 | data = json.load(f) 1590 | return BenchmarkResult(**data) 1591 | except Exception as e: 1592 | self.logger.debug(f"Failed to load benchmark from {filename}: {e}") 1593 | return None 1594 | 1595 | def compare_benchmarks(self, before: BenchmarkResult, after: BenchmarkResult) -> BenchmarkComparison: 1596 | """Compare before and after benchmark results""" 1597 | improvements = {} 1598 | regressions = {} 1599 | 1600 | # Compare download speed 1601 | if after.download_mbps > before.download_mbps * 1.05: # 5% threshold 1602 | improvement = ((after.download_mbps - before.download_mbps) / before.download_mbps) * 100 1603 | improvements['download_speed'] = f"+{improvement:.1f}% ({before.download_mbps:.1f} → {after.download_mbps:.1f} Mbps)" 1604 | elif after.download_mbps < before.download_mbps * 0.95: 1605 | regression = ((before.download_mbps - after.download_mbps) / before.download_mbps) * 100 1606 | regressions['download_speed'] = f"-{regression:.1f}% ({before.download_mbps:.1f} → {after.download_mbps:.1f} Mbps)" 1607 | 1608 | # Compare upload speed 1609 | if after.upload_mbps > before.upload_mbps * 1.05: 1610 | improvement = ((after.upload_mbps - before.upload_mbps) / before.upload_mbps) * 100 1611 | improvements['upload_speed'] = f"+{improvement:.1f}% ({before.upload_mbps:.1f} → {after.upload_mbps:.1f} Mbps)" 1612 | elif after.upload_mbps < before.upload_mbps * 0.95: 1613 | regression = ((before.upload_mbps - after.upload_mbps) / before.upload_mbps) * 100 1614 | regressions['upload_speed'] = f"-{regression:.1f}% ({before.upload_mbps:.1f} → {after.upload_mbps:.1f} Mbps)" 1615 | 1616 | # Compare ping latency (lower is better) 1617 | if before.ping_ms > 0 and after.ping_ms > 0: 1618 | if after.ping_ms < before.ping_ms * 0.95: 1619 | improvement = ((before.ping_ms - after.ping_ms) / before.ping_ms) * 100 1620 | improvements['ping_latency'] = f"-{improvement:.1f}% ({before.ping_ms:.1f} → {after.ping_ms:.1f} ms)" 1621 | elif after.ping_ms > before.ping_ms * 1.05: 1622 | regression = ((after.ping_ms - before.ping_ms) / before.ping_ms) * 100 1623 | regressions['ping_latency'] = f"+{regression:.1f}% ({before.ping_ms:.1f} → {after.ping_ms:.1f} ms)" 1624 | 1625 | # Compare TCP retransmissions (lower is better) 1626 | if before.tcp_retransmissions > 0: 1627 | if after.tcp_retransmissions < before.tcp_retransmissions * 0.95: 1628 | improvement = ((before.tcp_retransmissions - after.tcp_retransmissions) / before.tcp_retransmissions) * 100 1629 | improvements['tcp_retransmissions'] = f"-{improvement:.1f}% ({before.tcp_retransmissions} → {after.tcp_retransmissions})" 1630 | elif after.tcp_retransmissions > before.tcp_retransmissions * 1.05: 1631 | regression = ((after.tcp_retransmissions - before.tcp_retransmissions) / before.tcp_retransmissions) * 100 1632 | regressions['tcp_retransmissions'] = f"+{regression:.1f}% ({before.tcp_retransmissions} → {after.tcp_retransmissions})" 1633 | 1634 | # Compare DNS latency (lower is better) 1635 | if before.dns_latency_ms > 0 and after.dns_latency_ms > 0: 1636 | if after.dns_latency_ms < before.dns_latency_ms * 0.95: 1637 | improvement = ((before.dns_latency_ms - after.dns_latency_ms) / before.dns_latency_ms) * 100 1638 | improvements['dns_latency'] = f"-{improvement:.1f}% ({before.dns_latency_ms:.1f} → {after.dns_latency_ms:.1f} ms)" 1639 | elif after.dns_latency_ms > before.dns_latency_ms * 1.05: 1640 | regression = ((after.dns_latency_ms - before.dns_latency_ms) / before.dns_latency_ms) * 100 1641 | regressions['dns_latency'] = f"+{regression:.1f}% ({before.dns_latency_ms:.1f} → {after.dns_latency_ms:.1f} ms)" 1642 | 1643 | # Calculate overall improvement score 1644 | improvement_count = len(improvements) 1645 | regression_count = len(regressions) 1646 | if improvement_count + regression_count > 0: 1647 | overall_improvement = (improvement_count - regression_count) / (improvement_count + regression_count) * 100 1648 | else: 1649 | overall_improvement = 0.0 1650 | 1651 | return BenchmarkComparison( 1652 | before=before, 1653 | after=after, 1654 | improvements=improvements, 1655 | regressions=regressions, 1656 | overall_improvement=overall_improvement 1657 | ) 1658 | 1659 | def print_benchmark_comparison(self, comparison: BenchmarkComparison): 1660 | """Print benchmark comparison results""" 1661 | print(f"\n{Colors.BOLD}{'='*60}{Colors.RESET}") 1662 | print(f"{Colors.BOLD}Benchmark Comparison Results{Colors.RESET}") 1663 | print(f"{Colors.BOLD}{'='*60}{Colors.RESET}") 1664 | 1665 | print(f"\n{Colors.BOLD}Before Optimization:{Colors.RESET} {comparison.before.timestamp}") 1666 | print(f" Download: {comparison.before.download_mbps:.1f} Mbps") 1667 | print(f" Upload: {comparison.before.upload_mbps:.1f} Mbps") 1668 | print(f" Ping: {comparison.before.ping_ms:.1f} ms") 1669 | print(f" TCP Retransmissions: {comparison.before.tcp_retransmissions}") 1670 | print(f" DNS Latency: {comparison.before.dns_latency_ms:.1f} ms") 1671 | 1672 | print(f"\n{Colors.BOLD}After Optimization:{Colors.RESET} {comparison.after.timestamp}") 1673 | print(f" Download: {comparison.after.download_mbps:.1f} Mbps") 1674 | print(f" Upload: {comparison.after.upload_mbps:.1f} Mbps") 1675 | print(f" Ping: {comparison.after.ping_ms:.1f} ms") 1676 | print(f" TCP Retransmissions: {comparison.after.tcp_retransmissions}") 1677 | print(f" DNS Latency: {comparison.after.dns_latency_ms:.1f} ms") 1678 | 1679 | if comparison.improvements: 1680 | print(f"\n{Colors.BOLD}{Colors.GREEN}Improvements:{Colors.RESET}") 1681 | for metric, change in comparison.improvements.items(): 1682 | print(f" • {metric.replace('_', ' ').title()}: {Colors.GREEN}{change}{Colors.RESET}") 1683 | 1684 | if comparison.regressions: 1685 | print(f"\n{Colors.BOLD}{Colors.RED}Regressions:{Colors.RESET}") 1686 | for metric, change in comparison.regressions.items(): 1687 | print(f" • {metric.replace('_', ' ').title()}: {Colors.RED}{change}{Colors.RESET}") 1688 | 1689 | if not comparison.improvements and not comparison.regressions: 1690 | print(f"\n{Colors.YELLOW}No significant changes detected (>5% threshold){Colors.RESET}") 1691 | 1692 | # Overall assessment 1693 | if comparison.overall_improvement > 50: 1694 | print(f"\n{Colors.BOLD}{Colors.GREEN}Overall Assessment: Significant Improvement{Colors.RESET}") 1695 | elif comparison.overall_improvement > 0: 1696 | print(f"\n{Colors.BOLD}{Colors.YELLOW}Overall Assessment: Moderate Improvement{Colors.RESET}") 1697 | elif comparison.overall_improvement == 0: 1698 | print(f"\n{Colors.BOLD}Overall Assessment: No Change{Colors.RESET}") 1699 | else: 1700 | print(f"\n{Colors.BOLD}{Colors.RED}Overall Assessment: Performance Regression{Colors.RESET}") 1701 | 1702 | print(f"{Colors.BOLD}{'='*60}{Colors.RESET}") 1703 | 1704 | def generate_benchmark_report(self, comparison: BenchmarkComparison) -> str: 1705 | """Generate markdown benchmark comparison report""" 1706 | report = [] 1707 | 1708 | report.append("# Network Performance Benchmark Report") 1709 | report.append(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}") 1710 | report.append("") 1711 | 1712 | report.append("## Before vs After Optimization") 1713 | report.append("") 1714 | report.append("| Metric | Before | After | Change |") 1715 | report.append("|--------|--------|-------|--------|") 1716 | 1717 | # Download speed 1718 | before_dl = comparison.before.download_mbps 1719 | after_dl = comparison.after.download_mbps 1720 | dl_change = f"+{((after_dl - before_dl) / before_dl * 100):.1f}%" if before_dl > 0 else "N/A" 1721 | report.append(f"| Download Speed | {before_dl:.1f} Mbps | {after_dl:.1f} Mbps | {dl_change} |") 1722 | 1723 | # Upload speed 1724 | before_ul = comparison.before.upload_mbps 1725 | after_ul = comparison.after.upload_mbps 1726 | ul_change = f"+{((after_ul - before_ul) / before_ul * 100):.1f}%" if before_ul > 0 else "N/A" 1727 | report.append(f"| Upload Speed | {before_ul:.1f} Mbps | {after_ul:.1f} Mbps | {ul_change} |") 1728 | 1729 | # Ping latency 1730 | before_ping = comparison.before.ping_ms 1731 | after_ping = comparison.after.ping_ms 1732 | ping_change = f"{((after_ping - before_ping) / before_ping * 100):+.1f}%" if before_ping > 0 else "N/A" 1733 | report.append(f"| Ping Latency | {before_ping:.1f} ms | {after_ping:.1f} ms | {ping_change} |") 1734 | 1735 | # TCP retransmissions 1736 | before_tcp = comparison.before.tcp_retransmissions 1737 | after_tcp = comparison.after.tcp_retransmissions 1738 | tcp_change = f"{((after_tcp - before_tcp) / before_tcp * 100):+.1f}%" if before_tcp > 0 else "N/A" 1739 | report.append(f"| TCP Retransmissions | {before_tcp} | {after_tcp} | {tcp_change} |") 1740 | 1741 | # DNS latency 1742 | before_dns = comparison.before.dns_latency_ms 1743 | after_dns = comparison.after.dns_latency_ms 1744 | dns_change = f"{((after_dns - before_dns) / before_dns * 100):+.1f}%" if before_dns > 0 else "N/A" 1745 | report.append(f"| DNS Latency | {before_dns:.1f} ms | {after_dns:.1f} ms | {dns_change} |") 1746 | 1747 | report.append("") 1748 | 1749 | if comparison.improvements: 1750 | report.append("## ✅ Improvements") 1751 | for metric, change in comparison.improvements.items(): 1752 | report.append(f"- **{metric.replace('_', ' ').title()}**: {change}") 1753 | report.append("") 1754 | 1755 | if comparison.regressions: 1756 | report.append("## ⚠️ Regressions") 1757 | for metric, change in comparison.regressions.items(): 1758 | report.append(f"- **{metric.replace('_', ' ').title()}**: {change}") 1759 | report.append("") 1760 | 1761 | return "\n".join(report) 1762 | 1763 | def apply_optimizations_interactive(self, optimization: NetworkOptimization) -> Dict[str, bool]: 1764 | """Apply optimizations with interactive prompts""" 1765 | mode_title = "Interactive Optimization Mode (Dry Run)" if self.dry_run else "Interactive Optimization Mode" 1766 | print(f"{self.color_manager.color(Colors.BOLD, mode_title)}") 1767 | 1768 | # Check if we're running as root for system optimizations 1769 | is_root = self._is_root() 1770 | system_optimizations = any('sysctl.conf' in rule.fix_command for rule in optimization.optimization_rules if rule.safe_to_auto_apply) 1771 | 1772 | if system_optimizations and not is_root and not self.dry_run: 1773 | warning_text = self.color_manager.color(Colors.YELLOW, "Note: System-level optimizations require root privileges.") 1774 | command_text = self.color_manager.color(Colors.CYAN, "sudo python3 network_debug.py --interactive") 1775 | print(f"{warning_text}") 1776 | print(f"For full functionality, run: {command_text}") 1777 | print(f"Continuing with available optimizations...\n") 1778 | 1779 | if self.dry_run: 1780 | print("Available actions:") 1781 | print(" y/yes - Simulate this optimization") 1782 | print(" n/no - Skip this optimization (default)") 1783 | print(" a/all - Simulate all remaining optimizations") 1784 | print(" s/skip - Skip all remaining optimizations") 1785 | print(" q/quit - Exit interactive mode") 1786 | else: 1787 | print("Available actions:") 1788 | print(" y/yes - Apply this optimization") 1789 | print(" n/no - Skip this optimization (default)") 1790 | print(" a/all - Apply all remaining optimizations") 1791 | print(" s/skip - Skip all remaining optimizations") 1792 | print(" q/quit - Exit interactive mode") 1793 | 1794 | results = {} 1795 | safe_rules = [r for r in optimization.optimization_rules if r.safe_to_auto_apply] 1796 | apply_all = False 1797 | skip_all = False 1798 | 1799 | for i, rule in enumerate(safe_rules): 1800 | if skip_all: 1801 | results[rule.name] = False 1802 | continue 1803 | 1804 | if not apply_all: 1805 | counter_text = self.color_manager.color(Colors.BOLD, f"[{i+1}/{len(safe_rules)}]") 1806 | print(f"\n{counter_text}") 1807 | choice = self._prompt_user_interactive(rule) 1808 | 1809 | if choice == 'quit': 1810 | print("Exiting interactive mode...") 1811 | break 1812 | elif choice == 'skip_all': 1813 | skip_all = True 1814 | results[rule.name] = False 1815 | continue 1816 | elif choice == 'apply_all': 1817 | apply_all = True 1818 | elif choice == 'skip': 1819 | results[rule.name] = False 1820 | continue 1821 | 1822 | if apply_all or choice == 'apply': 1823 | # Apply or simulate the optimization using the common method 1824 | success, details = self._apply_fix(rule, dry_run=self.dry_run) 1825 | results[rule.name] = success 1826 | 1827 | if success: 1828 | status_icon = "→" if self.dry_run else "✓" 1829 | status_color = Colors.BRIGHT_CYAN if self.dry_run else Colors.BRIGHT_GREEN 1830 | status_msg = self.color_manager.color(status_color, f"{status_icon} {rule.name}") 1831 | print(f" {status_msg}") 1832 | if details: 1833 | print(f" {details}") 1834 | else: 1835 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, f"✗ {rule.name}") 1836 | print(f" {error_msg}") 1837 | if details: 1838 | print(f" {details}") 1839 | 1840 | if not self.dry_run and not is_root and ('sysctl.conf' in rule.fix_command or 'modprobe.d' in rule.fix_command): 1841 | hint_msg = self.color_manager.color(Colors.YELLOW, 1842 | "Hint: Try running with sudo for system-level changes") 1843 | print(f" {hint_msg}") 1844 | 1845 | # Verify applied settings if not in dry-run mode 1846 | if not self.dry_run: 1847 | applied_rules = [rule for rule in safe_rules if results.get(rule.name, False)] 1848 | if applied_rules: 1849 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Verifying Applied Settings:')}") 1850 | verification_results = self._verify_applied_settings(applied_rules) 1851 | 1852 | for rule_name, (verified, message) in verification_results.items(): 1853 | if verified: 1854 | verify_msg = self.color_manager.color(Colors.BRIGHT_GREEN, f"✓ {message}") 1855 | print(f" {verify_msg}") 1856 | else: 1857 | verify_msg = self.color_manager.color(Colors.BRIGHT_RED, f"✗ {message}") 1858 | print(f" {verify_msg}") 1859 | 1860 | return results 1861 | 1862 | 1863 | 1864 | def check_dependencies(self) -> Dict[str, bool]: 1865 | """Check availability of optional dependencies""" 1866 | dependencies = { 1867 | 'iperf3': False, 1868 | 'speedtest-cli': False, 1869 | 'iw': False, 1870 | 'iwconfig': False, 1871 | 'ethtool': False, 1872 | 'dig': False, 1873 | 'nslookup': False, 1874 | 'nmcli': False, 1875 | 'rfkill': False, 1876 | 'ping': False 1877 | } 1878 | 1879 | for tool in dependencies.keys(): 1880 | dependencies[tool] = shutil.which(tool) is not None 1881 | 1882 | return dependencies 1883 | 1884 | def _check_internet_connectivity(self) -> bool: 1885 | """Check if internet connectivity is available""" 1886 | try: 1887 | # Try to reach Google DNS 1888 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1889 | sock.settimeout(3) 1890 | result = sock.connect_ex(('8.8.8.8', 53)) 1891 | sock.close() 1892 | 1893 | if result == 0: 1894 | return True 1895 | 1896 | # Fallback: try ping 1897 | code, _, _ = self._run_command(['ping', '-c', '1', '-W', '3', '8.8.8.8']) 1898 | return code == 0 1899 | 1900 | except Exception: 1901 | return False 1902 | 1903 | def _is_systemd_active(self) -> bool: 1904 | """Check if systemd is the active init system""" 1905 | try: 1906 | # Method 1: Check /proc/1/comm (most reliable) 1907 | try: 1908 | with open('/proc/1/comm', 'r') as f: 1909 | init_name = f.read().strip() 1910 | if init_name == 'systemd': 1911 | self.logger.debug("systemd detected via /proc/1/comm") 1912 | return True 1913 | except (FileNotFoundError, IOError): 1914 | pass 1915 | 1916 | # Method 2: Check if systemd is running via pidof 1917 | if shutil.which('pidof'): 1918 | code, stdout, _ = self._run_command(['pidof', 'systemd']) 1919 | if code == 0 and stdout.strip(): 1920 | self.logger.debug("systemd detected via pidof") 1921 | return True 1922 | 1923 | # Method 3: Check for systemd directory 1924 | if os.path.exists('/run/systemd/system'): 1925 | self.logger.debug("systemd detected via /run/systemd/system") 1926 | return True 1927 | 1928 | # Method 4: Try systemctl (may work even without systemd as PID 1) 1929 | if shutil.which('systemctl'): 1930 | code, _, _ = self._run_command(['systemctl', 'is-system-running'], timeout=5) 1931 | if code in [0, 1]: # 0 = running, 1 = degraded but still systemd 1932 | self.logger.debug("systemd detected via systemctl is-system-running") 1933 | return True 1934 | 1935 | self.logger.debug("systemd not detected") 1936 | return False 1937 | 1938 | except Exception as e: 1939 | self.logger.debug(f"Error checking systemd status: {e}") 1940 | return False 1941 | 1942 | def _is_root(self) -> bool: 1943 | """Check if running as root""" 1944 | return os.geteuid() == 0 1945 | 1946 | def _check_root_required(self, operation: str) -> bool: 1947 | """Check if root is required for operation and warn if not root""" 1948 | if not self._is_root(): 1949 | warning_text = self.color_manager.color(Colors.YELLOW, f"Warning: {operation} requires root privileges for system-level changes") 1950 | command_text = self.color_manager.color(Colors.CYAN, "sudo python3 network_debug.py") 1951 | print(warning_text) 1952 | print(f"Run with sudo for full functionality: {command_text}") 1953 | return False 1954 | return True 1955 | 1956 | def _create_timestamped_backup(self, file_path: str) -> Optional[str]: 1957 | """Create a timestamped backup of a file""" 1958 | try: 1959 | if not os.path.exists(file_path): 1960 | self.logger.debug(f"File {file_path} does not exist, no backup needed") 1961 | return None 1962 | 1963 | # Generate timestamp 1964 | timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") 1965 | backup_path = f"{file_path}.bak.{timestamp}" 1966 | 1967 | # Create backup 1968 | shutil.copy2(file_path, backup_path) 1969 | self.logger.info(f"Created backup: {backup_path}") 1970 | return backup_path 1971 | 1972 | except Exception as e: 1973 | self.logger.error(f"Failed to create backup of {file_path}: {e}") 1974 | return None 1975 | 1976 | def _safe_write_file(self, file_path: str, content: str, backup: bool = True) -> bool: 1977 | """Safely write content to file with atomic operation and optional backup""" 1978 | try: 1979 | # Create backup if requested 1980 | if backup: 1981 | backup_path = self._create_timestamped_backup(file_path) 1982 | if backup_path is None and os.path.exists(file_path): 1983 | self.logger.warning(f"Could not create backup for {file_path}, proceeding anyway") 1984 | 1985 | # Write to temporary file first 1986 | dir_path = os.path.dirname(file_path) 1987 | with tempfile.NamedTemporaryFile(mode='w', dir=dir_path, delete=False, 1988 | prefix=f".{os.path.basename(file_path)}.tmp") as tmp_file: 1989 | tmp_file.write(content) 1990 | tmp_file.flush() 1991 | os.fsync(tmp_file.fileno()) # Force write to disk 1992 | tmp_path = tmp_file.name 1993 | 1994 | # Atomic rename 1995 | os.rename(tmp_path, file_path) 1996 | self.logger.debug(f"Successfully wrote {file_path}") 1997 | return True 1998 | 1999 | except Exception as e: 2000 | self.logger.error(f"Failed to write {file_path}: {e}") 2001 | # Clean up temp file if it exists 2002 | try: 2003 | if 'tmp_path' in locals(): 2004 | os.unlink(tmp_path) 2005 | except: 2006 | pass 2007 | return False 2008 | 2009 | def _read_sysctl_config_file(self, file_path: str) -> Dict[str, str]: 2010 | """Read existing sysctl configuration from file""" 2011 | config = {} 2012 | try: 2013 | if os.path.exists(file_path): 2014 | with open(file_path, 'r') as f: 2015 | for line in f: 2016 | line = line.strip() 2017 | if line and not line.startswith('#') and '=' in line: 2018 | key, value = line.split('=', 1) 2019 | config[key.strip()] = value.strip() 2020 | except Exception as e: 2021 | self.logger.error(f"Failed to read {file_path}: {e}") 2022 | return config 2023 | 2024 | def _deduplicate_sysctl_params(self, existing_config: Dict[str, str], 2025 | new_params: Dict[str, str]) -> Dict[str, str]: 2026 | """Remove parameters that already have the correct value""" 2027 | filtered_params = {} 2028 | for param, value in new_params.items(): 2029 | existing_value = existing_config.get(param) 2030 | if existing_value != value: 2031 | filtered_params[param] = value 2032 | if existing_value: 2033 | self.logger.debug(f"Parameter {param} will be updated: {existing_value} -> {value}") 2034 | else: 2035 | self.logger.debug(f"Parameter {param} will be added: {value}") 2036 | else: 2037 | self.logger.debug(f"Parameter {param} already has correct value: {value}") 2038 | return filtered_params 2039 | 2040 | def _verify_sysctl_settings(self, expected_params: Dict[str, str]) -> Dict[str, bool]: 2041 | """Verify that sysctl settings match expected values""" 2042 | verification_results = {} 2043 | 2044 | for param, expected_value in expected_params.items(): 2045 | try: 2046 | # Get current value from system 2047 | code, stdout, stderr = self._run_command(['sysctl', '-n', param]) 2048 | if code == 0: 2049 | current_value = stdout.strip() 2050 | matches = current_value == expected_value 2051 | verification_results[param] = matches 2052 | 2053 | if matches: 2054 | success_msg = self.color_manager.color(Colors.BRIGHT_GREEN, 2055 | f"✓ {param} successfully set to {expected_value}") 2056 | print(f" {success_msg}") 2057 | else: 2058 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, 2059 | f"✗ {param} verification failed: expected {expected_value}, got {current_value}") 2060 | print(f" {error_msg}") 2061 | self.logger.warning(f"Sysctl verification failed for {param}: expected {expected_value}, got {current_value}") 2062 | else: 2063 | verification_results[param] = False 2064 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, 2065 | f"✗ {param} verification failed: {stderr.strip()}") 2066 | print(f" {error_msg}") 2067 | self.logger.error(f"Failed to read sysctl parameter {param}: {stderr}") 2068 | 2069 | except Exception as e: 2070 | verification_results[param] = False 2071 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, 2072 | f"✗ {param} verification error: {e}") 2073 | print(f" {error_msg}") 2074 | self.logger.error(f"Exception while verifying {param}: {e}") 2075 | 2076 | return verification_results 2077 | 2078 | def _safe_update_sysctl_config(self, sysctl_file: str, new_params: Dict[str, str]) -> bool: 2079 | """Safely update sysctl configuration with deduplication and verification""" 2080 | backup_path = None 2081 | try: 2082 | # Read existing configuration 2083 | existing_config = self._read_sysctl_config_file(sysctl_file) 2084 | 2085 | # Deduplicate parameters 2086 | params_to_add = self._deduplicate_sysctl_params(existing_config, new_params) 2087 | 2088 | if not params_to_add: 2089 | info_msg = self.color_manager.color(Colors.BRIGHT_CYAN, 2090 | "All sysctl parameters already have correct values") 2091 | print(f" {info_msg}") 2092 | return True 2093 | 2094 | # Read current file content 2095 | current_content = "" 2096 | if os.path.exists(sysctl_file): 2097 | with open(sysctl_file, 'r') as f: 2098 | current_content = f.read() 2099 | 2100 | # Prepare new content 2101 | new_lines = [] 2102 | if current_content and not current_content.endswith('\n'): 2103 | new_lines.append('') # Add newline if file doesn't end with one 2104 | 2105 | new_lines.append(f"# Network optimization settings added by network_debug.py on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") 2106 | for param, value in params_to_add.items(): 2107 | new_lines.append(f"{param} = {value}") 2108 | new_lines.append('') # End with newline 2109 | 2110 | updated_content = current_content + '\n'.join(new_lines) 2111 | 2112 | # Write safely with backup 2113 | backup_path = self._create_timestamped_backup(sysctl_file) 2114 | if self._safe_write_file(sysctl_file, updated_content, backup=False): # Already have backup 2115 | success_msg = self.color_manager.color(Colors.BRIGHT_GREEN, 2116 | f"✓ Updated {sysctl_file} with {len(params_to_add)} parameters") 2117 | print(f" {success_msg}") 2118 | 2119 | # Apply sysctl changes 2120 | print(f" Applying sysctl changes...") 2121 | code, stdout, stderr = self._run_command(['sysctl', '-p', sysctl_file]) 2122 | if code == 0: 2123 | success_msg = self.color_manager.color(Colors.BRIGHT_GREEN, 2124 | "✓ sysctl -p completed successfully") 2125 | print(f" {success_msg}") 2126 | 2127 | # Verify settings 2128 | print(f" Verifying applied settings...") 2129 | verification_results = self._verify_sysctl_settings(params_to_add) 2130 | 2131 | failed_verifications = [param for param, success in verification_results.items() if not success] 2132 | if failed_verifications: 2133 | warning_msg = self.color_manager.color(Colors.BRIGHT_YELLOW, 2134 | f"⚠ {len(failed_verifications)} parameters failed verification") 2135 | print(f" {warning_msg}") 2136 | 2137 | # Attempt rollback if backup exists 2138 | if backup_path and os.path.exists(backup_path): 2139 | print(f" Attempting rollback from {backup_path}...") 2140 | try: 2141 | shutil.copy2(backup_path, sysctl_file) 2142 | rollback_msg = self.color_manager.color(Colors.BRIGHT_YELLOW, 2143 | "✓ Configuration rolled back successfully") 2144 | print(f" {rollback_msg}") 2145 | except Exception as e: 2146 | rollback_error = self.color_manager.color(Colors.BRIGHT_RED, 2147 | f"✗ Rollback failed: {e}") 2148 | print(f" {rollback_error}") 2149 | return False 2150 | else: 2151 | success_msg = self.color_manager.color(Colors.BRIGHT_GREEN, 2152 | "✓ All parameters verified successfully") 2153 | print(f" {success_msg}") 2154 | return True 2155 | else: 2156 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, 2157 | f"✗ sysctl -p failed: {stderr.strip()}") 2158 | print(f" {error_msg}") 2159 | 2160 | # Attempt rollback if backup exists 2161 | if backup_path and os.path.exists(backup_path): 2162 | print(f" Attempting rollback from {backup_path}...") 2163 | try: 2164 | shutil.copy2(backup_path, sysctl_file) 2165 | rollback_msg = self.color_manager.color(Colors.BRIGHT_YELLOW, 2166 | "✓ Configuration rolled back successfully") 2167 | print(f" {rollback_msg}") 2168 | except Exception as e: 2169 | rollback_error = self.color_manager.color(Colors.BRIGHT_RED, 2170 | f"✗ Rollback failed: {e}") 2171 | print(f" {rollback_error}") 2172 | return False 2173 | else: 2174 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, 2175 | f"✗ Failed to write {sysctl_file}") 2176 | print(f" {error_msg}") 2177 | return False 2178 | 2179 | except Exception as e: 2180 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, 2181 | f"✗ Exception updating sysctl config: {e}") 2182 | print(f" {error_msg}") 2183 | self.logger.error(f"Exception in _safe_update_sysctl_config: {e}") 2184 | return False 2185 | 2186 | def _safe_update_modprobe_config(self, module_name: str, options: str) -> bool: 2187 | """Safely update modprobe configuration for a module""" 2188 | try: 2189 | modprobe_dir = Path('/etc/modprobe.d') 2190 | config_file = modprobe_dir / f"{module_name}.conf" 2191 | 2192 | # Check if configuration already exists 2193 | existing_options = "" 2194 | if config_file.exists(): 2195 | with open(config_file, 'r') as f: 2196 | content = f.read() 2197 | for line in content.split('\n'): 2198 | if line.strip().startswith(f'options {module_name}'): 2199 | existing_options = line.strip() 2200 | break 2201 | 2202 | new_config_line = f"options {module_name} {options}" 2203 | 2204 | # Check if we need to update 2205 | if existing_options == new_config_line: 2206 | info_msg = self.color_manager.color(Colors.BRIGHT_CYAN, 2207 | f"Module {module_name} already has correct configuration") 2208 | print(f" {info_msg}") 2209 | return True 2210 | 2211 | # Prepare new content 2212 | if existing_options: 2213 | # Replace existing line 2214 | with open(config_file, 'r') as f: 2215 | content = f.read() 2216 | new_content = content.replace(existing_options, new_config_line) 2217 | else: 2218 | # Add new configuration 2219 | header = f"# Module configuration for {module_name} added by network_debug.py on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" 2220 | new_content = header + new_config_line + "\n" 2221 | 2222 | # Write safely with backup 2223 | if self._safe_write_file(str(config_file), new_content, backup=True): 2224 | success_msg = self.color_manager.color(Colors.BRIGHT_GREEN, 2225 | f"✓ Updated {config_file}") 2226 | print(f" {success_msg}") 2227 | 2228 | # Note about module reload requirement 2229 | info_msg = self.color_manager.color(Colors.BRIGHT_YELLOW, 2230 | f"ℹ Module {module_name} configuration updated (requires reload/reboot to take effect)") 2231 | print(f" {info_msg}") 2232 | return True 2233 | else: 2234 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, 2235 | f"✗ Failed to write {config_file}") 2236 | print(f" {error_msg}") 2237 | return False 2238 | 2239 | except Exception as e: 2240 | error_msg = self.color_manager.color(Colors.BRIGHT_RED, 2241 | f"✗ Exception updating modprobe config: {e}") 2242 | print(f" {error_msg}") 2243 | self.logger.error(f"Exception in _safe_update_modprobe_config: {e}") 2244 | return False 2245 | 2246 | def _apply_fix(self, rule: OptimizationRule, dry_run: bool = False) -> Tuple[bool, str]: 2247 | """Common method to apply or simulate optimization fixes 2248 | 2249 | Returns: 2250 | Tuple[bool, str]: (success, details) 2251 | """ 2252 | try: 2253 | if dry_run: 2254 | return self._simulate_fix(rule) 2255 | else: 2256 | return self._execute_fix(rule) 2257 | except Exception as e: 2258 | error_msg = f"Exception applying {rule.name}: {e}" 2259 | self.logger.error(error_msg) 2260 | return False, error_msg 2261 | 2262 | def _simulate_fix(self, rule: OptimizationRule) -> Tuple[bool, str]: 2263 | """Simulate what would be done for a fix without actually doing it""" 2264 | details = [] 2265 | 2266 | if rule.fix_command.startswith('echo') and 'sysctl.conf' in rule.fix_command: 2267 | # Simulate sysctl configuration update 2268 | parts = rule.fix_command.split(' >> ') 2269 | if len(parts) == 2: 2270 | content = parts[0].replace('echo ', '').strip("'\"") 2271 | file_path = parts[1].split(' &&')[0].strip() 2272 | 2273 | if '=' in content: 2274 | param, value = content.split('=', 1) 2275 | param = param.strip() 2276 | value = value.strip() 2277 | 2278 | details.append(f"Would append '{param} = {value}' to {file_path}") 2279 | details.append(f"Would run 'sysctl -p {file_path}'") 2280 | details.append(f"Would verify {param} is set to {value}") 2281 | 2282 | return True, '\n '.join(details) 2283 | 2284 | elif rule.fix_command.startswith('echo') and 'modprobe.d' in rule.fix_command: 2285 | # Simulate modprobe configuration update 2286 | parts = rule.fix_command.split(' >> ') 2287 | if len(parts) == 2: 2288 | content = parts[0].replace('echo ', '').strip("'\"") 2289 | file_path = parts[1].split(' &&')[0].strip() 2290 | 2291 | details.append(f"Would write '{content}' to {file_path}") 2292 | details.append(f"Would create backup of {file_path}") 2293 | 2294 | return True, '\n '.join(details) 2295 | 2296 | else: 2297 | # Simulate direct command execution 2298 | details.append(f"Would execute: {rule.fix_command}") 2299 | return True, '\n '.join(details) 2300 | 2301 | return False, "Could not simulate this fix" 2302 | 2303 | def _execute_fix(self, rule: OptimizationRule) -> Tuple[bool, str]: 2304 | """Actually execute the fix""" 2305 | details = [] 2306 | 2307 | if rule.fix_command.startswith('echo') and 'sysctl.conf' in rule.fix_command: 2308 | # Handle sysctl configuration updates 2309 | parts = rule.fix_command.split(' >> ') 2310 | if len(parts) == 2: 2311 | content = parts[0].replace('echo ', '').strip("'\"") 2312 | file_path = parts[1].split(' &&')[0].strip() 2313 | 2314 | if '=' in content: 2315 | param, value = content.split('=', 1) 2316 | param = param.strip() 2317 | value = value.strip() 2318 | 2319 | success = self._safe_update_sysctl_config(file_path, {param: value}) 2320 | if success: 2321 | details.append(f"Successfully updated {file_path}") 2322 | details.append(f"Applied sysctl changes") 2323 | details.append(f"Verified {param} = {value}") 2324 | return True, '\n '.join(details) 2325 | else: 2326 | details.append(f"Failed to update {file_path}") 2327 | return False, '\n '.join(details) 2328 | 2329 | elif rule.fix_command.startswith('echo') and 'modprobe.d' in rule.fix_command: 2330 | # Handle modprobe configuration updates 2331 | parts = rule.fix_command.split(' >> ') 2332 | if len(parts) == 2: 2333 | content = parts[0].replace('echo ', '').strip("'\"") 2334 | 2335 | if content.startswith('options '): 2336 | config_parts = content.split(' ', 2) 2337 | if len(config_parts) >= 3: 2338 | module_name = config_parts[1] 2339 | options = config_parts[2] 2340 | success = self._safe_update_modprobe_config(module_name, options) 2341 | if success: 2342 | details.append(f"Successfully updated modprobe config for {module_name}") 2343 | return True, '\n '.join(details) 2344 | else: 2345 | details.append(f"Failed to update modprobe config for {module_name}") 2346 | return False, '\n '.join(details) 2347 | 2348 | else: 2349 | # Execute command directly 2350 | code, stdout, stderr = self._run_command(rule.fix_command.split()) 2351 | if code == 0: 2352 | details.append(f"Successfully executed: {rule.fix_command}") 2353 | if stdout.strip(): 2354 | details.append(f"Output: {stdout.strip()}") 2355 | return True, '\n '.join(details) 2356 | else: 2357 | details.append(f"Failed to execute: {rule.fix_command}") 2358 | if stderr.strip(): 2359 | details.append(f"Error: {stderr.strip()}") 2360 | return False, '\n '.join(details) 2361 | 2362 | return False, "Unsupported fix command" 2363 | 2364 | def _verify_applied_settings(self, rules: List[OptimizationRule]) -> Dict[str, Tuple[bool, str]]: 2365 | """Verify that applied settings are actually in effect""" 2366 | verification_results = {} 2367 | 2368 | for rule in rules: 2369 | if rule.fix_command.startswith('echo') and 'sysctl.conf' in rule.fix_command: 2370 | # Verify sysctl parameters 2371 | parts = rule.fix_command.split(' >> ') 2372 | if len(parts) == 2: 2373 | content = parts[0].replace('echo ', '').strip("'\"") 2374 | if '=' in content: 2375 | param, expected_value = content.split('=', 1) 2376 | param = param.strip() 2377 | expected_value = expected_value.strip() 2378 | 2379 | # Get current value 2380 | code, stdout, stderr = self._run_command(['sysctl', '-n', param]) 2381 | if code == 0: 2382 | current_value = stdout.strip() 2383 | if current_value == expected_value: 2384 | verification_results[rule.name] = (True, f"{param} set correctly") 2385 | else: 2386 | verification_results[rule.name] = (False, 2387 | f"{param} still incorrect (expected {expected_value}, found {current_value})") 2388 | else: 2389 | verification_results[rule.name] = (False, 2390 | f"{param} verification failed: {stderr.strip()}") 2391 | else: 2392 | # For non-sysctl commands, we can't easily verify, so assume success if applied 2393 | verification_results[rule.name] = (True, f"{rule.name} applied") 2394 | 2395 | return verification_results 2396 | 2397 | def print_dependency_status(self, dependencies: Dict[str, bool]): 2398 | """Print dependency availability status with installation guidance""" 2399 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Dependency Status:')}") 2400 | 2401 | required = ['ping', 'ip'] # Essential tools 2402 | recommended = ['iperf3', 'speedtest-cli', 'iw', 'ethtool', 'nmcli'] 2403 | optional = ['dig', 'nslookup', 'iwconfig', 'rfkill'] 2404 | 2405 | missing_by_category = {} 2406 | distribution = self._detect_distribution() 2407 | 2408 | for category, tools in [('Required', required), ('Recommended', recommended), ('Optional', optional)]: 2409 | category_text = self.color_manager.color(Colors.BOLD, f"{category}:") 2410 | print(f"\n {category_text}") 2411 | missing_tools = [] 2412 | 2413 | for tool in tools: 2414 | if tool in dependencies: 2415 | status = dependencies[tool] 2416 | color = Colors.BRIGHT_GREEN if status else Colors.BRIGHT_RED 2417 | symbol = "✓" if status else "✗" 2418 | status_text = self.color_manager.color(color, f"{symbol} {tool}") 2419 | print(f" {status_text}") 2420 | 2421 | if not status: 2422 | missing_tools.append(tool) 2423 | else: 2424 | unknown_text = self.color_manager.color(Colors.YELLOW, f"? {tool} (not checked)") 2425 | print(f" {unknown_text}") 2426 | 2427 | if missing_tools: 2428 | missing_by_category[category] = missing_tools 2429 | 2430 | # Enhanced installation guidance 2431 | if missing_by_category: 2432 | # Package mapping for different distributions 2433 | package_mapping = { 2434 | 'Ubuntu': {'speedtest-cli': 'speedtest-cli', 'iperf3': 'iperf3', 'iw': 'iw', 'ethtool': 'ethtool', 'nmcli': 'network-manager', 'dig': 'dnsutils', 'nslookup': 'dnsutils', 'iwconfig': 'wireless-tools', 'rfkill': 'rfkill'}, 2435 | 'Fedora': {'speedtest-cli': 'speedtest-cli', 'iperf3': 'iperf3', 'iw': 'iw', 'ethtool': 'ethtool', 'nmcli': 'NetworkManager', 'dig': 'bind-utils', 'nslookup': 'bind-utils', 'iwconfig': 'wireless-tools', 'rfkill': 'rfkill'}, 2436 | 'Arch': {'speedtest-cli': 'speedtest-cli', 'iperf3': 'iperf3', 'iw': 'iw', 'ethtool': 'ethtool', 'nmcli': 'networkmanager', 'dig': 'bind', 'nslookup': 'bind', 'iwconfig': 'wireless_tools', 'rfkill': 'rfkill'}, 2437 | 'Alpine': {'speedtest-cli': 'speedtest-cli', 'iperf3': 'iperf3', 'iw': 'iw', 'ethtool': 'ethtool', 'nmcli': 'networkmanager', 'dig': 'bind-tools', 'nslookup': 'bind-tools', 'iwconfig': 'wireless-tools', 'rfkill': 'rfkill'} 2438 | } 2439 | 2440 | dist_mapping = package_mapping.get(distribution, package_mapping['Ubuntu']) 2441 | 2442 | for category, tools in missing_by_category.items(): 2443 | if category == 'Required': 2444 | severity_color = Colors.BRIGHT_RED 2445 | severity_text = "CRITICAL" 2446 | elif category == 'Recommended': 2447 | severity_color = Colors.BRIGHT_YELLOW 2448 | severity_text = "WARNING" 2449 | else: 2450 | severity_color = Colors.BRIGHT_CYAN 2451 | severity_text = "INFO" 2452 | 2453 | warning_text = self.color_manager.color(severity_color, f"{severity_text}: Missing {category.lower()} tools may limit functionality:") 2454 | print(f"\n{warning_text}") 2455 | for tool in tools: 2456 | print(f" • {tool}") 2457 | 2458 | # Distribution-specific installation commands 2459 | packages = [dist_mapping.get(tool, tool) for tool in tools] 2460 | unique_packages = sorted(set(pkg for pkg in packages if pkg is not None)) 2461 | 2462 | if distribution == 'Ubuntu': 2463 | print(f"\nInstall with: sudo apt update && sudo apt install {' '.join(unique_packages)}") 2464 | elif distribution == 'Fedora': 2465 | print(f"\nInstall with: sudo dnf install {' '.join(unique_packages)}") 2466 | elif distribution == 'Arch': 2467 | print(f"\nInstall with: sudo pacman -S {' '.join(unique_packages)}") 2468 | elif distribution == 'Alpine': 2469 | print(f"\nInstall with: sudo apk add {' '.join(unique_packages)}") 2470 | else: 2471 | print(f"\nInstall packages: {' '.join(unique_packages)}") 2472 | print(f" Ubuntu/Debian: sudo apt install {' '.join(unique_packages)}") 2473 | print(f" Fedora: sudo dnf install {' '.join(unique_packages)}") 2474 | print(f" Arch: sudo pacman -S {' '.join(unique_packages)}") 2475 | print(f" Alpine: sudo apk add {' '.join(unique_packages)}") 2476 | 2477 | print(f"{self.color_manager.color(Colors.BOLD, '=' * 60)}") 2478 | 2479 | def check_speed(self) -> Optional[SpeedTestResult]: 2480 | """Perform network speed test""" 2481 | print(f"{self.color_manager.color(Colors.BOLD, 'Testing network speed...')}") 2482 | 2483 | # Check internet connectivity first 2484 | if not self._check_internet_connectivity(): 2485 | warning_text = self.color_manager.color(Colors.YELLOW, "Warning: No internet connectivity detected - skipping speed test") 2486 | print(warning_text) 2487 | self._add_finding( 2488 | 'speed_test', 2489 | Severity.WARNING, 2490 | "No internet connectivity detected", 2491 | "Check network configuration and DNS settings" 2492 | ) 2493 | return None 2494 | 2495 | # Try iperf3 first 2496 | result = self._test_speed_iperf3() 2497 | if result: 2498 | return result 2499 | 2500 | # Fallback to speedtest-cli 2501 | result = self._test_speed_speedtest() 2502 | if result: 2503 | return result 2504 | 2505 | self._add_finding( 2506 | 'speed_test', 2507 | Severity.WARNING, 2508 | "Could not perform speed test", 2509 | "Install iperf3 or speedtest-cli for speed testing" 2510 | ) 2511 | 2512 | return None 2513 | 2514 | def diagnose(self) -> NetworkDiagnostic: 2515 | """Perform comprehensive network diagnosis""" 2516 | print(f"{self.color_manager.color(Colors.BOLD, 'Performing network diagnosis...')}") 2517 | 2518 | # Gather system information 2519 | distribution = self._detect_distribution() 2520 | kernel_version = self._get_kernel_version() 2521 | interfaces = self._get_network_interfaces() 2522 | wifi_info = self._get_wifi_info() 2523 | 2524 | # Perform speed test 2525 | speed_test = self.check_speed() 2526 | 2527 | # Run diagnostic checks 2528 | self._check_driver_issues(interfaces) 2529 | self._check_rfkill_status() 2530 | self._check_channel_congestion() 2531 | 2532 | if wifi_info: 2533 | self._check_band_usage(wifi_info) 2534 | 2535 | # Check DNS and TCP performance with context awareness 2536 | dns_latency = self._check_dns_latency() 2537 | if dns_latency: 2538 | if dns_latency > 100: 2539 | self._add_finding( 2540 | 'dns_latency', 2541 | Severity.WARNING, 2542 | f"High DNS latency: {dns_latency:.1f}ms", 2543 | "Consider using faster DNS servers (8.8.8.8, 1.1.1.1)" 2544 | ) 2545 | else: 2546 | self.logger.debug(f"DNS latency acceptable: {dns_latency:.1f}ms") 2547 | 2548 | tcp_stats = self._check_tcp_retransmissions() 2549 | if tcp_stats.get('retransmissions', 0) > 1000: 2550 | self._add_finding( 2551 | 'tcp_retransmissions', 2552 | Severity.WARNING, 2553 | f"High TCP retransmissions: {tcp_stats['retransmissions']}", 2554 | "Network congestion or packet loss detected" 2555 | ) 2556 | 2557 | # Suggest optimizations 2558 | self._suggest_tcp_tuning() 2559 | 2560 | # Create diagnostic result 2561 | diagnostic = NetworkDiagnostic( 2562 | timestamp=time.strftime('%Y-%m-%d %H:%M:%S'), 2563 | hostname=os.uname().nodename, 2564 | distribution=distribution, 2565 | kernel_version=kernel_version, 2566 | interfaces=interfaces, 2567 | wifi_info=wifi_info, 2568 | speed_test=speed_test, 2569 | findings=self.findings, 2570 | system_info={ 2571 | 'dns_latency_ms': str(dns_latency) if dns_latency else 'N/A', 2572 | **{k: str(v) for k, v in tcp_stats.items()} 2573 | } 2574 | ) 2575 | 2576 | return diagnostic 2577 | 2578 | def tune(self): 2579 | """Apply automatic performance tuning""" 2580 | mode_text = "Simulating performance tuning..." if self.dry_run else "Applying performance tuning..." 2581 | print(f"{self.color_manager.color(Colors.BOLD, mode_text)}") 2582 | 2583 | # Check root privileges (skip in dry-run mode) 2584 | if not self.dry_run and not self._check_root_required("Performance tuning"): 2585 | warning_text = self.color_manager.color(Colors.YELLOW, "Skipping system-level tuning (requires root)") 2586 | print(warning_text) 2587 | return 2588 | 2589 | # Disable power management on Wi-Fi interfaces 2590 | wifi_iface = self._get_wifi_interface() 2591 | if wifi_iface and shutil.which('iw'): 2592 | if self.dry_run: 2593 | simulate_text = self.color_manager.color(Colors.BRIGHT_CYAN, f"→ Would disable power saving on {wifi_iface}") 2594 | print(simulate_text) 2595 | print(f" Would execute: iw {wifi_iface} set power_save off") 2596 | else: 2597 | code, _, _ = self._run_command(['iw', wifi_iface, 'set', 'power_save', 'off']) 2598 | if code == 0: 2599 | success_text = self.color_manager.color(Colors.BRIGHT_GREEN, f"✓ Disabled power saving on {wifi_iface}") 2600 | print(success_text) 2601 | else: 2602 | error_text = self.color_manager.color(Colors.BRIGHT_RED, f"✗ Failed to disable power saving on {wifi_iface}") 2603 | print(error_text) 2604 | elif wifi_iface: 2605 | if self.dry_run: 2606 | warning_text = self.color_manager.color(Colors.YELLOW, "⚠ iw not available, would skip Wi-Fi power management") 2607 | else: 2608 | warning_text = self.color_manager.color(Colors.YELLOW, "⚠ iw not available, skipping Wi-Fi power management") 2609 | print(warning_text) 2610 | 2611 | # Apply TCP tuning using safe configuration update 2612 | tcp_params = { 2613 | 'net.core.rmem_max': '134217728', 2614 | 'net.core.wmem_max': '134217728', 2615 | 'net.ipv4.tcp_window_scaling': '1', 2616 | 'net.ipv4.tcp_timestamps': '1', 2617 | 'net.ipv4.tcp_sack': '1' 2618 | } 2619 | 2620 | if self.dry_run: 2621 | print("Simulating TCP optimization parameters...") 2622 | simulate_text = self.color_manager.color(Colors.BRIGHT_CYAN, "→ Would update /etc/sysctl.conf with:") 2623 | print(simulate_text) 2624 | for param, value in tcp_params.items(): 2625 | print(f" {param} = {value}") 2626 | print(f" Would run 'sysctl -p /etc/sysctl.conf'") 2627 | print(f" Would verify all parameters are set correctly") 2628 | else: 2629 | print("Applying TCP optimization parameters...") 2630 | sysctl_success = self._safe_update_sysctl_config('/etc/sysctl.conf', tcp_params) 2631 | 2632 | if sysctl_success: 2633 | success_text = self.color_manager.color(Colors.BRIGHT_GREEN, 2634 | "✓ TCP optimization parameters applied successfully") 2635 | print(success_text) 2636 | else: 2637 | error_text = self.color_manager.color(Colors.BRIGHT_RED, 2638 | "✗ Failed to apply TCP optimization parameters") 2639 | print(error_text) 2640 | 2641 | def expert_json(self) -> str: 2642 | """Generate expert-level JSON diagnostic""" 2643 | diagnostic = self.diagnose() 2644 | return json.dumps(asdict(diagnostic), indent=2, default=str) 2645 | 2646 | def print_results(self, diagnostic: NetworkDiagnostic): 2647 | """Print diagnostic results with color coding""" 2648 | header = "Network Diagnostic Results" 2649 | separator = "=" * 60 2650 | 2651 | print(f"\n{self.color_manager.color(Colors.BOLD, separator)}") 2652 | print(f"{self.color_manager.color(Colors.BOLD, header)}") 2653 | print(f"{self.color_manager.color(Colors.BOLD, separator)}") 2654 | 2655 | # System information 2656 | print(f"\n{self.color_manager.color(Colors.BOLD, 'System Information:')}") 2657 | print(f"Hostname: {diagnostic.hostname}") 2658 | print(f"Distribution: {diagnostic.distribution}") 2659 | print(f"Kernel: {diagnostic.kernel_version}") 2660 | print(f"Timestamp: {diagnostic.timestamp}") 2661 | 2662 | # Network interfaces 2663 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Network Interfaces:')}") 2664 | for interface in diagnostic.interfaces: 2665 | status_color = Colors.BRIGHT_GREEN if interface.state == 'UP' else Colors.BRIGHT_RED 2666 | status_text = self.color_manager.color(status_color, interface.state) 2667 | print(f" {interface.name}: {status_text} ({interface.type}, {interface.driver})") 2668 | 2669 | # Wi-Fi information 2670 | if diagnostic.wifi_info: 2671 | wifi = diagnostic.wifi_info 2672 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Wi-Fi Information:')}") 2673 | print(f" Interface: {wifi.interface}") 2674 | print(f" SSID: {wifi.ssid}") 2675 | if wifi.frequency: 2676 | band = "5GHz" if wifi.frequency > 4.0 else "2.4GHz" 2677 | print(f" Frequency: {wifi.frequency:.1f} GHz ({band})") 2678 | if wifi.channel: 2679 | print(f" Channel: {wifi.channel}") 2680 | if wifi.signal_dbm: 2681 | signal_color = Colors.BRIGHT_GREEN if wifi.signal_dbm > -50 else Colors.BRIGHT_YELLOW if wifi.signal_dbm > -70 else Colors.BRIGHT_RED 2682 | signal_text = self.color_manager.color(signal_color, f"{wifi.signal_dbm} dBm") 2683 | print(f" Signal: {signal_text}") 2684 | if wifi.bitrate: 2685 | print(f" Bitrate: {wifi.bitrate}") 2686 | 2687 | # Speed test results 2688 | if diagnostic.speed_test: 2689 | speed = diagnostic.speed_test 2690 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Speed Test Results:')}") 2691 | download_text = self.color_manager.color(Colors.BRIGHT_CYAN, f"{speed.download_mbps:.1f} Mbps") 2692 | upload_text = self.color_manager.color(Colors.BRIGHT_CYAN, f"{speed.upload_mbps:.1f} Mbps") 2693 | print(f" Download: {download_text}") 2694 | print(f" Upload: {upload_text}") 2695 | if speed.ping_ms > 0: 2696 | ping_text = self.color_manager.color(Colors.BRIGHT_CYAN, f"{speed.ping_ms:.1f} ms") 2697 | print(f" Ping: {ping_text}") 2698 | print(f" Server: {speed.server}") 2699 | print(f" Method: {speed.test_method}") 2700 | 2701 | # Diagnostic findings 2702 | if diagnostic.findings: 2703 | print(f"\n{self.color_manager.color(Colors.BOLD, 'Diagnostic Findings:')}") 2704 | 2705 | # Group findings by severity 2706 | by_severity = {} 2707 | for finding in diagnostic.findings: 2708 | if finding.severity not in by_severity: 2709 | by_severity[finding.severity] = [] 2710 | by_severity[finding.severity].append(finding) 2711 | 2712 | # Print in order of severity 2713 | for severity in [Severity.CRITICAL, Severity.WARNING, Severity.INFO, Severity.SUCCESS]: 2714 | if severity in by_severity: 2715 | severity_display = self.color_manager.get_severity_display(severity) 2716 | print(f"\n {severity_display}:") 2717 | for finding in by_severity[severity]: 2718 | print(f" • {finding.message}") 2719 | if finding.recommendation: 2720 | recommendation_text = self.color_manager.color(Colors.BLUE, finding.recommendation) 2721 | print(f" → {recommendation_text}") 2722 | 2723 | # Print diagnostic summary 2724 | self._print_diagnostic_summary() 2725 | 2726 | # Print context awareness summary 2727 | self._print_context_summary() 2728 | 2729 | print(f"\n{self.color_manager.color(Colors.BOLD, separator)}") 2730 | 2731 | def list_rules(self): 2732 | """List all available optimization rules""" 2733 | available_rules = self.rule_registry.get_available_rules() 2734 | enabled_rules = self.rule_registry.get_enabled_rules() 2735 | disabled_rules = self.rule_registry.disabled_rules 2736 | 2737 | print(f"{self.color_manager.color(Colors.BOLD, 'Available Optimization Rules:')}") 2738 | print(f"Total: {len(available_rules)} rules") 2739 | 2740 | if enabled_rules: 2741 | print(f"\n{self.color_manager.color(Colors.BRIGHT_GREEN, 'Enabled Rules:')}") 2742 | for rule in sorted(enabled_rules): 2743 | print(f" ✓ {rule}") 2744 | 2745 | if disabled_rules: 2746 | print(f"\n{self.color_manager.color(Colors.BRIGHT_RED, 'Disabled Rules:')}") 2747 | for rule in sorted(disabled_rules): 2748 | print(f" ✗ {rule}") 2749 | 2750 | if not available_rules: 2751 | warning_text = self.color_manager.color(Colors.YELLOW, "No rules loaded. Check rules/ directory.") 2752 | print(f"\n{warning_text}") 2753 | 2754 | @dataclass 2755 | class SystemContext: 2756 | """Shared context for optimization rules with caching""" 2757 | system_config: SystemConfig 2758 | interfaces: List[NetworkInterface] 2759 | wifi_info: Optional[WifiInfo] 2760 | logger: logging.Logger 2761 | 2762 | # Method references (not stored as callables to avoid typing issues) 2763 | _debugger: Optional['NetworkDebugger'] = None 2764 | 2765 | # Cached expensive operations 2766 | _cached_interfaces: Optional[List[NetworkInterface]] = None 2767 | _cached_wifi_info: Optional[WifiInfo] = None 2768 | _cached_internet_connectivity: Optional[bool] = None 2769 | _cached_systemd_status: Optional[bool] = None 2770 | 2771 | def run_command(self, cmd: Union[str, List[str]], timeout: int = 30, check: bool = False) -> Tuple[int, str, str]: 2772 | """Run command through debugger instance""" 2773 | if self._debugger: 2774 | return self._debugger._run_command(cmd, timeout, check) 2775 | return -1, "", "No debugger instance available" 2776 | 2777 | def check_internet_connectivity(self) -> bool: 2778 | """Check internet connectivity through debugger instance""" 2779 | if self._debugger: 2780 | return self._debugger._check_internet_connectivity() 2781 | return False 2782 | 2783 | def is_systemd_active(self) -> bool: 2784 | """Check systemd status through debugger instance""" 2785 | if self._debugger: 2786 | return self._debugger._is_systemd_active() 2787 | return False 2788 | 2789 | def get_interfaces(self) -> List[NetworkInterface]: 2790 | """Get network interfaces with caching""" 2791 | if self._cached_interfaces is None: 2792 | self._cached_interfaces = self.interfaces 2793 | return self._cached_interfaces 2794 | 2795 | def get_wifi_info(self) -> Optional[WifiInfo]: 2796 | """Get Wi-Fi info with caching""" 2797 | if self._cached_wifi_info is None: 2798 | self._cached_wifi_info = self.wifi_info 2799 | return self._cached_wifi_info 2800 | 2801 | def is_internet_available(self) -> bool: 2802 | """Check internet connectivity with caching""" 2803 | if self._cached_internet_connectivity is None: 2804 | self._cached_internet_connectivity = self.check_internet_connectivity() 2805 | return self._cached_internet_connectivity 2806 | 2807 | def is_systemd(self) -> bool: 2808 | """Check systemd status with caching""" 2809 | if self._cached_systemd_status is None: 2810 | self._cached_systemd_status = self.is_systemd_active() 2811 | return self._cached_systemd_status 2812 | 2813 | class RuleRegistry: 2814 | """Registry for dynamically loaded optimization rules""" 2815 | 2816 | def __init__(self, logger: logging.Logger): 2817 | self.logger = logger 2818 | self.rules = {} 2819 | self.disabled_rules = set() 2820 | 2821 | def load_rules(self, rules_package: str = "rules"): 2822 | """Dynamically load all rule modules from the rules package""" 2823 | try: 2824 | # Import the rules package 2825 | rules_path = Path(__file__).parent / rules_package 2826 | if not rules_path.exists(): 2827 | self.logger.warning(f"Rules directory {rules_path} not found") 2828 | return 2829 | 2830 | # Find all .py files in rules directory 2831 | for module_info in pkgutil.iter_modules([str(rules_path)]): 2832 | module_name = module_info.name 2833 | if module_name.startswith('_'): # Skip private modules 2834 | continue 2835 | 2836 | try: 2837 | # Dynamically import the module 2838 | module = importlib.import_module(f"{rules_package}.{module_name}") 2839 | 2840 | # Check if module has analyze function 2841 | if hasattr(module, 'analyze') and callable(module.analyze): 2842 | self.rules[module_name] = module.analyze 2843 | self.logger.debug(f"Loaded rule module: {module_name}") 2844 | else: 2845 | self.logger.warning(f"Rule module {module_name} missing analyze() function") 2846 | 2847 | except Exception as e: 2848 | self.logger.error(f"Failed to load rule module {module_name}: {e}") 2849 | 2850 | except Exception as e: 2851 | self.logger.error(f"Failed to load rules from {rules_package}: {e}") 2852 | 2853 | def disable_rule(self, rule_name: str): 2854 | """Disable a specific rule""" 2855 | self.disabled_rules.add(rule_name) 2856 | self.logger.info(f"Disabled rule: {rule_name}") 2857 | 2858 | def enable_rule(self, rule_name: str): 2859 | """Enable a previously disabled rule""" 2860 | self.disabled_rules.discard(rule_name) 2861 | self.logger.info(f"Enabled rule: {rule_name}") 2862 | 2863 | def get_available_rules(self) -> List[str]: 2864 | """Get list of all available rule names""" 2865 | return list(self.rules.keys()) 2866 | 2867 | def get_enabled_rules(self) -> List[str]: 2868 | """Get list of enabled rule names""" 2869 | return [name for name in self.rules.keys() if name not in self.disabled_rules] 2870 | 2871 | def analyze_all(self, context: SystemContext) -> List[OptimizationRule]: 2872 | """Run all enabled rules and collect optimization rules""" 2873 | all_rules = [] 2874 | 2875 | for rule_name, analyze_func in self.rules.items(): 2876 | if rule_name in self.disabled_rules: 2877 | self.logger.debug(f"Skipping disabled rule: {rule_name}") 2878 | continue 2879 | 2880 | try: 2881 | rules = analyze_func(context) 2882 | all_rules.extend(rules) 2883 | self.logger.debug(f"Rule {rule_name} generated {len(rules)} optimization(s)") 2884 | except Exception as e: 2885 | self.logger.error(f"Rule {rule_name} failed: {e}") 2886 | 2887 | return all_rules 2888 | 2889 | def load_config(self, config_path: str): 2890 | """Load rule configuration from YAML file""" 2891 | try: 2892 | with open(config_path, 'r') as f: 2893 | config = yaml.safe_load(f) 2894 | 2895 | # Handle disabled rules 2896 | disabled_rules = config.get('disabled_rules', []) 2897 | for rule in disabled_rules: 2898 | self.disable_rule(rule) 2899 | 2900 | self.logger.info(f"Loaded rule configuration from {config_path}") 2901 | 2902 | except FileNotFoundError: 2903 | self.logger.debug(f"No config file found at {config_path}") 2904 | except Exception as e: 2905 | self.logger.error(f"Failed to load config from {config_path}: {e}") 2906 | 2907 | def main(): 2908 | """Main entry point""" 2909 | # Check Python version compatibility 2910 | if sys.version_info < (3, 7): 2911 | print("Error: This tool requires Python 3.7 or higher", file=sys.stderr) 2912 | print(f"Current version: {sys.version}", file=sys.stderr) 2913 | sys.exit(1) 2914 | 2915 | parser = argparse.ArgumentParser( 2916 | description="Network Debug Tool for Linux Wi-Fi Performance Optimization", 2917 | formatter_class=argparse.RawDescriptionHelpFormatter, 2918 | epilog=""" 2919 | Examples: 2920 | %(prog)s --diagnose # Full diagnostic scan 2921 | %(prog)s --check-speed # Speed test only 2922 | %(prog)s --tune # Apply performance tuning 2923 | %(prog)s --tune --dry-run # Simulate performance tuning 2924 | %(prog)s --crawl-config # Crawl system configuration 2925 | %(prog)s --optimize-suggestions # Generate optimization recommendations 2926 | %(prog)s --auto-fix # Apply safe optimizations automatically 2927 | %(prog)s --auto-fix --dry-run # Simulate safe optimizations 2928 | %(prog)s --interactive # Interactive optimization mode 2929 | %(prog)s --interactive --dry-run # Interactive simulation mode 2930 | %(prog)s --benchmark-before # Run baseline benchmark 2931 | %(prog)s --benchmark-after # Run benchmark and compare 2932 | %(prog)s --check-deps # Check system dependencies 2933 | %(prog)s --expert-json # Export detailed JSON report 2934 | %(prog)s --diagnose --verbose # Verbose diagnostic output 2935 | %(prog)s --list-rules # List all available optimization rules 2936 | %(prog)s --skip-rule tcp --skip-rule dns # Skip specific rules 2937 | %(prog)s --config config.yaml # Use custom configuration file 2938 | """ 2939 | ) 2940 | 2941 | parser.add_argument( 2942 | '--check-speed', 2943 | action='store_true', 2944 | help='Perform network speed test' 2945 | ) 2946 | 2947 | parser.add_argument( 2948 | '--diagnose', 2949 | action='store_true', 2950 | help='Perform comprehensive network diagnosis' 2951 | ) 2952 | 2953 | parser.add_argument( 2954 | '--tune', 2955 | action='store_true', 2956 | help='Apply automatic performance tuning' 2957 | ) 2958 | 2959 | parser.add_argument( 2960 | '--expert-json', 2961 | action='store_true', 2962 | help='Export detailed diagnostic data as JSON' 2963 | ) 2964 | 2965 | parser.add_argument( 2966 | '--crawl-config', 2967 | action='store_true', 2968 | help='Crawl system configuration files and parameters' 2969 | ) 2970 | 2971 | parser.add_argument( 2972 | '--optimize-suggestions', 2973 | action='store_true', 2974 | help='Generate optimization suggestions based on system analysis' 2975 | ) 2976 | 2977 | parser.add_argument( 2978 | '--auto-fix', 2979 | action='store_true', 2980 | help='Automatically apply safe optimizations' 2981 | ) 2982 | 2983 | parser.add_argument( 2984 | '--interactive', 2985 | action='store_true', 2986 | help='Interactively prompt before applying each optimization' 2987 | ) 2988 | 2989 | parser.add_argument( 2990 | '--benchmark-before', 2991 | action='store_true', 2992 | help='Run network benchmark and save baseline measurements' 2993 | ) 2994 | 2995 | parser.add_argument( 2996 | '--benchmark-after', 2997 | action='store_true', 2998 | help='Run network benchmark and compare with previous baseline' 2999 | ) 3000 | 3001 | parser.add_argument( 3002 | '--benchmark-report', 3003 | action='store_true', 3004 | help='Generate markdown benchmark comparison report' 3005 | ) 3006 | 3007 | parser.add_argument( 3008 | '--check-deps', 3009 | action='store_true', 3010 | help='Check availability of system dependencies' 3011 | ) 3012 | 3013 | parser.add_argument( 3014 | '--version', 3015 | action='version', 3016 | version=f'Network Debug Tool v{VERSION}' 3017 | ) 3018 | 3019 | parser.add_argument( 3020 | '--verbose', '-v', 3021 | action='store_true', 3022 | help='Enable verbose output' 3023 | ) 3024 | 3025 | parser.add_argument( 3026 | '--no-color', 3027 | action='store_true', 3028 | help='Disable colored output' 3029 | ) 3030 | 3031 | parser.add_argument( 3032 | '--dry-run', 3033 | action='store_true', 3034 | help='Simulate changes without modifying system (show what would be done)' 3035 | ) 3036 | 3037 | parser.add_argument( 3038 | '--output', '-o', 3039 | help='Output file for JSON export' 3040 | ) 3041 | 3042 | parser.add_argument( 3043 | '--skip-rule', 3044 | action='append', 3045 | dest='skip_rules', 3046 | help='Skip specific optimization rule (can be used multiple times)' 3047 | ) 3048 | 3049 | parser.add_argument( 3050 | '--config', 3051 | help='Path to configuration YAML file for rule settings' 3052 | ) 3053 | 3054 | parser.add_argument( 3055 | '--list-rules', 3056 | action='store_true', 3057 | help='List all available optimization rules' 3058 | ) 3059 | 3060 | args = parser.parse_args() 3061 | 3062 | # Default to diagnose if no specific action is specified 3063 | if not any([args.check_speed, args.diagnose, args.tune, args.expert_json, 3064 | args.crawl_config, args.optimize_suggestions, args.auto_fix, 3065 | args.interactive, args.benchmark_before, args.benchmark_after, 3066 | args.check_deps]): 3067 | args.diagnose = True 3068 | 3069 | # Initialize debugger 3070 | debugger = NetworkDebugger( 3071 | verbose=args.verbose, 3072 | no_color=args.no_color, 3073 | dry_run=args.dry_run, 3074 | skip_rules=args.skip_rules, 3075 | config_file=args.config 3076 | ) 3077 | 3078 | # Check for root privileges when needed (skip check in dry-run mode) 3079 | if (args.tune or args.auto_fix or args.interactive) and not args.dry_run: 3080 | if not debugger._is_root(): 3081 | warning_text = debugger.color_manager.color(Colors.YELLOW, "Warning: This operation requires root privileges for system-level changes") 3082 | command_text = debugger.color_manager.color(Colors.CYAN, "sudo python3 network_debug.py") 3083 | print(f"{warning_text}") 3084 | print(f"Run with sudo for full functionality: {command_text}") 3085 | print(f"Continuing with limited functionality...\n") 3086 | elif args.dry_run and (args.tune or args.auto_fix or args.interactive): 3087 | info_text = debugger.color_manager.color(Colors.BRIGHT_CYAN, "Dry-run mode: No system changes will be made") 3088 | print(f"{info_text}\n") 3089 | 3090 | try: 3091 | if args.list_rules: 3092 | debugger.list_rules() 3093 | return 3094 | 3095 | if args.check_deps: 3096 | dependencies = debugger.check_dependencies() 3097 | debugger.print_dependency_status(dependencies) 3098 | return 3099 | 3100 | if args.benchmark_before: 3101 | benchmark = debugger.run_benchmark() 3102 | filename = args.output or "network_baseline_before.json" 3103 | debugger.save_benchmark(benchmark, filename) 3104 | print(f"\n{Colors.BOLD}Baseline benchmark completed:{Colors.RESET}") 3105 | print(f"Download: {benchmark.download_mbps:.1f} Mbps") 3106 | print(f"Upload: {benchmark.upload_mbps:.1f} Mbps") 3107 | print(f"Ping: {benchmark.ping_ms:.1f} ms") 3108 | print(f"TCP Retransmissions: {benchmark.tcp_retransmissions}") 3109 | print(f"DNS Latency: {benchmark.dns_latency_ms:.1f} ms") 3110 | print(f"\nRun optimizations, then use --benchmark-after to measure improvements") 3111 | return 3112 | 3113 | if args.benchmark_after: 3114 | after_benchmark = debugger.run_benchmark() 3115 | 3116 | # Try to load before benchmark 3117 | before_file = "network_baseline_before.json" 3118 | before_benchmark = debugger.load_benchmark(before_file) 3119 | 3120 | if before_benchmark: 3121 | comparison = debugger.compare_benchmarks(before_benchmark, after_benchmark) 3122 | debugger.print_benchmark_comparison(comparison) 3123 | 3124 | if args.benchmark_report or args.output: 3125 | report = debugger.generate_benchmark_report(comparison) 3126 | report_file = args.output or "benchmark_comparison.md" 3127 | with open(report_file, 'w') as f: 3128 | f.write(report) 3129 | print(f"\nBenchmark report saved to {report_file}") 3130 | 3131 | # Save after benchmark 3132 | after_file = args.output or "network_baseline_after.json" 3133 | if after_file.endswith('.md'): 3134 | after_file = "network_baseline_after.json" 3135 | debugger.save_benchmark(after_benchmark, after_file) 3136 | else: 3137 | print(f"{Colors.YELLOW}No baseline benchmark found. Run --benchmark-before first.{Colors.RESET}") 3138 | filename = args.output or "network_baseline_after.json" 3139 | debugger.save_benchmark(after_benchmark, filename) 3140 | return 3141 | 3142 | if args.tune: 3143 | debugger.tune() 3144 | 3145 | if args.crawl_config: 3146 | system_config = debugger.crawl_config() 3147 | print(f"\n{Colors.BOLD}System Configuration Summary:{Colors.RESET}") 3148 | print(f"Sysctl parameters: {len(system_config.sysctl_params)}") 3149 | print(f"NetworkManager configs: {len(system_config.network_manager_configs)}") 3150 | print(f"DNS config entries: {len(system_config.dns_config)}") 3151 | print(f"Firewall rules: {len(system_config.firewall_rules)}") 3152 | print(f"Driver configs: {len(system_config.driver_params)}") 3153 | print(f"Interface settings: {len(system_config.interface_settings)}") 3154 | 3155 | if args.output: 3156 | json_data = json.dumps(asdict(system_config), indent=2, default=str) 3157 | with open(args.output, 'w') as f: 3158 | f.write(json_data) 3159 | print(f"\nSystem configuration exported to {args.output}") 3160 | 3161 | if args.optimize_suggestions or args.auto_fix or args.interactive: 3162 | # Run comprehensive analysis 3163 | diagnostic = debugger.diagnose() 3164 | system_config = debugger.crawl_config() 3165 | 3166 | # Generate optimization analysis 3167 | optimization = debugger.analyze_optimizations( 3168 | system_config, 3169 | diagnostic.interfaces, 3170 | diagnostic.wifi_info 3171 | ) 3172 | 3173 | # Add optimization analysis to diagnostic 3174 | diagnostic.optimization_analysis = optimization 3175 | 3176 | if args.interactive: 3177 | # Interactive optimization mode 3178 | results = debugger.apply_optimizations_interactive(optimization) 3179 | print(f"\n{Colors.BOLD}Interactive Mode Results:{Colors.RESET}") 3180 | applied = sum(1 for success in results.values() if success) 3181 | total = len(results) 3182 | print(f"Applied {applied}/{total} optimizations") 3183 | 3184 | # Suggest running benchmark if optimizations were applied 3185 | if applied > 0: 3186 | print(f"\n{Colors.BOLD}Recommendation:{Colors.RESET}") 3187 | print("Run --benchmark-after to measure the impact of these optimizations") 3188 | 3189 | elif args.auto_fix: 3190 | # Apply safe optimizations 3191 | results = debugger.apply_safe_optimizations(optimization) 3192 | print(f"\n{Colors.BOLD}Auto-fix Results:{Colors.RESET}") 3193 | applied = sum(1 for success in results.values() if success) 3194 | total = len(results) 3195 | print(f"Applied {applied}/{total} optimizations successfully") 3196 | 3197 | # Suggest running benchmark if optimizations were applied 3198 | if applied > 0: 3199 | print(f"\n{Colors.BOLD}Recommendation:{Colors.RESET}") 3200 | print("Run --benchmark-after to measure the impact of these optimizations") 3201 | 3202 | if args.optimize_suggestions: 3203 | # Print optimization recommendations 3204 | debugger.print_optimization_results(optimization) 3205 | 3206 | # Generate markdown report if output specified 3207 | if args.output: 3208 | report = debugger.generate_optimization_report(optimization) 3209 | with open(args.output, 'w') as f: 3210 | f.write(report) 3211 | print(f"\nOptimization report exported to {args.output}") 3212 | 3213 | elif args.expert_json: 3214 | json_data = debugger.expert_json() 3215 | if args.output: 3216 | with open(args.output, 'w') as f: 3217 | f.write(json_data) 3218 | print(f"Expert diagnostic exported to {args.output}") 3219 | else: 3220 | print(json_data) 3221 | 3222 | elif args.check_speed: 3223 | speed_result = debugger.check_speed() 3224 | if speed_result: 3225 | print(f"\nSpeed Test Results:") 3226 | print(f"Download: {speed_result.download_mbps:.1f} Mbps") 3227 | print(f"Upload: {speed_result.upload_mbps:.1f} Mbps") 3228 | print(f"Server: {speed_result.server}") 3229 | 3230 | # Provide benchmark links 3231 | print(f"\n{Colors.BOLD}Verify Results:{Colors.RESET}") 3232 | print(f"• Fast.com: {Colors.BLUE}https://fast.com{Colors.RESET}") 3233 | print(f"• Cloudflare: {Colors.BLUE}https://speed.cloudflare.com{Colors.RESET}") 3234 | print(f"• Google: {Colors.BLUE}https://speed.measurementlab.net{Colors.RESET}") 3235 | else: 3236 | print("Speed test failed") 3237 | 3238 | elif args.diagnose: 3239 | diagnostic = debugger.diagnose() 3240 | debugger.print_results(diagnostic) 3241 | 3242 | if args.output: 3243 | json_data = json.dumps(asdict(diagnostic), indent=2, default=str) 3244 | with open(args.output, 'w') as f: 3245 | f.write(json_data) 3246 | print(f"\nDiagnostic results exported to {args.output}") 3247 | 3248 | except KeyboardInterrupt: 3249 | print(f"\n{Colors.YELLOW}Operation cancelled by user{Colors.RESET}") 3250 | sys.exit(1) 3251 | except Exception as e: 3252 | print(f"\n{Colors.RED}Error: {e}{Colors.RESET}") 3253 | if args.verbose: 3254 | import traceback 3255 | traceback.print_exc() 3256 | sys.exit(1) 3257 | 3258 | if __name__ == '__main__': 3259 | main() 3260 | --------------------------------------------------------------------------------