diff --git a/net/nss-switch/CHANGELOG b/net/nss-switch/CHANGELOG new file mode 100644 index 0000000000000..7d823d7c31b1e --- /dev/null +++ b/net/nss-switch/CHANGELOG @@ -0,0 +1,224 @@ +# Changelog + +## 07 July 2026 +### Pull Request Review Fixes, Part 2 + +* **fix(UCI config):** + * Migrated from shell-sourced `/etc/config/nss-switch-conf` to proper UCI format `/etc/config/nss-switch`. + * Added `uci_load_config()` with automatic creation of a default config file if missing. + * Updated `cmd_config()` to use `uci` commands for reading/writing configuration. + * Updated `debug.sh` `cmd_debug_log()` to use `uci` for enabling/disabling debug mode. + * Update lifecycle methods to get vars via UCI. + +* **fix(comment validation):** + * Added `nft_validate_comment()` to reject forbidden characters: `;`, `"`, `'`, `\`, `$`, `|`, control characters. + * Updated `rules_validate()` to accept `$comment` as the 7th parameter, and validate them. + * Fixed `cmd_add()` to pass comment to `rules_validate()`. + +* **fix(nft rules, ct mark preservation):** + * Replaced mark-overwriting rules (`ct mark set meta mark and ${NSS_MARK}`) with bit-preserving operations (`ct mark set (ct mark & ~${NSS_MARK}) | (meta mark & ${NSS_MARK})`) to avoid clobbering bits used by QoS, `fw4`, policy routing, and other subsystems. + * Moved preservation rules from global `nft_add_chains()` into per-rule `_nft_emit_rule()`, so they only apply to matching connections. + * Added `mangle_output` hook to apply bypass rules to router-originated traffic. + +* **fix(conntrack flushing):** + * Added `_ct_clear_all_marks()` helper to clean all NSS-Switch marks from conntrack. + * `ct_clear_rule_marks()` now always calls `_ct_clear_all_marks()` after flushing, ensuring no residual marks remain. + * Enhanced interface flushing to use `conntrack -D -i`, with fallback to IP/subnet-based flushing for both src/dst. + +* **fix(`nss-ct-dump.c` IPv6 compression bug):** + * Fixed `compress_ipv6()`: a zero-group run not starting at group 0 emitted only a single `:` instead of `::`, producing malformed addresses (e.g. `2606:4700:4700:1111` instead of `2606:4700:4700::1111`) that were invalid both for `nft_validate_ipv6()` and for nftables syntax. + * Root cause fixed at the source (compression), so no downstream expansion/workaround is needed in `rules_validate()` or `_nft_emit_rule()`. + * Removed hardcoded `NSS_MARK` define; now read dynamically from UCI via `get_nss_mark_from_uci()`. + * Added `strcpy(iface, "?")` before `popen()` to prevent reuse of stale interface values from previous lookups. + * Improved local IP detection via `load_local_ips()` / `is_local_ip()` for more accurate interface display of router-originated traffic. + +* **fix(watch UI, sorting):** + * Added interactive column sorting in watch mode. + * Added `_flush_input()` helper to prevent key echo and input buffer flooding. + * Updated `ui_conn_header()` to display numbered column titles (`NUM₁ PROTO₂ SRC₃ DST₄ IFACE₅ NSS₆`). + * Updated `ui_hint_bar()` to display current sort column and available key shortcuts. + +* **fix(postrm & `flush --all`):** + * Corrected `flush --all` to remove `/etc/firewall.d/nss-bypass-rules` instead of the incorrect path previously used. + * Added `conntrack -D -m "$NSS_MARK"` to `flush --all` to remove residual conntrack marks. + * Removed deletion of conffiles from `postrm` to respect `opkg` conffile handling and preserve user configuration during package removal. + +* **fix(debug logging):** + * Debug log (`/tmp/nss-switch.log`) is now created only when explicitly enabled via `nss-switch debug log`. + * `dbg()` writes to the log only when debug mode is enabled and the log file already exists. + * `nft_apply()` redirects firewall reload output to the debug log only while debug logging is enabled. + +* **fix(`debug env`, BusyBox version)** + +* **fix(nft_generate_script execution):** + * Added explicit execution of the generated firewall script (`sh "$FW_SCRIPT"`) inside `nft_apply()` after firewall reload, ensuring generated nftables rules are actually applied. + +--- + +## 28 May 2026: + +* feat(kill): Added nss-switch kill command to forcibly flush conntrack entries matching a rule ID or custom criteria (--proto, --src-ip, --dst-ip, --src-port, --dst-port, --iface). Removes the rule automatically when ID is given. + +* feat(nft): Added mangle_output hook to apply bypass rules to locally generated traffic (router-originated DNS, NTP, WireGuard, etc...). + +* fix(interface display): Improved `nss-ct-dump` to show real output interface (pppoe-wan, etc.) instead of lo for router-originated traffic by checking local IPs and using dst_ip for route lookup. + +* fix(conntrack flush): Enhanced interface handling in kill command to derive IP/subnet from interface name for both input (iifname) and output (oifname/local:) cases. + +--- + +## 27 May 2026: + +### OpenWrt PR Review Fixes Part 1 + +* **fix(paths):** + * Relocated all package files to standard FHS paths: `/usr/lib/nss-switch/` for binaries/libraries, `/etc/config/nss-switch-conf` for configuration, `/etc/nss-switch/rules.conf` for persistent rules, and `/etc/firewall.d/nss-bypass-rules` for firewall hooks. + * Fixed Makefile installation paths to match new directory structure. + * Removed hardcoded interface names (`lan2`, `lan3`, etc.) from `_normalize_iface_rule()`; now only strips direction prefixes (`out:`, `in:`, `local:`). + +* **fix(nft):** + - **IPv4/IPv6 support:** `_nft_emit_rule()` now detects IP version and uses `ip saddr/daddr` for IPv4, `ip6 saddr/daddr` for IPv6. + - **Port rules:** No longer generates invalid syntax when `proto=any` with port macthes; ports only added when protocol is specified. + - **Comment escaping:** User-provided comments sanitised to prevent nftables syntax errors and command injection. + - **Interface direction:** `local:` prefix now correctly maps to `oifname` (router-originated traffic), while unprefixed interfaces map to `iifname` (normal ingress). + +* **fix(interface validation):** + - `nft_validate_iface()` now strips direction prefixes (`out:`, `in:`, `local:`) before validating interface existance with `ip link show`. + +* **fix(conntrack clearing):** + - `ct_clear_rule_marks()` no longer appends duplicate `-s` filter when `--src-ip` is already specified; interface-derived subnet is only added when no source IP is provided. + +* **fix(UCI integration):** + - Replaced direct config file editing (`sed`/`printf >>`) with proper `uci` commands for adding and removing firewall include sections (`_nft_ensure_uci_include`, `_nft_remove_uci_include`). + - Fixed `prerm` UCI existence check: now uses `uci -q get firewall.nss_bypass_include.type` instead of invalid `uci get firewall.nss_bypass_include`. + +* **fix(debug logging):** + - `DEBUG_LOG` (`/tmp/nss-switch.log`) is now only created when debug is explicitly enabled via `nss-switch debug log` menu. + - `dbg()` function no longer writes to log unless debug is active AND log file exists. + - `nft_apply()` only redirects firewall reload output to debug log when debug is enabled. + +* **fix(cursor restoration):** + - Corrected `ui_cursor_show` --> `ui_cursor_hide` in `cmd_watch()` (comment said hide, code was showing). + +* **fix(dependencies):** + - Removed unnecessary `conntrackd` dependency from Makefile (only `conntrack` is required). + +* **fix(port extraction in UI):** + - `ui_conn_row()` now correctly extracts destination port from `ip:port`, `[ipv6]:port`, and `ip#port` formats using `case` statement. + +* **fix(comment validation):** + - Added `nft_validate_comment()` to reject forbidden characters (`;`, `"`, `'`, `\`, `$`, `|`, control chars) in rule comments. + +* **fix(input validation in `pick`):** + - `ui_ask_input()` now supports type validation: `ip`, `port`, `proto`, `string`. Invalid inputs are rejected and retried. + +* **fix(`ui_ask_yn`):** + - Now strictly accepts `y/Y/yes/YES` or `n/N/no/NO`; any other input (including garbage) is rejected and reprompts. `q/Q` cancels the entire operation. + +* **fix(rule listing UI alignment):** + - Implemented dynamic column width calculation in `rules_list()` to avoid hardcoded widths that broke layout; colours preserved for ID, PERSIST, and COMMENT fields. + +* **fix(nss-ct-dump binary performance):** + - `get_nss_state()` now caches `access()` results for debugfs paths (`ecm_nss_ipv4`, `ecm_sfe_ipv4`), eliminating redundant syscalls on every conntrack line (optimisation for large connection tables). + +* **fix(C interface resolution):** + - `get_iface_for_ip()` simplified: no longer attempts to parse MAC addresses or bridge names; returns `lo` for router-local IPs, otherwise passes through `ip route get` output unchanged. + - Removed `normalize_iface()` hardcoded transformations (`pppoe-wan` -> `wan`, `br-lan` ->`lan`); interface names now appear as-is. + +* **fix(protocol display):** + - Expanded protocol number-to-name mapping in C binary to include all common protocols (GRE, ESP, AH, SCTP, etc.) using data from `/etc/protocols` for visual display while preserving numeric values for nftables rules. + +### New Features & Enhancements (beyond PR review) + +* **feat(offload engine compatibility):** + - Complete rewrite of `ecm.sh` to support multiple offload engines: + - **NSS** --> Qualcomm NSS hardware offload (ECM debugfs) + - **SFE_ECM** --> Qualcomm SFE as ECM frontend + - **SFE** --> Qualcomm SFE standalone (`/dev/sfe`, `shortcut_fe` module) + - **MTK_PPE** --> MediaTek PPE/HNAT hardware offload (`/sys/kernel/debug/ppe0`) + - **SW_FLOW** --> Linux `nf_flow_table` software flow offload + - Unified API (`ecm_frontend`, `ecm_defunct_all`, `ecm_restart`, `ecm_connections`, `ecm_stats`) works across all engines. + - Added `offload_detect()` with result caching to avoid redundant detection. + - `detect_check_all()` now shows engine-specific information and warnings. + +* **feat(UI improvements):** + - `ui_ask_input` now validates IP addresses, ports, and protocols in real-time. + - `debug log` command now interactive: shows status, last lines, and prompts to enable/disable logging. + - Better coloured output in `nss-switch list`. + +* **feat(ECM connections parser):** + - `ecm_connections()` now parses connection data directly from engine sources (ecm_dump.sh, /dev/sfe, PPE debugfs, conntrack) without external dependencies. + +* **refactor(build system):** + - Restructured package directories to follow OpenWrt and FHS standards. + - Makefile now explicitly lists all library scripts (no wildcards). + - `preinst` detects offload engine type and reports to user during installation. + + +### 22 May 2026: Release v1.0.0 (aarch64, C compiled components) + +- **Architecture:** native aarch64 support for Qualcomm IPQ807x / NSS platform +- **Performance:** `ct_dump_all_full()` fully migrated from shell to C + - **Speed improvement: ~30s -> <100ms for 1000+ connections** +- **Hybrid design:** UI remains in shell , heavy parsing in C (fast) +- **APK:** aarch64-only, includes both shell scripts and compiled binary, ccompiled with GitHub Actions for transparency + + + +### 21 May 2026 +* **feat(ui):** + * Added PROTO name mapping for common ports (SSH, HTTP, HTTPS, DNS, WG, MYSQL, PG, MONGO, REDIS, etc...) max 6 chars. + * Implemented terminal width check (`ui_check_width`), requires minimum 120 columns for proper UI display. + * Improved spinner with colour cycling animation. +* **feat(conntrack):** + * Added interface normalisation functions `_normalize_iface_display` and `_normalize_iface_rule`. + * Normalised interface names: `local:pppoe-wan`/`pppoe-wan` -> `wan`, `local:br-lan`/`br-lan` -> `lan`, etc. +* **fix(cleanup):** + * Fixed cursor not restoring after exit in `cmd_watch()` and `cmd_pick()` commands. + * Fixed cleanup for orphaned `/tmp/nss-iface.*` files on trap and exit. + * Unified temporary file cleanup in `_clean_tmp()`. +* **fix(nft):** + * Added interface normalisation for rule validation and emission, now `wan`, `wan.20`, `pppoe-wan`, `wan_6` all belong to `pppoe-wan` for nftables rules. + + +### 20 May 2026 +* **fix(ui):** + * Adjusted and fixed column widths to a strict `45/45/6` layout. + * Disabled `alt_screen` buffer to enable native terminal scrolling. + * Integrated a visual loading indicator. + * Corrected colour palette rendering issues specifically for the `ash` shell environment. +* **fix(watch):** + * Added `printf "\033[2J\033[3J"` to clear both screen and scrollback buffer on each `watch` refresh. + * Eliminated garbage/artifacts left behind after terminal scroll or PgUp/PgDown. + * Unified header bar, hint bar, separator lines and footer messages to match exact table width. + * Added `ui_table_width()` function to calculate total table width dynamically. +* **fix(pick):** + * Removed pagination, now shows ALL connections at once with native terminal scroll. + * Fixed tmpfile persistence issue that caused "Connection X not found" errors. + * Disabled alt_screen for proper PgUp/PgDown support. +* **fix(colours):** + * Finalised ANSI colour pattern using `ESC=$(printf '\033')` for ash/BusyBox v1.1+ compatibility. + +### 19 May 2026 +* **feat:** + * Introduced initial **IPv6 support**. + * Added a dedicated real-time debug monitor (`nss-switch debug monitor`). + * Added debug subcommands: `env`, `ecm`, `nft`, `conntrack`, `mark`, `defunct-all`, `frontend-stop`, `frontend-restart`, `log`, `log-clear`, `rules-raw`, `script-raw`. + * Resolved underlying issues within the `pick` command execution logic. + * Unified the connection tracking dump tool (`ct_dump`). + * Optimised network rule validation routines and post-execution cleanup processes. + +### 14 May 2026 +* **fix:** Corrected IPv6 compression logic and standardised bracketed notation `[]:` for address configurations. + +### 13 May 2026 +* **feat:** + * Extended IPv6 operational support. + * Introduced `ct_mark_all_full` logic designed for robust Local Loopback (`lo`) and Wide Area Network (`wan`) boundary management. + * Patched and stabilised the Command Line Interface (CLI) UI layout. + +### 11 May 2026 +* **fix/feat:** + * Refactored multiple instances of legacy syntax to strictly comply with `ash` shell constraints. + * Rolled out comprehensive `conntrack` filtering support across parameters: interfaces (`iface`), source IPs (`src_ip`), destination IPs (`dst_ip`), destination ports, protocols, and auxiliary variables. + * Drafted and committed **Blueprint 1** for the *NSS Switch Test Tool*. diff --git a/net/nss-switch/LICENSE b/net/nss-switch/LICENSE new file mode 100644 index 0000000000000..d159169d10508 --- /dev/null +++ b/net/nss-switch/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/net/nss-switch/Makefile b/net/nss-switch/Makefile new file mode 100644 index 0000000000000..f2bb8771a23ab --- /dev/null +++ b/net/nss-switch/Makefile @@ -0,0 +1,138 @@ +# +# SPDX-License-Identifier: GPL-2.0-only +# +include $(TOPDIR)/rules.mk +PKG_NAME:=nss-switch +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_MAINTAINER:=Alexander Gomez +PKG_LICENSE:=GPL-2.0-only +PKG_LICENSE_FILES:=LICENSE +include $(INCLUDE_DIR)/package.mk +define Package/nss-switch + SECTION:=net + CATEGORY:=Network + TITLE:=Qualcomm NSS Bypass tool + DEPENDS:=+conntrack +endef +define Package/nss-switch/description + A tool designed to manage Hardware Offload for qualcommax NSS platforms. + While optimised for NSS-enabled firmware to allow deep packet inspection, + it remains compatible with any platform using Software Offloading (SFE). +endef +define Package/nss-switch/conffiles +/etc/config/nss-switch +/etc/nss-switch/rules.conf +endef +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + $(CP) ./src/* $(PKG_BUILD_DIR)/ +endef +define Build/Compile + $(MAKE) -C $(PKG_BUILD_DIR) \ + CC="$(TARGET_CC)" \ + CFLAGS="$(TARGET_CFLAGS)" \ + LDFLAGS="$(TARGET_LDFLAGS)" +endef +define Package/nss-switch/install + $(INSTALL_DIR) $(1)/usr/lib/nss-switch/bin + $(INSTALL_DIR) $(1)/usr/lib/nss-switch/lib + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_DIR) $(1)/etc/nss-switch + $(INSTALL_DIR) $(1)/etc/firewall.d + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/nss-ct-dump $(1)/usr/lib/nss-switch/bin/nss-ct-dump + $(INSTALL_BIN) ./files/usr/lib/nss-switch/nss-switch.sh $(1)/usr/lib/nss-switch/nss-switch.sh + $(INSTALL_BIN) ./files/etc/firewall.d/nss-bypass-rules $(1)/etc/firewall.d/nss-bypass-rules + $(INSTALL_DATA) ./files/etc/config/nss-switch $(1)/etc/config/nss-switch + $(INSTALL_BIN) ./files/usr/lib/nss-switch/lib/chandler.sh $(1)/usr/lib/nss-switch/lib/chandler.sh + $(INSTALL_BIN) ./files/usr/lib/nss-switch/lib/conntrack.sh $(1)/usr/lib/nss-switch/lib/conntrack.sh + $(INSTALL_BIN) ./files/usr/lib/nss-switch/lib/debug.sh $(1)/usr/lib/nss-switch/lib/debug.sh + $(INSTALL_BIN) ./files/usr/lib/nss-switch/lib/detect.sh $(1)/usr/lib/nss-switch/lib/detect.sh + $(INSTALL_BIN) ./files/usr/lib/nss-switch/lib/ecm.sh $(1)/usr/lib/nss-switch/lib/ecm.sh + $(INSTALL_BIN) ./files/usr/lib/nss-switch/lib/nft.sh $(1)/usr/lib/nss-switch/lib/nft.sh + $(INSTALL_BIN) ./files/usr/lib/nss-switch/lib/rules.sh $(1)/usr/lib/nss-switch/lib/rules.sh + $(INSTALL_BIN) ./files/usr/lib/nss-switch/lib/ui.sh $(1)/usr/lib/nss-switch/lib/ui.sh + $(INSTALL_DATA) ./files/etc/nss-switch/rules.conf $(1)/etc/nss-switch/rules.conf + ln -sf /usr/lib/nss-switch/nss-switch.sh $(1)/usr/bin/nss-switch +endef +define Package/nss-switch/preinst +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + echo "Checking platform compatibility..." + if lsmod 2>/dev/null | grep -q "qca_nss_drv"; then + echo "[OK] Qualcomm NSS (Hardware offload)" + elif [ -d /sys/kernel/debug/ecm/ecm_sfe_ipv4 ] || \ + lsmod 2>/dev/null | grep -q "^shortcut_fe"; then + echo "[OK] Qualcomm SFE (Software offload)" + elif [ -d /sys/kernel/debug/ppe0 ]; then + echo "[OK] MediaTek PPE/HNAT (Hardware offload)" + elif lsmod 2>/dev/null | grep -q "^nf_flow_table"; then + echo "[OK] Linux flow offload (Software offload)" + else + echo "[NOTICE] No offload engine detected, tool will run in fallback mode" + fi + echo "Starting NSS-Switch installation..." +fi +exit 0 +endef +define Package/nss-switch/postinst +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + if [ -x /etc/init.d/firewall ]; then + /etc/init.d/firewall reload 2>/dev/null + echo "[NSS-Switch] Firewall reloaded to apply post-install hooks" >&2 + fi +fi +exit 0 +endef +define Package/nss-switch/prerm +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + NSS_MARK=$(uci -q get nss-switch.settings.nss_mark 2>/dev/null || echo "0x00010000") + NFT_CHAIN_PRE=$(uci -q get nss-switch.settings.nft_chain_pre 2>/dev/null || echo "nss_bypass_pre") + NFT_CHAIN_POST=$(uci -q get nss-switch.settings.nft_chain_post 2>/dev/null || echo "nss_bypass_post") + echo "Preparing a proper clean-up before package removal..." + if command -v nft >/dev/null 2>&1; then + for chain in mangle_prerouting mangle_postrouting mangle_output; do + handles=$$(nft -a list chain inet fw4 "$$chain" 2>/dev/null | grep -E "jump $$NFT_CHAIN_PRE|jump $$NFT_CHAIN_POST" | grep -oE 'handle [0-9]+' | awk '{print $$2}') + for h in $$handles; do + nft delete rule inet fw4 "$$chain" handle "$$h" 2>/dev/null + done + done + nft delete chain inet fw4 $$NFT_CHAIN_PRE 2>/dev/null + nft delete chain inet fw4 $$NFT_CHAIN_POST 2>/dev/null + fi + if [ -f /proc/net/nf_conntrack ] && command -v conntrack >/dev/null 2>&1; then + conntrack -L 2>/dev/null | grep "mark=$$NSS_MARK" | while read -r line; do + proto=$$(echo "$$line" | awk '{print $$1}') + src=$$(echo "$$line" | grep -oE 'src=[^ ]+' | head -1 | cut -d= -f2) + dst=$$(echo "$$line" | grep -oE 'dst=[^ ]+' | head -1 | cut -d= -f2) + [ -n "$$proto" ] && [ -n "$$src" ] && [ -n "$$dst" ] && \ + conntrack -D -p "$$proto" -s "$$src" -d "$$dst" 2>/dev/null + done + fi + if [ -f /sys/kernel/debug/ecm/ecm_db/defunct_all ]; then + echo 1 > /sys/kernel/debug/ecm/ecm_db/defunct_all 2>/dev/null + fi + if uci -q get firewall.nss_bypass_include.type >/dev/null 2>&1; then + uci delete firewall.nss_bypass_include + uci commit firewall + fi +fi +exit 0 +endef +define Package/nss-switch/postrm +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + rm -rf /usr/lib/nss-switch 2>/dev/null || true + rm -f /etc/config/nss-switch 2>/dev/null || true + rm -rf /etc/nss-switch 2>/dev/null || true + rm -f /etc/firewall.d/nss-bypass-rules 2>/dev/null || true + if [ -x /etc/init.d/firewall ]; then + /etc/init.d/firewall reload 2>/dev/null + fi +fi +exit 0 +endef +$(eval $(call BuildPackage,nss-switch)) diff --git a/net/nss-switch/README b/net/nss-switch/README new file mode 100644 index 0000000000000..21bc5fb3cb30a --- /dev/null +++ b/net/nss-switch/README @@ -0,0 +1,138 @@ +# NSS-Switch - QualcommAX NSS Bypass Tool + +A selective CPU bypass manager for Qualcomm NSS (Network Subsystem) on OpenWrt. +This tool allows you to mark specific connections so they are processed by the **CPU** instead of the **NSS hardware accelerator**. +Also compatible with MediaTek PPE/HNAT, other SFE solutions and Software Flow Offload. + + +Useful (and **needed**) for: + +- Traffic that needs deep inspection (e.g., `tcpdump`, `bandwidthd`, `snort`) +- Debugging or troubleshooting NSS offload issues +- Per-flow bypass rules without disabling NSS globally + +**No service/network restart required to bypass (hot-swapping) any NSS connection.** + +> 📌 Check [`CHANGELOG.md`](./CHANGELOG.md) for detailed history + +--- + +## Usage + +```bash +# Bypass all traffic from a specific device +nss-switch add --src-ip 192.168.1.50 --comment "PC off NSS" + +# Bypass SSH traffic (temporary) +nss-switch add --proto tcp --dst-port 22 --temp + +# Interactive: pick a connection and create a rule +nss-switch pick + +# Live monitor with 5-second refresh +nss-switch watch 5 +``` + + + +### 🚀 Commands + +| Command | Description | +|---|---| +| `nss-switch watch [--once] [interval]` | Live TUI monitor – refreshes every interval seconds. Use PgUp/PgDown / mouse to scroll. | +| `nss-switch pick` | Browse all active connections interactively and create a bypass rule from the selected one. | +| `nss-switch add [options]` | Manually add a bypass rule. | +| `nss-switch list` | List all defined bypass rules. | +| `nss-switch remove ` | Remove a bypass rule by ID. | +| `nss-switch flush [--rules\|--all\|--temp]` | Remove rules from nftables. | +| `nss-switch apply` | Re-apply `rules.conf` to nftables. | +| `nss-switch status` | Show full status dashboard (ECM state, rules, conntrack). | +| `nss-switch config [KEY] [VALUE]` | View or set configuration (`PERSIST_DEFAULT`, `DEBUG_MODE`, `WATCH_INTERVAL`). | +| `nss-switch debug ` | Debugging tools – see `nss-switch debug --help`. | + +--- + +# ➕ `add` Options + +| Option | Description | +|---|---| +| `--proto tcp\|udp\|icmp\|any` | Match protocol | +| `--src-ip ` | Match source IP or subnet | +| `--dst-ip ` | Match destination IP or subnet | +| `--src-port ` | Match source port (TCP/UDP) | +| `--dst-port ` | Match destination port (TCP/UDP) | +| `--iface ` | Match input interface (`out:` for egress) | +| `--persist` | Rule survives reboot | +| `--temp` | Temporary rule (lost on reboot) | +| `--comment ` | Human-readable label | +| `--no-defunct` | Skip ECM defunct after adding | + + +--- + +# 📦 Installation on OpenWrt + +## Pre-requisites +- [X] - OpenWRT +25.12 with NSS (QCA SSDK) enabled +- [X] - Full `conntrack` / `iptables-nft` support. + +--- + +## How it works + +``` +Mark 0x00010000 to desired packet -> Makes an ECM defunct -> So, CPU handles this traffic -> NSS bypass achieved +``` + +> NSS-Switch injects `nftables` rules into the kernel's `mangle` `pre` & `post` tables to set a specific packet mark (`0x00010000`, **changeable via UCI/conf file) on matching connections. This mark is designed to not interfere with other marking schemes. + +1. **Marking**: When a packet matches a user-defined rule (by IP, port, interface, etc.), nftables applies the `0x00010000` mark to the packet. + +2. **Saving to conntrack**: A postrouting rule saves this mark to the connection tracking entry (`ct mark`), ensuring both directions of the flow carry the same mark. + + +> When ECM (Enhanced Connection Manager) detects this mark in a packet, it **defuncts** the connection (to shut it down for an established NSS path), forcing an immediate re‑evaluation. The mark persists as long as the rule persist, so ECM hands the flow to the **CPU** instead of the NSS hardware accelerator as needed. + + + +3. **ECM defunct**: The Enhanced Connection Manager (ECM), which normally accelerates connections via NSS hardware, detects this mark and immediately **defuncts** the connection, forcing a full re‑evaluation. + +> Now, the connection is processed by the CPU, bypassing NSS acceleration entirely. + +> No service restart is required, and only the marked connections are affected , and NSS continues to accelerate all other traffic normally. + +4. **CPU takeover**: Because the mark persists, ECM hands the flow to the standard Linux network stack (CPU) instead of offloading it to NSS hardware. + +5. **Restore on reply**: A prerouting rule restores the mark from conntrack for reply packets, maintaining bypass status in both directions. + + + +--- + +# 🧪 Status +| | | +|---|---| +| Qualcomm NSS (ipq807x, ipq60xx, ipq50xx) | ✅ Tested on ipq807x, expected to work on others | +| Other offload engines (SFE, PPE, Flowtable) | ✅ Compatible, not tested, expected to work | +| `nftables` + `conntrack` | ✅ Fully working | +| Interactive UI (watch/pick) | ✅ Working | +| Persistent rules | ✅ Survive reboots | + +> 📌 See [`CHANGELOG.md`](./CHANGELOG.md) for details + +--- + + +## If you want to help + +- Test on other SoC's which includes any NSS solution (`ipq60xx`, `ipq50xx`) +- Report bugs with `nss-switch debug env` output +- Submit issues, fixes or improvements + +--- + +# License + +GPL-2.0 + +--- diff --git a/net/nss-switch/files/etc/config/nss-switch b/net/nss-switch/files/etc/config/nss-switch new file mode 100644 index 0000000000000..ead1505086d13 --- /dev/null +++ b/net/nss-switch/files/etc/config/nss-switch @@ -0,0 +1,13 @@ +config nss-switch 'settings' + option persist_default 'no' + option nss_mark '0x00010000' + option nss_mark_mask '0x00010000' + option ecm_debugfs '/sys/kernel/debug/ecm' + option nft_table 'inet fw4' + option nft_chain_pre 'nss_bypass_pre' + option nft_chain_post 'nss_bypass_post' + option rules_file '/etc/nss-switch/rules.conf' + option debug_log '/tmp/nss-switch.log' + option fw_script '/etc/firewall.d/nss-bypass-rules' + option watch_interval '3' + option debug_mode 'no' diff --git a/net/nss-switch/files/etc/firewall.d/nss-bypass-rules b/net/nss-switch/files/etc/firewall.d/nss-bypass-rules new file mode 100755 index 0000000000000..41ddde3559b2e --- /dev/null +++ b/net/nss-switch/files/etc/firewall.d/nss-bypass-rules @@ -0,0 +1,40 @@ +#!/usr/bin/env ash +# NSS-Switch firewall.d hook — auto-generated, do not edit manually +# This file is regenerated by nss-switch whenever rules change. +# To modify rules: use nss-switch add / remove / pick +# To regenerate: nss-switch apply + +load_uci_values() { + NSS_MARK=$(uci -q get nss-switch.settings.nss_mark 2>/dev/null || echo "0x00010000") + NFT_CHAIN_PRE=$(uci -q get nss-switch.settings.nft_chain_pre 2>/dev/null || echo "nss_bypass_pre") + NFT_CHAIN_POST=$(uci -q get nss-switch.settings.nft_chain_post 2>/dev/null || echo "nss_bypass_post") +} + +load_uci_values + +nft_add_chains() { + nft add chain inet fw4 ${NFT_CHAIN_PRE} 2>/dev/null || true + nft flush chain inet fw4 ${NFT_CHAIN_PRE} 2>/dev/null || true + nft add chain inet fw4 ${NFT_CHAIN_POST} 2>/dev/null || true + nft flush chain inet fw4 ${NFT_CHAIN_POST} 2>/dev/null || true + handles=$(nft -a list chain inet fw4 mangle_prerouting 2>/dev/null | grep "jump ${NFT_CHAIN_PRE}" | grep -oE 'handle [0-9]+' | awk '{print $2}') + for h in $handles; do nft delete rule inet fw4 mangle_prerouting handle "$h" 2>/dev/null; done + handles=$(nft -a list chain inet fw4 mangle_postrouting 2>/dev/null | grep "jump ${NFT_CHAIN_POST}" | grep -oE 'handle [0-9]+' | awk '{print $2}') + for h in $handles; do nft delete rule inet fw4 mangle_postrouting handle "$h" 2>/dev/null; done + handles=$(nft -a list chain inet fw4 mangle_output 2>/dev/null | grep "jump ${NFT_CHAIN_PRE}" | grep -oE 'handle [0-9]+' | awk '{print $2}') + for h in $handles; do nft delete rule inet fw4 mangle_output handle "$h" 2>/dev/null; done + nft add rule inet fw4 mangle_prerouting jump ${NFT_CHAIN_PRE} comment '"NSS-Switch prerouting"' + nft add rule inet fw4 mangle_postrouting jump ${NFT_CHAIN_POST} comment '"NSS-Switch postrouting"' + nft add rule inet fw4 mangle_output jump ${NFT_CHAIN_PRE} comment '"NSS-Switch output"' + nft add rule inet fw4 ${NFT_CHAIN_POST} ct mark set ct mark xor ${NSS_MARK} comment '"NSS-Switch: clear bypass mark from ct"' + nft add rule inet fw4 ${NFT_CHAIN_POST} ct mark set ct mark or meta mark and ${NSS_MARK} comment '"NSS-Switch: save bypass mark to conntrack"' + nft add rule inet fw4 ${NFT_CHAIN_PRE} meta mark set meta mark xor ${NSS_MARK} comment '"NSS-Switch: clear bypass mark from meta"' + nft add rule inet fw4 ${NFT_CHAIN_PRE} meta mark set meta mark or ct mark and ${NSS_MARK} comment '"NSS-Switch: restore bypass mark from conntrack"' +} + +nft_add_rules() { + true +} + +nft_add_chains +nft_add_rules diff --git a/net/nss-switch/files/etc/nss-switch/rules.conf b/net/nss-switch/files/etc/nss-switch/rules.conf new file mode 100644 index 0000000000000..2f29a3288c93c --- /dev/null +++ b/net/nss-switch/files/etc/nss-switch/rules.conf @@ -0,0 +1,9 @@ +# NSS-Switch rules: id|proto|src_ip|dst_ip|src_port|dst_port|iface|persist|comment +# Example: +# 1|tcp|192.168.1.3|any|80|any|any|no|This rule bypasses any FROM tcp 192.168.1.3:80 and it does NOT survive +# 2|any|any|any|any|any|wan|yes|This other bypasses ALL from/to WAN and persists on reboot +# NSS-Switch rules.conf — auto-generated, do not edit manually +# This file is regenerated by nss-switch whenever rules are added or removed. +# To modify rules: use nss-switch add / remove / pick +# To regenerate: nss-switch apply + diff --git a/net/nss-switch/files/usr/lib/nss-switch/bin/.placeholder b/net/nss-switch/files/usr/lib/nss-switch/bin/.placeholder new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/net/nss-switch/files/usr/lib/nss-switch/lib/chandler.sh b/net/nss-switch/files/usr/lib/nss-switch/lib/chandler.sh new file mode 100644 index 0000000000000..948efab87fe33 --- /dev/null +++ b/net/nss-switch/files/usr/lib/nss-switch/lib/chandler.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env ash +# lib/chandler.sh: C compiled bin handler +BIN_DIR="/usr/lib/nss-switch/bin" +LIB_DIR="/usr/lib/nss-switch/lib" +HAS_CT_DUMP="no" +if [ -f "$BIN_DIR/nss-ct-dump" ] && [ -x "$BIN_DIR/nss-ct-dump" ]; then + HAS_CT_DUMP="yes" + CT_DUMP_BIN="$BIN_DIR/nss-ct-dump" +fi +export HAS_CT_DUMP +export BIN_DIR +export LIB_DIR +export CT_DUMP_BIN diff --git a/net/nss-switch/files/usr/lib/nss-switch/lib/conntrack.sh b/net/nss-switch/files/usr/lib/nss-switch/lib/conntrack.sh new file mode 100755 index 0000000000000..cd60cb209a0a8 --- /dev/null +++ b/net/nss-switch/files/usr/lib/nss-switch/lib/conntrack.sh @@ -0,0 +1,400 @@ +#!/usr/bin/env ash +# lib/conntrack.sh — Parse /proc/net/nf_conntrack, correlate with NSS state +# NSS-Switch — ASH compatible, BusyBox v1.37+ +# ct_dump_all_full migrado a C, cambio en rendimiento brutal + +. "$SELF_DIR/lib/chandler.sh" +CONNTRACK_FILE=/proc/net/nf_conntrack +# ─── Check conntrack available ──────────────────────────────────────────────── +ct_check() { + [ -f "$CONNTRACK_FILE" ] || { ui_error "conntrack not available"; return 1; } +} +# ─── IP to decimal ──────────────────────────────────────────────────────────── +_ip_to_dec() { + local ip="$1" + local a b c d + a=$(echo "$ip" | cut -d'.' -f1) + b=$(echo "$ip" | cut -d'.' -f2) + c=$(echo "$ip" | cut -d'.' -f3) + d=$(echo "$ip" | cut -d'.' -f4) + echo $(( (a<<24) + (b<<16) + (c<<8) + d )) +} +# ─── Check if IP is in CIDR ─────────────────────────────────────────────────── +_ct_ip_in_cidr() { + local ip="$1" cidr="$2" + local net prefix + net=$(echo "$cidr" | cut -d'/' -f1) + prefix=$(echo "$cidr" | cut -d'/' -f2) + local ip_dec net_dec mask_dec + ip_dec=$(_ip_to_dec "$ip") + net_dec=$(_ip_to_dec "$net") + if [ "$prefix" -eq 0 ]; then + mask_dec=0 + else + mask_dec=$(( ( (1<<31) | ( (1<<31)-1 ) ) ^ ( (1<<(32-prefix))-1 ) )) + fi + [ $(( ip_dec & mask_dec )) -eq $(( net_dec & mask_dec )) ] +} + +# ─── Build interface map from ip addr show ──────────────────────────────────── +# Writes to a tmpfile: "ip cidr iface" per line +_ct_build_iface_map() { + local tmpfile="$1" + ip addr show 2>/dev/null | awk ' + /^[0-9]+: / { iface=$2; gsub(/:$/,"",iface) } + /inet / { + if ($0 ~ /peer/) { + print $2, $2"/32", iface + } else { + split($2, a, "/") + print a[1], $2, iface + } + } + ' > "$tmpfile" +} +# ─── Normalizar nombres de interfaz para mostrar ────────────────────────────── +_normalize_iface_display() { + local iface="$1" + case "$iface" in + local:pppoe-wan|pppoe-wan) + echo "wan" + ;; + local:br-lan|br-lan) + echo "lan" + ;; + local:lo|lo) + echo "lo" + ;; + local:lan2|lan2) + echo "lan2" + ;; + local:lan3|lan3) + echo "lan3" + ;; + wan.20|wan) + echo "wan" + ;; + wan_6) + echo "wan6" + ;; + ?) + echo "?" + ;; + *) + echo "$iface" + ;; + esac +} +# ─── Normalizar nombres de interfaz para reglas (nftables) ──────────────────── +_normalize_iface_rule() { + local iface="$1" + case "$iface" in + *:*) iface="${iface#*:}" ;; + esac + [ -z "$iface" ] && echo "any" && return + + echo "$iface" +} + +# ─── Compress IPv6 address RFC 5952 ────────────────────────────────────────── +_ipv6_compress() { + echo "$1" | awk '{ + split($0, a, ":") + for(i=1;i<=8;i++) { + gsub(/^0+/,"",a[i]) + if(a[i]=="") a[i]="0" + } + max_len=0; max_start=0; cur_len=0; cur_start=0 + for(i=1;i<=8;i++) { + if(a[i]=="0") { + if(cur_len==0) cur_start=i + cur_len++ + if(cur_len>max_len) { max_len=cur_len; max_start=cur_start } + } else { cur_len=0 } + } + result="" + i=1 + while(i<=8) { + if(max_len>1 && i==max_start) { + if(i==1) result="::" + else result=result"::" + i+=max_len + } else { + if(result!="" && substr(result,length(result),1)!=":") result=result":" + result=result a[i] + i++ + } + } + print result + }' +} + +# ─── Get interface for a src IP ─────────────────────────────────────────────── +# Returns: iface name, "local:iface" for router-own IPs, or "?" +ct_iface_for_src() { + local src="$1" + local found="" + if echo "$src" | grep -q ":"; then + local dev + dev=$(ip -6 route get "$src" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1); exit}') + if [ -z "$dev" ] || [ "$dev" = "lo" ]; then + local own_iface + own_iface=$(ip -6 addr show 2>/dev/null | awk -v src="$src" ' + /^[0-9]+: / { iface=$2; gsub(/:$/,"",iface) } + /inet6 / { + split($2, a, "/") + if(a[1]==src) print iface + } + ') + [ -n "$own_iface" ] && echo "local:$own_iface" || echo "local:pppoe-wan" + else + echo "$dev" + fi + return + fi + local tmp + tmp=$(mktemp /tmp/nss-iface.XXXXXX) + _ct_build_iface_map "$tmp" + while IFS=' ' read -r ip cidr iface; do + if [ "$src" = "$ip" ]; then + rm -f "$tmp" + echo "local:$iface" + return + fi + if _ct_ip_in_cidr "$src" "$cidr" 2>/dev/null; then + found="$iface" + fi + done < "$tmp" + rm -f "$tmp" + [ -n "$found" ] && echo "$found" && return + echo "?" +} + +# ─── Parse one conntrack line into variables ────────────────────────────────── +# Sets: CT_PROTO CT_SRC CT_SPORT CT_DST CT_DPORT CT_MARK CT_STATE CT_STATUS +ct_parse_line() { + local line="$1" + CT_PROTO="" + CT_SRC="" CT_SPORT="" CT_DST="" CT_DPORT="" + CT_MARK=0 CT_STATE="" CT_STATUS="" + CT_PROTO=$(echo "$line" | awk '{print $3}') + case "$CT_PROTO" in + tcp|6) CT_STATE=$(echo "$line" | awk '{print $4}') ;; + udp|17) CT_STATE="stateless" ;; + *) CT_STATE="?" ;; + esac + CT_SRC=$(echo "$line" | grep -oE 'src=[^ ]+' | head -1 | cut -d= -f2) + CT_DST=$(echo "$line" | grep -oE 'dst=[^ ]+' | head -1 | cut -d= -f2) + CT_SPORT=$(echo "$line" | grep -oE 'sport=[^ ]+' | head -1 | cut -d= -f2) + CT_DPORT=$(echo "$line" | grep -oE 'dport=[^ ]+' | head -1 | cut -d= -f2) + CT_MARK=$(echo "$line" | grep -oE 'mark=[^ ]+' | head -1 | cut -d= -f2) + CT_STATUS=$(echo "$line"| grep -oE 'status=[^ ]+' | head -1 | cut -d= -f2) + CT_SPORT="${CT_SPORT:-?}" + CT_DPORT="${CT_DPORT:-?}" + CT_MARK="${CT_MARK:-0}" +} + +# ─── Check if mark has our NSS bypass bit set ───────────────────────────────── +ct_is_bypassed() { + local mark="$1" + local mark_dec nss_dec + mark_dec=$(printf '%d' "$mark" 2>/dev/null) || mark_dec=0 + nss_dec=$(printf '%d' "$NSS_MARK" 2>/dev/null) || nss_dec=65536 + [ $(( mark_dec & nss_dec )) -ne 0 ] +} + +# ─── Determine NSS status for a connection ──────────────────────────────────── +ct_nss_status() { + local mark="$1" + if ct_is_bypassed "$mark"; then + echo "CPU" + return + fi + if [ -d "$ECM_DEBUGFS/ecm_nss_ipv4" ]; then + echo "HW" + elif [ -d "$ECM_DEBUGFS/ecm_sfe_ipv4" ]; then + echo "SFE" + else + echo "CPU" + fi +} + + +# ─── Dump ALL connections including router-local ────────────────────────────── +ct_dump_all_full() { + + if [ "$HAS_CT_DUMP" = "yes" ]; then + "$BIN_DIR/nss-ct-dump" + else + _ct_dump_all_full_shell + fi +} +_ct_dump_all_full_shell() { + + ct_check || return 1 + + local num=0 + while IFS= read -r line; do + ct_parse_line "$line" + [ -z "$CT_SRC" ] && continue + local iface + iface=$(ct_iface_for_src "$CT_SRC") + [ -z "$iface" ] && iface="?" + iface=$(_normalize_iface_display "$iface") + num=$((num+1)) + local nss_status bypassed + nss_status=$(ct_nss_status "$CT_MARK") + bypassed="NO" + ct_is_bypassed "$CT_MARK" && bypassed="YES" + local display_src display_dst + if echo "$CT_SRC" | grep -q ":"; then + display_src=$(_ipv6_compress "$CT_SRC") + else + display_src="$CT_SRC" + fi + if echo "$CT_DST" | grep -q ":"; then + display_dst=$(_ipv6_compress "$CT_DST") + else + display_dst="$CT_DST" + fi + printf "%d|%s|%s#%s|%s#%s|%s|%s|%s|%s|%s\n" \ + "$num" "$CT_PROTO" \ + "$display_src" "$CT_SPORT" \ + "$display_dst" "$CT_DPORT" \ + "$iface" "$nss_status" "$bypassed" \ + "$CT_MARK" "$CT_STATE" + done < "$CONNTRACK_FILE" + +} + +# ─── Get single connection by NUM ───────────────────────────────────────────── +ct_get_by_num() { + local target="$1" + ct_dump_all_full | awk -F'|' -v n="$target" '$1==n {print; exit}' +} + +# ─── Count total connections ────────────────────────────────────────────────── +ct_count() { + wc -l < "$CONNTRACK_FILE" 2>/dev/null || echo 0 +} + +# ─── Count bypassed connections ─────────────────────────────────────────────── +ct_count_bypassed() { + local nss_dec + nss_dec=$(printf '%d' "$NSS_MARK" 2>/dev/null) || nss_dec=65536 + local count=0 + while IFS= read -r line; do + local mark mark_dec + mark=$(echo "$line" | grep -oE 'mark=[^ ]+' | head -1 | cut -d= -f2) + mark_dec=$(printf '%d' "${mark:-0}" 2>/dev/null) || mark_dec=0 + [ $(( mark_dec & nss_dec )) -ne 0 ] && count=$((count+1)) + done < "$CONNTRACK_FILE" + echo "$count" +} + +# DEBUG PR-1 +# ─── Clear conntrack entries matching rule criteria ─────────────────────────── +_ct_clear_all_marks() { + dbg "Clearing all NSS-Switch marks from conntrack" + local count=0 + + # Usar conntrack -m si está disponible (más eficiente) + if conntrack -D -m "$NSS_MARK" 2>/dev/null | grep -q "deleted"; then + count=$(conntrack -D -m "$NSS_MARK" 2>/dev/null | wc -l) + else + # Fallback: buscar y eliminar manualmente + conntrack -L 2>/dev/null | grep "mark=$NSS_MARK" | while read -r line; do + proto=$(echo "$line" | awk '{print $1}') + src=$(echo "$line" | grep -oE 'src=[^ ]+' | head -1 | cut -d= -f2) + dst=$(echo "$line" | grep -oE 'dst=[^ ]+' | head -1 | cut -d= -f2) + sport=$(echo "$line" | grep -oE 'sport=[^ ]+' | head -1 | cut -d= -f2) + dport=$(echo "$line" | grep -oE 'dport=[^ ]+' | head -1 | cut -d= -f2) + [ -n "$proto" ] && [ -n "$src" ] && [ -n "$dst" ] && \ + conntrack -D -p "$proto" -s "$src" -d "$dst" --sport "$sport" --dport "$dport" 2>/dev/null && count=$((count+1)) + done + fi + + ui_ok "Cleared $count connections with NSS-Switch mark" + ecm_defunct_all +} +# ─── Clear conntrack entries matching rule criteria ─────────────────────────── +ct_clear_rule_marks() { + local proto="$1" src_ip="$2" dst_ip="$3" + local sport="$4" dport="$5" iface="$6" + dbg "Flushing connections for rule: proto=$proto src=$src_ip dst=$dst_ip sport=$sport dport=$dport iface=$iface" + + local filter="" + [ "$proto" != "any" ] && filter="$filter -p $proto" + + if [ "$src_ip" != "any" ] && [ "$dst_ip" != "any" ]; then + filter="$filter -s $src_ip -d $dst_ip" + elif [ "$src_ip" != "any" ]; then + filter="$filter -s $src_ip" + elif [ "$dst_ip" != "any" ]; then + filter="$filter -d $dst_ip" + fi + + if [ "$sport" != "any" ] && [ "$dport" != "any" ]; then + filter="$filter --sport $sport --dport $dport" + elif [ "$sport" != "any" ]; then + filter="$filter --sport $sport" + elif [ "$dport" != "any" ]; then + filter="$filter --dport $dport" + fi + + # DEBUG PR-1 + if [ -n "$filter" ]; then + dbg "conntrack -D $filter" + conntrack -D $filter 2>/dev/null + ui_ok "Flushed matching conntrack entries" + else + # IFACE ONLY + if [ "$iface" != "any" ]; then + local real_iface="${iface#out:}" + # CONNTRACK -i + conntrack -D -i "$real_iface" 2>/dev/null && \ + ui_ok "Flushed connections on interface $real_iface" + fi + + # ANY rules + if [ "$proto" = "any" ] && [ "$src_ip" = "any" ] && [ "$dst_ip" = "any" ] && \ + [ "$sport" = "any" ] && [ "$dport" = "any" ] && [ "$iface" = "any" ]; then + ui_warn "Rule matches ALL connections - flushing entire conntrack" + conntrack -F 2>/dev/null + ui_ok "All conntrack entries flushed" + _ct_clear_all_marks + return 0 + fi + fi + + # DEBUG PR-1 + _ct_clear_all_marks +} + + + + + +# ─── Debug: show conntrack entries with our mark ───────────────────────────── +ct_debug_mark() { + ui_section "Conntrack entries with NSS-Switch mark ($NSS_MARK)" + local found=0 + while IFS= read -r line; do + local mark mark_dec nss_dec + mark=$(echo "$line" | grep -oE 'mark=[^ ]+' | head -1 | cut -d= -f2) + mark_dec=$(printf '%d' "${mark:-0}" 2>/dev/null) || mark_dec=0 + nss_dec=$(printf '%d' "$NSS_MARK" 2>/dev/null) || nss_dec=65536 + if [ $(( mark_dec & nss_dec )) -ne 0 ]; then + echo " $line" + found=$((found+1)) + fi + done < "$CONNTRACK_FILE" + [ "$found" -eq 0 ] && ui_warn "No entries with our mark found" + ui_kv "Total bypassed" "$found" +} + +# ─── Debug: dump raw conntrack ──────────────────────────────────────────────── +ct_debug_raw() { + ui_section "Raw /proc/net/nf_conntrack" + cat "$CONNTRACK_FILE" 2>/dev/null || ui_error "Cannot read conntrack" +} diff --git a/net/nss-switch/files/usr/lib/nss-switch/lib/debug.sh b/net/nss-switch/files/usr/lib/nss-switch/lib/debug.sh new file mode 100644 index 0000000000000..69d31ae0e02c3 --- /dev/null +++ b/net/nss-switch/files/usr/lib/nss-switch/lib/debug.sh @@ -0,0 +1,385 @@ +#!/usr/bin/env ash +# lib/debug.sh - Real-time monitoring panel for NSS-Switch +# Usage: nss-switch debug monitor [interface] +DEBUG_SESSION_START=$(date +%Y%m%d_%H%M%S) +DEBUG_LOG_FILE="/tmp/nss-debug-${DEBUG_SESSION_START}.log" +MON_GREEN='\033[0;32m' +MON_RED='\033[0;31m' +MON_YELLOW='\033[0;33m' +MON_CYAN='\033[0;36m' +MON_BOLD='\033[1m' +MON_DIM='\033[2m' +MON_RESET='\033[0m' +PREV_FILE="/tmp/nss-debug-prev-$$" +RULES_SNAPSHOT="/tmp/nss-debug-rules-$$" +PID_FILE="/tmp/nss-debug-monitor.pid" +total_conn_prev=0 +bypassed_prev=0 +_debug_cleanup() { + rm -f "$PREV_FILE" "$RULES_SNAPSHOT" "$PID_FILE" 2>/dev/null + echo "[$(date '+%H:%M:%S')] Monitor stopped" >> "$DEBUG_LOG_FILE" + exit 0 +} +_format_bytes() { + local bytes=$1 + [ -z "$bytes" ] && bytes=0 + + if [ "$bytes" -ge 1073741824 ]; then + echo "$((bytes / 1073741824))G" + elif [ "$bytes" -ge 1048576 ]; then + echo "$((bytes / 1048576))M" + elif [ "$bytes" -ge 1024 ]; then + echo "$((bytes / 1024))K" + else + echo "${bytes}B" + fi +} +_debug_init() { + rm -f "$PREV_FILE" "$RULES_SNAPSHOT" 2>/dev/null + for iface in lan2 lan3 br-lan pppoe-wan; do + local stats=$(ip -s link show "$iface" 2>/dev/null) + if [ -n "$stats" ]; then + local rx_bytes=$(echo "$stats" | awk '/RX:/{getline; print $1}') + local tx_bytes=$(echo "$stats" | awk '/TX:/{getline; print $1}') + local rx_packets=$(echo "$stats" | awk '/RX:/{getline; print $2}') + local tx_packets=$(echo "$stats" | awk '/TX:/{getline; print $2}') + echo "${iface}_rx_pkts=$rx_packets" >> "$PREV_FILE" + echo "${iface}_tx_pkts=$tx_packets" >> "$PREV_FILE" + echo "${iface}_rx_bytes=$rx_bytes" >> "$PREV_FILE" + echo "${iface}_tx_bytes=$tx_bytes" >> "$PREV_FILE" + fi + done + nft list chain inet fw4 nss_bypass_pre 2>/dev/null | grep "comment \"NSS-Switch" > "$RULES_SNAPSHOT" + echo "[$(date '+%H:%M:%S')] Monitor started" >> "$DEBUG_LOG_FILE" +} +_get_prev() { + grep "^${1}=" "$PREV_FILE" 2>/dev/null | cut -d'=' -f2 +} +_update_prev() { + if grep -q "^${1}=" "$PREV_FILE" 2>/dev/null; then + sed -i "s/^${1}=.*/${1}=${2}/" "$PREV_FILE" + else + echo "${1}=${2}" >> "$PREV_FILE" + fi +} +_check_rule_changes() { + local new_rules="/tmp/nss-debug-newrules-$$" + nft list chain inet fw4 nss_bypass_pre 2>/dev/null | grep "comment \"NSS-Switch" > "$new_rules" + + if ! cmp -s "$RULES_SNAPSHOT" "$new_rules" 2>/dev/null; then + local added=$(comm -13 "$RULES_SNAPSHOT" "$new_rules" 2>/dev/null | wc -l) + local removed=$(comm -23 "$RULES_SNAPSHOT" "$new_rules" 2>/dev/null | wc -l) + echo "[$(date '+%H:%M:%S')] RULES CHANGED: +$added -$removed" >> "$DEBUG_LOG_FILE" + comm -13 "$RULES_SNAPSHOT" "$new_rules" 2>/dev/null | while read line; do + local comment=$(echo "$line" | sed -n 's/.*comment "NSS-Switch id=\([0-9]\+\): \(.*\)".*/\1: \2/p') + [ -n "$comment" ] && echo "[$(date '+%H:%M:%S')] ADDED: $comment" >> "$DEBUG_LOG_FILE" + done + comm -23 "$RULES_SNAPSHOT" "$new_rules" 2>/dev/null | while read line; do + local comment=$(echo "$line" | sed -n 's/.*comment "NSS-Switch id=\([0-9]\+\): \(.*\)".*/\1: \2/p') + [ -n "$comment" ] && echo "[$(date '+%H:%M:%S')] REMOVED: $comment" >> "$DEBUG_LOG_FILE" + done + cp "$new_rules" "$RULES_SNAPSHOT" + fi + rm -f "$new_rules" +} +cmd_debug_monitor() { + local focus_iface="${1:-lan3}" + command -v nss_stats >/dev/null 2>&1 || { echo "nss_stats not found"; return 1; } + trap '_debug_cleanup; exit 0' INT TERM EXIT + echo $$ > "$PID_FILE" + echo -e "${MON_BOLD}Starting NSS-Switch Real-Time Monitor${MON_RESET}" + echo "Log file: $DEBUG_LOG_FILE" + echo "Focus interface: $focus_iface" + echo -e "${MON_DIM}Press Ctrl+C to exit${MON_RESET}" + _debug_init + total_conn_prev=$(wc -l < /proc/net/nf_conntrack 2>/dev/null) + bypassed_prev=$(ct_count_bypassed 2>/dev/null) + while true; do + clear + + # Banner + echo -e "${MON_BOLD}${MON_CYAN}" + echo "╔═══════════════════════════════════════════════════════════════════════════════════╗" + echo "║ NSS-Switch Real-Time Monitor ║" + echo "╚═══════════════════════════════════════════════════════════════════════════════════╝" + echo -e "${MON_RESET}" + echo " Session: $(basename "$DEBUG_LOG_FILE")" + echo " Time: $(date '+%Y-%m-%d %H:%M:%S')" + echo -e " Focus: ${focus_iface}" + echo "" + + # === SECCIÓN 1: Resumen rápido con cambios === + echo -e "${MON_BOLD}${MON_CYAN}═══ 1. Quick Summary ═══${MON_RESET}" + + local total_conn=$(wc -l < /proc/net/nf_conntrack 2>/dev/null) + local bypassed=$(ct_count_bypassed 2>/dev/null) + local frontend=$(ecm_frontend 2>/dev/null) + + # Detectar cambios + local conn_diff=$((total_conn - total_conn_prev)) + local bypass_diff=$((bypassed - bypassed_prev)) + + printf " %-25s %s" "Total connections:" "$total_conn" + if [ "$conn_diff" -ne 0 ]; then + echo -e " (${MON_GREEN}+$conn_diff${MON_RESET})" + echo "[$(date '+%H:%M:%S')] CONN COUNT: $total_conn_prev → $total_conn ($([ $conn_diff -gt 0 ] && echo "+$conn_diff" || echo "$conn_diff"))" >> "$DEBUG_LOG_FILE" + else + echo "" + fi + + printf " %-25s %s" "Bypassed (CPU):" "$bypassed" + if [ "$bypass_diff" -ne 0 ]; then + echo -e " (${MON_GREEN}+$bypass_diff${MON_RESET})" + echo "[$(date '+%H:%M:%S')] BYPASSED: $bypassed_prev → $bypassed ($([ $bypass_diff -gt 0 ] && echo "+$bypass_diff" || echo "$bypass_diff"))" >> "$DEBUG_LOG_FILE" + else + echo "" + fi + + printf " %-25s %s\n" "ECM Frontend:" "$frontend" + echo "" + + # Actualizar previos + total_conn_prev=$total_conn + bypassed_prev=$bypassed + + # === SECCIÓN 2: Tráfico por interfaz (con KB/MB) === + echo -e "${MON_BOLD}${MON_CYAN}═══ 2. Interface Traffic (delta) ═══${MON_RESET}" + echo -e " ${MON_DIM}IFACE RX_PKTS RX_DATA TX_PKTS TX_DATA${MON_RESET}" + + for iface in lan2 lan3 br-lan pppoe-wan; do + local stats=$(ip -s link show "$iface" 2>/dev/null) + if [ -n "$stats" ]; then + local rx_bytes=$(echo "$stats" | awk '/RX:/{getline; print $1}') + local tx_bytes=$(echo "$stats" | awk '/TX:/{getline; print $1}') + local rx_packets=$(echo "$stats" | awk '/RX:/{getline; print $2}') + local tx_packets=$(echo "$stats" | awk '/TX:/{getline; print $2}') + + local prev_rx_pkt=$(_get_prev "${iface}_rx_pkts") + local prev_tx_pkt=$(_get_prev "${iface}_tx_pkts") + local prev_rx_byte=$(_get_prev "${iface}_rx_bytes") + local prev_tx_byte=$(_get_prev "${iface}_tx_bytes") + + [ -z "$prev_rx_pkt" ] && prev_rx_pkt=0 + [ -z "$prev_tx_pkt" ] && prev_tx_pkt=0 + [ -z "$prev_rx_byte" ] && prev_rx_byte=0 + [ -z "$prev_tx_byte" ] && prev_tx_byte=0 + + local delta_rx_pkt=$((rx_packets - prev_rx_pkt)) + local delta_tx_pkt=$((tx_packets - prev_tx_pkt)) + local delta_rx_data=$(_format_bytes $((rx_bytes - prev_rx_byte))) + local delta_tx_data=$(_format_bytes $((tx_bytes - prev_tx_byte))) + + if [ "$iface" = "$focus_iface" ]; then + printf " ${MON_GREEN}%-10s${MON_RESET} %-8s %-8s %-8s %-8s\n" \ + "$iface" "$delta_rx_pkt" "$delta_rx_data" "$delta_tx_pkt" "$delta_tx_data" + else + printf " %-10s %-8s %-8s %-8s %-8s\n" \ + "$iface" "$delta_rx_pkt" "$delta_rx_data" "$delta_tx_pkt" "$delta_tx_data" + fi + + _update_prev "${iface}_rx_pkts" "$rx_packets" + _update_prev "${iface}_tx_pkts" "$tx_packets" + _update_prev "${iface}_rx_bytes" "$rx_bytes" + _update_prev "${iface}_tx_bytes" "$tx_bytes" + fi + done + echo "" + + # === SECCIÓN 3: Conexiones activas por interfaz === + echo -e "${MON_BOLD}${MON_CYAN}═══ 3. Active Connections (by interface) ═══${MON_RESET}" + printf " %-10s %8s %8s\n" "IFACE" "TOTAL" "BYPASSED" + + for iface in lan2 lan3 br-lan pppoe-wan; do + local tmp_conn="/tmp/nss-debug-conn-$$" + ct_dump_all_full 2>/dev/null | grep "|${iface}|" > "$tmp_conn" + local total=$(wc -l < "$tmp_conn") + local bypass_count=$(grep -c "|YES|" "$tmp_conn") + rm -f "$tmp_conn" + + if [ "$iface" = "$focus_iface" ]; then + printf " ${MON_GREEN}%-10s${MON_RESET} %8s %8s\n" "$iface" "$total" "$bypass_count" + else + printf " %-10s %8s %8s\n" "$iface" "$total" "$bypass_count" + fi + done + echo "" + + # === SECCIÓN 4: Top conexiones (focus interface) === + echo -e "${MON_BOLD}${MON_CYAN}═══ 4. Top Connections (${focus_iface}) ═══${MON_RESET}" + echo -e " ${MON_DIM}PROTO SRC:PORT -> DST:PORT BYPASS${MON_RESET}" + + ct_dump_all_full 2>/dev/null | grep "|${focus_iface}|" | head -8 | while IFS='|' read -r num proto src dst iface nss bypass mark state; do + local src_ip=$(echo "$src" | cut -d'#' -f1 | cut -c1-30) + local src_port=$(echo "$src" | cut -d'#' -f2) + local dst_ip=$(echo "$dst" | cut -d'#' -f1 | cut -c1-30) + local dst_port=$(echo "$dst" | cut -d'#' -f2) + + local bypass_color="" + [ "$bypass" = "YES" ] && bypass_color="$MON_YELLOW" + + if [ "$bypass" = "YES" ]; then + echo -e " ${MON_DIM}${proto}${MON_RESET} ${MON_YELLOW}${src_ip}:${src_port} -> ${dst_ip}:${dst_port} BYPASS${MON_RESET}" + else + echo -e " ${MON_DIM}${proto}${MON_RESET} ${src_ip}:${src_port} -> ${dst_ip}:${dst_port}" + fi + done + echo "" + + # === SECCIÓN 5: Reglas activas === + echo -e "${MON_BOLD}${MON_CYAN}═══ 5. Active Bypass Rules ═══${MON_RESET}" + + # Guardar reglas en archivo temporal para evitar subshell + local rules_tmp="/tmp/nss-debug-rules-display-$$" + nft list chain inet fw4 nss_bypass_pre 2>/dev/null | grep "comment \"NSS-Switch" > "$rules_tmp" + local rule_count=$(wc -l < "$rules_tmp") + + if [ "$rule_count" -gt 0 ]; then + while read line; do + local comment=$(echo "$line" | sed -n 's/.*comment "NSS-Switch id=\([0-9]\+\): \(.*\)".*/\1: \2/p') + if [ -n "$comment" ]; then + echo -e " • $comment" + fi + done < "$rules_tmp" + else + echo -e " ${MON_DIM}No active rules${MON_RESET}" + fi + rm -f "$rules_tmp" + echo "" + + + echo -e "${MON_DIM}Press Ctrl+C to exit | Refreshing every 2 seconds${MON_RESET}" + + # === LOGGING: Registrar TODO el snapshot === + { + echo "=== SNAPSHOT $(date '+%Y-%m-%d %H:%M:%S') ===" + echo "═══ 1. Quick Summary ═══" + echo " Total connections: $total_conn ($([ $conn_diff -ge 0 ] && echo "+$conn_diff" || echo "$conn_diff"))" + echo " Bypassed (CPU): $bypassed ($([ $bypass_diff -ge 0 ] && echo "+$bypass_diff" || echo "$bypass_diff"))" + echo " ECM Frontend: $frontend" + echo "" + echo "═══ 2. Interface Traffic (delta) ═══" + echo " IFACE RX_PKTS RX_DATA TX_PKTS TX_DATA" + + for iface in lan2 lan3 br-lan pppoe-wan; do + local stats=$(ip -s link show "$iface" 2>/dev/null) + if [ -n "$stats" ]; then + local rx_bytes=$(echo "$stats" | awk '/RX:/{getline; print $1}') + local tx_bytes=$(echo "$stats" | awk '/TX:/{getline; print $1}') + local rx_packets=$(echo "$stats" | awk '/RX:/{getline; print $2}') + local tx_packets=$(echo "$stats" | awk '/TX:/{getline; print $2}') + + local prev_rx_pkt=$(_get_prev "${iface}_rx_pkts") + local prev_tx_pkt=$(_get_prev "${iface}_tx_pkts") + local prev_rx_byte=$(_get_prev "${iface}_rx_bytes") + local prev_tx_byte=$(_get_prev "${iface}_tx_bytes") + + [ -z "$prev_rx_pkt" ] && prev_rx_pkt=0 + [ -z "$prev_tx_pkt" ] && prev_tx_pkt=0 + [ -z "$prev_rx_byte" ] && prev_rx_byte=0 + [ -z "$prev_tx_byte" ] && prev_tx_byte=0 + + local delta_rx_pkt=$((rx_packets - prev_rx_pkt)) + local delta_tx_pkt=$((tx_packets - prev_tx_pkt)) + local delta_rx_data=$(_format_bytes $((rx_bytes - prev_rx_byte))) + local delta_tx_data=$(_format_bytes $((tx_bytes - prev_tx_byte))) + + echo " $iface $delta_rx_pkt $delta_rx_data $delta_tx_pkt $delta_tx_data" + + _update_prev "${iface}_rx_pkts" "$rx_packets" + _update_prev "${iface}_tx_pkts" "$tx_packets" + _update_prev "${iface}_rx_bytes" "$rx_bytes" + _update_prev "${iface}_tx_bytes" "$tx_bytes" + fi + done + echo "" + echo "═══ 3. Active Connections (by interface) ═══" + echo " IFACE TOTAL BYPASSED" + + for iface in lan2 lan3 br-lan pppoe-wan; do + local tmp_conn="/tmp/nss-debug-conn-$$" + ct_dump_all_full 2>/dev/null | grep "|${iface}|" > "$tmp_conn" + local total=$(wc -l < "$tmp_conn") + local bypass_count=$(grep -c "|YES|" "$tmp_conn") + rm -f "$tmp_conn" + echo " $iface $total $bypass_count" + done + echo "" + echo "═══ 4. Top Connections (${focus_iface}) ═══" + + ct_dump_all_full 2>/dev/null | grep "|${focus_iface}|" | head -8 | while IFS='|' read -r num proto src dst iface nss bypass mark state; do + local src_ip=$(echo "$src" | cut -d'#' -f1 | cut -c1-30) + local src_port=$(echo "$src" | cut -d'#' -f2) + local dst_ip=$(echo "$dst" | cut -d'#' -f1 | cut -c1-30) + local dst_port=$(echo "$dst" | cut -d'#' -f2) + + if [ "$bypass" = "YES" ]; then + echo " $proto $src_ip:$src_port -> $dst_ip:$dst_port BYPASS" + else + echo " $proto $src_ip:$src_port -> $dst_ip:$dst_port" + fi + done + echo "" + echo "═══ 5. Active Bypass Rules ═══" + + nft list chain inet fw4 nss_bypass_pre 2>/dev/null | grep "comment \"NSS-Switch" | while read line; do + local comment=$(echo "$line" | sed -n 's/.*comment "NSS-Switch id=\([0-9]\+\): \(.*\)".*/\1: \2/p') + [ -n "$comment" ] && echo " • $comment" + done + echo "" + echo "==========================================" + echo "" + } >> "$DEBUG_LOG_FILE" + + # Detectar cambios en reglas (opcional, ya que el snapshot completo ya lo muestra todo) + _check_rule_changes + + sleep 2 + done + +} + +# DEBUG PR-1 +cmd_debug_log() { + local log_exists=0 + if [ -f "$DEBUG_LOG" ]; then + log_exists=1 + fi + ui_section "Debug Log Status" + ui_kv "Status" "$([ $log_exists -eq 1 ] && echo "enabled" || echo "disabled")" + ui_kv "Log file" "$DEBUG_LOG" + if [ $log_exists -eq 1 ]; then + echo "" + ui_info "Last 5 lines:" + tail -5 "$DEBUG_LOG" 2>/dev/null | while read line; do + printf " ${C_DIM}%s${C_RESET}\n" "$line" + done + echo "" + if ui_ask_yn "Show full debug log? " n; then + cat "$DEBUG_LOG" + echo "" + fi + if ui_ask_yn "Disable debug logging? (log will be deleted) " n; then + rm -f "$DEBUG_LOG" + + # DEBUG PR-1: Usar UCI para desactivar debug + uci set nss-switch.settings.debug_mode="no" + uci commit nss-switch + + ui_ok "Debug logging disabled, log file deleted" + fi + else + ui_info "Debug logging is currently disabled" + echo "" + if ui_ask_yn "Enable debug logging?" y; then + echo "=== NSS-Switch Debug Log ===" > "$DEBUG_LOG" + echo "=== Started: $(date) ===" >> "$DEBUG_LOG" + + # DEBUG PR-1: Usar UCI para activar debug + uci set nss-switch.settings.debug_mode="yes" + uci commit nss-switch + + ui_ok "Debug logging enabled at $DEBUG_LOG" + ui_info "Use 'nss-switch debug log' again to disable" + fi + fi +} diff --git a/net/nss-switch/files/usr/lib/nss-switch/lib/detect.sh b/net/nss-switch/files/usr/lib/nss-switch/lib/detect.sh new file mode 100755 index 0000000000000..23d314b49c72c --- /dev/null +++ b/net/nss-switch/files/usr/lib/nss-switch/lib/detect.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env ash +# lib/detect.sh — Environment detection for NSS-Switch +# ASH compatible, BusyBox v1.37+ + +# ─── Detect firewall backend ────────────────────────────────────────────────── +detect_fw_backend() { + if grep -q "fw4" /etc/init.d/firewall 2>/dev/null; then + echo "fw4" + elif grep -q "fw3" /etc/init.d/firewall 2>/dev/null; then + echo "fw3" + else + echo "unknown" + fi +} +# ─── List all network interfaces ───────────────────────────────────────────── +detect_interfaces() { + ip link show 2>/dev/null | grep -E '^[0-9]+:' | \ + awk '{print $2}' | sed 's/://' | grep -v '^lo$' +} +# ─── Detect if interface is WAN ─────────────────────────────────────────────── +detect_is_wan() { + local iface="$1" + # Check UCI network config + grep -l "option ifname.*$iface\|option device.*$iface" /etc/config/network 2>/dev/null | \ + xargs grep -l "wan\|pppoe\|dhcp" 2>/dev/null | head -1 | grep -q . && return 0 + # Fallback: check if it's the default route interface + ip route | grep "^default" | grep -q "$iface" +} +# ─── Detect if NAT/masquerade applies on an interface ──────────────────────── +detect_has_nat() { + local iface="$1" + # Check if masquerade or dnat rules exist for this interface in live nft + nft list ruleset 2>/dev/null | grep -q "oifname \"$iface\".*masquerade\|iifname \"$iface\".*dnat" +} +# ─── Detect if DNAT applies on an interface ─────────────────────────────────── +detect_has_dnat() { + local iface="$1" + nft list ruleset 2>/dev/null | grep -q "iifname \"$iface\".*dnat" +} +# ─── Get zone name for an interface from fw4 chains ────────────────────────── +detect_zone_for_iface() { + local iface="$1" + # fw4 names chains like input_lan, forward_lan, etc. + # Look for iifname matches in chain jumps + nft list ruleset 2>/dev/null | \ + grep "iifname \"$iface\".*jump" | \ + grep -oE 'jump [a-z_]+' | \ + head -1 | awk '{print $2}' | sed 's/input_//;s/forward_//;s/output_//' +} +# ─── Get all interfaces that have DNAT rules ────────────────────────────────── +detect_dnat_ifaces() { + nft list ruleset 2>/dev/null | \ + grep "iifname.*dnat" | \ + grep -oE '"[^"]+"' | head -1 | tr -d '"' +} +# ─── Full environment check ─────────────────────────────────────────────────── +detect_check_all() { + local ok=0 warn=0 err=0 + local frontend=$(ecm_frontend) + + ui_section "System Environment" + ui_kv "Kernel" "$(uname -r)" + ui_kv "BusyBox" "$(awk 2>&1 | head -1 | sed 's/^BusyBox //')" + ui_kv "nft" "$(nft --version 2>/dev/null | head -1)" + + ui_section "Firewall" + local fw + fw=$(detect_fw_backend) + ui_kv "Backend" "$fw" + if [ "$fw" != "fw4" ]; then + ui_warn "NSS-Switch is designed for fw4/nftables" + warn=$((warn+1)) + else + ui_ok "fw4 detected" + ok=$((ok+1)) + fi + + ui_section "nftables Tables" + nft list tables 2>/dev/null | while IFS= read -r t; do + ui_kv "table" "$t" + done + + ui_section "fw4 Mangle Chains (our injection points)" + for chain in mangle_prerouting mangle_postrouting raw_prerouting; do + if nft list chain inet fw4 "$chain" >/dev/null 2>&1; then + ui_ok "$chain — present" + ok=$((ok+1)) + else + ui_warn "$chain — NOT present (unexpected)" + warn=$((warn+1)) + fi + done + + ui_section "Offload Engine" + case "$frontend" in + NSS) + ui_ok "Qualcomm NSS hardware offload detected" + ok=$((ok+1)) + + if [ -d "$ECM_DEBUGFS" ]; then + ui_ok "ECM debugfs present at $ECM_DEBUGFS" + ok=$((ok+1)) + else + ui_error "ECM debugfs NOT found — NSS offload may not be active" + err=$((err+1)) + fi + + if ecm_mark_classifier_available; then + ui_ok "ecm_classifier_mark — AVAILABLE (ct mark bypass will work)" + ok=$((ok+1)) + else + ui_error "ecm_classifier_mark — NOT AVAILABLE (bypass via ct mark will NOT work!)" + err=$((err+1)) + fi + + ui_kv "accel_delay_pkts" "$(ecm_accel_delay_pkts)" + ;; + SFE) + ui_ok "Shortcut Forwarding Engine (SFE) detected" + ok=$((ok+1)) + ui_info "ct mark bypass support depends on SFE implementation" + ;; + MTK_PPE) + ui_ok "MediaTek PPE/HNAT hardware offload detected" + ok=$((ok+1)) + ui_info "ct mark bypass not applicable for PPE hardware" + ;; + SW_FLOW) + ui_ok "Linux nf_flow_table software offload detected" + ok=$((ok+1)) + ui_info "ct mark bypass not applicable for software flow offload" + ;; + *) + ui_warn "No offload engine detected" + warn=$((warn+1)) + ;; + esac + + ui_kv "ECM frontend" "$frontend" + ui_kv "ECM engine (UCI)" "$(ecm_engine)" + + ui_section "Conntrack" + if [ -f /proc/net/nf_conntrack ]; then + ui_ok "nf_conntrack available" + ui_kv "Total connections" "$(wc -l < /proc/net/nf_conntrack)" + ok=$((ok+1)) + else + ui_error "nf_conntrack not available" + err=$((err+1)) + fi + + ui_section "NSS-Switch State" + if [ -f "$RULES_FILE" ]; then + local rule_count + rule_count=$(grep -cv -e '^#' -e '^$' "$RULES_FILE" 2>/dev/null) + ui_kv "Rules file" "$RULES_FILE" + ui_kv "Active rules" "$rule_count" + else + ui_warn "No rules file yet (no rules defined)" + fi + + if nft_chains_exist 2>/dev/null; then + ui_ok "NSS-Switch nft chains present in live ruleset" + else + ui_warn "NSS-Switch chains NOT in live ruleset (firewall not reloaded yet?)" + fi + + ui_section "NSS-Switch Mark" + ui_kv "Our ct mark" "$NSS_MARK" + ui_kv "QoS mark range" "0x000000ff (no conflict)" + ui_kv "Bypassed connections" "$(ct_count_bypassed)" + + ui_section "Interfaces" + detect_interfaces | while IFS= read -r iface; do + local nat_flag="" dnat_flag="" zone + detect_has_nat "$iface" && nat_flag=" [NAT]" + detect_has_dnat "$iface" && dnat_flag=" [DNAT]" + zone=$(detect_zone_for_iface "$iface") + [ -z "$zone" ] && zone="?" + printf " %-15s zone=%-10s%s%s\n" "$iface" "$zone" "$nat_flag" "$dnat_flag" + done + + ui_section "Summary" + printf " ${C_GREEN}OK: %d${C_RESET} ${C_YELLOW}WARN: %d${C_RESET} ${C_RED}ERR: %d${C_RESET}\n" \ + "$ok" "$warn" "$err" + [ "$err" -gt 0 ] && return 1 || return 0 +} diff --git a/net/nss-switch/files/usr/lib/nss-switch/lib/ecm.sh b/net/nss-switch/files/usr/lib/nss-switch/lib/ecm.sh new file mode 100644 index 0000000000000..ed94908a8cb67 --- /dev/null +++ b/net/nss-switch/files/usr/lib/nss-switch/lib/ecm.sh @@ -0,0 +1,564 @@ +#!/usr/bin/env ash +# lib/ecm.sh — ECM / NSS / SFE / MTK-PPE / SW-FlowOffload interaction via debugfs +# NSS-Switch — ASH compatible, BusyBox v1.37+ +# +# Compatibnle engines: +# NSS - Qualcomm NSS under ECM (kmod-qca-nss-ecm) +# SFE_ECM - Qualcomm SFE as ECM frontend (ecm + shortcut-fe) +# SFE - Qualcomm SFE standalone (no ECM layer, /dev/sfe) +# MTK_PPE - MediaTek PPE/HNAT (mt7621/mt7622/mt7986/mt7988) +# SW_FLOW - Linux nf_flow_table software flow offload (nft flowtable) +# NONE - No offload detected + +# ─── Internal cache ─────────────────────────────────────────────────────────── +_ECM_OFFLOAD_ENGINE="" + +# ─── Detect active offload engine (result is cached) ───────────────────────── +offload_detect() { + if [ -n "$_ECM_OFFLOAD_ENGINE" ]; then + echo "$_ECM_OFFLOAD_ENGINE" + return 0 + fi + + local engine="NONE" + + # 1, NSS - Qualcomm NSS under ECM (kmod-qca-nss-ecm) + if [ -d "$ECM_DEBUGFS/ecm_nss_ipv4" ]; then + engine="NSS" + + # 2. SFE_ECM - Qualcomm SFE as ECM frontend (ecm + shortcut-fe) + elif [ -d "$ECM_DEBUGFS/ecm_sfe_ipv4" ]; then + engine="SFE_ECM" + + # 3. SFE - Qualcomm SFE standalone (no ECM layer, /dev/sfe) + elif [ -c /dev/sfe ] || lsmod 2>/dev/null | grep -q "^shortcut_fe"; then + engine="SFE" + + # 4. MTK_PPE - MediaTek PPE/HNAT (mt7621/mt7622/mt7986/mt7988) + elif [ -d /sys/kernel/debug/ppe0 ]; then + engine="MTK_PPE" + + # 5. SW_FLOW - Linux nf_flow_table software flow offload (nft flowtable) + elif lsmod 2>/dev/null | grep -q "^nf_flow_table"; then + engine="SW_FLOW" + fi + + _ECM_OFFLOAD_ENGINE="$engine" + dbg "offload_detect: $engine" + echo "$engine" +} + +# ─── Check offload is loaded and usable ─────────────────────────────────────── +ecm_check() { + # NEDDED for NSS, SFE_ECM, MTK_PPE + if ! mount | grep -q debugfs; then + dbg "debugfs not mounted, attempting mount" + mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null || true + fi + + local engine + engine=$(offload_detect) + + case "$engine" in + NSS|SFE_ECM) + if [ ! -d "$ECM_DEBUGFS" ]; then + ui_error "ECM debugfs not found at $ECM_DEBUGFS" + ui_error "Is kmod-qca-nss-ecm loaded?" + return 1 + fi + ;; + SFE) + if ! [ -c /dev/sfe ] && ! lsmod 2>/dev/null | grep -q "^shortcut_fe"; then + ui_error "SFE standalone: module not loaded and /dev/sfe missing" + return 1 + fi + ;; + MTK_PPE) + if [ ! -d /sys/kernel/debug/ppe0 ]; then + ui_error "MTK PPE: /sys/kernel/debug/ppe0 not found" + return 1 + fi + ;; + SW_FLOW) + if ! lsmod 2>/dev/null | grep -q "^nf_flow_table"; then + ui_error "SW flow offload: nf_flow_table module not loaded" + return 1 + fi + ;; + NONE) + ui_error "No hardware or software offload engine detected!" + return 1 + ;; + esac + + dbg "ecm_check: OK (engine=$engine)" + return 0 +} + +# ─── Check Mark Classifier is available ─────────────────────────────────────── +# ONLY NSS +ecm_mark_classifier_available() { + [ -d "$ECM_DEBUGFS/ecm_classifier_mark" ] +} + +# ─── Get current frontend ──────────────────────────────────────────────────── +ecm_frontend() { + local engine + engine=$(offload_detect) + case "$engine" in + NSS) echo "NSS" ;; + SFE_ECM) echo "SFE" ;; + SFE) echo "SFE" ;; + MTK_PPE) echo "MTK_PPE" ;; + SW_FLOW) echo "SW_FLOW" ;; + *) echo "UNKNOWN"; return 1 ;; + esac + return 0 +} + +# ─── Get acceleration engine from UCI ───────────────────────────────────────── +ecm_engine() { + local engine + engine=$(grep -A2 "config ecm 'global'" /etc/config/ecm 2>/dev/null | \ + grep "acceleration_engine" | \ + awk '{print $3}' | tr -d "'") + echo "${engine:-auto}" +} + +# ─── Stop IPv4 frontend ──────────────────────────────────────────────────────── +ecm_stop_ipv4() { + local engine + engine=$(offload_detect) + + case "$engine" in + NSS|SFE_ECM) + local f="$ECM_DEBUGFS/front_end_ipv4_stop" + if [ ! -f "$f" ]; then + ui_error "front_end_ipv4_stop not found" + return 1 + fi + dbg "Writing 1 to $f" + echo 1 > "$f" 2>/dev/null + ui_ok "ECM IPv4 frontend stopped" + ;; + SFE|MTK_PPE|SW_FLOW) + # No per-protocol stop API: flush IPv4 conntrack so connections exit offload + dbg "$engine: flushing IPv4 conntrack" + conntrack -F -f ipv4 2>/dev/null + ui_ok "$engine: IPv4 connections flushed (will re-accelerate on demand)" + ;; + *) + ui_error "No offload engine detected" + return 1 + ;; + esac + return 0 +} + +# ─── Stop IPv6 frontend ──────────────────────────────────────────────────────── +ecm_stop_ipv6() { + local engine + engine=$(offload_detect) + + case "$engine" in + NSS|SFE_ECM) + local f="$ECM_DEBUGFS/front_end_ipv6_stop" + if [ ! -f "$f" ]; then + ui_error "front_end_ipv6_stop not found" + return 1 + fi + dbg "Writing 1 to $f" + echo 1 > "$f" 2>/dev/null + ui_ok "ECM IPv6 frontend stopped" + ;; + SFE|MTK_PPE|SW_FLOW) + dbg "$engine: flushing IPv6 conntrack" + conntrack -F -f ipv6 2>/dev/null + ui_ok "$engine: IPv6 connections flushed (will re-accelerate on demand)" + ;; + *) + ui_error "No offload engine detected" + return 1 + ;; + esac + return 0 +} + +# ─── Restart offload service ─────────────────────────────────────────────────── +ecm_restart() { + local engine + engine=$(offload_detect) + _ECM_OFFLOAD_ENGINE="" + + case "$engine" in + NSS) + if [ -f /etc/init.d/qca-nss-ecm ]; then + dbg "Restarting qca-nss-ecm" + /etc/init.d/qca-nss-ecm restart 2>/dev/null + else + dbg "qca-nss-ecm init script not found" + fi + ;; + SFE_ECM) + if [ -f /etc/init.d/sfe-ecm ]; then + dbg "Restarting sfe-ecm" + /etc/init.d/sfe-ecm restart 2>/dev/null + elif [ -f /etc/init.d/ecm ]; then + dbg "Restarting ecm" + /etc/init.d/ecm restart 2>/dev/null + else + dbg "No SFE_ECM init script found" + fi + ;; + SFE) + # Standalone SFE: reload kernel module + dbg "Reloading shortcut_fe module" + rmmod shortcut_fe 2>/dev/null + modprobe shortcut_fe 2>/dev/null + ;; + MTK_PPE) + # PPE is embedded in the Ethernet driver; restart via firewall + dbg "MTK PPE: restarting firewall to re-init PPE bindings" + conntrack -F + # DEBUG: if defunct does NOT work for PPE, will try restarting fw: + # /etc/init.d/firewall restart 2>/dev/null + ;; + SW_FLOW) + dbg "SW_FLOW: restarting firewall" + # DEBUG: Mandatory restarting + /etc/init.d/firewall restart 2>/dev/null + ;; + *) + dbg "No offload service found to restart" + ;; + esac + return 0 +} + +# ─── Defunct ALL connections ─────────────────────────────────────────────────── +ecm_defunct_all() { + local engine + engine=$(offload_detect) + + case "$engine" in + NSS|SFE_ECM) + local f="$ECM_DEBUGFS/ecm_db/defunct_all" + if [ ! -f "$f" ]; then + ui_error "defunct_all not found at $f" + return 1 + fi + dbg "Writing 1 to defunct_all" + echo 1 > "$f" 2>/dev/null + ui_ok "All ECM connections defuncted (will be re-evaluated)" + ;; + SFE|MTK_PPE|SW_FLOW) + dbg "$engine: flushing all conntrack entries" + conntrack -F 2>/dev/null + ui_ok "$engine: all connections flushed (will re-accelerate on demand)" + ;; + *) + ui_error "No offload engine detected" + return 1 + ;; + esac + return 0 +} + +# ─── Defunct connections by port ────────────────────────────────────────────── +ecm_defunct_by_port() { + local port="$1" + local engine + engine=$(offload_detect) + + case "$engine" in + NSS|SFE_ECM) + local f="$ECM_DEBUGFS/ecm_db/defunct_by_port" + if [ -f "$f" ]; then + dbg "defunct_by_port: $port" + echo "$port" > "$f" 2>/dev/null + ui_ok "ECM connections on port $port defuncted" + else + dbg "defunct_by_port not available, falling back to defunct_all" + ecm_defunct_all + fi + ;; + SFE|MTK_PPE|SW_FLOW) + dbg "$engine: flushing conntrack for port $port" + conntrack -D -p tcp --dport "$port" 2>/dev/null + conntrack -D -p udp --dport "$port" 2>/dev/null + conntrack -D -p tcp --sport "$port" 2>/dev/null + conntrack -D -p udp --sport "$port" 2>/dev/null + ui_ok "$engine: connections on port $port flushed" + ;; + *) + ui_error "No offload engine detected" + return 1 + ;; + esac + return 0 +} + +# ─── Get connection list ─────────────────────────────────────────────────────── +ecm_connections() { + local engine + engine=$(offload_detect) + + case "$engine" in + NSS|SFE_ECM) + local dump_bin + dump_bin=$(command -v ecm_dump.sh 2>/dev/null) + if [ -z "$dump_bin" ]; then + dbg "ecm_dump.sh not found" + return 1 + fi + # Real ecm_dump.sh output is key=value, grouped by connection index: + # conns.conn.N.sip_address=... conns.conn.N.protocol=6 etc. + # Connection boundary = index N changes. + # accel_mode: 0=CPU 1=PENDING 2=accelerated (NSS or SFE) + ecm_dump.sh 2>/dev/null | awk -F= ' + function emit() { + ps = (proto=="6")?"tcp":(proto=="17")?"udp":proto + as = (am=="2")?"NSS":(am=="1")?"PENDING":"CPU" + print ps"|"src"|"sport"|"dst"|"dport"|"as + } + /^conns\.conn\.[0-9]+\./ { + split($1, p, ".") + n = p[3] + if (n != cur) { + if (cur != "") emit() + proto="?"; src="?"; sport="?"; dst="?"; dport="?"; am="0" + cur = n + } + key = $1; sub(/^conns\.conn\.[0-9]+\./, "", key) + val = $2 + if (key == "protocol") proto = val + else if (key == "sip_address") src = val + else if (key == "sport") sport = val + else if (key == "dip_address") dst = val + else if (key == "dport") dport = val + else if (key ~ /\.ported\.accel_mode$/) am = val + } + END { if (cur != "") emit() } + ' + ;; + SFE) + # /dev/sfe outputs XML-ish per-connection data + if [ ! -c /dev/sfe ]; then + dbg "/dev/sfe not found" + return 1 + fi + # /dev/sfe is XML-ish; use 2-arg match + RSTART/RLENGTH + cat /dev/sfe 2>/dev/null | awk ' + function xtag(tag, pat) { + pat = "<" tag ">[^<]+" + if (match($0, pat)) + return substr($0, RSTART+length(tag)+2, RLENGTH-length(tag)-2) + return "?" + } + // { in_c=1; proto="?"; src="?"; sport="?"; dst="?"; dport="?" } + /<\/connection>/{ if (in_c) print proto"|"src"|"sport"|"dst"|"dport"|""SFE""; in_c=0 } + in_c && // { proto = xtag("protocol") } + in_c && // { src = xtag("src_ip") } + in_c && // { sport = xtag("src_port") } + in_c && // { dst = xtag("dest_ip") } + in_c && //{ dport = xtag("dest_port")} + ' + ;; + MTK_PPE) + # PPE entries: one per line, BND = hardware-bound (actively offloaded) + # Format: [idx] BND proto src=IP:port dst=IP:port ... + local f="/sys/kernel/debug/ppe0/entries" + if [ ! -f "$f" ]; then + dbg "ppe0/entries not found" + return 1 + fi + cat "$f" 2>/dev/null | awk ' + /BND/ { + proto="?"; src="?"; sport="?"; dst="?"; dport="?" + # proto=VALUE + if (match($0, /proto=[^ ]+/)) + proto = substr($0, RSTART+6, RLENGTH-6) + # src=IP:port + if (match($0, /src=[^ ]+/)) { + pair = substr($0, RSTART+4, RLENGTH-4) + n = split(pair, a, ":"); src = a[1] + if (n > 1) sport = a[2] + } + # dst=IP:port + if (match($0, /dst=[^ ]+/)) { + pair = substr($0, RSTART+4, RLENGTH-4) + n = split(pair, a, ":"); dst = a[1] + if (n > 1) dport = a[2] + } + print proto"|"src"|"sport"|"dst"|"dport"|""HW_OFFLOAD"" + } + ' + ;; + SW_FLOW) + # No flowtable-specific connection list exposed to userspace. + # Use conntrack as proxy for established (potentially offloaded) flows. + # Format: ipv4 2 tcp 6 TTL ESTABLISHED src=.. dst=.. sport=.. dport=.. + # nf_conntrack format: ipv4 2 tcp 6 TTL STATE src=.. dst=.. sport=.. dport=.. [reply] ... + # $3 = proto name (tcp/udp/...), first src=/dst=/sport=/dport= = forward direction + cat /proc/net/nf_conntrack 2>/dev/null | awk ' + { + proto=$3; src="?"; sport="?"; dst="?"; dport="?" + if (match($0, /src=[^ ]+/)) src = substr($0, RSTART+4, RLENGTH-4) + if (match($0, /dst=[^ ]+/)) dst = substr($0, RSTART+4, RLENGTH-4) + if (match($0, /sport=[^ ]+/)) sport = substr($0, RSTART+6, RLENGTH-6) + if (match($0, /dport=[^ ]+/)) dport = substr($0, RSTART+6, RLENGTH-6) + print proto"|"src"|"sport"|"dst"|"dport"|""SW_OFFLOAD"" + } + ' + ;; + *) + dbg "No offload engine detected!" + return 1 + ;; + esac +} + +# ─── Get stats summary ──────────────────────────────────────────────────────── +ecm_stats() { + local engine + engine=$(offload_detect) + + case "$engine" in + NSS|SFE_ECM) + local f="$ECM_DEBUGFS/stats" + if [ -f "$f" ]; then + cat "$f" + return 0 + fi + ui_warn "No stats file found in ECM debugfs" + return 1 + ;; + SFE) + if [ -c /dev/sfe ]; then + cat /dev/sfe 2>/dev/null + return 0 + fi + ui_warn "SFE: /dev/sfe not available" + return 1 + ;; + MTK_PPE) + if [ -f /sys/kernel/debug/ppe0/entries ]; then + echo "=== PPE0 ===" + cat /sys/kernel/debug/ppe0/entries 2>/dev/null + # Dual-PPE SoCs (mt7986, mt7988) + if [ -f /sys/kernel/debug/ppe1/entries ]; then + echo "=== PPE1 ===" + cat /sys/kernel/debug/ppe1/entries 2>/dev/null + fi + return 0 + fi + ui_warn "MTK PPE: entries file not found" + return 1 + ;; + SW_FLOW) + local total + total=$(wc -l < /proc/net/nf_conntrack 2>/dev/null || echo 0) + echo "Total conntrack entries: $total" + if command -v nft >/dev/null 2>&1; then + nft list flowtables 2>/dev/null || true + fi + return 0 + ;; + *) + ui_warn "No offload engine detected" + return 1 + ;; + esac +} + +# ─── Check if a connection (by mark) is being bypassed ──────────────────────── +ecm_is_bypassed_by_mark() { + local src_ip="$1" + cat /proc/net/nf_conntrack 2>/dev/null | \ + grep "src=$src_ip " | \ + grep -c "mark=$NSS_MARK" 2>/dev/null; true +} + +# ─── Get accel_delay_pkts ───────────────────────────────────────────────────── +ecm_accel_delay_pkts() { + local engine + engine=$(offload_detect) + + case "$engine" in + NSS|SFE_ECM) + local f="$ECM_DEBUGFS/ecm_classifier_default/accel_delay_pkts" + if [ -f "$f" ]; then + cat "$f" + return 0 + fi + ;; + esac + echo "N/A (for MTK-PPP, SW Flow or SFE standalone)" +} + +# ─── Full environment dump for debug ────────────────────────────────────────── +ecm_debug_dump() { + local engine + engine=$(offload_detect) + + ui_section "Offload Environment" + ui_kv "Offload engine" "$engine" + ui_kv "Frontend (compat)" "$(ecm_frontend)" + ui_kv "Engine (UCI)" "$(ecm_engine)" + ui_kv "accel_delay_pkts" "$(ecm_accel_delay_pkts)" + + ui_section "Engine Details" + case "$engine" in + NSS|SFE_ECM) + ui_kv "ECM debugfs" "$ECM_DEBUGFS" + ui_kv "ECM loaded" "$([ -d "$ECM_DEBUGFS" ] && echo YES || echo NO)" + ui_kv "Mark classifier" "$(ecm_mark_classifier_available && echo AVAILABLE || echo MISSING)" + ui_kv "Debugfs dirs" "$(ls "$ECM_DEBUGFS" 2>/dev/null | tr '\n' ' ')" + if [ -d "$ECM_DEBUGFS/ecm_db" ]; then + ui_section "ECM DB Files" + ls -la "$ECM_DEBUGFS/ecm_db/" 2>/dev/null + fi + ui_section "ECM Stats (first 40 lines)" + ecm_stats 2>/dev/null | head -40 + if [ "$engine" = "NSS" ] && ecm_mark_classifier_available; then + ui_section "Mark Classifier State" + ls -la "$ECM_DEBUGFS/ecm_classifier_mark/" 2>/dev/null + for f in "$ECM_DEBUGFS/ecm_classifier_mark/"*; do + [ -f "$f" ] && printf " %s = %s\n" "$(basename "$f")" "$(cat "$f" 2>/dev/null)" + done + fi + ;; + SFE) + ui_kv "/dev/sfe" "$([ -c /dev/sfe ] && echo PRESENT || echo MISSING)" + ui_kv "Module" "$(lsmod 2>/dev/null | grep "^shortcut_fe" | awk '{print $1" (size="$2")"}')" + ui_section "SFE Stats (first 40 lines)" + ecm_stats 2>/dev/null | head -40 + ;; + MTK_PPE) + local bound + bound=$(grep -c "BND" /sys/kernel/debug/ppe0/entries 2>/dev/null || echo 0) + ui_kv "PPE0 bound flows" "$bound" + if [ -f /sys/kernel/debug/ppe1/entries ]; then + bound=$(grep -c "BND" /sys/kernel/debug/ppe1/entries 2>/dev/null || echo 0) + ui_kv "PPE1 bound flows" "$bound" + fi + ui_section "PPE0 Entries (first 40 lines)" + cat /sys/kernel/debug/ppe0/entries 2>/dev/null | head -40 + ;; + SW_FLOW) + ui_kv "nf_flow_table" "$(lsmod 2>/dev/null | grep "^nf_flow_table" | awk '{print "loaded (size="$2")"}')" + ui_kv "UCI config" "$(grep -A5 'config defaults' /etc/config/firewall 2>/dev/null | \ + grep 'flow_offloading' | sed 's/^\s*//' | tr '\n' ' ')" + ui_section "Active Flowtables" + if command -v nft >/dev/null 2>&1; then + nft list flowtables 2>/dev/null || ui_warn "nft: no flowtables found" + else + ui_warn "nft not available" + fi + ui_kv "Conntrack entries" "$(wc -l < /proc/net/nf_conntrack 2>/dev/null || echo 0)" + ;; + *) + ui_warn "No offload engine detected" + ;; + esac +} diff --git a/net/nss-switch/files/usr/lib/nss-switch/lib/nft.sh b/net/nss-switch/files/usr/lib/nss-switch/lib/nft.sh new file mode 100755 index 0000000000000..10283f513e4a7 --- /dev/null +++ b/net/nss-switch/files/usr/lib/nss-switch/lib/nft.sh @@ -0,0 +1,426 @@ +#!/usr/bin/env ash +# lib/nft.sh — nftables chain/rule management for NSS-Switch +# Works by editing /usr/bin/NSS-Switch/firewall.d/nss-bypass-rules (nft file) +# and reloading via /etc/init.d/firewall restart +# ASH compatible, BusyBox v1.37+ + +# ─── Paths ──────────────────────────────────────────────────────────────────── +NFT_INCLUDE_LINK=/etc/firewall.d/nss-bypass-rules +NFT_RULES_HEADER="# NSS-Switch managed rules — do not edit manually" + +# ─── Check nft binary ───────────────────────────────────────────────────────── +nft_check() { + command -v nft >/dev/null 2>&1 || { ui_error "nft not found"; return 1; } +} + +# ─── Check our chains exist in live ruleset ─────────────────────────────────── +nft_chains_exist() { + nft list chain inet fw4 "$NFT_CHAIN_PRE" >/dev/null 2>&1 && \ + nft list chain inet fw4 "$NFT_CHAIN_POST" >/dev/null 2>&1 +} + + +# DEBUG PR-1 +# ─── Generate the full nss-bypass-rules nft script from rules.conf ────────────────── +nft_generate_script() { + dbg "Generating $FW_SCRIPT from $RULES_FILE" + + echo '#!/bin/ash' > "$FW_SCRIPT" + echo '# NSS-Switch firewall.d hook — auto-generated, do not edit manually' >> "$FW_SCRIPT" + echo "# Generated: $(date)" >> "$FW_SCRIPT" + echo '' >> "$FW_SCRIPT" + echo "NSS_MARK=${NSS_MARK}" >> "$FW_SCRIPT" + echo "NFT_CHAIN_PRE=${NFT_CHAIN_PRE}" >> "$FW_SCRIPT" + echo "NFT_CHAIN_POST=${NFT_CHAIN_POST}" >> "$FW_SCRIPT" + echo '' >> "$FW_SCRIPT" + # DEBUG PR-1 + echo 'nft_add_chains() {' >> "$FW_SCRIPT" + echo ' nft add chain inet fw4 ${NFT_CHAIN_PRE} 2>/dev/null || true' >> "$FW_SCRIPT" + echo ' nft flush chain inet fw4 ${NFT_CHAIN_PRE} 2>/dev/null || true' >> "$FW_SCRIPT" + echo ' nft add chain inet fw4 ${NFT_CHAIN_POST} 2>/dev/null || true' >> "$FW_SCRIPT" + echo ' nft flush chain inet fw4 ${NFT_CHAIN_POST} 2>/dev/null || true' >> "$FW_SCRIPT" + echo '' >> "$FW_SCRIPT" + echo ' handles=$(nft -a list chain inet fw4 mangle_prerouting 2>/dev/null | grep "jump ${NFT_CHAIN_PRE}" | grep -oE '"'"'handle [0-9]+'"'"' | awk '"'"'{print $2}'"'"')' >> "$FW_SCRIPT" + echo ' for h in $handles; do nft delete rule inet fw4 mangle_prerouting handle "$h" 2>/dev/null; done' >> "$FW_SCRIPT" + echo '' >> "$FW_SCRIPT" + echo ' handles=$(nft -a list chain inet fw4 mangle_postrouting 2>/dev/null | grep "jump ${NFT_CHAIN_POST}" | grep -oE '"'"'handle [0-9]+'"'"' | awk '"'"'{print $2}'"'"')' >> "$FW_SCRIPT" + echo ' for h in $handles; do nft delete rule inet fw4 mangle_postrouting handle "$h" 2>/dev/null; done' >> "$FW_SCRIPT" + echo '' >> "$FW_SCRIPT" + echo ' handles=$(nft -a list chain inet fw4 mangle_output 2>/dev/null | grep "jump ${NFT_CHAIN_PRE}" | grep -oE '"'"'handle [0-9]+'"'"' | awk '"'"'{print $2}'"'"')' >> "$FW_SCRIPT" + echo ' for h in $handles; do nft delete rule inet fw4 mangle_output handle "$h" 2>/dev/null; done' >> "$FW_SCRIPT" + echo '' >> "$FW_SCRIPT" + echo ' nft add rule inet fw4 mangle_prerouting jump ${NFT_CHAIN_PRE} comment "\"NSS-Switch prerouting\""' >> "$FW_SCRIPT" + echo ' nft add rule inet fw4 mangle_postrouting jump ${NFT_CHAIN_POST} comment "\"NSS-Switch postrouting\""' >> "$FW_SCRIPT" + echo ' nft add rule inet fw4 mangle_output jump ${NFT_CHAIN_PRE} comment "\"NSS-Switch output\""' >> "$FW_SCRIPT" + echo '}' >> "$FW_SCRIPT" + echo '' >> "$FW_SCRIPT" + echo 'nft_add_rules() {' >> "$FW_SCRIPT" + + if [ -f "$RULES_FILE" ]; then + while IFS='|' read -r id proto src_ip dst_ip src_port dst_port iface persist comment; do + case "$id" in '#'*|'') continue ;; esac + dbg "Generating nft rule for id=$id" + _nft_emit_rule "$id" "$proto" "$src_ip" "$dst_ip" \ + "$src_port" "$dst_port" "$iface" "$comment" + done < "$RULES_FILE" + fi + + echo ' true' >> "$FW_SCRIPT" + echo '}' >> "$FW_SCRIPT" + echo '' >> "$FW_SCRIPT" + echo 'nft_add_chains' >> "$FW_SCRIPT" + echo 'nft_add_rules' >> "$FW_SCRIPT" + + chmod +x "$FW_SCRIPT" + dbg "Script generated at $FW_SCRIPT" +} + + + +# ─── Emit a single nft rule (used by nft_generate_script) ──────────────────── +# DEBUG PR-1 +_nft_emit_rule() { + local id="$1" proto="$2" src_ip="$3" dst_ip="$4" + local src_port="$5" dst_port="$6" iface="$7" comment="$8" + local match="" + + # 1. Manejo de interfaz-> Respetar exactamente lo especificado + if [ "$iface" != "any" ]; then + case "$iface" in + out:*) + # Usuario especificó out: explícitamente + local real_iface="${iface#out:}" + match="${match} oifname \"${real_iface}\"" + ;; + local:*) + # Usuario eligió una interfaz local (tráfico del router) + local real_iface="${iface#local:}" + match="${match} oifname \"${real_iface}\"" + ;; + *) + # Usuario especificó interfaz normal (sin prefijo) + match="${match} iifname \"${iface}\"" + ;; + esac + fi + + # 2. Protocolo + [ "$proto" != "any" ] && match="${match} meta l4proto ${proto}" + + # 3. IP origen - detectar IPv4 vs IPv6 por el formato + if [ "$src_ip" != "any" ]; then + case "$src_ip" in + *:*) match="${match} ip6 saddr ${src_ip}" ;; + *) match="${match} ip saddr ${src_ip}" ;; + esac + fi + + # 4. IP destino - detectar IPv4 vs IPv6 por el formato + if [ "$dst_ip" != "any" ]; then + case "$dst_ip" in + *:*) match="${match} ip6 daddr ${dst_ip}" ;; + *) match="${match} ip daddr ${dst_ip}" ;; + esac + fi + + # 5. Puertos - SOLO si proto no es any + if [ "$proto" != "any" ]; then + [ "$src_port" != "any" ] && match="${match} ${proto} sport ${src_port}" + [ "$dst_port" != "any" ] && match="${match} ${proto} dport ${dst_port}" + fi + + # 6. Verificar que hay al menos un criterio + if [ -z "$(echo "$match" | tr -d ' ')" ]; then + printf " # SKIPPED rule id=%s — no match criteria\n" "$id" >> "$FW_SCRIPT" + return + fi + + # 7. Escapar comillas simples en el comentario para seguridad + local safe_comment + safe_comment=$(printf "%s" "$comment" | sed "s/'/'\\\\''/g") + + # 8. Regla de marcado principal (en nss_bypass_pre) + printf " # Rule id=%s: %s\n" "$id" "$safe_comment" >> "$FW_SCRIPT" + printf " nft add rule inet fw4 %s %s ct mark set ct mark or %s comment '\"NSS-Switch id=%s: %s\"'\n" \ + "$NFT_CHAIN_PRE" "$match" "$NSS_MARK" "$id" "$safe_comment" >> "$FW_SCRIPT" + + # 9. Reglas de preservación de bits (solo para esta regla) + # POSTROUTING: guardar en ct mark + printf " nft add rule inet fw4 %s %s ct mark set ct mark xor %s comment '\"NSS-Switch: clear bypass mark from ct\"'\n" \ + "$NFT_CHAIN_POST" "$match" "$NSS_MARK" >> "$FW_SCRIPT" + printf " nft add rule inet fw4 %s %s ct mark set ct mark or %s comment '\"NSS-Switch: save bypass mark to conntrack\"'\n" \ + "$NFT_CHAIN_POST" "$match" "$NSS_MARK" >> "$FW_SCRIPT" + + # PREROUTING: restaurar en meta mark + printf " nft add rule inet fw4 %s %s meta mark set meta mark xor %s comment '\"NSS-Switch: clear bypass mark from meta\"'\n" \ + "$NFT_CHAIN_PRE" "$match" "$NSS_MARK" >> "$FW_SCRIPT" + printf " nft add rule inet fw4 %s %s meta mark set meta mark or %s comment '\"NSS-Switch: restore bypass mark from conntrack\"'\n" \ + "$NFT_CHAIN_PRE" "$match" "$NSS_MARK" >> "$FW_SCRIPT" +} + + + + +# ─── Apply: generate script and reload firewall ─────────────────────────────── +# DEBUG PR-1 +# firewall reload NO está funcionando en todos los casos, y tampoco podemos hacer un fw restart +nft_apply() { + nft_generate_script || return 1 + _nft_ensure_fw4_include + + if [ "${DEBUG:-0}" = "1" ] || [ "$DEBUG_MODE" = "yes" ]; then + dbg "Reloading firewall" + /etc/init.d/firewall reload >> "$DEBUG_LOG" 2>&1 + else + /etc/init.d/firewall reload > /dev/null 2>&1 + fi + + # DEBUG PR-1 Por eso, ejecutamos especificamente AQUI el script generado + if [ -f "$FW_SCRIPT" ]; then + dbg "Executing $FW_SCRIPT" + sh "$FW_SCRIPT" 2>/dev/null + fi + + ui_ok "Firewall reloaded, NSS-Switch rules applied" +} + +# ─── Ensure /etc/firewall.d/nss-bypass-rules but in the proper way ──────── +_nft_ensure_fw4_include() { + # DEBUG + # LO hardcodeo, porque querré exportar esta y otras funcs a C + local target="/etc/firewall.d/nss-bypass-rules" + if [ ! -e "$target" ] && [ ! -L "$target" ]; then + dbg "Creating symlink $target -> $FW_SCRIPT" + ln -s "$FW_SCRIPT" "$target" + elif [ -L "$target" ]; then + local current + current=$(readlink -f "$target" 2>/dev/null) + if [ "$current" != "$FW_SCRIPT" ]; then + dbg "Updating symlink $target -> $FW_SCRIPT" + ln -sf "$FW_SCRIPT" "$target" + fi + fi + _nft_ensure_uci_include +} + +# ─── Ensure UCI include block exists in /etc/config/firewall ────────────────── +_nft_ensure_uci_include() { + if ! uci -q show firewall.nss_bypass_include >/dev/null 2>&1; then + dbg "Adding UCI include for nss-bypass-rules" + # DEBUG -> Evaluate possible error warns + # uci add firewall include > /dev/null + if ! uci add firewall include > /dev/null 2>&1; then + ui_error "Failed to add UCI include" + return 1 + fi + uci rename firewall.@include[-1]="nss_bypass_include" + uci set firewall.nss_bypass_include.type='script' + uci set firewall.nss_bypass_include.path='/etc/firewall.d/nss-bypass-rules' + uci commit firewall + ui_ok "UCI include added to /etc/config/firewall" + fi +} + +# ─── Remove UCI include from /etc/config/firewall ──────────────────────────── +_nft_remove_uci_include() { + if uci -q show firewall.nss_bypass_include >/dev/null 2>&1; then + dbg "Removing UCI include for nss-bypass-rules" + uci -q delete firewall.nss_bypass_include + uci -q commit firewall + dbg "UCI include removed from /etc/config/firewall" + fi +} +# ─── Remove all our nft chains from live ruleset (without reload) ───────────── +nft_remove_live_chains() { + dbg "Removing live NSS-Switch chains from nft" + local handles h + for chain in mangle_prerouting mangle_postrouting; do + handles=$(nft -a list chain inet fw4 "$chain" 2>/dev/null | grep -E "jump $NFT_CHAIN_PRE|jump $NFT_CHAIN_POST" | grep -oE 'handle [0-9]+' | awk '{print $2}') + for h in $handles; do + nft delete rule inet fw4 "$chain" handle "$h" 2>/dev/null + dbg "Deleted jump handle $h from $chain" + done + done + nft delete chain inet fw4 "$NFT_CHAIN_PRE" 2>/dev/null && dbg "Deleted $NFT_CHAIN_PRE" + nft delete chain inet fw4 "$NFT_CHAIN_POST" 2>/dev/null && dbg "Deleted $NFT_CHAIN_POST" +} + +# ─── Show only our rules from live ruleset ──────────────────────────────────── +nft_show_our_rules() { + ui_section "NSS-Switch live nftables rules" + if nft list chain inet fw4 "$NFT_CHAIN_PRE" 2>/dev/null; then + echo "" + nft list chain inet fw4 "$NFT_CHAIN_POST" 2>/dev/null + else + ui_warn "NSS-Switch chains not present in live ruleset" + ui_warn "Run 'nss-switch.sh apply' or reload the firewall" + fi +} + +# ─── Validate rule fields ───────────────────────────────────────────────────── +nft_validate_ipv6() { + local ip="$1" + local original="$ip" + + local cidr="" + case "$ip" in + */*) + cidr="${ip##*/}" + ip="${ip%%/*}" + [ "$cidr" -ge 0 ] 2>/dev/null || return 1 + [ "$cidr" -le 128 ] 2>/dev/null || return 1 + ;; + esac + + ip=$(echo "$ip" | tr 'A-F' 'a-f') + echo "$ip" | grep -qE '^[0-9a-f:]+$' || return 1 + + case "$ip" in + :*) [ "$ip" != "::" ] && return 1 ;; + *:) [ "$ip" != "::" ] && return 1 ;; + esac + + local double_colon_count=$(echo "$ip" | grep -o "::" | wc -l) + [ "$double_colon_count" -gt 1 ] && return 1 + + # Contar grupos (sin ::) + local groups=$(echo "$ip" | tr ':' '\n' | grep -c . 2>/dev/null) + local has_double_colon=0 + echo "$ip" | grep -q "::" && has_double_colon=1 + + # Con ::, los grupos visibles pueden ser entre 0 y 8 + # Sin ::, deben ser exactamente 8 + if [ "$has_double_colon" -eq 0 ]; then + [ "$groups" -ne 8 ] && return 1 + else + # Con ::, los grupos visibles pueden ser 0-7 (8 sería :: con 8 grupos? no es válido) + [ "$groups" -gt 8 ] && return 1 + fi + + # Validar cada grupo + local old_ifs="$IFS" + IFS=':' + for group in $ip; do + [ -z "$group" ] && continue + len=$(echo -n "$group" | wc -c) + [ "$len" -lt 1 ] && return 1 + [ "$len" -gt 4 ] && return 1 + echo "$group" | grep -qE '^[0-9a-f]+$' || return 1 + local dec + dec=$(printf "%d" "0x$group" 2>/dev/null) + [ "$dec" -ge 0 ] 2>/dev/null || return 1 + [ "$dec" -le 65535 ] 2>/dev/null || return 1 + done + IFS="$old_ifs" + + if echo "$original" | grep -qiE '::ffff:[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+'; then + local ipv4_part="${original##*:}" + nft_validate_ipv4 "$ipv4_part" || return 1 + fi + + return 0 +} +nft_validate_ip() { + local ip="$1" + + # IPv4 + if echo "$ip" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$'; then + # Validación IPv4 existente + local ip_only="${ip%%/*}" + local oct1=$(echo "$ip_only" | cut -d'.' -f1) + local oct2=$(echo "$ip_only" | cut -d'.' -f2) + local oct3=$(echo "$ip_only" | cut -d'.' -f3) + local oct4=$(echo "$ip_only" | cut -d'.' -f4) + [ "$oct1" -le 255 ] && [ "$oct2" -le 255 ] && \ + [ "$oct3" -le 255 ] && [ "$oct4" -le 255 ] || return 1 + + local cidr="${ip##*/}" + if [ "$ip" != "$ip_only" ]; then + [ "$cidr" -ge 0 ] 2>/dev/null && [ "$cidr" -le 32 ] 2>/dev/null || return 1 + fi + return 0 + fi + + # IPv6 + if echo "$ip" | grep -q ":"; then + nft_validate_ipv6 "$ip" + return $? + fi + + return 1 +} + +nft_validate_port() { + echo "$1" | grep -qE '^[0-9]{1,5}$' && [ "$1" -ge 1 ] && [ "$1" -le 65535 ] +} + +nft_validate_proto() { + case "$1" in tcp|udp|icmp|icmpv6|any) return 0 ;; *) return 1 ;; esac +} + +nft_validate_iface() { + local iface="$1" + [ "$iface" = "any" ] && return 0 + + # Limpiar prefijos (out:, in:, local:) antes de validar + case "$iface" in + out:*) iface="${iface#out:}" ;; + in:*) iface="${iface#in:}" ;; + local:*) iface="${iface#local:}" ;; + esac + + # Verificar que la interfaz existe + ip link show "$iface" >/dev/null 2>&1 +} + +nft_validate_comment() { + local comment="$1" + + # 1. Empty + [ -z "$comment" ] && return 0 + + + # 2. ; + if echo "$comment" | grep -q ';'; then + ui_error "Comment cannot contain semicolon ';'" + return 1 + fi + + # 3. "" + if echo "$comment" | grep -q '"'; then + ui_error "Comment cannot contain double quotes '\"'" + return 1 + fi + + # 4. '' + if echo "$comment" | grep -q "'"; then + ui_error "Comment cannot contain single quotes \"'\"" + return 1 + fi + + # 5. \\ + if echo "$comment" | grep -q '\\'; then + ui_error "Comment cannot contain backslash '\\'" + return 1 + fi + + # 6. $ + if echo "$comment" | grep -q '\\$'; then + ui_error "Comment cannot contain dollar sign '$'" + return 1 + fi + + # 7. | + if echo "$comment" | grep -q '|'; then + ui_error "Comment cannot contain pipe '|'" + return 1 + fi + + # 8. [[:cntrl:]] + if echo "$comment" | grep -q '[[:cntrl:]]'; then + ui_error "Comment cannot contain control characters" + return 1 + fi + + return 0 +} diff --git a/net/nss-switch/files/usr/lib/nss-switch/lib/rules.sh b/net/nss-switch/files/usr/lib/nss-switch/lib/rules.sh new file mode 100755 index 0000000000000..dcb916c2f0565 --- /dev/null +++ b/net/nss-switch/files/usr/lib/nss-switch/lib/rules.sh @@ -0,0 +1,277 @@ +#!/usr/bin/env ash +# lib/rules.sh — CRUD operations on state/rules.conf +# Format: id|proto|src_ip|dst_ip|src_port|dst_port|iface|persist|comment +# ASH compatible, BusyBox v1.37+ + +RULES_HEADER="# NSS-Switch rules: id|proto|src_ip|dst_ip|src_port|dst_port|iface|persist|comment" + +# ─── Ensure rules file exists ───────────────────────────────────────────────── +rules_init() { + if [ ! -f "$RULES_FILE" ]; then + mkdir -p "$(dirname "$RULES_FILE")" + printf "%s\n" "$RULES_HEADER" > "$RULES_FILE" + dbg "Created $RULES_FILE" + fi +} + +# ─── Get next available ID ──────────────────────────────────────────────────── +rules_next_id() { + rules_init + local max=0 id + while IFS='|' read -r id _rest; do + case "$id" in '#'*|'') continue ;; esac + [ "$id" -gt "$max" ] 2>/dev/null && max="$id" + done < "$RULES_FILE" + echo $((max+1)) +} + +# ─── Add a rule ─────────────────────────────────────────────────────────────── +rules_add() { + local proto="${1:-any}" + local src_ip="${2:-any}" + local dst_ip="${3:-any}" + local src_port="${4:-any}" + local dst_port="${5:-any}" + local iface="${6:-any}" + local persist="${7:-$PERSIST_DEFAULT}" + local comment="${8:-manual rule}" + + rules_init + local id + id=$(rules_next_id) + printf "%s|%s|%s|%s|%s|%s|%s|%s|%s\n" \ + "$id" "$proto" "$src_ip" "$dst_ip" \ + "$src_port" "$dst_port" "$iface" "$persist" "$comment" \ + >> "$RULES_FILE" + dbg "Rule $id added: $proto $src_ip->$dst_ip iface=$iface persist=$persist" + echo "$id" +} + +# ─── Remove a rule by ID ────────────────────────────────────────────────────── +rules_remove() { + local target_id="$1" + rules_init + if ! rules_get "$target_id" >/dev/null 2>&1; then + ui_error "Rule ID $target_id not found" + return 1 + fi + # Rewrite file without that ID + local tmp="${RULES_FILE}.tmp" + while IFS='|' read -r id rest; do + case "$id" in + '#'*|'') printf "%s\n" "$id${id:+|}$rest" ;; + "$target_id") dbg "Removing rule $id" ;; + *) printf "%s|%s\n" "$id" "$rest" ;; + esac + done < "$RULES_FILE" > "$tmp" + mv "$tmp" "$RULES_FILE" + ui_ok "Rule $target_id removed" +} + +# ─── Get a rule by ID (print the raw line) ─────────────────────────────────── +rules_get() { + local target_id="$1" + rules_init + while IFS='|' read -r id rest; do + case "$id" in '#'*|'') continue ;; esac + if [ "$id" = "$target_id" ]; then + printf "%s|%s\n" "$id" "$rest" + return 0 + fi + done < "$RULES_FILE" + return 1 +} + +# ─── Count rules ────────────────────────────────────────────────────────────── +rules_count() { + rules_init + local n + n=$(grep -cv -e '^#' -e '^$' "$RULES_FILE" 2>/dev/null) + echo "${n:-0}" +} + +# ─── List all rules (formatted) ─────────────────────────────────────────────── +rules_list() { + rules_init + local count + count=$(rules_count) + if [ "$count" -eq 0 ]; then + ui_warn "No bypass rules defined" + return 0 + fi + + # DEBUG + # Vamos a tratar de usar anchos dinamicos, si funciona bien aqui, refactorizaremos para watch y pick + # Overrideamos ui_rule_header + # Este cambio también afecta en ui.sh ...creamos ui_rule_row_dynamic() + + # DEBUG PR-1 -> NO está funcionando nada bien tema anchos dinámicos, pero se queda comentado para futuro + + # Calcular anchos máximos basados en el contenido + local max_id=2 max_proto=5 max_src=7 max_dst=7 max_sport=5 max_dport=5 max_iface=5 max_persist=7 + max_id=2 + max_proto=5 + + while IFS='|' read -r id proto src_ip dst_ip src_port dst_port iface persist comment; do + case "$id" in '#'*|'') continue ;; esac + [ ${#id} -gt $max_id ] && max_id=${#id} + [ ${#proto} -gt $max_proto ] && max_proto=${#proto} + [ ${#src_ip} -gt $max_src ] && max_src=${#src_ip} + [ ${#dst_ip} -gt $max_dst ] && max_dst=${#dst_ip} + [ ${#src_port} -gt $max_sport ] && max_sport=${#src_port} + [ ${#dst_port} -gt $max_dport ] && max_dport=${#dst_port} + [ ${#iface} -gt $max_iface ] && max_iface=${#iface} + [ ${#persist} -gt $max_persist ] && max_persist=${#persist} + done < "$RULES_FILE" + + [ $max_src -gt 30 ] && max_src=30 + [ $max_dst -gt 30 ] && max_dst=30 + [ $max_iface -gt 15 ] && max_iface=15 + + local id_fmt="%-${max_id}s" + local proto_fmt="%-${max_proto}s" + local src_fmt="%-${max_src}s" + local dst_fmt="%-${max_dst}s" + local sport_fmt="%-${max_sport}s" + local dport_fmt="%-${max_dport}s" + local iface_fmt="%-${max_iface}s" + local persist_fmt="%-${max_persist}s" + printf "${BG_MED}${FG_DIM}" + printf " ${id_fmt} ${proto_fmt} ${src_fmt} ${dst_fmt} ${sport_fmt} ${dport_fmt} ${iface_fmt} ${persist_fmt} %s" \ + "ID" "PROTO" "SRC_IP" "DST_IP" "SPORT" "DPORT" "IFACE" "PERSIST" "COMMENT" + printf "${C_RESET}\n" + ui_sep + + while IFS='|' read -r id proto src_ip dst_ip src_port dst_port iface persist comment; do + case "$id" in '#'*|'') continue ;; esac + ui_rule_row_dynamic "$id" "$proto" "$src_ip" "$dst_ip" \ + "$src_port" "$dst_port" "$iface" "$persist" "$comment" \ + "$max_id" "$max_proto" "$max_src" "$max_dst" \ + "$max_sport" "$max_dport" "$max_iface" "$max_persist" + done < "$RULES_FILE" + + ui_sep + ui_kv "Total rules" "$count" +} + + +# ─── Clear all rules ────────────────────────────────────────────────────────── +rules_clear() { + printf "%s\n" "$RULES_HEADER" > "$RULES_FILE" + ui_ok "All rules cleared from $RULES_FILE" +} + +# ─── Clear only non-persistent rules ───────────────────────────────────────── +rules_clear_temp() { + rules_init + local tmp="${RULES_FILE}.tmp" + local removed=0 + while IFS='|' read -r id proto src_ip dst_ip src_port dst_port iface persist comment; do + case "$id" in + '#'*|'') + printf "%s\n" "$id${id:+|}$proto${proto:+|}$src_ip${src_ip:+|}$dst_ip${dst_ip:+|}$src_port${src_port:+|}$dst_port${dst_port:+|}$iface${iface:+|}$persist${persist:+|}$comment" + continue + ;; + esac + if [ "$persist" = "yes" ]; then + printf "%s|%s|%s|%s|%s|%s|%s|%s|%s\n" \ + "$id" "$proto" "$src_ip" "$dst_ip" \ + "$src_port" "$dst_port" "$iface" "$persist" "$comment" + else + removed=$((removed+1)) + dbg "Removing temp rule $id" + fi + done < "$RULES_FILE" > "$tmp" + mv "$tmp" "$RULES_FILE" + ui_ok "Removed $removed temporary rule(s)" +} + +# ─── Get fields of a rule as variables ─────────────────────────────────────── +# Sets: RULE_ID RULE_PROTO RULE_SRC_IP RULE_DST_IP +# RULE_SPORT RULE_DPORT RULE_IFACE RULE_PERSIST RULE_COMMENT +rules_parse() { + local line="$1" + + RULE_ID=$(echo "$line" | cut -d'|' -f1) + RULE_PROTO=$(echo "$line" | cut -d'|' -f2) + RULE_SRC_IP=$(echo "$line" | cut -d'|' -f3) + RULE_DST_IP=$(echo "$line" | cut -d'|' -f4) + RULE_SPORT=$(echo "$line" | cut -d'|' -f5) + RULE_DPORT=$(echo "$line" | cut -d'|' -f6) + RULE_IFACE=$(echo "$line" | cut -d'|' -f7) + RULE_PERSIST=$(echo "$line" | cut -d'|' -f8) + RULE_COMMENT=$(echo "$line" | cut -d'|' -f9) +} + +# ─── Validate all fields of a pending rule ──────────────────────────────────── +rules_validate() { + local proto="$1" src_ip="$2" dst_ip="$3" + local src_port="$4" dst_port="$5" iface="$6" comment="$7" + local ok=1 + + # DEBUG PR-1: Completar IPv6 para nftables + _expand_ipv6() { + local ip="$1" + # Si no es IPv6 o ya tiene ::, devolver tal cual + echo "$ip" | grep -q ":" || { echo "$ip"; return; } + echo "$ip" | grep -q "::" && { echo "$ip"; return; } + + local groups=$(echo "$ip" | tr ':' '\n' | grep -c . 2>/dev/null) + [ -z "$groups" ] && { echo "$ip"; return; } + + if [ "$groups" -lt 8 ]; then + case "$ip" in + */*) + local cidr="${ip#*/}" + local ip_part="${ip%/*}" + echo "${ip_part}::/${cidr}" + ;; + *) + echo "${ip}::" + ;; + esac + else + echo "$ip" + fi + } + + # Normalizar IPs antes de validar + [ "$src_ip" != "any" ] && src_ip=$(_expand_ipv6 "$src_ip") + [ "$dst_ip" != "any" ] && dst_ip=$(_expand_ipv6 "$dst_ip") + + [ "$proto" != "any" ] && ! nft_validate_proto "$proto" && { + ui_error "Invalid protocol: $proto (use tcp|udp|icmp|icmpv6|any)" + ok=0 + } + [ "$src_ip" != "any" ] && ! nft_validate_ip "$src_ip" && { + ui_error "Invalid src IP/CIDR: $src_ip" + ok=0 + } + [ "$dst_ip" != "any" ] && ! nft_validate_ip "$dst_ip" && { + ui_error "Invalid dst IP/CIDR: $dst_ip" + ok=0 + } + [ "$src_port" != "any" ] && ! nft_validate_port "$src_port" && { + ui_error "Invalid src port: $src_port" + ok=0 + } + [ "$dst_port" != "any" ] && ! nft_validate_port "$dst_port" && { + ui_error "Invalid dst port: $dst_port" + ok=0 + } + [ "$iface" != "any" ] && ! nft_validate_iface "$iface" && { + ui_error "Interface not found: $iface" + ok=0 + } + if [ -n "$comment" ] && ! nft_validate_comment "$comment"; then + ui_error "Invalid comment: contains forbidden characters" + ok=0 + fi + if [ "$proto" = "any" ] && [ "$src_ip" = "any" ] && [ "$dst_ip" = "any" ] && \ + [ "$src_port" = "any" ] && [ "$dst_port" = "any" ] && [ "$iface" = "any" ]; then + ui_warn "Rule matches ALL connections - NSS will be disabled for everything" + ui_warn "This may impact routing performance significantly" + fi + + [ "$ok" -eq 1 ] +} diff --git a/net/nss-switch/files/usr/lib/nss-switch/lib/ui.sh b/net/nss-switch/files/usr/lib/nss-switch/lib/ui.sh new file mode 100755 index 0000000000000..a28cd88ae2ef4 --- /dev/null +++ b/net/nss-switch/files/usr/lib/nss-switch/lib/ui.sh @@ -0,0 +1,839 @@ +#!/usr/bin/env ash +# lib/ui.sh — UI helpers: TUI dashboard, colors, interactive prompts +# NSS-Switch — ASH compatible, BusyBox v1.37+ +# Features: dynamic terminal size, btop-style layout, proper Ctrl+C handling + +# ─── Column width configuration (unified for all tables) ───────────────────── +# IPv6 max compressed: 39 chars + []:port = ~45-50 chars +# FIXED widths - NO dynamic calculation that truncates IPv6 +UI_SRC_WIDTH=40 +UI_DST_WIDTH=40 +UI_NUM_WIDTH=4 +UI_PROTO_WIDTH=6 +UI_IFACE_WIDTH=15 +UI_NSS_WIDTH=4 +UI_BYPASS_WIDTH=6 + + +# ─── Terminal geometry ──────────────────────────────────────────────────────── +ui_get_term_size() { + local sz + sz=$(stty size 2>/dev/null) + if [ -n "$sz" ]; then + TERM_ROWS=$(echo "$sz" | cut -d' ' -f1) + TERM_COLS=$(echo "$sz" | cut -d' ' -f2) + else + TERM_ROWS=24 + TERM_COLS=80 + fi + [ "$TERM_ROWS" -lt 10 ] && TERM_ROWS=10 + [ "$TERM_COLS" -lt 40 ] && TERM_COLS=40 +} + +# ─── ANSI escape shortcuts ──────────────────────────────────────────────────── +ui_cursor_home() { printf '\033[H'; } +ui_cursor_pos() { printf '\033[%d;%dH' "$1" "$2"; } +ui_cursor_hide() { printf '\033[?25l'; } +ui_cursor_show() { printf '\033[?25h'; } +ui_clear_screen() { printf '\033[2J'; } +ui_clear_screen_scrollback() { printf '\033[2J\033[3J'; } +ui_alt_screen_on() { printf '\033[?1049h'; } +ui_alt_screen_off() { printf '\033[?1049l'; } +ui_clear_eol() { printf '\033[K'; } +ui_clear_cursor_bellow() { printf '\033[J'; } + +# ─── Colors ─────────────────────────────────────────────────────────────────── +if [ -t 1 ]; then + ESC=$(printf '\033') + C_RED="${ESC}[0;31m" + C_GREEN="${ESC}[0;32m" + C_YELLOW="${ESC}[0;33m" + C_BLUE="${ESC}[0;34m" + C_MAGENTA="${ESC}[0;35m" + C_CYAN="${ESC}[0;36m" + C_BOLD="${ESC}[1m" + C_DIM="${ESC}[2m" + C_ITALIC="${ESC}[3m" + C_UNDER="${ESC}[4m" + C_INVERT="${ESC}[7m" + C_RESET="${ESC}[0m" + BG_DARK="${ESC}[48;2;12;18;28m" + BG_MED="${ESC}[48;2;22;32;48m" + BG_ACCENT="${ESC}[48;2;0;70;110m" + BG_GREEN="${ESC}[48;2;0;70;35m" + BG_RED="${ESC}[48;2;90;18;18m" + BG_YELLOW="${ESC}[48;2;80;60;0m" + FG_BRIGHT="${ESC}[38;2;200;225;255m" + FG_DIM="${ESC}[38;2;70;90;115m" + FG_ACCENT="${ESC}[38;2;60;190;255m" + FG_GREEN="${ESC}[38;2;70;210;110m" + FG_RED="${ESC}[38;2;255;90;90m" + FG_YELLOW="${ESC}[38;2;255;195;70m" + FG_ORANGE="${ESC}[38;2;255;135;35m" +else + C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' C_MAGENTA='' + C_CYAN='' C_BOLD='' C_DIM='' C_ITALIC='' C_UNDER='' C_INVERT='' C_RESET='' + BG_DARK='' BG_MED='' BG_ACCENT='' BG_GREEN='' BG_RED='' BG_YELLOW='' + FG_BRIGHT='' FG_DIM='' FG_ACCENT='' FG_GREEN='' FG_RED='' FG_YELLOW='' + FG_ORANGE='' +fi + +# ─── Box-drawing chars ──────────────────────────────────────────────────────── +BOX_TL='╔' BOX_TR='╗' BOX_BL='╚' BOX_BR='╝' +BOX_H='═' BOX_V='║' +BOX_ML='╠' BOX_MR='╣' +SLIM_TL='┌' SLIM_TR='┐' SLIM_BL='└' SLIM_BR='┘' +SLIM_H='─' SLIM_V='│' +SLIM_ML='├' SLIM_MR='┤' +TICK='✓' CROSS='✗' WARN_SYM='⚠' INFO_SYM='ℹ' ARROW='▶' DOT='•' +BAR_FULL='█' BAR_EMPTY='░' + +# ─── Internal: repeat char N times ─────────────────────────────────────────── +_rep() { + local char="$1" n="$2" i=0 + while [ "$i" -lt "$n" ]; do + printf '%s' "$char" + i=$((i+1)) + done +} + +# ─── Print helpers ──────────────────────────────────────────────────────────── +ui_info() { printf "${FG_ACCENT}${INFO_SYM}${C_RESET} %s\n" "$*"; } +ui_ok() { printf "${FG_GREEN}${TICK}${C_RESET} %s\n" "$*"; } +ui_warn() { printf "${FG_YELLOW}${WARN_SYM}${C_RESET} %s\n" "$*"; } +ui_error() { printf "${FG_RED}${CROSS}${C_RESET} %s\n" "$*" >&2; } +ui_debug() { printf "${C_DIM}[DBG] %s${C_RESET}\n" "$*"; } +ui_bold() { printf "${C_BOLD}%s${C_RESET}\n" "$*"; } + +ui_section() { + ui_get_term_size 2>/dev/null || true + local title="$*" w="${TERM_COLS:-80}" + printf "\n${FG_ACCENT}${SLIM_ML}" + _rep "$SLIM_H" 2 + printf " ${C_BOLD}${FG_BRIGHT}%s${C_RESET}${FG_ACCENT} " "$title" + local used=$(( ${#title} + 6 )) + local remain=$(( w - used - 1 )) + [ "$remain" -gt 0 ] && _rep "$SLIM_H" "$remain" + printf "${SLIM_MR}${C_RESET}\n" +} + + +# ─── Check terminal width ───────────────────────────────────────────────────── +ui_check_width() { + ui_get_term_size + local min_width=120 + + if [ $TERM_COLS -lt $min_width ]; then + ui_clear_screen + ui_cursor_home + + + printf " Buenísima idea pero mal aplicada aún..." + printf "Sólo debería ver esto cuando la shell es >120px!" + + # Loop hasta que el ancho sea suficiente + while [ $TERM_COLS -lt $min_width ]; do + sleep 1 + ui_get_term_size + done + + # Limpiar y continuar + ui_clear_screen + ui_cursor_home + return 0 + fi + + return 0 +} + + +# ─── Calculate total table width (sum of all columns + spaces) ──────────────── +ui_table_width() { + local num_spaces=6 + echo $(( UI_NUM_WIDTH + UI_PROTO_WIDTH + UI_SRC_WIDTH + UI_DST_WIDTH + UI_IFACE_WIDTH + UI_NSS_WIDTH + UI_BYPASS_WIDTH + num_spaces )) +} + +# ─── Separator line matching table width (not terminal width) ───────────────── +ui_sep() { + local w + w=$(ui_table_width) + printf "${FG_DIM}" + _rep "$SLIM_H" "$w" + printf "${C_RESET}\n" +} + +ui_kv() { + printf " ${C_DIM}%-22s${C_RESET} ${C_BOLD}%s${C_RESET}\n" "${1}:" "$2" +} + +# ─── Progress bar: value max width ─────────────────────────────────────────── +ui_progress_bar() { + local val="$1" max="$2" width="$3" label="${4:-}" + [ "$max" -le 0 ] && max=1 + local filled=$(( val * width / max )) + [ "$filled" -gt "$width" ] && filled="$width" + local empty=$(( width - filled )) + local pct=$(( val * 100 / max )) + local color="$FG_GREEN" + [ "$pct" -gt 60 ] && color="$FG_YELLOW" + [ "$pct" -gt 85 ] && color="$FG_RED" + printf "${C_DIM}[${C_RESET}${color}" + _rep "$BAR_FULL" "$filled" + printf "${C_DIM}" + _rep "$BAR_EMPTY" "$empty" + printf "]${C_RESET} ${C_BOLD}%3d%%${C_RESET}" "$pct" + [ -n "$label" ] && printf " ${C_DIM}%s${C_RESET}" "$label" +} + +# ─── Banner ─────────────────────────────────────────────────────────────────── +ui_banner() { + ui_get_term_size 2>/dev/null || true + local w="${TERM_COLS:-80}" inner + inner=$(( w - 2 )) + + printf "${FG_ACCENT}" + printf '%s' "$BOX_TL"; _rep "$BOX_H" "$inner"; printf '%s\n' "$BOX_TR" + + local l1="NSS-Switch v1.0" l2="Qualcomm NSS selective bypass" + local p1=$(( (inner - ${#l1}) / 2 )) + local p2=$(( (inner - ${#l2}) / 2 )) + + printf '%s' "$BOX_V" + _rep ' ' "$p1" + printf "${C_BOLD}${FG_BRIGHT}%s${C_RESET}${FG_ACCENT}" "$l1" + _rep ' ' "$(( inner - p1 - ${#l1} ))" + printf '%s\n' "$BOX_V" + + printf '%s' "$BOX_V" + _rep ' ' "$p2" + printf "${C_DIM}%s${C_RESET}${FG_ACCENT}" "$l2" + _rep ' ' "$(( inner - p2 - ${#l2} ))" + printf '%s\n' "$BOX_V" + + printf '%s' "$BOX_BL"; _rep "$BOX_H" "$inner"; printf '%s\n' "$BOX_BR" + printf "${C_RESET}" +} + +# ─── Header bar (usa el ancho de la tabla) ────────────────────────────── +ui_header_bar() { + local title="$1" subtitle="$2" right="$3" + local table_width + table_width=$(ui_table_width) + + # Truncate title if too long for table width + local max_title_len=$(( table_width / 3 )) + [ ${#title} -gt $max_title_len ] && title="${title:0:$((max_title_len-3))}..." + + local left_len=$(( ${#title} + ${#subtitle} + 3 )) + local right_len=${#right} + local pad=$(( table_width - left_len - right_len - 2 )) + [ "$pad" -lt 1 ] && pad=1 + + printf '%b%b%b %s%b %s%b' \ + "$BG_DARK" "$FG_ACCENT" "$C_BOLD" "$title" \ + "${C_RESET}${BG_DARK}${FG_DIM}" "$subtitle" "${C_RESET}${BG_DARK}" + _rep ' ' "$pad" + printf '%b%s %b\n' "$FG_BRIGHT" "$right" "$C_RESET" +} + + +# DEBUG PR-1 +# ─── Keybind hint bar ────────────────────────────── +ui_hint_bar() { + local hints="Ctrl+C / q exit • refresh every ${interval}s • Sorted by: ${sort_name} • 1-6: Sort column" + local table_width + table_width=$(ui_table_width) + + printf "${BG_DARK}${FG_DIM} %s" "$hints" + local used=$(( ${#hints} + 1 )) + local pad=$(( table_width - used )) + [ "$pad" -gt 0 ] && _rep ' ' "$pad" + printf "${C_RESET}${C_DIM}%b" "$(ui_clear_eol)" + printf "\n" +} +# ui_hint_bar() { +# local hints="$*" +# local table_width +# table_width=$(ui_table_width) +# +# printf "${BG_DARK}${FG_DIM} %s" "$hints" +# local used=$(( ${#hints} + 1 )) +# local pad=$(( table_width - used )) +# [ "$pad" -gt 0 ] && _rep ' ' "$pad" +# printf "${C_RESET}${C_DIM}%b" "$(ui_clear_eol)" +# printf "\n" +# } + +# ─── Watch stats panel ──────────────────────────────────────────────────────── +ui_watch_stats_panel() { + local total="$1" bypassed="$2" frontend="$3" engine="$4" + local rules="$5" interval="$6" + local normal=$(( total - bypassed )) + + ui_get_term_size + # ui_calc_column_widths + + printf "\n" + + # Connections line - use full width + printf " ${FG_BRIGHT}${C_BOLD}%-14s${C_RESET}" "Connections" + printf " ${FG_GREEN}${C_BOLD}%d${C_RESET} total" "$total" + printf " ${FG_ACCENT}${C_BOLD}%d${C_RESET} NSS/HW" "$normal" + printf " ${FG_ORANGE}${C_BOLD}%d${C_RESET} CPU-bypass" "$bypassed" + printf " " + if [ "$total" -gt 0 ]; then + local prog_width=$(( TERM_COLS - 70 )) + [ $prog_width -lt 10 ] && prog_width=10 + [ $prog_width -gt 30 ] && prog_width=30 + ui_progress_bar "$bypassed" "$total" $prog_width "bypass ratio" + else + printf "${C_DIM}no connections${C_RESET}" + fi + printf "\n" + + # ECM line + printf " ${FG_BRIGHT}${C_BOLD}%-14s${C_RESET}" "ECM" + local fe_color="$FG_GREEN" + case "$frontend" in SFE) fe_color="$FG_YELLOW" ;; UNKNOWN) fe_color="$FG_RED" ;; esac + printf " ${fe_color}${C_BOLD}%s${C_RESET}" "$frontend" + printf " ${C_DIM}engine=${C_RESET}${C_BOLD}%s${C_RESET}" "$engine" + printf " ${C_DIM}rules=${C_RESET}${FG_ACCENT}${C_BOLD}%s${C_RESET}" "$rules" + printf " ${C_DIM}refresh=${C_RESET}${C_BOLD}%ss${C_RESET}" "$interval" + printf "\n\n" +} + + + + +# ─── Connection table header ────────────────────────────────────────────────── +ui_conn_header() { + local total_width + total_width=$(ui_table_width) + + printf "${BG_MED}${FG_DIM}" + + printf " %-5s %-7s %-42s %-42s %-17s %-5s %-6s" \ + "NUM₁" "PROTO₂" "SOURCE₃" "DESTINATION₄" "INTERFACE₅" "NSS₆" "BYPASS" + printf "${C_RESET}\n" + + printf "${FG_DIM}" + _rep "$SLIM_H" "$total_width" + printf "${C_RESET}\n" +} + +# ─── Map port to service name (max 6 chars) ─────────────────────────────────── +_proto_to_service() { + local proto="$1" + local port="$2" + + # Si es ICMP, mantener como está + case "$proto" in + icmp|icmpv6) + echo "$proto" + return + ;; + esac + + # Traducir por puerto, el el futuro podría usarse /etc/procotols + case "$port" in + 22) echo "SSH" ;; + 25) echo "SMTP" ;; + 53) echo "DNS" ;; + 80) echo "HTTP" ;; + 110) echo "POP3" ;; + 123) echo "NTP" ;; + 143) echo "IMAP" ;; + 194) echo "IRC" ;; + 443) echo "HTTPS" ;; + 465) echo "SMTPS" ;; + 587) echo "SMTP" ;; + 853) echo "DNS" ;; + 993) echo "IMAPS" ;; + 995) echo "POP3S" ;; + 1433) echo "MSSQL" ;; + 1723) echo "PPTP" ;; + 2082) echo "CPANEL" ;; + 2083) echo "CPANEL" ;; + 2086) echo "WHM" ;; + 2087) echo "WHM" ;; + 2095) echo "WEBMAIL" ;; + 2096) echo "WEBMAIL" ;; + 3306) echo "MYSQL" ;; + 3389) echo "RDP" ;; + 5060) echo "SIP" ;; + 5061) echo "SIPS" ;; + 5222) echo "XMPP" ;; + 5228) echo "GCM" ;; + 5432) echo "PG" ;; + 5900) echo "VNC" ;; + 5901) echo "VNC" ;; + 5902) echo "VNC" ;; + 5903) echo "VNC" ;; + 5904) echo "VNC" ;; + 5905) echo "VNC" ;; + 5906) echo "VNC" ;; + 5907) echo "VNC" ;; + 5908) echo "VNC" ;; + 5909) echo "VNC" ;; + 5910) echo "VNC" ;; + 5911) echo "VNC" ;; + 5912) echo "VNC" ;; + 5913) echo "VNC" ;; + 5914) echo "VNC" ;; + 5915) echo "VNC" ;; + 6379) echo "REDIS" ;; + 6666) echo "IRC" ;; + 6667) echo "IRCssl" ;; + 8080) echo "HTTP" ;; + 8443) echo "HTTPS" ;; + 8888) echo "HTTP" ;; + 9000) echo "PHP" ;; + 9090) echo "HTTP" ;; + 9100) echo "PJL" ;; + 9200) echo "ES" ;; + 9418) echo "GIT" ;; + 11211) echo "MEMCACHE" ;; + 27017) echo "MONGO" ;; + 27018) echo "MONGO" ;; + 27019) echo "MONGO" ;; + 51820) echo "WG" ;; + *) + echo "$proto" + ;; + esac +} + + +# ─── Connection row ─────────────────────────────────────────────────────────── +ui_conn_row() { + local num="$1" proto="$2" src="$3" dst="$4" + local iface="$5" nss="$6" bypass="$7" + + # Extraer puerto destino para la traducción + # Tal y como funciona nuestro parser NECESITAMOS partirlo en #, pero ante otros casos ... switch case + local dst_port + case "$dst" in + *'#'*) + dst_port="${dst##*#}" ;; + *']:'*) + dst_port="${dst##*]:}" ;; + *:*) + dst_port="${dst##*:}" ;; + *) + dst_port="" ;; + esac + + # Traducir protocolo a nombre de servicio (si aplica) + local service_name + service_name=$(_proto_to_service "$proto" "$dst_port") + + local proto_c="$C_RESET" + case "$service_name" in + HTTP|HTTPS) proto_c="$FG_GREEN" ;; + SSH|WG) proto_c="$FG_CYAN" ;; + DNS) proto_c="$FG_YELLOW" ;; + MYSQL|PG|MONGO|REDIS|MEMCACHE) proto_c="$FG_MAGENTA" ;; + *) + case "$proto" in + tcp) proto_c="$FG_ACCENT" ;; + udp) proto_c="$FG_YELLOW" ;; + icmp*) proto_c="$FG_ORANGE" ;; + *) proto_c="$C_RESET" ;; + esac + ;; + esac + + local nss_c + case "$nss" in + HW) nss_c="$FG_GREEN" ;; + SFE) nss_c="$FG_YELLOW" ;; + CPU) nss_c="$FG_RED" ;; + *) nss_c="$C_DIM" ;; + esac + + local byp_c byp_s + if [ "$bypass" = "YES" ]; then byp_c="$FG_ORANGE" byp_s="BYPASS" + else byp_c="$C_DIM" byp_s="-" + fi + + local iface_c="$C_RESET" + case "$iface" in local:*) iface_c="$C_DIM" ;; esac + + # Alternate row bg + local row_bg="" + [ $(( num % 2 )) -eq 0 ] && row_bg="$BG_MED" + + printf '%b %b%-*s%b%b %b%-*s%b%b' \ + "$row_bg" "$C_BOLD" "$UI_NUM_WIDTH" "$num" "$C_RESET" "$row_bg" \ + "$proto_c" "$UI_PROTO_WIDTH" "$service_name" "$C_RESET" "$row_bg" + printf " %-*s %-*s " "$UI_SRC_WIDTH" "$src" "$UI_DST_WIDTH" "$dst" + printf '%b%-*s%b%b %b%-*s%b%b %b%-*s%b\n' \ + "$iface_c" "$UI_IFACE_WIDTH" "$iface" "$C_RESET" "$row_bg" \ + "$nss_c" "$UI_NSS_WIDTH" "$nss" "$C_RESET" "$row_bg" \ + "$byp_c" "$UI_BYPASS_WIDTH" "$byp_s" "$C_RESET" +} + +# ─── Rule list header/row ───────────────────────────────────────────────────── +ui_rule_header() { + printf "${BG_MED}${FG_DIM}" + printf " %-4s %-6s %-${UI_SRC_WIDTH}s %-${UI_DST_WIDTH}s %-7s %-7s %-12s %-8s %s" \ + "ID" "PROTO" "SRC_IP" "DST_IP" "SPORT" "DPORT" "IFACE" "PERSIST" "COMMENT" + printf "${C_RESET}\n" + ui_sep +} + +ui_rule_row() { + + # echo "DEBUG: id='$1' proto='$2' src='$3' dst='$4'" >&2 + + local id="$1" proto="$2" src="$3" dst="$4" + local sport="$5" dport="$6" iface="$7" persist="$8" comment="$9" + + local pc="$C_DIM" ps="temp" + [ "$persist" = "yes" ] && pc="$FG_GREEN" ps="persist" + local row_bg="" + [ $(( id % 2 )) -eq 0 ] && row_bg="$BG_MED" + + # DEBUG + local src_fmt="%-${UI_SRC_WIDTH}s" + local dst_fmt="%-${UI_DST_WIDTH}s" + + printf "%b %b%-4s%b%b %b%-6s%b%b ${src_fmt} ${dst_fmt} %-7s %-7s %-12s %b%-8s%b%b %b%s%b\n" \ + "$row_bg" \ + "$FG_ACCENT" "$C_BOLD" "$id" "$C_RESET" "$row_bg" \ + "$FG_YELLOW" "$proto" "$C_RESET" "$row_bg" \ + "$src" "$dst" \ + "$sport" "$dport" "$iface" \ + "$pc" "$ps" "$C_RESET" "$row_bg" \ + "$C_DIM" "$comment" "$C_RESET" +} + +# DEBUG +# viene de rules.sh -> rules_list() ... subyacente de ui_rule_row() +# Si funciona bien, lo exportaremos a watch y pick +ui_rule_row_dynamic() { + local rule_id="$1" proto="$2" src="$3" dst="$4" + local sport="$5" dport="$6" iface="$7" persist="$8" comment="$9" + local max_id="${10}" max_proto="${11}" max_src="${12}" max_dst="${13}" + local max_sport="${14}" max_dport="${15}" max_iface="${16}" max_persist="${17}" + + # Configurar colores para persist + local pc="${C_MAGENTA}" ps="temp" + [ "$persist" = "yes" ] && pc="${FG_GREEN}${C_BOLD}" ps="persist" + local row_bg="" + [ $(( rule_id % 2 )) -eq 0 ] && row_bg="$BG_MED" + + local id_fmt="%-${max_id}s" + local proto_fmt="%-${max_proto}s" + local src_fmt="%-${max_src}s" + local dst_fmt="%-${max_dst}s" + local sport_fmt="%-${max_sport}s" + local dport_fmt="%-${max_dport}s" + local iface_fmt="%-${max_iface}s" + local persist_fmt="%-${max_persist}s" + + printf '%b' "$row_bg" + + # ID con color amarillo bold + printf " ${FG_YELLOW}${C_BOLD}" + printf "${id_fmt}" "$rule_id" + printf "${C_RESET}" + + # PROTO sin color + printf " ${proto_fmt}" "$proto" + + # SRC sin color + printf " ${src_fmt}" "$src" + + # DST sin color + printf " ${dst_fmt}" "$dst" + + # SPORT sin color + printf " ${sport_fmt}" "$sport" + + # DPORT sin color + printf " ${dport_fmt}" "$dport" + + # IFACE sin color + printf " ${iface_fmt}" "$iface" + + # PERSIST: temp en magenta dim, persist en green bold + printf " %b" "$pc" + printf "${persist_fmt}" "$ps" + printf "${C_RESET}" + + # COMMENT con color gris + printf " %b%s%b\n" "$C_BLUE" "$comment" "$C_RESET" +} + + +# ─── Ctrl+C safe: write dump to tmpfile before rendering ───────────────────── +# The core problem in ash pipes: `cmd | while read` puts 'while' in a subshell. +# Ctrl+C kills the subshell but the parent continues. Solution: tmpfile. +# Pattern: +# tmpfile=$(mktemp /tmp/nss-watch.XXXXXX) +# ct_dump_all_full > "$tmpfile" # dump outside the rendering loop +# while IFS='|' read ... < "$tmpfile" # read from file, no subshell +# rm -f "$tmpfile" + +# ─── Watch exit flag management ─────────────────────────────────────────────── +_UI_EXIT=0 + +ui_watch_init() { + _UI_EXIT=0 + # ui_alt_screen_on + ui_cursor_hide +} + +ui_watch_cleanup() { + ui_cursor_show # Mostrar cursor + ui_clear_screen_scrollback # Limpia screen y scrollback + ui_cursor_home # Ir a home + rm -f /tmp/nss-switch-pick.* /tmp/nss-switch-watch.* 2>/dev/null + rm -f /tmp/nss-iface.* 2>/dev/null +} + +# ─── Pick display: show ALL connections, normal terminal mode ───────────────── +# NO alt_screen - terminal scroll works normally +ui_pick_display_normal() { + local tmpfile="$1" total="$2" + ui_get_term_size + + while true; do + ui_clear_screen + ui_cursor_home + + ui_header_bar "NSS-Switch" "Connection Picker" "$(date +'%H:%M:%S')" + ui_hint_bar "${ARROW} q=cancel • type number + Enter to select — ${total} connections total" + printf "\n" + + ui_conn_header + + # Read entire file - show ALL connections + local _display_tmp + _display_tmp=$(mktemp /tmp/nss-display.XXXXXX) + cat "$tmpfile" > "$_display_tmp" + + while IFS='|' read -r n proto src dst iface nss bypass mark state; do + local sip sp dip dp ss ds + sip=$(echo "$src" | cut -d'#' -f1) + sp=$(echo "$src" | cut -d'#' -f2) + dip=$(echo "$dst" | cut -d'#' -f1) + dp=$(echo "$dst" | cut -d'#' -f2) + if echo "$sip" | grep -q ":"; then ss="[${sip}]:${sp}"; else ss="${sip}:${sp}"; fi + if echo "$dip" | grep -q ":"; then ds="[${dip}]:${dp}"; else ds="${dip}:${dp}"; fi + ui_conn_row "$n" "$proto" "$ss" "$ds" "$iface" "$nss" "$bypass" + done < "$_display_tmp" + rm -f "$_display_tmp" + + ui_sep + + # Warning if terminal too narrow for IPv6 + if [ $TERM_COLS -lt 120 ] && [ $total -gt 0 ]; then + printf " ${FG_YELLOW}${WARN_SYM}${C_RESET} ${C_DIM}Terminal narrow (${TERM_COLS}c), IPv6 addresses may be truncated.${C_RESET}\n" + fi + + printf " ${C_DIM}Total connections: %d${C_RESET}\n" "$total" + printf "\n ${FG_ACCENT}${ARROW}${C_RESET} ${C_BOLD}Enter connection number (or q to cancel):${C_RESET} " + + # Simple read - no filtering, just read normally + # In normal terminal mode, PgUp/PgDown will NOT be captured - they will scroll + read -r _input + + # Clean the input - remove any non-digit and non-q characters + local _cleaned="" + _cleaned=$(printf '%s' "$_input" | tr -d -c '0-9qQ') + _cleaned=$(echo "$_cleaned" | tr 'Q' 'q') + + case "$_cleaned" in + q|'') + return 1 + ;; + [0-9]*) + if [ "$_cleaned" -ge 1 ] 2>/dev/null && [ "$_cleaned" -le "$total" ] 2>/dev/null; then + UI_NUM="$_cleaned" + return 0 + else + printf " ${FG_RED}${CROSS}${C_RESET} Out of range [1-%d] — press Enter to continue" "$total" + read -r _dummy + fi + ;; + *) + if [ -n "$_cleaned" ]; then + printf " ${FG_RED}${CROSS}${C_RESET} Invalid input '%s' — press Enter to continue" "$_cleaned" + else + printf " ${FG_RED}${CROSS}${C_RESET} Invalid input — press Enter to continue" + fi + read -r _dummy + ;; + esac + done +} + +# ─── Ask yes/no ─────────────────────────────────────────────────────────────── +ui_ask_yn() { + local question="$1" default="${2:-n}" hint ans + local valid=0 + + # Construir hint visual + case "$default" in + y|Y) hint="${C_BOLD}Y${C_RESET}/${C_DIM}n${C_RESET}" ;; + n|N) hint="${C_DIM}y/${C_RESET}${C_BOLD}N${C_RESET}" ;; + *) hint="y/n" ;; + esac + + while [ $valid -eq 0 ]; do + printf " ${FG_ACCENT}${ARROW}${C_RESET} ${C_BOLD}%s${C_RESET} [%b]: " "$question" "$hint" + read -r ans + + # Permitir salir con q/Q + case "$ans" in + q|Q|quit|exit|cancel) + ui_warn "Operation cancelled by user" + exit 0 + ;; + esac + + # Si está vacío, usar default + [ -z "$ans" ] && ans="$default" + + # Validar respuesta + case "$ans" in + y|Y|yes|YES|Yes) + return 0 + ;; + n|N|no|NO|No) + return 1 + ;; + *) + ui_error "Invalid answer: '$ans', please answer 'yes' or 'no' (or 'q' to cancel)" + ;; + esac + done +} + +# ─── Ask with option list ───────────────────────────────────────────────────── +ui_ask_choice() { + local question="$1"; shift + local i=1 opt ans + printf "\n ${C_BOLD}%s${C_RESET}\n" "$question" + for opt in "$@"; do + printf " ${FG_ACCENT}%d)${C_RESET} %s\n" "$i" "$opt" + i=$((i+1)) + done + printf " ${FG_ACCENT}${ARROW}${C_RESET} ${C_BOLD}Choice [1-%d]:${C_RESET} " "$(($#))" + read -r ans + if ! echo "$ans" | grep -qE '^[0-9]+$'; then + ui_error "Invalid choice"; UI_CHOICE=""; return 1 + fi + i=1 + for opt in "$@"; do + [ "$i" = "$ans" ] && { UI_CHOICE="$opt"; return 0; } + i=$((i+1)) + done + ui_error "Choice out of range"; UI_CHOICE=""; return 1 +} + + +# DEBUG PR-1 +# ─── Ask free text ──────────────────────────────────────────────────────────── +ui_ask_input() { + local question="$1" default="$2" type="${3:-string}" + local input + + while true; do + printf " ${FG_ACCENT}${ARROW}${C_RESET} ${C_BOLD}%s${C_RESET} ${C_DIM}[%s]${C_RESET}: " "$question" "$default" + read -r input + [ -z "$input" ] && input="$default" + + case "$type" in + port) + if [ "$input" = "any" ] || (echo "$input" | grep -qE '^[0-9]{1,5}$' && [ "$input" -ge 1 ] && [ "$input" -le 65535 ]); then + UI_INPUT="$input" + return 0 + fi + ui_error "Invalid port: $input (must be 1-65535 or 'any')" + ;; + ip) + # Limpiar corchetes si existen + input=$(echo "$input" | sed 's/^\[//;s/\]$//') + + # Si es IPv6 y tiene menos de 8 grupos y no tiene ::, añadir :: al final + if echo "$input" | grep -q ":" && ! echo "$input" | grep -q "::"; then + local groups=$(echo "$input" | tr ':' '\n' | grep -c . 2>/dev/null) + if [ -n "$groups" ] && [ "$groups" -lt 8 ]; then + # Completar con :: al final + input="${input}::" + fi + fi + + if [ "$input" = "any" ] || nft_validate_ip "$input"; then + UI_INPUT="$input" + return 0 + fi + ui_error "Invalid IP/CIDR: $input" + ;; + proto) + if [ "$input" = "any" ] || nft_validate_proto "$input"; then + UI_INPUT="$input" + return 0 + fi + ui_error "Invalid protocol: $input (tcp, udp, icmp, icmpv6, any)" + ;; + *) + UI_INPUT="$input" + return 0 + ;; + esac + done +} + +# ─── Ask numeric ────────────────────────────────────────────────────────────── +ui_ask_num() { + local question="$1" min="$2" max="$3" + printf "\n ${FG_ACCENT}${ARROW}${C_RESET} ${C_BOLD}%s${C_RESET} ${C_DIM}[%d-%d]${C_RESET}: " \ + "$question" "$min" "$max" + read -r UI_NUM + if [ -z "$UI_NUM" ]; then ui_error "Cancelled"; return 1; fi + if ! echo "$UI_NUM" | grep -qE '^[0-9]+$'; then + ui_error "Not a number: $UI_NUM"; return 1 + fi + if [ "$UI_NUM" -lt "$min" ] || [ "$UI_NUM" -gt "$max" ]; then + ui_error "Out of range [$min-$max]"; return 1 + fi + return 0 +} + +# ─── Confirm ───────────────────────────────────────────────────────────────── +ui_confirm() { ui_ask_yn "$1" "n"; } + + +# ─── Spinner ────────────────────────────────────────────────────────────────── +ui_spinner_start() { + export _SPINNER_MSG="$1" _SPINNER_PID="" + ( + local i=0 + while true; do + local c color + case $((i % 4)) in + 0) c='⠋' ; color="$FG_ACCENT" ;; + 1) c='⠙' ; color="$FG_GREEN" ;; + 2) c='⠸' ; color="$FG_YELLOW" ;; + 3) c='⠴' ; color="$FG_BRIGHT" ;; + esac + printf "\r ${color}%s${C_RESET} ${C_DIM}%s${C_RESET} " "$c" "$_SPINNER_MSG" + i=$((i + 1)) + sleep 1 + done + ) & + _SPINNER_PID=$! +} + +ui_spinner_stop() { + if [ -n "$_SPINNER_PID" ]; then + kill "$_SPINNER_PID" 2>/dev/null + wait "$_SPINNER_PID" 2>/dev/null + printf "\r%${TERM_COLS:-80}s\r" "" + _SPINNER_PID="" + fi +} diff --git a/net/nss-switch/files/usr/lib/nss-switch/nss-switch.sh b/net/nss-switch/files/usr/lib/nss-switch/nss-switch.sh new file mode 100755 index 0000000000000..ce3d14de61bdd --- /dev/null +++ b/net/nss-switch/files/usr/lib/nss-switch/nss-switch.sh @@ -0,0 +1,910 @@ +#!/usr/bin/env ash +# nss-switch.sh — Qualcomm NSS selective bypass manager +# /usr/lib/nss-switch/nss-switch.sh symlinked to /usr/bin/nss-switch +# ASH compatible — BusyBox v1.37+ +# Usage: nss-switch [options] + +# set -e + +# ─── Resolve our own directory ──────────────────────────────────────────────── +SELF_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)" + +# ─── UCI cinfig handler ──────────────────────── +uci_load_config() { + local section="settings" + local uci_file="/etc/config/nss-switch" + + if [ ! -f "$uci_file" ]; then + uci -q set nss-switch.$section=settings + uci -q set nss-switch.$section.persist_default="no" + uci -q set nss-switch.$section.nss_mark="0x00010000" + uci -q set nss-switch.$section.nss_mark_mask="0x00010000" + uci -q set nss-switch.$section.ecm_debugfs="/sys/kernel/debug/ecm" + uci -q set nss-switch.$section.nft_table="inet fw4" + uci -q set nss-switch.$section.nft_chain_pre="nss_bypass_pre" + uci -q set nss-switch.$section.nft_chain_post="nss_bypass_post" + uci -q set nss-switch.$section.rules_file="/etc/nss-switch/rules.conf" + uci -q set nss-switch.$section.debug_log="/tmp/nss-switch.log" + uci -q set nss-switch.$section.fw_script="/etc/firewall.d/nss-bypass-rules" + uci -q set nss-switch.$section.watch_interval="3" + uci -q set nss-switch.$section.debug_mode="no" + uci commit nss-switch + dbg "Created default UCI config: $uci_file" + fi + + PERSIST_DEFAULT=$(uci -q get nss-switch.$section.persist_default || echo "no") + NSS_MARK=$(uci -q get nss-switch.$section.nss_mark || echo "0x00010000") + NSS_MARK_MASK=$(uci -q get nss-switch.$section.nss_mark_mask || echo "0x00010000") + ECM_DEBUGFS=$(uci -q get nss-switch.$section.ecm_debugfs || echo "/sys/kernel/debug/ecm") + NFT_TABLE=$(uci -q get nss-switch.$section.nft_table || echo "inet fw4") + NFT_CHAIN_PRE=$(uci -q get nss-switch.$section.nft_chain_pre || echo "nss_bypass_pre") + NFT_CHAIN_POST=$(uci -q get nss-switch.$section.nft_chain_post || echo "nss_bypass_post") + RULES_FILE=$(uci -q get nss-switch.$section.rules_file || echo "/etc/nss-switch/rules.conf") + DEBUG_LOG=$(uci -q get nss-switch.$section.debug_log || echo "/tmp/nss-switch.log") + FW_SCRIPT=$(uci -q get nss-switch.$section.fw_script || echo "/etc/firewall.d/nss-bypass-rules") + WATCH_INTERVAL=$(uci -q get nss-switch.$section.watch_interval || echo "3") + DEBUG_MODE=$(uci -q get nss-switch.$section.debug_mode || echo "no") +} + +uci_load_config + +# ─── Debug logging helper (available before libs load) ──────────────────────── +dbg() { + # Solo proceder si debug está activado + if [ "${DEBUG:-0}" != "1" ] && [ "$DEBUG_MODE" != "yes" ]; then + return 0 + fi + + [ -f "$DEBUG_LOG" ] || return 0 + + local ts + ts=$(date '+%Y-%m-%d %H:%M:%S') + printf "[DBG ] %s\n" "$*" >&2 + printf "%s [DBG] %s\n" "$ts" "$*" >> "$DEBUG_LOG" 2>/dev/null || true +} + +# ─── Load libraries ─────────────────────────────────────────────────────────── +for lib in chandler ui ecm conntrack nft detect rules; do + lib_file="$SELF_DIR/lib/${lib}.sh" + if [ ! -f "$lib_file" ]; then + echo "[ERR ] Missing lib: $lib_file" >&2 + exit 1 + fi + . "$lib_file" +done +# ─── Load debug monitor ──────────────────────── +DEBUG_LIB="$SELF_DIR/lib/debug.sh" +if [ -f "$DEBUG_LIB" ]; then + . "$DEBUG_LIB" +fi + +# ─── Root check ─────────────────────────────────────────────────────────────── +check_root() { + [ "$(id -u)" = "0" ] || { ui_error "Must be run as root"; exit 1; } +} + +# ─── Log every invocation ──────────────────────────────────────────────────── +dbg "Invoked: $0 $*" + +# ─── Clean tmp files ───────────────────────────────────────────────────────── +_clean_tmp() { + rm -f /tmp/nss-switch-pick.* 2>/dev/null + rm -f /tmp/nss-switch-watch.* 2>/dev/null + rm -f /tmp/nss-ifmap.* 2>/dev/null + rm -f /tmp/nss-switch-exit.* 2>/dev/null + rm -f /tmp/nss-iface.* 2>/dev/null + rm -f /tmp/nss-display.* 2>/dev/null + rm -f /tmp/nss-page.* 2>/dev/null +} +trap '_clean_tmp' EXIT + +# ─── COMMAND: watch ─────────────────────────────────────────────────────────── +cmd_watch() { + check_root + + # PENDING + # ui_check_width + + local sort_mode=1 + local interval="${1:-$WATCH_INTERVAL}" + local once=0 + [ "$1" = "--once" ] && { once=1; interval=0; } + + # ── Ctrl+C fix ────────────────────────────────────────────────────────── + local _watch_exit=0 + local _watch_tmp + _watch_tmp=$(mktemp /tmp/nss-switch-watch.XXXXXX) + + # Trap: clean up and exit + trap ' + _watch_exit=1 + # Show cursor again + ui_cursor_show + # ui_watch_cleanup + rm -f "$_watch_tmp" + rm -f /tmp/nss-iface.* 2>/dev/null + printf "\n" + trap - INT TERM + exit 0 + ' INT TERM + + # HAVE cursor (but NO alt_screen) + ui_cursor_show + + # Helper: flush all pending input from stdin (non-blocking) + _flush_input() { + local dummy + while read -s -t 0 -n 1 dummy 2>/dev/null; do + : # consume and discard + done + } + + # ── First run: show loading indicator ─────────────────────────────────── + ui_clear_screen + ui_cursor_home + ui_header_bar "NSS-Switch" "NSS Conntrack Live Monitor" "$(date +'%a %d %b %H:%M:%S')" + printf "\n" + ui_spinner_start "Loading connections ..." + + # Dump outside any pipe — fills tmpfile, no subshell issues + ct_dump_all_full > "$_watch_tmp" 2>/dev/null + + ui_spinner_stop + printf "\n" + + local total bypassed rules + total=$(ct_count) + bypassed=$(ct_count_bypassed) + rules=$(rules_count 2>/dev/null || echo 0) + + # Subfunction to render the full display (with optional sorting) + _render_watch() { + local input_file="$1" + local current_sort="$2" + + # Map sort mode to readable name + local sort_name="" + case "$current_sort" in + 1) sort_name="ID" ;; + 2) sort_name="PROTO" ;; + 3) sort_name="SRC" ;; + 4) sort_name="DST" ;; + 5) sort_name="IFACE" ;; + 6) sort_name="NSS" ;; + *) sort_name="ID" ;; + esac + + # Apply sorting if a sort mode is specified + local display_file="$input_file" + if [ -n "$current_sort" ] && [ "$current_sort" -ge 1 ] && [ "$current_sort" -le 6 ]; then + local sorted_tmp="/tmp/nss-watch-sorted.$$" + case "$current_sort" in + 1) sort -t'|' -k1 -n "$input_file" > "$sorted_tmp" ;; + 2) sort -t'|' -k2 "$input_file" > "$sorted_tmp" ;; + 3) sort -t'|' -k3 "$input_file" > "$sorted_tmp" ;; + 4) sort -t'|' -k4 "$input_file" > "$sorted_tmp" ;; + 5) sort -t'|' -k5 "$input_file" > "$sorted_tmp" ;; + 6) sort -t'|' -k6 "$input_file" > "$sorted_tmp" ;; + esac + display_file="$sorted_tmp" + fi + + ui_clear_screen_scrollback + ui_cursor_home + + ui_header_bar "NSS-Switch" "NSS Conntrack Live Monitor" "$(date +'%a %d %b %H:%M:%S')" + ui_hint_bar "Ctrl+C / q exit • refresh every ${interval}s • Sorted by: ${sort_name} • 1-6: Sort column" + + ui_watch_stats_panel "$total" "$bypassed" \ + "$(ecm_frontend)" "$(ecm_engine)" "$rules" "$interval" + + ui_conn_header + + while IFS='|' read -r n proto src dst iface nss bypass mark state; do + local sip sp dip dp ss ds + sip=$(echo "$src" | cut -d'#' -f1) + sp=$(echo "$src" | cut -d'#' -f2) + dip=$(echo "$dst" | cut -d'#' -f1) + dp=$(echo "$dst" | cut -d'#' -f2) + if echo "$sip" | grep -q ":"; then ss="[${sip}]:${sp}"; else ss="${sip}:${sp}"; fi + if echo "$dip" | grep -q ":"; then ds="[${dip}]:${dp}"; else ds="${dip}:${dp}"; fi + ui_conn_row "$n" "$proto" "$ss" "$ds" "$iface" "$nss" "$bypass" + done < "$display_file" + + [ "$display_file" != "$input_file" ] && rm -f "$display_file" + + ui_sep + + local file_total + file_total=$(wc -l < "$input_file" 2>/dev/null || echo 0) + + local table_width + table_width=$(ui_table_width) + + if [ $TERM_COLS -lt $((table_width + 10)) ] && [ $file_total -gt 0 ]; then + printf "${FG_DIM}%*s${C_RESET}\n" "$table_width" " " + printf " ${FG_YELLOW}${WARN_SYM}${C_RESET} ${C_DIM}Terminal narrow (${TERM_COLS}c), long IPv6 addresses may be truncated. Use wider terminal (125c) for full visibility.${C_RESET}\n" + else + printf "${C_DIM}Total connections: %d • Use PgUp/PgDown or mouse to scroll${C_RESET}\n" "$file_total" + fi + + ui_clear_cursor_bellow + } + + # Render first time + _render_watch "$_watch_tmp" "$sort_mode" + + [ "$_watch_exit" -eq 1 ] && { ui_cursor_show; rm -f "$_watch_tmp"; rm -f /tmp/nss-iface.* 2>/dev/null; trap - INT TERM; return 0; } + [ "$once" -eq 1 ] && { ui_cursor_show; rm -f "$_watch_tmp"; rm -f /tmp/nss-iface.* 2>/dev/null; trap - INT TERM; return 0; } + + # ── Main loop (subsequent refreshes) ──────────────────────────────────── + while [ "$_watch_exit" -eq 0 ]; do + # Read one key with timeout (waits up to interval seconds) + key="" + read -s -t "$interval" -n 1 key 2>/dev/null + + # Process valid keys + case "$key" in + 1|2|3|4|5|6) + sort_mode="$key" + _render_watch "$_watch_tmp" "$sort_mode" + _flush_input + ;; + q|Q) + _watch_exit=1 + break + ;; + *) + if [ -n "$key" ]; then + _flush_input + fi + ;; + esac + + [ "$_watch_exit" -eq 1 ] && break + + ui_get_term_size + + ct_dump_all_full > "$_watch_tmp" 2>/dev/null + total=$(ct_count) + bypassed=$(ct_count_bypassed) + rules=$(rules_count 2>/dev/null || echo 0) + + _render_watch "$_watch_tmp" "$sort_mode" + + [ "$once" -eq 1 ] && break + done + + ui_cursor_show + rm -f "$_watch_tmp" + rm -f /tmp/nss-iface.* 2>/dev/null + trap - INT TERM +} + +# ─── COMMAND: pick ──────────────────────────────────────────────────────────── +cmd_pick() { + check_root + + local _pick_tmp + _pick_tmp=$(mktemp /tmp/nss-switch-pick.XXXXXX) + + local _selection_tmp + _selection_tmp=$(mktemp /tmp/nss-switch-selection.XXXXXX) + + # Trap for cleanup only... NO alt_screen tricks + trap ' + rm -f "$_pick_tmp" "$_selection_tmp" 2>/dev/null + rm -f /tmp/nss-iface.* 2>/dev/null + printf "\n" + # Ensure cursor is visible + ui_cursor_show + trap - INT TERM + exit 0 + ' INT TERM + + # NO ui_watch_init() that enables alt_screen NOT needed for pick + # Just clear screen and show the picker normally + + ui_clear_screen + ui_cursor_home + ui_header_bar "NSS-Switch" "Connection Picker" "$(date +'%H:%M:%S')" + printf "\n" + ui_spinner_start "Loading connections ..." + + ct_dump_all_full > "$_pick_tmp" 2>/dev/null + + ui_spinner_stop + printf "\n" + + local total + total=$(wc -l < "$_pick_tmp" 2>/dev/null || echo 0) + + if [ "$total" -eq 0 ]; then + rm -f "$_pick_tmp" "$_selection_tmp" 2>/dev/null + rm -f /tmp/nss-iface.* 2>/dev/null + trap - INT TERM + ui_warn "No connections found in conntrack" + return 0 + fi + + # Make a permanent copy for the selection phase + cp "$_pick_tmp" "$_selection_tmp" + + + # Display ALL connections (uses normal terminal mode, scroll works!) + if ! ui_pick_display_normal "$_pick_tmp" "$total"; then + rm -f "$_pick_tmp" "$_selection_tmp" 2>/dev/null + rm -f /tmp/nss-iface.* 2>/dev/null + trap - INT TERM + ui_warn "Cancelled" + return 0 + fi + + local sel="$UI_NUM" + + # Now read from the SELECTION tmpfile + local conn_line + conn_line=$(awk -F'|' -v n="$sel" '$1==n {print; exit}' "$_selection_tmp" 2>/dev/null) + # Clean up tmpfiles + rm -f "$_pick_tmp" "$_selection_tmp" 2>/dev/null + rm -f /tmp/nss-iface.* 2>/dev/null + trap - INT TERM + + if [ -z "$conn_line" ]; then + ui_error "Connection $sel not found" + return 1 + fi + + local num proto src dst iface nss bypass mark state + num=$(echo "$conn_line" | cut -d'|' -f1) + proto=$(echo "$conn_line" | cut -d'|' -f2) + src=$(echo "$conn_line" | cut -d'|' -f3) + dst=$(echo "$conn_line" | cut -d'|' -f4) + iface=$(echo "$conn_line" | cut -d'|' -f5) + nss=$(echo "$conn_line" | cut -d'|' -f6) + bypass=$(echo "$conn_line"| cut -d'|' -f7) + mark=$(echo "$conn_line" | cut -d'|' -f8) + state=$(echo "$conn_line" | cut -d'|' -f9) + + local src_ip src_port dst_ip dst_port + src_ip=$(echo "$src" | cut -d'#' -f1) + src_port=$(echo "$src" | cut -d'#' -f2) + dst_ip=$(echo "$dst" | cut -d'#' -f1) + dst_port=$(echo "$dst" | cut -d'#' -f2) + + ui_section "Selected Connection" + ui_kv "Protocol" "$proto" + ui_kv "Source" "$src_ip : $src_port" + ui_kv "Dest" "$dst_ip : $dst_port" + ui_kv "Interface" "$iface" + ui_kv "NSS state" "$nss" + ui_kv "Bypassed" "$bypass" + ui_sep + + ui_section "What should the bypass rule match on? " + ui_bold "You can combine multiple criteria. Answer each: " + printf "\n" + + local r_proto="any" r_src_ip="any" r_dst_ip="any" + local r_sport="any" r_dport="any" r_iface="any" + + # PRTOTOCOL + if ui_ask_yn "Match on protocol ($proto)?" y; then + r_proto="$proto" + fi + + # SRC-IP + if ui_ask_yn "Match on source IP ($src_ip)?" n; then + ui_ask_input "Enter source IP/CIDR (or press Enter to keep '$src_ip')" "$src_ip" "ip" + r_src_ip="$UI_INPUT" + fi + + # DST-IP + if ui_ask_yn "Match on destination IP ($dst_ip)?" n; then + ui_ask_input "Enter destination IP/CIDR (or press Enter to keep '$dst_ip')" "$dst_ip" "ip" + r_dst_ip="$UI_INPUT" + fi + + # PORT + if [ "$proto" = "tcp" ] || [ "$proto" = "udp" ]; then + if ui_ask_yn "Match on source port ($src_port)?" n; then + ui_ask_input "Source port" "$src_port" "port" + r_sport="$UI_INPUT" + fi + if ui_ask_yn "Match on destination port ($dst_port)?" n; then + ui_ask_input "Destination port" "$dst_port" "port" + r_dport="$UI_INPUT" + fi + fi + + # IFACE + if [ "$iface" != "?" ] && [ -n "$iface" ]; then + case "$iface" in + local:*) + local real_iface="${iface#local:}" + ui_warn "Router-generated traffic (not a LAN device)" + if ui_ask_yn "Match by output interface ($real_iface)?" y; then + r_iface="out:$real_iface" + fi + ;; + *) + if ui_ask_yn "Match on interface ($iface)?" n; then + r_iface="$iface" + fi + ;; + esac + fi + + # PERSISTANCE + local persist="$PERSIST_DEFAULT" + if ui_ask_yn "Make this rule persistent (survive reboot)?" "$([ "$PERSIST_DEFAULT" = "yes" ] && echo y || echo n)"; then + persist="yes" + else + persist="no" + fi + + # COMMENT + local default_comment="bypass from pick: $src_ip -> $dst_ip" + ui_ask_input "Comment for this rule" "$default_comment" "string" + local comment="$UI_INPUT" + + ui_section "Rule Preview" + ui_kv "Protocol" "$r_proto" + ui_kv "Src IP" "$r_src_ip" + ui_kv "Dst IP" "$r_dst_ip" + ui_kv "Src Port" "$r_sport" + ui_kv "Dst Port" "$r_dport" + ui_kv "Interface" "$r_iface" + ui_kv "Persistent" "$persist" + ui_kv "Comment" "$comment" + ui_sep + + rm -f /tmp/nss-iface.* 2>/dev/null + + # VALIDATE ALL + if ! rules_validate "$r_proto" "$r_src_ip" "$r_dst_ip" "$r_sport" "$r_dport" "$r_iface" "$comment"; then + ui_error "Validation failed — rule not added" + return 1 + fi + + ui_confirm "Apply this bypass rule?" || { ui_warn "Aborted"; return 0; } + + local new_id + new_id=$(rules_add "$r_proto" "$r_src_ip" "$r_dst_ip" \ + "$r_sport" "$r_dport" "$r_iface" "$persist" "$comment") + ui_ok "Rule $new_id saved" + + nft_apply + + ui_info "Flushing matched connections..." + ct_clear_rule_marks "$r_proto" "$r_src_ip" "$r_dst_ip" \ + "$r_sport" "$r_dport" "$r_iface" + + ui_ok "Done. Connection will be handled by CPU (not NSS) going forward." +} + +# ─── COMMAND: add ───────────────────────────────────────────────────────────── +cmd_add() { + check_root + local proto="any" src_ip="any" dst_ip="any" + local sport="any" dport="any" iface="any" + local persist="$PERSIST_DEFAULT" comment="manual rule" + local defunct_after=1 + + while [ $# -gt 0 ]; do + case "$1" in + --proto) proto="$2"; shift 2 ;; + --src-ip) src_ip="$2"; shift 2 ;; + --dst-ip) dst_ip="$2"; shift 2 ;; + --src-port) sport="$2"; shift 2 ;; + --dst-port) dport="$2"; shift 2 ;; + --iface) iface="$2"; shift 2 ;; + --persist) persist="yes"; shift ;; + --temp) persist="no"; shift ;; + --comment) comment="$2"; shift 2 ;; + --no-defunct) defunct_after=0; shift ;; + *) + ui_error "Unknown option: $1" + cmd_help + return 1 + ;; + esac + done + + ui_banner + ui_section "Add Manual Bypass Rule" + ui_kv "Protocol" "$proto" + ui_kv "Src IP" "$src_ip" + ui_kv "Dst IP" "$dst_ip" + ui_kv "Src Port" "$sport" + ui_kv "Dst Port" "$dport" + ui_kv "Interface" "$iface" + ui_kv "Persistent" "$persist" + ui_kv "Comment" "$comment" + [ "$defunct_after" = "0" ] && ui_kv "Defunct" "SKIP" + + # DEBUG PR1 + rules_validate "$proto" "$src_ip" "$dst_ip" "$sport" "$dport" "$iface" "$comment" || return 1 + + local new_id + new_id=$(rules_add "$proto" "$src_ip" "$dst_ip" "$sport" "$dport" "$iface" "$persist" "$comment") + ui_ok "Rule $new_id added to $RULES_FILE" + + nft_apply + + if [ "$defunct_after" = "1" ]; then + ui_info "Flushing matched connections..." + ct_clear_rule_marks "$proto" "$src_ip" "$dst_ip" "$sport" "$dport" "$iface" + else + ui_info "Skipped connection flush (--no-defunct)" + ui_info "New connections will be affected, existing ones will keep their state" + fi + + ui_ok "Bypass rule $new_id is active" +} + +# ─── COMMAND: list ──────────────────────────────────────────────────────────── +cmd_list() { + ui_banner + ui_section "Active NSS Bypass Rules" + rules_list + echo "" + ui_kv "Rules file" "$RULES_FILE" + ui_kv "Firewall script" "$FW_SCRIPT" + if nft_chains_exist 2>/dev/null; then + ui_ok "NSS-Switch chains are live in nftables" + else + ui_warn "NSS-Switch chains NOT in live ruleset — run: nss-switch apply" + fi +} + +# ─── COMMAND: remove ────────────────────────────────────────────────────────── +cmd_remove() { + + # DEBUG PR 1 + # echo "DEBUG: $# arguments: $@" >&2 + + check_root + local id="$1" + + # DEBUG PR 1 + # echo "DEBUG: id=$id" >&2 + + if [ -z "$id" ]; then + ui_error "Usage: nss-switch remove " + return 1 + fi + ui_banner + ui_section "Remove Rule $id" + + local line + line=$(rules_get "$id") || { ui_error "Rule $id not found"; return 1; } + rules_parse "$line" + ui_kv "ID" "$RULE_ID" + ui_kv "Proto" "$RULE_PROTO" + ui_kv "Src IP" "$RULE_SRC_IP" + ui_kv "Dst IP" "$RULE_DST_IP" + ui_kv "Sport" "$RULE_SPORT" + ui_kv "Dport" "$RULE_DPORT" + ui_kv "Iface" "$RULE_IFACE" + ui_kv "Comment" "$RULE_COMMENT" + + ui_confirm "Remove this rule?" || { ui_warn "Aborted"; return 0; } + + rules_remove "$id" + nft_apply + + ui_info "Clearing conntrack entries for this rule..." + ct_clear_rule_marks "$RULE_PROTO" "$RULE_SRC_IP" "$RULE_DST_IP" \ + "$RULE_SPORT" "$RULE_DPORT" "$RULE_IFACE" + + ui_info "Defuncting ECM so NSS can re-accelerate..." + ecm_defunct_all + + ui_ok "Rule $id removed. ECM will re-evaluate and re-accelerate those flows." +} + +# ─── COMMAND: flush ─────────────────────────────────────────────────────────── +cmd_flush() { + check_root + local mode="${1:---rules}" + ui_banner + ui_section "Flush NSS-Switch Rules" + + case "$mode" in + --rules) + ui_info "Removing all bypass rules from nftables (keeping rules.conf)" + ui_confirm "Continue?" || return 0 + # Clear rules.conf, regenerate (empty) script, reload + rules_clear + nft_apply + ecm_defunct_all + ui_ok "All rules flushed from nftables. ECM will re-accelerate all flows." + ;; + # DEBUG PR-1 + --all) + ui_warn "This removes ALL NSS-Switch configuration including persistent rules" + ui_confirm "Are you sure?" || return 0 + rules_clear + nft_apply + ecm_defunct_all + # Remove fw4 include + _nft_remove_uci_include + + # DEBUG PR-1 + rm -f "$FW_SCRIPT" 2>/dev/null || true + conntrack -D -m "$NSS_MARK" 2>/dev/null + + ui_ok "NSS-Switch fully removed. Reload firewall to clean live rules." + ;; + --temp) + ui_info "Removing only non-persistent (temporary) rules" + ui_confirm "Continue?" || return 0 + rules_clear_temp + nft_apply + ecm_defunct_all + ui_ok "Temporary rules flushed" + ;; + *) + ui_error "Usage: nss-switch flush [--rules|--all|--temp]" + return 1 + ;; + esac +} + +# ─── COMMAND: apply ─────────────────────────────────────────────────────────── +# Re-generate script and reload firewall (useful after manual edits) +cmd_apply() { + check_root + ui_banner + ui_info "Regenerating firewall script from rules.conf and reloading..." + nft_apply + ui_ok "Applied. Current rules:" + rules_list +} + +# ─── COMMAND: debug ─────────────────────────────────────────────────────────── +cmd_debug() { + local subcmd="${1:-env}" + shift 2>/dev/null || true + + ui_banner + case "$subcmd" in + env) + detect_check_all + ;; + ecm) + ecm_debug_dump + ;; + nft) + nft_show_our_rules + ;; + conntrack) + ct_debug_raw + ;; + mark) + ct_debug_mark + ;; + defunct-all) + check_root + ui_warn "This will defunct ALL connections in ECM — they will be re-evaluated" + ui_confirm "Proceed?" || return 0 + ecm_defunct_all + ;; + frontend-stop) + check_root + local fam="${1:-both}" + case "$fam" in + ipv4) ecm_stop_ipv4 ;; + ipv6) ecm_stop_ipv6 ;; + both) ecm_stop_ipv4; ecm_stop_ipv6 ;; + *) ui_error "Usage: debug frontend-stop [ipv4|ipv6|both]"; return 1 ;; + esac + ;; + frontend-restart) + check_root + ecm_restart + ui_ok "ECM service restarted" + ;; + log) + cmd_debug_log + ;; + log-clear) + if [ -f "$DEBUG_LOG" ]; then + > "$DEBUG_LOG" + ui_ok "Debug log cleared" + else + ui_warn "Debug log not active or not found" + fi + ;; + rules-raw) + ui_section "Raw rules.conf" + cat "$RULES_FILE" 2>/dev/null || ui_warn "No rules file" + ;; + script-raw) + ui_section "Raw generated firewall script" + cat "$FW_SCRIPT" 2>/dev/null || ui_warn "No script generated yet" + ;; + monitor) + cmd_debug_monitor "$@" + ;; + *) + ui_error "Unknown debug subcommand: $subcmd" + cmd_debug_help + return 1 + ;; + esac +} + +cmd_debug_help() { + printf "\n${C_BOLD}debug subcommands:${C_RESET}\n" + printf " %-25s %s\n" "env" "Full environment check" + printf " %-25s %s\n" "ecm" "ECM/NSS state dump" + printf " %-25s %s\n" "nft" "Show our live nftables chains" + printf " %-25s %s\n" "conntrack" "Dump raw /proc/net/nf_conntrack" + printf " %-25s %s\n" "mark" "Show conntrack entries with our bypass mark" + printf " %-25s %s\n" "defunct-all" "Force defunct ALL connections in ECM" + printf " %-25s %s\n" "frontend-stop [ipv4|ipv6|both]" "Stop NSS frontend(s)" + printf " %-25s %s\n" "frontend-restart" "Restart ECM service" + printf " %-25s %s\n" "log" "Enable/Disable debugging log" + printf " %-25s %s\n" "log-clear" "Clear debug log" + printf " %-25s %s\n" "rules-raw" "Show raw rules.conf content" + printf " %-25s %s\n" "script-raw" "Show raw generated firewall script" +} + +# ─── COMMAND: config ────────────────────────────────────────────────────────── +cmd_config() { + local key="$1" val="$2" + ui_banner + ui_section "NSS-Switch Configuration" + + # DEBUG PR-1 UCI show config + if [ -z "$key" ]; then + uci show nss-switch.settings 2>/dev/null | sed 's/^nss-switch\.settings\.//' + return 0 + fi + + # Set a config value + case "$key" in + PERSIST_DEFAULT) uci_key="persist_default" ;; + DEBUG_MODE) uci_key="debug_mode" ;; + WATCH_INTERVAL) uci_key="watch_interval" ;; + *) + ui_error "Unknown config key: $key" + ui_info "Valid keys: PERSIST_DEFAULT, DEBUG_MODE, WATCH_INTERVAL" + return 1 + ;; + esac + + uci set nss-switch.settings."$uci_key"="$val" + uci commit nss-switch + ui_ok "Set $key=$val" +} + +# ─── COMMAND: status ────────────────────────────────────────────────────────── +cmd_status() { + ui_banner + + ui_section "NSS-Switch Status" + local r_total r_temp r_persist + r_total=$(rules_count) + r_temp=$(grep -c '|no|' "$RULES_FILE" 2>/dev/null || echo 0) + r_persist=$(grep -c '|yes|' "$RULES_FILE" 2>/dev/null || echo 0) + + ui_kv "Rules defined" "$r_total (${r_persist} persist, ${r_temp} temp)" + ui_kv "Rules file" "$RULES_FILE" + + echo "" + local fe eng mark_avail + fe=$(ecm_frontend) + eng=$(ecm_engine) + mark_avail=$(ecm_mark_classifier_available && echo "AVAILABLE" || echo "MISSING") + local fe_color="$FG_GREEN" + case "$fe" in SFE) fe_color="$FG_YELLOW";; UNKNOWN) fe_color="$FG_RED";; esac + local mk_color="$FG_GREEN" + [ "$mark_avail" = "MISSING" ] && mk_color="$FG_RED" + + printf " ${C_DIM}%-22s${C_RESET} %b${C_BOLD}%s${C_RESET} ${C_DIM}engine=%s${C_RESET}\n" \ + "ECM frontend:" "$fe_color" "$fe" "$eng" + printf " ${C_DIM}%-22s${C_RESET} %b${C_BOLD}%s${C_RESET}\n" \ + "Mark classifier:" "$mk_color" "$mark_avail" + + echo "" + local ct_total ct_bypass + ct_total=$(ct_count) + ct_bypass=$(ct_count_bypassed) + ui_kv "Conntrack total" "$ct_total" + printf " ${C_DIM}%-22s${C_RESET} ${FG_ORANGE}${C_BOLD}%s${C_RESET}" "Bypassed (CPU):" "$ct_bypass" + if [ "$ct_total" -gt 0 ]; then + printf " " + local prog_width=20 + [ $TERM_COLS -gt 80 ] && prog_width=30 + ui_progress_bar "$ct_bypass" "$ct_total" $prog_width "of total" + fi + printf "\n" + + echo "" + if nft_chains_exist 2>/dev/null; then + ui_ok "NSS-Switch nft chains: ${FG_GREEN}LIVE${C_RESET}" + else + ui_warn "NSS-Switch nft chains: NOT ACTIVE — run: nss-switch apply" + fi + ui_kv "Our ct mark" "$NSS_MARK" + + ui_section "Active Rules" + rules_list +} + +# ─── HELP ───────────────────────────────────────────────────────────────────── +cmd_help() { + ui_banner + printf "\n${C_BOLD}Usage:${C_RESET} ${FG_ACCENT}nss-switch${C_RESET} ${C_BOLD}${C_RESET} [options]\n\n" + + printf "${C_BOLD}${FG_BRIGHT}Commands:${C_RESET}\n" + _help_cmd "watch [--once] [interval]" "Live connection monitor ${C_DIM}(btop-style, Ctrl+C to exit, use terminal scroll)${C_RESET}" + _help_cmd "pick" "Interactive: browse connections and bypass one" + _help_cmd "add [options]" "Manually add a bypass rule" + _help_cmd "list" "List all defined bypass rules" + _help_cmd "remove " "Remove a bypass rule by ID" + _help_cmd "flush [--rules|--all|--temp]" "Remove rules from nftables" + _help_cmd "apply" "Re-apply rules.conf to nftables" + _help_cmd "status" "Full status dashboard" + _help_cmd "config [KEY] [VALUE]" "View or set configuration" + _help_cmd "debug " "Debug and diagnostic tools" + printf "\n" + + printf "${C_BOLD}${FG_BRIGHT}add options:${C_RESET}\n" + _help_opt "--proto tcp|udp|icmp|any" "Match protocol" + _help_opt "--src-ip " "Match source IP or subnet" + _help_opt "--dst-ip " "Match destination IP or subnet" + _help_opt "--src-port " "Match source port (tcp/udp)" + _help_opt "--dst-port " "Match destination port" + _help_opt "--iface " "Match input interface (out: for egress)" + _help_opt "--persist" "Survive reboot" + _help_opt "--temp" "Temporary, lost on reboot (default)" + _help_opt "--comment " "Human-readable label" + _help_opt "--no-defunct" "Skip ECM defunct after adding" + printf "\n" + + printf "${C_BOLD}${FG_BRIGHT}Examples:${C_RESET}\n" + printf " ${FG_ACCENT}nss-switch add --iface lan2 --persist --comment 'Deco off NSS'${C_RESET}\n" + printf " ${FG_ACCENT}nss-switch add --src-ip 192.168.1.50 --comment 'PC off NSS'${C_RESET}\n" + printf " ${FG_ACCENT}nss-switch add --proto tcp --dst-port 22 --temp${C_RESET}\n" + printf " ${FG_ACCENT}nss-switch watch${C_RESET}\n" + printf " ${FG_ACCENT}nss-switch pick${C_RESET}\n" + printf " ${FG_ACCENT}nss-switch debug env${C_RESET}\n" + printf "\n" + cmd_debug_help + printf "\n" +} + +_help_cmd() { + printf " ${FG_ACCENT}${C_BOLD}%-36s${C_RESET} %b\n" "$1" "$2" +} + +_help_opt() { + printf " ${C_DIM}%-32s${C_RESET} %s\n" "$1" "$2" +} + +# ─── MAIN DISPATCHER ────────────────────────────────────────────────────────── +COMMAND="${1:-help}" +shift 1 2>/dev/null || true + +case "$COMMAND" in + watch) cmd_watch "$@" ;; + pick) cmd_pick "$@" ;; + add) cmd_add "$@" ;; + list) cmd_list "$@" ;; + remove|rm) cmd_remove "$@" ;; + flush) cmd_flush "$@" ;; + apply) cmd_apply "$@" ;; + status) cmd_status "$@" ;; + config) cmd_config "$@" ;; + debug) cmd_debug "$@" ;; + help|-h|--help) cmd_help ;; + *) + ui_error "Unknown command: $COMMAND" + cmd_help + exit 1 + ;; +esac diff --git a/net/nss-switch/src/Makefile b/net/nss-switch/src/Makefile new file mode 100644 index 0000000000000..704d4ddc45038 --- /dev/null +++ b/net/nss-switch/src/Makefile @@ -0,0 +1,10 @@ +# +# SPDX-License-Identifier: GPL-2.0-only +# +TARGET = nss-ct-dump +SRCS = nss-ct-dump.c +all: $(TARGET) +$(TARGET): $(SRCS) + $(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(SRCS) +clean: + rm -f $(TARGET) *.o diff --git a/net/nss-switch/src/nss-ct-dump.c b/net/nss-switch/src/nss-ct-dump.c new file mode 100644 index 0000000000000..9f604f4d54c32 --- /dev/null +++ b/net/nss-switch/src/nss-ct-dump.c @@ -0,0 +1,415 @@ +/* + * nss-ct-dump.c + */ + +#include +#include +#include +#include +#include + +#define CONNTRACK_FILE "/proc/net/nf_conntrack" + +/* DEBUG PR-1 NO hardcoded values, uci get instead */ +/* #define NSS_MARK 0x00010000 */ +#define MAX_LINE 2048 +#define CACHE_SIZE 256 + +/* DEBUG PR-1 NO hardcoded values, uci get instead */ +static unsigned int NSS_MARK_GLOBAL = 0x00010000; + + + +/* Cache para IP -> interfaz */ +typedef struct { + char ip[64]; + char iface[32]; + int valid; +} iface_cache_t; + +static iface_cache_t cache[CACHE_SIZE]; +static int cache_idx = 0; + +/* DEBUG: Cache para estado */ +static int cached_nss_available = -1; +static int cached_sfe_available = -1; + +/* headers */ +void compress_ipv6(char *dest, const char *src); +char* get_iface_for_ip(const char *ip); +int is_bypassed(unsigned int mark); +const char* get_nss_state(unsigned int mark); +void parse_conntrack_line(const char *line, char *proto, char *src_ip, unsigned int *src_port, char *dst_ip, unsigned int *dst_port, unsigned int *mark, char *state); +/* DEBUG PR-1 NO hardcoded values, uci get instead */ +unsigned int get_nss_mark_from_uci(void); +/* unsigned int get_nss_mark_mask_from_uci(void); */ +/* unsigned int parse_mark(const char *line); */ +/* int is_router_local(const char *ip); */ + +/* DEBUG PR-1 NO hardcoded values, uci get instead */ +/* Get NSS_MARK FROM UCI */ +unsigned int get_nss_mark_from_uci(void) { + FILE *fp; + char line[256]; + unsigned int mark = 0x00010000; + + fp = popen("uci -q get nss-switch.settings.nss_mark 2>/dev/null", "r"); + if (fp) { + if (fgets(line, sizeof(line), fp) != NULL) { + line[strcspn(line, "\n")] = '\0'; + if (strncmp(line, "0x", 2) == 0) { + mark = strtoul(line, NULL, 16); + } else { + mark = strtoul(line, NULL, 0); + } + } + pclose(fp); + } + + return mark; +} + + + +/* Compress IPv6 */ +void compress_ipv6(char *dest, const char *src) { + char temp[128]; + char *token, *saveptr; + char result[128] = ""; + int groups[8] = {0}; + int group_count = 0; + int i; + int max_len = 0, max_start = -1; + int cur_len = 0, cur_start = -1; + int compressed = 0; + + if (strchr(src, ':') == NULL) { + strcpy(dest, src); + return; + } + + strcpy(temp, src); + + token = strtok_r(temp, ":", &saveptr); + while (token && group_count < 8) { + groups[group_count++] = strtol(token, NULL, 16); + token = strtok_r(NULL, ":", &saveptr); + } + + for (i = 0; i < group_count; i++) { + if (groups[i] == 0) { + if (cur_len == 0) cur_start = i; + cur_len++; + if (cur_len > max_len) { + max_len = cur_len; + max_start = cur_start; + } + } else { + cur_len = 0; + } + } + + if (max_len > 1) { + for (i = 0; i < group_count; i++) { + if (i == max_start) { + strcat(result, "::"); + compressed = 1; + i += max_len - 1; + } else { + if (i > 0 && !compressed) strcat(result, ":"); + sprintf(result + strlen(result), "%x", groups[i]); + compressed = 0; + } + } + } else { + for (i = 0; i < group_count; i++) { + if (i > 0) strcat(result, ":"); + sprintf(result + strlen(result), "%x", groups[i]); + } + } + + strcpy(dest, result); +} + + +/* IP uiface< */ +char* get_iface_for_ip(const char *ip) { + static char iface[32] = "?"; + char cmd[256]; + FILE *fp; + int i; + + /* Buscar en cache */ + for (i = 0; i < cache_idx; i++) { + if (strcmp(cache[i].ip, ip) == 0 && cache[i].valid) { + return cache[i].iface; + } + } + + /* Si es IPv6, formato especial */ + if (strchr(ip, ':') != NULL) { + snprintf(cmd, sizeof(cmd), "ip -6 route get %s 2>/dev/null | grep -oE 'dev [^ ]+' | cut -d' ' -f2", ip); + /* Y si no, es IPv4 */ + } else { + snprintf(cmd, sizeof(cmd), "ip route get %s 2>/dev/null | grep -oE 'dev [^ ]+' | cut -d' ' -f2", ip); + } + + /* DEBUG PR-1: resetea valor SIEMPRE antes ed uso */ + strcpy(iface, "?"); + + + fp = popen(cmd, "r"); + if (fp) { + if (fgets(iface, sizeof(iface), fp) != NULL) { + iface[strcspn(iface, "\n")] = '\0'; + } + pclose(fp); + } + + if (strlen(iface) == 0 || strcmp(iface, "lo") == 0) { + /* Es tráfico hacia/desde el propio router */ + strcpy(iface, "lo"); + } + + if (strlen(iface) == 0) { + strcpy(iface, "?"); + } + + /* Guardar en cache para velocidad en las proximas, tal como en modo shell */ + if (cache_idx < CACHE_SIZE) { + strcpy(cache[cache_idx].ip, ip); + strcpy(cache[cache_idx].iface, iface); + cache[cache_idx].valid = 1; + cache_idx++; + } + + return iface; +} + + +/* Verifica bypass por mark */ +/* DEBUG PR-1 NO hardcoded values, uci get instead */ +int is_bypassed(unsigned int mark) { + return (mark & NSS_MARK_GLOBAL) != 0; +} + +/* Obtener offload (HW/SFE/CPU) - con cache para evitar multiples access() */ +const char* get_nss_state(unsigned int mark) { + if (is_bypassed(mark)) { + return "CPU"; + } + + /* DEBUG: Inicializar cache si es primera vez */ + if (cached_nss_available == -1) { + cached_nss_available = (access("/sys/kernel/debug/ecm/ecm_nss_ipv4", F_OK) == 0); + cached_sfe_available = (access("/sys/kernel/debug/ecm/ecm_sfe_ipv4", F_OK) == 0); + } + + if (cached_nss_available) { + return "HW"; + } + + if (cached_sfe_available) { + return "SFE"; + } + + return "CPU"; +} + + +/* Parsear cada linea de conntrack */ +void parse_conntrack_line(const char *line, char *proto, char *src_ip, unsigned int *src_port, char *dst_ip, unsigned int *dst_port, unsigned int *mark, char *state) { + char *src_start, *dst_start, *sport_start, *dport_start, *mark_start; + char temp_line[MAX_LINE]; + char *token; + int field_idx = 0; + + /* Valores por defecto */ + strcpy(proto, "?"); + strcpy(src_ip, "?"); + strcpy(dst_ip, "?"); + *src_port = 0; + *dst_port = 0; + *mark = 0; + strcpy(state, "?"); + + /* Extraer protocolo (tercer campo) */ + strcpy(temp_line, line); + token = strtok(temp_line, " "); + while (token && field_idx < 4) { + if (field_idx == 2) { + strncpy(proto, token, 7); + proto[7] = '\0'; + } + field_idx++; + token = strtok(NULL, " "); + } + + /* Extraer src= */ + src_start = strstr(line, "src="); + if (src_start) { + sscanf(src_start, "src=%63s", src_ip); + } + + /* Extraer dst= */ + dst_start = strstr(line, "dst="); + if (dst_start) { + sscanf(dst_start, "dst=%63s", dst_ip); + } + + /* Extraer sport= */ + sport_start = strstr(line, "sport="); + if (sport_start) { + sscanf(sport_start, "sport=%u", src_port); + } + + /* Extraer dport= */ + dport_start = strstr(line, "dport="); + if (dport_start) { + sscanf(dport_start, "dport=%u", dst_port); + } + + /* Extraer mark= */ + mark_start = strstr(line, "mark="); + if (mark_start) { + sscanf(mark_start, "mark=%u", mark); + } + + /* Estado COMO EN SHELL: num de protocolo para TCP */ + /* Esto es un error tremendo de interpretacion, que lo arrastramos desde shell, pero ...*/ + if (strcmp(proto, "tcp") == 0 || strcmp(proto, "6") == 0) { + /* La shell emite "6" para TCP, NO el estado real */ + strcpy(state, "6"); + } else if (strcmp(proto, "udp") == 0 || strcmp(proto, "17") == 0) { + strcpy(state, "stateless"); + } else { + strcpy(state, "?"); + } +} + + +/* Normalizar protocolo: convierte número a nombre (si se conoce) */ +void normalize_proto(char *proto) { + if (strcmp(proto, "0") == 0) strcpy(proto, "hopopt"); + else if (strcmp(proto, "1") == 0) strcpy(proto, "icmp"); + else if (strcmp(proto, "2") == 0) strcpy(proto, "igmp"); + else if (strcmp(proto, "3") == 0) strcpy(proto, "ggp"); + else if (strcmp(proto, "4") == 0) strcpy(proto, "ipencap"); + else if (strcmp(proto, "5") == 0) strcpy(proto, "st"); + else if (strcmp(proto, "6") == 0) strcpy(proto, "tcp"); + else if (strcmp(proto, "8") == 0) strcpy(proto, "egp"); + else if (strcmp(proto, "9") == 0) strcpy(proto, "igp"); + else if (strcmp(proto, "12") == 0) strcpy(proto, "pup"); + else if (strcmp(proto, "17") == 0) strcpy(proto, "udp"); + else if (strcmp(proto, "20") == 0) strcpy(proto, "hmp"); + else if (strcmp(proto, "22") == 0) strcpy(proto, "xns-idp"); + else if (strcmp(proto, "27") == 0) strcpy(proto, "rdp"); + else if (strcmp(proto, "29") == 0) strcpy(proto, "iso-tp4"); + else if (strcmp(proto, "33") == 0) strcpy(proto, "dccp"); + else if (strcmp(proto, "36") == 0) strcpy(proto, "xtp"); + else if (strcmp(proto, "37") == 0) strcpy(proto, "ddp"); + else if (strcmp(proto, "38") == 0) strcpy(proto, "idpr-cmtp"); + else if (strcmp(proto, "41") == 0) strcpy(proto, "ipv6"); + else if (strcmp(proto, "43") == 0) strcpy(proto, "ipv6-route"); + else if (strcmp(proto, "44") == 0) strcpy(proto, "ipv6-frag"); + else if (strcmp(proto, "45") == 0) strcpy(proto, "idrp"); + else if (strcmp(proto, "46") == 0) strcpy(proto, "rsvp"); + else if (strcmp(proto, "47") == 0) strcpy(proto, "gre"); + else if (strcmp(proto, "50") == 0) strcpy(proto, "esp"); + else if (strcmp(proto, "51") == 0) strcpy(proto, "ah"); + else if (strcmp(proto, "57") == 0) strcpy(proto, "skip"); + else if (strcmp(proto, "58") == 0) strcpy(proto, "ipv6-icmp"); + else if (strcmp(proto, "59") == 0) strcpy(proto, "ipv6-nonxt"); + else if (strcmp(proto, "60") == 0) strcpy(proto, "ipv6-opts"); + else if (strcmp(proto, "73") == 0) strcpy(proto, "rspf"); + else if (strcmp(proto, "81") == 0) strcpy(proto, "vmtp"); + else if (strcmp(proto, "88") == 0) strcpy(proto, "eigrp"); + else if (strcmp(proto, "89") == 0) strcpy(proto, "ospf"); + else if (strcmp(proto, "93") == 0) strcpy(proto, "ax.25"); + else if (strcmp(proto, "94") == 0) strcpy(proto, "ipip"); + else if (strcmp(proto, "97") == 0) strcpy(proto, "etherip"); + else if (strcmp(proto, "98") == 0) strcpy(proto, "encap"); + else if (strcmp(proto, "103") == 0) strcpy(proto, "pim"); + else if (strcmp(proto, "108") == 0) strcpy(proto, "ipcomp"); + else if (strcmp(proto, "112") == 0) strcpy(proto, "vrrp"); + else if (strcmp(proto, "115") == 0) strcpy(proto, "l2tp"); + else if (strcmp(proto, "124") == 0) strcpy(proto, "isis"); + else if (strcmp(proto, "132") == 0) strcpy(proto, "sctp"); + else if (strcmp(proto, "133") == 0) strcpy(proto, "fc"); + /* Si no está en la lista, mantener el número */ + + /* DEBUG (no PR) */ + /* En el futuro, lo ideas sería basarnos en /etc/protocols ... */ +} + + + + + + + +/* MAIN */ +int main(int argc, char *argv[]) { + FILE *fp; + char line[MAX_LINE]; + char proto[16]; + char src_ip[64], dst_ip[64]; + unsigned int src_port, dst_port; + unsigned int mark; + char state[32]; + int num = 0; + char src_comp[64], dst_comp[64]; + char src_display[128], dst_display[128]; + + + /* DEBUG PR-1 NO hardcoded values, uci get instead */ + NSS_MARK_GLOBAL = get_nss_mark_from_uci(); + + fp = fopen(CONNTRACK_FILE, "r"); + if (!fp) { + fprintf(stderr, "ERROR: Cannot open %s\n", CONNTRACK_FILE); + return 1; + } + + while (fgets(line, sizeof(line), fp)) { + parse_conntrack_line(line, proto, src_ip, &src_port, dst_ip, &dst_port, &mark, state); + + /* Saltar líneas mal formadas */ + if (strcmp(src_ip, "?") == 0 || strcmp(dst_ip, "?") == 0) { + continue; + } + + num++; + + /* Obtener interfaz */ + char *iface = get_iface_for_ip(src_ip); + + /* Estado NSS y bypass */ + const char *nss_state = get_nss_state(mark); + const char *bypass = is_bypassed(mark) ? "YES" : "NO"; + + /* Comprimir IPv6 si es necesario */ + if (strchr(src_ip, ':')) { + compress_ipv6(src_comp, src_ip); + snprintf(src_display, sizeof(src_display), "%s#%u", src_comp, src_port); + } else { + snprintf(src_display, sizeof(src_display), "%s#%u", src_ip, src_port); + } + + if (strchr(dst_ip, ':')) { + compress_ipv6(dst_comp, dst_ip); + snprintf(dst_display, sizeof(dst_display), "%s#%u", dst_comp, dst_port); + } else { + snprintf(dst_display, sizeof(dst_display), "%s#%u", dst_ip, dst_port); + } + + /* Normalizar protocolo para mostrar (UI) y para reglas */ + normalize_proto(proto); + + /* Output con el mismo formato pipes que shell */ + printf("%d|%s|%s|%s|%s|%s|%s|%u|%s\n", num, proto, src_display, dst_display, iface, nss_state, bypass, mark, state); + } + + fclose(fp); + return 0; +}